diff options
author | wolfbeast <mcwerewolf@gmail.com> | 2018-06-04 15:50:03 +0200 |
---|---|---|
committer | wolfbeast <mcwerewolf@gmail.com> | 2018-06-04 15:50:03 +0200 |
commit | e3b7744bee37c3d4a026d2193bed5e9439c40ff3 (patch) | |
tree | f3f7b07ca9bd78bf7ac2d76dd55b61b2a8bb549e /application/basilisk/base/content | |
parent | cbce4f0b6a337f8250b62cae028f1c6d4cce51df (diff) | |
parent | 031afcafe288bf0f46c0c5caae20dd3db8bd0297 (diff) | |
download | UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.tar UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.tar.gz UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.tar.lz UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.tar.xz UXP-e3b7744bee37c3d4a026d2193bed5e9439c40ff3.zip |
Merge branch 'move-basilisk'
Diffstat (limited to 'application/basilisk/base/content')
183 files changed, 58042 insertions, 0 deletions
diff --git a/application/basilisk/base/content/aboutDialog-appUpdater.js b/application/basilisk/base/content/aboutDialog-appUpdater.js new file mode 100644 index 000000000..4b4fc6618 --- /dev/null +++ b/application/basilisk/base/content/aboutDialog-appUpdater.js @@ -0,0 +1,428 @@ +/* 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/. */ + +// Note: this file is included in aboutDialog.xul if MOZ_UPDATER is defined. + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils", + "resource://gre/modules/UpdateUtils.jsm"); + +const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx"; +const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never"; + +var gAppUpdater; + +function onUnload(aEvent) { + if (gAppUpdater.isChecking) + gAppUpdater.checker.stopChecking(Components.interfaces.nsIUpdateChecker.CURRENT_CHECK); + // Safe to call even when there isn't a download in progress. + gAppUpdater.removeDownloadListener(); + gAppUpdater = null; +} + + +function appUpdater() +{ + XPCOMUtils.defineLazyServiceGetter(this, "aus", + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService"); + XPCOMUtils.defineLazyServiceGetter(this, "checker", + "@mozilla.org/updates/update-checker;1", + "nsIUpdateChecker"); + XPCOMUtils.defineLazyServiceGetter(this, "um", + "@mozilla.org/updates/update-manager;1", + "nsIUpdateManager"); + + this.updateDeck = document.getElementById("updateDeck"); + + // Hide the update deck when the update window is already open and it's not + // already applied, to avoid syncing issues between them. Applied updates + // don't have any information to sync between the windows as they both just + // show the "Restart to continue"-type button. + if (Services.wm.getMostRecentWindow("Update:Wizard") && + !this.isApplied) { + this.updateDeck.hidden = true; + return; + } + + this.bundle = Services.strings. + createBundle("chrome://browser/locale/browser.properties"); + + let manualURL = Services.urlFormatter.formatURLPref("app.update.url.manual"); + let manualLink = document.getElementById("manualLink"); + manualLink.value = manualURL; + manualLink.href = manualURL; + document.getElementById("failedLink").href = manualURL; + + if (this.updateDisabledAndLocked) { + this.selectPanel("adminDisabled"); + return; + } + + if (this.isPending || this.isApplied) { + this.selectPanel("apply"); + return; + } + + if (this.aus.isOtherInstanceHandlingUpdates) { + this.selectPanel("otherInstanceHandlingUpdates"); + return; + } + + if (this.isDownloading) { + this.startDownload(); + // selectPanel("downloading") is called from setupDownloadingUI(). + return; + } + + // Honor the "Never check for updates" option by not only disabling background + // update checks, but also in the About dialog, by presenting a + // "Check for updates" button. + // If updates are found, the user is then asked if he wants to "Update to <version>". + if (!this.updateEnabled || + Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + this.selectPanel("checkForUpdates"); + return; + } + + // That leaves the options + // "Check for updates, but let me choose whether to install them", and + // "Automatically install updates". + // In both cases, we check for updates without asking. + // In the "let me choose" case, we ask before downloading though, in onCheckComplete. + this.checkForUpdates(); +} + +appUpdater.prototype = +{ + // true when there is an update check in progress. + isChecking: false, + + // true when there is an update already staged / ready to be applied. + get isPending() { + if (this.update) { + return this.update.state == "pending" || + this.update.state == "pending-service" || + this.update.state == "pending-elevate"; + } + return this.um.activeUpdate && + (this.um.activeUpdate.state == "pending" || + this.um.activeUpdate.state == "pending-service" || + this.um.activeUpdate.state == "pending-elevate"); + }, + + // true when there is an update already installed in the background. + get isApplied() { + if (this.update) + return this.update.state == "applied" || + this.update.state == "applied-service"; + return this.um.activeUpdate && + (this.um.activeUpdate.state == "applied" || + this.um.activeUpdate.state == "applied-service"); + }, + + // true when there is an update download in progress. + get isDownloading() { + if (this.update) + return this.update.state == "downloading"; + return this.um.activeUpdate && + this.um.activeUpdate.state == "downloading"; + }, + + // true when updating is disabled by an administrator. + get updateDisabledAndLocked() { + return !this.updateEnabled && + Services.prefs.prefIsLocked("app.update.enabled"); + }, + + // true when updating is enabled. + get updateEnabled() { + try { + return Services.prefs.getBoolPref("app.update.enabled"); + } + catch (e) { } + return true; // Firefox default is true + }, + + // true when updating in background is enabled. + get backgroundUpdateEnabled() { + return this.updateEnabled && + gAppUpdater.aus.canStageUpdates; + }, + + // true when updating is automatic. + get updateAuto() { + try { + return Services.prefs.getBoolPref("app.update.auto"); + } + catch (e) { } + return true; // Firefox default is true + }, + + /** + * Sets the panel of the updateDeck. + * + * @param aChildID + * The id of the deck's child to select, e.g. "apply". + */ + selectPanel: function(aChildID) { + let panel = document.getElementById(aChildID); + + let button = panel.querySelector("button"); + if (button) { + if (aChildID == "downloadAndInstall") { + let updateVersion = gAppUpdater.update.displayVersion; + button.label = this.bundle.formatStringFromName("update.downloadAndInstallButton.label", [updateVersion], 1); + button.accessKey = this.bundle.GetStringFromName("update.downloadAndInstallButton.accesskey"); + } + this.updateDeck.selectedPanel = panel; + if (!document.commandDispatcher.focusedElement || // don't steal the focus + document.commandDispatcher.focusedElement.localName == "button") // except from the other buttons + button.focus(); + + } else { + this.updateDeck.selectedPanel = panel; + } + }, + + /** + * Check for updates + */ + checkForUpdates: function() { + // Clear prefs that could prevent a user from discovering available updates. + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX); + } + if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) { + Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER); + } + this.selectPanel("checkingForUpdates"); + this.isChecking = true; + this.checker.checkForUpdates(this.updateCheckListener, true); + // after checking, onCheckComplete() is called + }, + + /** + * Handles oncommand for the "Restart to Update" button + * which is presented after the download has been downloaded. + */ + buttonRestartAfterDownload: function() { + if (!this.isPending && !this.isApplied) { + return; + } + + gAppUpdater.selectPanel("restarting"); + + // Notify all windows that an application quit has been requested. + let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]. + createInstance(Components.interfaces.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + // Something aborted the quit process. + if (cancelQuit.data) { + gAppUpdater.selectPanel("apply"); + return; + } + + let appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]. + getService(Components.interfaces.nsIAppStartup); + + // If already in safe mode restart in safe mode (bug 327119) + if (Services.appinfo.inSafeMode) { + appStartup.restartInSafeMode(Components.interfaces.nsIAppStartup.eAttemptQuit); + return; + } + + appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit | + Components.interfaces.nsIAppStartup.eRestart); + }, + + /** + * Implements nsIUpdateCheckListener. The methods implemented by + * nsIUpdateCheckListener are in a different scope from nsIIncrementalDownload + * to make it clear which are used by each interface. + */ + updateCheckListener: { + /** + * See nsIUpdateService.idl + */ + onCheckComplete: function(aRequest, aUpdates, aUpdateCount) { + gAppUpdater.isChecking = false; + gAppUpdater.update = gAppUpdater.aus. + selectUpdate(aUpdates, aUpdates.length); + if (!gAppUpdater.update) { + gAppUpdater.selectPanel("noUpdatesFound"); + return; + } + + if (gAppUpdater.update.unsupported) { + if (gAppUpdater.update.detailsURL) { + let unsupportedLink = document.getElementById("unsupportedLink"); + unsupportedLink.href = gAppUpdater.update.detailsURL; + } + gAppUpdater.selectPanel("unsupportedSystem"); + return; + } + + if (!gAppUpdater.aus.canApplyUpdates) { + gAppUpdater.selectPanel("manualUpdate"); + return; + } + + if (gAppUpdater.updateAuto) // automatically download and install + gAppUpdater.startDownload(); + else // ask + gAppUpdater.selectPanel("downloadAndInstall"); + }, + + /** + * See nsIUpdateService.idl + */ + onError: function(aRequest, aUpdate) { + // Errors in the update check are treated as no updates found. If the + // update check fails repeatedly without a success the user will be + // notified with the normal app update user interface so this is safe. + gAppUpdater.isChecking = false; + gAppUpdater.selectPanel("noUpdatesFound"); + }, + + /** + * See nsISupports.idl + */ + QueryInterface: function(aIID) { + if (!aIID.equals(Components.interfaces.nsIUpdateCheckListener) && + !aIID.equals(Components.interfaces.nsISupports)) + throw Components.results.NS_ERROR_NO_INTERFACE; + return this; + } + }, + + /** + * Starts the download of an update mar. + */ + startDownload: function() { + if (!this.update) + this.update = this.um.activeUpdate; + this.update.QueryInterface(Components.interfaces.nsIWritablePropertyBag); + this.update.setProperty("foregroundDownload", "true"); + + this.aus.pauseDownload(); + let state = this.aus.downloadUpdate(this.update, false); + if (state == "failed") { + this.selectPanel("downloadFailed"); + return; + } + + this.setupDownloadingUI(); + }, + + /** + * Switches to the UI responsible for tracking the download. + */ + setupDownloadingUI: function() { + this.downloadStatus = document.getElementById("downloadStatus"); + this.downloadStatus.value = + DownloadUtils.getTransferTotal(0, this.update.selectedPatch.size); + this.selectPanel("downloading"); + this.aus.addDownloadListener(this); + }, + + removeDownloadListener: function() { + if (this.aus) { + this.aus.removeDownloadListener(this); + } + }, + + /** + * See nsIRequestObserver.idl + */ + onStartRequest: function(aRequest, aContext) { + }, + + /** + * See nsIRequestObserver.idl + */ + onStopRequest: function(aRequest, aContext, aStatusCode) { + switch (aStatusCode) { + case Components.results.NS_ERROR_UNEXPECTED: + if (this.update.selectedPatch.state == "download-failed" && + (this.update.isCompleteUpdate || this.update.patchCount != 2)) { + // Verification error of complete patch, informational text is held in + // the update object. + this.removeDownloadListener(); + this.selectPanel("downloadFailed"); + break; + } + // Verification failed for a partial patch, complete patch is now + // downloading so return early and do NOT remove the download listener! + break; + case Components.results.NS_BINDING_ABORTED: + // Do not remove UI listener since the user may resume downloading again. + break; + case Components.results.NS_OK: + this.removeDownloadListener(); + if (this.backgroundUpdateEnabled) { + this.selectPanel("applying"); + let self = this; + Services.obs.addObserver(function (aSubject, aTopic, aData) { + // Update the UI when the background updater is finished + let status = aData; + if (status == "applied" || status == "applied-service" || + status == "pending" || status == "pending-service" || + status == "pending-elevate") { + // If the update is successfully applied, or if the updater has + // fallen back to non-staged updates, show the "Restart to Update" + // button. + self.selectPanel("apply"); + } else if (status == "failed") { + // Background update has failed, let's show the UI responsible for + // prompting the user to update manually. + self.selectPanel("downloadFailed"); + } else if (status == "downloading") { + // We've fallen back to downloading the full update because the + // partial update failed to get staged in the background. + // Therefore we need to keep our observer. + self.setupDownloadingUI(); + return; + } + Services.obs.removeObserver(arguments.callee, "update-staged"); + }, "update-staged", false); + } else { + this.selectPanel("apply"); + } + break; + default: + this.removeDownloadListener(); + this.selectPanel("downloadFailed"); + break; + } + }, + + /** + * See nsIProgressEventSink.idl + */ + onStatus: function(aRequest, aContext, aStatus, aStatusArg) { + }, + + /** + * See nsIProgressEventSink.idl + */ + onProgress: function(aRequest, aContext, aProgress, aProgressMax) { + this.downloadStatus.value = + DownloadUtils.getTransferTotal(aProgress, aProgressMax); + }, + + /** + * See nsISupports.idl + */ + QueryInterface: function(aIID) { + if (!aIID.equals(Components.interfaces.nsIProgressEventSink) && + !aIID.equals(Components.interfaces.nsIRequestObserver) && + !aIID.equals(Components.interfaces.nsISupports)) + throw Components.results.NS_ERROR_NO_INTERFACE; + return this; + } +}; diff --git a/application/basilisk/base/content/aboutDialog.css b/application/basilisk/base/content/aboutDialog.css new file mode 100644 index 000000000..65830c8bb --- /dev/null +++ b/application/basilisk/base/content/aboutDialog.css @@ -0,0 +1,97 @@ +/* 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/. */ + +#aboutDialog { + width: 620px; + /* Set an explicit line-height to avoid discrepancies in 'auto' spacing + across screens with different device DPI, which may cause font metrics + to round differently. */ + line-height: 1.5; +} + +#rightBox { + background-image: url("chrome://branding/content/about-wordmark.png"); + background-repeat: no-repeat; + /* padding-top creates room for the wordmark */ + padding-top: 38px; + margin-top:20px; +} + +#rightBox:-moz-locale-dir(rtl) { + background-position: 100% 0; +} + +#bottomBox { + padding: 15px 10px 0; +} + +#version { + font-weight: bold; + margin-top: 10px; + margin-left: 0; + -moz-user-select: text; + -moz-user-focus: normal; + cursor: text; +} + +#version:-moz-locale-dir(rtl) { + direction: ltr; + text-align: right; + margin-left: 5px; + margin-right: 0; +} + +#releasenotes { + margin-top: 10px; +} + +#distribution, +#distributionId { + display: none; + margin-top: 0; + margin-bottom: 0; +} + +.text-blurb { + margin-bottom: 10px; + margin-inline-start: 0; + padding-inline-start: 0; +} + +#updateButton, +#updateDeck > hbox > label { + margin-inline-start: 0; + padding-inline-start: 0; +} + +.update-throbber { + width: 16px; + min-height: 16px; + margin-inline-end: 3px; + list-style-image: url("chrome://global/skin/icons/loading.png"); +} + +@media (min-resolution: 1.1dppx) { + .update-throbber { + list-style-image: url("chrome://global/skin/icons/loading@2x.png"); + } +} + +description > .text-link, +description > .text-link:focus { + margin: 0px; + padding: 0px; +} + +.bottom-link, +.bottom-link:focus { + text-align: center; + margin: 0 40px; +} + +#currentChannel { + margin: 0; + padding: 0; + font-weight: bold; +} diff --git a/application/basilisk/base/content/aboutDialog.js b/application/basilisk/base/content/aboutDialog.js new file mode 100644 index 000000000..f9571621f --- /dev/null +++ b/application/basilisk/base/content/aboutDialog.js @@ -0,0 +1,91 @@ +/* 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/. */ + +"use strict"; + +// Services = object with smart getters for common XPCOM services +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/AppConstants.jsm"); + +function init(aEvent) +{ + if (aEvent.target != document) + return; + + try { + var distroId = Services.prefs.getCharPref("distribution.id"); + if (distroId) { + var distroVersion = Services.prefs.getCharPref("distribution.version"); + + var distroIdField = document.getElementById("distributionId"); + distroIdField.value = distroId + " - " + distroVersion; + distroIdField.style.display = "block"; + + try { + // This is in its own try catch due to bug 895473 and bug 900925. + var distroAbout = Services.prefs.getComplexValue("distribution.about", + Components.interfaces.nsISupportsString); + var distroField = document.getElementById("distribution"); + distroField.value = distroAbout; + distroField.style.display = "block"; + } + catch (ex) { + // Pref is unset + Components.utils.reportError(ex); + } + } + } + catch (e) { + // Pref is unset + } + + // Include the build ID + let versionField = document.getElementById("version"); + let version = Services.appinfo.version; + let buildID = Services.appinfo.appBuildID; + let year = buildID.slice(0, 4); + let month = buildID.slice(4, 6); + let day = buildID.slice(6, 8); + let hour = buildID.slice(8, 10); + let minute = buildID.slice(10, 12); + if (Services.prefs.getBoolPref("general.useragent.appVersionIsBuildID")) { + versionField.textContent = `${year}.${month}.${day}`; + } else { + versionField.textContent = `v` + version + ` (${year}-${month}-${day})`; + } + + // Display warning if this is an "a#" (nightly or aurora) build + if (/a\d+$/.test(version)) { + document.getElementById("experimental").hidden = false; + document.getElementById("communityDesc").hidden = true; + } + + // Append "(32-bit)" or "(64-bit)" build architecture to the version number: + let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); + let archResource = Services.appinfo.is64Bit + ? "aboutDialog.architecture.sixtyFourBit" + : "aboutDialog.architecture.thirtyTwoBit"; + let arch = bundle.GetStringFromName(archResource); + versionField.textContent += ` (${arch})`; + + // Get Release Notes URL from Preferences + let releaseNotesURL = Services.prefs.getCharPref("app.releaseNotesURL"); + document.getElementById("releasenotes").setAttribute("href", releaseNotesURL); + + if (AppConstants.MOZ_UPDATER) { + gAppUpdater = new appUpdater(); + + let channelLabel = document.getElementById("currentChannel"); + let currentChannelText = document.getElementById("currentChannelText"); + channelLabel.value = UpdateUtils.UpdateChannel; + if (/^release($|\-)/.test(channelLabel.value)) + currentChannelText.hidden = true; + } + + if (AppConstants.platform == "macosx") { + // it may not be sized at this point, and we need its width to calculate its position + window.sizeToContent(); + window.moveTo((screen.availWidth / 2) - (window.outerWidth / 2), screen.availHeight / 5); + } +} diff --git a/application/basilisk/base/content/aboutDialog.xul b/application/basilisk/base/content/aboutDialog.xul new file mode 100644 index 000000000..5780e5ec1 --- /dev/null +++ b/application/basilisk/base/content/aboutDialog.xul @@ -0,0 +1,157 @@ +<?xml version="1.0"?> <!-- -*- Mode: HTML -*- --> + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/aboutDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://branding/content/aboutDialog.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % aboutDialogDTD SYSTEM "chrome://browser/locale/aboutDialog.dtd" > +%aboutDialogDTD; +]> + +#ifdef XP_MACOSX +<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?> +#endif + +<window xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="aboutDialog" + windowtype="Browser:About" + onload="init(event);" +#ifdef MOZ_UPDATER + onunload="onUnload(event);" +#endif +#ifdef XP_MACOSX + inwindowmenu="false" +#else + title="&aboutDialog.title;" +#endif + role="dialog" + aria-describedby="version distribution distributionId communityDesc contributeDesc trademark" + > + + <script type="application/javascript" src="chrome://browser/content/aboutDialog.js"/> +#ifdef MOZ_UPDATER + <script type="application/javascript" src="chrome://browser/content/aboutDialog-appUpdater.js"/> +#endif + <vbox id="aboutDialogContainer"> + <hbox id="clientBox"> + <vbox id="leftBox" flex="1"/> + <vbox id="rightBox" flex="1"> + <hbox align="baseline"> +#expand <label id="version"></label> +#ifndef NIGHTLY_BUILD + <label id="releasenotes" class="text-link">&releaseNotes.link;</label> +#endif + </hbox> + + <label id="distribution" class="text-blurb"/> + <label id="distributionId" class="text-blurb"/> + + <vbox id="detailsBox"> + <vbox id="updateBox"> +#ifdef MOZ_UPDATER + <deck id="updateDeck" orient="vertical"> + <hbox id="checkForUpdates" align="center"> + <button id="checkForUpdatesButton" align="start" + label="&update.checkForUpdatesButton.label;" + accesskey="&update.checkForUpdatesButton.accesskey;" + oncommand="gAppUpdater.checkForUpdates();"/> + <spacer flex="1"/> + </hbox> + <hbox id="downloadAndInstall" align="center"> + <button id="downloadAndInstallButton" align="start" + oncommand="gAppUpdater.startDownload();"/> + <!-- label and accesskey will be filled by JS --> + <spacer flex="1"/> + </hbox> + <hbox id="apply" align="center"> + <button id="updateButton" align="start" + label="&update.updateButton.label2;" + accesskey="&update.updateButton.accesskey;" + oncommand="gAppUpdater.buttonRestartAfterDownload();"/> + <spacer flex="1"/> + </hbox> + <hbox id="checkingForUpdates" align="center"> + <image class="update-throbber"/><label>&update.checkingForUpdates;</label> + </hbox> + <hbox id="downloading" align="center"> + <image class="update-throbber"/><label>&update.downloading.start;</label><label id="downloadStatus"/><label>&update.downloading.end;</label> + </hbox> + <hbox id="applying" align="center"> + <image class="update-throbber"/><label>&update.applying;</label> + </hbox> + <hbox id="downloadFailed" align="center"> + <label>&update.failed.start;</label><label id="failedLink" class="text-link">&update.failed.linkText;</label><label>&update.failed.end;</label> + </hbox> + <hbox id="adminDisabled" align="center"> + <label>&update.adminDisabled;</label> + </hbox> + <hbox id="noUpdatesFound" align="center"> + <label>&update.noUpdatesFound;</label> + </hbox> + <hbox id="otherInstanceHandlingUpdates" align="center"> + <label>&update.otherInstanceHandlingUpdates;</label> + </hbox> + <hbox id="manualUpdate" align="center"> + <label>&update.manual.start;</label><label id="manualLink" class="text-link"/><label>&update.manual.end;</label> + </hbox> + <hbox id="unsupportedSystem" align="center"> + <label>&update.unsupported.start;</label><label id="unsupportedLink" class="text-link">&update.unsupported.linkText;</label><label>&update.unsupported.end;</label> + </hbox> + <hbox id="restarting" align="center"> + <label>&update.restarting;</label> + </hbox> + </deck> +#endif + </vbox> + +#ifdef MOZ_UPDATER + <description class="text-blurb" id="currentChannelText"> + &channel.description.start;<label id="currentChannel"/>&channel.description.end; + </description> +#endif + <vbox id="experimental" hidden="true"> + <description class="text-blurb" id="warningDesc"> + &warningDesc.version; + </description> + </vbox> +#ifdef MC_PRIVATE_BUILD +<description class="text-blurb" id="communityDesc"> + This is a private build of Basilisk. If you did not manually build this copy from source yourself, then please download an official version from the <label class="text-link" href="http://www.basilisk-browser.org/">Basilisk website</label>. + </description> +#else + <description class="text-blurb" id="communityDesc"> + Basilisk is community software released by <label class="text-link" href="http://www.palemoon.org/">the Pale Moon team</label> and Mozilla developers. Learn <label class="text-link" useoriginprincipal="true" href="about:credits">who contributed</label> to this software. + </description> + <description class="text-blurb" id="contributeDesc"> + Want to help? Please consider <label class="text-link" href="https://www.palemoon.org/donations.shtml">donating</label> or get involved with our <label class="text-link" href="https://github.com/MoonchildProductions/UXP">development</label> of the Unified XUL Platform. + </description> +#endif + </vbox> + </vbox> + </hbox> + <vbox id="bottomBox"> + <hbox pack="center"> + <label class="text-link bottom-link" useoriginprincipal="true" href="about:license">&bottomLinks.license;</label> + <label class="text-link bottom-link" useoriginprincipal="true" href="about:rights">&bottomLinks.rights;</label> + <label class="text-link bottom-link" href="https://www.palemoon.org/privacy.shtml">&bottomLinks.privacy;</label> + </hbox> + <description id="trademark">&trademarkInfo.part1;</description> + </vbox> + </vbox> + + <keyset> + <key keycode="VK_ESCAPE" oncommand="window.close();"/> + </keyset> + +#ifdef XP_MACOSX +#include browserMountPoints.inc +#endif +</window> diff --git a/application/basilisk/base/content/aboutNetError.xhtml b/application/basilisk/base/content/aboutNetError.xhtml new file mode 100644 index 000000000..f2de106c2 --- /dev/null +++ b/application/basilisk/base/content/aboutNetError.xhtml @@ -0,0 +1,699 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % netErrorDTD + SYSTEM "chrome://global/locale/netError.dtd"> + %netErrorDTD; + <!ENTITY % globalDTD + SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; +]> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&loadError.label;</title> + <link rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" /> + <!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in + toolkit/components/places/src/nsFaviconService.h should be updated. --> + <link rel="icon" type="image/png" id="favicon" href="chrome://global/skin/icons/warning-16.png"/> + + <script type="application/javascript"><![CDATA[ + // The following parameters are parsed from the error URL: + // e - the error code + // s - custom CSS class to allow alternate styling/favicons + // d - error description + // captive - "true" to indicate we're behind a captive portal. + // Any other value is ignored. + + // Note that this file uses document.documentURI to get + // the URL (with the format from above). This is because + // document.location.href gets the current URI off the docshell, + // which is the URL displayed in the location bar, i.e. + // the URI that the user attempted to load. + + let searchParams = new URLSearchParams(document.documentURI.split("?")[1]); + + // Set to true on init if the error code is nssBadCert. + let gIsCertError; + + function getErrorCode() + { + return searchParams.get("e"); + } + + function getCSSClass() + { + return searchParams.get("s"); + } + + function getDescription() + { + return searchParams.get("d"); + } + + function isCaptive() { + return searchParams.get("captive") == "true"; + } + + function retryThis(buttonEl) + { + // Note: The application may wish to handle switching off "offline mode" + // before this event handler runs, but using a capturing event handler. + + // Session history has the URL of the page that failed + // to load, not the one of the error page. So, just call + // reload(), which will also repost POST data correctly. + try { + location.reload(); + } catch (e) { + // We probably tried to reload a URI that caused an exception to + // occur; e.g. a nonexistent file. + } + + buttonEl.disabled = true; + } + + function doOverride(buttonEl) { + var event = new CustomEvent("AboutNetErrorOverride", {bubbles:true}); + document.dispatchEvent(event); + retryThis(buttonEl); + } + + function toggleDisplay(node) { + const toggle = { + "": "block", + "none": "block", + "block": "none" + }; + return (node.style.display = toggle[node.style.display]); + } + + function showCertificateErrorReporting() { + // Display error reporting UI + document.getElementById("certificateErrorReporting").style.display = "block"; + } + + function showPrefChangeContainer() { + const panel = document.getElementById("prefChangeContainer"); + panel.style.display = "block"; + document.getElementById("netErrorButtonContainer").style.display = "none"; + document.getElementById("prefResetButton").addEventListener("click", function resetPreferences(e) { + const event = new CustomEvent("AboutNetErrorResetPreferences", {bubbles:true}); + document.dispatchEvent(event); + }); + addAutofocus("prefResetButton", "beforeend"); + } + + function setupAdvancedButton(allowOverride) { + // Get the hostname and add it to the panel + var panelId = gIsCertError ? "badCertAdvancedPanel" : "weakCryptoAdvancedPanel"; + var panel = document.getElementById(panelId); + for (var span of panel.querySelectorAll("span.hostname")) { + span.textContent = document.location.hostname; + } + if (!gIsCertError) { + panel.replaceChild(document.getElementById("errorLongDesc"), + document.getElementById("advancedLongDesc")); + } + + // Register click handler for the weakCryptoAdvancedPanel + document.getElementById("advancedButton") + .addEventListener("click", function togglePanelVisibility() { + toggleDisplay(panel); + if (gIsCertError) { + // Toggling the advanced panel must ensure that the debugging + // information panel is hidden as well, since it's opened by the + // error code link in the advanced panel. + var div = document.getElementById("certificateErrorDebugInformation"); + div.style.display = "none"; + } + + if (panel.style.display == "block") { + // send event to trigger telemetry ping + var event = new CustomEvent("AboutNetErrorUIExpanded", {bubbles:true}); + document.dispatchEvent(event); + } + }); + + if (allowOverride) { + document.getElementById("overrideWeakCryptoPanel").style.display = "flex"; + var overrideLink = document.getElementById("overrideWeakCrypto"); + overrideLink.addEventListener("click", () => doOverride(overrideLink), false); + } + if (!gIsCertError) { + return; + } + + if (getCSSClass() == "expertBadCert") { + toggleDisplay(document.getElementById("badCertAdvancedPanel")); + // Toggling the advanced panel must ensure that the debugging + // information panel is hidden as well, since it's opened by the + // error code link in the advanced panel. + var div = document.getElementById("certificateErrorDebugInformation"); + div.style.display = "none"; + } + + disallowCertOverridesIfNeeded(); + + document.getElementById("badCertTechnicalInfo").textContent = getDescription(); + } + + function disallowCertOverridesIfNeeded() { + var cssClass = getCSSClass(); + // Disallow overrides if this is a Strict-Transport-Security + // host and the cert is bad (STS Spec section 7.3) or if the + // certerror is in a frame (bug 633691). + if (cssClass == "badStsCert" || window != top) { + document.getElementById("exceptionDialogButton").setAttribute("hidden", "true"); + } + if (cssClass == "badStsCert") { + document.getElementById("badStsCertExplanation").removeAttribute("hidden"); + } + } + + function initPage() + { + var err = getErrorCode(); + gIsCertError = (err == "nssBadCert"); + // Only worry about captive portals if this is a cert error. + let showCaptivePortalUI = isCaptive() && gIsCertError; + if (showCaptivePortalUI) { + err = "captivePortal"; + } + + // if it's an unknown error or there's no title or description + // defined, get the generic message + var errTitle = document.getElementById("et_" + err); + var errDesc = document.getElementById("ed_" + err); + if (!errTitle || !errDesc) + { + errTitle = document.getElementById("et_generic"); + errDesc = document.getElementById("ed_generic"); + } + + document.querySelector(".title-text").innerHTML = errTitle.innerHTML; + + var sd = document.getElementById("errorShortDescText"); + if (sd) { + if (gIsCertError) { + sd.innerHTML = errDesc.innerHTML; + } + else { + sd.textContent = getDescription(); + } + } + if (showCaptivePortalUI) { + initPageCaptivePortal(); + return; + } + if (gIsCertError) { + initPageCertError(); + return; + } + addAutofocus("errorTryAgain"); + + document.body.className = "neterror"; + + var ld = document.getElementById("errorLongDesc"); + if (ld) + { + ld.innerHTML = errDesc.innerHTML; + } + + if (err == "sslv3Used") { + document.getElementById("learnMoreContainer").style.display = "block"; + var learnMoreLink = document.getElementById("learnMoreLink"); + learnMoreLink.href = "https://support.mozilla.org/kb/how-resolve-sslv3-error-messages-firefox"; + document.body.className = "certerror"; + } + + if (err == "weakCryptoUsed") { + document.body.className = "certerror"; + } + + // remove undisplayed errors to avoid bug 39098 + var errContainer = document.getElementById("errorContainer"); + errContainer.parentNode.removeChild(errContainer); + + var className = getCSSClass(); + if (className && className != "expertBadCert") { + // Associate a CSS class with the root of the page, if one was passed in, + // to allow custom styling. + // Not "expertBadCert" though, don't want to deal with the favicon + document.documentElement.className = className; + + // Also, if they specified a CSS class, they must supply their own + // favicon. In order to trigger the browser to repaint though, we + // need to remove/add the link element. + var favicon = document.getElementById("favicon"); + var faviconParent = favicon.parentNode; + faviconParent.removeChild(favicon); + favicon.setAttribute("href", "chrome://global/skin/icons/" + className + "_favicon.png"); + faviconParent.appendChild(favicon); + } + + if (err == "remoteXUL") { + // Remove the "Try again" button for remote XUL errors given that + // it is useless. + document.getElementById("netErrorButtonContainer").style.display = "none"; + } + + if (err == "cspBlocked") { + // Remove the "Try again" button for CSP violations, since it's + // almost certainly useless. (Bug 553180) + document.getElementById("netErrorButtonContainer").style.display = "none"; + } + + window.addEventListener("AboutNetErrorOptions", function(evt) { + // Pinning errors are of type nssFailure2 + if (getErrorCode() == "nssFailure2" || getErrorCode() == "weakCryptoUsed") { + document.getElementById("learnMoreContainer").style.display = "block"; + var learnMoreLink = document.getElementById("learnMoreLink"); + // nssFailure2 also gets us other non-overrideable errors. Choose + // a "learn more" link based on description: + if (getDescription().includes("mozilla_pkix_error_key_pinning_failure")) { + learnMoreLink.href = "https://support.mozilla.org/kb/certificate-pinning-reports"; + } + if (getErrorCode() == "weakCryptoUsed") { + learnMoreLink.href = "https://support.mozilla.org/kb/how-resolve-weak-crypto-error-messages-firefox"; + } + + var options = JSON.parse(evt.detail); + if (options && options.enabled) { + var checkbox = document.getElementById("automaticallyReportInFuture"); + showCertificateErrorReporting(); + if (options.automatic) { + // set the checkbox + checkbox.checked = true; + } + + checkbox.addEventListener("change", function(evt) { + var event = new CustomEvent("AboutNetErrorSetAutomatic", + {bubbles:true, detail:evt.target.checked}); + document.dispatchEvent(event); + }, false); + } + const hasPrefStyleError = [ + "interrupted", // This happens with subresources that are above the max tls + "SSL_ERROR_PROTOCOL_VERSION_ALERT", + "SSL_ERROR_UNSUPPORTED_VERSION", + "SSL_ERROR_NO_CYPHER_OVERLAP", + "SSL_ERROR_NO_CIPHERS_SUPPORTED" + ].some((substring) => getDescription().includes(substring)); + // If it looks like an error that is user config based + if (getErrorCode() == "nssFailure2" && hasPrefStyleError && options && options.changedCertPrefs) { + showPrefChangeContainer(); + } + } + if (getErrorCode() == "weakCryptoUsed" || getErrorCode() == "sslv3Used") { + setupAdvancedButton(getErrorCode() == "weakCryptoUsed"); + } + }.bind(this), true, true); + + var event = new CustomEvent("AboutNetErrorLoad", {bubbles:true}); + document.dispatchEvent(event); + + if (err == "inadequateSecurityError") { + // Remove the "Try again" button for HTTP/2 inadequate security as it + // is useless. + document.getElementById("errorTryAgain").style.display = "none"; + + var container = document.getElementById("errorLongDesc"); + for (var span of container.querySelectorAll("span.hostname")) { + span.textContent = document.location.hostname; + } + } + + addDomainErrorLinks(); + } + + function initPageCaptivePortal() + { + document.body.className = "captiveportal"; + document.title = document.getElementById("captivePortalPageTitle").textContent; + + document.getElementById("openPortalLoginPageButton") + .addEventListener("click", () => { + let event = new CustomEvent("AboutNetErrorOpenCaptivePortal", {bubbles:true}); + document.dispatchEvent(event); + }); + + addAutofocus("openPortalLoginPageButton"); + setupAdvancedButton(true); + + addDomainErrorLinks(); + + // When the portal is freed, an event is generated by the frame script + // that we can pick up and attempt to reload the original page. + window.addEventListener("AboutNetErrorCaptivePortalFreed", () => { + document.location.reload(); + }); + } + + function initPageCertError() { + document.body.className = "certerror"; + document.title = document.getElementById("certErrorPageTitle").textContent; + for (let host of document.querySelectorAll(".hostname")) { + host.textContent = document.location.hostname; + } + + addAutofocus("returnButton"); + setupAdvancedButton(true); + + document.getElementById("learnMoreContainer").style.display = "block"; + + let checkbox = document.getElementById("automaticallyReportInFuture"); + checkbox.addEventListener("change", function({target: {checked}}) { + document.dispatchEvent(new CustomEvent("AboutNetErrorSetAutomatic", { + detail: checked, + bubbles: true + })); + }); + + addEventListener("AboutNetErrorOptions", function(event) { + var options = JSON.parse(event.detail); + if (options && options.enabled) { + // Display error reporting UI + document.getElementById("certificateErrorReporting").style.display = "block"; + + // set the checkbox + checkbox.checked = !!options.automatic; + } + }, true, true); + + let event = new CustomEvent("AboutNetErrorLoad", {bubbles:true}); + document.getElementById("advancedButton").dispatchEvent(event); + + addDomainErrorLinks(); + } + + /* Only do autofocus if we're the toplevel frame; otherwise we + don't want to call attention to ourselves! The key part is + that autofocus happens on insertion into the tree, so we + can remove the button, add @autofocus, and reinsert the + button. + */ + function addAutofocus(buttonId, position = "afterbegin") { + if (window.top == window) { + var button = document.getElementById(buttonId); + var parent = button.parentNode; + button.remove(); + button.setAttribute("autofocus", "true"); + parent.insertAdjacentElement(position, button); + } + } + + /* Try to preserve the links contained in the error description, like + the error code. + + Also, in the case of SSL error pages about domain mismatch, see if + we can hyperlink the user to the correct site. We don't want + to do this generically since it allows MitM attacks to redirect + users to a site under attacker control, but in certain cases + it is safe (and helpful!) to do so. Bug 402210 + */ + function addDomainErrorLinks() { + // Rather than textContent, we need to treat description as HTML + var sdid = gIsCertError ? "badCertTechnicalInfo" : "errorShortDescText"; + var sd = document.getElementById(sdid); + if (sd) { + var desc = getDescription(); + + // sanitize description text - see bug 441169 + + // First, find the index of the <a> tags we care about, being + // careful not to use an over-greedy regex. + var codeRe = /<a id="errorCode" title="([^"]+)">/; + var codeResult = codeRe.exec(desc); + var domainRe = /<a id="cert_domain_link" title="([^"]+)">/; + var domainResult = domainRe.exec(desc); + + // The order of these links in the description is fixed in + // TransportSecurityInfo.cpp:formatOverridableCertErrorMessage. + var firstResult = domainResult; + if (!domainResult) + firstResult = codeResult; + if (!firstResult) + return; + // Remove sd's existing children + sd.textContent = ""; + + // Everything up to the first link should be text content. + sd.appendChild(document.createTextNode(desc.slice(0, firstResult.index))); + + // Now create the actual links. + if (domainResult) { + createLink(sd, "cert_domain_link", domainResult[1]) + // Append text for anything between the two links. + sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length, codeResult.index))); + } + createLink(sd, "errorCode", codeResult[1]) + + // Finally, append text for anything after the last closing </a>. + sd.appendChild(document.createTextNode(desc.slice(desc.lastIndexOf("</a>") + "</a>".length))); + } + + if (gIsCertError) { + // Initialize the error code link embedded in the error message to + // display debug information about the cert error. + var errorCode = document.getElementById("errorCode"); + if (errorCode) { + errorCode.href = "javascript:void(0)"; + errorCode.addEventListener("click", () => { + let debugInfo = document.getElementById("certificateErrorDebugInformation"); + debugInfo.style.display = "block"; + debugInfo.scrollIntoView({block: "start", behavior: "smooth"}); + }, false); + } + } + + // Initialize the cert domain link. + var link = document.getElementById("cert_domain_link"); + if (!link) + return; + + var okHost = link.getAttribute("title"); + var thisHost = document.location.hostname; + var proto = document.location.protocol; + + // If okHost is a wildcard domain ("*.example.com") let's + // use "www" instead. "*.example.com" isn't going to + // get anyone anywhere useful. bug 432491 + okHost = okHost.replace(/^\*\./, "www."); + + /* case #1: + * example.com uses an invalid security certificate. + * + * The certificate is only valid for www.example.com + * + * Make sure to include the "." ahead of thisHost so that + * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com" + * + * We'd normally just use a RegExp here except that we lack a + * library function to escape them properly (bug 248062), and + * domain names are famous for having '.' characters in them, + * which would allow spurious and possibly hostile matches. + */ + if (okHost.endsWith("." + thisHost)) + link.href = proto + okHost; + + /* case #2: + * browser.garage.maemo.org uses an invalid security certificate. + * + * The certificate is only valid for garage.maemo.org + */ + if (thisHost.endsWith("." + okHost)) + link.href = proto + okHost; + + // If we set a link, meaning there's something helpful for + // the user here, expand the section by default + if (link.href && getCSSClass() != "expertBadCert") { + var panelId = gIsCertError ? "badCertAdvancedPanel" : "weakCryptoAdvancedPanel" + toggleDisplay(document.getElementById(panelId)); + if (gIsCertError) { + // Toggling the advanced panel must ensure that the debugging + // information panel is hidden as well, since it's opened by the + // error code link in the advanced panel. + var div = document.getElementById("certificateErrorDebugInformation"); + div.style.display = "none"; + } + } + } + + function createLink(el, id, text) { + var anchorEl = document.createElement("a"); + anchorEl.setAttribute("id", id); + anchorEl.setAttribute("title", text); + anchorEl.appendChild(document.createTextNode(text)); + el.appendChild(anchorEl); + } + ]]></script> + </head> + + <body dir="&locale.dir;"> + <!-- Contains an alternate page title set on page init for cert errors. --> + <div id="certErrorPageTitle" style="display: none;">&certerror.pagetitle1;</div> + <div id="captivePortalPageTitle" style="display: none;">&captivePortal.title;</div> + + <!-- ERROR ITEM CONTAINER (removed during loading to avoid bug 39098) --> + <div id="errorContainer"> + <div id="errorTitlesContainer"> + <h1 id="et_generic">&generic.title;</h1> + <h1 id="et_captivePortal">&captivePortal.title;</h1> + <h1 id="et_dnsNotFound">&dnsNotFound.title;</h1> + <h1 id="et_fileNotFound">&fileNotFound.title;</h1> + <h1 id="et_fileAccessDenied">&fileAccessDenied.title;</h1> + <h1 id="et_malformedURI">&malformedURI.title;</h1> + <h1 id="et_unknownProtocolFound">&unknownProtocolFound.title;</h1> + <h1 id="et_connectionFailure">&connectionFailure.title;</h1> + <h1 id="et_netTimeout">&netTimeout.title;</h1> + <h1 id="et_redirectLoop">&redirectLoop.title;</h1> + <h1 id="et_unknownSocketType">&unknownSocketType.title;</h1> + <h1 id="et_netReset">&netReset.title;</h1> + <h1 id="et_notCached">¬Cached.title;</h1> + <h1 id="et_netOffline">&netOffline.title;</h1> + <h1 id="et_netInterrupt">&netInterrupt.title;</h1> + <h1 id="et_deniedPortAccess">&deniedPortAccess.title;</h1> + <h1 id="et_proxyResolveFailure">&proxyResolveFailure.title;</h1> + <h1 id="et_proxyConnectFailure">&proxyConnectFailure.title;</h1> + <h1 id="et_contentEncodingError">&contentEncodingError.title;</h1> + <h1 id="et_unsafeContentType">&unsafeContentType.title;</h1> + <h1 id="et_nssFailure2">&nssFailure2.title;</h1> + <h1 id="et_nssBadCert">&certerror.longpagetitle1;</h1> + <h1 id="et_cspBlocked">&cspBlocked.title;</h1> + <h1 id="et_remoteXUL">&remoteXUL.title;</h1> + <h1 id="et_corruptedContentErrorv2">&corruptedContentErrorv2.title;</h1> + <h1 id="et_sslv3Used">&sslv3Used.title;</h1> + <h1 id="et_weakCryptoUsed">&weakCryptoUsed.title;</h1> + <h1 id="et_inadequateSecurityError">&inadequateSecurityError.title;</h1> + </div> + <div id="errorDescriptionsContainer"> + <div id="ed_generic">&generic.longDesc;</div> + <div id="ed_captivePortal">&captivePortal.longDesc;</div> + <div id="ed_dnsNotFound">&dnsNotFound.longDesc;</div> + <div id="ed_fileNotFound">&fileNotFound.longDesc;</div> + <div id="ed_fileAccessDenied">&fileAccessDenied.longDesc;</div> + <div id="ed_malformedURI">&malformedURI.longDesc;</div> + <div id="ed_unknownProtocolFound">&unknownProtocolFound.longDesc;</div> + <div id="ed_connectionFailure">&connectionFailure.longDesc;</div> + <div id="ed_netTimeout">&netTimeout.longDesc;</div> + <div id="ed_redirectLoop">&redirectLoop.longDesc;</div> + <div id="ed_unknownSocketType">&unknownSocketType.longDesc;</div> + <div id="ed_netReset">&netReset.longDesc;</div> + <div id="ed_notCached">¬Cached.longDesc;</div> + <div id="ed_netOffline">&netOffline.longDesc2;</div> + <div id="ed_netInterrupt">&netInterrupt.longDesc;</div> + <div id="ed_deniedPortAccess">&deniedPortAccess.longDesc;</div> + <div id="ed_proxyResolveFailure">&proxyResolveFailure.longDesc;</div> + <div id="ed_proxyConnectFailure">&proxyConnectFailure.longDesc;</div> + <div id="ed_contentEncodingError">&contentEncodingError.longDesc;</div> + <div id="ed_unsafeContentType">&unsafeContentType.longDesc;</div> + <div id="ed_nssFailure2">&nssFailure2.longDesc2;</div> + <div id="ed_nssBadCert">&certerror.introPara;</div> + <div id="ed_cspBlocked">&cspBlocked.longDesc;</div> + <div id="ed_remoteXUL">&remoteXUL.longDesc;</div> + <div id="ed_corruptedContentErrorv2">&corruptedContentErrorv2.longDesc;</div> + <div id="ed_sslv3Used">&sslv3Used.longDesc2;</div> + <div id="ed_weakCryptoUsed">&weakCryptoUsed.longDesc2;</div> + <div id="ed_inadequateSecurityError">&inadequateSecurityError.longDesc;</div> + </div> + </div> + + <!-- PAGE CONTAINER (for styling purposes only) --> + <div id="errorPageContainer" class="container"> + + <!-- Error Title --> + <div class="title"> + <h1 class="title-text"/> + </div> + + <!-- LONG CONTENT (the section most likely to require scrolling) --> + <div id="errorLongContent"> + + <!-- Short Description --> + <div id="errorShortDesc"> + <p id="errorShortDescText" /> + </div> + <p id="badStsCertExplanation" hidden="true">&certerror.whatShouldIDo.badStsCertExplanation;</p> + + <div id="wrongSystemTimePanel" style="display: none;"> + &certerror.wrongSystemTime; + </div> + + <!-- Long Description (Note: See netError.dtd for used XHTML tags) --> + <div id="errorLongDesc" /> + + <div id="learnMoreContainer"> + <p><a href="https://support.mozilla.org/kb/what-does-your-connection-is-not-secure-mean" id="learnMoreLink" target="new">&errorReporting.learnMore;</a></p> + </div> + + <div id="prefChangeContainer" class="button-container"> + <p>&prefReset.longDesc;</p> + <button id="prefResetButton" class="primary" autocomplete="off">&prefReset.label;</button> + </div> + + <div id="certErrorAndCaptivePortalButtonContainer" class="button-container"> + <button id="returnButton" class="primary" autocomplete="off">&returnToPreviousPage.label;</button> + <button id="openPortalLoginPageButton" class="primary" autocomplete="off">&openPortalLoginPage.label;</button> + <div class="button-spacer"></div> + <button id="advancedButton" autocomplete="off">&advanced.label;</button> + </div> + </div> + + <div id="netErrorButtonContainer" class="button-container"> + <button id="errorTryAgain" class="primary" autocomplete="off" onclick="retryThis(this);">&retry.label;</button> + </div> + + <!-- UI for option to report certificate errors to Mozilla. Removed on + init for other error types .--> + <div id="certificateErrorReporting"> + <p class="toggle-container-with-text"> + <input type="checkbox" id="automaticallyReportInFuture" /> + <label for="automaticallyReportInFuture" id="automaticallyReportInFuture">&errorReporting.automatic2;</label> + </p> + </div> + + <div id="advancedPanelContainer"> + <div id="weakCryptoAdvancedPanel" class="advanced-panel"> + <div id="weakCryptoAdvancedDescription"> + <p>&weakCryptoAdvanced.longDesc;</p> + </div> + <div id="advancedLongDesc" /> + <div id="overrideWeakCryptoPanel"> + <a id="overrideWeakCrypto" href="#">&weakCryptoAdvanced.override;</a> + </div> + </div> + + <div id="badCertAdvancedPanel" class="advanced-panel"> + <p id="badCertTechnicalInfo"/> + <button id="exceptionDialogButton">&securityOverride.exceptionButtonLabel;</button> + </div> + </div> + + </div> + + <div id="certificateErrorDebugInformation"> + <button id="copyToClipboard">&certerror.copyToClipboard.label;</button> + <div id="certificateErrorText"/> + <button id="copyToClipboard">&certerror.copyToClipboard.label;</button> + </div> + + <!-- + - Note: It is important to run the script this way, instead of using + - an onload handler. This is because error pages are loaded as + - LOAD_BACKGROUND, which means that onload handlers will not be executed. + --> + <script type="application/javascript"> + initPage(); + </script> + + </body> +</html> diff --git a/application/basilisk/base/content/aboutRobots-icon.png b/application/basilisk/base/content/aboutRobots-icon.png Binary files differnew file mode 100644 index 000000000..1c4899aaf --- /dev/null +++ b/application/basilisk/base/content/aboutRobots-icon.png diff --git a/application/basilisk/base/content/aboutRobots-widget-left.png b/application/basilisk/base/content/aboutRobots-widget-left.png Binary files differnew file mode 100644 index 000000000..3a1e48d5f --- /dev/null +++ b/application/basilisk/base/content/aboutRobots-widget-left.png diff --git a/application/basilisk/base/content/aboutRobots.xhtml b/application/basilisk/base/content/aboutRobots.xhtml new file mode 100644 index 000000000..23fe3ba17 --- /dev/null +++ b/application/basilisk/base/content/aboutRobots.xhtml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % netErrorDTD + SYSTEM "chrome://global/locale/netError.dtd"> + %netErrorDTD; + <!ENTITY % globalDTD + SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % aboutrobotsDTD + SYSTEM "chrome://browser/locale/aboutRobots.dtd"> + %aboutrobotsDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&robots.pagetitle;</title> + <link rel="stylesheet" href="chrome://global/skin/netError.css" type="text/css" media="all" /> + <link rel="icon" type="image/png" id="favicon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8%2F9hAAAACGFjVEwAAAASAAAAAJNtBPIAAAAaZmNUTAAAAAAAAAAQAAAAEAAAAAAAAAAALuAD6AABhIDeugAAALhJREFUOI2Nk8sNxCAMRDlGohauXFOMpfTiAlxICqAELltHLqlgctg1InzMRhpFAc%2BLGWTnmoeZYamt78zXdZmaQtQMADlnU0OIAlbmJUBEcO4bRKQY2rUXIPmAGnDuG%2FBx3%2FfvOPVaDUg%2BoAPUf1PArIMCSD5glMEsUGaG%2BkyAFWIBaCsKuA%2BHGCNijLgP133XgOEtaPFMy2vUolEGJoCIzBmoRUR9%2B7rxj16DZaW%2FmgtmxnJ8V3oAnApQwNS5zpcAAAAaZmNUTAAAAAEAAAAQAAAAEAAAAAAAAAAAAB4D6AIB52fclgAAACpmZEFUAAAAAjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9WF%2Bu8QAAABpmY1RMAAAAAwAAABAAAAAQAAAAAAAAAAAAHgPoAgEK8Q9%2FAAAAFmZkQVQAAAAEOI1jYBgFo2AUjAIIAAAEEAAB0xIn4wAAABpmY1RMAAAABQAAABAAAAAQAAAAAAAAAAAAHgPoAgHnO30FAAAAQGZkQVQAAAAGOI1jYBieYKcaw39ixHCC%2F6cwFWMTw2rz%2F1MM%2F6Vu%2Ff%2F%2F%2FxTD%2F51qEIwuRjsXILuEGLFRMApgAADhNCsVfozYcAAAABpmY1RMAAAABwAAABAAAAAQAAAAAAAAAAAAHgPoAgEKra7sAAAAFmZkQVQAAAAIOI1jYBgFo2AUjAIIAAAEEAABM9s3hAAAABpmY1RMAAAACQAAABAAAAAQAAAAAAAAAAAAHgPoAgHn3p%2BwAAAAKmZkQVQAAAAKOI1jYBgFWEFzc%2FN%2FYsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF%2F1BhPl6AAAAGmZjVEwAAAALAAAAEAAAABAAAAAAAAAAAAAeA%2BgCAQpITFkAAAAWZmRBVAAAAAw4jWNgGAWjYBSMAggAAAQQAAHaszpmAAAAGmZjVEwAAAANAAAAEAAAABAAAAAAAAAAAAAeA%2BgCAeeCPiMAAABAZmRBVAAAAA44jWNgGJ5gpxrDf2LEcIL%2FpzAVYxPDavP%2FUwz%2FpW79%2F%2F%2F%2FFMP%2FnWoQjC5GOxcgu4QYsVEwCmAAAOE0KxUmBL0KAAAAGmZjVEwAAAAPAAAAEAAAABAAAAAAAAAAAAAeA%2BgCAQoU7coAAAAWZmRBVAAAABA4jWNgGAWjYBSMAggAAAQQAAEpOBELAAAAGmZjVEwAAAARAAAAEAAAABAAAAAAAAAAAAAeA%2BgCAeYVWtoAAAAqZmRBVAAAABI4jWNgGAVYQXNz839ixHBq3qnG8B9ZAzYx2rlgFIwCcgAA8psX%2FWvpAecAAAAaZmNUTAAAABMAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC4OJMwAAABZmZEFUAAAAFDiNY2AYBaNgFIwCCAAABBAAAcBQHOkAAAAaZmNUTAAAABUAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5kn7SQAAAEBmZEFUAAAAFjiNY2AYnmCnGsN%2FYsRwgv%2BnMBVjE8Nq8%2F9TDP%2Blbv3%2F%2F%2F8Uw%2F%2BdahCMLkY7FyC7hBixUTAKYAAA4TQrFc%2BcEoQAAAAaZmNUTAAAABcAAAAQAAAAEAAAAAAAAAAAAB4D6AIBC98ooAAAABZmZEFUAAAAGDiNY2AYBaNgFIwCCAAABBAAASCZDI4AAAAaZmNUTAAAABkAAAAQAAAAEAAAAAAAAAAAAB4D6AIB5qwZ%2FAAAACpmZEFUAAAAGjiNY2AYBVhBc3Pzf2LEcGreqcbwH1kDNjHauWAUjAJyAADymxf9cjJWbAAAABpmY1RMAAAAGwAAABAAAAAQAAAAAAAAAAAAHgPoAgELOsoVAAAAFmZkQVQAAAAcOI1jYBgFo2AUjAIIAAAEEAAByfEBbAAAABpmY1RMAAAAHQAAABAAAAAQAAAAAAAAAAAAHgPoAgHm8LhvAAAAQGZkQVQAAAAeOI1jYBieYKcaw39ixHCC%2F6cwFWMTw2rz%2F1MM%2F6Vu%2Ff%2F%2F%2FxTD%2F51qEIwuRjsXILuEGLFRMApgAADhNCsVlxR3%2FgAAABpmY1RMAAAAHwAAABAAAAAQAAAAAAAAAAAAHgPoAgELZmuGAAAAFmZkQVQAAAAgOI1jYBgFo2AUjAIIAAAEEAABHP5cFQAAABpmY1RMAAAAIQAAABAAAAAQAAAAAAAAAAAAHgPoAgHlgtAOAAAAKmZkQVQAAAAiOI1jYBgFWEFzc%2FN%2FYsRwat6pxvAfWQM2Mdq5YBSMAnIAAPKbF%2F0%2FMvDdAAAAAElFTkSuQmCC"/> + + <script type="application/javascript"><![CDATA[ + var buttonClicked = false; + function robotButton() + { + var button = document.getElementById('errorTryAgain'); + if (buttonClicked) { + button.style.visibility = "hidden"; + } else { + var newLabel = button.getAttribute("label2"); + button.textContent = newLabel; + buttonClicked = true; + } + } + ]]></script> + + <style type="text/css"><![CDATA[ + #errorPageContainer { + background-image: none; + } + + #errorPageContainer:before { + content: url('chrome://browser/content/aboutRobots-icon.png'); + position: absolute; + } + + body[dir=rtl] #icon, + body[dir=rtl] #errorPageContainer:before { + transform: scaleX(-1); + } + ]]></style> + </head> + + <body dir="&locale.dir;"> + + <!-- PAGE CONTAINER (for styling purposes only) --> + <div id="errorPageContainer"> + + <!-- Error Title --> + <div id="errorTitle"> + <h1 id="errorTitleText">&robots.errorTitleText;</h1> + </div> + + <!-- LONG CONTENT (the section most likely to require scrolling) --> + <div id="errorLongContent"> + + <!-- Short Description --> + <div id="errorShortDesc"> + <p id="errorShortDescText">&robots.errorShortDescText;</p> + </div> + + <!-- Long Description (Note: See netError.dtd for used XHTML tags) --> + <div id="errorLongDesc"> + <ul> + <li>&robots.errorLongDesc1;</li> + <li>&robots.errorLongDesc2;</li> + <li>&robots.errorLongDesc3;</li> + <li>&robots.errorLongDesc4;</li> + </ul> + </div> + + <!-- Short Description --> + <div id="errorTrailerDesc"> + <p id="errorTrailerDescText">&robots.errorTrailerDescText;</p> + </div> + + </div> + + <!-- Button --> + <button id="errorTryAgain" + label2="&robots.dontpress;" + onclick="robotButton();">&retry.label;</button> + + <img src="chrome://browser/content/aboutRobots-widget-left.png" + style="position: absolute; bottom: -12px; left: -10px;"/> + <img src="chrome://browser/content/aboutRobots-widget-left.png" + style="position: absolute; bottom: -12px; right: -10px; transform: scaleX(-1);"/> + </div> + + </body> +</html> diff --git a/application/basilisk/base/content/aboutTabCrashed.css b/application/basilisk/base/content/aboutTabCrashed.css new file mode 100644 index 000000000..de0eabe8b --- /dev/null +++ b/application/basilisk/base/content/aboutTabCrashed.css @@ -0,0 +1,11 @@ +/* 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/. */ + +html:not(.crashDumpSubmitted) #reportSent, +html:not(.crashDumpAvailable) #reportBox, +.container[multiple="true"] > .offers > #offerHelpMessageSingle, +.container[multiple="false"] > .offers > #offerHelpMessageMultiple, +.container[multiple="false"] > .button-container > #restoreAll { + display: none; +}
\ No newline at end of file diff --git a/application/basilisk/base/content/aboutTabCrashed.js b/application/basilisk/base/content/aboutTabCrashed.js new file mode 100644 index 000000000..337add1d2 --- /dev/null +++ b/application/basilisk/base/content/aboutTabCrashed.js @@ -0,0 +1,309 @@ +/* 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 AboutTabCrashed = { + /** + * This can be set to true once this page receives a message from the + * parent saying whether or not a crash report is available. + */ + hasReport: false, + + /** + * The messages that we might receive from the parent. + */ + MESSAGES: [ + "SetCrashReportAvailable", + "CrashReportSent", + "UpdateCount", + ], + + /** + * Items for which we will listen for click events. + */ + CLICK_TARGETS: [ + "closeTab", + "restoreTab", + "restoreAll", + "sendReport", + ], + + /** + * Returns information about this crashed tab. + * + * @return (Object) An object with the following properties: + * title (String): + * The title of the page that crashed. + * URL (String): + * The URL of the page that crashed. + */ + get pageData() { + delete this.pageData; + + let URL = document.documentURI; + let queryString = URL.replace(/^about:tabcrashed?e=tabcrashed/, ""); + + let titleMatch = queryString.match(/d=([^&]*)/); + let URLMatch = queryString.match(/u=([^&]*)/); + + return this.pageData = { + title: titleMatch && titleMatch[1] ? decodeURIComponent(titleMatch[1]) : "", + URL: URLMatch && URLMatch[1] ? decodeURIComponent(URLMatch[1]) : "", + }; + }, + + init() { + this.MESSAGES.forEach((msg) => addMessageListener(msg, this.receiveMessage.bind(this))); + addEventListener("DOMContentLoaded", this); + + document.title = this.pageData.title; + }, + + receiveMessage(message) { + switch (message.name) { + case "UpdateCount": { + this.setMultiple(message.data.count > 1); + break; + } + case "SetCrashReportAvailable": { + this.onSetCrashReportAvailable(message); + break; + } + case "CrashReportSent": { + this.onCrashReportSent(); + break; + } + } + }, + + handleEvent(event) { + switch (event.type) { + case "DOMContentLoaded": { + this.onDOMContentLoaded(); + break; + } + case "click": { + this.onClick(event); + break; + } + case "input": { + this.onInput(event); + break; + } + } + }, + + onDOMContentLoaded() { + this.CLICK_TARGETS.forEach((targetID) => { + let el = document.getElementById(targetID); + el.addEventListener("click", this); + }); + + // For setting "emailMe" checkbox automatically on email value change. + document.getElementById("email").addEventListener("input", this); + + // Error pages are loaded as LOAD_BACKGROUND, so they don't get load events. + let event = new CustomEvent("AboutTabCrashedLoad", {bubbles:true}); + document.dispatchEvent(event); + + sendAsyncMessage("Load"); + }, + + onClick(event) { + switch (event.target.id) { + case "closeTab": { + this.sendMessage("closeTab"); + break; + } + + case "restoreTab": { + this.sendMessage("restoreTab"); + break; + } + + case "restoreAll": { + this.sendMessage("restoreAll"); + break; + } + + case "sendReport": { + this.showCrashReportUI(event.target.checked); + break; + } + } + }, + + onInput(event) { + switch (event.target.id) { + case "email": { + document.getElementById("emailMe").checked = !!event.target.value; + break; + } + } + }, + /** + * After this page tells the parent that it has loaded, the parent + * will respond with whether or not a crash report is available. This + * method handles that message. + * + * @param message + * The message from the parent, which should contain a data + * Object property with the following properties: + * + * hasReport (bool): + * Whether or not there is a crash report. + * + * sendReport (bool): + * Whether or not the the user prefers to send the report + * by default. + * + * includeURL (bool): + * Whether or not the user prefers to send the URL of + * the tab that crashed. + * + * emailMe (bool): + * Whether or not to send the email address of the user + * in the report. + * + * email (String): + * The email address of the user (empty if emailMe is false). + * + * requestAutoSubmit (bool): + * Whether or not we should ask the user to automatically + * submit backlogged crash reports. + * + */ + onSetCrashReportAvailable(message) { + let data = message.data; + + if (data.hasReport) { + this.hasReport = true; + document.documentElement.classList.add("crashDumpAvailable"); + + document.getElementById("sendReport").checked = data.sendReport; + document.getElementById("includeURL").checked = data.includeURL; + + if (data.requestEmail) { + document.getElementById("requestEmail").hidden = false; + document.getElementById("emailMe").checked = data.emailMe; + if (data.emailMe) { + document.getElementById("email").value = data.email; + } + } + + this.showCrashReportUI(data.sendReport); + } else { + this.showCrashReportUI(false); + } + + if (data.requestAutoSubmit) { + document.getElementById("requestAutoSubmit").hidden = false; + } + + let event = new CustomEvent("AboutTabCrashedReady", {bubbles:true}); + document.dispatchEvent(event); + }, + + /** + * Handler for when the parent reports that the crash report associated + * with this about:tabcrashed page has been sent. + */ + onCrashReportSent() { + document.documentElement.classList.remove("crashDumpAvailable"); + document.documentElement.classList.add("crashDumpSubmitted"); + }, + + /** + * Toggles the display of the crash report form. + * + * @param shouldShow (bool) + * True if the crash report form should be shown + */ + showCrashReportUI(shouldShow) { + let options = document.getElementById("options"); + options.hidden = !shouldShow; + }, + + /** + * Toggles whether or not the page is one of several visible pages + * showing the crash reporter. This controls some of the language + * on the page, along with what the "primary" button is. + * + * @param hasMultiple (bool) + * True if there are multiple crash report pages being shown. + */ + setMultiple(hasMultiple) { + let main = document.getElementById("main"); + main.setAttribute("multiple", hasMultiple); + + let restoreTab = document.getElementById("restoreTab"); + + // The "Restore All" button has the "primary" class by default, so + // we only need to modify the "Restore Tab" button. + if (hasMultiple) { + restoreTab.classList.remove("primary"); + } else { + restoreTab.classList.add("primary"); + } + }, + + /** + * Sends a message to the parent in response to the user choosing + * one of the actions available on the page. This might also send up + * crash report information if the user has chosen to submit a crash + * report. + * + * @param messageName (String) + * The message to send to the parent + */ + sendMessage(messageName) { + let comments = ""; + let email = ""; + let URL = ""; + let sendReport = false; + let emailMe = false; + let includeURL = false; + let autoSubmit = false; + + if (this.hasReport) { + sendReport = document.getElementById("sendReport").checked; + if (sendReport) { + comments = document.getElementById("comments").value.trim(); + + includeURL = document.getElementById("includeURL").checked; + if (includeURL) { + URL = this.pageData.URL.trim(); + } + + if (!document.getElementById("requestEmail").hidden) { + emailMe = document.getElementById("emailMe").checked; + if (emailMe) { + email = document.getElementById("email").value.trim(); + } + } + } + } + + let requestAutoSubmit = document.getElementById("requestAutoSubmit"); + if (requestAutoSubmit.hidden) { + // The checkbox is hidden if the user has already opted in to sending + // backlogged crash reports. + autoSubmit = true; + } else { + autoSubmit = document.getElementById("autoSubmit").checked; + } + + sendAsyncMessage(messageName, { + sendReport, + comments, + email, + emailMe, + includeURL, + URL, + autoSubmit, + hasReport: this.hasReport, + }); + }, +}; + +AboutTabCrashed.init(); diff --git a/application/basilisk/base/content/aboutTabCrashed.xhtml b/application/basilisk/base/content/aboutTabCrashed.xhtml new file mode 100644 index 000000000..8b18bee9c --- /dev/null +++ b/application/basilisk/base/content/aboutTabCrashed.xhtml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % globalDTD + SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > + %brandDTD; + <!ENTITY % tabCrashedDTD + SYSTEM "chrome://browser/locale/aboutTabCrashed.dtd"> + %tabCrashedDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <link rel="stylesheet" type="text/css" media="all" + href="chrome://global/skin/in-content/info-pages.css"/> + <link rel="stylesheet" type="text/css" media="all" + href="chrome://browser/content/aboutTabCrashed.css"/> + <link rel="stylesheet" type="text/css" media="all" + href="chrome://browser/skin/aboutTabCrashed.css"/> + </head> + + <body dir="&locale.dir;"> + <div id="main" class="container" multiple="false"> + + <div class="title"> + <h1 class="title-text">&tabCrashed.header2;</h1> + </div> + + <div class="offers"> + <h2>&tabCrashed.offerHelp;</h2> + <p id="offerHelpMessageSingle">&tabCrashed.single.offerHelpMessage;</p> + <p id="offerHelpMessageMultiple">&tabCrashed.multiple.offerHelpMessage;</p> + </div> + + <div id="reportBox"> + <h2>&tabCrashed.requestHelp;</h2> + <p>&tabCrashed.requestHelpMessage;</p> + + <h2>&tabCrashed.requestReport;</h2> + + <div class="checkbox-with-label"> + <input type="checkbox" id="sendReport"/> + <label for="sendReport">&tabCrashed.sendReport2;</label> + </div> + + <ul id="options"> + <li> + <textarea id="comments" placeholder="&tabCrashed.commentPlaceholder2;" rows="4"></textarea> + </li> + + <li class="checkbox-with-label"> + <input type="checkbox" id="includeURL"/> + <label for="includeURL">&tabCrashed.includeURL2;</label> + </li> + + <li id="requestEmail" hidden="true"> + <div class="checkbox-with-label"> + <input type="checkbox" id="emailMe"/> + <label for="emailMe">&tabCrashed.emailMe;</label> + </div> + <input type="text" id="email" placeholder="&tabCrashed.emailPlaceholder;"/> + </li> + </ul> + + <div id="requestAutoSubmit" hidden="true"> + <h2>&tabCrashed.requestAutoSubmit2;</h2> + <div class="checkbox-with-label"> + <input type="checkbox" id="autoSubmit"/> + <label for="autoSubmit">&tabCrashed.autoSubmit;</label> + </div> + </div> + </div> + + <p id="reportSent">&tabCrashed.reportSent;</p> + + <div class="button-container"> + <button id="closeTab"> + &tabCrashed.closeTab;</button> + <button id="restoreTab" class="primary"> + &tabCrashed.restoreTab;</button> + <button id="restoreAll" autofocus="true" class="primary"> + &tabCrashed.restoreAll;</button> + </div> + </div> + </body> + <script type="text/javascript;version=1.8" src="chrome://browser/content/aboutTabCrashed.js"/> +</html> diff --git a/application/basilisk/base/content/aboutaccounts/aboutaccounts.css b/application/basilisk/base/content/aboutaccounts/aboutaccounts.css new file mode 100644 index 000000000..a2c5cb8f0 --- /dev/null +++ b/application/basilisk/base/content/aboutaccounts/aboutaccounts.css @@ -0,0 +1,24 @@ +html, body { + height: 100%; +} + +#remote { + width: 100%; + height: 100%; + border: 0; + display: none; +} + +#networkError, #manage, #intro, #stage, #configError { + display: none; +} + +#oldsync { + background: none; + border: 0; + color: #0095dd; +} + +#oldsync:focus { + outline: 1px dotted #0095dd; +} diff --git a/application/basilisk/base/content/aboutaccounts/aboutaccounts.js b/application/basilisk/base/content/aboutaccounts/aboutaccounts.js new file mode 100644 index 000000000..a05c1ea75 --- /dev/null +++ b/application/basilisk/base/content/aboutaccounts/aboutaccounts.js @@ -0,0 +1,543 @@ +/* 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/. */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FxAccounts.jsm"); + +var fxAccountsCommon = {}; +Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon); + +// for master-password utilities +Cu.import("resource://services-sync/util.js"); + +const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash"; +const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog"; + +const ACTION_URL_PARAM = "action"; + +const OBSERVER_TOPICS = [ + fxAccountsCommon.ONVERIFIED_NOTIFICATION, + fxAccountsCommon.ONLOGOUT_NOTIFICATION, +]; + +function log(msg) { + // dump("FXA: " + msg + "\n"); +} + +function error(msg) { + console.log("Firefox Account Error: " + msg + "\n"); +} + +function getPreviousAccountNameHash() { + try { + return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data; + } catch (_) { + return ""; + } +} + +function setPreviousAccountNameHash(acctName) { + let string = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + string.data = sha256(acctName); + Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string); +} + +function needRelinkWarning(acctName) { + let prevAcctHash = getPreviousAccountNameHash(); + return prevAcctHash && prevAcctHash != sha256(acctName); +} + +// Given a string, returns the SHA265 hash in base64 +function sha256(str) { + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + // Data is an array of bytes. + let data = converter.convertToByteArray(str, {}); + let hasher = Cc["@mozilla.org/security/hash;1"] + .createInstance(Ci.nsICryptoHash); + hasher.init(hasher.SHA256); + hasher.update(data, data.length); + + return hasher.finish(true); +} + +function promptForRelink(acctName) { + let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); + let continueLabel = sb.GetStringFromName("continue.label"); + let title = sb.GetStringFromName("relinkVerify.title"); + let description = sb.formatStringFromName("relinkVerify.description", + [acctName], 1); + let body = sb.GetStringFromName("relinkVerify.heading") + + "\n\n" + description; + let ps = Services.prompt; + let buttonFlags = (ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING) + + (ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL) + + ps.BUTTON_POS_1_DEFAULT; + let pressed = Services.prompt.confirmEx(window, title, body, buttonFlags, + continueLabel, null, null, null, + {}); + return pressed == 0; // 0 is the "continue" button +} + +// If the last fxa account used for sync isn't this account, we display +// a modal dialog checking they really really want to do this... +// (This is sync-specific, so ideally would be in sync's identity module, +// but it's a little more seamless to do here, and sync is currently the +// only fxa consumer, so... +function shouldAllowRelink(acctName) { + return !needRelinkWarning(acctName) || promptForRelink(acctName); +} + +function updateDisplayedEmail(user) { + let emailDiv = document.getElementById("email"); + if (emailDiv && user) { + emailDiv.textContent = user.email; + } +} + +var wrapper = { + iframe: null, + + init: function (url, urlParams) { + // If a master-password is enabled, we want to encourage the user to + // unlock it. Things still work if not, but the user will probably need + // to re-auth next startup (in which case we will get here again and + // re-prompt) + Utils.ensureMPUnlocked(); + + let iframe = document.getElementById("remote"); + this.iframe = iframe; + this.iframe.QueryInterface(Ci.nsIFrameLoaderOwner); + let docShell = this.iframe.frameLoader.docShell; + docShell.QueryInterface(Ci.nsIWebProgress); + docShell.addProgressListener(this.iframeListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); + iframe.addEventListener("load", this); + + // Ideally we'd just merge urlParams with new URL(url).searchParams, but our + // URLSearchParams implementation doesn't support iteration (bug 1085284). + let urlParamStr = urlParams.toString(); + if (urlParamStr) { + url += (url.includes("?") ? "&" : "?") + urlParamStr; + } + this.url = url; + // Set the iframe's location with loadURI/LOAD_FLAGS_REPLACE_HISTORY to + // avoid having a new history entry being added. REPLACE_HISTORY is used + // to replace the current entry, which is `about:blank`. + let webNav = iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation); + webNav.loadURI(url, Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, null, null, null); + }, + + retry: function () { + let webNav = this.iframe.frameLoader.docShell.QueryInterface(Ci.nsIWebNavigation); + webNav.loadURI(this.url, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null); + }, + + iframeListener: { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference, + Ci.nsISupports]), + + onStateChange: function(aWebProgress, aRequest, aState, aStatus) { + let failure = false; + + // Captive portals sometimes redirect users + if ((aState & Ci.nsIWebProgressListener.STATE_REDIRECTING)) { + failure = true; + } else if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) { + if (aRequest instanceof Ci.nsIHttpChannel) { + try { + failure = aRequest.responseStatus != 200; + } catch (e) { + failure = aStatus != Components.results.NS_OK; + } + } + } + + // Calling cancel() will raise some OnStateChange notifications by itself, + // so avoid doing that more than once + if (failure && aStatus != Components.results.NS_BINDING_ABORTED) { + aRequest.cancel(Components.results.NS_BINDING_ABORTED); + setErrorPage("networkError"); + } + }, + + onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { + if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { + aRequest.cancel(Components.results.NS_BINDING_ABORTED); + setErrorPage("networkError"); + } + }, + + onProgressChange: function() {}, + onStatusChange: function() {}, + onSecurityChange: function() {}, + }, + + handleEvent: function (evt) { + switch (evt.type) { + case "load": + this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this); + this.iframe.removeEventListener("load", this); + break; + case "FirefoxAccountsCommand": + this.handleRemoteCommand(evt); + break; + } + }, + + /** + * onLogin handler receives user credentials from the jelly after a + * sucessful login and stores it in the fxaccounts service + * + * @param accountData the user's account data and credentials + */ + onLogin: function (accountData) { + log("Received: 'login'. Data:" + JSON.stringify(accountData)); + + if (accountData.customizeSync) { + Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true); + } + delete accountData.customizeSync; + // sessionTokenContext is erroneously sent by the content server. + // https://github.com/mozilla/fxa-content-server/issues/2766 + // To avoid having the FxA storage manager not knowing what to do with + // it we delete it here. + delete accountData.sessionTokenContext; + + // We need to confirm a relink - see shouldAllowRelink for more + let newAccountEmail = accountData.email; + // The hosted code may have already checked for the relink situation + // by sending the can_link_account command. If it did, then + // it will indicate we don't need to ask twice. + if (!accountData.verifiedCanLinkAccount && !shouldAllowRelink(newAccountEmail)) { + // we need to tell the page we successfully received the message, but + // then bail without telling fxAccounts + this.injectData("message", { status: "login" }); + // after a successful login we return to preferences + openPrefs(); + return; + } + delete accountData.verifiedCanLinkAccount; + + // Remember who it was so we can log out next time. + setPreviousAccountNameHash(newAccountEmail); + + // A sync-specific hack - we want to ensure sync has been initialized + // before we set the signed-in user. + let xps = Cc["@mozilla.org/weave/service;1"] + .getService(Ci.nsISupports) + .wrappedJSObject; + xps.whenLoaded().then(() => { + updateDisplayedEmail(accountData); + return fxAccounts.setSignedInUser(accountData); + }).then(() => { + // If the user data is verified, we want it to immediately look like + // they are signed in without waiting for messages to bounce around. + if (accountData.verified) { + openPrefs(); + } + this.injectData("message", { status: "login" }); + // until we sort out a better UX, just leave the jelly page in place. + // If the account email is not yet verified, it will tell the user to + // go check their email, but then it will *not* change state after + // the verification completes (the browser will begin syncing, but + // won't notify the user). If the email has already been verified, + // the jelly will say "Welcome! You are successfully signed in as + // EMAIL", but it won't then say "syncing started". + }, (err) => this.injectData("message", { status: "error", error: err }) + ); + }, + + onCanLinkAccount: function(accountData) { + // We need to confirm a relink - see shouldAllowRelink for more + let ok = shouldAllowRelink(accountData.email); + this.injectData("message", { status: "can_link_account", data: { ok: ok } }); + }, + + /** + * onSignOut handler erases the current user's session from the fxaccounts service + */ + onSignOut: function () { + log("Received: 'sign_out'."); + + fxAccounts.signOut().then( + () => this.injectData("message", { status: "sign_out" }), + (err) => this.injectData("message", { status: "error", error: err }) + ); + }, + + handleRemoteCommand: function (evt) { + log('command: ' + evt.detail.command); + let data = evt.detail.data; + + switch (evt.detail.command) { + case "login": + this.onLogin(data); + break; + case "can_link_account": + this.onCanLinkAccount(data); + break; + case "sign_out": + this.onSignOut(data); + break; + default: + log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command."); + break; + } + }, + + injectData: function (type, content) { + return fxAccounts.promiseAccountsSignUpURI().then(authUrl => { + let data = { + type: type, + content: content + }; + this.iframe.contentWindow.postMessage(data, authUrl); + }) + .catch(e => { + console.log("Failed to inject data", e); + setErrorPage("configError"); + }); + }, +}; + + +// Button onclick handlers +function handleOldSync() { + let chromeWin = window + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "old-sync"; + chromeWin.switchToTabHavingURI(url, true); +} + +function getStarted() { + show("remote"); +} + +function retry() { + show("remote"); + wrapper.retry(); +} + +function openPrefs() { + // Bug 1199303 calls for this tab to always be replaced with Preferences + // rather than it opening in a different tab. + window.location = "about:preferences#sync"; +} + +function init() { + fxAccounts.getSignedInUser().then(user => { + // tests in particular might cause the window to start closing before + // getSignedInUser has returned. + if (window.closed) { + return Promise.resolve(); + } + + updateDisplayedEmail(user); + + // Ideally we'd use new URL(document.URL).searchParams, but for about: URIs, + // searchParams is empty. + let urlParams = new URLSearchParams(document.URL.split("?")[1] || ""); + let action = urlParams.get(ACTION_URL_PARAM); + urlParams.delete(ACTION_URL_PARAM); + + switch (action) { + case "signin": + if (user) { + // asking to sign-in when already signed in just shows manage. + show("stage", "manage"); + } else { + return fxAccounts.promiseAccountsSignInURI().then(url => { + show("remote"); + wrapper.init(url, urlParams); + }); + } + break; + case "signup": + if (user) { + // asking to sign-up when already signed in just shows manage. + show("stage", "manage"); + } else { + return fxAccounts.promiseAccountsSignUpURI().then(url => { + show("remote"); + wrapper.init(url, urlParams); + }); + } + break; + case "reauth": + // ideally we would only show this when we know the user is in a + // "must reauthenticate" state - but we don't. + // As the email address will be included in the URL returned from + // promiseAccountsForceSigninURI, just always show it. + return fxAccounts.promiseAccountsForceSigninURI().then(url => { + show("remote"); + wrapper.init(url, urlParams); + }); + default: + // No action specified. + if (user) { + show("stage", "manage"); + } else { + // Attempt a migration if enabled or show the introductory page + // otherwise. + return migrateToDevEdition(urlParams).then(migrated => { + if (!migrated) { + show("stage", "intro"); + // load the remote frame in the background + return fxAccounts.promiseAccountsSignUpURI().then(uri => + wrapper.init(uri, urlParams)); + } + return Promise.resolve(); + }); + } + break; + } + return Promise.resolve(); + }).catch(err => { + console.log("Configuration or sign in error", err); + setErrorPage("configError"); + }); +} + +function setErrorPage(errorType) { + show("stage", errorType); +} + +// Causes the "top-level" element with |id| to be shown - all other top-level +// elements are hidden. Optionally, ensures that only 1 "second-level" element +// inside the top-level one is shown. +function show(id, childId) { + // top-level items are either <div> or <iframe> + let allTop = document.querySelectorAll("body > div, iframe"); + for (let elt of allTop) { + if (elt.getAttribute("id") == id) { + elt.style.display = 'block'; + } else { + elt.style.display = 'none'; + } + } + if (childId) { + // child items are all <div> + let allSecond = document.querySelectorAll("#" + id + " > div"); + for (let elt of allSecond) { + if (elt.getAttribute("id") == childId) { + elt.style.display = 'block'; + } else { + elt.style.display = 'none'; + } + } + } +} + +// Migrate sync data from the default profile to the dev-edition profile. +// Returns a promise of a true value if migration succeeded, or false if it +// failed. +function migrateToDevEdition(urlParams) { + let defaultProfilePath; + try { + defaultProfilePath = window.getDefaultProfilePath(); + } catch (e) {} // no default profile. + let migrateSyncCreds = false; + if (defaultProfilePath) { + try { + migrateSyncCreds = Services.prefs.getBoolPref("identity.fxaccounts.migrateToDevEdition"); + } catch (e) {} + } + + if (!migrateSyncCreds) { + return Promise.resolve(false); + } + + Cu.import("resource://gre/modules/osfile.jsm"); + let fxAccountsStorage = OS.Path.join(defaultProfilePath, fxAccountsCommon.DEFAULT_STORAGE_FILENAME); + return OS.File.read(fxAccountsStorage, { encoding: "utf-8" }).then(text => { + let accountData = JSON.parse(text).accountData; + updateDisplayedEmail(accountData); + return fxAccounts.setSignedInUser(accountData); + }).then(() => { + return fxAccounts.promiseAccountsForceSigninURI().then(url => { + show("remote"); + wrapper.init(url, urlParams); + }); + }).then(null, error => { + log("Failed to migrate FX Account: " + error); + show("stage", "intro"); + // load the remote frame in the background + fxAccounts.promiseAccountsSignUpURI().then(uri => { + wrapper.init(uri, urlParams) + }).catch(e => { + console.log("Failed to load signup page", e); + setErrorPage("configError"); + }); + }).then(() => { + // Reset the pref after migration. + Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false); + return true; + }).then(null, err => { + Cu.reportError("Failed to reset the migrateToDevEdition pref: " + err); + return false; + }); +} + +// Helper function that returns the path of the default profile on disk. Will be +// overridden in tests. +function getDefaultProfilePath() { + let defaultProfile = Cc["@mozilla.org/toolkit/profile-service;1"] + .getService(Ci.nsIToolkitProfileService) + .defaultProfile; + return defaultProfile.rootDir.path; +} + +document.addEventListener("DOMContentLoaded", function onload() { + document.removeEventListener("DOMContentLoaded", onload, true); + init(); + var buttonGetStarted = document.getElementById('buttonGetStarted'); + buttonGetStarted.addEventListener('click', getStarted); + + var buttonRetry = document.getElementById('buttonRetry'); + buttonRetry.addEventListener('click', retry); + + var oldsync = document.getElementById('oldsync'); + oldsync.addEventListener('click', handleOldSync); + + var buttonOpenPrefs = document.getElementById('buttonOpenPrefs') + buttonOpenPrefs.addEventListener('click', openPrefs); +}, true); + +function initObservers() { + function observe(subject, topic, data) { + log("about:accounts observed " + topic); + if (topic == fxAccountsCommon.ONLOGOUT_NOTIFICATION) { + // All about:account windows get changed to action=signin on logout. + window.location = "about:accounts?action=signin"; + return; + } + + // must be onverified - we want to open preferences. + openPrefs(); + } + + for (let topic of OBSERVER_TOPICS) { + Services.obs.addObserver(observe, topic, false); + } + window.addEventListener("unload", function(event) { + log("about:accounts unloading") + for (let topic of OBSERVER_TOPICS) { + Services.obs.removeObserver(observe, topic); + } + }); +} +initObservers(); diff --git a/application/basilisk/base/content/aboutaccounts/aboutaccounts.xhtml b/application/basilisk/base/content/aboutaccounts/aboutaccounts.xhtml new file mode 100644 index 000000000..475f0e86f --- /dev/null +++ b/application/basilisk/base/content/aboutaccounts/aboutaccounts.xhtml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + %brandDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % aboutAccountsDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd"> + %aboutAccountsDTD; + <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> + %syncBrandDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml" dir="&locale.dir;"> + <head> + <title>&syncBrand.fullName.label;</title> + <meta name="viewport" content="width=device-width"/> + + + <link rel="icon" type="image/png" id="favicon" + href="chrome://branding/content/icon32.png"/> + <link rel="stylesheet" + href="chrome://browser/content/aboutaccounts/normalize.css" + type="text/css" /> + <link rel="stylesheet" + href="chrome://browser/content/aboutaccounts/main.css" + type="text/css" /> + <link rel="stylesheet" + href="chrome://browser/content/aboutaccounts/aboutaccounts.css" + type="text/css" /> + </head> + <body> + <div id="stage"> + + <div id="manage"> + <header> + <h1>&aboutAccounts.connected;</h1> + <div id="email"></div> + </header> + + <section> + <div class="graphic graphic-sync-intro"> </div> + + <div class="button-row"> + <button id="buttonOpenPrefs" class="button" href="#" tabindex="0">&aboutAccountsConfig.syncPreferences.label;</button> + </div> + </section> + </div> + + <div id="intro"> + <header> + <h1>&aboutAccounts.welcome;</h1> + </header> + + <section> + <div class="graphic graphic-sync-intro"> </div> + + <div class="description">&aboutAccountsConfig.description;</div> + + <div class="button-row"> + <button id="buttonGetStarted" class="button" tabindex="1">&aboutAccountsConfig.startButton.label;</button> + </div> + + <div class="links"> + <button id="oldsync" tabindex="2">&aboutAccountsConfig.useOldSync.label;</button> + </div> + </section> + </div> + + <div id="networkError"> + <header> + <h1>&aboutAccounts.noConnection.title;</h1> + </header> + + <section> + <div class="graphic graphic-sync-intro"> </div> + + <div class="description">&aboutAccounts.noConnection.description;</div> + + <div class="button-row"> + <button id="buttonRetry" class="button" tabindex="3">&aboutAccounts.noConnection.retry;</button> + </div> + </section> + </div> + + <div id="configError"> + <header> + <h1>&aboutAccounts.badConfig.title;</h1> + </header> + + <section> + <div class="graphic graphic-sync-intro"> </div> + + <div class="description">&aboutAccounts.badConfig.description;</div> + + </section> + </div> + + </div> + + <iframe mozframetype="content" id="remote" /> + + <script type="application/javascript;version=1.8" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="text/javascript;version=1.8" + src="chrome://browser/content/aboutaccounts/aboutaccounts.js" /> + </body> +</html> diff --git a/application/basilisk/base/content/aboutaccounts/images/fox.png b/application/basilisk/base/content/aboutaccounts/images/fox.png Binary files differnew file mode 100644 index 000000000..83af78d6c --- /dev/null +++ b/application/basilisk/base/content/aboutaccounts/images/fox.png diff --git a/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro.png b/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro.png Binary files differnew file mode 100644 index 000000000..ff5f482f0 --- /dev/null +++ b/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro.png diff --git a/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro@2x.png b/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro@2x.png Binary files differnew file mode 100644 index 000000000..89fda0681 --- /dev/null +++ b/application/basilisk/base/content/aboutaccounts/images/graphic_sync_intro@2x.png diff --git a/application/basilisk/base/content/aboutaccounts/main.css b/application/basilisk/base/content/aboutaccounts/main.css new file mode 100644 index 000000000..8f4c3b34e --- /dev/null +++ b/application/basilisk/base/content/aboutaccounts/main.css @@ -0,0 +1,166 @@ +*, +*:before, +*:after { + box-sizing: border-box; +} + +html { + background-color: #F2F2F2; + height: 100%; +} + +body { + color: #424f59; + font: message-box; + font-size: 14px; + height: 100%; +} + +a { + color: #0095dd; + cursor: pointer; /* Use the correct cursor for anchors without an href */ +} + +a:active { + outline: none; +} + +a:focus { + outline: 1px dotted #0095dd; +} + + +a.no-underline { + text-decoration: none; +} + +#stage { + background:#fff; + border-radius: 5px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.25); + margin: 0 auto; + min-height: 300px; + padding: 60px 40px 40px 40px; + position: relative; + text-align: center; + top: 80px; + width: 420px; +} + +header h1 +{ + font-size: 24px; + font-weight: 200; + line-height: 1em; +} + +#intro header h1 { + margin: 0 0 32px 0; +} + +#manage header h1 { + margin: 0 0 12px 0; +} + +#manage header #email { + margin-bottom: 23px; + color: rgb(138, 155, 168); + font-size: 19px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.description { + font-size: 18px; +} + +.button-row { + margin-top: 45px; + margin-bottom:20px; +} + +.button-row button, +.button-row a.button { + background: #0095dd; + border: none; + border-radius: 5px; + color: #FFFFFF; + cursor: pointer; + font-size: 24px; + padding: 15px 0; + transition-duration: 150ms; + transition-property: background-color; + width: 100%; +} + +.button-row a.button { + display: inline-block; + text-decoration: none; +} + +.button-row a.button:active, +.button-row a.button:hover, +.button-row a.button:focus, +.button-row button:active, +.button-row button:hover, +.button-row button:focus { + background: #08c; +} + + +.graphic-sync-intro { + background-image: url(images/graphic_sync_intro.png); + background-repeat: no-repeat; + background-size: 150px 195px; + height: 195px; + margin: 0 auto; + overflow: hidden; + text-indent: 100%; + white-space: nowrap; + width: 150px; +} + +.description, +.button-row { + margin-top: 30px; +} + +.links { + margin: 20px 0; +} + +@media only screen and (max-width: 500px) { + html { + background: #fff; + } + + #stage { + box-shadow: none; + margin: 30px auto 0 auto; + min-height: none; + min-width: 320px; + padding: 0 10px; + width: 100%; + } + + .button-row { + margin-top: 20px; + } + + .button-row button, + .button-row a.button { + padding: 10px 0; + } + +} + +/* Retina */ +@media +only screen and (min-device-pixel-ratio: 2), +only screen and ( min-resolution: 192dpi), +only screen and ( min-resolution: 2dppx) { + .graphic-sync-intro { + background-image: url(images/graphic_sync_intro@2x.png); + } +} diff --git a/application/basilisk/base/content/aboutaccounts/normalize.css b/application/basilisk/base/content/aboutaccounts/normalize.css new file mode 100644 index 000000000..c02ab25de --- /dev/null +++ b/application/basilisk/base/content/aboutaccounts/normalize.css @@ -0,0 +1,402 @@ +/*! normalize.css v2.1.3 | MIT License | git.io/normalize */
+
+/* ==========================================================================
+ HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined in IE 8/9.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * Correct `inline-block` display not defined in IE 8/9.
+ */
+
+audio,
+canvas,
+video {
+ display: inline-block;
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9.
+ * Hide the `template` element in IE, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* ==========================================================================
+ Base
+ ========================================================================== */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS text size adjust after orientation change, without disabling
+ * user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background: transparent;
+}
+
+/**
+ * Address `outline` inconsistency between Chrome and other browsers.
+ */
+
+a:focus {
+ outline: thin dotted;
+}
+
+/**
+ * Improve readability when focused and also mouse hovered in all browsers.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* ==========================================================================
+ Typography
+ ========================================================================== */
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari 5, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9, Safari 5, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari 5 and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Correct font family set oddly in Safari 5 and Chrome.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, serif;
+ font-size: 1em;
+}
+
+/**
+ * Improve readability of pre-formatted text in all browsers.
+ */
+
+pre {
+ white-space: pre-wrap;
+}
+
+/**
+ * Set consistent quote types.
+ */
+
+q {
+ quotes: "\201C" "\201D" "\2018" "\2019";
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* ==========================================================================
+ Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow displayed oddly in IE 9.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* ==========================================================================
+ Figures
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari 5.
+ */
+
+figure {
+ margin: 0;
+}
+
+/* ==========================================================================
+ Forms
+ ========================================================================== */
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * 1. Correct font family not being inherited in all browsers.
+ * 2. Correct font size not being inherited in all browsers.
+ * 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome.
+ */
+
+button,
+input,
+select,
+textarea {
+ font-family: inherit; /* 1 */
+ font-size: 100%; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+button,
+input {
+ line-height: normal;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+.
+ * Correct `select` style inheritance in Firefox 4+ and Opera.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome.
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ box-sizing: content-box; /* 2 */
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari 5 and Chrome
+ * on OS X.
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * 1. Remove default vertical scrollbar in IE 8/9.
+ * 2. Improve readability and alignment in all browsers.
+ */
+
+textarea {
+ overflow: auto; /* 1 */
+ vertical-align: top; /* 2 */
+}
+
+/* ==========================================================================
+ Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/application/basilisk/base/content/abouthealthreport/abouthealth.css b/application/basilisk/base/content/abouthealthreport/abouthealth.css new file mode 100644 index 000000000..3dd40fc24 --- /dev/null +++ b/application/basilisk/base/content/abouthealthreport/abouthealth.css @@ -0,0 +1,15 @@ +* { + margin: 0; + padding: 0; +} + +html, body { + height: 100%; +} + +#remote-report { + width: 100%; + height: 100%; + border: 0; + display: flex; +} diff --git a/application/basilisk/base/content/abouthealthreport/abouthealth.js b/application/basilisk/base/content/abouthealthreport/abouthealth.js new file mode 100644 index 000000000..66cbe16f5 --- /dev/null +++ b/application/basilisk/base/content/abouthealthreport/abouthealth.js @@ -0,0 +1,180 @@ +/* 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/. */ + +"use strict"; + +var {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const prefs = new Preferences("datareporting.healthreport."); + +const PREF_UNIFIED = "toolkit.telemetry.unified"; +const PREF_REPORTING_URL = "datareporting.healthreport.about.reportUrl"; + +var healthReportWrapper = { + init: function () { + let iframe = document.getElementById("remote-report"); + iframe.addEventListener("load", healthReportWrapper.initRemotePage, false); + iframe.src = this._getReportURI().spec; + prefs.observe("uploadEnabled", this.updatePrefState, healthReportWrapper); + }, + + uninit: function () { + prefs.ignore("uploadEnabled", this.updatePrefState, healthReportWrapper); + }, + + _getReportURI: function () { + let url = Services.urlFormatter.formatURLPref(PREF_REPORTING_URL); + return Services.io.newURI(url, null, null); + }, + + setDataSubmission: function (enabled) { + MozSelfSupport.healthReportDataSubmissionEnabled = enabled; + this.updatePrefState(); + }, + + updatePrefState: function () { + try { + let prefs = { + enabled: MozSelfSupport.healthReportDataSubmissionEnabled, + }; + healthReportWrapper.injectData("prefs", prefs); + } + catch (ex) { + healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PREFS_FAILED); + } + }, + + sendTelemetryPingList: function () { + console.log("AboutHealthReport: Collecting Telemetry ping list."); + MozSelfSupport.getTelemetryPingList().then((list) => { + console.log("AboutHealthReport: Sending Telemetry ping list."); + this.injectData("telemetry-ping-list", list); + }).catch((ex) => { + console.log("AboutHealthReport: Collecting ping list failed: " + ex); + }); + }, + + sendTelemetryPingData: function (pingId) { + console.log("AboutHealthReport: Collecting Telemetry ping data."); + MozSelfSupport.getTelemetryPing(pingId).then((ping) => { + console.log("AboutHealthReport: Sending Telemetry ping data."); + this.injectData("telemetry-ping-data", { + id: pingId, + pingData: ping, + }); + }).catch((ex) => { + console.log("AboutHealthReport: Loading ping data failed: " + ex); + this.injectData("telemetry-ping-data", { + id: pingId, + error: "error-generic", + }); + }); + }, + + sendCurrentEnvironment: function () { + console.log("AboutHealthReport: Sending Telemetry environment data."); + MozSelfSupport.getCurrentTelemetryEnvironment().then((environment) => { + this.injectData("telemetry-current-environment-data", environment); + }).catch((ex) => { + console.log("AboutHealthReport: Collecting current environment data failed: " + ex); + }); + }, + + sendCurrentPingData: function () { + console.log("AboutHealthReport: Sending current Telemetry ping data."); + MozSelfSupport.getCurrentTelemetrySubsessionPing().then((ping) => { + this.injectData("telemetry-current-ping-data", ping); + }).catch((ex) => { + console.log("AboutHealthReport: Collecting current ping data failed: " + ex); + }); + }, + + injectData: function (type, content) { + let report = this._getReportURI(); + + // file URIs can't be used for targetOrigin, so we use "*" for this special case + // in all other cases, pass in the URL to the report so we properly restrict the message dispatch + let reportUrl = report.scheme == "file" ? "*" : report.spec; + + let data = { + type: type, + content: content + } + + let iframe = document.getElementById("remote-report"); + iframe.contentWindow.postMessage(data, reportUrl); + }, + + handleRemoteCommand: function (evt) { + // Do an origin check to harden against the frame content being loaded from unexpected locations. + let allowedPrincipal = Services.scriptSecurityManager.getCodebasePrincipal(this._getReportURI()); + let targetPrincipal = evt.target.nodePrincipal; + if (!allowedPrincipal.equals(targetPrincipal)) { + Cu.reportError(`Origin check failed for message "${evt.detail.command}": ` + + `target origin is "${targetPrincipal.origin}", expected "${allowedPrincipal.origin}"`); + return; + } + + switch (evt.detail.command) { + case "DisableDataSubmission": + this.setDataSubmission(false); + break; + case "EnableDataSubmission": + this.setDataSubmission(true); + break; + case "RequestCurrentPrefs": + this.updatePrefState(); + break; + case "RequestTelemetryPingList": + this.sendTelemetryPingList(); + break; + case "RequestTelemetryPingData": + this.sendTelemetryPingData(evt.detail.id); + break; + case "RequestCurrentEnvironment": + this.sendCurrentEnvironment(); + break; + case "RequestCurrentPingData": + this.sendCurrentPingData(); + break; + default: + Cu.reportError("Unexpected remote command received: " + evt.detail.command + ". Ignoring command."); + break; + } + }, + + initRemotePage: function () { + let iframe = document.getElementById("remote-report").contentDocument; + iframe.addEventListener("RemoteHealthReportCommand", + function onCommand(e) { healthReportWrapper.handleRemoteCommand(e); }, + false); + healthReportWrapper.updatePrefState(); + }, + + // error handling + ERROR_INIT_FAILED: 1, + ERROR_PAYLOAD_FAILED: 2, + ERROR_PREFS_FAILED: 3, + + reportFailure: function (error) { + let details = { + errorType: error, + } + healthReportWrapper.injectData("error", details); + }, + + handleInitFailure: function () { + healthReportWrapper.reportFailure(healthReportWrapper.ERROR_INIT_FAILED); + }, + + handlePayloadFailure: function () { + healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PAYLOAD_FAILED); + }, +} + +window.addEventListener("load", function () { healthReportWrapper.init(); }); +window.addEventListener("unload", function () { healthReportWrapper.uninit(); }); diff --git a/application/basilisk/base/content/abouthealthreport/abouthealth.xhtml b/application/basilisk/base/content/abouthealthreport/abouthealth.xhtml new file mode 100644 index 000000000..464635788 --- /dev/null +++ b/application/basilisk/base/content/abouthealthreport/abouthealth.xhtml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + %brandDTD; + <!ENTITY % securityPrefsDTD SYSTEM "chrome://browser/locale/preferences/security.dtd"> + %securityPrefsDTD; + <!ENTITY % aboutHealthReportDTD SYSTEM "chrome://browser/locale/aboutHealthReport.dtd"> + %aboutHealthReportDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&abouthealth.pagetitle;</title> + <link rel="icon" type="image/png" id="favicon" + href="chrome://branding/content/icon32.png"/> + <link rel="stylesheet" + href="chrome://browser/content/abouthealthreport/abouthealth.css" + type="text/css" /> + <script type="text/javascript;version=1.8" + src="chrome://browser/content/abouthealthreport/abouthealth.js" /> + </head> + <body> + <iframe id="remote-report"/> + </body> +</html> + diff --git a/application/basilisk/base/content/abouthome/aboutHome.css b/application/basilisk/base/content/abouthome/aboutHome.css new file mode 100644 index 000000000..bc3f9882c --- /dev/null +++ b/application/basilisk/base/content/abouthome/aboutHome.css @@ -0,0 +1,397 @@ +%if 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +%endif + +html { + font: message-box; + font-size: 100%; + background-color: hsl(0,0%,95%); + color: #000; + height: 100%; +} + +body { + margin: 0; + display: -moz-box; + -moz-box-orient: vertical; + width: 100%; + height: 100%; +} + +input, +button { + font-size: inherit; + font-family: inherit; +} + +a { + color: -moz-nativehyperlinktext; + text-decoration: none; +} + +.spacer { + -moz-box-flex: 1; +} + +#topSection { + text-align: center; +} + +#brandLogo { + height: 192px; + width: 192px; + margin: 22px auto 31px; + background-image: url("chrome://branding/content/about-logo.png"); + background-size: 192px auto; + background-position: center center; + background-repeat: no-repeat; +} + +#searchIconAndTextContainer { + width: 470px; +} + +#searchIconAndTextContainer { + display: -moz-box; + height: 36px; + position: relative; +} + +#searchIcon { + border: 1px transparent; + padding: 0; + margin: 0; + width: 36px; + height: 36px; + background: url("chrome://browser/skin/search-indicator-magnifying-glass.svg") center center no-repeat; + position: absolute; +} + +#searchText { + margin-left: 0; + -moz-box-flex: 1; + padding-top: 6px; + padding-bottom: 6px; + padding-inline-start: 34px; + padding-inline-end: 8px; + background: hsla(0,0%,100%,.9) padding-box; + border: 1px solid; + border-radius: 2px 0 0 2px; + border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2); + box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset, + 0 0 2px hsla(210,65%,9%,.1) inset, + 0 1px 0 hsla(0,0%,100%,.2); + color: inherit; + unicode-bidi: plaintext; +} + +#searchText:dir(rtl) { + border-radius: 0 2px 2px 0; +} + +#searchText[aria-expanded="true"] { + border-radius: 2px 0 0 0; +} + +#searchText[aria-expanded="true"]:dir(rtl) { + border-radius: 0 2px 0 0; +} + +#searchText[keepfocus], +#searchText:focus, +#searchText[autofocus] { + border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6); +} + +#searchSubmit { + margin-inline-start: -1px; + color: transparent; + background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go") center center no-repeat, linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box; + padding: 0; + border: 1px solid; + border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2); + border-radius: 0 2px 2px 0; + border-inline-start: 1px solid transparent; + box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset, + 0 1px 0 hsla(0,0%,100%,.2); + cursor: pointer; + transition-property: background-color, border-color, box-shadow; + transition-duration: 150ms; + width: 50px; +} + +#searchSubmit:dir(rtl) { + border-radius: 2px 0 0 2px; + background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl"), linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)); +} + +#searchText:focus + #searchSubmit, +#searchText[keepfocus] + #searchSubmit, +#searchText + #searchSubmit:hover, +#searchText[autofocus] + #searchSubmit { + border-color: #59b5fc #45a3e7 #3294d5; +} + +#searchText:focus + #searchSubmit, +#searchText[keepfocus] + #searchSubmit, +#searchText[autofocus] + #searchSubmit { + background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#4cb1ff, #1793e5); + box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset, + 0 0 0 1px hsla(0,0%,100%,.1) inset, + 0 1px 0 hsla(210,54%,20%,.03); +} + +#searchText:focus + #searchSubmit:dir(rtl), +#searchText[keepfocus] + #searchSubmit:dir(rtl), +#searchText[autofocus] + #searchSubmit:dir(rtl) { + background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl-inverted"), linear-gradient(#4cb1ff, #1793e5); +} + +#searchText + #searchSubmit:hover { + background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#66bdff, #0d9eff); + box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset, + 0 0 0 1px hsla(0,0%,100%,.1) inset, + 0 1px 0 hsla(210,54%,20%,.03), + 0 0 4px hsla(206,100%,20%,.2); +} + +#searchText + #searchSubmit:dir(rtl):hover { + background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl-inverted"), linear-gradient(#66bdff, #0d9eff); +} + +#searchText + #searchSubmit:hover:active { + box-shadow: 0 1px 1px hsla(211,79%,6%,.1) inset, + 0 0 1px hsla(211,79%,6%,.2) inset; + transition-duration: 0ms; +} + +#launcher { + display: -moz-box; + -moz-box-align: center; + -moz-box-pack: center; + width: 100%; + background-color: hsla(0,0%,0%,.03); + border-top: 1px solid hsla(0,0%,0%,.03); + box-shadow: 0 1px 2px hsla(0,0%,0%,.02) inset, + 0 -1px 0 hsla(0,0%,100%,.25); +} + +#launcher:not([session]), +body[narrow] #launcher[session] { + display: block; /* display separator and restore button on separate lines */ + text-align: center; + white-space: nowrap; /* prevent navigational buttons from wrapping */ +} + +.launchButton { + display: -moz-box; + -moz-box-orient: vertical; + margin: 16px 1px; + padding: 14px 6px; + min-width: 88px; + max-width: 176px; + max-height: 85px; + vertical-align: top; + white-space: normal; + background: transparent padding-box; + border: 1px solid transparent; + border-radius: 2px; + color: #525c66; + font-size: 75%; + cursor: pointer; + transition-property: background-color, border-color, box-shadow; + transition-duration: 150ms; +} + +body[narrow] #launcher[session] > .launchButton { + margin: 4px 1px; +} + +.launchButton:hover { + background-color: hsla(211,79%,6%,.03); + border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2); +} + +.launchButton:hover:active { + background-image: linear-gradient(hsla(211,79%,6%,.02), hsla(211,79%,6%,.05)); + border-color: hsla(210,54%,20%,.2) hsla(210,54%,20%,.23) hsla(210,54%,20%,.25); + box-shadow: 0 1px 1px hsla(211,79%,6%,.05) inset, + 0 0 1px hsla(211,79%,6%,.1) inset; + transition-duration: 0ms; +} + +.launchButton[hidden], +#launcher:not([session]) > #restorePreviousSessionSeparator, +#launcher:not([session]) > #restorePreviousSession { + display: none; +} + +#restorePreviousSessionSeparator { + width: 3px; + height: 116px; + margin: 0 10px; + background-image: linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)), + linear-gradient(hsla(211,79%,6%,0), hsla(211,79%,6%,.2), hsla(211,79%,6%,0)), + linear-gradient(hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)); + background-position: left top, center, right bottom; + background-size: 1px auto; + background-repeat: no-repeat; +} + +body[narrow] #restorePreviousSessionSeparator { + margin: 0 auto; + width: 512px; + height: 3px; + background-image: linear-gradient(to right, hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)), + linear-gradient(to right, hsla(211,79%,6%,0), hsla(211,79%,6%,.2), hsla(211,79%,6%,0)), + linear-gradient(to right, hsla(0,0%,100%,0), hsla(0,0%,100%,.35), hsla(0,0%,100%,0)); + background-size: auto 1px; +} + +#restorePreviousSession { + max-width: none; + font-size: 90%; +} + +body[narrow] #restorePreviousSession { + font-size: 80%; +} + +.launchButton::before { + display: block; + width: 32px; + height: 32px; + margin: 0 auto 6px; + line-height: 0; /* remove extra vertical space due to non-zero font-size */ +} + +#downloads::before { + content: url("chrome://browser/content/abouthome/downloads.png"); +} + +#bookmarks::before { + content: url("chrome://browser/content/abouthome/bookmarks.png"); +} + +#history::before { + content: url("chrome://browser/content/abouthome/history.png"); +} + +#addons::before { + content: url("chrome://browser/content/abouthome/addons.png"); +} + +#sync::before { + content: url("chrome://browser/content/abouthome/sync.png"); +} + +#settings::before { + content: url("chrome://browser/content/abouthome/settings.png"); +} + +#restorePreviousSession::before { + content: url("chrome://browser/content/abouthome/restore-large.png"); + height: 48px; + width: 48px; + display: inline-block; /* display on same line as text label */ + vertical-align: middle; + margin-bottom: 0; + margin-inline-end: 8px; +} + +#restorePreviousSession:dir(rtl)::before { + transform: scaleX(-1); +} + +body[narrow] #restorePreviousSession::before { + content: url("chrome://browser/content/abouthome/restore.png"); + height: 32px; + width: 32px; +} + +#aboutMozilla { + display: block; + position: relative; /* pin wordmark to edge of document, not of viewport */ + -moz-box-ordinal-group: 0; + opacity: .5; + transition: opacity 150ms; +} + +#aboutMozilla:hover { + opacity: 1; +} + +#aboutMozilla::before { + content: url("chrome://browser/content/abouthome/mozilla.png"); + display: block; + position: absolute; + top: 12px; + right: 12px; + width: 69px; + height: 19px; +} + +/* [HiDPI] + * At resolutions above 1dppx, prefer downscaling the 2x Retina graphics + * rather than upscaling the original-size ones (bug 818940). + */ +@media not all and (max-resolution: 1dppx) { + #brandLogo { + background-image: url("chrome://branding/content/about-logo@2x.png"); + } + + .launchButton::before, + #aboutMozilla::before { + transform: scale(.5); + transform-origin: 0 0; + } + + .launchButton:dir(rtl)::before, + #aboutMozilla:dir(rtl)::before { + transform: scale(.5) translateX(32px); + } + + #downloads::before { + content: url("chrome://browser/content/abouthome/downloads@2x.png"); + } + + #bookmarks::before { + content: url("chrome://browser/content/abouthome/bookmarks@2x.png"); + } + + #history::before { + content: url("chrome://browser/content/abouthome/history@2x.png"); + } + + #addons::before { + content: url("chrome://browser/content/abouthome/addons@2x.png"); + } + + #sync::before { + content: url("chrome://browser/content/abouthome/sync@2x.png"); + } + + #settings::before { + content: url("chrome://browser/content/abouthome/settings@2x.png"); + } + + #restorePreviousSession::before { + content: url("chrome://browser/content/abouthome/restore-large@2x.png"); + } + + body[narrow] #restorePreviousSession::before { + content: url("chrome://browser/content/abouthome/restore@2x.png"); + } + + #restorePreviousSession:dir(rtl)::before { + transform: scale(-0.5, 0.5) translateX(24px); + transform-origin: top center; + } + + #aboutMozilla::before { + content: url("chrome://browser/content/abouthome/mozilla@2x.png"); + } +} + diff --git a/application/basilisk/base/content/abouthome/aboutHome.js b/application/basilisk/base/content/abouthome/aboutHome.js new file mode 100644 index 000000000..0cbcc835a --- /dev/null +++ b/application/basilisk/base/content/abouthome/aboutHome.js @@ -0,0 +1,118 @@ +/* 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/. */ + +"use strict"; + +/* import-globals-from ../contentSearchUI.js */ + +// IndexedDB storage constants. +const DATABASE_NAME = "abouthome"; +const DATABASE_VERSION = 1; +const DATABASE_STORAGE = "persistent"; +var searchText; + +// This global tracks if the page has been set up before, to prevent double inits +var gInitialized = false; +var gObserver = new MutationObserver(function (mutations) { + for (let mutation of mutations) { + // The addition of the restore session button changes our width: + if (mutation.attributeName == "session") { + fitToWidth(); + } + } +}); + +window.addEventListener("pageshow", function () { + // Delay search engine setup, cause browser.js::BrowserOnAboutPageLoad runs + // later and may use asynchronous getters. + window.gObserver.observe(document.documentElement, { attributes: true }); + window.gObserver.observe(document.getElementById("launcher"), { attributes: true }); + fitToWidth(); + setupSearch(); + window.addEventListener("resize", fitToWidth); + + // Ask chrome to update snippets. + var event = new CustomEvent("AboutHomeLoad", {bubbles:true}); + document.dispatchEvent(event); +}); + +window.addEventListener("pagehide", function() { + window.gObserver.disconnect(); + window.removeEventListener("resize", fitToWidth); +}); + +window.addEventListener("keypress", ev => { + if (ev.defaultPrevented) { + return; + } + + // don't focus the search-box on keypress if something other than the + // body or document element has focus - don't want to steal input from other elements + // Make an exception for <a> and <button> elements (and input[type=button|submit]) + // which don't usefully take keypresses anyway. + // (except space, which is handled below) + if (document.activeElement && document.activeElement != document.body && + document.activeElement != document.documentElement && + !["a", "button"].includes(document.activeElement.localName) && + !document.activeElement.matches("input:-moz-any([type=button],[type=submit])")) { + return; + } + + let modifiers = ev.ctrlKey + ev.altKey + ev.metaKey; + // ignore Ctrl/Cmd/Alt, but not Shift + // also ignore Tab, Insert, PageUp, etc., and Space + if (modifiers != 0 || ev.charCode == 0 || ev.charCode == 32) + return; + + searchText.focus(); + // need to send the first keypress outside the search-box manually to it + searchText.value += ev.key; +}); + +function onSearchSubmit(aEvent) +{ + gContentSearchController.search(aEvent); +} + + +var gContentSearchController; + +function setupSearch() +{ + // Set submit button label for when CSS background are disabled (e.g. + // high contrast mode). + document.getElementById("searchSubmit").value = + document.body.getAttribute("dir") == "ltr" ? "\u25B6" : "\u25C0"; + + // The "autofocus" attribute doesn't focus the form element + // immediately when the element is first drawn, so the + // attribute is also used for styling when the page first loads. + searchText = document.getElementById("searchText"); + searchText.addEventListener("blur", function searchText_onBlur() { + searchText.removeEventListener("blur", searchText_onBlur); + searchText.removeAttribute("autofocus"); + }); + + if (!gContentSearchController) { + gContentSearchController = + new ContentSearchUIController(searchText, searchText.parentNode, + "abouthome", "homepage"); + } +} + +/** + * Inform the test harness that we're done loading the page. + */ +function loadCompleted() +{ +} + +function fitToWidth() { + if (document.documentElement.scrollWidth > window.innerWidth) { + document.body.setAttribute("narrow", "true"); + } else if (document.body.hasAttribute("narrow")) { + document.body.removeAttribute("narrow"); + fitToWidth(); + } +} diff --git a/application/basilisk/base/content/abouthome/aboutHome.xhtml b/application/basilisk/base/content/abouthome/aboutHome.xhtml new file mode 100644 index 000000000..22bf2e7e8 --- /dev/null +++ b/application/basilisk/base/content/abouthome/aboutHome.xhtml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd"> + %aboutHomeDTD; + <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" > + %browserDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&abouthome.pageTitle;</title> + + <link rel="icon" type="image/png" id="favicon" + href="chrome://branding/content/icon32.png"/> + <link rel="stylesheet" type="text/css" media="all" + href="chrome://browser/content/contentSearchUI.css"/> + <link rel="stylesheet" type="text/css" media="all" defer="defer" + href="chrome://browser/content/abouthome/aboutHome.css"/> + + <script type="text/javascript;version=1.8" + src="chrome://browser/content/abouthome/aboutHome.js"/> + <script type="text/javascript;version=1.8" + src="chrome://browser/content/contentSearchUI.js"/> + </head> + + <body dir="&locale.dir;"> + <div class="spacer"/> + <div id="topSection"> + <div id="brandLogo"></div> + + <div id="searchIconAndTextContainer"> + <div id="searchIcon"/> + <input type="text" name="q" value="" id="searchText" maxlength="256" + aria-label="&contentSearchInput.label;" autofocus="autofocus"/> + <input id="searchSubmit" type="button" onclick="onSearchSubmit(event)" + title="&contentSearchSubmit.tooltip;"/> + </div> + </div> + <div class="spacer"/> + + <div id="launcher"> + <button class="launchButton" id="downloads">&abouthome.downloadsButton.label;</button> + <button class="launchButton" id="bookmarks">&abouthome.bookmarksButton.label;</button> + <button class="launchButton" id="history">&abouthome.historyButton.label;</button> + <button class="launchButton" id="addons">&abouthome.addonsButton.label;</button> + <button class="launchButton" id="sync">&abouthome.syncButton.label;</button> +#ifdef XP_WIN + <button class="launchButton" id="settings">&abouthome.preferencesButtonWin.label;</button> +#else + <button class="launchButton" id="settings">&abouthome.preferencesButtonUnix.label;</button> +#endif + <div id="restorePreviousSessionSeparator"/> + <button class="launchButton" id="restorePreviousSession">&historyRestoreLastSession.label;</button> + </div> + + </body> +</html> diff --git a/application/basilisk/base/content/abouthome/addons.png b/application/basilisk/base/content/abouthome/addons.png Binary files differnew file mode 100644 index 000000000..41519ce49 --- /dev/null +++ b/application/basilisk/base/content/abouthome/addons.png diff --git a/application/basilisk/base/content/abouthome/addons@2x.png b/application/basilisk/base/content/abouthome/addons@2x.png Binary files differnew file mode 100644 index 000000000..d4d04ee8c --- /dev/null +++ b/application/basilisk/base/content/abouthome/addons@2x.png diff --git a/application/basilisk/base/content/abouthome/bookmarks.png b/application/basilisk/base/content/abouthome/bookmarks.png Binary files differnew file mode 100644 index 000000000..5c7e194a6 --- /dev/null +++ b/application/basilisk/base/content/abouthome/bookmarks.png diff --git a/application/basilisk/base/content/abouthome/bookmarks@2x.png b/application/basilisk/base/content/abouthome/bookmarks@2x.png Binary files differnew file mode 100644 index 000000000..7ede00744 --- /dev/null +++ b/application/basilisk/base/content/abouthome/bookmarks@2x.png diff --git a/application/basilisk/base/content/abouthome/downloads.png b/application/basilisk/base/content/abouthome/downloads.png Binary files differnew file mode 100644 index 000000000..3d4d10e7a --- /dev/null +++ b/application/basilisk/base/content/abouthome/downloads.png diff --git a/application/basilisk/base/content/abouthome/downloads@2x.png b/application/basilisk/base/content/abouthome/downloads@2x.png Binary files differnew file mode 100644 index 000000000..d384a22c6 --- /dev/null +++ b/application/basilisk/base/content/abouthome/downloads@2x.png diff --git a/application/basilisk/base/content/abouthome/history.png b/application/basilisk/base/content/abouthome/history.png Binary files differnew file mode 100644 index 000000000..ae742b1aa --- /dev/null +++ b/application/basilisk/base/content/abouthome/history.png diff --git a/application/basilisk/base/content/abouthome/history@2x.png b/application/basilisk/base/content/abouthome/history@2x.png Binary files differnew file mode 100644 index 000000000..696902e7c --- /dev/null +++ b/application/basilisk/base/content/abouthome/history@2x.png diff --git a/application/basilisk/base/content/abouthome/mozilla.png b/application/basilisk/base/content/abouthome/mozilla.png Binary files differnew file mode 100644 index 000000000..f2c348d13 --- /dev/null +++ b/application/basilisk/base/content/abouthome/mozilla.png diff --git a/application/basilisk/base/content/abouthome/mozilla@2x.png b/application/basilisk/base/content/abouthome/mozilla@2x.png Binary files differnew file mode 100644 index 000000000..f8fc622d0 --- /dev/null +++ b/application/basilisk/base/content/abouthome/mozilla@2x.png diff --git a/application/basilisk/base/content/abouthome/restore-large.png b/application/basilisk/base/content/abouthome/restore-large.png Binary files differnew file mode 100644 index 000000000..ef593e6e1 --- /dev/null +++ b/application/basilisk/base/content/abouthome/restore-large.png diff --git a/application/basilisk/base/content/abouthome/restore-large@2x.png b/application/basilisk/base/content/abouthome/restore-large@2x.png Binary files differnew file mode 100644 index 000000000..d5c71d0b0 --- /dev/null +++ b/application/basilisk/base/content/abouthome/restore-large@2x.png diff --git a/application/basilisk/base/content/abouthome/restore.png b/application/basilisk/base/content/abouthome/restore.png Binary files differnew file mode 100644 index 000000000..5c3d6f437 --- /dev/null +++ b/application/basilisk/base/content/abouthome/restore.png diff --git a/application/basilisk/base/content/abouthome/restore@2x.png b/application/basilisk/base/content/abouthome/restore@2x.png Binary files differnew file mode 100644 index 000000000..5acb63052 --- /dev/null +++ b/application/basilisk/base/content/abouthome/restore@2x.png diff --git a/application/basilisk/base/content/abouthome/settings.png b/application/basilisk/base/content/abouthome/settings.png Binary files differnew file mode 100644 index 000000000..4b0c30990 --- /dev/null +++ b/application/basilisk/base/content/abouthome/settings.png diff --git a/application/basilisk/base/content/abouthome/settings@2x.png b/application/basilisk/base/content/abouthome/settings@2x.png Binary files differnew file mode 100644 index 000000000..c77cb9a92 --- /dev/null +++ b/application/basilisk/base/content/abouthome/settings@2x.png diff --git a/application/basilisk/base/content/abouthome/snippet1.png b/application/basilisk/base/content/abouthome/snippet1.png Binary files differnew file mode 100644 index 000000000..ce2ec55c2 --- /dev/null +++ b/application/basilisk/base/content/abouthome/snippet1.png diff --git a/application/basilisk/base/content/abouthome/snippet1@2x.png b/application/basilisk/base/content/abouthome/snippet1@2x.png Binary files differnew file mode 100644 index 000000000..f57cd0a82 --- /dev/null +++ b/application/basilisk/base/content/abouthome/snippet1@2x.png diff --git a/application/basilisk/base/content/abouthome/snippet2.png b/application/basilisk/base/content/abouthome/snippet2.png Binary files differnew file mode 100644 index 000000000..e0724fb6d --- /dev/null +++ b/application/basilisk/base/content/abouthome/snippet2.png diff --git a/application/basilisk/base/content/abouthome/snippet2@2x.png b/application/basilisk/base/content/abouthome/snippet2@2x.png Binary files differnew file mode 100644 index 000000000..40577f52f --- /dev/null +++ b/application/basilisk/base/content/abouthome/snippet2@2x.png diff --git a/application/basilisk/base/content/abouthome/sync.png b/application/basilisk/base/content/abouthome/sync.png Binary files differnew file mode 100644 index 000000000..11e40cc93 --- /dev/null +++ b/application/basilisk/base/content/abouthome/sync.png diff --git a/application/basilisk/base/content/abouthome/sync@2x.png b/application/basilisk/base/content/abouthome/sync@2x.png Binary files differnew file mode 100644 index 000000000..6354f5bf9 --- /dev/null +++ b/application/basilisk/base/content/abouthome/sync@2x.png diff --git a/application/basilisk/base/content/baseMenuOverlay.xul b/application/basilisk/base/content/baseMenuOverlay.xul new file mode 100644 index 000000000..546103de1 --- /dev/null +++ b/application/basilisk/base/content/baseMenuOverlay.xul @@ -0,0 +1,114 @@ +<?xml version="1.0"?> + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<!DOCTYPE overlay [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % baseMenuOverlayDTD SYSTEM "chrome://browser/locale/baseMenuOverlay.dtd"> +%baseMenuOverlayDTD; +]> +<overlay id="baseMenuOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + +#ifdef XP_MACOSX +<!-- nsMenuBarX hides these and uses them to build the Application menu. + When using Carbon widgets for Mac OS X widgets, some of these are not + used as they only apply to Cocoa widget builds. All version of Firefox + through Firefox 2 will use Carbon widgets. --> + <menupopup id="menu_ToolsPopup"> + <menuitem id="menu_preferences" label="&preferencesCmdMac.label;" key="key_preferencesCmdMac" oncommand="openPreferences();"/> + <menuitem id="menu_mac_services" label="&servicesMenuMac.label;"/> + <menuitem id="menu_mac_hide_app" label="&hideThisAppCmdMac2.label;" key="key_hideThisAppCmdMac"/> + <menuitem id="menu_mac_hide_others" label="&hideOtherAppsCmdMac.label;" key="key_hideOtherAppsCmdMac"/> + <menuitem id="menu_mac_show_all" label="&showAllAppsCmdMac.label;"/> + </menupopup> +<!-- Mac window menu --> +#include ../../../toolkit/content/macWindowMenu.inc +#endif + +#ifdef XP_WIN + <menu id="helpMenu" + label="&helpMenuWin.label;" + accesskey="&helpMenuWin.accesskey;"> +#else + <menu id="helpMenu" + label="&helpMenu.label;" + accesskey="&helpMenu.accesskey;"> +#endif + <menupopup id="menu_HelpPopup" onpopupshowing="buildHelpMenu();"> + <menuitem id="menu_openHelp" + oncommand="openHelpLink('firefox-help')" + onclick="checkForMiddleClick(this, event);" + label="&productHelp2.label;" + accesskey="&productHelp2.accesskey;" +#ifdef XP_MACOSX + key="key_openHelpMac"/> +#else + /> +#endif + <menuitem id="menu_keyboardShortcuts" + oncommand="openHelpLink('keyboard-shortcuts')" + onclick="checkForMiddleClick(this, event);" + label="&helpKeyboardShortcuts.label;" + accesskey="&helpKeyboardShortcuts.accesskey;"/> +#ifdef MOZ_TELEMETRY_REPORTING + <menuitem id="healthReport" + label="&healthReport2.label;" + accesskey="&healthReport2.accesskey;" + oncommand="openHealthReport()" + onclick="checkForMiddleClick(this, event);"/> +#endif + <menuitem id="troubleShooting" + accesskey="&helpTroubleshootingInfo.accesskey;" + label="&helpTroubleshootingInfo.label;" + oncommand="openTroubleshootingPage()" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="feedbackPage" + accesskey="&helpFeedbackPage.accesskey;" + label="&helpFeedbackPage.label;" + oncommand="openFeedbackPage()" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="helpSafeMode" + accesskey="&helpSafeMode.accesskey;" + label="&helpSafeMode.label;" + stopaccesskey="&helpSafeMode.stop.accesskey;" + stoplabel="&helpSafeMode.stop.label;" + oncommand="safeModeRestart();"/> + <menuseparator id="aboutSeparator"/> + <menuitem id="aboutName" + accesskey="&aboutProduct2.accesskey;" + label="&aboutProduct2.label;" + oncommand="openAboutDialog();"/> + </menupopup> + </menu> + + <keyset id="baseMenuKeyset"> +#ifdef XP_MACOSX + <key id="key_openHelpMac" + oncommand="openHelpLink('firefox-osxkey');" + key="&helpMac.commandkey;" + modifiers="accel"/> +<!-- These are used to build the Application menu under Cocoa widgets --> + <key id="key_preferencesCmdMac" + key="&preferencesCmdMac.commandkey;" + modifiers="accel"/> + <key id="key_hideThisAppCmdMac" + key="&hideThisAppCmdMac2.commandkey;" + modifiers="accel"/> + <key id="key_hideOtherAppsCmdMac" + key="&hideOtherAppsCmdMac.commandkey;" + modifiers="accel,alt"/> +#endif + </keyset> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/> + <stringbundle id="bundle_browser_region" src="chrome://browser-region/locale/region.properties"/> + </stringbundleset> +</overlay> diff --git a/application/basilisk/base/content/blockedSite.xhtml b/application/basilisk/base/content/blockedSite.xhtml new file mode 100644 index 000000000..10a4b33e8 --- /dev/null +++ b/application/basilisk/base/content/blockedSite.xhtml @@ -0,0 +1,196 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > + %brandDTD; + <!ENTITY % blockedSiteDTD SYSTEM "chrome://browser/locale/safebrowsing/phishing-afterload-warning-message.dtd"> + %blockedSiteDTD; +]> + +<!-- 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/. --> + +<html xmlns="http://www.w3.org/1999/xhtml" class="blacklist"> + <head> + <link rel="stylesheet" href="chrome://browser/skin/blockedSite.css" type="text/css" media="all" /> + <link rel="icon" type="image/png" id="favicon" href="chrome://global/skin/icons/blacklist_favicon.png"/> + + <script type="application/javascript"><![CDATA[ + // Error url MUST be formatted like this: + // about:blocked?e=error_code&u=url(&o=1)? + // (o=1 when user overrides are allowed) + + // Note that this file uses document.documentURI to get + // the URL (with the format from above). This is because + // document.location.href gets the current URI off the docshell, + // which is the URL displayed in the location bar, i.e. + // the URI that the user attempted to load. + + function getErrorCode() + { + var url = document.documentURI; + var error = url.search(/e\=/); + var duffUrl = url.search(/\&u\=/); + return decodeURIComponent(url.slice(error + 2, duffUrl)); + } + + function getURL() + { + var url = document.documentURI; + var match = url.match(/&u=([^&]+)&/); + + // match == null if not found; if so, return an empty string + // instead of what would turn out to be portions of the URI + if (!match) + return ""; + + url = decodeURIComponent(match[1]); + + // If this is a view-source page, then get then real URI of the page + if (url.startsWith("view-source:")) + url = url.slice(12); + return url; + } + + /** + * Check whether this warning page should be overridable or whether + * the "ignore warning" button should be hidden. + */ + function getOverride() + { + var url = document.documentURI; + var match = url.match(/&o=1&/); + return !!match; + } + + /** + * Attempt to get the hostname via document.location. Fail back + * to getURL so that we always return something meaningful. + */ + function getHostString() + { + try { + return document.location.hostname; + } catch (e) { + return getURL(); + } + } + + function initPage() + { + var error = ""; + switch (getErrorCode()) { + case "malwareBlocked" : + error = "malware"; + break; + case "deceptiveBlocked" : + error = "phishing"; + break; + case "unwantedBlocked" : + error = "unwanted"; + break; + default: + return; + } + + var el; + + if (error !== "malware") { + el = document.getElementById("errorTitleText_malware"); + el.parentNode.removeChild(el); + el = document.getElementById("errorShortDescText_malware"); + el.parentNode.removeChild(el); + el = document.getElementById("errorLongDescText_malware"); + el.parentNode.removeChild(el); + } + + if (error !== "phishing") { + el = document.getElementById("errorTitleText_phishing"); + el.parentNode.removeChild(el); + el = document.getElementById("errorShortDescText_phishing"); + el.parentNode.removeChild(el); + el = document.getElementById("errorLongDescText_phishing"); + el.parentNode.removeChild(el); + } + + if (error !== "unwanted") { + el = document.getElementById("errorTitleText_unwanted"); + el.parentNode.removeChild(el); + el = document.getElementById("errorShortDescText_unwanted"); + el.parentNode.removeChild(el); + el = document.getElementById("errorLongDescText_unwanted"); + el.parentNode.removeChild(el); + } + + // Set sitename + document.getElementById(error + "_sitename").textContent = getHostString(); + document.title = document.getElementById("errorTitleText_" + error) + .innerHTML; + + if (!getOverride()) { + var btn = document.getElementById("ignoreWarningButton"); + if (btn) { + btn.parentNode.removeChild(btn); + } + } + + // Inform the test harness that we're done loading the page + var event = new CustomEvent("AboutBlockedLoaded"); + document.dispatchEvent(event); + } + ]]></script> + </head> + + <body dir="&locale.dir;"> + <div id="errorPageContainer" class="container"> + + <!-- Error Title --> + <div id="errorTitle" class="title"> + <h1 class="title-text" id="errorTitleText_phishing">&safeb.blocked.phishingPage.title2;</h1> + <h1 class="title-text" id="errorTitleText_malware">&safeb.blocked.malwarePage.title;</h1> + <h1 class="title-text" id="errorTitleText_unwanted">&safeb.blocked.unwantedPage.title;</h1> + </div> + + <div id="errorLongContent"> + + <!-- Short Description --> + <div id="errorShortDesc"> + <p id="errorShortDescText_phishing">&safeb.blocked.phishingPage.shortDesc2;</p> + <p id="errorShortDescText_malware">&safeb.blocked.malwarePage.shortDesc;</p> + <p id="errorShortDescText_unwanted">&safeb.blocked.unwantedPage.shortDesc;</p> + </div> + + <!-- Long Description --> + <div id="errorLongDesc"> + <p id="errorLongDescText_phishing">&safeb.blocked.phishingPage.longDesc2;</p> + <p id="errorLongDescText_malware">&safeb.blocked.malwarePage.longDesc;</p> + <p id="errorLongDescText_unwanted">&safeb.blocked.unwantedPage.longDesc;</p> + </div> + + <!-- Action buttons --> + <div id="buttons" class="button-container"> + <!-- Commands handled in browser.js --> + <button id="getMeOutButton" class="primary">&safeb.palm.accept.label;</button> + <div class="button-spacer"></div> + <button id="reportButton">&safeb.palm.reportPage.label;</button> + </div> + </div> + <div id="ignoreWarning"> + <button id="ignoreWarningButton">&safeb.palm.decline.label;</button> + </div> + </div> + <!-- + - Note: It is important to run the script this way, instead of using + - an onload handler. This is because error pages are loaded as + - LOAD_BACKGROUND, which means that onload handlers will not be executed. + --> + <script type="application/javascript"> + initPage(); + </script> + </body> +</html> diff --git a/application/basilisk/base/content/browser-addons.js b/application/basilisk/base/content/browser-addons.js new file mode 100644 index 000000000..1d881536a --- /dev/null +++ b/application/basilisk/base/content/browser-addons.js @@ -0,0 +1,742 @@ +/* -*- 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/. */ + +// Removes a doorhanger notification if all of the installs it was notifying +// about have ended in some way. +function removeNotificationOnEnd(notification, installs) { + let count = installs.length; + + function maybeRemove(install) { + install.removeListener(this); + + if (--count == 0) { + // Check that the notification is still showing + let current = PopupNotifications.getNotification(notification.id, notification.browser); + if (current === notification) + notification.remove(); + } + } + + for (let install of installs) { + install.addListener({ + onDownloadCancelled: maybeRemove, + onDownloadFailed: maybeRemove, + onInstallFailed: maybeRemove, + onInstallEnded: maybeRemove + }); + } +} + +const gXPInstallObserver = { + _findChildShell: function (aDocShell, aSoughtShell) + { + if (aDocShell == aSoughtShell) + return aDocShell; + + var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem); + for (var i = 0; i < node.childCount; ++i) { + var docShell = node.getChildAt(i); + docShell = this._findChildShell(docShell, aSoughtShell); + if (docShell == aSoughtShell) + return docShell; + } + return null; + }, + + _getBrowser: function (aDocShell) + { + for (let browser of gBrowser.browsers) { + if (this._findChildShell(browser.docShell, aDocShell)) + return browser; + } + return null; + }, + + pendingInstalls: new WeakMap(), + + showInstallConfirmation: function(browser, installInfo, height = undefined) { + // If the confirmation notification is already open cache the installInfo + // and the new confirmation will be shown later + if (PopupNotifications.getNotification("addon-install-confirmation", browser)) { + let pending = this.pendingInstalls.get(browser); + if (pending) { + pending.push(installInfo); + } else { + this.pendingInstalls.set(browser, [installInfo]); + } + return; + } + + let showNextConfirmation = () => { + // Make sure the browser is still alive. + if (gBrowser.browsers.indexOf(browser) == -1) + return; + + let pending = this.pendingInstalls.get(browser); + if (pending && pending.length) + this.showInstallConfirmation(browser, pending.shift()); + } + + // If all installs have already been cancelled in some way then just show + // the next confirmation + if (installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)) { + showNextConfirmation(); + return; + } + + const anchorID = "addons-notification-icon"; + + // Make notifications persist a minimum of 30 seconds + var options = { + displayURI: installInfo.originatingURI, + timeout: Date.now() + 30000, + }; + + let cancelInstallation = () => { + if (installInfo) { + for (let install of installInfo.installs) { + // The notification may have been closed because the add-ons got + // cancelled elsewhere, only try to cancel those that are still + // pending install. + if (install.state != AddonManager.STATE_CANCELLED) + install.cancel(); + } + } + + this.acceptInstallation = null; + + showNextConfirmation(); + }; + + let unsigned = installInfo.installs.filter(i => i.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING); + let someUnsigned = unsigned.length > 0 && unsigned.length < installInfo.installs.length; + + options.eventCallback = (aEvent) => { + switch (aEvent) { + case "removed": + cancelInstallation(); + break; + case "shown": + let addonList = document.getElementById("addon-install-confirmation-content"); + while (addonList.firstChild) + addonList.firstChild.remove(); + + for (let install of installInfo.installs) { + let container = document.createElement("hbox"); + + let name = document.createElement("label"); + name.setAttribute("value", install.addon.name); + name.setAttribute("class", "addon-install-confirmation-name"); + container.appendChild(name); + + if (someUnsigned && install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) { + let unsigned = document.createElement("label"); + unsigned.setAttribute("value", gNavigatorBundle.getString("addonInstall.unsigned")); + unsigned.setAttribute("class", "addon-install-confirmation-unsigned"); + container.appendChild(unsigned); + } + + addonList.appendChild(container); + } + + this.acceptInstallation = () => { + for (let install of installInfo.installs) + install.install(); + installInfo = null; + + Services.telemetry + .getHistogramById("SECURITY_UI") + .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH); + }; + break; + } + }; + + options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); + + let messageString; + let notification = document.getElementById("addon-install-confirmation-notification"); + if (unsigned.length == installInfo.installs.length) { + // None of the add-ons are verified + messageString = gNavigatorBundle.getString("addonConfirmInstallUnsigned.message"); + notification.setAttribute("warning", "true"); + options.learnMoreURL += "unsigned-addons"; + } + else if (unsigned.length == 0) { + // All add-ons are verified or don't need to be verified + messageString = gNavigatorBundle.getString("addonConfirmInstall.message"); + notification.removeAttribute("warning"); + options.learnMoreURL += "find-and-install-add-ons"; + } + else { + // Some of the add-ons are unverified, the list of names will indicate + // which + messageString = gNavigatorBundle.getString("addonConfirmInstallSomeUnsigned.message"); + notification.setAttribute("warning", "true"); + options.learnMoreURL += "unsigned-addons"; + } + + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + + messageString = PluralForm.get(installInfo.installs.length, messageString); + messageString = messageString.replace("#1", brandShortName); + messageString = messageString.replace("#2", installInfo.installs.length); + + let cancelButton = document.getElementById("addon-install-confirmation-cancel"); + cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label"); + cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"); + + let acceptButton = document.getElementById("addon-install-confirmation-accept"); + acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label"); + acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey"); + + if (height) { + let notification = document.getElementById("addon-install-confirmation-notification"); + notification.style.minHeight = height + "px"; + } + + let tab = gBrowser.getTabForBrowser(browser); + if (tab) { + gBrowser.selectedTab = tab; + } + + let popup = PopupNotifications.show(browser, "addon-install-confirmation", + messageString, anchorID, null, null, + options); + + removeNotificationOnEnd(popup, installInfo.installs); + + Services.telemetry + .getHistogramById("SECURITY_UI") + .add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL); + }, + + observe: function (aSubject, aTopic, aData) + { + var brandBundle = document.getElementById("bundle_brand"); + var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo); + var browser = installInfo.browser; + + // Make sure the browser is still alive. + if (!browser || gBrowser.browsers.indexOf(browser) == -1) + return; + + const anchorID = "addons-notification-icon"; + var messageString, action; + var brandShortName = brandBundle.getString("brandShortName"); + + var notificationID = aTopic; + // Make notifications persist a minimum of 30 seconds + var options = { + displayURI: installInfo.originatingURI, + timeout: Date.now() + 30000, + }; + + switch (aTopic) { + case "addon-install-disabled": { + notificationID = "xpinstall-disabled"; + + if (gPrefService.prefIsLocked("xpinstall.enabled")) { + messageString = gNavigatorBundle.getString("xpinstallDisabledMessageLocked"); + buttons = []; + } + else { + messageString = gNavigatorBundle.getString("xpinstallDisabledMessage"); + + action = { + label: gNavigatorBundle.getString("xpinstallDisabledButton"), + accessKey: gNavigatorBundle.getString("xpinstallDisabledButton.accesskey"), + callback: function editPrefs() { + gPrefService.setBoolPref("xpinstall.enabled", true); + } + }; + } + + PopupNotifications.show(browser, notificationID, messageString, anchorID, + action, null, options); + break; } + case "addon-install-origin-blocked": { + messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage", + [brandShortName]); + + let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI"); + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED); + let popup = PopupNotifications.show(browser, notificationID, + messageString, anchorID, + null, null, options); + removeNotificationOnEnd(popup, installInfo.installs); + break; } + case "addon-install-blocked": { + messageString = gNavigatorBundle.getFormattedString("xpinstallPromptMessage", + [brandShortName]); + + let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI"); + action = { + label: gNavigatorBundle.getString("xpinstallPromptAllowButton"), + accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"), + callback: function() { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH); + installInfo.install(); + } + }; + + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED); + let popup = PopupNotifications.show(browser, notificationID, + messageString, anchorID, + action, null, options); + removeNotificationOnEnd(popup, installInfo.installs); + break; } + case "addon-install-started": { + let needsDownload = function needsDownload(aInstall) { + return aInstall.state != AddonManager.STATE_DOWNLOADED; + } + // If all installs have already been downloaded then there is no need to + // show the download progress + if (!installInfo.installs.some(needsDownload)) + return; + notificationID = "addon-progress"; + messageString = gNavigatorBundle.getString("addonDownloadingAndVerifying"); + messageString = PluralForm.get(installInfo.installs.length, messageString); + messageString = messageString.replace("#1", installInfo.installs.length); + options.installs = installInfo.installs; + options.contentWindow = browser.contentWindow; + options.sourceURI = browser.currentURI; + options.eventCallback = (aEvent) => { + switch (aEvent) { + case "removed": + options.contentWindow = null; + options.sourceURI = null; + break; + } + }; + let notification = PopupNotifications.show(browser, notificationID, messageString, + anchorID, null, null, options); + notification._startTime = Date.now(); + + let cancelButton = document.getElementById("addon-progress-cancel"); + cancelButton.label = gNavigatorBundle.getString("addonInstall.cancelButton.label"); + cancelButton.accessKey = gNavigatorBundle.getString("addonInstall.cancelButton.accesskey"); + + let acceptButton = document.getElementById("addon-progress-accept"); + if (Preferences.get("xpinstall.customConfirmationUI", false)) { + acceptButton.label = gNavigatorBundle.getString("addonInstall.acceptButton.label"); + acceptButton.accessKey = gNavigatorBundle.getString("addonInstall.acceptButton.accesskey"); + } else { + acceptButton.hidden = true; + } + break; } + case "addon-install-failed": { + // TODO This isn't terribly ideal for the multiple failure case + for (let install of installInfo.installs) { + let host; + try { + host = options.displayURI.host; + } catch (e) { + // displayURI might be missing or 'host' might throw for non-nsStandardURL nsIURIs. + } + + if (!host) + host = (install.sourceURI instanceof Ci.nsIStandardURL) && + install.sourceURI.host; + + let error = (host || install.error == 0) ? "addonInstallError" : "addonLocalInstallError"; + let args; + if (install.error < 0) { + error += install.error; + args = [brandShortName, install.name]; + } else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { + error += "Blocklisted"; + args = [install.name]; + } else { + error += "Incompatible"; + args = [brandShortName, Services.appinfo.version, install.name]; + } + + // Add Learn More link when refusing to install an unsigned add-on + if (install.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) { + options.learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons"; + } + + messageString = gNavigatorBundle.getFormattedString(error, args); + + PopupNotifications.show(browser, notificationID, messageString, anchorID, + action, null, options); + + // Can't have multiple notifications with the same ID, so stop here. + break; + } + this._removeProgressNotification(browser); + break; } + case "addon-install-confirmation": { + let showNotification = () => { + let height = undefined; + + if (PopupNotifications.isPanelOpen) { + let rect = document.getElementById("addon-progress-notification").getBoundingClientRect(); + height = rect.height; + } + + this._removeProgressNotification(browser); + this.showInstallConfirmation(browser, installInfo, height); + }; + + let progressNotification = PopupNotifications.getNotification("addon-progress", browser); + if (progressNotification) { + let downloadDuration = Date.now() - progressNotification._startTime; + let securityDelay = Services.prefs.getIntPref("security.dialog_enable_delay") - downloadDuration; + if (securityDelay > 0) { + setTimeout(() => { + // The download may have been cancelled during the security delay + if (PopupNotifications.getNotification("addon-progress", browser)) + showNotification(); + }, securityDelay); + break; + } + } + showNotification(); + break; } + case "addon-install-complete": { + let needsRestart = installInfo.installs.some(function(i) { + return i.addon.pendingOperations != AddonManager.PENDING_NONE; + }); + + if (needsRestart) { + notificationID = "addon-install-restart"; + messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart"); + action = { + label: gNavigatorBundle.getString("addonInstallRestartButton"), + accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"), + callback: function() { + BrowserUtils.restartApplication(); + } + }; + } + else { + messageString = gNavigatorBundle.getString("addonsInstalled"); + action = null; + } + + messageString = PluralForm.get(installInfo.installs.length, messageString); + messageString = messageString.replace("#1", installInfo.installs[0].name); + messageString = messageString.replace("#2", installInfo.installs.length); + messageString = messageString.replace("#3", brandShortName); + + // Remove notificaion on dismissal, since it's possible to cancel the + // install through the addons manager UI, making the "restart" prompt + // irrelevant. + options.removeOnDismissal = true; + + PopupNotifications.show(browser, notificationID, messageString, anchorID, + action, null, options); + break; } + } + }, + _removeProgressNotification(aBrowser) { + let notification = PopupNotifications.getNotification("addon-progress", aBrowser); + if (notification) + notification.remove(); + } +}; + +var LightWeightThemeWebInstaller = { + init: function () { + let mm = window.messageManager; + mm.addMessageListener("LightWeightThemeWebInstaller:Install", this); + mm.addMessageListener("LightWeightThemeWebInstaller:Preview", this); + mm.addMessageListener("LightWeightThemeWebInstaller:ResetPreview", this); + }, + + receiveMessage: function (message) { + // ignore requests from background tabs + if (message.target != gBrowser.selectedBrowser) { + return; + } + + let data = message.data; + + switch (message.name) { + case "LightWeightThemeWebInstaller:Install": { + this._installRequest(data.themeData, data.principal, data.baseURI); + break; + } + case "LightWeightThemeWebInstaller:Preview": { + this._preview(data.themeData, data.principal, data.baseURI); + break; + } + case "LightWeightThemeWebInstaller:ResetPreview": { + this._resetPreview(data && data.principal); + break; + } + } + }, + + handleEvent: function (event) { + switch (event.type) { + case "TabSelect": { + this._resetPreview(); + break; + } + } + }, + + get _manager () { + let temp = {}; + Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); + delete this._manager; + return this._manager = temp.LightweightThemeManager; + }, + + _installRequest(dataString, principal, baseURI) { + // Don't allow installing off null principals. + if (!principal.URI) { + return; + } + + let data = this._manager.parseTheme(dataString, baseURI); + + if (!data) { + return; + } + + // A notification bar with the option to undo is normally shown after a + // theme is installed. But the discovery pane served from the url(s) + // below has its own toggle switch for quick undos, so don't show the + // notification in that case. + let notify = this._shouldShowUndoPrompt(principal); + if (this._isAllowed(principal)) { + this._install(data, notify); + return; + } + + let allowButtonText = + gNavigatorBundle.getString("lwthemeInstallRequest.allowButton"); + let allowButtonAccesskey = + gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey"); + let message = + gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message", + [principal.URI.host]); + let buttons = [{ + label: allowButtonText, + accessKey: allowButtonAccesskey, + callback: function () { + LightWeightThemeWebInstaller._install(data, notify); + } + }]; + + this._removePreviousNotifications(); + + let notificationBox = gBrowser.getNotificationBox(); + let notificationBar = + notificationBox.appendNotification(message, "lwtheme-install-request", "", + notificationBox.PRIORITY_INFO_MEDIUM, + buttons); + notificationBar.persistence = 1; + }, + + _install: function (newLWTheme, notify) { + let previousLWTheme = this._manager.currentTheme; + + let listener = { + onEnabling: function(aAddon, aRequiresRestart) { + if (!aRequiresRestart) { + return; + } + + let messageString = gNavigatorBundle.getFormattedString("lwthemeNeedsRestart.message", + [aAddon.name], 1); + + let action = { + label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"), + accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"), + callback: function () { + BrowserUtils.restartApplication(); + } + }; + + let options = { + timeout: Date.now() + 30000 + }; + + PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change", + messageString, "addons-notification-icon", + action, null, options); + }, + + onEnabled: function(aAddon) { + if (notify) { + LightWeightThemeWebInstaller._postInstallNotification(newLWTheme, previousLWTheme); + } + } + }; + + AddonManager.addAddonListener(listener); + this._manager.currentTheme = newLWTheme; + AddonManager.removeAddonListener(listener); + }, + + _postInstallNotification: function (newTheme, previousTheme) { + function text(id) { + return gNavigatorBundle.getString("lwthemePostInstallNotification." + id); + } + + let buttons = [{ + label: text("undoButton"), + accessKey: text("undoButton.accesskey"), + callback: function () { + LightWeightThemeWebInstaller._manager.forgetUsedTheme(newTheme.id); + LightWeightThemeWebInstaller._manager.currentTheme = previousTheme; + } + }, { + label: text("manageButton"), + accessKey: text("manageButton.accesskey"), + callback: function () { + BrowserOpenAddonsMgr("addons://list/theme"); + } + }]; + + this._removePreviousNotifications(); + + let notificationBox = gBrowser.getNotificationBox(); + let notificationBar = + notificationBox.appendNotification(text("message"), + "lwtheme-install-notification", "", + notificationBox.PRIORITY_INFO_MEDIUM, + buttons); + notificationBar.persistence = 1; + notificationBar.timeout = Date.now() + 20000; // 20 seconds + }, + + _removePreviousNotifications: function () { + let box = gBrowser.getNotificationBox(); + + ["lwtheme-install-request", + "lwtheme-install-notification"].forEach(function (value) { + let notification = box.getNotificationWithValue(value); + if (notification) + box.removeNotification(notification); + }); + }, + + _preview(dataString, principal, baseURI) { + if (!this._isAllowed(principal)) + return; + + let data = this._manager.parseTheme(dataString, baseURI); + if (!data) + return; + + this._resetPreview(); + gBrowser.tabContainer.addEventListener("TabSelect", this, false); + this._manager.previewTheme(data); + }, + + _resetPreview(principal) { + if (!this._isAllowed(principal)) + return; + gBrowser.tabContainer.removeEventListener("TabSelect", this, false); + this._manager.resetPreview(); + }, + + _isAllowed(principal) { + if (!principal || !principal.URI || !principal.URI.schemeIs("https")) { + return false; + } + + let pm = Services.perms; + return pm.testPermission(principal.URI, "install") == pm.ALLOW_ACTION; + }, + + _shouldShowUndoPrompt(principal) { + if (!principal || !principal.URI) { + return true; + } + + let prePath = principal.URI.prePath; + if (prePath == "https://addons.palemoon.org") { + return false; + } + + return true; + } +}; + +/* + * Listen for Lightweight Theme styling changes and update the browser's theme accordingly. + */ +var LightweightThemeListener = { + _modifiedStyles: [], + + init: function () { + XPCOMUtils.defineLazyGetter(this, "styleSheet", function() { + for (let i = document.styleSheets.length - 1; i >= 0; i--) { + let sheet = document.styleSheets[i]; + if (sheet.href == "chrome://browser/skin/browser-lightweightTheme.css") + return sheet; + } + return undefined; + }); + + Services.obs.addObserver(this, "lightweight-theme-styling-update", false); + Services.obs.addObserver(this, "lightweight-theme-optimized", false); + if (document.documentElement.hasAttribute("lwtheme")) + this.updateStyleSheet(document.documentElement.style.backgroundImage); + }, + + uninit: function () { + Services.obs.removeObserver(this, "lightweight-theme-styling-update"); + Services.obs.removeObserver(this, "lightweight-theme-optimized"); + }, + + /** + * Append the headerImage to the background-image property of all rulesets in + * browser-lightweightTheme.css. + * + * @param headerImage - a string containing a CSS image for the lightweight theme header. + */ + updateStyleSheet: function(headerImage) { + if (!this.styleSheet) + return; + this.substituteRules(this.styleSheet.cssRules, headerImage); + }, + + substituteRules: function(ruleList, headerImage, existingStyleRulesModified = 0) { + let styleRulesModified = 0; + for (let i = 0; i < ruleList.length; i++) { + let rule = ruleList[i]; + if (rule instanceof Ci.nsIDOMCSSGroupingRule) { + // Add the number of modified sub-rules to the modified count + styleRulesModified += this.substituteRules(rule.cssRules, headerImage, existingStyleRulesModified + styleRulesModified); + } else if (rule instanceof Ci.nsIDOMCSSStyleRule) { + if (!rule.style.backgroundImage) + continue; + let modifiedIndex = existingStyleRulesModified + styleRulesModified; + if (!this._modifiedStyles[modifiedIndex]) + this._modifiedStyles[modifiedIndex] = { backgroundImage: rule.style.backgroundImage }; + + rule.style.backgroundImage = this._modifiedStyles[modifiedIndex].backgroundImage + ", " + headerImage; + styleRulesModified++; + } else { + Cu.reportError("Unsupported rule encountered"); + } + } + return styleRulesModified; + }, + + // nsIObserver + observe: function (aSubject, aTopic, aData) { + if ((aTopic != "lightweight-theme-styling-update" && aTopic != "lightweight-theme-optimized") || + !this.styleSheet) + return; + + if (aTopic == "lightweight-theme-optimized" && aSubject != window) + return; + + let themeData = JSON.parse(aData); + if (!themeData) + return; + this.updateStyleSheet("url(" + themeData.headerURL + ")"); + }, +}; diff --git a/application/basilisk/base/content/browser-captivePortal.js b/application/basilisk/base/content/browser-captivePortal.js new file mode 100644 index 000000000..c2e45c4ed --- /dev/null +++ b/application/basilisk/base/content/browser-captivePortal.js @@ -0,0 +1,257 @@ +/* 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/. */ + +XPCOMUtils.defineLazyServiceGetter(this, "cps", + "@mozilla.org/network/captive-portal-service;1", + "nsICaptivePortalService"); + +var CaptivePortalWatcher = { + /** + * This constant is chosen to be large enough for a portal recheck to complete, + * and small enough that the delay in opening a tab isn't too noticeable. + * Please see comments for _delayedCaptivePortalDetected for more details. + */ + PORTAL_RECHECK_DELAY_MS: Preferences.get("captivedetect.portalRecheckDelayMS", 500), + + // This is the value used to identify the captive portal notification. + PORTAL_NOTIFICATION_VALUE: "captive-portal-detected", + + // This holds a weak reference to the captive portal tab so that we + // don't leak it if the user closes it. + _captivePortalTab: null, + + /** + * If a portal is detected when we don't have focus, we first wait for focus + * and then add the tab if, after a recheck, the portal is still active. This + * is set to true while we wait so that in the unlikely event that we receive + * another notification while waiting, we don't do things twice. + */ + _delayedCaptivePortalDetectedInProgress: false, + + // In the situation above, this is set to true while we wait for the recheck. + // This flag exists so that tests can appropriately simulate a recheck. + _waitingForRecheck: false, + + get _captivePortalNotification() { + let nb = document.getElementById("high-priority-global-notificationbox"); + return nb.getNotificationWithValue(this.PORTAL_NOTIFICATION_VALUE); + }, + + get canonicalURL() { + return Services.prefs.getCharPref("captivedetect.canonicalURL"); + }, + + get _browserBundle() { + delete this._browserBundle; + return this._browserBundle = + Services.strings.createBundle("chrome://browser/locale/browser.properties"); + }, + + init() { + Services.obs.addObserver(this, "captive-portal-login", false); + Services.obs.addObserver(this, "captive-portal-login-abort", false); + Services.obs.addObserver(this, "captive-portal-login-success", false); + + if (cps.state == cps.LOCKED_PORTAL) { + // A captive portal has already been detected. + this._captivePortalDetected(); + + // Automatically open a captive portal tab if there's no other browser window. + let windows = Services.wm.getEnumerator("navigator:browser"); + if (windows.getNext() == window && !windows.hasMoreElements()) { + this.ensureCaptivePortalTab(); + } + } + + cps.recheckCaptivePortal(); + }, + + uninit() { + Services.obs.removeObserver(this, "captive-portal-login"); + Services.obs.removeObserver(this, "captive-portal-login-abort"); + Services.obs.removeObserver(this, "captive-portal-login-success"); + + + if (this._delayedCaptivePortalDetectedInProgress) { + Services.obs.removeObserver(this, "xul-window-visible"); + } + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "captive-portal-login": + this._captivePortalDetected(); + break; + case "captive-portal-login-abort": + case "captive-portal-login-success": + this._captivePortalGone(); + break; + case "xul-window-visible": + this._delayedCaptivePortalDetected(); + break; + } + }, + + _captivePortalDetected() { + if (this._delayedCaptivePortalDetectedInProgress) { + return; + } + + let win = RecentWindow.getMostRecentBrowserWindow(); + // If no browser window has focus, open and show the tab when we regain focus. + // This is so that if a different application was focused, when the user + // (re-)focuses a browser window, we open the tab immediately in that window + // so they can log in before continuing to browse. + if (win != Services.ww.activeWindow) { + this._delayedCaptivePortalDetectedInProgress = true; + Services.obs.addObserver(this, "xul-window-visible", false); + } + + this._showNotification(); + }, + + /** + * Called after we regain focus if we detect a portal while a browser window + * doesn't have focus. Triggers a portal recheck to reaffirm state, and adds + * the tab if needed after a short delay to allow the recheck to complete. + */ + _delayedCaptivePortalDetected() { + if (!this._delayedCaptivePortalDetectedInProgress) { + return; + } + + let win = RecentWindow.getMostRecentBrowserWindow(); + if (win != Services.ww.activeWindow) { + // The window that got focused was not a browser window. + return; + } + Services.obs.removeObserver(this, "xul-window-visible"); + this._delayedCaptivePortalDetectedInProgress = false; + + if (win != window) { + // Some other browser window got focus, we don't have to do anything. + return; + } + // Trigger a portal recheck. The user may have logged into the portal via + // another client, or changed networks. + cps.recheckCaptivePortal(); + this._waitingForRecheck = true; + let requestTime = Date.now(); + + let self = this; + Services.obs.addObserver(function observer() { + let time = Date.now() - requestTime; + Services.obs.removeObserver(observer, "captive-portal-check-complete"); + self._waitingForRecheck = false; + if (cps.state != cps.LOCKED_PORTAL) { + // We're free of the portal! + return; + } + + if (time <= self.PORTAL_RECHECK_DELAY_MS) { + // The amount of time elapsed since we requested a recheck (i.e. since + // the browser window was focused) was small enough that we can add and + // focus a tab with the login page with no noticeable delay. + self.ensureCaptivePortalTab(); + } + }, "captive-portal-check-complete", false); + }, + + _captivePortalGone() { + if (this._delayedCaptivePortalDetectedInProgress) { + Services.obs.removeObserver(this, "xul-window-visible"); + this._delayedCaptivePortalDetectedInProgress = false; + } + + this._removeNotification(); + }, + + handleEvent(aEvent) { + if (aEvent.type != "TabSelect" || !this._captivePortalTab || !this._captivePortalNotification) { + return; + } + + let tab = this._captivePortalTab.get(); + let n = this._captivePortalNotification; + if (!tab || !n) { + return; + } + + let doc = tab.ownerDocument; + let button = n.querySelector("button.notification-button"); + if (doc.defaultView.gBrowser.selectedTab == tab) { + button.style.visibility = "hidden"; + } else { + button.style.visibility = "visible"; + } + }, + + _showNotification() { + let buttons = [ + { + label: this._browserBundle.GetStringFromName("captivePortal.showLoginPage"), + callback: () => { + this.ensureCaptivePortalTab(); + + // Returning true prevents the notification from closing. + return true; + }, + isDefault: true, + }, + ]; + + let message = this._browserBundle.GetStringFromName("captivePortal.infoMessage2"); + + let closeHandler = (aEventName) => { + if (aEventName != "removed") { + return; + } + gBrowser.tabContainer.removeEventListener("TabSelect", this); + }; + + let nb = document.getElementById("high-priority-global-notificationbox"); + nb.appendNotification(message, this.PORTAL_NOTIFICATION_VALUE, "", + nb.PRIORITY_INFO_MEDIUM, buttons, closeHandler); + + gBrowser.tabContainer.addEventListener("TabSelect", this); + }, + + _removeNotification() { + let n = this._captivePortalNotification; + if (!n || !n.parentNode) { + return; + } + n.close(); + }, + + ensureCaptivePortalTab() { + let tab; + if (this._captivePortalTab) { + tab = this._captivePortalTab.get(); + } + + // If the tab is gone or going, we need to open a new one. + if (!tab || tab.closing || !tab.parentNode) { + tab = gBrowser.addTab(this.canonicalURL, { ownerTab: gBrowser.selectedTab }); + this._captivePortalTab = Cu.getWeakReference(tab); + } + + gBrowser.selectedTab = tab; + + let canonicalURI = makeURI(this.canonicalURL); + + // When we are no longer captive, close the tab if it's at the canonical URL. + let tabCloser = () => { + Services.obs.removeObserver(tabCloser, "captive-portal-login-abort"); + Services.obs.removeObserver(tabCloser, "captive-portal-login-success"); + if (!tab || tab.closing || !tab.parentNode || !tab.linkedBrowser || + !tab.linkedBrowser.currentURI.equalsExceptRef(canonicalURI)) { + return; + } + gBrowser.removeTab(tab); + } + Services.obs.addObserver(tabCloser, "captive-portal-login-abort", false); + Services.obs.addObserver(tabCloser, "captive-portal-login-success", false); + }, +}; diff --git a/application/basilisk/base/content/browser-charsetmenu.inc b/application/basilisk/base/content/browser-charsetmenu.inc new file mode 100644 index 000000000..806b1cf03 --- /dev/null +++ b/application/basilisk/base/content/browser-charsetmenu.inc @@ -0,0 +1,12 @@ +# 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/. + +<menu id="charsetMenu" + label="&charsetMenu2.label;" + accesskey="&charsetMenu2.accesskey;" + oncommand="BrowserSetForcedCharacterSet(event.target.getAttribute('charset'));" + onpopupshowing="CharsetMenu.build(event.target); UpdateCurrentCharset(this);"> + <menupopup> + </menupopup> +</menu> diff --git a/application/basilisk/base/content/browser-context.inc b/application/basilisk/base/content/browser-context.inc new file mode 100644 index 000000000..9fa90b11c --- /dev/null +++ b/application/basilisk/base/content/browser-context.inc @@ -0,0 +1,454 @@ +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# NB: IF YOU ADD ITEMS TO THIS FILE, PLEASE UPDATE THE WHITELIST IN +# BrowserUITelemetry.jsm. SEE BUG 991757 FOR DETAILS. + + <menugroup id="context-navigation"> + <menuitem id="context-back" + class="menuitem-iconic" + tooltiptext="&backButton.tooltip;" + aria-label="&backCmd.label;" + command="Browser:BackOrBackDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-forward" + class="menuitem-iconic" + tooltiptext="&forwardButton.tooltip;" + aria-label="&forwardCmd.label;" + command="Browser:ForwardOrForwardDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-reload" + class="menuitem-iconic" + tooltiptext="&reloadButton.tooltip;" + aria-label="&reloadCmd.label;" + oncommand="gContextMenu.reload(event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-stop" + class="menuitem-iconic" + tooltiptext="&stopButton.tooltip;" + aria-label="&stopCmd.label;" + command="Browser:Stop"/> + <menuitem id="context-bookmarkpage" + class="menuitem-iconic" + observes="bookmarkThisPageBroadcaster" + aria-label="&bookmarkPageCmd2.label;" + oncommand="gContextMenu.bookmarkThisPage();"/> + </menugroup> + <menuseparator id="context-sep-navigation"/> + <menuseparator id="page-menu-separator"/> + <menuitem id="spell-no-suggestions" + disabled="true" + label="&spellNoSuggestions.label;"/> + <menuitem id="spell-add-to-dictionary" + label="&spellAddToDictionary.label;" + accesskey="&spellAddToDictionary.accesskey;" + oncommand="InlineSpellCheckerUI.addToDictionary();"/> + <menuitem id="spell-undo-add-to-dictionary" + label="&spellUndoAddToDictionary.label;" + accesskey="&spellUndoAddToDictionary.accesskey;" + oncommand="InlineSpellCheckerUI.undoAddToDictionary();" /> + <menuseparator id="spell-suggestions-separator"/> + <menuitem id="context-openlinkincurrent" + label="&openLinkCmdInCurrent.label;" + accesskey="&openLinkCmdInCurrent.accesskey;" + oncommand="gContextMenu.openLinkInCurrent();"/> +# label and data-usercontextid are dynamically set. + <menuitem id="context-openlinkincontainertab" + accesskey="&openLinkCmdInTab.accesskey;" + oncommand="gContextMenu.openLinkInTab(event);"/> + <menuitem id="context-openlinkintab" + label="&openLinkCmdInTab.label;" + accesskey="&openLinkCmdInTab.accesskey;" + data-usercontextid="0" + oncommand="gContextMenu.openLinkInTab(event);"/> + + <menu id="context-openlinkinusercontext-menu" + label="&openLinkCmdInContainerTab.label;" + accesskey="&openLinkCmdInContainerTab.accesskey;" + hidden="true"> + <menupopup oncommand="gContextMenu.openLinkInTab(event);" + onpopupshowing="return gContextMenu.createContainerMenu(event);" /> + </menu> + + <menuitem id="context-openlink" + label="&openLinkCmd.label;" + accesskey="&openLinkCmd.accesskey;" + oncommand="gContextMenu.openLink();"/> + <menuitem id="context-openlinkprivate" + label="&openLinkInPrivateWindowCmd.label;" + accesskey="&openLinkInPrivateWindowCmd.accesskey;" + oncommand="gContextMenu.openLinkInPrivateWindow();"/> + <menuseparator id="context-sep-open"/> + <menuitem id="context-bookmarklink" + label="&bookmarkThisLinkCmd.label;" + accesskey="&bookmarkThisLinkCmd.accesskey;" + oncommand="gContextMenu.bookmarkLink();"/> + <menuitem id="context-savelink" + label="&saveLinkCmd.label;" + accesskey="&saveLinkCmd.accesskey;" + oncommand="gContextMenu.saveLink();"/> + <menuitem id="context-copyemail" + label="©EmailCmd.label;" + accesskey="©EmailCmd.accesskey;" + oncommand="gContextMenu.copyEmail();"/> + <menuitem id="context-copylink" + label="©LinkCmd.label;" + accesskey="©LinkCmd.accesskey;" + oncommand="gContextMenu.copyLink();"/> + <menuseparator id="context-sep-copylink"/> + <menuitem id="context-media-play" + label="&mediaPlay.label;" + accesskey="&mediaPlay.accesskey;" + oncommand="gContextMenu.mediaCommand('play');"/> + <menuitem id="context-media-pause" + label="&mediaPause.label;" + accesskey="&mediaPause.accesskey;" + oncommand="gContextMenu.mediaCommand('pause');"/> + <menuitem id="context-media-mute" + label="&mediaMute.label;" + accesskey="&mediaMute.accesskey;" + oncommand="gContextMenu.mediaCommand('mute');"/> + <menuitem id="context-media-unmute" + label="&mediaUnmute.label;" + accesskey="&mediaUnmute.accesskey;" + oncommand="gContextMenu.mediaCommand('unmute');"/> + <menu id="context-media-playbackrate" label="&mediaPlaybackRate2.label;" accesskey="&mediaPlaybackRate2.accesskey;"> + <menupopup> + <menuitem id="context-media-playbackrate-050x" + label="&mediaPlaybackRate050x2.label;" + accesskey="&mediaPlaybackRate050x2.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 0.5);"/> + <menuitem id="context-media-playbackrate-100x" + label="&mediaPlaybackRate100x2.label;" + accesskey="&mediaPlaybackRate100x2.accesskey;" + type="radio" + name="playbackrate" + checked="true" + oncommand="gContextMenu.mediaCommand('playbackRate', 1.0);"/> + <menuitem id="context-media-playbackrate-125x" + label="&mediaPlaybackRate125x2.label;" + accesskey="&mediaPlaybackRate125x2.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 1.25);"/> + <menuitem id="context-media-playbackrate-150x" + label="&mediaPlaybackRate150x2.label;" + accesskey="&mediaPlaybackRate150x2.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 1.5);"/> + <menuitem id="context-media-playbackrate-200x" + label="&mediaPlaybackRate200x2.label;" + accesskey="&mediaPlaybackRate200x2.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 2.0);"/> + </menupopup> + </menu> + <menuitem id="context-media-loop" + label="&mediaLoop.label;" + accesskey="&mediaLoop.accesskey;" + type="checkbox" + oncommand="gContextMenu.mediaCommand('loop');"/> + <menuitem id="context-media-showcontrols" + label="&mediaShowControls.label;" + accesskey="&mediaShowControls.accesskey;" + oncommand="gContextMenu.mediaCommand('showcontrols');"/> + <menuitem id="context-media-hidecontrols" + label="&mediaHideControls.label;" + accesskey="&mediaHideControls.accesskey;" + oncommand="gContextMenu.mediaCommand('hidecontrols');"/> + <menuitem id="context-video-fullscreen" + accesskey="&videoFullScreen.accesskey;" + label="&videoFullScreen.label;" + oncommand="gContextMenu.mediaCommand('fullscreen');"/> + <menuitem id="context-leave-dom-fullscreen" + accesskey="&leaveDOMFullScreen.accesskey;" + label="&leaveDOMFullScreen.label;" + oncommand="gContextMenu.leaveDOMFullScreen();"/> + <menuseparator id="context-media-sep-commands"/> + <menuitem id="context-reloadimage" + label="&reloadImageCmd.label;" + accesskey="&reloadImageCmd.accesskey;" + oncommand="gContextMenu.reloadImage();"/> + <menuitem id="context-viewimage" + label="&viewImageCmd.label;" + accesskey="&viewImageCmd.accesskey;" + oncommand="gContextMenu.viewMedia(event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-viewvideo" + label="&viewVideoCmd.label;" + accesskey="&viewVideoCmd.accesskey;" + oncommand="gContextMenu.viewMedia(event);" + onclick="checkForMiddleClick(this, event);"/> +#ifdef CONTEXT_COPY_IMAGE_CONTENTS + <menuitem id="context-copyimage-contents" + label="©ImageContentsCmd.label;" + accesskey="©ImageContentsCmd.accesskey;" + oncommand="goDoCommand('cmd_copyImage');"/> +#endif + <menuitem id="context-copyimage" + label="©ImageCmd.label;" + accesskey="©ImageCmd.accesskey;" + oncommand="gContextMenu.copyMediaLocation();"/> + <menuitem id="context-copyvideourl" + label="©VideoURLCmd.label;" + accesskey="©VideoURLCmd.accesskey;" + oncommand="gContextMenu.copyMediaLocation();"/> + <menuitem id="context-copyaudiourl" + label="©AudioURLCmd.label;" + accesskey="©AudioURLCmd.accesskey;" + oncommand="gContextMenu.copyMediaLocation();"/> + <menuseparator id="context-sep-copyimage"/> + <menuitem id="context-saveimage" + label="&saveImageCmd.label;" + accesskey="&saveImageCmd.accesskey;" + oncommand="gContextMenu.saveMedia();"/> + <menuitem id="context-sendimage" + label="&emailImageCmd.label;" + accesskey="&emailImageCmd.accesskey;" + oncommand="gContextMenu.sendMedia();"/> + <menuitem id="context-setDesktopBackground" + label="&setDesktopBackgroundCmd.label;" + accesskey="&setDesktopBackgroundCmd.accesskey;" + oncommand="gContextMenu.setDesktopBackground();"/> + <menuitem id="context-viewimageinfo" + label="&viewImageInfoCmd.label;" + accesskey="&viewImageInfoCmd.accesskey;" + oncommand="gContextMenu.viewImageInfo();"/> + <menuitem id="context-viewimagedesc" + label="&viewImageDescCmd.label;" + accesskey="&viewImageDescCmd.accesskey;" + oncommand="gContextMenu.viewImageDesc(event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-savevideo" + label="&saveVideoCmd.label;" + accesskey="&saveVideoCmd.accesskey;" + oncommand="gContextMenu.saveMedia();"/> + <menuitem id="context-saveaudio" + label="&saveAudioCmd.label;" + accesskey="&saveAudioCmd.accesskey;" + oncommand="gContextMenu.saveMedia();"/> + <menuitem id="context-video-saveimage" + accesskey="&videoSaveImage.accesskey;" + label="&videoSaveImage.label;" + oncommand="gContextMenu.saveVideoFrameAsImage();"/> + <menuitem id="context-sendvideo" + label="&emailVideoCmd.label;" + accesskey="&emailVideoCmd.accesskey;" + oncommand="gContextMenu.sendMedia();"/> + <menu id="context-castvideo" + label="&castVideoCmd.label;" + accesskey="&castVideoCmd.accesskey;"> + <menupopup id="context-castvideo-popup" onpopupshowing="gContextMenu.populateCastVideoMenu(this)"/> + </menu> + <menuitem id="context-sendaudio" + label="&emailAudioCmd.label;" + accesskey="&emailAudioCmd.accesskey;" + oncommand="gContextMenu.sendMedia();"/> + <menuitem id="context-ctp-play" + label="&playPluginCmd.label;" + accesskey="&playPluginCmd.accesskey;" + oncommand="gContextMenu.playPlugin();"/> + <menuitem id="context-ctp-hide" + label="&hidePluginCmd.label;" + accesskey="&hidePluginCmd.accesskey;" + oncommand="gContextMenu.hidePlugin();"/> + <menuseparator id="context-sep-ctp"/> + <menuitem id="context-savepage" + label="&savePageCmd.label;" + accesskey="&savePageCmd.accesskey2;" + oncommand="gContextMenu.savePageAs();"/> + <menuseparator id="context-sep-sendpagetodevice" hidden="true"/> + <menu id="context-sendpagetodevice" + label="&sendPageToDevice.label;" + accesskey="&sendPageToDevice.accesskey;" + hidden="true"> + <menupopup id="context-sendpagetodevice-popup" + onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/> + </menu> + <menuseparator id="context-sep-viewbgimage"/> + <menuitem id="context-viewbgimage" + label="&viewBGImageCmd.label;" + accesskey="&viewBGImageCmd.accesskey;" + oncommand="gContextMenu.viewBGImage(event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-undo" + label="&undoCmd.label;" + accesskey="&undoCmd.accesskey;" + command="cmd_undo"/> + <menuseparator id="context-sep-undo"/> + <menuitem id="context-cut" + label="&cutCmd.label;" + accesskey="&cutCmd.accesskey;" + command="cmd_cut"/> + <menuitem id="context-copy" + label="©Cmd.label;" + accesskey="©Cmd.accesskey;" + command="cmd_copy"/> + <menuitem id="context-paste" + label="&pasteCmd.label;" + accesskey="&pasteCmd.accesskey;" + command="cmd_paste"/> + <menuitem id="context-delete" + label="&deleteCmd.label;" + accesskey="&deleteCmd.accesskey;" + command="cmd_delete"/> + <menuseparator id="context-sep-paste"/> + <menuitem id="context-selectall" + label="&selectAllCmd.label;" + accesskey="&selectAllCmd.accesskey;" + command="cmd_selectAll"/> + <menuseparator id="context-sep-selectall"/> + <menuitem id="context-keywordfield" + label="&keywordfield.label;" + accesskey="&keywordfield.accesskey;" + oncommand="AddKeywordForSearchField();"/> + <menuitem id="context-searchselect" + oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/> + <menuseparator id="context-sep-sendlinktodevice" hidden="true"/> + <menu id="context-sendlinktodevice" + label="&sendLinkToDevice.label;" + accesskey="&sendLinkToDevice.accesskey;" + hidden="true"> + <menupopup id="context-sendlinktodevice-popup" + onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/> + </menu> + <menuseparator id="frame-sep"/> + <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;"> + <menupopup> + <menuitem id="context-showonlythisframe" + label="&showOnlyThisFrameCmd.label;" + accesskey="&showOnlyThisFrameCmd.accesskey;" + oncommand="gContextMenu.showOnlyThisFrame();"/> + <menuitem id="context-openframeintab" + label="&openFrameCmdInTab.label;" + accesskey="&openFrameCmdInTab.accesskey;" + oncommand="gContextMenu.openFrameInTab();"/> + <menuitem id="context-openframe" + label="&openFrameCmd.label;" + accesskey="&openFrameCmd.accesskey;" + oncommand="gContextMenu.openFrame();"/> + <menuseparator id="open-frame-sep"/> + <menuitem id="context-reloadframe" + label="&reloadFrameCmd.label;" + accesskey="&reloadFrameCmd.accesskey;" + oncommand="gContextMenu.reloadFrame();"/> + <menuseparator/> + <menuitem id="context-bookmarkframe" + label="&bookmarkThisFrameCmd.label;" + accesskey="&bookmarkThisFrameCmd.accesskey;" + oncommand="gContextMenu.addBookmarkForFrame();"/> + <menuitem id="context-saveframe" + label="&saveFrameCmd.label;" + accesskey="&saveFrameCmd.accesskey;" + oncommand="gContextMenu.saveFrame();"/> + <menuseparator/> + <menuitem id="context-printframe" + label="&printFrameCmd.label;" + accesskey="&printFrameCmd.accesskey;" + oncommand="gContextMenu.printFrame();"/> + <menuseparator/> + <menuitem id="context-viewframesource" + label="&viewFrameSourceCmd.label;" + accesskey="&viewFrameSourceCmd.accesskey;" + oncommand="gContextMenu.viewFrameSource();" + observes="isFrameImage"/> + <menuitem id="context-viewframeinfo" + label="&viewFrameInfoCmd.label;" + accesskey="&viewFrameInfoCmd.accesskey;" + oncommand="gContextMenu.viewFrameInfo();"/> + </menupopup> + </menu> + <menuitem id="context-viewpartialsource-selection" + label="&viewPartialSourceForSelectionCmd.label;" + accesskey="&viewPartialSourceCmd.accesskey;" + oncommand="gContextMenu.viewPartialSource('selection');" + observes="isImage"/> + <menuitem id="context-viewpartialsource-mathml" + label="&viewPartialSourceForMathMLCmd.label;" + accesskey="&viewPartialSourceCmd.accesskey;" + oncommand="gContextMenu.viewPartialSource('mathml');" + observes="isImage"/> + <menuseparator id="context-sep-viewsource"/> + <menuitem id="context-viewsource" + label="&viewPageSourceCmd.label;" + accesskey="&viewPageSourceCmd.accesskey;" + oncommand="BrowserViewSource(gContextMenu.browser);" + observes="canViewSource"/> + <menuitem id="context-viewinfo" + label="&viewPageInfoCmd.label;" + accesskey="&viewPageInfoCmd.accesskey;" + oncommand="gContextMenu.viewInfo();"/> + <menuseparator id="spell-separator"/> + <menuitem id="spell-check-enabled" + label="&spellCheckToggle.label;" + type="checkbox" + accesskey="&spellCheckToggle.accesskey;" + oncommand="InlineSpellCheckerUI.toggleEnabled(window);"/> + <menuitem id="spell-add-dictionaries-main" + label="&spellAddDictionaries.label;" + accesskey="&spellAddDictionaries.accesskey;" + oncommand="gContextMenu.addDictionaries();"/> + <menu id="spell-dictionaries" + label="&spellDictionaries.label;" + accesskey="&spellDictionaries.accesskey;"> + <menupopup id="spell-dictionaries-menu"> + <menuseparator id="spell-language-separator"/> + <menuitem id="spell-add-dictionaries" + label="&spellAddDictionaries.label;" + accesskey="&spellAddDictionaries.accesskey;" + oncommand="gContextMenu.addDictionaries();"/> + </menupopup> + </menu> + <menuseparator hidden="true" id="context-sep-bidi"/> + <menuitem hidden="true" id="context-bidi-text-direction-toggle" + label="&bidiSwitchTextDirectionItem.label;" + accesskey="&bidiSwitchTextDirectionItem.accesskey;" + command="cmd_switchTextDirection"/> + <menuitem hidden="true" id="context-bidi-page-direction-toggle" + label="&bidiSwitchPageDirectionItem.label;" + accesskey="&bidiSwitchPageDirectionItem.accesskey;" + oncommand="gContextMenu.switchPageDirection();"/> + <menuseparator id="fill-login-separator" hidden="true"/> + <menu id="fill-login" + label="&fillLoginMenu.label;" + label-login="&fillLoginMenu.label;" + label-password="&fillPasswordMenu.label;" + label-username="&fillUsernameMenu.label;" + accesskey="&fillLoginMenu.accesskey;" + accesskey-login="&fillLoginMenu.accesskey;" + accesskey-password="&fillPasswordMenu.accesskey;" + accesskey-username="&fillUsernameMenu.accesskey;" + hidden="true"> + <menupopup id="fill-login-popup"> + <menuitem id="fill-login-no-logins" + label="&noLoginSuggestions.label;" + disabled="true" + hidden="true"/> + <menuseparator id="saved-logins-separator"/> + <menuitem id="fill-login-saved-passwords" + label="&viewSavedLogins.label;" + oncommand="gContextMenu.openPasswordManager();"/> + </menupopup> + </menu> +#ifdef MOZ_DEVTOOLS + <menuseparator id="inspect-separator" hidden="true"/> + <menuitem id="context-inspect" + hidden="true" + label="&inspectContextMenu.label;" + accesskey="&inspectContextMenu.accesskey;" + oncommand="gContextMenu.inspectNode();"/> +#endif + <menuseparator id="context-media-eme-separator" hidden="true"/> + <menuitem id="context-media-eme-learnmore" + class="menuitem-iconic" + hidden="true" + label="&emeLearnMoreContextMenu.label;" + accesskey="&emeLearnMoreContextMenu.accesskey;" + oncommand="gContextMenu.drmLearnMore(event);" + onclick="checkForMiddleClick(this, event);"/> diff --git a/application/basilisk/base/content/browser-ctrlTab.js b/application/basilisk/base/content/browser-ctrlTab.js new file mode 100644 index 000000000..c761ea095 --- /dev/null +++ b/application/basilisk/base/content/browser-ctrlTab.js @@ -0,0 +1,587 @@ +/* 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/. */ + +/** + * Tab previews utility, produces thumbnails + */ +var tabPreviews = { + init: function tabPreviews_init() { + if (this._selectedTab) + return; + this._selectedTab = gBrowser.selectedTab; + + gBrowser.tabContainer.addEventListener("TabSelect", this, false); + gBrowser.tabContainer.addEventListener("SSTabRestored", this, false); + + let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager); + let left = {}, top = {}, width = {}, height = {}; + screenManager.primaryScreen.GetRectDisplayPix(left, top, width, height); + this.aspectRatio = height.value / width.value; + }, + + get: function tabPreviews_get(aTab) { + let uri = aTab.linkedBrowser.currentURI.spec; + + if (aTab.__thumbnail_lastURI && + aTab.__thumbnail_lastURI != uri) { + aTab.__thumbnail = null; + aTab.__thumbnail_lastURI = null; + } + + if (aTab.__thumbnail) + return aTab.__thumbnail; + + if (aTab.getAttribute("pending") == "true") { + let img = new Image; + img.src = PageThumbs.getThumbnailURL(uri); + return img; + } + + return this.capture(aTab, !aTab.hasAttribute("busy")); + }, + + capture: function tabPreviews_capture(aTab, aShouldCache) { + let browser = aTab.linkedBrowser; + let uri = browser.currentURI.spec; + let canvas = PageThumbs.createCanvas(window); + PageThumbs.shouldStoreThumbnail(browser, (aDoStore) => { + if (aDoStore && aShouldCache) { + PageThumbs.captureAndStore(browser, function () { + let img = new Image; + img.src = PageThumbs.getThumbnailURL(uri); + aTab.__thumbnail = img; + aTab.__thumbnail_lastURI = uri; + canvas.getContext("2d").drawImage(img, 0, 0); + }); + } else { + PageThumbs.captureToCanvas(browser, canvas, () => { + if (aShouldCache) { + aTab.__thumbnail = canvas; + aTab.__thumbnail_lastURI = uri; + } + }); + } + }); + return canvas; + }, + + handleEvent: function tabPreviews_handleEvent(event) { + switch (event.type) { + case "TabSelect": + if (this._selectedTab && + this._selectedTab.parentNode && + !this._pendingUpdate) { + // Generate a thumbnail for the tab that was selected. + // The timeout keeps the UI snappy and prevents us from generating thumbnails + // for tabs that will be closed. During that timeout, don't generate other + // thumbnails in case multiple TabSelect events occur fast in succession. + this._pendingUpdate = true; + setTimeout(function (self, aTab) { + self._pendingUpdate = false; + if (aTab.parentNode && + !aTab.hasAttribute("busy") && + !aTab.hasAttribute("pending")) + self.capture(aTab, true); + }, 2000, this, this._selectedTab); + } + this._selectedTab = event.target; + break; + case "SSTabRestored": + this.capture(event.target, true); + break; + } + } +}; + +var tabPreviewPanelHelper = { + opening: function (host) { + host.panel.hidden = false; + + var handler = this._generateHandler(host); + host.panel.addEventListener("popupshown", handler, false); + host.panel.addEventListener("popuphiding", handler, false); + + host._prevFocus = document.commandDispatcher.focusedElement; + }, + _generateHandler: function (host) { + var self = this; + return function (event) { + if (event.target == host.panel) { + host.panel.removeEventListener(event.type, arguments.callee, false); + self["_" + event.type](host); + } + }; + }, + _popupshown: function (host) { + if ("setupGUI" in host) + host.setupGUI(); + }, + _popuphiding: function (host) { + if ("suspendGUI" in host) + host.suspendGUI(); + + if (host._prevFocus) { + Services.focus.setFocus(host._prevFocus, Ci.nsIFocusManager.FLAG_NOSCROLL); + host._prevFocus = null; + } else + gBrowser.selectedBrowser.focus(); + + if (host.tabToSelect) { + gBrowser.selectedTab = host.tabToSelect; + host.tabToSelect = null; + } + } +}; + +/** + * Ctrl-Tab panel + */ +var ctrlTab = { + get panel () { + delete this.panel; + return this.panel = document.getElementById("ctrlTab-panel"); + }, + get showAllButton () { + delete this.showAllButton; + return this.showAllButton = document.getElementById("ctrlTab-showAll"); + }, + get previews () { + delete this.previews; + return this.previews = this.panel.getElementsByClassName("ctrlTab-preview"); + }, + get maxTabPreviews () { + delete this.maxTabPreviews; + return this.maxTabPreviews = this.previews.length - 1; + }, + get canvasWidth () { + delete this.canvasWidth; + return this.canvasWidth = Math.ceil(screen.availWidth * .85 / this.maxTabPreviews); + }, + get canvasHeight () { + delete this.canvasHeight; + return this.canvasHeight = Math.round(this.canvasWidth * tabPreviews.aspectRatio); + }, + get keys () { + var keys = {}; + ["close", "find", "selectAll"].forEach(function (key) { + keys[key] = document.getElementById("key_" + key) + .getAttribute("key") + .toLocaleLowerCase().charCodeAt(0); + }); + delete this.keys; + return this.keys = keys; + }, + _selectedIndex: 0, + get selected () { + return this._selectedIndex < 0 ? + document.activeElement : + this.previews.item(this._selectedIndex); + }, + get isOpen () { + return this.panel.state == "open" || this.panel.state == "showing" || this._timer; + }, + get tabCount () { + return this.tabList.length; + }, + get tabPreviewCount () { + return Math.min(this.maxTabPreviews, this.tabCount); + }, + + get tabList () { + return this._recentlyUsedTabs; + }, + + init: function ctrlTab_init() { + if (!this._recentlyUsedTabs) { + tabPreviews.init(); + + this._initRecentlyUsedTabs(); + this._init(true); + } + }, + + uninit: function ctrlTab_uninit() { + this._recentlyUsedTabs = null; + this._init(false); + }, + + prefName: "browser.ctrlTab.previews", + readPref: function ctrlTab_readPref() { + var enable = + gPrefService.getBoolPref(this.prefName) && + (!gPrefService.prefHasUserValue("browser.ctrlTab.disallowForScreenReaders") || + !gPrefService.getBoolPref("browser.ctrlTab.disallowForScreenReaders")); + + if (enable) + this.init(); + else + this.uninit(); + }, + observe: function (aSubject, aTopic, aPrefName) { + this.readPref(); + }, + + updatePreviews: function ctrlTab_updatePreviews() { + for (let i = 0; i < this.previews.length; i++) + this.updatePreview(this.previews[i], this.tabList[i]); + + var showAllLabel = gNavigatorBundle.getString("ctrlTab.listAllTabs.label"); + this.showAllButton.label = + PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount); + this.showAllButton.hidden = !allTabs.canOpen; + }, + + updatePreview: function ctrlTab_updatePreview(aPreview, aTab) { + if (aPreview == this.showAllButton) + return; + + aPreview._tab = aTab; + + if (aPreview.firstChild) + aPreview.removeChild(aPreview.firstChild); + if (aTab) { + let canvasWidth = this.canvasWidth; + let canvasHeight = this.canvasHeight; + aPreview.appendChild(tabPreviews.get(aTab)); + aPreview.setAttribute("label", aTab.label); + aPreview.setAttribute("tooltiptext", aTab.label); + aPreview.setAttribute("crop", aTab.crop); + aPreview.setAttribute("canvaswidth", canvasWidth); + aPreview.setAttribute("canvasstyle", + "max-width:" + canvasWidth + "px;" + + "min-width:" + canvasWidth + "px;" + + "max-height:" + canvasHeight + "px;" + + "min-height:" + canvasHeight + "px;"); + if (aTab.image) + aPreview.setAttribute("image", aTab.image); + else + aPreview.removeAttribute("image"); + aPreview.hidden = false; + } else { + aPreview.hidden = true; + aPreview.removeAttribute("label"); + aPreview.removeAttribute("tooltiptext"); + aPreview.removeAttribute("image"); + } + }, + + advanceFocus: function ctrlTab_advanceFocus(aForward) { + let selectedIndex = Array.indexOf(this.previews, this.selected); + do { + selectedIndex += aForward ? 1 : -1; + if (selectedIndex < 0) + selectedIndex = this.previews.length - 1; + else if (selectedIndex >= this.previews.length) + selectedIndex = 0; + } while (this.previews[selectedIndex].hidden); + + if (this._selectedIndex == -1) { + // Focus is already in the panel. + this.previews[selectedIndex].focus(); + } else { + this._selectedIndex = selectedIndex; + } + + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + this._openPanel(); + } + }, + + _mouseOverFocus: function ctrlTab_mouseOverFocus(aPreview) { + if (this._trackMouseOver) + aPreview.focus(); + }, + + pick: function ctrlTab_pick(aPreview) { + if (!this.tabCount) + return; + + var select = (aPreview || this.selected); + + if (select == this.showAllButton) + this.showAllTabs(); + else + this.close(select._tab); + }, + + showAllTabs: function ctrlTab_showAllTabs(aPreview) { + this.close(); + document.getElementById("Browser:ShowAllTabs").doCommand(); + }, + + remove: function ctrlTab_remove(aPreview) { + if (aPreview._tab) + gBrowser.removeTab(aPreview._tab); + }, + + attachTab: function ctrlTab_attachTab(aTab, aPos) { + if (aTab.closing) + return; + + if (aPos == 0) + this._recentlyUsedTabs.unshift(aTab); + else if (aPos) + this._recentlyUsedTabs.splice(aPos, 0, aTab); + else + this._recentlyUsedTabs.push(aTab); + }, + + detachTab: function ctrlTab_detachTab(aTab) { + var i = this._recentlyUsedTabs.indexOf(aTab); + if (i >= 0) + this._recentlyUsedTabs.splice(i, 1); + }, + + open: function ctrlTab_open() { + if (this.isOpen) + return; + + document.addEventListener("keyup", this, true); + + this.updatePreviews(); + this._selectedIndex = 1; + + // Add a slight delay before showing the UI, so that a quick + // "ctrl-tab" keypress just flips back to the MRU tab. + this._timer = setTimeout(function (self) { + self._timer = null; + self._openPanel(); + }, 200, this); + }, + + _openPanel: function ctrlTab_openPanel() { + tabPreviewPanelHelper.opening(this); + + this.panel.width = Math.min(screen.availWidth * .99, + this.canvasWidth * 1.25 * this.tabPreviewCount); + var estimateHeight = this.canvasHeight * 1.25 + 75; + this.panel.openPopupAtScreen(screen.availLeft + (screen.availWidth - this.panel.width) / 2, + screen.availTop + (screen.availHeight - estimateHeight) / 2, + false); + }, + + close: function ctrlTab_close(aTabToSelect) { + if (!this.isOpen) + return; + + if (this._timer) { + clearTimeout(this._timer); + this._timer = null; + this.suspendGUI(); + if (aTabToSelect) + gBrowser.selectedTab = aTabToSelect; + return; + } + + this.tabToSelect = aTabToSelect; + this.panel.hidePopup(); + }, + + setupGUI: function ctrlTab_setupGUI() { + this.selected.focus(); + this._selectedIndex = -1; + + // Track mouse movement after a brief delay so that the item that happens + // to be under the mouse pointer initially won't be selected unintentionally. + this._trackMouseOver = false; + setTimeout(function (self) { + if (self.isOpen) + self._trackMouseOver = true; + }, 0, this); + }, + + suspendGUI: function ctrlTab_suspendGUI() { + document.removeEventListener("keyup", this, true); + + for (let preview of this.previews) { + this.updatePreview(preview, null); + } + }, + + onKeyPress: function ctrlTab_onKeyPress(event) { + var isOpen = this.isOpen; + + if (isOpen) { + event.preventDefault(); + event.stopPropagation(); + } + + switch (event.keyCode) { + case event.DOM_VK_TAB: + if (event.ctrlKey && !event.altKey && !event.metaKey) { + if (isOpen) { + this.advanceFocus(!event.shiftKey); + } else if (!event.shiftKey) { + event.preventDefault(); + event.stopPropagation(); + let tabs = gBrowser.visibleTabs; + if (tabs.length > 2) { + this.open(); + } else if (tabs.length == 2) { + let index = tabs[0].selected ? 1 : 0; + gBrowser.selectedTab = tabs[index]; + } + } + } + break; + default: + if (isOpen && event.ctrlKey) { + if (event.keyCode == event.DOM_VK_DELETE) { + this.remove(this.selected); + break; + } + switch (event.charCode) { + case this.keys.close: + this.remove(this.selected); + break; + case this.keys.find: + case this.keys.selectAll: + this.showAllTabs(); + break; + } + } + } + }, + + removeClosingTabFromUI: function ctrlTab_removeClosingTabFromUI(aTab) { + if (this.tabCount == 2) { + this.close(); + return; + } + + this.updatePreviews(); + + if (this.selected.hidden) + this.advanceFocus(false); + if (this.selected == this.showAllButton) + this.advanceFocus(false); + + // If the current tab is removed, another tab can steal our focus. + if (aTab.selected && this.panel.state == "open") { + setTimeout(function (selected) { + selected.focus(); + }, 0, this.selected); + } + }, + + handleEvent: function ctrlTab_handleEvent(event) { + switch (event.type) { + case "SSWindowRestored": + this._initRecentlyUsedTabs(); + break; + case "TabAttrModified": + // tab attribute modified (e.g. label, crop, busy, image, selected) + for (let i = this.previews.length - 1; i >= 0; i--) { + if (this.previews[i]._tab && this.previews[i]._tab == event.target) { + this.updatePreview(this.previews[i], event.target); + break; + } + } + break; + case "TabSelect": + this.detachTab(event.target); + this.attachTab(event.target, 0); + break; + case "TabOpen": + this.attachTab(event.target, 1); + break; + case "TabClose": + this.detachTab(event.target); + if (this.isOpen) + this.removeClosingTabFromUI(event.target); + break; + case "keypress": + this.onKeyPress(event); + break; + case "keyup": + if (event.keyCode == event.DOM_VK_CONTROL) + this.pick(); + break; + case "popupshowing": + if (event.target.id == "menu_viewPopup") + document.getElementById("menu_showAllTabs").hidden = !allTabs.canOpen; + break; + } + }, + + filterForThumbnailExpiration: function (aCallback) { + // Save a few more thumbnails than we actually display, so that when tabs + // are closed, the previews we add instead still get thumbnails. + const extraThumbnails = 3; + const thumbnailCount = Math.min(this.tabPreviewCount + extraThumbnails, + this.tabCount); + + let urls = []; + for (let i = 0; i < thumbnailCount; i++) + urls.push(this.tabList[i].linkedBrowser.currentURI.spec); + + aCallback(urls); + }, + + _initRecentlyUsedTabs: function () { + this._recentlyUsedTabs = + Array.filter(gBrowser.tabs, tab => !tab.closing) + .sort((tab1, tab2) => tab2.lastAccessed - tab1.lastAccessed); + }, + + _init: function ctrlTab__init(enable) { + var toggleEventListener = enable ? "addEventListener" : "removeEventListener"; + + window[toggleEventListener]("SSWindowRestored", this, false); + + var tabContainer = gBrowser.tabContainer; + tabContainer[toggleEventListener]("TabOpen", this, false); + tabContainer[toggleEventListener]("TabAttrModified", this, false); + tabContainer[toggleEventListener]("TabSelect", this, false); + tabContainer[toggleEventListener]("TabClose", this, false); + + document[toggleEventListener]("keypress", this, false); + gBrowser.mTabBox.handleCtrlTab = !enable; + + if (enable) + PageThumbs.addExpirationFilter(this); + else + PageThumbs.removeExpirationFilter(this); + + // If we're not running, hide the "Show All Tabs" menu item, + // as Shift+Ctrl+Tab will be handled by the tab bar. + document.getElementById("menu_showAllTabs").hidden = !enable; + document.getElementById("menu_viewPopup")[toggleEventListener]("popupshowing", this); + + // Also disable the <key> to ensure Shift+Ctrl+Tab never triggers + // Show All Tabs. + var key_showAllTabs = document.getElementById("key_showAllTabs"); + if (enable) + key_showAllTabs.removeAttribute("disabled"); + else + key_showAllTabs.setAttribute("disabled", "true"); + } +}; + + +/** + * All Tabs menu + */ +var allTabs = { + get toolbarButton() { + return document.getElementById("alltabs-button"); + }, + + get canOpen() { + return isElementVisible(this.toolbarButton); + }, + + open: function allTabs_open() { + if (this.canOpen) { + // Without setTimeout, the menupopup won't stay open when invoking + // "View > Show All Tabs" and the menu bar auto-hides. + setTimeout(() => { + this.toolbarButton.open = true; + }, 0); + } + } +}; diff --git a/application/basilisk/base/content/browser-customization.js b/application/basilisk/base/content/browser-customization.js new file mode 100644 index 000000000..d5d51b893 --- /dev/null +++ b/application/basilisk/base/content/browser-customization.js @@ -0,0 +1,100 @@ +/* -*- 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/. */ + +/** + * Customization handler prepares this browser window for entering and exiting + * customization mode by handling customizationstarting and customizationending + * events. + */ +var CustomizationHandler = { + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "customizationstarting": + this._customizationStarting(); + break; + case "customizationchange": + this._customizationChange(); + break; + case "customizationending": + this._customizationEnding(aEvent.detail); + break; + } + }, + + isCustomizing: function() { + return document.documentElement.hasAttribute("customizing"); + }, + + _customizationStarting: function() { + // Disable the toolbar context menu items + let menubar = document.getElementById("main-menubar"); + for (let childNode of menubar.childNodes) + childNode.setAttribute("disabled", true); + + let cmd = document.getElementById("cmd_CustomizeToolbars"); + cmd.setAttribute("disabled", "true"); + + UpdateUrlbarSearchSplitterState(); + + CombinedStopReload.uninit(); + PlacesToolbarHelper.customizeStart(); + DownloadsButton.customizeStart(); + + // The additional padding on the sides of the browser + // can cause the customize tab to get clipped. + let tabContainer = gBrowser.tabContainer; + if (tabContainer.getAttribute("overflow") == "true") { + let tabstrip = tabContainer.mTabstrip; + tabstrip.ensureElementIsVisible(gBrowser.selectedTab, true); + } + }, + + _customizationChange: function() { + PlacesToolbarHelper.customizeChange(); + }, + + _customizationEnding: function(aDetails) { + // Update global UI elements that may have been added or removed + if (aDetails.changed) { + gURLBar = document.getElementById("urlbar"); + + gHomeButton.updateTooltip(); + XULBrowserWindow.init(); + + if (AppConstants.platform != "macosx") + updateEditUIVisibility(); + + // Hacky: update the PopupNotifications' object's reference to the iconBox, + // if it already exists, since it may have changed if the URL bar was + // added/removed. + if (!window.__lookupGetter__("PopupNotifications")) { + PopupNotifications.iconBox = + document.getElementById("notification-popup-box"); + } + + } + + PlacesToolbarHelper.customizeDone(); + DownloadsButton.customizeDone(); + + // The url bar splitter state is dependent on whether stop/reload + // and the location bar are combined, so we need this ordering + CombinedStopReload.init(); + UpdateUrlbarSearchSplitterState(); + + // Update the urlbar + URLBarSetURI(); + XULBrowserWindow.asyncUpdateUI(); + + // Re-enable parts of the UI we disabled during the dialog + let menubar = document.getElementById("main-menubar"); + for (let childNode of menubar.childNodes) + childNode.setAttribute("disabled", false); + let cmd = document.getElementById("cmd_CustomizeToolbars"); + cmd.removeAttribute("disabled"); + + gBrowser.selectedBrowser.focus(); + } +} diff --git a/application/basilisk/base/content/browser-data-submission-info-bar.js b/application/basilisk/base/content/browser-data-submission-info-bar.js new file mode 100644 index 000000000..0c87d199f --- /dev/null +++ b/application/basilisk/base/content/browser-data-submission-info-bar.js @@ -0,0 +1,127 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const LOGGER_NAME = "Toolkit.Telemetry"; +const LOGGER_PREFIX = "DataNotificationInfoBar::"; + +/** + * Represents an info bar that shows a data submission notification. + */ +var gDataNotificationInfoBar = { + _OBSERVERS: [ + "datareporting:notify-data-policy:request", + "datareporting:notify-data-policy:close", + ], + + _DATA_REPORTING_NOTIFICATION: "data-reporting", + + get _notificationBox() { + delete this._notificationBox; + return this._notificationBox = document.getElementById("global-notificationbox"); + }, + + get _log() { + let Log = Cu.import("resource://gre/modules/Log.jsm", {}).Log; + delete this._log; + return this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX); + }, + + init: function() { + window.addEventListener("unload", () => { + for (let o of this._OBSERVERS) { + Services.obs.removeObserver(this, o); + } + }, false); + + for (let o of this._OBSERVERS) { + Services.obs.addObserver(this, o, true); + } + }, + + _getDataReportingNotification: function (name=this._DATA_REPORTING_NOTIFICATION) { + return this._notificationBox.getNotificationWithValue(name); + }, + + _displayDataPolicyInfoBar: function (request) { + if (this._getDataReportingNotification()) { + return; + } + + let brandBundle = document.getElementById("bundle_brand"); + let appName = brandBundle.getString("brandShortName"); + let vendorName = brandBundle.getString("vendorShortName"); + + let message = gNavigatorBundle.getFormattedString( + "dataReportingNotification.message", + [appName, vendorName]); + + this._actionTaken = false; + + let buttons = [{ + label: gNavigatorBundle.getString("dataReportingNotification.button.label"), + accessKey: gNavigatorBundle.getString("dataReportingNotification.button.accessKey"), + popup: null, + callback: () => { + this._actionTaken = true; + window.openAdvancedPreferences("dataChoicesTab"); + }, + }]; + + this._log.info("Creating data reporting policy notification."); + this._notificationBox.appendNotification( + message, + this._DATA_REPORTING_NOTIFICATION, + null, + this._notificationBox.PRIORITY_INFO_HIGH, + buttons, + event => { + if (event == "removed") { + Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close", null); + } + } + ); + // It is important to defer calling onUserNotifyComplete() until we're + // actually sure the notification was displayed. If we ever called + // onUserNotifyComplete() without showing anything to the user, that + // would be very good for user choice. It may also have legal impact. + request.onUserNotifyComplete(); + }, + + _clearPolicyNotification: function () { + let notification = this._getDataReportingNotification(); + if (notification) { + this._log.debug("Closing notification."); + notification.close(); + } + }, + + observe: function(subject, topic, data) { + switch (topic) { + case "datareporting:notify-data-policy:request": + let request = subject.wrappedJSObject.object; + try { + this._displayDataPolicyInfoBar(request); + } catch (ex) { + request.onUserNotifyFailed(ex); + return; + } + break; + + case "datareporting:notify-data-policy:close": + // If this observer fires, it means something else took care of + // responding. Therefore, we don't need to do anything. So, we + // act like we took action and clear state. + this._actionTaken = true; + this._clearPolicyNotification(); + break; + + default: + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIObserver, + Ci.nsISupportsWeakReference, + ]), +}; diff --git a/application/basilisk/base/content/browser-devedition.js b/application/basilisk/base/content/browser-devedition.js new file mode 100644 index 000000000..0dc1e94da --- /dev/null +++ b/application/basilisk/base/content/browser-devedition.js @@ -0,0 +1,142 @@ +/* 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/. */ + +/** + * Listeners for the DevEdition theme. This adds an extra stylesheet + * to browser.xul if a pref is set and no other themes are applied. + */ +var DevEdition = { + _devtoolsThemePrefName: "devtools.theme", + styleSheetLocation: "chrome://browser/skin/devedition.css", + styleSheet: null, + initialized: false, + + get isStyleSheetEnabled() { + return this.styleSheet && !this.styleSheet.sheet.disabled; + }, + + get isThemeCurrentlyApplied() { + let theme = LightweightThemeManager.currentTheme; + return theme && theme.id == "firefox-devedition@mozilla.org"; + }, + + init: function () { + this.initialized = true; + Services.prefs.addObserver(this._devtoolsThemePrefName, this, false); + Services.obs.addObserver(this, "lightweight-theme-styling-update", false); + Services.obs.addObserver(this, "lightweight-theme-window-updated", false); + this._updateDevtoolsThemeAttribute(); + + if (this.isThemeCurrentlyApplied) { + this._toggleStyleSheet(true); + } + }, + + createStyleSheet: function() { + let styleSheetAttr = `href="${this.styleSheetLocation}" type="text/css"`; + this.styleSheet = document.createProcessingInstruction( + "xml-stylesheet", styleSheetAttr); + this.styleSheet.addEventListener("load", this); + document.insertBefore(this.styleSheet, document.documentElement); + this.styleSheet.sheet.disabled = true; + }, + + observe: function (subject, topic, data) { + if (topic == "lightweight-theme-styling-update") { + let newTheme = JSON.parse(data); + if (newTheme && newTheme.id == "firefox-devedition@mozilla.org") { + this._toggleStyleSheet(true); + } else { + this._toggleStyleSheet(false); + } + } else if (topic == "lightweight-theme-window-updated" && subject == window) { + this._updateLWTBrightness(); + } + + if (topic == "nsPref:changed" && data == this._devtoolsThemePrefName) { + this._updateDevtoolsThemeAttribute(); + } + }, + + _inferBrightness: function() { + ToolbarIconColor.inferFromText(); + // Get an inverted full screen button if the dark theme is applied. + if (this.isStyleSheetEnabled && + document.documentElement.getAttribute("devtoolstheme") == "dark") { + document.documentElement.setAttribute("brighttitlebarforeground", "true"); + } else { + document.documentElement.removeAttribute("brighttitlebarforeground"); + } + }, + + _updateLWTBrightness() { + if (this.isThemeCurrentlyApplied) { + let devtoolsTheme = Services.prefs.getCharPref(this._devtoolsThemePrefName); + let textColor = devtoolsTheme == "dark" ? "bright" : "dark"; + document.documentElement.setAttribute("lwthemetextcolor", textColor); + } + }, + + _updateDevtoolsThemeAttribute: function() { + // Set an attribute on root element to make it possible + // to change colors based on the selected devtools theme. + let devtoolsTheme = Services.prefs.getCharPref(this._devtoolsThemePrefName); + if (devtoolsTheme != "dark") { + devtoolsTheme = "light"; + } + document.documentElement.setAttribute("devtoolstheme", devtoolsTheme); + this._updateLWTBrightness(); + this._inferBrightness(); + }, + + handleEvent: function(e) { + if (e.type === "load") { + this.styleSheet.removeEventListener("load", this); + this.refreshBrowserDisplay(); + } + }, + + refreshBrowserDisplay: function() { + // Don't touch things on the browser if gBrowserInit.onLoad hasn't + // yet fired. + if (this.initialized) { + gBrowser.tabContainer._positionPinnedTabs(); + this._inferBrightness(); + } + }, + + _toggleStyleSheet: function(deveditionThemeEnabled) { + let wasEnabled = this.isStyleSheetEnabled; + if (deveditionThemeEnabled && !wasEnabled) { + // The stylesheet may not have been created yet if it wasn't + // needed on initial load. Make it now. + if (!this.styleSheet) { + this.createStyleSheet(); + } + this.styleSheet.sheet.disabled = false; + this.refreshBrowserDisplay(); + } else if (!deveditionThemeEnabled && wasEnabled) { + this.styleSheet.sheet.disabled = true; + this.refreshBrowserDisplay(); + } + }, + + uninit: function () { + Services.prefs.removeObserver(this._devtoolsThemePrefName, this); + Services.obs.removeObserver(this, "lightweight-theme-styling-update", false); + Services.obs.removeObserver(this, "lightweight-theme-window-updated", false); + if (this.styleSheet) { + this.styleSheet.removeEventListener("load", this); + } + this.styleSheet = null; + } +}; + +// If the DevEdition theme is going to be applied in gBrowserInit.onLoad, +// then preload it now. This prevents a flash of unstyled content where the +// normal theme is applied while the DevEdition stylesheet is loading. +if (!AppConstants.RELEASE_OR_BETA && + this != Services.appShell.hiddenDOMWindow && DevEdition.isThemeCurrentlyApplied) { + DevEdition.createStyleSheet(); +} diff --git a/application/basilisk/base/content/browser-doctype.inc b/application/basilisk/base/content/browser-doctype.inc new file mode 100644 index 000000000..ad08f4b03 --- /dev/null +++ b/application/basilisk/base/content/browser-doctype.inc @@ -0,0 +1,25 @@ +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" > +%browserDTD; +<!ENTITY % baseMenuDTD SYSTEM "chrome://browser/locale/baseMenuOverlay.dtd" > +%baseMenuDTD; +<!ENTITY % charsetDTD SYSTEM "chrome://global/locale/charsetMenu.dtd" > +%charsetDTD; +<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" > +%textcontextDTD; +<!ENTITY % customizeToolbarDTD SYSTEM "chrome://global/locale/customizeToolbar.dtd"> + %customizeToolbarDTD; +<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd"> +%placesDTD; +#ifdef MOZ_SAFE_BROWSING +<!ENTITY % safebrowsingDTD SYSTEM "chrome://browser/locale/safebrowsing/phishing-afterload-warning-message.dtd"> +%safebrowsingDTD; +#endif +<!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd"> +%aboutHomeDTD; +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +%syncBrandDTD; +]> + diff --git a/application/basilisk/base/content/browser-feeds.js b/application/basilisk/base/content/browser-feeds.js new file mode 100644 index 000000000..6f29d8915 --- /dev/null +++ b/application/basilisk/base/content/browser-feeds.js @@ -0,0 +1,646 @@ +/* -*- 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/. */ + +XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", + "resource://gre/modules/DeferredTask.jsm"); + +const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; +const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; +const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; + +const PREF_SHOW_FIRST_RUN_UI = "browser.feeds.showFirstRunUI"; + +const PREF_SELECTED_APP = "browser.feeds.handlers.application"; +const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; +const PREF_SELECTED_ACTION = "browser.feeds.handler"; +const PREF_SELECTED_READER = "browser.feeds.handler.default"; + +const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application"; +const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; +const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler"; +const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default"; + +const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application"; +const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice"; +const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler"; +const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default"; + +const PREF_UPDATE_DELAY = 2000; + +const SETTABLE_PREFS = new Set([ + PREF_VIDEO_SELECTED_ACTION, + PREF_AUDIO_SELECTED_ACTION, + PREF_SELECTED_ACTION, + PREF_VIDEO_SELECTED_READER, + PREF_AUDIO_SELECTED_READER, + PREF_SELECTED_READER, + PREF_VIDEO_SELECTED_WEB, + PREF_AUDIO_SELECTED_WEB, + PREF_SELECTED_WEB +]); + +const EXECUTABLE_PREFS = new Set([ + PREF_SELECTED_APP, + PREF_VIDEO_SELECTED_APP, + PREF_AUDIO_SELECTED_APP +]); + +const VALID_ACTIONS = new Set(["ask", "reader", "bookmarks"]); +const VALID_READERS = new Set(["web", "client", "default", "bookmarks"]); + +XPCOMUtils.defineLazyPreferenceGetter(this, "SHOULD_LOG", + "feeds.log", false); + +function LOG(str) { + if (SHOULD_LOG) + dump("*** Feeds: " + str + "\n"); +} + +function getPrefActionForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_ACTION; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_ACTION; + + default: + return PREF_SELECTED_ACTION; + } +} + +function getPrefReaderForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_READER; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_READER; + + default: + return PREF_SELECTED_READER; + } +} + +function getPrefWebForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_WEB; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_WEB; + + default: + return PREF_SELECTED_WEB; + } +} + +function getPrefAppForType(t) { + switch (t) { + case Ci.nsIFeed.TYPE_VIDEO: + return PREF_VIDEO_SELECTED_APP; + + case Ci.nsIFeed.TYPE_AUDIO: + return PREF_AUDIO_SELECTED_APP; + + default: + return PREF_SELECTED_APP; + } +} + +/** + * Maps a feed type to a maybe-feed mimetype. + */ +function getMimeTypeForFeedType(aFeedType) { + switch (aFeedType) { + case Ci.nsIFeed.TYPE_VIDEO: + return TYPE_MAYBE_VIDEO_FEED; + + case Ci.nsIFeed.TYPE_AUDIO: + return TYPE_MAYBE_AUDIO_FEED; + + default: + return TYPE_MAYBE_FEED; + } +} + +/** + * The Feed Handler object manages discovery of RSS/ATOM feeds in web pages + * and shows UI when they are discovered. + */ +var FeedHandler = { + _prefChangeCallback: null, + + /** Called when the user clicks on the Subscribe to This Page... menu item, + * or when the user clicks the feed button when the page contains multiple + * feeds. + * Builds a menu of unique feeds associated with the page, and if there + * is only one, shows the feed inline in the browser window. + * @param container + * The feed list container (menupopup or subview) to be populated. + * @param isSubview + * Whether we're creating a subview (true) or menu (false/undefined) + * @return true if the menu/subview should be shown, false if there was only + * one feed and the feed should be shown inline in the browser + * window (do not show the menupopup/subview). + */ + buildFeedList(container, isSubview) { + let feeds = gBrowser.selectedBrowser.feeds; + if (!isSubview && feeds == null) { + // XXX hack -- menu opening depends on setting of an "open" + // attribute, and the menu refuses to open if that attribute is + // set (because it thinks it's already open). onpopupshowing gets + // called after the attribute is unset, and it doesn't get unset + // if we return false. so we unset it here; otherwise, the menu + // refuses to work past this point. + container.parentNode.removeAttribute("open"); + return false; + } + + for (let i = container.childNodes.length - 1; i >= 0; --i) { + let node = container.childNodes[i]; + if (isSubview && node.localName == "label") + continue; + container.removeChild(node); + } + + if (!feeds || feeds.length <= 1) + return false; + + // Build the menu showing the available feed choices for viewing. + let itemNodeType = isSubview ? "toolbarbutton" : "menuitem"; + for (let feedInfo of feeds) { + let item = document.createElement(itemNodeType); + let baseTitle = feedInfo.title || feedInfo.href; + item.setAttribute("label", baseTitle); + item.setAttribute("feed", feedInfo.href); + item.setAttribute("tooltiptext", feedInfo.href); + item.setAttribute("crop", "center"); + let className = "feed-" + itemNodeType; + if (isSubview) { + className += " subviewbutton"; + } + item.setAttribute("class", className); + container.appendChild(item); + } + return true; + }, + + /** + * Subscribe to a given feed. Called when + * 1. Page has a single feed and user clicks feed icon in location bar + * 2. Page has a single feed and user selects Subscribe menu item + * 3. Page has multiple feeds and user selects from feed icon popup (or subview) + * 4. Page has multiple feeds and user selects from Subscribe submenu + * @param href + * The feed to subscribe to. May be null, in which case the + * event target's feed attribute is examined. + * @param event + * The event this method is handling. Used to decide where + * to open the preview UI. (Optional, unless href is null) + */ + subscribeToFeed(href, event) { + // Just load the feed in the content area to either subscribe or show the + // preview UI + if (!href) + href = event.target.getAttribute("feed"); + urlSecurityCheck(href, gBrowser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + let feedURI = makeURI(href, document.characterSet); + // Use the feed scheme so X-Moz-Is-Feed will be set + // The value doesn't matter + if (/^https?$/.test(feedURI.scheme)) + href = "feed:" + href; + this.loadFeed(href, event); + }, + + loadFeed(href, event) { + let feeds = gBrowser.selectedBrowser.feeds; + try { + openUILink(href, event, { ignoreAlt: true }); + } + finally { + // We might default to a livebookmarks modal dialog, + // so reset that if the user happens to click it again + gBrowser.selectedBrowser.feeds = feeds; + } + }, + + get _feedMenuitem() { + delete this._feedMenuitem; + return this._feedMenuitem = document.getElementById("singleFeedMenuitemState"); + }, + + get _feedMenupopup() { + delete this._feedMenupopup; + return this._feedMenupopup = document.getElementById("multipleFeedsMenuState"); + }, + + /** + * Update the browser UI to show whether or not feeds are available when + * a page is loaded or the user switches tabs to a page that has feeds. + */ + updateFeeds() { + if (this._updateFeedTimeout) + clearTimeout(this._updateFeedTimeout); + + let feeds = gBrowser.selectedBrowser.feeds; + let haveFeeds = feeds && feeds.length > 0; + + let feedButton = document.getElementById("feed-button"); + if (feedButton) { + if (haveFeeds) { + feedButton.removeAttribute("disabled"); + } else { + feedButton.setAttribute("disabled", "true"); + } + } + + if (!haveFeeds) { + this._feedMenuitem.setAttribute("disabled", "true"); + this._feedMenuitem.removeAttribute("hidden"); + this._feedMenupopup.setAttribute("hidden", "true"); + return; + } + + if (feeds.length > 1) { + this._feedMenuitem.setAttribute("hidden", "true"); + this._feedMenupopup.removeAttribute("hidden"); + } else { + this._feedMenuitem.setAttribute("feed", feeds[0].href); + this._feedMenuitem.removeAttribute("disabled"); + this._feedMenuitem.removeAttribute("hidden"); + this._feedMenupopup.setAttribute("hidden", "true"); + } + }, + + addFeed(link, browserForLink) { + if (!browserForLink.feeds) + browserForLink.feeds = []; + + browserForLink.feeds.push({ href: link.href, title: link.title }); + + // If this addition was for the current browser, update the UI. For + // background browsers, we'll update on tab switch. + if (browserForLink == gBrowser.selectedBrowser) { + // Batch updates to avoid updating the UI for multiple onLinkAdded events + // fired within 100ms of each other. + if (this._updateFeedTimeout) + clearTimeout(this._updateFeedTimeout); + this._updateFeedTimeout = setTimeout(this.updateFeeds.bind(this), 100); + } + }, + + /** + * Get the human-readable display name of a file. This could be the + * application name. + * @param file + * A nsIFile to look up the name of + * @return The display name of the application represented by the file. + */ + _getFileDisplayName(file) { + switch (AppConstants.platform) { + case "win": + if (file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } + break; + case "macosx": + if (file instanceof Ci.nsILocalFileMac) { + try { + return file.bundleDisplayName; + } catch (e) {} + } + break; + } + + return file.leafName; + }, + + _chooseClientApp(aTitle, aTypeName, aBrowser) { + const prefName = getPrefAppForType(aTypeName); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + + fp.init(window, aTitle, Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + + fp.open((aResult) => { + if (aResult == Ci.nsIFilePicker.returnOK) { + let selectedApp = fp.file; + if (selectedApp) { + // XXXben - we need to compare this with the running instance + // executable just don't know how to do that via script + // XXXmano TBD: can probably add this to nsIShellService + let appName = ""; + switch (AppConstants.platform) { + case "win": + appName = AppConstants.MOZ_APP_NAME + ".exe"; + break; + case "macosx": + appName = AppConstants.MOZ_MACBUNDLE_NAME; + break; + default: + appName = AppConstants.MOZ_APP_NAME + "-bin"; + break; + } + + if (fp.file.leafName != appName) { + Services.prefs.setComplexValue(prefName, Ci.nsILocalFile, selectedApp); + aBrowser.messageManager.sendAsyncMessage("FeedWriter:SetApplicationLauncherMenuItem", + { name: this._getFileDisplayName(selectedApp), + type: "SelectedAppMenuItem" }); + } + } + } + }); + + }, + + executeClientApp(aSpec, aTitle, aSubtitle, aFeedHandler) { + // aFeedHandler is either "default", indicating the system default reader, or a pref-name containing + // an nsILocalFile pointing to the feed handler's executable. + + let clientApp = null; + if (aFeedHandler == "default") { + clientApp = Cc["@mozilla.org/browser/shell-service;1"] + .getService(Ci.nsIShellService) + .defaultFeedReader; + } else { + clientApp = Services.prefs.getComplexValue(aFeedHandler, Ci.nsILocalFile); + } + + // For the benefit of applications that might know how to deal with more + // URLs than just feeds, send feed: URLs in the following format: + // + // http urls: replace scheme with feed, e.g. + // http://foo.com/index.rdf -> feed://foo.com/index.rdf + // other urls: prepend feed: scheme, e.g. + // https://foo.com/index.rdf -> feed:https://foo.com/index.rdf + let feedURI = NetUtil.newURI(aSpec); + if (feedURI.schemeIs("http")) { + feedURI.scheme = "feed"; + aSpec = feedURI.spec; + } else { + aSpec = "feed:" + aSpec; + } + + // Retrieving the shell service might fail on some systems, most + // notably systems where GNOME is not installed. + try { + let ss = Cc["@mozilla.org/browser/shell-service;1"] + .getService(Ci.nsIShellService); + ss.openApplicationWithURI(clientApp, aSpec); + } catch (e) { + // If we couldn't use the shell service, fallback to using a + // nsIProcess instance + let p = Cc["@mozilla.org/process/util;1"] + .createInstance(Ci.nsIProcess); + p.init(clientApp); + p.run(false, [aSpec], 1); + } + }, + + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + init() { + window.messageManager.addMessageListener("FeedWriter:ChooseClientApp", this); + window.messageManager.addMessageListener("FeedWriter:GetSubscriptionUI", this); + window.messageManager.addMessageListener("FeedWriter:SetFeedPrefsAndSubscribe", this); + window.messageManager.addMessageListener("FeedWriter:ShownFirstRun", this); + + Services.ppmm.addMessageListener("FeedConverter:ExecuteClientApp", this); + + const prefs = Services.prefs; + prefs.addObserver(PREF_SELECTED_ACTION, this, true); + prefs.addObserver(PREF_SELECTED_READER, this, true); + prefs.addObserver(PREF_SELECTED_WEB, this, true); + prefs.addObserver(PREF_VIDEO_SELECTED_ACTION, this, true); + prefs.addObserver(PREF_VIDEO_SELECTED_READER, this, true); + prefs.addObserver(PREF_VIDEO_SELECTED_WEB, this, true); + prefs.addObserver(PREF_AUDIO_SELECTED_ACTION, this, true); + prefs.addObserver(PREF_AUDIO_SELECTED_READER, this, true); + prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this, true); + }, + + uninit() { + Services.ppmm.removeMessageListener("FeedConverter:ExecuteClientApp", this); + + this._prefChangeCallback = null; + }, + + // nsIObserver + observe(subject, topic, data) { + if (topic == "nsPref:changed") { + LOG(`Pref changed ${data}`) + if (this._prefChangeCallback) { + this._prefChangeCallback.disarm(); + } + // Multiple prefs are set at the same time, debounce to reduce noise + // This can happen in one feed and we want to message all feed pages + this._prefChangeCallback = new DeferredTask(() => { + this._prefChanged(data); + }, PREF_UPDATE_DELAY); + this._prefChangeCallback.arm(); + } + }, + + _prefChanged(prefName) { + // Don't observe for PREF_*SELECTED_APP as user likely just picked one + // That is also handled by SetApplicationLauncherMenuItem call + // Rather than the others which happen on subscription + switch (prefName) { + case PREF_SELECTED_READER: + case PREF_SELECTED_WEB: + case PREF_VIDEO_SELECTED_READER: + case PREF_VIDEO_SELECTED_WEB: + case PREF_AUDIO_SELECTED_READER: + case PREF_AUDIO_SELECTED_WEB: + case PREF_SELECTED_ACTION: + case PREF_VIDEO_SELECTED_ACTION: + case PREF_AUDIO_SELECTED_ACTION: + const response = { + default: this._getReaderForType(Ci.nsIFeed.TYPE_FEED), + [Ci.nsIFeed.TYPE_AUDIO]: this._getReaderForType(Ci.nsIFeed.TYPE_AUDIO), + [Ci.nsIFeed.TYPE_VIDEO]: this._getReaderForType(Ci.nsIFeed.TYPE_VIDEO) + }; + Services.mm.broadcastAsyncMessage("FeedWriter:PreferenceUpdated", + response); + break; + } + }, + + _initSubscriptionUIResponse(feedType) { + const wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + const handlersRaw = wccr.getContentHandlers(getMimeTypeForFeedType(feedType)); + const handlers = []; + for (let handler of handlersRaw) { + LOG(`Handler found: ${handler}`); + handlers.push({ + name: handler.name, + uri: handler.uri + }); + } + let showFirstRunUI = true; + // eslint-disable-next-line mozilla/use-default-preference-values + try { + showFirstRunUI = Services.prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI); + } catch (ex) { } + const response = { handlers, showFirstRunUI }; + let selectedClientApp; + const feedTypePref = getPrefAppForType(feedType); + try { + selectedClientApp = Services.prefs.getComplexValue(feedTypePref, Ci.nsILocalFile); + } catch (ex) { + // Just do nothing, then we won't bother populating + } + + let defaultClientApp = null; + try { + // This can sometimes not exist + defaultClientApp = Cc["@mozilla.org/browser/shell-service;1"] + .getService(Ci.nsIShellService) + .defaultFeedReader; + } catch (ex) { + // Just do nothing, then we don't bother populating + } + + if (selectedClientApp && selectedClientApp.exists()) { + if (defaultClientApp && selectedClientApp.path != defaultClientApp.path) { + // Only set the default menu item if it differs from the selected one + response.defaultMenuItem = this._getFileDisplayName(defaultClientApp); + } + response.selectedMenuItem = this._getFileDisplayName(selectedClientApp); + } + response.reader = this._getReaderForType(feedType); + return response; + }, + + _setPref(aPrefName, aPrefValue, aIsComplex = false) { + LOG(`FeedWriter._setPref ${aPrefName}`); + // Ensure we have a pref that is settable + if (aPrefName && SETTABLE_PREFS.has(aPrefName)) { + if (aIsComplex) { + const supportsString = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + supportsString.data = aPrefValue; + Services.prefs.setComplexValue(aPrefName, Ci.nsISupportsString, supportsString); + } else { + Services.prefs.setCharPref(aPrefName, aPrefValue); + } + } else { + LOG(`FeedWriter._setPref ${aPrefName} not allowed`); + } + }, + + _getReaderForType(feedType) { + let prefs = Services.prefs; + let handler = "bookmarks"; + let url; + // eslint-disable-next-line mozilla/use-default-preference-values + try { + handler = prefs.getCharPref(getPrefReaderForType(feedType)); + } catch (ex) { } + + if (handler === "web") { + try { + url = prefs.getComplexValue(getPrefWebForType(feedType), Ci.nsISupportsString).data; + } catch (ex) { + LOG("FeedWriter._setSelectedHandler: invalid or no handler in prefs"); + url = null; + } + } + const alwaysUse = this._getAlwaysUseState(feedType); + const action = prefs.getCharPref(getPrefActionForType(feedType)); + return { handler, url, alwaysUse, action }; + }, + + _getAlwaysUseState(feedType) { + try { + return Services.prefs.getCharPref(getPrefActionForType(feedType)) != "ask"; + } catch (ex) { } + return false; + }, + + receiveMessage(msg) { + let handler; + switch (msg.name) { + case "FeedWriter:GetSubscriptionUI": + const response = this._initSubscriptionUIResponse(msg.data.feedType); + msg.target.messageManager + .sendAsyncMessage("FeedWriter:GetSubscriptionUIResponse", + response); + break; + case "FeedWriter:ChooseClientApp": + this._chooseClientApp(msg.data.title, msg.data.feedType, msg.target); + break; + case "FeedWriter:ShownFirstRun": + Services.prefs.setBoolPref(PREF_SHOW_FIRST_RUN_UI, false); + break; + case "FeedWriter:SetFeedPrefsAndSubscribe": + const settings = msg.data; + if (!settings.action || !VALID_ACTIONS.has(settings.action)) { + LOG(`Invalid action ${settings.action}`); + return; + } + if (!settings.reader || !VALID_READERS.has(settings.reader)) { + LOG(`Invalid reader ${settings.reader}`); + return; + } + const actionPref = getPrefActionForType(settings.feedType); + this._setPref(actionPref, settings.action); + const readerPref = getPrefReaderForType(settings.feedType); + this._setPref(readerPref, settings.reader); + handler = null; + + switch (settings.reader) { + case "web": + // This is a web set URI by content using window.registerContentHandler() + // Lets make sure we know about it before setting it + const webPref = getPrefWebForType(settings.feedType); + let wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. + getService(Ci.nsIWebContentConverterService); + // If the user provided an invalid web URL this function won't give us a reference + handler = wccr.getWebContentHandlerByURI(getMimeTypeForFeedType(settings.feedType), settings.uri); + if (handler) { + this._setPref(webPref, settings.uri, true); + if (settings.useAsDefault) { + wccr.setAutoHandler(getMimeTypeForFeedType(settings.feedType), handler); + } + msg.target.messageManager + .sendAsyncMessage("FeedWriter:SetFeedPrefsAndSubscribeResponse", + { redirect: handler.getHandlerURI(settings.feedLocation) }); + } else { + LOG(`No handler found for web ${settings.feedType} ${settings.uri}`); + } + break; + default: + const feedService = Cc["@mozilla.org/browser/feeds/result-service;1"]. + getService(Ci.nsIFeedResultService); + + feedService.addToClientReader(settings.feedLocation, + settings.feedTitle, + settings.feedSubtitle, + settings.feedType, + settings.reader); + } + break; + case "FeedConverter:ExecuteClientApp": + // Always check feedHandler is from a set array of executable prefs + if (EXECUTABLE_PREFS.has(msg.data.feedHandler)) { + this.executeClientApp(msg.data.spec, msg.data.title, + msg.data.subtitle, msg.data.feedHandler); + } else { + LOG(`FeedConverter:ExecuteClientApp - Will not exec ${msg.data.feedHandler}`); + } + break; + } + }, +}; diff --git a/application/basilisk/base/content/browser-fullScreenAndPointerLock.js b/application/basilisk/base/content/browser-fullScreenAndPointerLock.js new file mode 100644 index 000000000..497e51121 --- /dev/null +++ b/application/basilisk/base/content/browser-fullScreenAndPointerLock.js @@ -0,0 +1,673 @@ +/* -*- 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 PointerlockFsWarning = { + + _element: null, + _origin: null, + + init: function() { + this.Timeout.prototype = { + start: function() { + this.cancel(); + this._id = setTimeout(() => this._handle(), this._delay); + }, + cancel: function() { + if (this._id) { + clearTimeout(this._id); + this._id = 0; + } + }, + _handle: function() { + this._id = 0; + this._func(); + }, + get delay() { + return this._delay; + } + }; + }, + + /** + * Timeout object for managing timeout request. If it is started when + * the previous call hasn't finished, it would automatically cancelled + * the previous one. + */ + Timeout: function(func, delay) { + this._id = 0; + this._func = func; + this._delay = delay; + }, + + showPointerLock: function(aOrigin) { + if (!document.fullscreen) { + let timeout = gPrefService.getIntPref("pointer-lock-api.warning.timeout"); + this.show(aOrigin, "pointerlock-warning", timeout, 0); + } + }, + + showFullScreen: function(aOrigin) { + let timeout = gPrefService.getIntPref("full-screen-api.warning.timeout"); + let delay = gPrefService.getIntPref("full-screen-api.warning.delay"); + this.show(aOrigin, "fullscreen-warning", timeout, delay); + }, + + // Shows a warning that the site has entered fullscreen or + // pointer lock for a short duration. + show: function(aOrigin, elementId, timeout, delay) { + + if (!this._element) { + this._element = document.getElementById(elementId); + // Setup event listeners + this._element.addEventListener("transitionend", this); + window.addEventListener("mousemove", this, true); + // The timeout to hide the warning box after a while. + this._timeoutHide = new this.Timeout(() => { + this._state = "hidden"; + }, timeout); + // The timeout to show the warning box when the pointer is at the top + this._timeoutShow = new this.Timeout(() => { + this._state = "ontop"; + this._timeoutHide.start(); + }, delay); + } + + // Set the strings on the warning UI. + if (aOrigin) { + this._origin = aOrigin; + } + let uri = BrowserUtils.makeURI(this._origin); + let host = null; + try { + host = uri.host; + } catch (e) { } + let textElem = this._element.querySelector(".pointerlockfswarning-domain-text"); + if (!host) { + textElem.setAttribute("hidden", true); + } else { + textElem.removeAttribute("hidden"); + let hostElem = this._element.querySelector(".pointerlockfswarning-domain"); + // Document's principal's URI has a host. Display a warning including it. + let utils = {}; + Cu.import("resource://gre/modules/DownloadUtils.jsm", utils); + hostElem.textContent = utils.DownloadUtils.getURIHost(uri.spec)[0]; + } + + this._element.dataset.identity = + gIdentityHandler.pointerlockFsWarningClassName; + + // User should be allowed to explicitly disable + // the prompt if they really want. + if (this._timeoutHide.delay <= 0) { + return; + } + + // Explicitly set the last state to hidden to avoid the warning + // box being hidden immediately because of mousemove. + this._state = "onscreen"; + this._lastState = "hidden"; + this._timeoutHide.start(); + }, + + close: function() { + if (!this._element) { + return; + } + // Cancel any pending timeout + this._timeoutHide.cancel(); + this._timeoutShow.cancel(); + // Reset state of the warning box + this._state = "hidden"; + this._element.setAttribute("hidden", true); + // Remove all event listeners + this._element.removeEventListener("transitionend", this); + window.removeEventListener("mousemove", this, true); + // Clear fields + this._element = null; + this._timeoutHide = null; + this._timeoutShow = null; + + // Ensure focus switches away from the (now hidden) warning box. + // If the user clicked buttons in the warning box, it would have + // been focused, and any key events would be directed at the (now + // hidden) chrome document instead of the target document. + gBrowser.selectedBrowser.focus(); + }, + + // State could be one of "onscreen", "ontop", "hiding", and + // "hidden". Setting the state to "onscreen" and "ontop" takes + // effect immediately, while setting it to "hidden" actually + // turns the state to "hiding" before the transition finishes. + _lastState: null, + _STATES: ["hidden", "ontop", "onscreen"], + get _state() { + for (let state of this._STATES) { + if (this._element.hasAttribute(state)) { + return state; + } + } + return "hiding"; + }, + set _state(newState) { + let currentState = this._state; + if (currentState == newState) { + return; + } + if (currentState != "hiding") { + this._lastState = currentState; + this._element.removeAttribute(currentState); + } + if (newState != "hidden") { + if (currentState != "hidden") { + this._element.setAttribute(newState, true); + } else { + // When the previous state is hidden, the display was none, + // thus no box was constructed. We need to wait for the new + // display value taking effect first, otherwise, there won't + // be any transition. Since requestAnimationFrame callback is + // generally triggered before any style flush and layout, we + // should wait for the second animation frame. + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (this._element) { + this._element.setAttribute(newState, true); + } + }); + }); + } + } + }, + + handleEvent: function(event) { + switch (event.type) { + case "mousemove": { + let state = this._state; + if (state == "hidden") { + // If the warning box is currently hidden, show it after + // a short delay if the pointer is at the top. + if (event.clientY != 0) { + this._timeoutShow.cancel(); + } else if (this._timeoutShow.delay >= 0) { + this._timeoutShow.start(); + } + } else { + let elemRect = this._element.getBoundingClientRect(); + if (state == "hiding" && this._lastState != "hidden") { + // If we are on the hiding transition, and the pointer + // moved near the box, restore to the previous state. + if (event.clientY <= elemRect.bottom + 50) { + this._state = this._lastState; + this._timeoutHide.start(); + } + } else if (state == "ontop" || this._lastState != "hidden") { + // State being "ontop" or the previous state not being + // "hidden" indicates this current warning box is shown + // in response to user's action. Hide it immediately when + // the pointer leaves that area. + if (event.clientY > elemRect.bottom + 50) { + this._state = "hidden"; + this._timeoutHide.cancel(); + } + } + } + break; + } + case "transitionend": { + if (this._state == "hiding") { + this._element.setAttribute("hidden", true); + } + break; + } + } + } +}; + +var PointerLock = { + + init: function() { + window.messageManager.addMessageListener("PointerLock:Entered", this); + window.messageManager.addMessageListener("PointerLock:Exited", this); + }, + + receiveMessage: function(aMessage) { + switch (aMessage.name) { + case "PointerLock:Entered": { + PointerlockFsWarning.showPointerLock(aMessage.data.originNoSuffix); + break; + } + case "PointerLock:Exited": { + PointerlockFsWarning.close(); + break; + } + } + } +}; + +var FullScreen = { + _MESSAGES: [ + "DOMFullscreen:Request", + "DOMFullscreen:NewOrigin", + "DOMFullscreen:Exit", + "DOMFullscreen:Painted", + ], + + init: function() { + // called when we go into full screen, even if initiated by a web page script + window.addEventListener("fullscreen", this, true); + window.addEventListener("MozDOMFullscreen:Entered", this, + /* useCapture */ true, + /* wantsUntrusted */ false); + window.addEventListener("MozDOMFullscreen:Exited", this, + /* useCapture */ true, + /* wantsUntrusted */ false); + for (let type of this._MESSAGES) { + window.messageManager.addMessageListener(type, this); + } + + if (window.fullScreen) + this.toggle(); + }, + + uninit: function() { + for (let type of this._MESSAGES) { + window.messageManager.removeMessageListener(type, this); + } + this.cleanup(); + }, + + toggle: function () { + var enterFS = window.fullScreen; + + // Toggle the View:FullScreen command, which controls elements like the + // fullscreen menuitem, and menubars. + let fullscreenCommand = document.getElementById("View:FullScreen"); + if (enterFS) { + fullscreenCommand.setAttribute("checked", enterFS); + } else { + fullscreenCommand.removeAttribute("checked"); + } + + if (AppConstants.platform == "macosx") { + // Make sure the menu items are adjusted. + document.getElementById("enterFullScreenItem").hidden = enterFS; + document.getElementById("exitFullScreenItem").hidden = !enterFS; + } + + if (!this._fullScrToggler) { + this._fullScrToggler = document.getElementById("fullscr-toggler"); + this._fullScrToggler.addEventListener("mouseover", this._expandCallback, false); + this._fullScrToggler.addEventListener("dragenter", this._expandCallback, false); + this._fullScrToggler.addEventListener("touchmove", this._expandCallback, {passive: true}); + } + + if (enterFS) { + gNavToolbox.setAttribute("inFullscreen", true); + document.documentElement.setAttribute("inFullscreen", true); + if (!document.fullscreenElement && this.useLionFullScreen) + document.documentElement.setAttribute("OSXLionFullscreen", true); + } else { + gNavToolbox.removeAttribute("inFullscreen"); + document.documentElement.removeAttribute("inFullscreen"); + document.documentElement.removeAttribute("OSXLionFullscreen"); + } + + if (!document.fullscreenElement) + this._updateToolbars(enterFS); + + if (enterFS) { + document.addEventListener("keypress", this._keyToggleCallback, false); + document.addEventListener("popupshown", this._setPopupOpen, false); + document.addEventListener("popuphidden", this._setPopupOpen, false); + // In DOM fullscreen mode, we hide toolbars with CSS + if (!document.fullscreenElement) + this.hideNavToolbox(true); + } + else { + this.showNavToolbox(false); + // This is needed if they use the context menu to quit fullscreen + this._isPopupOpen = false; + this.cleanup(); + // In TabsInTitlebar._update(), we cancel the appearance update on + // resize event for exiting fullscreen, since that happens before we + // change the UI here in the "fullscreen" event. Hence we need to + // call it here to ensure the appearance is properly updated. See + // TabsInTitlebar._update() and bug 1173768. + TabsInTitlebar.updateAppearance(true); + } + + if (enterFS && !document.fullscreenElement) { + Services.telemetry.getHistogramById("FX_BROWSER_FULLSCREEN_USED") + .add(1); + } + }, + + exitDomFullScreen : function() { + document.exitFullscreen(); + }, + + handleEvent: function (event) { + switch (event.type) { + case "fullscreen": + this.toggle(); + break; + case "MozDOMFullscreen:Entered": { + // The event target is the element which requested the DOM + // fullscreen. If we were entering DOM fullscreen for a remote + // browser, the target would be `gBrowser` and the original + // target would be the browser which was the parameter of + // `remoteFrameFullscreenChanged` call. If the fullscreen + // request was initiated from an in-process browser, we need + // to get its corresponding browser here. + let browser; + if (event.target == gBrowser) { + browser = event.originalTarget; + } else { + let topWin = event.target.ownerGlobal.top; + browser = gBrowser.getBrowserForContentWindow(topWin); + } + TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS"); + this.enterDomFullscreen(browser); + break; + } + case "MozDOMFullscreen:Exited": + TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS"); + this.cleanupDomFullscreen(); + break; + } + }, + + receiveMessage: function(aMessage) { + let browser = aMessage.target; + switch (aMessage.name) { + case "DOMFullscreen:Request": { + this._windowUtils.remoteFrameFullscreenChanged(browser); + break; + } + case "DOMFullscreen:NewOrigin": { + // Don't show the warning if we've already exited fullscreen. + if (document.fullscreen) { + PointerlockFsWarning.showFullScreen(aMessage.data.originNoSuffix); + } + break; + } + case "DOMFullscreen:Exit": { + this._windowUtils.remoteFrameFullscreenReverted(); + break; + } + case "DOMFullscreen:Painted": { + Services.obs.notifyObservers(window, "fullscreen-painted", ""); + TelemetryStopwatch.finish("FULLSCREEN_CHANGE_MS"); + break; + } + } + }, + + enterDomFullscreen : function(aBrowser) { + + if (!document.fullscreenElement) { + return; + } + + // If we have a current pointerlock warning shown then hide it + // before transition. + PointerlockFsWarning.close(); + + // If it is a remote browser, send a message to ask the content + // to enter fullscreen state. We don't need to do so if it is an + // in-process browser, since all related document should have + // entered fullscreen state at this point. + // This should be done before the active tab check below to ensure + // that the content document handles the pending request. Doing so + // before the check is fine since we also check the activeness of + // the requesting document in content-side handling code. + if (this._isRemoteBrowser(aBrowser)) { + aBrowser.messageManager.sendAsyncMessage("DOMFullscreen:Entered"); + } + + // If we've received a fullscreen notification, we have to ensure that the + // element that's requesting fullscreen belongs to the browser that's currently + // active. If not, we exit fullscreen since the "full-screen document" isn't + // actually visible now. + if (!aBrowser || gBrowser.selectedBrowser != aBrowser || + // The top-level window has lost focus since the request to enter + // full-screen was made. Cancel full-screen. + Services.focus.activeWindow != window) { + // This function is called synchronously in fullscreen change, so + // we have to avoid calling exitFullscreen synchronously here. + setTimeout(() => document.exitFullscreen(), 0); + return; + } + + document.documentElement.setAttribute("inDOMFullscreen", true); + + if (gFindBarInitialized) { + gFindBar.close(true); + } + + // Exit DOM full-screen mode upon open, close, or change tab. + gBrowser.tabContainer.addEventListener("TabOpen", this.exitDomFullScreen); + gBrowser.tabContainer.addEventListener("TabClose", this.exitDomFullScreen); + gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen); + + // Add listener to detect when the fullscreen window is re-focused. + // If a fullscreen window loses focus, we show a warning when the + // fullscreen window is refocused. + window.addEventListener("activate", this); + }, + + cleanup: function () { + if (!window.fullScreen) { + MousePosTracker.removeListener(this); + document.removeEventListener("keypress", this._keyToggleCallback, false); + document.removeEventListener("popupshown", this._setPopupOpen, false); + document.removeEventListener("popuphidden", this._setPopupOpen, false); + } + }, + + cleanupDomFullscreen: function () { + window.messageManager + .broadcastAsyncMessage("DOMFullscreen:CleanUp"); + + PointerlockFsWarning.close(); + gBrowser.tabContainer.removeEventListener("TabOpen", this.exitDomFullScreen); + gBrowser.tabContainer.removeEventListener("TabClose", this.exitDomFullScreen); + gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen); + window.removeEventListener("activate", this); + + document.documentElement.removeAttribute("inDOMFullscreen"); + }, + + _isRemoteBrowser: function (aBrowser) { + return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true"; + }, + + get _windowUtils() { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + }, + + getMouseTargetRect: function() + { + return this._mouseTargetRect; + }, + + // Event callbacks + _expandCallback: function() + { + FullScreen.showNavToolbox(); + }, + onMouseEnter: function() + { + FullScreen.hideNavToolbox(); + }, + _keyToggleCallback: function(aEvent) + { + // if we can use the keyboard (eg Ctrl+L or Ctrl+E) to open the toolbars, we + // should provide a way to collapse them too. + if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { + FullScreen.hideNavToolbox(); + } + // F6 is another shortcut to the address bar, but its not covered in OpenLocation() + else if (aEvent.keyCode == aEvent.DOM_VK_F6) + FullScreen.showNavToolbox(); + }, + + // Checks whether we are allowed to collapse the chrome + _isPopupOpen: false, + _isChromeCollapsed: false, + _safeToCollapse: function () { + if (!gPrefService.getBoolPref("browser.fullscreen.autohide")) + return false; + + // a popup menu is open in chrome: don't collapse chrome + if (this._isPopupOpen) + return false; + + // On OS X Lion we don't want to hide toolbars. + if (this.useLionFullScreen) + return false; + + // a textbox in chrome is focused (location bar anyone?): don't collapse chrome + if (document.commandDispatcher.focusedElement && + document.commandDispatcher.focusedElement.ownerDocument == document && + document.commandDispatcher.focusedElement.localName == "input") { + return false; + } + + return true; + }, + + _setPopupOpen: function(aEvent) + { + // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed. + // Otherwise, they would not affect chrome and the user would expect the chrome to go away. + // e.g. we wouldn't want the autoscroll icon firing this event, so when the user + // toggles chrome when moving mouse to the top, it doesn't go away again. + if (aEvent.type == "popupshown" && !FullScreen._isChromeCollapsed && + aEvent.target.localName != "tooltip" && aEvent.target.localName != "window") + FullScreen._isPopupOpen = true; + else if (aEvent.type == "popuphidden" && aEvent.target.localName != "tooltip" && + aEvent.target.localName != "window") { + FullScreen._isPopupOpen = false; + // Try again to hide toolbar when we close the popup. + FullScreen.hideNavToolbox(true); + } + }, + + // Autohide helpers for the context menu item + getAutohide: function(aItem) + { + aItem.setAttribute("checked", gPrefService.getBoolPref("browser.fullscreen.autohide")); + }, + setAutohide: function() + { + gPrefService.setBoolPref("browser.fullscreen.autohide", !gPrefService.getBoolPref("browser.fullscreen.autohide")); + // Try again to hide toolbar when we change the pref. + FullScreen.hideNavToolbox(true); + }, + + showNavToolbox: function(trackMouse = true) { + this._fullScrToggler.hidden = true; + gNavToolbox.removeAttribute("fullscreenShouldAnimate"); + gNavToolbox.style.marginTop = ""; + + if (!this._isChromeCollapsed) { + return; + } + + // Track whether mouse is near the toolbox + if (trackMouse && !this.useLionFullScreen) { + let rect = gBrowser.mPanelContainer.getBoundingClientRect(); + this._mouseTargetRect = { + top: rect.top + 50, + bottom: rect.bottom, + left: rect.left, + right: rect.right + }; + MousePosTracker.addListener(this); + } + + this._isChromeCollapsed = false; + }, + + hideNavToolbox: function (aAnimate = false) { + if (this._isChromeCollapsed || !this._safeToCollapse()) + return; + + this._fullScrToggler.hidden = false; + + if (aAnimate && gPrefService.getBoolPref("browser.fullscreen.animate")) { + gNavToolbox.setAttribute("fullscreenShouldAnimate", true); + // Hide the fullscreen toggler until the transition ends. + let listener = () => { + gNavToolbox.removeEventListener("transitionend", listener, true); + if (this._isChromeCollapsed) + this._fullScrToggler.hidden = false; + }; + gNavToolbox.addEventListener("transitionend", listener, true); + this._fullScrToggler.hidden = true; + } + + gNavToolbox.style.marginTop = + -gNavToolbox.getBoundingClientRect().height + "px"; + this._isChromeCollapsed = true; + MousePosTracker.removeListener(this); + }, + + _updateToolbars: function (aEnterFS) { + for (let el of document.querySelectorAll("toolbar[fullscreentoolbar=true]")) { + if (aEnterFS) { + // Give the main nav bar and the tab bar the fullscreen context menu, + // otherwise remove context menu to prevent breakage + el.setAttribute("saved-context", el.getAttribute("context")); + if (el.id == "nav-bar" || el.id == "TabsToolbar") + el.setAttribute("context", "autohide-context"); + else + el.removeAttribute("context"); + + // Set the inFullscreen attribute to allow specific styling + // in fullscreen mode + el.setAttribute("inFullscreen", true); + } else { + if (el.hasAttribute("saved-context")) { + el.setAttribute("context", el.getAttribute("saved-context")); + el.removeAttribute("saved-context"); + } + el.removeAttribute("inFullscreen"); + } + } + + ToolbarIconColor.inferFromText(); + + // For Lion fullscreen, all fullscreen controls are hidden, don't + // bother to touch them. If we don't stop here, the following code + // could cause the native fullscreen button be shown unexpectedly. + // See bug 1165570. + if (this.useLionFullScreen) { + return; + } + + var fullscreenctls = document.getElementById("window-controls"); + var navbar = document.getElementById("nav-bar"); + var ctlsOnTabbar = window.toolbar.visible; + if (fullscreenctls.parentNode == navbar && ctlsOnTabbar) { + fullscreenctls.removeAttribute("flex"); + document.getElementById("TabsToolbar").appendChild(fullscreenctls); + } + else if (fullscreenctls.parentNode.id == "TabsToolbar" && !ctlsOnTabbar) { + fullscreenctls.setAttribute("flex", "1"); + navbar.appendChild(fullscreenctls); + } + fullscreenctls.hidden = !aEnterFS; + } +}; +XPCOMUtils.defineLazyGetter(FullScreen, "useLionFullScreen", function() { + // We'll only use OS X Lion full screen if we're + // * on OS X + // * on Lion or higher (Darwin 11+) + // * have fullscreenbutton="true" + return AppConstants.isPlatformAndVersionAtLeast("macosx", 11) && + document.documentElement.getAttribute("fullscreenbutton") == "true"; +}); diff --git a/application/basilisk/base/content/browser-fullZoom.js b/application/basilisk/base/content/browser-fullZoom.js new file mode 100644 index 000000000..890cd8440 --- /dev/null +++ b/application/basilisk/base/content/browser-fullZoom.js @@ -0,0 +1,526 @@ +/* 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/. */ + +/** + * Controls the "full zoom" setting and its site-specific preferences. + */ +var FullZoom = { + // Identifies the setting in the content prefs database. + name: "browser.content.full-zoom", + + // browser.zoom.siteSpecific preference cache + _siteSpecificPref: undefined, + + // browser.zoom.updateBackgroundTabs preference cache + updateBackgroundTabs: undefined, + + // This maps the browser to monotonically increasing integer + // tokens. _browserTokenMap[browser] is increased each time the zoom is + // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses. + _browserTokenMap: new WeakMap(), + + // Stores initial locations if we receive onLocationChange + // events before we're initialized. + _initialLocations: new WeakMap(), + + get siteSpecific() { + return this._siteSpecificPref; + }, + + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener, + Ci.nsIObserver, + Ci.nsIContentPrefObserver, + Ci.nsISupportsWeakReference, + Ci.nsISupports]), + + // Initialization & Destruction + + init: function FullZoom_init() { + gBrowser.addEventListener("ZoomChangeUsingMouseWheel", this); + + // Register ourselves with the service so we know when our pref changes. + this._cps2 = Cc["@mozilla.org/content-pref/service;1"]. + getService(Ci.nsIContentPrefService2); + this._cps2.addObserverForName(this.name, this); + + this._siteSpecificPref = + gPrefService.getBoolPref("browser.zoom.siteSpecific"); + this.updateBackgroundTabs = + gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs"); + // Listen for changes to the browser.zoom branch so we can enable/disable + // updating background tabs and per-site saving and restoring of zoom levels. + gPrefService.addObserver("browser.zoom.", this, true); + + // If we received onLocationChange events for any of the current browsers + // before we were initialized we want to replay those upon initialization. + for (let browser of gBrowser.browsers) { + if (this._initialLocations.has(browser)) { + this.onLocationChange(...this._initialLocations.get(browser), browser); + } + } + + // This should be nulled after initialization. + this._initialLocations = null; + }, + + destroy: function FullZoom_destroy() { + gPrefService.removeObserver("browser.zoom.", this); + this._cps2.removeObserverForName(this.name, this); + gBrowser.removeEventListener("ZoomChangeUsingMouseWheel", this); + }, + + + // Event Handlers + + // nsIDOMEventListener + + handleEvent: function FullZoom_handleEvent(event) { + switch (event.type) { + case "ZoomChangeUsingMouseWheel": + let browser = this._getTargetedBrowser(event); + this._ignorePendingZoomAccesses(browser); + this._applyZoomToPref(browser); + break; + } + }, + + // nsIObserver + + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "nsPref:changed": + switch (aData) { + case "browser.zoom.siteSpecific": + this._siteSpecificPref = + gPrefService.getBoolPref("browser.zoom.siteSpecific"); + break; + case "browser.zoom.updateBackgroundTabs": + this.updateBackgroundTabs = + gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs"); + break; + } + break; + } + }, + + // nsIContentPrefObserver + + onContentPrefSet: function FullZoom_onContentPrefSet(aGroup, aName, aValue, aIsPrivate) { + this._onContentPrefChanged(aGroup, aValue, aIsPrivate); + }, + + onContentPrefRemoved: function FullZoom_onContentPrefRemoved(aGroup, aName, aIsPrivate) { + this._onContentPrefChanged(aGroup, undefined, aIsPrivate); + }, + + /** + * Appropriately updates the zoom level after a content preference has + * changed. + * + * @param aGroup The group of the changed preference. + * @param aValue The new value of the changed preference. Pass undefined to + * indicate the preference's removal. + */ + _onContentPrefChanged: function FullZoom__onContentPrefChanged(aGroup, aValue, aIsPrivate) { + if (this._isNextContentPrefChangeInternal) { + // Ignore changes that FullZoom itself makes. This works because the + // content pref service calls callbacks before notifying observers, and it + // does both in the same turn of the event loop. + delete this._isNextContentPrefChangeInternal; + return; + } + + let browser = gBrowser.selectedBrowser; + if (!browser.currentURI) + return; + + let ctxt = this._loadContextFromBrowser(browser); + let domain = this._cps2.extractDomain(browser.currentURI.spec); + if (aGroup) { + if (aGroup == domain && ctxt.usePrivateBrowsing == aIsPrivate) + this._applyPrefToZoom(aValue, browser); + return; + } + + this._globalValue = aValue === undefined ? aValue : + this._ensureValid(aValue); + + // If the current page doesn't have a site-specific preference, then its + // zoom should be set to the new global preference now that the global + // preference has changed. + let hasPref = false; + let token = this._getBrowserToken(browser); + this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, { + handleResult: function () { hasPref = true; }, + handleCompletion: function () { + if (!hasPref && token.isCurrent) + this._applyPrefToZoom(undefined, browser); + }.bind(this) + }); + }, + + // location change observer + + /** + * Called when the location of a tab changes. + * When that happens, we need to update the current zoom level if appropriate. + * + * @param aURI + * A URI object representing the new location. + * @param aIsTabSwitch + * Whether this location change has happened because of a tab switch. + * @param aBrowser + * (optional) browser object displaying the document + */ + onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) { + let browser = aBrowser || gBrowser.selectedBrowser; + + // If we haven't been initialized yet but receive an onLocationChange + // notification then let's store and replay it upon initialization. + if (this._initialLocations) { + this._initialLocations.set(browser, [aURI, aIsTabSwitch]); + return; + } + + // Ignore all pending async zoom accesses in the browser. Pending accesses + // that started before the location change will be prevented from applying + // to the new location. + this._ignorePendingZoomAccesses(browser); + + if (!aURI || (aIsTabSwitch && !this.siteSpecific)) { + this._notifyOnLocationChange(browser); + return; + } + + // Avoid the cps roundtrip and apply the default/global pref. + if (aURI.spec == "about:blank") { + this._applyPrefToZoom(undefined, browser, + this._notifyOnLocationChange.bind(this, browser)); + return; + } + + // Media documents should always start at 1, and are not affected by prefs. + if (!aIsTabSwitch && browser.isSyntheticDocument) { + ZoomManager.setZoomForBrowser(browser, 1); + // _ignorePendingZoomAccesses already called above, so no need here. + this._notifyOnLocationChange(browser); + return; + } + + // See if the zoom pref is cached. + let ctxt = this._loadContextFromBrowser(browser); + let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt); + if (pref) { + this._applyPrefToZoom(pref.value, browser, + this._notifyOnLocationChange.bind(this, browser)); + return; + } + + // It's not cached, so we have to asynchronously fetch it. + let value = undefined; + let token = this._getBrowserToken(browser); + this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, { + handleResult: function (resultPref) { value = resultPref.value; }, + handleCompletion: function () { + if (!token.isCurrent) { + this._notifyOnLocationChange(browser); + return; + } + this._applyPrefToZoom(value, browser, + this._notifyOnLocationChange.bind(this, browser)); + }.bind(this) + }); + }, + + // update state of zoom type menu item + + updateMenu: function FullZoom_updateMenu() { + var menuItem = document.getElementById("toggle_zoom"); + + menuItem.setAttribute("checked", !ZoomManager.useFullZoom); + }, + + // Setting & Pref Manipulation + + /** + * Reduces the zoom level of the page in the current browser. + */ + reduce: function FullZoom_reduce() { + ZoomManager.reduce(); + let browser = gBrowser.selectedBrowser; + this._ignorePendingZoomAccesses(browser); + this._applyZoomToPref(browser); + }, + + /** + * Enlarges the zoom level of the page in the current browser. + */ + enlarge: function FullZoom_enlarge() { + ZoomManager.enlarge(); + let browser = gBrowser.selectedBrowser; + this._ignorePendingZoomAccesses(browser); + this._applyZoomToPref(browser); + }, + + /** + * Sets the zoom level for the given browser to the given floating + * point value, where 1 is the default zoom level. + */ + setZoom: function (value, browser = gBrowser.selectedBrowser) { + ZoomManager.setZoomForBrowser(browser, value); + this._ignorePendingZoomAccesses(browser); + this._applyZoomToPref(browser); + }, + + /** + * Sets the zoom level of the page in the given browser to the global zoom + * level. + * + * @return A promise which resolves when the zoom reset has been applied. + */ + reset: function FullZoom_reset(browser = gBrowser.selectedBrowser) { + let token = this._getBrowserToken(browser); + let result = this._getGlobalValue(browser).then(value => { + if (token.isCurrent) { + ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value); + this._ignorePendingZoomAccesses(browser); + Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", ""); + } + }); + this._removePref(browser); + return result; + }, + + /** + * Set the zoom level for a given browser. + * + * Per nsPresContext::setFullZoom, we can set the zoom to its current value + * without significant impact on performance, as the setting is only applied + * if it differs from the current setting. In fact getting the zoom and then + * checking ourselves if it differs costs more. + * + * And perhaps we should always set the zoom even if it was more expensive, + * since nsDocumentViewer::SetTextZoom claims that child documents can have + * a different text zoom (although it would be unusual), and it implies that + * those child text zooms should get updated when the parent zoom gets set, + * and perhaps the same is true for full zoom + * (although nsDocumentViewer::SetFullZoom doesn't mention it). + * + * So when we apply new zoom values to the browser, we simply set the zoom. + * We don't check first to see if the new value is the same as the current + * one. + * + * @param aValue The zoom level value. + * @param aBrowser The zoom is set in this browser. Required. + * @param aCallback If given, it's asynchronously called when complete. + */ + _applyPrefToZoom: function FullZoom__applyPrefToZoom(aValue, aBrowser, aCallback) { + if (!this.siteSpecific || gInPrintPreviewMode) { + this._executeSoon(aCallback); + return; + } + + // The browser is sometimes half-destroyed because this method is called + // by content pref service callbacks, which themselves can be called at any + // time, even after browsers are closed. + if (!aBrowser.parentNode || aBrowser.isSyntheticDocument) { + this._executeSoon(aCallback); + return; + } + + if (aValue !== undefined) { + ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue)); + this._ignorePendingZoomAccesses(aBrowser); + this._executeSoon(aCallback); + return; + } + + let token = this._getBrowserToken(aBrowser); + this._getGlobalValue(aBrowser).then(value => { + if (token.isCurrent) { + ZoomManager.setZoomForBrowser(aBrowser, value === undefined ? 1 : value); + this._ignorePendingZoomAccesses(aBrowser); + } + this._executeSoon(aCallback); + }); + }, + + /** + * Saves the zoom level of the page in the given browser to the content + * prefs store. + * + * @param browser The zoom of this browser will be saved. Required. + */ + _applyZoomToPref: function FullZoom__applyZoomToPref(browser) { + Services.obs.notifyObservers(browser, "browser-fullZoom:zoomChange", ""); + if (!this.siteSpecific || + gInPrintPreviewMode || + browser.isSyntheticDocument) + return; + + this._cps2.set(browser.currentURI.spec, this.name, + ZoomManager.getZoomForBrowser(browser), + this._loadContextFromBrowser(browser), { + handleCompletion: function () { + this._isNextContentPrefChangeInternal = true; + }.bind(this), + }); + }, + + /** + * Removes from the content prefs store the zoom level of the given browser. + * + * @param browser The zoom of this browser will be removed. Required. + */ + _removePref: function FullZoom__removePref(browser) { + Services.obs.notifyObservers(browser, "browser-fullZoom:zoomReset", ""); + if (browser.isSyntheticDocument) + return; + let ctxt = this._loadContextFromBrowser(browser); + this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, { + handleCompletion: function () { + this._isNextContentPrefChangeInternal = true; + }.bind(this), + }); + }, + + // Utilities + + /** + * Returns the zoom change token of the given browser. Asynchronous + * operations that access the given browser's zoom should use this method to + * capture the token before starting and use token.isCurrent to determine if + * it's safe to access the zoom when done. If token.isCurrent is false, then + * after the async operation started, either the browser's zoom was changed or + * the browser was destroyed, and depending on what the operation is doing, it + * may no longer be safe to set and get its zoom. + * + * @param browser The token of this browser will be returned. + * @return An object with an "isCurrent" getter. + */ + _getBrowserToken: function FullZoom__getBrowserToken(browser) { + let map = this._browserTokenMap; + if (!map.has(browser)) + map.set(browser, 0); + return { + token: map.get(browser), + get isCurrent() { + // At this point, the browser may have been destructed and unbound but + // its outer ID not removed from the map because outer-window-destroyed + // hasn't been received yet. In that case, the browser is unusable, it + // has no properties, so return false. Check for this case by getting a + // property, say, docShell. + return map.get(browser) === this.token && browser.parentNode; + }, + }; + }, + + /** + * Returns the browser that the supplied zoom event is associated with. + * @param event The ZoomChangeUsingMouseWheel event. + * @return The associated browser element, if one exists, otherwise null. + */ + _getTargetedBrowser: function FullZoom__getTargetedBrowser(event) { + let target = event.originalTarget; + + // With remote content browsers, the event's target is the browser + // we're looking for. + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + if (target instanceof window.XULElement && + target.localName == "browser" && + target.namespaceURI == XUL_NS) + return target; + + // With in-process content browsers, the event's target is the content + // document. + if (target.nodeType == Node.DOCUMENT_NODE) + return gBrowser.getBrowserForDocument(target); + + throw new Error("Unexpected ZoomChangeUsingMouseWheel event source"); + }, + + /** + * Increments the zoom change token for the given browser so that pending + * async operations know that it may be unsafe to access they zoom when they + * finish. + * + * @param browser Pending accesses in this browser will be ignored. + */ + _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(browser) { + let map = this._browserTokenMap; + map.set(browser, (map.get(browser) || 0) + 1); + }, + + _ensureValid: function FullZoom__ensureValid(aValue) { + // Note that undefined is a valid value for aValue that indicates a known- + // not-to-exist value. + if (isNaN(aValue)) + return 1; + + if (aValue < ZoomManager.MIN) + return ZoomManager.MIN; + + if (aValue > ZoomManager.MAX) + return ZoomManager.MAX; + + return aValue; + }, + + /** + * Gets the global browser.content.full-zoom content preference. + * + * @param browser The browser pertaining to the zoom. + * @returns Promise<prefValue> + * Resolves to the preference value when done. + */ + _getGlobalValue: function FullZoom__getGlobalValue(browser) { + // * !("_globalValue" in this) => global value not yet cached. + // * this._globalValue === undefined => global value known not to exist. + // * Otherwise, this._globalValue is a number, the global value. + return new Promise(resolve => { + if ("_globalValue" in this) { + resolve(this._globalValue); + return; + } + let value = undefined; + this._cps2.getGlobal(this.name, this._loadContextFromBrowser(browser), { + handleResult: function (pref) { value = pref.value; }, + handleCompletion: (reason) => { + this._globalValue = this._ensureValid(value); + resolve(this._globalValue); + } + }); + }); + }, + + /** + * Gets the load context from the given Browser. + * + * @param Browser The Browser whose load context will be returned. + * @return The nsILoadContext of the given Browser. + */ + _loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) { + return browser.loadContext; + }, + + /** + * Asynchronously broadcasts "browser-fullZoom:location-change" so that + * listeners can be notified when the zoom levels on those pages change. + * The notification is always asynchronous so that observers are guaranteed a + * consistent behavior. + */ + _notifyOnLocationChange: function FullZoom__notifyOnLocationChange(browser) { + this._executeSoon(function () { + Services.obs.notifyObservers(browser, "browser-fullZoom:location-change", ""); + }); + }, + + _executeSoon: function FullZoom__executeSoon(callback) { + if (!callback) + return; + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); + }, +}; diff --git a/application/basilisk/base/content/browser-fxaccounts.js b/application/basilisk/base/content/browser-fxaccounts.js new file mode 100644 index 000000000..0bbce3e26 --- /dev/null +++ b/application/basilisk/base/content/browser-fxaccounts.js @@ -0,0 +1,459 @@ +/* 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 gFxAccounts = { + + SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration", + + _initialized: false, + _inCustomizationMode: false, + _cachedProfile: null, + + get weave() { + delete this.weave; + return this.weave = Cc["@mozilla.org/weave/service;1"] + .getService(Ci.nsISupports) + .wrappedJSObject; + }, + + get topics() { + // Do all this dance to lazy-load FxAccountsCommon. + delete this.topics; + return this.topics = [ + "weave:service:ready", + "weave:service:login:change", + "weave:service:setup-complete", + "weave:service:sync:error", + "weave:ui:login:error", + "fxa-migration:state-changed", + this.FxAccountsCommon.ONLOGIN_NOTIFICATION, + this.FxAccountsCommon.ONLOGOUT_NOTIFICATION, + this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, + ]; + }, + + get panelUIFooter() { + delete this.panelUIFooter; + return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa"); + }, + + get panelUIStatus() { + delete this.panelUIStatus; + return this.panelUIStatus = document.getElementById("PanelUI-fxa-status"); + }, + + get panelUIAvatar() { + delete this.panelUIAvatar; + return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar"); + }, + + get panelUILabel() { + delete this.panelUILabel; + return this.panelUILabel = document.getElementById("PanelUI-fxa-label"); + }, + + get panelUIIcon() { + delete this.panelUIIcon; + return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon"); + }, + + get strings() { + delete this.strings; + return this.strings = Services.strings.createBundle( + "chrome://browser/locale/accounts.properties" + ); + }, + + get loginFailed() { + // Referencing Weave.Service will implicitly initialize sync, and we don't + // want to force that - so first check if it is ready. + let service = Cc["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + if (!service.ready) { + return false; + } + // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in". + // All other login failures are assumed to be transient and should go + // away by themselves, so aren't reflected here. + return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; + }, + + get sendTabToDeviceEnabled() { + return Services.prefs.getBoolPref("services.sync.sendTabToDevice.enabled"); + }, + + get remoteClients() { + return Weave.Service.clientsEngine.remoteClients + .sort((a, b) => a.name.localeCompare(b.name)); + }, + + init: function () { + // Bail out if we're already initialized and for pop-up windows. + if (this._initialized || !window.toolbar.visible) { + return; + } + + for (let topic of this.topics) { + Services.obs.addObserver(this, topic, false); + } + + gNavToolbox.addEventListener("customizationstarting", this); + gNavToolbox.addEventListener("customizationending", this); + + EnsureFxAccountsWebChannel(); + this._initialized = true; + + this.updateUI(); + }, + + uninit: function () { + if (!this._initialized) { + return; + } + + for (let topic of this.topics) { + Services.obs.removeObserver(this, topic); + } + + this._initialized = false; + }, + + observe: function (subject, topic, data) { + switch (topic) { + case "fxa-migration:state-changed": + this.onMigrationStateChanged(data, subject); + break; + case this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION: + this._cachedProfile = null; + // Fallthrough intended + default: + this.updateUI(); + break; + } + }, + + onMigrationStateChanged: function () { + // Since we nuked most of the migration code, this notification will fire + // once after legacy Sync has been disconnected (and should never fire + // again) + let nb = window.document.getElementById("global-notificationbox"); + + let msg = this.strings.GetStringFromName("autoDisconnectDescription") + let signInLabel = this.strings.GetStringFromName("autoDisconnectSignIn.label"); + let signInAccessKey = this.strings.GetStringFromName("autoDisconnectSignIn.accessKey"); + let learnMoreLink = this.fxaMigrator.learnMoreLink; + + let buttons = [ + { + label: signInLabel, + accessKey: signInAccessKey, + callback: () => { + this.openPreferences(); + } + } + ]; + + let fragment = document.createDocumentFragment(); + let msgNode = document.createTextNode(msg); + fragment.appendChild(msgNode); + if (learnMoreLink) { + let link = document.createElement("label"); + link.className = "text-link"; + link.setAttribute("value", learnMoreLink.text); + link.href = learnMoreLink.href; + fragment.appendChild(link); + } + + nb.appendNotification(fragment, + this.SYNC_MIGRATION_NOTIFICATION_TITLE, + undefined, + nb.PRIORITY_WARNING_LOW, + buttons); + + // ensure the hamburger menu reflects the newly disconnected state. + this.updateAppMenuItem(); + }, + + handleEvent: function (event) { + this._inCustomizationMode = event.type == "customizationstarting"; + this.updateAppMenuItem(); + }, + + updateUI: function () { + // It's possible someone signed in to FxA after seeing our notification + // about "Legacy Sync migration" (which now is actually "Legacy Sync + // auto-disconnect") so kill that notification if it still exists. + let nb = window.document.getElementById("global-notificationbox"); + let n = nb.getNotificationWithValue(this.SYNC_MIGRATION_NOTIFICATION_TITLE); + if (n) { + nb.removeNotification(n, true); + } + + this.updateAppMenuItem(); + }, + + // Note that updateAppMenuItem() returns a Promise that's only used by tests. + updateAppMenuItem: function () { + let profileInfoEnabled = false; + try { + profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled"); + } catch (e) { } + + // Bail out if FxA is disabled. + if (!this.weave.fxAccountsEnabled) { + return Promise.resolve(); + } + + this.panelUIFooter.hidden = false; + + // Make sure the button is disabled in customization mode. + if (this._inCustomizationMode) { + this.panelUIStatus.setAttribute("disabled", "true"); + this.panelUILabel.setAttribute("disabled", "true"); + this.panelUIAvatar.setAttribute("disabled", "true"); + this.panelUIIcon.setAttribute("disabled", "true"); + } else { + this.panelUIStatus.removeAttribute("disabled"); + this.panelUILabel.removeAttribute("disabled"); + this.panelUIAvatar.removeAttribute("disabled"); + this.panelUIIcon.removeAttribute("disabled"); + } + + let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel"); + let errorLabel = this.panelUIStatus.getAttribute("errorlabel"); + let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel"); + // The localization string is for the signed in text, but it's the default text as well + let defaultTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext"); + + let updateWithUserData = (userData) => { + // Window might have been closed while fetching data. + if (window.closed) { + return; + } + + // Reset the button to its original state. + this.panelUILabel.setAttribute("label", defaultLabel); + this.panelUIStatus.setAttribute("tooltiptext", defaultTooltiptext); + this.panelUIFooter.removeAttribute("fxastatus"); + this.panelUIFooter.removeAttribute("fxaprofileimage"); + this.panelUIAvatar.style.removeProperty("list-style-image"); + let showErrorBadge = false; + if (userData) { + // At this point we consider the user as logged-in (but still can be in an error state) + if (this.loginFailed) { + let tooltipDescription = this.strings.formatStringFromName("reconnectDescription", [userData.email], 1); + this.panelUIFooter.setAttribute("fxastatus", "error"); + this.panelUILabel.setAttribute("label", errorLabel); + this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription); + showErrorBadge = true; + } else if (!userData.verified) { + let tooltipDescription = this.strings.formatStringFromName("verifyDescription", [userData.email], 1); + this.panelUIFooter.setAttribute("fxastatus", "error"); + this.panelUIFooter.setAttribute("unverified", "true"); + this.panelUILabel.setAttribute("label", unverifiedLabel); + this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription); + showErrorBadge = true; + } else { + this.panelUIFooter.setAttribute("fxastatus", "signedin"); + this.panelUILabel.setAttribute("label", userData.email); + } + if (profileInfoEnabled) { + this.panelUIFooter.setAttribute("fxaprofileimage", "enabled"); + } + } + if (showErrorBadge) { + gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication"); + } else { + gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA); + } + } + + let updateWithProfile = (profile) => { + if (profileInfoEnabled) { + if (profile.displayName) { + this.panelUILabel.setAttribute("label", profile.displayName); + } + if (profile.avatar) { + this.panelUIFooter.setAttribute("fxaprofileimage", "set"); + let bgImage = "url(\"" + profile.avatar + "\")"; + this.panelUIAvatar.style.listStyleImage = bgImage; + + let img = new Image(); + img.onerror = () => { + // Clear the image if it has trouble loading. Since this callback is asynchronous + // we check to make sure the image is still the same before we clear it. + if (this.panelUIAvatar.style.listStyleImage === bgImage) { + this.panelUIFooter.removeAttribute("fxaprofileimage"); + this.panelUIAvatar.style.removeProperty("list-style-image"); + } + }; + img.src = profile.avatar; + } + } + } + + return fxAccounts.getSignedInUser().then(userData => { + // userData may be null here when the user is not signed-in, but that's expected + updateWithUserData(userData); + // unverified users cause us to spew log errors fetching an OAuth token + // to fetch the profile, so don't even try in that case. + if (!userData || !userData.verified || !profileInfoEnabled) { + return null; // don't even try to grab the profile. + } + if (this._cachedProfile) { + return this._cachedProfile; + } + return fxAccounts.getSignedInUserProfile().catch(err => { + // Not fetching the profile is sad but the FxA logs will already have noise. + return null; + }); + }).then(profile => { + if (!profile) { + return; + } + updateWithProfile(profile); + this._cachedProfile = profile; // Try to avoid fetching the profile on every UI update + }).catch(error => { + // This is most likely in tests, were we quickly log users in and out. + // The most likely scenario is a user logged out, so reflect that. + // Bug 995134 calls for better errors so we could retry if we were + // sure this was the failure reason. + this.FxAccountsCommon.log.error("Error updating FxA account info", error); + updateWithUserData(null); + }); + }, + + onMenuPanelCommand: function () { + + switch (this.panelUIFooter.getAttribute("fxastatus")) { + case "signedin": + this.openPreferences(); + break; + case "error": + if (this.panelUIFooter.getAttribute("unverified")) { + this.openPreferences(); + } else { + this.openSignInAgainPage("menupanel"); + } + break; + default: + this.openPreferences(); + break; + } + + PanelUI.hide(); + }, + + openPreferences: function () { + openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } }); + }, + + openAccountsPage: function (action, urlParams={}) { + let params = new URLSearchParams(); + if (action) { + params.set("action", action); + } + for (let name in urlParams) { + if (urlParams[name] !== undefined) { + params.set(name, urlParams[name]); + } + } + let url = "about:accounts?" + params; + switchToTabHavingURI(url, true, { + replaceQueryString: true + }); + }, + + openSignInAgainPage: function (entryPoint) { + this.openAccountsPage("reauth", { entrypoint: entryPoint }); + }, + + sendTabToDevice: function (url, clientId, title) { + Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title); + }, + + populateSendTabToDevicesMenu: function (devicesPopup, url, title) { + // remove existing menu items + while (devicesPopup.hasChildNodes()) { + devicesPopup.removeChild(devicesPopup.firstChild); + } + + const fragment = document.createDocumentFragment(); + + const onTargetDeviceCommand = (event) => { + const clientId = event.target.getAttribute("clientId"); + const clients = clientId + ? [clientId] + : this.remoteClients.map(client => client.id); + + clients.forEach(clientId => this.sendTabToDevice(url, clientId, title)); + } + + function addTargetDevice(clientId, name) { + const targetDevice = document.createElement("menuitem"); + targetDevice.addEventListener("command", onTargetDeviceCommand, true); + targetDevice.setAttribute("class", "sendtab-target"); + targetDevice.setAttribute("clientId", clientId); + targetDevice.setAttribute("label", name); + fragment.appendChild(targetDevice); + } + + const clients = this.remoteClients; + for (let client of clients) { + addTargetDevice(client.id, client.name); + } + + // "All devices" menu item + if (clients.length > 1) { + const separator = document.createElement("menuseparator"); + fragment.appendChild(separator); + const allDevicesLabel = this.strings.GetStringFromName("sendTabToAllDevices.menuitem"); + addTargetDevice("", allDevicesLabel); + } + + devicesPopup.appendChild(fragment); + }, + + updateTabContextMenu: function (aPopupMenu) { + if (!this.sendTabToDeviceEnabled) { + return; + } + + const remoteClientPresent = this.remoteClients.length > 0; + ["context_sendTabToDevice", "context_sendTabToDevice_separator"] + .forEach(id => { document.getElementById(id).hidden = !remoteClientPresent }); + }, + + initPageContextMenu: function (contextMenu) { + if (!this.sendTabToDeviceEnabled) { + return; + } + + const remoteClientPresent = this.remoteClients.length > 0; + // showSendLink and showSendPage are mutually exclusive + const showSendLink = remoteClientPresent + && (contextMenu.onSaveableLink || contextMenu.onPlainTextLink); + const showSendPage = !showSendLink && remoteClientPresent + && !(contextMenu.isContentSelected || + contextMenu.onImage || contextMenu.onCanvas || + contextMenu.onVideo || contextMenu.onAudio || + contextMenu.onLink || contextMenu.onTextInput); + + ["context-sendpagetodevice", "context-sep-sendpagetodevice"] + .forEach(id => contextMenu.showItem(id, showSendPage)); + ["context-sendlinktodevice", "context-sep-sendlinktodevice"] + .forEach(id => contextMenu.showItem(id, showSendLink)); + } +}; + +XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () { + return Cu.import("resource://gre/modules/FxAccountsCommon.js", {}); +}); + +XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator", + "resource://services-sync/FxaMigrator.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel", + "resource://gre/modules/FxAccountsWebChannel.jsm"); diff --git a/application/basilisk/base/content/browser-gestureSupport.js b/application/basilisk/base/content/browser-gestureSupport.js new file mode 100644 index 000000000..f472e5c9a --- /dev/null +++ b/application/basilisk/base/content/browser-gestureSupport.js @@ -0,0 +1,1244 @@ +/* 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/. */ + +// Simple gestures support +// +// As per bug #412486, web content must not be allowed to receive any +// simple gesture events. Multi-touch gesture APIs are in their +// infancy and we do NOT want to be forced into supporting an API that +// will probably have to change in the future. (The current Mac OS X +// API is undocumented and was reverse-engineered.) Until support is +// implemented in the event dispatcher to keep these events as +// chrome-only, we must listen for the simple gesture events during +// the capturing phase and call stopPropagation on every event. + +var gGestureSupport = { + _currentRotation: 0, + _lastRotateDelta: 0, + _rotateMomentumThreshold: .75, + + /** + * Add or remove mouse gesture event listeners + * + * @param aAddListener + * True to add/init listeners and false to remove/uninit + */ + init: function GS_init(aAddListener) { + const gestureEvents = ["SwipeGestureMayStart", "SwipeGestureStart", + "SwipeGestureUpdate", "SwipeGestureEnd", "SwipeGesture", + "MagnifyGestureStart", "MagnifyGestureUpdate", "MagnifyGesture", + "RotateGestureStart", "RotateGestureUpdate", "RotateGesture", + "TapGesture", "PressTapGesture"]; + + let addRemove = aAddListener ? window.addEventListener : + window.removeEventListener; + + for (let event of gestureEvents) { + addRemove("Moz" + event, this, true); + } + }, + + /** + * Dispatch events based on the type of mouse gesture event. For now, make + * sure to stop propagation of every gesture event so that web content cannot + * receive gesture events. + * + * @param aEvent + * The gesture event to handle + */ + handleEvent: function GS_handleEvent(aEvent) { + if (!Services.prefs.getBoolPref( + "dom.debug.propagate_gesture_events_through_content")) { + aEvent.stopPropagation(); + } + + // Create a preference object with some defaults + let def = (aThreshold, aLatched) => + ({ threshold: aThreshold, latched: !!aLatched }); + + switch (aEvent.type) { + case "MozSwipeGestureMayStart": + if (this._shouldDoSwipeGesture(aEvent)) { + aEvent.preventDefault(); + } + break; + case "MozSwipeGestureStart": + aEvent.preventDefault(); + this._setupSwipeGesture(); + break; + case "MozSwipeGestureUpdate": + aEvent.preventDefault(); + this._doUpdate(aEvent); + break; + case "MozSwipeGestureEnd": + aEvent.preventDefault(); + this._doEnd(aEvent); + break; + case "MozSwipeGesture": + aEvent.preventDefault(); + this.onSwipe(aEvent); + break; + case "MozMagnifyGestureStart": + aEvent.preventDefault(); + let pinchPref = AppConstants.platform == "win" + ? def(25, 0) + : def(150, 1); + this._setupGesture(aEvent, "pinch", pinchPref, "out", "in"); + break; + case "MozRotateGestureStart": + aEvent.preventDefault(); + this._setupGesture(aEvent, "twist", def(25, 0), "right", "left"); + break; + case "MozMagnifyGestureUpdate": + case "MozRotateGestureUpdate": + aEvent.preventDefault(); + this._doUpdate(aEvent); + break; + case "MozTapGesture": + aEvent.preventDefault(); + this._doAction(aEvent, ["tap"]); + break; + case "MozRotateGesture": + aEvent.preventDefault(); + this._doAction(aEvent, ["twist", "end"]); + break; + /* case "MozPressTapGesture": + break; */ + } + }, + + /** + * Called at the start of "pinch" and "twist" gestures to setup all of the + * information needed to process the gesture + * + * @param aEvent + * The continual motion start event to handle + * @param aGesture + * Name of the gesture to handle + * @param aPref + * Preference object with the names of preferences and defaults + * @param aInc + * Command to trigger for increasing motion (without gesture name) + * @param aDec + * Command to trigger for decreasing motion (without gesture name) + */ + _setupGesture: function GS__setupGesture(aEvent, aGesture, aPref, aInc, aDec) { + // Try to load user-set values from preferences + for (let [pref, def] of Object.entries(aPref)) + aPref[pref] = this._getPref(aGesture + "." + pref, def); + + // Keep track of the total deltas and latching behavior + let offset = 0; + let latchDir = aEvent.delta > 0 ? 1 : -1; + let isLatched = false; + + // Create the update function here to capture closure state + this._doUpdate = function GS__doUpdate(aEvent) { + // Update the offset with new event data + offset += aEvent.delta; + + // Check if the cumulative deltas exceed the threshold + if (Math.abs(offset) > aPref["threshold"]) { + // Trigger the action if we don't care about latching; otherwise, make + // sure either we're not latched and going the same direction of the + // initial motion; or we're latched and going the opposite way + let sameDir = (latchDir ^ offset) >= 0; + if (!aPref["latched"] || (isLatched ^ sameDir)) { + this._doAction(aEvent, [aGesture, offset > 0 ? aInc : aDec]); + + // We must be getting latched or leaving it, so just toggle + isLatched = !isLatched; + } + + // Reset motion counter to prepare for more of the same gesture + offset = 0; + } + }; + + // The start event also contains deltas, so handle an update right away + this._doUpdate(aEvent); + }, + + /** + * Checks whether a swipe gesture event can navigate the browser history or + * not. + * + * @param aEvent + * The swipe gesture event. + * @return true if the swipe event may navigate the history, false othwerwise. + */ + _swipeNavigatesHistory: function GS__swipeNavigatesHistory(aEvent) { + return this._getCommand(aEvent, ["swipe", "left"]) + == "Browser:BackOrBackDuplicate" && + this._getCommand(aEvent, ["swipe", "right"]) + == "Browser:ForwardOrForwardDuplicate"; + }, + + /** + * Checks whether we want to start a swipe for aEvent and sets + * aEvent.allowedDirections to the right values. + * + * @param aEvent + * The swipe gesture "MayStart" event. + * @return true if we're willing to start a swipe for this event, false + * otherwise. + */ + _shouldDoSwipeGesture: function GS__shouldDoSwipeGesture(aEvent) { + if (!this._swipeNavigatesHistory(aEvent)) { + return false; + } + + let isVerticalSwipe = false; + if (aEvent.direction == aEvent.DIRECTION_UP) { + if (gMultiProcessBrowser || content.pageYOffset > 0) { + return false; + } + isVerticalSwipe = true; + } else if (aEvent.direction == aEvent.DIRECTION_DOWN) { + if (gMultiProcessBrowser || content.pageYOffset < content.scrollMaxY) { + return false; + } + isVerticalSwipe = true; + } + if (isVerticalSwipe) { + // Vertical overscroll has been temporarily disabled until bug 939480 is + // fixed. + return false; + } + + let canGoBack = gHistorySwipeAnimation.canGoBack(); + let canGoForward = gHistorySwipeAnimation.canGoForward(); + let isLTR = gHistorySwipeAnimation.isLTR; + + if (canGoBack) { + aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_LEFT : + aEvent.DIRECTION_RIGHT; + } + if (canGoForward) { + aEvent.allowedDirections |= isLTR ? aEvent.DIRECTION_RIGHT : + aEvent.DIRECTION_LEFT; + } + + return true; + }, + + /** + * Sets up swipe gestures. This includes setting up swipe animations for the + * gesture, if enabled. + * + * @param aEvent + * The swipe gesture start event. + * @return true if swipe gestures could successfully be set up, false + * othwerwise. + */ + _setupSwipeGesture: function GS__setupSwipeGesture() { + gHistorySwipeAnimation.startAnimation(false); + + this._doUpdate = function GS__doUpdate(aEvent) { + gHistorySwipeAnimation.updateAnimation(aEvent.delta); + }; + + this._doEnd = function GS__doEnd(aEvent) { + gHistorySwipeAnimation.swipeEndEventReceived(); + + this._doUpdate = function (aEvent) {}; + this._doEnd = function (aEvent) {}; + } + }, + + /** + * Generator producing the powerset of the input array where the first result + * is the complete set and the last result (before StopIteration) is empty. + * + * @param aArray + * Source array containing any number of elements + * @yield Array that is a subset of the input array from full set to empty + */ + _power: function* GS__power(aArray) { + // Create a bitmask based on the length of the array + let num = 1 << aArray.length; + while (--num >= 0) { + // Only select array elements where the current bit is set + yield aArray.reduce(function (aPrev, aCurr, aIndex) { + if (num & 1 << aIndex) + aPrev.push(aCurr); + return aPrev; + }, []); + } + }, + + /** + * Determine what action to do for the gesture based on which keys are + * pressed and which commands are set, and execute the command. + * + * @param aEvent + * The original gesture event to convert into a fake click event + * @param aGesture + * Array of gesture name parts (to be joined by periods) + * @return Name of the executed command. Returns null if no command is + * found. + */ + _doAction: function GS__doAction(aEvent, aGesture) { + let command = this._getCommand(aEvent, aGesture); + return command && this._doCommand(aEvent, command); + }, + + /** + * Determine what action to do for the gesture based on which keys are + * pressed and which commands are set + * + * @param aEvent + * The original gesture event to convert into a fake click event + * @param aGesture + * Array of gesture name parts (to be joined by periods) + */ + _getCommand: function GS__getCommand(aEvent, aGesture) { + // Create an array of pressed keys in a fixed order so that a command for + // "meta" is preferred over "ctrl" when both buttons are pressed (and a + // command for both don't exist) + let keyCombos = []; + for (let key of ["shift", "alt", "ctrl", "meta"]) { + if (aEvent[key + "Key"]) + keyCombos.push(key); + } + + // Try each combination of key presses in decreasing order for commands + for (let subCombo of this._power(keyCombos)) { + // Convert a gesture and pressed keys into the corresponding command + // action where the preference has the gesture before "shift" before + // "alt" before "ctrl" before "meta" all separated by periods + let command; + try { + command = this._getPref(aGesture.concat(subCombo).join(".")); + } catch (e) {} + + if (command) + return command; + } + return null; + }, + + /** + * Execute the specified command. + * + * @param aEvent + * The original gesture event to convert into a fake click event + * @param aCommand + * Name of the command found for the event's keys and gesture. + */ + _doCommand: function GS__doCommand(aEvent, aCommand) { + let node = document.getElementById(aCommand); + if (node) { + if (node.getAttribute("disabled") != "true") { + let cmdEvent = document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent("command", true, true, window, 0, + aEvent.ctrlKey, aEvent.altKey, + aEvent.shiftKey, aEvent.metaKey, aEvent); + node.dispatchEvent(cmdEvent); + } + + } + else { + goDoCommand(aCommand); + } + }, + + /** + * Handle continual motion events. This function will be set by + * _setupGesture or _setupSwipe. + * + * @param aEvent + * The continual motion update event to handle + */ + _doUpdate: function(aEvent) {}, + + /** + * Handle gesture end events. This function will be set by _setupSwipe. + * + * @param aEvent + * The gesture end event to handle + */ + _doEnd: function(aEvent) {}, + + /** + * Convert the swipe gesture into a browser action based on the direction. + * + * @param aEvent + * The swipe event to handle + */ + onSwipe: function GS_onSwipe(aEvent) { + // Figure out which one (and only one) direction was triggered + for (let dir of ["UP", "RIGHT", "DOWN", "LEFT"]) { + if (aEvent.direction == aEvent["DIRECTION_" + dir]) { + this._coordinateSwipeEventWithAnimation(aEvent, dir); + break; + } + } + }, + + /** + * Process a swipe event based on the given direction. + * + * @param aEvent + * The swipe event to handle + * @param aDir + * The direction for the swipe event + */ + processSwipeEvent: function GS_processSwipeEvent(aEvent, aDir) { + this._doAction(aEvent, ["swipe", aDir.toLowerCase()]); + }, + + /** + * Coordinates the swipe event with the swipe animation, if any. + * If an animation is currently running, the swipe event will be + * processed once the animation stops. This will guarantee a fluid + * motion of the animation. + * + * @param aEvent + * The swipe event to handle + * @param aDir + * The direction for the swipe event + */ + _coordinateSwipeEventWithAnimation: + function GS__coordinateSwipeEventWithAnimation(aEvent, aDir) { + if ((gHistorySwipeAnimation.isAnimationRunning()) && + (aDir == "RIGHT" || aDir == "LEFT")) { + gHistorySwipeAnimation.processSwipeEvent(aEvent, aDir); + } + else { + this.processSwipeEvent(aEvent, aDir); + } + }, + + /** + * Get a gesture preference or use a default if it doesn't exist + * + * @param aPref + * Name of the preference to load under the gesture branch + * @param aDef + * Default value if the preference doesn't exist + */ + _getPref: function GS__getPref(aPref, aDef) { + // Preferences branch under which all gestures preferences are stored + const branch = "browser.gesture."; + + try { + // Determine what type of data to load based on default value's type + let type = typeof aDef; + let getFunc = "Char"; + if (type == "boolean") + getFunc = "Bool"; + else if (type == "number") + getFunc = "Int"; + return gPrefService["get" + getFunc + "Pref"](branch + aPref); + } + catch (e) { + return aDef; + } + }, + + /** + * Perform rotation for ImageDocuments + * + * @param aEvent + * The MozRotateGestureUpdate event triggering this call + */ + rotate: function(aEvent) { + if (!(content.document instanceof ImageDocument)) + return; + + let contentElement = content.document.body.firstElementChild; + if (!contentElement) + return; + // If we're currently snapping, cancel that snap + if (contentElement.classList.contains("completeRotation")) + this._clearCompleteRotation(); + + this.rotation = Math.round(this.rotation + aEvent.delta); + contentElement.style.transform = "rotate(" + this.rotation + "deg)"; + this._lastRotateDelta = aEvent.delta; + }, + + /** + * Perform a rotation end for ImageDocuments + */ + rotateEnd: function() { + if (!(content.document instanceof ImageDocument)) + return; + + let contentElement = content.document.body.firstElementChild; + if (!contentElement) + return; + + let transitionRotation = 0; + + // The reason that 360 is allowed here is because when rotating between + // 315 and 360, setting rotate(0deg) will cause it to rotate the wrong + // direction around--spinning wildly. + if (this.rotation <= 45) + transitionRotation = 0; + else if (this.rotation > 45 && this.rotation <= 135) + transitionRotation = 90; + else if (this.rotation > 135 && this.rotation <= 225) + transitionRotation = 180; + else if (this.rotation > 225 && this.rotation <= 315) + transitionRotation = 270; + else + transitionRotation = 360; + + // If we're going fast enough, and we didn't already snap ahead of rotation, + // then snap ahead of rotation to simulate momentum + if (this._lastRotateDelta > this._rotateMomentumThreshold && + this.rotation > transitionRotation) + transitionRotation += 90; + else if (this._lastRotateDelta < -1 * this._rotateMomentumThreshold && + this.rotation < transitionRotation) + transitionRotation -= 90; + + // Only add the completeRotation class if it is is necessary + if (transitionRotation != this.rotation) { + contentElement.classList.add("completeRotation"); + contentElement.addEventListener("transitionend", this._clearCompleteRotation); + } + + contentElement.style.transform = "rotate(" + transitionRotation + "deg)"; + this.rotation = transitionRotation; + }, + + /** + * Gets the current rotation for the ImageDocument + */ + get rotation() { + return this._currentRotation; + }, + + /** + * Sets the current rotation for the ImageDocument + * + * @param aVal + * The new value to take. Can be any value, but it will be bounded to + * 0 inclusive to 360 exclusive. + */ + set rotation(aVal) { + this._currentRotation = aVal % 360; + if (this._currentRotation < 0) + this._currentRotation += 360; + return this._currentRotation; + }, + + /** + * When the location/tab changes, need to reload the current rotation for the + * image + */ + restoreRotationState: function() { + // Bug 863514 - Make gesture support work in electrolysis + if (gMultiProcessBrowser) + return; + + if (!(content.document instanceof ImageDocument)) + return; + + let contentElement = content.document.body.firstElementChild; + let transformValue = content.window.getComputedStyle(contentElement, null) + .transform; + + if (transformValue == "none") { + this.rotation = 0; + return; + } + + // transformValue is a rotation matrix--split it and do mathemagic to + // obtain the real rotation value + transformValue = transformValue.split("(")[1] + .split(")")[0] + .split(","); + this.rotation = Math.round(Math.atan2(transformValue[1], transformValue[0]) * + (180 / Math.PI)); + }, + + /** + * Removes the transition rule by removing the completeRotation class + */ + _clearCompleteRotation: function() { + let contentElement = content.document && + content.document instanceof ImageDocument && + content.document.body && + content.document.body.firstElementChild; + if (!contentElement) + return; + contentElement.classList.remove("completeRotation"); + contentElement.removeEventListener("transitionend", this._clearCompleteRotation); + }, +}; + +// History Swipe Animation Support (bug 678392) +var gHistorySwipeAnimation = { + + active: false, + isLTR: false, + + /** + * Initializes the support for history swipe animations, if it is supported + * by the platform/configuration. + */ + init: function HSA_init() { + if (!this._isSupported()) + return; + + this.active = false; + this.isLTR = document.documentElement.matches(":-moz-locale-dir(ltr)"); + this._trackedSnapshots = []; + this._startingIndex = -1; + this._historyIndex = -1; + this._boxWidth = -1; + this._boxHeight = -1; + this._maxSnapshots = this._getMaxSnapshots(); + this._lastSwipeDir = ""; + this._direction = "horizontal"; + + // We only want to activate history swipe animations if we store snapshots. + // If we don't store any, we handle horizontal swipes without animations. + if (this._maxSnapshots > 0) { + this.active = true; + gBrowser.addEventListener("pagehide", this, false); + gBrowser.addEventListener("pageshow", this, false); + gBrowser.addEventListener("popstate", this, false); + gBrowser.addEventListener("DOMModalDialogClosed", this, false); + gBrowser.tabContainer.addEventListener("TabClose", this, false); + } + }, + + /** + * Uninitializes the support for history swipe animations. + */ + uninit: function HSA_uninit() { + gBrowser.removeEventListener("pagehide", this, false); + gBrowser.removeEventListener("pageshow", this, false); + gBrowser.removeEventListener("popstate", this, false); + gBrowser.removeEventListener("DOMModalDialogClosed", this, false); + gBrowser.tabContainer.removeEventListener("TabClose", this, false); + + this.active = false; + this.isLTR = false; + }, + + /** + * Starts the swipe animation and handles fast swiping (i.e. a swipe animation + * is already in progress when a new one is initiated). + * + * @param aIsVerticalSwipe + * Whether we're dealing with a vertical swipe or not. + */ + startAnimation: function HSA_startAnimation(aIsVerticalSwipe) { + this._direction = aIsVerticalSwipe ? "vertical" : "horizontal"; + + if (this.isAnimationRunning()) { + // If this is a horizontal scroll, or if this is a vertical scroll that + // was started while a horizontal scroll was still running, handle it as + // as a fast swipe. In the case of the latter scenario, this allows us to + // start the vertical animation without first loading the final page, or + // taking another snapshot. If vertical scrolls are initiated repeatedly + // without prior horizontal scroll we skip this and restart the animation + // from 0. + if (this._direction == "horizontal" || this._lastSwipeDir != "") { + gBrowser.stop(); + this._lastSwipeDir = "RELOAD"; // just ensure that != "" + this._canGoBack = this.canGoBack(); + this._canGoForward = this.canGoForward(); + this._handleFastSwiping(); + } + this.updateAnimation(0); + } + else { + // Get the session history from SessionStore. + let updateSessionHistory = sessionHistory => { + this._startingIndex = sessionHistory.index; + this._historyIndex = this._startingIndex; + this._canGoBack = this.canGoBack(); + this._canGoForward = this.canGoForward(); + if (this.active) { + this._addBoxes(); + this._takeSnapshot(); + this._installPrevAndNextSnapshots(); + this._lastSwipeDir = ""; + } + this.updateAnimation(0); + } + SessionStore.getSessionHistory(gBrowser.selectedTab, updateSessionHistory); + } + }, + + /** + * Stops the swipe animation. + */ + stopAnimation: function HSA_stopAnimation() { + gHistorySwipeAnimation._removeBoxes(); + this._historyIndex = this._getCurrentHistoryIndex(); + }, + + /** + * Updates the animation between two pages in history. + * + * @param aVal + * A floating point value that represents the progress of the + * swipe gesture. + */ + updateAnimation: function HSA_updateAnimation(aVal) { + if (!this.isAnimationRunning()) { + return; + } + + // We use the following value to decrease the bounce effect when scrolling + // to the top or bottom of the page, or when swiping back/forward past the + // browsing history. This value was determined experimentally. + let dampValue = 4; + if (this._direction == "vertical") { + this._prevBox.collapsed = true; + this._nextBox.collapsed = true; + this._positionBox(this._curBox, -1 * aVal / dampValue); + } else if ((aVal >= 0 && this.isLTR) || + (aVal <= 0 && !this.isLTR)) { + let tempDampValue = 1; + if (this._canGoBack) { + this._prevBox.collapsed = false; + } else { + tempDampValue = dampValue; + this._prevBox.collapsed = true; + } + + // The current page is pushed to the right (LTR) or left (RTL), + // the intention is to go back. + // If there is a page to go back to, it should show in the background. + this._positionBox(this._curBox, aVal / tempDampValue); + + // The forward page should be pushed offscreen all the way to the right. + this._positionBox(this._nextBox, 1); + } else if (this._canGoForward) { + // The intention is to go forward. If there is a page to go forward to, + // it should slide in from the right (LTR) or left (RTL). + // Otherwise, the current page should slide to the left (LTR) or + // right (RTL) and the backdrop should appear in the background. + // For the backdrop to be visible in that case, the previous page needs + // to be hidden (if it exists). + this._nextBox.collapsed = false; + let offset = this.isLTR ? 1 : -1; + this._positionBox(this._curBox, 0); + this._positionBox(this._nextBox, offset + aVal); + } else { + this._prevBox.collapsed = true; + this._positionBox(this._curBox, aVal / dampValue); + } + }, + + _getCurrentHistoryIndex: function() { + return SessionStore.getSessionHistory(gBrowser.selectedTab).index; + }, + + /** + * Event handler for events relevant to the history swipe animation. + * + * @param aEvent + * An event to process. + */ + handleEvent: function HSA_handleEvent(aEvent) { + let browser = gBrowser.selectedBrowser; + switch (aEvent.type) { + case "TabClose": + let browserForTab = gBrowser.getBrowserForTab(aEvent.target); + this._removeTrackedSnapshot(-1, browserForTab); + break; + case "DOMModalDialogClosed": + this.stopAnimation(); + break; + case "pageshow": + if (aEvent.target == browser.contentDocument) { + this.stopAnimation(); + } + break; + case "popstate": + if (aEvent.target == browser.contentDocument.defaultView) { + this.stopAnimation(); + } + break; + case "pagehide": + if (aEvent.target == browser.contentDocument) { + // Take and compress a snapshot of a page whenever it's about to be + // navigated away from. We already have a snapshot of the page if an + // animation is running, so we're left with compressing it. + if (!this.isAnimationRunning()) { + this._takeSnapshot(); + } + this._compressSnapshotAtCurrentIndex(); + } + break; + } + }, + + /** + * Checks whether the history swipe animation is currently running or not. + * + * @return true if the animation is currently running, false otherwise. + */ + isAnimationRunning: function HSA_isAnimationRunning() { + return !!this._container; + }, + + /** + * Process a swipe event based on the given direction. + * + * @param aEvent + * The swipe event to handle + * @param aDir + * The direction for the swipe event + */ + processSwipeEvent: function HSA_processSwipeEvent(aEvent, aDir) { + if (aDir == "RIGHT") + this._historyIndex += this.isLTR ? 1 : -1; + else if (aDir == "LEFT") + this._historyIndex += this.isLTR ? -1 : 1; + else + return; + this._lastSwipeDir = aDir; + }, + + /** + * Checks if there is a page in the browser history to go back to. + * + * @return true if there is a previous page in history, false otherwise. + */ + canGoBack: function HSA_canGoBack() { + if (this.isAnimationRunning()) + return this._doesIndexExistInHistory(this._historyIndex - 1); + return gBrowser.webNavigation.canGoBack; + }, + + /** + * Checks if there is a page in the browser history to go forward to. + * + * @return true if there is a next page in history, false otherwise. + */ + canGoForward: function HSA_canGoForward() { + if (this.isAnimationRunning()) + return this._doesIndexExistInHistory(this._historyIndex + 1); + return gBrowser.webNavigation.canGoForward; + }, + + /** + * Used to notify the history swipe animation that the OS sent a swipe end + * event and that we should navigate to the page that the user swiped to, if + * any. This will also result in the animation overlay to be torn down. + */ + swipeEndEventReceived: function HSA_swipeEndEventReceived() { + // Update the session history before continuing. + let updateSessionHistory = sessionHistory => { + if (this._lastSwipeDir != "" && this._historyIndex != this._startingIndex) + this._navigateToHistoryIndex(); + else + this.stopAnimation(); + } + SessionStore.getSessionHistory(gBrowser.selectedTab, updateSessionHistory); + }, + + /** + * Checks whether a particular index exists in the browser history or not. + * + * @param aIndex + * The index to check for availability for in the history. + * @return true if the index exists in the browser history, false otherwise. + */ + _doesIndexExistInHistory: function HSA__doesIndexExistInHistory(aIndex) { + try { + return SessionStore.getSessionHistory(gBrowser.selectedTab).entries[aIndex] != null; + } + catch (ex) { + return false; + } + }, + + /** + * Navigates to the index in history that is currently being tracked by + * |this|. + */ + _navigateToHistoryIndex: function HSA__navigateToHistoryIndex() { + if (this._doesIndexExistInHistory(this._historyIndex)) + gBrowser.webNavigation.gotoIndex(this._historyIndex); + else + this.stopAnimation(); + }, + + /** + * Checks to see if history swipe animations are supported by this + * platform/configuration. + * + * return true if supported, false otherwise. + */ + _isSupported: function HSA__isSupported() { + return window.matchMedia("(-moz-swipe-animation-enabled)").matches; + }, + + /** + * Handle fast swiping (i.e. a swipe animation is already in + * progress when a new one is initiated). This will swap out the snapshots + * used in the previous animation with the appropriate new ones. + */ + _handleFastSwiping: function HSA__handleFastSwiping() { + this._installCurrentPageSnapshot(null); + this._installPrevAndNextSnapshots(); + }, + + /** + * Adds the boxes that contain the snapshots used during the swipe animation. + */ + _addBoxes: function HSA__addBoxes() { + let browserStack = + document.getAnonymousElementByAttribute(gBrowser.getNotificationBox(), + "class", "browserStack"); + this._container = this._createElement("historySwipeAnimationContainer", + "stack"); + browserStack.appendChild(this._container); + + this._prevBox = this._createElement("historySwipeAnimationPreviousPage", + "box"); + this._container.appendChild(this._prevBox); + + this._curBox = this._createElement("historySwipeAnimationCurrentPage", + "box"); + this._container.appendChild(this._curBox); + + this._nextBox = this._createElement("historySwipeAnimationNextPage", + "box"); + this._container.appendChild(this._nextBox); + + // Cache width and height. + this._boxWidth = this._curBox.getBoundingClientRect().width; + this._boxHeight = this._curBox.getBoundingClientRect().height; + }, + + /** + * Removes the boxes. + */ + _removeBoxes: function HSA__removeBoxes() { + this._curBox = null; + this._prevBox = null; + this._nextBox = null; + if (this._container) + this._container.parentNode.removeChild(this._container); + this._container = null; + this._boxWidth = -1; + this._boxHeight = -1; + }, + + /** + * Creates an element with a given identifier and tag name. + * + * @param aID + * An identifier to create the element with. + * @param aTagName + * The name of the tag to create the element for. + * @return the newly created element. + */ + _createElement: function HSA__createElement(aID, aTagName) { + let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let element = document.createElementNS(XULNS, aTagName); + element.id = aID; + return element; + }, + + /** + * Moves a given box to a given X coordinate position. + * + * @param aBox + * The box element to position. + * @param aPosition + * The position (in X coordinates) to move the box element to. + */ + _positionBox: function HSA__positionBox(aBox, aPosition) { + let transform = ""; + + if (this._direction == "vertical") + transform = "translateY(" + this._boxHeight * aPosition + "px)"; + else + transform = "translateX(" + this._boxWidth * aPosition + "px)"; + + aBox.style.transform = transform; + }, + + /** + * Verifies that we're ready to take snapshots based on the global pref and + * the current index in history. + * + * @return true if we're ready to take snapshots, false otherwise. + */ + _readyToTakeSnapshots: function HSA__readyToTakeSnapshots() { + return (this._maxSnapshots >= 1 && this._getCurrentHistoryIndex() >= 0); + }, + + /** + * Takes a snapshot of the page the browser is currently on. + */ + _takeSnapshot: function HSA__takeSnapshot() { + if (!this._readyToTakeSnapshots()) { + return; + } + + let canvas = null; + + let browser = gBrowser.selectedBrowser; + let r = browser.getBoundingClientRect(); + canvas = document.createElementNS("http://www.w3.org/1999/xhtml", + "canvas"); + canvas.mozOpaque = true; + let scale = window.devicePixelRatio; + canvas.width = r.width * scale; + canvas.height = r.height * scale; + let ctx = canvas.getContext("2d"); + let zoom = browser.markupDocumentViewer.fullZoom * scale; + ctx.scale(zoom, zoom); + ctx.drawWindow(browser.contentWindow, + 0, 0, canvas.width / zoom, canvas.height / zoom, "white", + ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW | + ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES | + ctx.DRAWWINDOW_USE_WIDGET_LAYERS); + + TelemetryStopwatch.start("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE"); + try { + this._installCurrentPageSnapshot(canvas); + this._assignSnapshotToCurrentBrowser(canvas); + } finally { + TelemetryStopwatch.finish("FX_GESTURE_INSTALL_SNAPSHOT_OF_PAGE"); + } + }, + + /** + * Retrieves the maximum number of snapshots that should be kept in memory. + * This limit is a global limit and is valid across all open tabs. + */ + _getMaxSnapshots: function HSA__getMaxSnapshots() { + return gPrefService.getIntPref("browser.snapshots.limit"); + }, + + /** + * Adds a snapshot to the list and initiates the compression of said snapshot. + * Once the compression is completed, it will replace the uncompressed + * snapshot in the list. + * + * @param aCanvas + * The snapshot to add to the list and compress. + */ + _assignSnapshotToCurrentBrowser: + function HSA__assignSnapshotToCurrentBrowser(aCanvas) { + let browser = gBrowser.selectedBrowser; + let currIndex = this._getCurrentHistoryIndex(); + + this._removeTrackedSnapshot(currIndex, browser); + this._addSnapshotRefToArray(currIndex, browser); + + if (!("snapshots" in browser)) + browser.snapshots = []; + let snapshots = browser.snapshots; + // Temporarily store the canvas as the compressed snapshot. + // This avoids a blank page if the user swipes quickly + // between pages before the compression could complete. + snapshots[currIndex] = { + image: aCanvas, + scale: window.devicePixelRatio + }; + }, + + /** + * Compresses the HTMLCanvasElement that's stored at the current history + * index in the snapshot array and stores the compressed image in its place. + */ + _compressSnapshotAtCurrentIndex: + function HSA__compressSnapshotAtCurrentIndex() { + if (!this._readyToTakeSnapshots()) { + // We didn't take a snapshot earlier because we weren't ready to, so + // there's nothing to compress. + return; + } + + TelemetryStopwatch.start("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE"); + try { + let browser = gBrowser.selectedBrowser; + let snapshots = browser.snapshots; + let currIndex = _getCurrentHistoryIndex(); + + // Kick off snapshot compression. + let canvas = snapshots[currIndex].image; + canvas.toBlob(function(aBlob) { + if (snapshots[currIndex]) { + snapshots[currIndex].image = aBlob; + } + }, "image/png" + ); + } finally { + TelemetryStopwatch.finish("FX_GESTURE_COMPRESS_SNAPSHOT_OF_PAGE"); + } + }, + + /** + * Removes a snapshot identified by the browser and index in the array of + * snapshots for that browser, if present. If no snapshot could be identified + * the method simply returns without taking any action. If aIndex is negative, + * all snapshots for a particular browser will be removed. + * + * @param aIndex + * The index in history of the new snapshot, or negative value if all + * snapshots for a browser should be removed. + * @param aBrowser + * The browser the new snapshot was taken in. + */ + _removeTrackedSnapshot: function HSA__removeTrackedSnapshot(aIndex, aBrowser) { + let arr = this._trackedSnapshots; + let requiresExactIndexMatch = aIndex >= 0; + for (let i = 0; i < arr.length; i++) { + if ((arr[i].browser == aBrowser) && + (aIndex < 0 || aIndex == arr[i].index)) { + delete aBrowser.snapshots[arr[i].index]; + arr.splice(i, 1); + if (requiresExactIndexMatch) + return; // Found and removed the only element. + i--; // Make sure to revisit the index that we just removed an + // element at. + } + } + }, + + /** + * Adds a new snapshot reference for a given index and browser to the array + * of references to tracked snapshots. + * + * @param aIndex + * The index in history of the new snapshot. + * @param aBrowser + * The browser the new snapshot was taken in. + */ + _addSnapshotRefToArray: + function HSA__addSnapshotRefToArray(aIndex, aBrowser) { + let id = { index: aIndex, + browser: aBrowser }; + let arr = this._trackedSnapshots; + arr.unshift(id); + + while (arr.length > this._maxSnapshots) { + let lastElem = arr[arr.length - 1]; + delete lastElem.browser.snapshots[lastElem.index].image; + delete lastElem.browser.snapshots[lastElem.index]; + arr.splice(-1, 1); + } + }, + + /** + * Converts a compressed blob to an Image object. In some situations + * (especially during fast swiping) aBlob may still be a canvas, not a + * compressed blob. In this case, we simply return the canvas. + * + * @param aBlob + * The compressed blob to convert, or a canvas if a blob compression + * couldn't complete before this method was called. + * @return A new Image object representing the converted blob. + */ + _convertToImg: function HSA__convertToImg(aBlob) { + if (!aBlob) + return null; + + // Return aBlob if it's still a canvas and not a compressed blob yet. + if (aBlob instanceof HTMLCanvasElement) + return aBlob; + + let img = new Image(); + let url = ""; + try { + url = URL.createObjectURL(aBlob); + img.onload = function() { + URL.revokeObjectURL(url); + }; + } + finally { + img.src = url; + return img; + } + }, + + /** + * Scales the background of a given box element (which uses a given snapshot + * as background) based on a given scale factor. + * @param aSnapshot + * The snapshot that is used as background of aBox. + * @param aScale + * The scale factor to use. + * @param aBox + * The box element that uses aSnapshot as background. + */ + _scaleSnapshot: function HSA__scaleSnapshot(aSnapshot, aScale, aBox) { + if (aSnapshot && aScale != 1 && aBox) { + if (aSnapshot instanceof HTMLCanvasElement) { + aBox.style.backgroundSize = + aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px"; + } else { + // snapshot is instanceof HTMLImageElement + aSnapshot.addEventListener("load", function() { + aBox.style.backgroundSize = + aSnapshot.width / aScale + "px " + aSnapshot.height / aScale + "px"; + }); + } + } + }, + + /** + * Sets the snapshot of the current page to the snapshot passed as parameter, + * or to the one previously stored for the current index in history if the + * parameter is null. + * + * @param aCanvas + * The snapshot to set the current page to. If this parameter is null, + * the previously stored snapshot for this index (if any) will be used. + */ + _installCurrentPageSnapshot: + function HSA__installCurrentPageSnapshot(aCanvas) { + let currSnapshot = aCanvas; + let scale = window.devicePixelRatio; + if (!currSnapshot) { + let snapshots = gBrowser.selectedBrowser.snapshots || {}; + let currIndex = this._historyIndex; + if (currIndex in snapshots) { + currSnapshot = this._convertToImg(snapshots[currIndex].image); + scale = snapshots[currIndex].scale; + } + } + this._scaleSnapshot(currSnapshot, scale, this._curBox ? this._curBox : + null); + document.mozSetImageElement("historySwipeAnimationCurrentPageSnapshot", + currSnapshot); + }, + + /** + * Sets the snapshots of the previous and next pages to the snapshots + * previously stored for their respective indeces. + */ + _installPrevAndNextSnapshots: + function HSA__installPrevAndNextSnapshots() { + let snapshots = gBrowser.selectedBrowser.snapshots || []; + let currIndex = this._historyIndex; + let prevIndex = currIndex - 1; + let prevSnapshot = null; + if (prevIndex in snapshots) { + prevSnapshot = this._convertToImg(snapshots[prevIndex].image); + this._scaleSnapshot(prevSnapshot, snapshots[prevIndex].scale, + this._prevBox); + } + document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot", + prevSnapshot); + + let nextIndex = currIndex + 1; + let nextSnapshot = null; + if (nextIndex in snapshots) { + nextSnapshot = this._convertToImg(snapshots[nextIndex].image); + this._scaleSnapshot(nextSnapshot, snapshots[nextIndex].scale, + this._nextBox); + } + document.mozSetImageElement("historySwipeAnimationNextPageSnapshot", + nextSnapshot); + }, +}; diff --git a/application/basilisk/base/content/browser-media.js b/application/basilisk/base/content/browser-media.js new file mode 100644 index 000000000..bd5c5b227 --- /dev/null +++ b/application/basilisk/base/content/browser-media.js @@ -0,0 +1,354 @@ +/* -*- 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 gEMEHandler = { + get uiEnabled() { +#ifdef MOZ_EME + let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled"); + // Force-disable on WinXP: + if (navigator.platform.toLowerCase().startsWith("win")) { + emeUIEnabled = emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6; + } + return emeUIEnabled; +#else + return false; +#endif + }, + ensureEMEEnabled: function(browser, keySystem) { + Services.prefs.setBoolPref("media.eme.enabled", true); + if (keySystem) { + if (keySystem.startsWith("com.adobe") && + Services.prefs.getPrefType("media.gmp-eme-adobe.enabled") && + !Services.prefs.getBoolPref("media.gmp-eme-adobe.enabled")) { + Services.prefs.setBoolPref("media.gmp-eme-adobe.enabled", true); + } else if (keySystem == "com.widevine.alpha" && + Services.prefs.getPrefType("media.gmp-widevinecdm.enabled") && + !Services.prefs.getBoolPref("media.gmp-widevinecdm.enabled")) { + Services.prefs.setBoolPref("media.gmp-widevinecdm.enabled", true); + } + } + browser.reload(); + }, + isKeySystemVisible: function(keySystem) { + if (!keySystem) { + return false; + } + if (keySystem.startsWith("com.adobe") && + Services.prefs.getPrefType("media.gmp-eme-adobe.visible")) { + return Services.prefs.getBoolPref("media.gmp-eme-adobe.visible"); + } + if (keySystem == "com.widevine.alpha" && + Services.prefs.getPrefType("media.gmp-widevinecdm.visible")) { + return Services.prefs.getBoolPref("media.gmp-widevinecdm.visible"); + } + return true; + }, + getEMEDisabledFragment: function(msgId) { + let mainMessage = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.message"); + let [prefix, suffix] = mainMessage.split(/%(?:\$\d)?S/).map(s => document.createTextNode(s)); + let text = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.learnMoreLabel"); + let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); + let link = document.createElement("label"); + link.className = "text-link"; + link.setAttribute("href", baseURL + "drm-content"); + link.textContent = text; + + let fragment = document.createDocumentFragment(); + [prefix, link, suffix].forEach(n => fragment.appendChild(n)); + return fragment; + }, + getMessageWithBrandName: function(notificationId) { + let msgId = "emeNotifications." + notificationId + ".message"; + return gNavigatorBundle.getFormattedString(msgId, [this._brandShortName]); + }, + receiveMessage: function({target: browser, data: data}) { + let parsedData; + try { + parsedData = JSON.parse(data); + } catch (ex) { + Cu.reportError("Malformed EME video message with data: " + data); + return; + } + let {status: status, keySystem: keySystem} = parsedData; + // Don't need to show if disabled or keysystem not visible. + if (!this.uiEnabled || !this.isKeySystemVisible(keySystem)) { + return; + } + + let notificationId; + let buttonCallback; + // Notification message can be either a string or a DOM fragment. + let notificationMessage; + switch (status) { + case "available": + case "cdm-created": + // Only show the chain icon for proprietary CDMs. Clearkey is not one. + if (keySystem != "org.w3.clearkey") { + this.showPopupNotificationForSuccess(browser, keySystem); + } + // ... and bail! + return; + + case "api-disabled": + case "cdm-disabled": + notificationId = "drmContentDisabled"; + buttonCallback = gEMEHandler.ensureEMEEnabled.bind(gEMEHandler, browser, keySystem) + notificationMessage = this.getEMEDisabledFragment(); + break; + + case "cdm-insufficient-version": + notificationId = "drmContentCDMInsufficientVersion"; + notificationMessage = this.getMessageWithBrandName(notificationId); + break; + + case "cdm-not-installed": + notificationId = "drmContentCDMInstalling"; + notificationMessage = this.getMessageWithBrandName(notificationId); + break; + + case "cdm-not-supported": + // Not to pop up user-level notification because they cannot do anything + // about it. + return; + default: + Cu.reportError(new Error("Unknown message ('" + status + "') dealing with EME key request: " + data)); + return; + } + + // Now actually create the notification + + let box = gBrowser.getNotificationBox(browser); + if (box.getNotificationWithValue(notificationId)) { + return; + } + + let buttons = []; + if (buttonCallback) { + let msgPrefix = "emeNotifications." + notificationId + "."; + let btnLabelId = msgPrefix + "button.label"; + let btnAccessKeyId = msgPrefix + "button.accesskey"; + buttons.push({ + label: gNavigatorBundle.getString(btnLabelId), + accessKey: gNavigatorBundle.getString(btnAccessKeyId), + callback: buttonCallback + }); + } + + let iconURL = "chrome://browser/skin/drm-icon.svg#chains-black"; + + box.appendNotification(notificationMessage, notificationId, iconURL, + box.PRIORITY_WARNING_MEDIUM, buttons); + }, + showPopupNotificationForSuccess: function(browser, keySystem) { + // We're playing EME content! Remove any "we can't play because..." messages. + var box = gBrowser.getNotificationBox(browser); + ["drmContentDisabled", + "drmContentCDMInstalling" + ].forEach(function (value) { + var notification = box.getNotificationWithValue(value); + if (notification) + box.removeNotification(notification); + }); + + // Don't bother creating it if it's already there: + if (PopupNotifications.getNotification("drmContentPlaying", browser)) { + return; + } + + let msgPrefix = "emeNotifications.drmContentPlaying."; + let msgId = msgPrefix + "message2"; + let btnLabelId = msgPrefix + "button.label"; + let btnAccessKeyId = msgPrefix + "button.accesskey"; + + let message = gNavigatorBundle.getFormattedString(msgId, [this._brandShortName]); + let anchorId = "eme-notification-icon"; + let firstPlayPref = "browser.eme.ui.firstContentShown"; + if (!Services.prefs.getPrefType(firstPlayPref) || + !Services.prefs.getBoolPref(firstPlayPref)) { + document.getElementById(anchorId).setAttribute("firstplay", "true"); + Services.prefs.setBoolPref(firstPlayPref, true); + } else { + document.getElementById(anchorId).removeAttribute("firstplay"); + } + + let mainAction = { + label: gNavigatorBundle.getString(btnLabelId), + accessKey: gNavigatorBundle.getString(btnAccessKeyId), + callback: function() { openPreferences("paneContent"); }, + dismiss: true + }; + let options = { + dismissed: true, + eventCallback: aTopic => aTopic == "swapping", + learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content", + }; + PopupNotifications.show(browser, "drmContentPlaying", message, anchorId, mainAction, null, options); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener]) +}; + +XPCOMUtils.defineLazyGetter(gEMEHandler, "_brandShortName", function() { + return document.getElementById("bundle_brand").getString("brandShortName"); +}); + +const TELEMETRY_DDSTAT_SHOWN = 0; +const TELEMETRY_DDSTAT_SHOWN_FIRST = 1; +const TELEMETRY_DDSTAT_CLICKED = 2; +const TELEMETRY_DDSTAT_CLICKED_FIRST = 3; +const TELEMETRY_DDSTAT_SOLVED = 4; + +let gDecoderDoctorHandler = { + getLabelForNotificationBox(type) { + if (type == "adobe-cdm-not-found" && + AppConstants.platform == "win") { + return gNavigatorBundle.getString("decoder.noCodecs.message"); + } + if (type == "adobe-cdm-not-activated" && + AppConstants.platform == "win") { + return gNavigatorBundle.getString("decoder.noCodecs.message"); + } + if (type == "platform-decoder-not-found") { + if (AppConstants.platform == "win") { + return gNavigatorBundle.getString("decoder.noHWAcceleration.message"); + } + if (AppConstants.platform == "linux") { + return gNavigatorBundle.getString("decoder.noCodecsLinux.message"); + } + } + if (type == "cannot-initialize-pulseaudio") { + return gNavigatorBundle.getString("decoder.noPulseAudio.message"); + } + if (type == "unsupported-libavcodec" && + AppConstants.platform == "linux") { + return gNavigatorBundle.getString("decoder.unsupportedLibavcodec.message"); + } + return ""; + }, + + getSumoForLearnHowButton(type) { + if (AppConstants.platform == "win") { + return "fix-video-audio-problems-firefox-windows"; + } + if (type == "cannot-initialize-pulseaudio") { + return "fix-common-audio-and-video-issues"; + } + return ""; + }, + + receiveMessage({target: browser, data: data}) { + let box = gBrowser.getNotificationBox(browser); + let notificationId = "decoder-doctor-notification"; + if (box.getNotificationWithValue(notificationId)) { + return; + } + + let parsedData; + try { + parsedData = JSON.parse(data); + } catch (ex) { + Cu.reportError("Malformed Decoder Doctor message with data: " + data); + return; + } + // parsedData (the result of parsing the incoming 'data' json string) + // contains analysis information from Decoder Doctor: + // - 'type' is the type of issue, it determines which text to show in the + // infobar. + // - 'decoderDoctorReportId' is the Decoder Doctor issue identifier, to be + // used here as key for the telemetry (counting infobar displays, + // "Learn how" buttons clicks, and resolutions) and for the prefs used + // to store at-issue formats. + // - 'formats' contains a comma-separated list of formats (or key systems) + // that suffer the issue. These are kept in a pref, which the backend + // uses to later find when an issue is resolved. + // - 'isSolved' is true when the notification actually indicates the + // resolution of that issue, to be reported as telemetry. + let {type, isSolved, decoderDoctorReportId, formats} = parsedData; + type = type.toLowerCase(); + // Error out early on invalid ReportId + if (!(/^\w+$/mi).test(decoderDoctorReportId)) { + return + } + let title = gDecoderDoctorHandler.getLabelForNotificationBox(type); + if (!title) { + return; + } + + // We keep the list of formats in prefs for the sake of the decoder itself, + // which reads it to determine when issues get solved for these formats. + // (Writing prefs from e10s content is now allowed.) + let formatsPref = "media.decoder-doctor." + decoderDoctorReportId + ".formats"; + let buttonClickedPref = "media.decoder-doctor." + decoderDoctorReportId + ".button-clicked"; + let histogram = + Services.telemetry.getKeyedHistogramById("DECODER_DOCTOR_INFOBAR_STATS"); + + let formatsInPref = Services.prefs.getPrefType(formatsPref) && + Services.prefs.getCharPref(formatsPref); + + if (!isSolved) { + if (!formats) { + Cu.reportError("Malformed Decoder Doctor unsolved message with no formats"); + return; + } + if (!formatsInPref) { + Services.prefs.setCharPref(formatsPref, formats); + histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SHOWN_FIRST); + } else { + // Split existing formats into an array of strings. + let existing = formatsInPref.split(",").map(String.trim); + // Keep given formats that were not already recorded. + let newbies = formats.split(",").map(String.trim) + .filter(x => !existing.includes(x)); + // And rewrite pref with the added new formats (if any). + if (newbies.length) { + Services.prefs.setCharPref(formatsPref, + existing.concat(newbies).join(", ")); + } + } + histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SHOWN); + + let buttons = []; + let sumo = gDecoderDoctorHandler.getSumoForLearnHowButton(type); + if (sumo) { + buttons.push({ + label: gNavigatorBundle.getString("decoder.noCodecs.button"), + accessKey: gNavigatorBundle.getString("decoder.noCodecs.accesskey"), + callback() { + let clickedInPref = Services.prefs.getPrefType(buttonClickedPref) && + Services.prefs.getBoolPref(buttonClickedPref); + if (!clickedInPref) { + Services.prefs.setBoolPref(buttonClickedPref, true); + histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED_FIRST); + } + histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_CLICKED); + + let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); + openUILinkIn(baseURL + sumo, "tab"); + } + }); + } + + box.appendNotification( + title, + notificationId, + "", // This uses the info icon as specified below. + box.PRIORITY_INFO_LOW, + buttons + ); + } else if (formatsInPref) { + // Issue is solved, and prefs haven't been cleared yet, meaning it's the + // first time we get this resolution -> Clear prefs and report telemetry. + Services.prefs.clearUserPref(formatsPref); + Services.prefs.clearUserPref(buttonClickedPref); + histogram.add(decoderDoctorReportId, TELEMETRY_DDSTAT_SOLVED); + } + }, +} + +window.getGroupMessageManager("browsers").addMessageListener("DecoderDoctor:Notification", gDecoderDoctorHandler); +window.getGroupMessageManager("browsers").addMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler); +window.addEventListener("unload", function() { + window.getGroupMessageManager("browsers").removeMessageListener("EMEVideo:ContentMediaKeysRequest", gEMEHandler); + window.getGroupMessageManager("browsers").removeMessageListener("DecoderDoctor:Notification", gDecoderDoctorHandler); +}, false); diff --git a/application/basilisk/base/content/browser-menubar.inc b/application/basilisk/base/content/browser-menubar.inc new file mode 100644 index 000000000..41734711c --- /dev/null +++ b/application/basilisk/base/content/browser-menubar.inc @@ -0,0 +1,521 @@ +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + <menubar id="main-menubar" + onpopupshowing="if (event.target.parentNode.parentNode == this && + !('@mozilla.org/widget/nativemenuservice;1' in Cc)) + this.setAttribute('openedwithkey', + event.target.parentNode.openedWithKey);" + style="border:0px;padding:0px;margin:0px;-moz-appearance:none"> + <menu id="file-menu" label="&fileMenu.label;" + accesskey="&fileMenu.accesskey;"> + <menupopup id="menu_FilePopup" + onpopupshowing="updateUserContextUIVisibility();"> + <menuitem id="menu_newNavigatorTab" + label="&tabCmd.label;" + command="cmd_newNavigatorTab" + key="key_newNavigatorTab" + accesskey="&tabCmd.accesskey;"/> + <menu id="menu_newUserContext" + label="&newUserContext.label;" + accesskey="&newUserContext.accesskey;" + hidden="true"> + <menupopup onpopupshowing="return createUserContextMenu(event);" /> + </menu> + <menuitem id="menu_newNavigator" + label="&newNavigatorCmd.label;" + accesskey="&newNavigatorCmd.accesskey;" + key="key_newNavigator" + command="cmd_newNavigator"/> + <menuitem id="menu_newPrivateWindow" + label="&newPrivateWindow.label;" + accesskey="&newPrivateWindow.accesskey;" + command="Tools:PrivateBrowsing" + key="key_privatebrowsing"/> +#ifdef MAC_NON_BROWSER_WINDOW + <menuitem id="menu_openLocation" + label="&openLocationCmd.label;" + command="Browser:OpenLocation" + key="focusURLBar"/> +#endif + <menuitem id="menu_openFile" + label="&openFileCmd.label;" + command="Browser:OpenFile" + key="openFileKb" + accesskey="&openFileCmd.accesskey;"/> + <menuitem id="menu_close" + class="show-only-for-keyboard" + label="&closeCmd.label;" + key="key_close" + accesskey="&closeCmd.accesskey;" + command="cmd_close"/> + <menuitem id="menu_closeWindow" + class="show-only-for-keyboard" + hidden="true" + command="cmd_closeWindow" + key="key_closeWindow" + label="&closeWindow.label;" + accesskey="&closeWindow.accesskey;"/> + <menuseparator/> + <menuitem id="menu_savePage" + label="&savePageCmd.label;" + accesskey="&savePageCmd.accesskey;" + key="key_savePage" + command="Browser:SavePage"/> + <menuitem id="menu_sendLink" + label="&emailPageCmd.label;" + accesskey="&emailPageCmd.accesskey;" + command="Browser:SendLink"/> + <menuseparator/> +#if !defined(MOZ_WIDGET_GTK) + <menuitem id="menu_printSetup" + label="&printSetupCmd.label;" + accesskey="&printSetupCmd.accesskey;" + command="cmd_pageSetup"/> +#endif +#ifndef XP_MACOSX + <menuitem id="menu_printPreview" + label="&printPreviewCmd.label;" + accesskey="&printPreviewCmd.accesskey;" + command="cmd_printPreview"/> +#endif + <menuitem id="menu_print" + label="&printCmd.label;" + accesskey="&printCmd.accesskey;" + key="printKb" + command="cmd_print"/> + <menuseparator/> + <menuitem id="goOfflineMenuitem" + label="&goOfflineCmd.label;" + accesskey="&goOfflineCmd.accesskey;" + type="checkbox" + observes="workOfflineMenuitemState" + oncommand="BrowserOffline.toggleOfflineStatus();"/> + <menuitem id="menu_FileQuitItem" +#ifdef XP_WIN + label="&quitApplicationCmdWin2.label;" + accesskey="&quitApplicationCmdWin2.accesskey;" +#else +#ifdef XP_MACOSX + label="&quitApplicationCmdMac2.label;" +#else + label="&quitApplicationCmd.label;" + accesskey="&quitApplicationCmd.accesskey;" +#endif +#ifdef XP_UNIX + key="key_quitApplication" +#endif +#endif + command="cmd_quitApplication"/> + </menupopup> + </menu> + + <menu id="edit-menu" label="&editMenu.label;" + accesskey="&editMenu.accesskey;"> + <menupopup id="menu_EditPopup" + onpopupshowing="updateEditUIVisibility()" + onpopuphidden="updateEditUIVisibility()"> + <menuitem id="menu_undo" + label="&undoCmd.label;" + key="key_undo" + accesskey="&undoCmd.accesskey;" + command="cmd_undo"/> + <menuitem id="menu_redo" + label="&redoCmd.label;" + key="key_redo" + accesskey="&redoCmd.accesskey;" + command="cmd_redo"/> + <menuseparator/> + <menuitem id="menu_cut" + label="&cutCmd.label;" + key="key_cut" + accesskey="&cutCmd.accesskey;" + command="cmd_cut"/> + <menuitem id="menu_copy" + label="©Cmd.label;" + key="key_copy" + accesskey="©Cmd.accesskey;" + command="cmd_copy"/> + <menuitem id="menu_paste" + label="&pasteCmd.label;" + key="key_paste" + accesskey="&pasteCmd.accesskey;" + command="cmd_paste"/> + <menuitem id="menu_delete" + label="&deleteCmd.label;" + key="key_delete" + accesskey="&deleteCmd.accesskey;" + command="cmd_delete"/> + <menuseparator/> + <menuitem id="menu_selectAll" + label="&selectAllCmd.label;" + key="key_selectAll" + accesskey="&selectAllCmd.accesskey;" + command="cmd_selectAll"/> + <menuseparator/> + <menuitem id="menu_find" + label="&findOnCmd.label;" + accesskey="&findOnCmd.accesskey;" + key="key_find" + command="cmd_find"/> + <menuitem id="menu_findAgain" + class="show-only-for-keyboard" + label="&findAgainCmd.label;" + accesskey="&findAgainCmd.accesskey;" + key="key_findAgain" + command="cmd_findAgain"/> + <menuseparator hidden="true" id="textfieldDirection-separator"/> + <menuitem id="textfieldDirection-swap" + command="cmd_switchTextDirection" + key="key_switchTextDirection" + label="&bidiSwitchTextDirectionItem.label;" + accesskey="&bidiSwitchTextDirectionItem.accesskey;" + hidden="true"/> + </menupopup> + </menu> + + <menu id="view-menu" label="&viewMenu.label;" + accesskey="&viewMenu.accesskey;"> + <menupopup id="menu_viewPopup" + onpopupshowing="updateCharacterEncodingMenuState();"> + <menu id="viewToolbarsMenu" + label="&viewToolbarsMenu.label;" + accesskey="&viewToolbarsMenu.accesskey;"> + <menupopup onpopupshowing="onViewToolbarsPopupShowing(event);"> + <menuseparator/> + <menuitem id="menu_customizeToolbars" + label="&viewCustomizeToolbar.label;" + accesskey="&viewCustomizeToolbar.accesskey;" + command="cmd_CustomizeToolbars"/> + </menupopup> + </menu> + <menu id="viewSidebarMenuMenu" + label="&viewSidebarMenu.label;" + accesskey="&viewSidebarMenu.accesskey;"> + <menupopup id="viewSidebarMenu"> + <menuitem id="menu_bookmarksSidebar" + key="viewBookmarksSidebarKb" + observes="viewBookmarksSidebar"/> + <menuitem id="menu_historySidebar" + key="key_gotoHistory" + observes="viewHistorySidebar" + label="&historyButton.label;"/> + <menuitem id="menu_tabsSidebar" + observes="viewTabsSidebar" + label="&syncedTabs.sidebar.label;"/> + </menupopup> + </menu> + <menuseparator/> + <menu id="viewFullZoomMenu" label="&fullZoom.label;" + accesskey="&fullZoom.accesskey;" + onpopupshowing="FullZoom.updateMenu();"> + <menupopup> + <menuitem id="menu_zoomEnlarge" + key="key_fullZoomEnlarge" + label="&fullZoomEnlargeCmd.label;" + accesskey="&fullZoomEnlargeCmd.accesskey;" + command="cmd_fullZoomEnlarge"/> + <menuitem id="menu_zoomReduce" + key="key_fullZoomReduce" + label="&fullZoomReduceCmd.label;" + accesskey="&fullZoomReduceCmd.accesskey;" + command="cmd_fullZoomReduce"/> + <menuseparator/> + <menuitem id="menu_zoomReset" + key="key_fullZoomReset" + label="&fullZoomResetCmd.label;" + accesskey="&fullZoomResetCmd.accesskey;" + command="cmd_fullZoomReset"/> + <menuseparator/> + <menuitem id="toggle_zoom" + label="&fullZoomToggleCmd.label;" + accesskey="&fullZoomToggleCmd.accesskey;" + type="checkbox" + command="cmd_fullZoomToggle" + checked="false"/> + </menupopup> + </menu> + <menu id="pageStyleMenu" label="&pageStyleMenu.label;" + accesskey="&pageStyleMenu.accesskey;" observes="isImage"> + <menupopup onpopupshowing="gPageStyleMenu.fillPopup(this);"> + <menuitem id="menu_pageStyleNoStyle" + label="&pageStyleNoStyle.label;" + accesskey="&pageStyleNoStyle.accesskey;" + oncommand="gPageStyleMenu.disableStyle();" + type="radio"/> + <menuitem id="menu_pageStylePersistentOnly" + label="&pageStylePersistentOnly.label;" + accesskey="&pageStylePersistentOnly.accesskey;" + oncommand="gPageStyleMenu.switchStyleSheet('');" + type="radio" + checked="true"/> + <menuseparator/> + </menupopup> + </menu> +#include browser-charsetmenu.inc + <menuseparator/> +#ifdef XP_MACOSX + <menuitem id="enterFullScreenItem" + accesskey="&enterFullScreenCmd.accesskey;" + label="&enterFullScreenCmd.label;" + key="key_fullScreen"> + <observes element="View:FullScreen" attribute="oncommand"/> + <observes element="View:FullScreen" attribute="disabled"/> + </menuitem> + <menuitem id="exitFullScreenItem" + accesskey="&exitFullScreenCmd.accesskey;" + label="&exitFullScreenCmd.label;" + key="key_fullScreen" + hidden="true"> + <observes element="View:FullScreen" attribute="oncommand"/> + <observes element="View:FullScreen" attribute="disabled"/> + </menuitem> +#else + <menuitem id="fullScreenItem" + accesskey="&fullScreenCmd.accesskey;" + label="&fullScreenCmd.label;" + key="key_fullScreen" + type="checkbox" + observes="View:FullScreen"/> +#endif + <menuitem id="menu_readerModeItem" + observes="View:ReaderView" + hidden="true"/> + <menuitem id="menu_showAllTabs" + hidden="true" + accesskey="&showAllTabsCmd.accesskey;" + label="&showAllTabsCmd.label;" + command="Browser:ShowAllTabs" + key="key_showAllTabs"/> + <menuseparator hidden="true" id="documentDirection-separator"/> + <menuitem id="documentDirection-swap" + hidden="true" + label="&bidiSwitchPageDirectionItem.label;" + accesskey="&bidiSwitchPageDirectionItem.accesskey;" + oncommand="gBrowser.selectedBrowser + .messageManager + .sendAsyncMessage('SwitchDocumentDirection');"/> + </menupopup> + </menu> + + <menu id="history-menu" + label="&historyMenu.label;" + accesskey="&historyMenu.accesskey;"> + <menupopup id="goPopup" +#ifndef XP_MACOSX + placespopup="true" +#endif + oncommand="this.parentNode._placesView._onCommand(event);" + onclick="checkForMiddleClick(this, event);" + onpopupshowing="if (!this.parentNode._placesView) + new HistoryMenu(event);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <menuitem id="menu_showAllHistory" + label="&showAllHistoryCmd2.label;" +#ifndef XP_MACOSX + key="showAllHistoryKb" +#endif + command="Browser:ShowAllHistory"/> + <menuitem id="sanitizeItem" + label="&clearRecentHistory.label;" + key="key_sanitize" + command="Tools:Sanitize"/> + <menuseparator id="sanitizeSeparator"/> + <menuitem id="sync-tabs-menuitem" + class="syncTabsMenuItem" + label="&syncTabsMenu3.label;" + oncommand="BrowserOpenSyncTabs();" + hidden="true"/> + <menuitem id="historyRestoreLastSession" + label="&historyRestoreLastSession.label;" + command="Browser:RestoreLastSession"/> + <menu id="historyUndoMenu" + class="recentlyClosedTabsMenu" + label="&historyUndoMenu.label;" + disabled="true"> + <menupopup id="historyUndoPopup" +#ifndef XP_MACOSX + placespopup="true" +#endif + onpopupshowing="document.getElementById('history-menu')._placesView.populateUndoSubmenu();"/> + </menu> + <menu id="historyUndoWindowMenu" + class="recentlyClosedWindowsMenu" + label="&historyUndoWindowMenu.label;" + disabled="true"> + <menupopup id="historyUndoWindowPopup" +#ifndef XP_MACOSX + placespopup="true" +#endif + onpopupshowing="document.getElementById('history-menu')._placesView.populateUndoWindowSubmenu();"/> + </menu> + <menuseparator id="startHistorySeparator" + class="hide-if-empty-places-result"/> + </menupopup> + </menu> + + <menu id="bookmarksMenu" + label="&bookmarksMenu.label;" + accesskey="&bookmarksMenu.accesskey;" + ondragenter="PlacesMenuDNDHandler.onDragEnter(event);" + ondragover="PlacesMenuDNDHandler.onDragOver(event);" + ondrop="PlacesMenuDNDHandler.onDrop(event);"> + <menupopup id="bookmarksMenuPopup" +#ifndef XP_MACOSX + placespopup="true" +#endif + context="placesContext" + openInTabs="children" + oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);" + onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);" + onpopupshowing="BookmarkingUI.onMainMenuPopupShowing(event); + if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=BOOKMARKS_MENU');" + tooltip="bhTooltip" popupsinherittooltip="true"> + <menuitem id="bookmarksShowAll" + label="&showAllBookmarks2.label;" + command="Browser:ShowAllBookmarks" + key="manBookmarkKb"/> + <menuseparator id="organizeBookmarksSeparator"/> + <menuitem id="menu_bookmarkThisPage" + command="Browser:AddBookmarkAs" + observes="bookmarkThisPageBroadcaster" + key="addBookmarkAsKb"/> + <menuitem id="subscribeToPageMenuitem" +#ifndef XP_MACOSX + class="menuitem-iconic" +#endif + label="&subscribeToPageMenuitem.label;" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);" + observes="singleFeedMenuitemState"/> + <menu id="subscribeToPageMenupopup" +#ifndef XP_MACOSX + class="menu-iconic" +#endif + label="&subscribeToPageMenupopup.label;" + observes="multipleFeedsMenuState"> + <menupopup id="subscribeToPageSubmenuMenupopup" + onpopupshowing="return FeedHandler.buildFeedList(event.target);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </menu> + <menuitem id="menu_bookmarkAllTabs" + label="&addCurPagesCmd.label;" + class="show-only-for-keyboard" + command="Browser:BookmarkAllTabs" + key="bookmarkAllTabsKb"/> + <menuseparator/> + <menuitem label="&recentBookmarks.label;" + id="menu_recentBookmarks" + disabled="true"/> + <menuseparator id="bookmarksToolbarSeparator"/> + <menu id="bookmarksToolbarFolderMenu" + class="menu-iconic bookmark-item" + label="&personalbarCmd.label;" + container="true"> + <menupopup id="bookmarksToolbarFolderPopup" +#ifndef XP_MACOSX + placespopup="true" +#endif + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=TOOLBAR');"/> + </menu> + <menu id="menu_unsortedBookmarks" + class="menu-iconic bookmark-item" + label="&otherBookmarksCmd.label;" + container="true"> + <menupopup id="otherBookmarksFolderPopup" +#ifndef XP_MACOSX + placespopup="true" +#endif + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=UNFILED_BOOKMARKS');"/> + </menu> + <menuseparator id="bookmarksMenuItemsSeparator"/> + <!-- Bookmarks menu items --> + </menupopup> + </menu> + + <menu id="tools-menu" + label="&toolsMenu.label;" + accesskey="&toolsMenu.accesskey;" + onpopupshowing="mirrorShow(this)"> + <menupopup id="menu_ToolsPopup" +# We have to use setTimeout() here to avoid a flickering menu bar when opening +# the Tools menu, see bug 970769. This can be removed once we got rid of the +# event loop spinning in Weave.Status._authManager. + onpopupshowing="setTimeout(() => gSyncUI.updateUI());" + > + <menuitem id="menu_openDownloads" + label="&downloads.label;" + accesskey="&downloads.accesskey;" + key="key_openDownloads" + command="Tools:Downloads"/> + <menuitem id="menu_openAddons" + label="&addons.label;" + accesskey="&addons.accesskey;" + key="key_openAddons" + command="Tools:Addons"/> + + <!-- only one of sync-setup, sync-syncnowitem or sync-reauthitem will be showing at once --> + <menuitem id="sync-setup" + label="&syncSignIn.label;" + accesskey="&syncSignIn.accesskey;" + observes="sync-setup-state" + oncommand="gSyncUI.openSetup(null, 'menubar')"/> + <menuitem id="sync-syncnowitem" + label="&syncSyncNowItem.label;" + accesskey="&syncSyncNowItem.accesskey;" + observes="sync-syncnow-state" + oncommand="gSyncUI.doSync(event);"/> + <menuitem id="sync-reauthitem" + label="&syncReAuthItem.label;" + accesskey="&syncReAuthItem.accesskey;" + observes="sync-reauth-state" + oncommand="gSyncUI.openSignInAgainPage('menubar');"/> + <menuseparator id="devToolsSeparator"/> + <menu id="webDeveloperMenu" + label="&webDeveloperMenu.label;" + accesskey="&webDeveloperMenu.accesskey;"> + <menupopup id="menuWebDeveloperPopup"> + <menuitem id="menu_pageSource" + observes="devtoolsMenuBroadcaster_PageSource" + accesskey="&pageSourceCmd.accesskey;"/> + <menuitem id="javascriptConsole" + observes="devtoolsMenuBroadcaster_ErrorConsole" + accesskey="&errorConsoleCmd.accesskey;"/> + </menupopup> + </menu> + <menuitem id="menu_pageInfo" + accesskey="&pageInfoCmd.accesskey;" + label="&pageInfoCmd.label;" +#ifndef XP_WIN + key="key_viewInfo" +#endif + command="View:PageInfo"/> + <menu id="menu_mirrorTabCmd" + hidden="true" + accesskey="&mirrorTabCmd.accesskey;" + label="&mirrorTabCmd.label;"> + <menupopup id="menu_mirrorTab-popup" + onpopupshowing="populateMirrorTabMenu(this)"/> + </menu> + <menuseparator id="prefSep"/> + <menuitem id="menu_preferences" + label="&preferencesCmd2.label;" + accesskey="&preferencesCmd2.accesskey;" + oncommand="openPreferences();"/> + </menupopup> + </menu> + +#ifdef XP_MACOSX + <menu id="windowMenu" /> +#endif + <menu id="helpMenu" /> + </menubar> diff --git a/application/basilisk/base/content/browser-places.js b/application/basilisk/base/content/browser-places.js new file mode 100644 index 000000000..83c737977 --- /dev/null +++ b/application/basilisk/base/content/browser-places.js @@ -0,0 +1,2024 @@ +/* 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 StarUI = { + _itemId: -1, + uri: null, + _batching: false, + _isNewBookmark: false, + _isComposing: false, + _autoCloseTimer: 0, + // The autoclose timer is diasbled if the user interacts with the + // popup, such as making a change through typing or clicking on + // the popup. + _autoCloseTimerEnabled: true, + + _element: function(aID) { + return document.getElementById(aID); + }, + + // Edit-bookmark panel + get panel() { + delete this.panel; + var element = this._element("editBookmarkPanel"); + // initially the panel is hidden + // to avoid impacting startup / new window performance + element.hidden = false; + element.addEventListener("keypress", this, false); + element.addEventListener("mousedown", this); + element.addEventListener("mouseout", this, false); + element.addEventListener("mousemove", this, false); + element.addEventListener("compositionstart", this, false); + element.addEventListener("compositionend", this, false); + element.addEventListener("input", this, false); + element.addEventListener("popuphidden", this, false); + element.addEventListener("popupshown", this, false); + return this.panel = element; + }, + + // Array of command elements to disable when the panel is opened. + get _blockedCommands() { + delete this._blockedCommands; + return this._blockedCommands = + ["cmd_close", "cmd_closeWindow"].map(id => this._element(id)); + }, + + _blockCommands: function SU__blockCommands() { + this._blockedCommands.forEach(function (elt) { + // make sure not to permanently disable this item (see bug 409155) + if (elt.hasAttribute("wasDisabled")) + return; + if (elt.getAttribute("disabled") == "true") { + elt.setAttribute("wasDisabled", "true"); + } else { + elt.setAttribute("wasDisabled", "false"); + elt.setAttribute("disabled", "true"); + } + }); + }, + + _restoreCommandsState: function SU__restoreCommandsState() { + this._blockedCommands.forEach(function (elt) { + if (elt.getAttribute("wasDisabled") != "true") + elt.removeAttribute("disabled"); + elt.removeAttribute("wasDisabled"); + }); + }, + + // nsIDOMEventListener + handleEvent(aEvent) { + switch (aEvent.type) { + case "mousemove": + clearTimeout(this._autoCloseTimer); + // The autoclose timer is not disabled on generic mouseout + // because the user may not have actually interacted with the popup. + break; + case "popuphidden": + clearTimeout(this._autoCloseTimer); + if (aEvent.originalTarget == this.panel) { + if (!this._element("editBookmarkPanelContent").hidden) + this.quitEditMode(); + + if (this._anchorToolbarButton) { + this._anchorToolbarButton.removeAttribute("open"); + this._anchorToolbarButton = null; + } + this._restoreCommandsState(); + this._itemId = -1; + if (this._batching) + this.endBatch(); + + if (this._uriForRemoval) { + if (this._isNewBookmark) { + if (!PlacesUtils.useAsyncTransactions) { + PlacesUtils.transactionManager.undoTransaction(); + break; + } + PlacesTransactions().undo().catch(Cu.reportError); + break; + } + // Remove all bookmarks for the bookmark's url, this also removes + // the tags for the url. + if (!PlacesUIUtils.useAsyncTransactions) { + let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval); + for (let itemId of itemIds) { + let txn = new PlacesRemoveItemTransaction(itemId); + PlacesUtils.transactionManager.doTransaction(txn); + } + break; + } + + PlacesTransactions.RemoveBookmarksForUrls([this._uriForRemoval]) + .transact().catch(Cu.reportError); + } + } + break; + case "keypress": + clearTimeout(this._autoCloseTimer); + this._autoCloseTimerEnabled = false; + + if (aEvent.defaultPrevented) { + // The event has already been consumed inside of the panel. + break; + } + + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_ESCAPE: + this.panel.hidePopup(); + break; + case KeyEvent.DOM_VK_RETURN: + if (aEvent.target.classList.contains("expander-up") || + aEvent.target.classList.contains("expander-down") || + aEvent.target.id == "editBMPanel_newFolderButton" || + aEvent.target.id == "editBookmarkPanelRemoveButton") { + // XXX Why is this necessary? The defaultPrevented check should + // be enough. + break; + } + this.panel.hidePopup(); + break; + // This case is for catching character-generating keypresses + case 0: + let accessKey = document.getElementById("key_close"); + if (eventMatchesKey(aEvent, accessKey)) { + this.panel.hidePopup(); + } + break; + } + break; + case "compositionend": + // After composition is committed, "mouseout" or something can set + // auto close timer. + this._isComposing = false; + break; + case "compositionstart": + if (aEvent.defaultPrevented) { + // If the composition was canceled, nothing to do here. + break; + } + this._isComposing = true; + // Explicit fall-through, during composition, panel shouldn't be + // hidden automatically. + case "input": + // Might have edited some text without keyboard events nor composition + // events. Fall-through to cancel auto close in such case. + case "mousedown": + clearTimeout(this._autoCloseTimer); + this._autoCloseTimerEnabled = false; + break; + case "mouseout": + if (!this._autoCloseTimerEnabled) { + // Don't autoclose the popup if the user has made a selection + // or keypress and then subsequently mouseout. + break; + } + // Explicit fall-through + case "popupshown": + // Don't handle events for descendent elements. + if (aEvent.target != aEvent.currentTarget) { + break; + } + // auto-close if new and not interacted with + if (this._isNewBookmark && !this._isComposing) { + // 3500ms matches the timeout that Pocket uses in + // browser/extensions/pocket/content/panels/js/saved.js + let delay = 3500; + if (this._closePanelQuickForTesting) { + delay /= 10; + } + clearTimeout(this._autoCloseTimer); + this._autoCloseTimer = setTimeout(() => { + if (!this.panel.mozMatchesSelector(":hover")) { + this.panel.hidePopup(); + } + }, delay); + this._autoCloseTimerEnabled = true; + } + break; + } + }, + + _overlayLoaded: false, + _overlayLoading: false, + showEditBookmarkPopup: Task.async(function* (aNode, aAnchorElement, aPosition, aIsNewBookmark) { + // Slow double-clicks (not true double-clicks) shouldn't + // cause the panel to flicker. + if (this.panel.state == "showing" || + this.panel.state == "open") { + return; + } + + this._isNewBookmark = aIsNewBookmark; + this._uriForRemoval = ""; + // TODO: Deprecate this once async transactions are enabled and the legacy + // transactions code is gone (bug 1131491) - we don't want addons to to use + // the completeNodeLikeObjectForItemId, so it's better if they keep passing + // the item-id for now). + if (typeof(aNode) == "number") { + let itemId = aNode; + if (PlacesUIUtils.useAsyncTransactions) { + let guid = yield PlacesUtils.promiseItemGuid(itemId); + aNode = yield PlacesUIUtils.promiseNodeLike(guid); + } + else { + aNode = { itemId }; + yield PlacesUIUtils.completeNodeLikeObjectForItemId(aNode); + } + } + + // Performance: load the overlay the first time the panel is opened + // (see bug 392443). + if (this._overlayLoading) + return; + + if (this._overlayLoaded) { + this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition); + return; + } + + this._overlayLoading = true; + document.loadOverlay( + "chrome://browser/content/places/editBookmarkOverlay.xul", + (function (aSubject, aTopic, aData) { + // Move the header (star, title, button) into the grid, + // so that it aligns nicely with the other items (bug 484022). + let header = this._element("editBookmarkPanelHeader"); + let rows = this._element("editBookmarkPanelGrid").lastChild; + rows.insertBefore(header, rows.firstChild); + header.hidden = false; + + this._overlayLoading = false; + this._overlayLoaded = true; + this._doShowEditBookmarkPanel(aNode, aAnchorElement, aPosition); + }).bind(this) + ); + }), + + _doShowEditBookmarkPanel: Task.async(function* (aNode, aAnchorElement, aPosition) { + if (this.panel.state != "closed") + return; + + this._blockCommands(); // un-done in the popuphidden handler + + this._element("editBookmarkPanelTitle").value = + this._isNewBookmark ? + gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") : + gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"); + + // No description; show the Done, Remove; + this._element("editBookmarkPanelDescription").textContent = ""; + this._element("editBookmarkPanelBottomButtons").hidden = false; + this._element("editBookmarkPanelContent").hidden = false; + + // The label of the remove button differs if the URI is bookmarked + // multiple times. + let bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI); + let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label"); + let label = PluralForm.get(bookmarks.length, forms).replace("#1", bookmarks.length); + this._element("editBookmarkPanelRemoveButton").label = label; + + // unset the unstarred state, if set + this._element("editBookmarkPanelStarIcon").removeAttribute("unstarred"); + + this._itemId = aNode.itemId; + this.beginBatch(); + + if (aAnchorElement) { + // Set the open=true attribute if the anchor is a + // descendent of a toolbarbutton. + let parent = aAnchorElement.parentNode; + while (parent) { + if (parent.localName == "toolbarbutton") { + break; + } + parent = parent.parentNode; + } + if (parent) { + this._anchorToolbarButton = parent; + parent.setAttribute("open", "true"); + } + } + let onPanelReady = fn => { + let target = this.panel; + if (target.parentNode) { + // By targeting the panel's parent and using a capturing listener, we + // can have our listener called before others waiting for the panel to + // be shown (which probably expect the panel to be fully initialized) + target = target.parentNode; + } + target.addEventListener("popupshown", function(event) { + fn(); + }, {"capture": true, "once": true}); + }; + gEditItemOverlay.initPanel({ node: aNode + , onPanelReady + , hiddenRows: ["description", "location", + "loadInSidebar", "keyword"] + , focusedElement: "preferred"}); + + this.panel.openPopup(aAnchorElement, aPosition); + }), + + panelShown: + function SU_panelShown(aEvent) { + if (aEvent.target == this.panel) { + if (this._element("editBookmarkPanelContent").hidden) { + // Note this isn't actually used anymore, we should remove this + // once we decide not to bring back the page bookmarked notification + this.panel.focus(); + } + } + }, + + quitEditMode: function SU_quitEditMode() { + this._element("editBookmarkPanelContent").hidden = true; + this._element("editBookmarkPanelBottomButtons").hidden = true; + gEditItemOverlay.uninitPanel(true); + }, + + removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() { + this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); + this.panel.hidePopup(); + }, + + // Matching the way it is used in the Library, editBookmarkOverlay implements + // an instant-apply UI, having no batched-Undo/Redo support. + // However, in this context (the Star UI) we have a Cancel button whose + // expected behavior is to undo all the operations done in the panel. + // Sometime in the future this needs to be reimplemented using a + // non-instant apply code path, but for the time being, we patch-around + // editBookmarkOverlay so that all of the actions done in the panel + // are treated by PlacesTransactions as a single batch. To do so, + // we start a PlacesTransactions batch when the star UI panel is shown, and + // we keep the batch ongoing until the panel is hidden. + _batchBlockingDeferred: null, + beginBatch() { + if (this._batching) + return; + if (PlacesUIUtils.useAsyncTransactions) { + this._batchBlockingDeferred = PromiseUtils.defer(); + PlacesTransactions.batch(function* () { + yield this._batchBlockingDeferred.promise; + }.bind(this)); + } + else { + PlacesUtils.transactionManager.beginBatch(null); + } + this._batching = true; + }, + + endBatch() { + if (!this._batching) + return; + + if (PlacesUIUtils.useAsyncTransactions) { + this._batchBlockingDeferred.resolve(); + this._batchBlockingDeferred = null; + } + else { + PlacesUtils.transactionManager.endBatch(false); + } + this._batching = false; + } +}; + +var PlacesCommandHook = { + /** + * Adds a bookmark to the page loaded in the given browser. + * + * @param aBrowser + * a <browser> element. + * @param [optional] aParent + * The folder in which to create a new bookmark if the page loaded in + * aBrowser isn't bookmarked yet, defaults to the unfiled root. + * @param [optional] aShowEditUI + * whether or not to show the edit-bookmark UI for the bookmark item + */ + bookmarkPage: Task.async(function* (aBrowser, aParent, aShowEditUI) { + if (PlacesUIUtils.useAsyncTransactions) { + yield this._bookmarkPagePT(aBrowser, aParent, aShowEditUI); + return; + } + + var uri = aBrowser.currentURI; + var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri); + let isNewBookmark = itemId == -1; + if (isNewBookmark) { + // Bug 1148838 - Make this code work for full page plugins. + var title; + var description; + var charset; + + let docInfo = yield this._getPageDetails(aBrowser); + + try { + title = docInfo.isErrorPage ? PlacesUtils.history.getPageTitle(uri) + : aBrowser.contentTitle; + title = title || uri.spec; + description = docInfo.description; + charset = aBrowser.characterSet; + } + catch (e) { } + + if (aShowEditUI && isNewBookmark) { + // If we bookmark the page here but open right into a cancelable + // state (i.e. new bookmark in Library), start batching here so + // all of the actions can be undone in a single undo step. + StarUI.beginBatch(); + } + + var parent = aParent !== undefined ? + aParent : PlacesUtils.unfiledBookmarksFolderId; + var descAnno = { name: PlacesUIUtils.DESCRIPTION_ANNO, value: description }; + var txn = new PlacesCreateBookmarkTransaction(uri, parent, + PlacesUtils.bookmarks.DEFAULT_INDEX, + title, null, [descAnno]); + PlacesUtils.transactionManager.doTransaction(txn); + itemId = txn.item.id; + // Set the character-set. + if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) + PlacesUtils.setCharsetForURI(uri, charset); + } + + // Revert the contents of the location bar + gURLBar.handleRevert(); + + // If it was not requested to open directly in "edit" mode, we are done. + if (!aShowEditUI) + return; + + // Try to dock the panel to: + // 1. the bookmarks menu button + // 2. the identity icon + // 3. the content area + if (BookmarkingUI.anchor) { + StarUI.showEditBookmarkPopup(itemId, BookmarkingUI.anchor, + "bottomcenter topright", isNewBookmark); + return; + } + + let identityIcon = document.getElementById("identity-icon"); + if (isElementVisible(identityIcon)) { + StarUI.showEditBookmarkPopup(itemId, identityIcon, + "bottomcenter topright", isNewBookmark); + } else { + StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap", isNewBookmark); + } + }), + + // TODO: Replace bookmarkPage code with this function once legacy + // transactions are removed. + _bookmarkPagePT: Task.async(function* (aBrowser, aParentId, aShowEditUI) { + let url = new URL(aBrowser.currentURI.spec); + let info = yield PlacesUtils.bookmarks.fetch({ url }); + let isNewBookmark = !info; + if (!info) { + let parentGuid = aParentId !== undefined ? + yield PlacesUtils.promiseItemGuid(aParentId) : + PlacesUtils.bookmarks.unfiledGuid; + info = { url, parentGuid }; + // Bug 1148838 - Make this code work for full page plugins. + let description = null; + let charset = null; + + let docInfo = yield this._getPageDetails(aBrowser); + + try { + info.title = docInfo.isErrorPage ? + (yield PlacesUtils.promisePlaceInfo(aBrowser.currentURI)).title : + aBrowser.contentTitle; + info.title = info.title || url.href; + description = docInfo.description; + charset = aBrowser.characterSet; + } + catch (e) { + Components.utils.reportError(e); + } + + if (aShowEditUI && isNewBookmark) { + // If we bookmark the page here but open right into a cancelable + // state (i.e. new bookmark in Library), start batching here so + // all of the actions can be undone in a single undo step. + StarUI.beginBatch(); + } + + if (description) { + info.annotations = [{ name: PlacesUIUtils.DESCRIPTION_ANNO + , value: description }]; + } + + info.guid = yield PlacesTransactions.NewBookmark(info).transact(); + + // Set the character-set + if (charset && !PrivateBrowsingUtils.isBrowserPrivate(aBrowser)) + PlacesUtils.setCharsetForURI(makeURI(url.href), charset); + } + + // Revert the contents of the location bar + gURLBar.handleRevert(); + + // If it was not requested to open directly in "edit" mode, we are done. + if (!aShowEditUI) + return; + + let node = yield PlacesUIUtils.promiseNodeLikeFromFetchInfo(info); + + // Try to dock the panel to: + // 1. the bookmarks menu button + // 2. the identity icon + // 3. the content area + if (BookmarkingUI.anchor) { + StarUI.showEditBookmarkPopup(node, BookmarkingUI.anchor, + "bottomcenter topright", isNewBookmark); + return; + } + + let identityIcon = document.getElementById("identity-icon"); + if (isElementVisible(identityIcon)) { + StarUI.showEditBookmarkPopup(node, identityIcon, + "bottomcenter topright", isNewBookmark); + } else { + StarUI.showEditBookmarkPopup(node, aBrowser, "overlap", isNewBookmark); + } + }), + + _getPageDetails(browser) { + return new Promise(resolve => { + let mm = browser.messageManager; + mm.addMessageListener("Bookmarks:GetPageDetails:Result", function listener(msg) { + mm.removeMessageListener("Bookmarks:GetPageDetails:Result", listener); + resolve(msg.data); + }); + + mm.sendAsyncMessage("Bookmarks:GetPageDetails", { }) + }); + }, + + /** + * Adds a bookmark to the page loaded in the current tab. + */ + bookmarkCurrentPage: function PCH_bookmarkCurrentPage(aShowEditUI, aParent) { + this.bookmarkPage(gBrowser.selectedBrowser, aParent, aShowEditUI); + }, + + /** + * Adds a bookmark to the page targeted by a link. + * @param aParent + * The folder in which to create a new bookmark if aURL isn't + * bookmarked. + * @param aURL (string) + * the address of the link target + * @param aTitle + * The link text + * @param [optional] aDescription + * The linked page description, if available + */ + bookmarkLink: Task.async(function* (aParentId, aURL, aTitle, aDescription="") { + let node = yield PlacesUIUtils.fetchNodeLike({ url: aURL }); + if (node) { + PlacesUIUtils.showBookmarkDialog({ action: "edit" + , node + }, window.top); + return; + } + + let ip = new InsertionPoint(aParentId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Components.interfaces.nsITreeView.DROP_ON); + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: makeURI(aURL) + , title: aTitle + , description: aDescription + , defaultInsertionPoint: ip + , hiddenRows: [ "description" + , "location" + , "loadInSidebar" + , "keyword" ] + }, window.top); + }), + + /** + * List of nsIURI objects characterizing the tabs currently open in the + * browser, modulo pinned tabs. The URIs will be in the order in which their + * corresponding tabs appeared and duplicates are discarded. + */ + get uniqueCurrentPages() { + let uniquePages = {}; + let URIs = []; + + gBrowser.visibleTabs.forEach(tab => { + let browser = tab.linkedBrowser; + let uri = browser.currentURI; + let title = browser.contentTitle || tab.label; + let spec = uri.spec; + if (!tab.pinned && !(spec in uniquePages)) { + uniquePages[spec] = null; + URIs.push({ uri, title }); + } + }); + return URIs; + }, + + /** + * Adds a folder with bookmarks to all of the currently open tabs in this + * window. + */ + bookmarkCurrentPages: function PCH_bookmarkCurrentPages() { + let pages = this.uniqueCurrentPages; + if (pages.length > 1) { + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "folder" + , URIList: pages + , hiddenRows: [ "description" ] + }, window); + } + }, + + /** + * Updates disabled state for the "Bookmark All Tabs" command. + */ + updateBookmarkAllTabsCommand: + function PCH_updateBookmarkAllTabsCommand() { + // There's nothing to do in non-browser windows. + if (window.location.href != getBrowserURL()) + return; + + // Disable "Bookmark All Tabs" if there are less than two + // "unique current pages". + goSetCommandEnabled("Browser:BookmarkAllTabs", + this.uniqueCurrentPages.length >= 2); + }, + + /** + * Adds a Live Bookmark to a feed associated with the current page. + * @param url + * The nsIURI of the page the feed was attached to + * @title title + * The title of the feed. Optional. + * @subtitle subtitle + * A short description of the feed. Optional. + */ + addLiveBookmark: Task.async(function *(url, feedTitle, feedSubtitle) { + let toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Components.interfaces.nsITreeView.DROP_ON); + + let feedURI = makeURI(url); + let title = feedTitle || gBrowser.contentTitle; + let description = feedSubtitle; + if (!description) { + description = (yield this._getPageDetails(gBrowser.selectedBrowser)).description; + } + + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "livemark" + , feedURI: feedURI + , siteURI: gBrowser.currentURI + , title: title + , description: description + , defaultInsertionPoint: toolbarIP + , hiddenRows: [ "feedLocation" + , "siteLocation" + , "description" ] + }, window); + }), + + /** + * Opens the Places Organizer. + * @param aLeftPaneRoot + * The query to select in the organizer window - options + * are: History, AllBookmarks, BookmarksMenu, BookmarksToolbar, + * UnfiledBookmarks, Tags and Downloads. + */ + showPlacesOrganizer: function PCH_showPlacesOrganizer(aLeftPaneRoot) { + var organizer = Services.wm.getMostRecentWindow("Places:Organizer"); + // Due to bug 528706, getMostRecentWindow can return closed windows. + if (!organizer || organizer.closed) { + // No currently open places window, so open one with the specified mode. + openDialog("chrome://browser/content/places/places.xul", + "", "chrome,toolbar=yes,dialog=no,resizable", aLeftPaneRoot); + } + else { + organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot); + organizer.focus(); + } + } +}; + +XPCOMUtils.defineLazyModuleGetter(this, "RecentlyClosedTabsAndWindowsMenuUtils", + "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm"); + +// View for the history menu. +function HistoryMenu(aPopupShowingEvent) { + // Workaround for Bug 610187. The sidebar does not include all the Places + // views definitions, and we don't need them there. + // Defining the prototype inheritance in the prototype itself would cause + // browser.js to halt on "PlacesMenu is not defined" error. + this.__proto__.__proto__ = PlacesMenu.prototype; + PlacesMenu.call(this, aPopupShowingEvent, + "place:sort=4&maxResults=15"); +} + +HistoryMenu.prototype = { + _getClosedTabCount() { + // SessionStore doesn't track the hidden window, so just return zero then. + if (window == Services.appShell.hiddenDOMWindow) { + return 0; + } + + return SessionStore.getClosedTabCount(window); + }, + + toggleRecentlyClosedTabs: function HM_toggleRecentlyClosedTabs() { + // enable/disable the Recently Closed Tabs sub menu + var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0]; + + // no restorable tabs, so disable menu + if (this._getClosedTabCount() == 0) + undoMenu.setAttribute("disabled", true); + else + undoMenu.removeAttribute("disabled"); + }, + + /** + * Populate when the history menu is opened + */ + populateUndoSubmenu: function PHM_populateUndoSubmenu() { + var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedTabsMenu")[0]; + var undoPopup = undoMenu.firstChild; + + // remove existing menu items + while (undoPopup.hasChildNodes()) + undoPopup.removeChild(undoPopup.firstChild); + + // no restorable tabs, so make sure menu is disabled, and return + if (this._getClosedTabCount() == 0) { + undoMenu.setAttribute("disabled", true); + return; + } + + // enable menu + undoMenu.removeAttribute("disabled"); + + // populate menu + let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment(window, "menuitem"); + undoPopup.appendChild(tabsFragment); + }, + + toggleRecentlyClosedWindows: function PHM_toggleRecentlyClosedWindows() { + // enable/disable the Recently Closed Windows sub menu + var undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0]; + + // no restorable windows, so disable menu + if (SessionStore.getClosedWindowCount() == 0) + undoMenu.setAttribute("disabled", true); + else + undoMenu.removeAttribute("disabled"); + }, + + /** + * Populate when the history menu is opened + */ + populateUndoWindowSubmenu: function PHM_populateUndoWindowSubmenu() { + let undoMenu = this._rootElt.getElementsByClassName("recentlyClosedWindowsMenu")[0]; + let undoPopup = undoMenu.firstChild; + + // remove existing menu items + while (undoPopup.hasChildNodes()) + undoPopup.removeChild(undoPopup.firstChild); + + // no restorable windows, so make sure menu is disabled, and return + if (SessionStore.getClosedWindowCount() == 0) { + undoMenu.setAttribute("disabled", true); + return; + } + + // enable menu + undoMenu.removeAttribute("disabled"); + + // populate menu + let windowsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment(window, "menuitem"); + undoPopup.appendChild(windowsFragment); + }, + + toggleTabsFromOtherComputers: function PHM_toggleTabsFromOtherComputers() { + // Enable/disable the Tabs From Other Computers menu. Some of the menus handled + // by HistoryMenu do not have this menuitem. + let menuitem = this._rootElt.getElementsByClassName("syncTabsMenuItem")[0]; + if (!menuitem) + return; + + if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) { + menuitem.setAttribute("hidden", true); + return; + } + + menuitem.setAttribute("hidden", false); + }, + + _onPopupShowing: function HM__onPopupShowing(aEvent) { + PlacesMenu.prototype._onPopupShowing.apply(this, arguments); + + // Don't handle events for submenus. + if (aEvent.target != aEvent.currentTarget) + return; + + this.toggleRecentlyClosedTabs(); + this.toggleRecentlyClosedWindows(); + this.toggleTabsFromOtherComputers(); + }, + + _onCommand: function HM__onCommand(aEvent) { + let placesNode = aEvent.target._placesNode; + if (placesNode) { + if (!PrivateBrowsingUtils.isWindowPrivate(window)) + PlacesUIUtils.markPageAsTyped(placesNode.uri); + openUILink(placesNode.uri, aEvent, { ignoreAlt: true }); + } + } +}; + +/** + * Functions for handling events in the Bookmarks Toolbar and menu. + */ +var BookmarksEventHandler = { + /** + * Handler for click event for an item in the bookmarks toolbar or menu. + * Menus and submenus from the folder buttons bubble up to this handler. + * Left-click is handled in the onCommand function. + * When items are middle-clicked (or clicked with modifier), open in tabs. + * If the click came through a menu, close the menu. + * @param aEvent + * DOMEvent for the click + * @param aView + * The places view which aEvent should be associated with. + */ + onClick: function BEH_onClick(aEvent, aView) { + // Only handle middle-click or left-click with modifiers. + let modifKey; + if (AppConstants.platform == "macosx") { + modifKey = aEvent.metaKey || aEvent.shiftKey; + } else { + modifKey = aEvent.ctrlKey || aEvent.shiftKey; + } + + if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey)) + return; + + var target = aEvent.originalTarget; + // If this event bubbled up from a menu or menuitem, close the menus. + // Do this before opening tabs, to avoid hiding the open tabs confirm-dialog. + if (target.localName == "menu" || target.localName == "menuitem") { + for (let node = target.parentNode; node; node = node.parentNode) { + if (node.localName == "menupopup") + node.hidePopup(); + else if (node.localName != "menu" && + node.localName != "splitmenu" && + node.localName != "hbox" && + node.localName != "vbox" ) + break; + } + } + + if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) { + // Don't open the root folder in tabs when the empty area on the toolbar + // is middle-clicked or when a non-bookmark item except for Open in Tabs) + // in a bookmarks menupopup is middle-clicked. + if (target.localName == "menu" || target.localName == "toolbarbutton") + PlacesUIUtils.openContainerNodeInTabs(target._placesNode, aEvent, aView); + } + else if (aEvent.button == 1) { + // left-clicks with modifier are already served by onCommand + this.onCommand(aEvent, aView); + } + }, + + /** + * Handler for command event for an item in the bookmarks toolbar. + * Menus and submenus from the folder buttons bubble up to this handler. + * Opens the item. + * @param aEvent + * DOMEvent for the command + * @param aView + * The places view which aEvent should be associated with. + */ + onCommand: function BEH_onCommand(aEvent, aView) { + var target = aEvent.originalTarget; + if (target._placesNode) + PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView); + }, + + fillInBHTooltip: function BEH_fillInBHTooltip(aDocument, aEvent) { + var node; + var cropped = false; + var targetURI; + + if (aDocument.tooltipNode.localName == "treechildren") { + var tree = aDocument.tooltipNode.parentNode; + var tbo = tree.treeBoxObject; + var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY); + if (cell.row == -1) + return false; + node = tree.view.nodeForTreeIndex(cell.row); + cropped = tbo.isCellCropped(cell.row, cell.col); + } + else { + // Check whether the tooltipNode is a Places node. + // In such a case use it, otherwise check for targetURI attribute. + var tooltipNode = aDocument.tooltipNode; + if (tooltipNode._placesNode) + node = tooltipNode._placesNode; + else { + // This is a static non-Places node. + targetURI = tooltipNode.getAttribute("targetURI"); + } + } + + if (!node && !targetURI) + return false; + + // Show node.label as tooltip's title for non-Places nodes. + var title = node ? node.title : tooltipNode.label; + + // Show URL only for Places URI-nodes or nodes with a targetURI attribute. + var url; + if (targetURI || PlacesUtils.nodeIsURI(node)) + url = targetURI || node.uri; + + // Show tooltip for containers only if their title is cropped. + if (!cropped && !url) + return false; + + var tooltipTitle = aDocument.getElementById("bhtTitleText"); + tooltipTitle.hidden = (!title || (title == url)); + if (!tooltipTitle.hidden) + tooltipTitle.textContent = title; + + var tooltipUrl = aDocument.getElementById("bhtUrlText"); + tooltipUrl.hidden = !url; + if (!tooltipUrl.hidden) + tooltipUrl.value = url; + + // Show tooltip. + return true; + } +}; + +// Handles special drag and drop functionality for Places menus that are not +// part of a Places view (e.g. the bookmarks menu in the menubar). +var PlacesMenuDNDHandler = { + _springLoadDelayMs: 350, + _closeDelayMs: 500, + _loadTimer: null, + _closeTimer: null, + _closingTimerNode: null, + + /** + * Called when the user enters the <menu> element during a drag. + * @param event + * The DragEnter event that spawned the opening. + */ + onDragEnter: function PMDH_onDragEnter(event) { + // Opening menus in a Places popup is handled by the view itself. + if (!this._isStaticContainer(event.target)) + return; + + // If we re-enter the same menu or anchor before the close timer runs out, + // we should ensure that we do not close: + if (this._closeTimer && this._closingTimerNode === event.currentTarget) { + this._closeTimer.cancel(); + this._closingTimerNode = null; + this._closeTimer = null; + } + + PlacesControllerDragHelper.currentDropTarget = event.target; + let popup = event.target.lastChild; + if (this._loadTimer || popup.state === "showing" || popup.state === "open") + return; + + this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._loadTimer.initWithCallback(() => { + this._loadTimer = null; + popup.setAttribute("autoopened", "true"); + popup.showPopup(popup); + }, this._springLoadDelayMs, Ci.nsITimer.TYPE_ONE_SHOT); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Handles dragleave on the <menu> element. + */ + onDragLeave: function PMDH_onDragLeave(event) { + // Handle menu-button separate targets. + if (event.relatedTarget === event.currentTarget || + (event.relatedTarget && + event.relatedTarget.parentNode === event.currentTarget)) + return; + + // Closing menus in a Places popup is handled by the view itself. + if (!this._isStaticContainer(event.target)) + return; + + PlacesControllerDragHelper.currentDropTarget = null; + let popup = event.target.lastChild; + + if (this._loadTimer) { + this._loadTimer.cancel(); + this._loadTimer = null; + } + this._closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._closingTimerNode = event.currentTarget; + this._closeTimer.initWithCallback(function() { + this._closeTimer = null; + this._closingTimerNode = null; + let node = PlacesControllerDragHelper.currentDropTarget; + let inHierarchy = false; + while (node && !inHierarchy) { + inHierarchy = node == event.target; + node = node.parentNode; + } + if (!inHierarchy && popup && popup.hasAttribute("autoopened")) { + popup.removeAttribute("autoopened"); + popup.hidePopup(); + } + }, this._closeDelayMs, Ci.nsITimer.TYPE_ONE_SHOT); + }, + + /** + * Determines if a XUL element represents a static container. + * @returns true if the element is a container element (menu or + *` menu-toolbarbutton), false otherwise. + */ + _isStaticContainer: function PMDH__isContainer(node) { + let isMenu = node.localName == "menu" || + (node.localName == "toolbarbutton" && + (node.getAttribute("type") == "menu" || + node.getAttribute("type") == "menu-button")); + let isStatic = !("_placesNode" in node) && node.lastChild && + node.lastChild.hasAttribute("placespopup") && + !node.parentNode.hasAttribute("placespopup"); + return isMenu && isStatic; + }, + + /** + * Called when the user drags over the <menu> element. + * @param event + * The DragOver event. + */ + onDragOver: function PMDH_onDragOver(event) { + let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Components.interfaces.nsITreeView.DROP_ON); + if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer)) + event.preventDefault(); + + event.stopPropagation(); + }, + + /** + * Called when the user drops on the <menu> element. + * @param event + * The Drop event. + */ + onDrop: function PMDH_onDrop(event) { + // Put the item at the end of bookmark menu. + let ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX, + Components.interfaces.nsITreeView.DROP_ON); + PlacesControllerDragHelper.onDrop(ip, event.dataTransfer); + PlacesControllerDragHelper.currentDropTarget = null; + event.stopPropagation(); + } +}; + +/** + * This object handles the initialization and uninitialization of the bookmarks + * toolbar. + */ +var PlacesToolbarHelper = { + _place: "place:folder=TOOLBAR", + + get _viewElt() { + return document.getElementById("PlacesToolbar"); + }, + + get _placeholder() { + return document.getElementById("bookmarks-toolbar-placeholder"); + }, + + init: function PTH_init(forceToolbarOverflowCheck) { + let viewElt = this._viewElt; + if (!viewElt || viewElt._placesView) + return; + + // CustomizableUI.addListener is idempotent, so we can safely + // call this multiple times. + CustomizableUI.addListener(this); + + // If the bookmarks toolbar item is: + // - not in a toolbar, or; + // - the toolbar is collapsed, or; + // - the toolbar is hidden some other way: + // don't initialize. Also, there is no need to initialize the toolbar if + // customizing, because that will happen when the customization is done. + let toolbar = this._getParentToolbar(viewElt); + if (!toolbar || toolbar.collapsed || this._isCustomizing || + getComputedStyle(toolbar, "").display == "none") + return; + + new PlacesToolbar(this._place); + if (forceToolbarOverflowCheck) { + viewElt._placesView.updateOverflowStatus(); + } + this._shouldWrap = false; + this._setupPlaceholder(); + }, + + uninit: function PTH_uninit() { + CustomizableUI.removeListener(this); + }, + + customizeStart: function PTH_customizeStart() { + try { + let viewElt = this._viewElt; + if (viewElt && viewElt._placesView) + viewElt._placesView.uninit(); + } finally { + this._isCustomizing = true; + } + this._shouldWrap = this._getShouldWrap(); + }, + + customizeChange: function PTH_customizeChange() { + this._setupPlaceholder(); + }, + + _setupPlaceholder: function PTH_setupPlaceholder() { + let placeholder = this._placeholder; + if (!placeholder) { + return; + } + + let shouldWrapNow = this._getShouldWrap(); + if (this._shouldWrap != shouldWrapNow) { + if (shouldWrapNow) { + placeholder.setAttribute("wrap", "true"); + } else { + placeholder.removeAttribute("wrap"); + } + this._shouldWrap = shouldWrapNow; + } + }, + + customizeDone: function PTH_customizeDone() { + this._isCustomizing = false; + this.init(true); + }, + + _getShouldWrap: function PTH_getShouldWrap() { + let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks"); + let area = placement && placement.area; + let areaType = area && CustomizableUI.getAreaType(area); + return !area || CustomizableUI.TYPE_MENU_PANEL == areaType; + }, + + onPlaceholderCommand: function () { + let widgetGroup = CustomizableUI.getWidget("personal-bookmarks"); + let widget = widgetGroup.forWindow(window); + if (widget.overflowed || + widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) { + PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar"); + } + }, + + _getParentToolbar: function(element) { + while (element) { + if (element.localName == "toolbar") { + return element; + } + element = element.parentNode; + } + return null; + }, + + onWidgetUnderflow: function(aNode, aContainer) { + // The view gets broken by being removed and reinserted by the overflowable + // toolbar, so we have to force an uninit and reinit. + let win = aNode.ownerGlobal; + if (aNode.id == "personal-bookmarks" && win == window) { + this._resetView(); + } + }, + + onWidgetAdded: function(aWidgetId, aArea, aPosition) { + if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) { + // It's possible (with the "Add to Menu", "Add to Toolbar" context + // options) that the Places Toolbar Items have been moved without + // letting us prepare and handle it with with customizeStart and + // customizeDone. If that's the case, we need to reset the views + // since they're probably broken from the DOM reparenting. + this._resetView(); + } + }, + + _resetView: function() { + if (this._viewElt) { + // It's possible that the placesView might not exist, and we need to + // do a full init. This could happen if the Bookmarks Toolbar Items are + // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar" + // context menu option, outside of customize mode. + if (this._viewElt._placesView) { + this._viewElt._placesView.uninit(); + } + this.init(true); + } + }, +}; + +/** + * Handles the bookmarks menu-button in the toolbar. + */ + +var BookmarkingUI = { + BOOKMARK_BUTTON_ID: "bookmarks-menu-button", + BOOKMARK_BUTTON_SHORTCUT: "addBookmarkAsKb", + get button() { + delete this.button; + let widgetGroup = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID); + return this.button = widgetGroup.forWindow(window).node; + }, + + /* Can't make this a self-deleting getter because it's anonymous content + * and might lose/regain bindings at some point. */ + get star() { + return document.getAnonymousElementByAttribute(this.button, "anonid", + "button"); + }, + + get anchor() { + if (!this._shouldUpdateStarState()) { + return null; + } + let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID) + .forWindow(window); + if (widget.overflowed) + return widget.anchor; + + let star = this.star; + return star ? document.getAnonymousElementByAttribute(star, "class", + "toolbarbutton-icon") + : null; + }, + + get notifier() { + delete this.notifier; + return this.notifier = document.getElementById("bookmarked-notification-anchor"); + }, + + get dropmarkerNotifier() { + delete this.dropmarkerNotifier; + return this.dropmarkerNotifier = document.getElementById("bookmarked-notification-dropmarker-anchor"); + }, + + get broadcaster() { + delete this.broadcaster; + let broadcaster = document.getElementById("bookmarkThisPageBroadcaster"); + return this.broadcaster = broadcaster; + }, + + STATUS_UPDATING: -1, + STATUS_UNSTARRED: 0, + STATUS_STARRED: 1, + get status() { + if (!this._shouldUpdateStarState()) { + return this.STATUS_UNSTARRED; + } + if (this._pendingStmt) + return this.STATUS_UPDATING; + return this.button.hasAttribute("starred") ? this.STATUS_STARRED + : this.STATUS_UNSTARRED; + }, + + get _starredTooltip() + { + delete this._starredTooltip; + return this._starredTooltip = + this._getFormattedTooltip("starButtonOn.tooltip2"); + }, + + get _unstarredTooltip() + { + delete this._unstarredTooltip; + return this._unstarredTooltip = + this._getFormattedTooltip("starButtonOff.tooltip2"); + }, + + _getFormattedTooltip: function(strId) { + let args = []; + let shortcut = document.getElementById(this.BOOKMARK_BUTTON_SHORTCUT); + if (shortcut) + args.push(ShortcutUtils.prettifyShortcut(shortcut)); + return gNavigatorBundle.getFormattedString(strId, args); + }, + + /** + * The type of the area in which the button is currently located. + * When in the panel, we don't update the button's icon. + */ + _currentAreaType: null, + _shouldUpdateStarState: function() { + return this._currentAreaType == CustomizableUI.TYPE_TOOLBAR; + }, + + /** + * The popup contents must be updated when the user customizes the UI, or + * changes the personal toolbar collapsed status. In such a case, any needed + * change should be handled in the popupshowing helper, for performance + * reasons. + */ + _popupNeedsUpdate: true, + onToolbarVisibilityChange: function BUI_onToolbarVisibilityChange() { + this._popupNeedsUpdate = true; + }, + + onPopupShowing: function BUI_onPopupShowing(event) { + // Don't handle events for submenus. + if (event.target != event.currentTarget) + return; + + // Ideally this code would never be reached, but if you click the outer + // button's border, some cpp code for the menu button's so-called XBL binding + // decides to open the popup even though the dropmarker is invisible. + if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) { + this._showSubview(); + event.preventDefault(); + event.stopPropagation(); + return; + } + + let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID) + .forWindow(window); + if (widget.overflowed) { + // Don't open a popup in the overflow popup, rather just open the Library. + event.preventDefault(); + widget.node.removeAttribute("closemenu"); + PlacesCommandHook.showPlacesOrganizer("BookmarksMenu"); + return; + } + + this._initRecentBookmarks(document.getElementById("BMB_recentBookmarks"), + "subviewbutton"); + + if (!this._popupNeedsUpdate) + return; + this._popupNeedsUpdate = false; + + let popup = event.target; + let getPlacesAnonymousElement = + aAnonId => document.getAnonymousElementByAttribute(popup.parentNode, + "placesanonid", + aAnonId); + + let viewToolbarMenuitem = getPlacesAnonymousElement("view-toolbar"); + if (viewToolbarMenuitem) { + // Update View bookmarks toolbar checkbox menuitem. + viewToolbarMenuitem.classList.add("subviewbutton"); + let personalToolbar = document.getElementById("PersonalToolbar"); + viewToolbarMenuitem.setAttribute("checked", !personalToolbar.collapsed); + } + }, + + attachPlacesView: function(event, node) { + // If the view is already there, bail out early. + if (node.parentNode._placesView) + return; + + new PlacesMenu(event, "place:folder=BOOKMARKS_MENU", { + extraClasses: { + entry: "subviewbutton", + footer: "panel-subview-footer" + }, + insertionPoint: ".panel-subview-footer" + }); + }, + + RECENTLY_BOOKMARKED_PREF: "browser.bookmarks.showRecentlyBookmarked", + + _initRecentBookmarks(aHeaderItem, aExtraCSSClass) { + this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass); + + // Add observers and listeners and remove them again when the menupopup closes. + + let bookmarksMenu = aHeaderItem.parentNode; + let placesContextMenu = document.getElementById("placesContext"); + + let prefObserver = () => { + this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass); + }; + + this._recentlyBookmarkedObserver = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver, + Ci.nsISupportsWeakReference + ]) + }; + this._recentlyBookmarkedObserver.onItemRemoved = () => { + // Update the menu when a bookmark has been removed. + // The native menubar on Mac doesn't support live update, so this won't + // work there. + this._populateRecentBookmarks(aHeaderItem, aExtraCSSClass); + }; + + let updatePlacesContextMenu = (shouldHidePrefUI = false) => { + let prefEnabled = !shouldHidePrefUI && Services.prefs.getBoolPref(this.RECENTLY_BOOKMARKED_PREF); + let showItem = document.getElementById("placesContext_showRecentlyBookmarked"); + let hideItem = document.getElementById("placesContext_hideRecentlyBookmarked"); + let separator = document.getElementById("placesContext_recentlyBookmarkedSeparator"); + showItem.hidden = shouldHidePrefUI || prefEnabled; + hideItem.hidden = shouldHidePrefUI || !prefEnabled; + separator.hidden = shouldHidePrefUI; + if (!shouldHidePrefUI) { + // Move to the bottom of the menu. + separator.parentNode.appendChild(separator); + showItem.parentNode.appendChild(showItem); + hideItem.parentNode.appendChild(hideItem); + } + }; + + let onPlacesContextMenuShowing = event => { + if (event.target == event.currentTarget) { + let triggerPopup = event.target.triggerNode; + while (triggerPopup && triggerPopup.localName != "menupopup") { + triggerPopup = triggerPopup.parentNode; + } + let shouldHidePrefUI = triggerPopup != bookmarksMenu; + updatePlacesContextMenu(shouldHidePrefUI); + } + }; + + let onBookmarksMenuHidden = event => { + if (event.target == event.currentTarget) { + updatePlacesContextMenu(true); + + Services.prefs.removeObserver(this.RECENTLY_BOOKMARKED_PREF, prefObserver, false); + PlacesUtils.bookmarks.removeObserver(this._recentlyBookmarkedObserver); + this._recentlyBookmarkedObserver = null; + if (placesContextMenu) { + placesContextMenu.removeEventListener("popupshowing", onPlacesContextMenuShowing); + } + bookmarksMenu.removeEventListener("popuphidden", onBookmarksMenuHidden); + } + }; + + Services.prefs.addObserver(this.RECENTLY_BOOKMARKED_PREF, prefObserver, false); + PlacesUtils.bookmarks.addObserver(this._recentlyBookmarkedObserver, true); + + // The context menu doesn't exist in non-browser windows on Mac + if (placesContextMenu) { + placesContextMenu.addEventListener("popupshowing", onPlacesContextMenuShowing); + } + + bookmarksMenu.addEventListener("popuphidden", onBookmarksMenuHidden); + }, + + _populateRecentBookmarks(aHeaderItem, aExtraCSSClass = "") { + while (aHeaderItem.nextSibling && + aHeaderItem.nextSibling.localName == "menuitem") { + aHeaderItem.nextSibling.remove(); + } + + let shouldShow = Services.prefs.getBoolPref(this.RECENTLY_BOOKMARKED_PREF); + let separator = aHeaderItem.previousSibling; + aHeaderItem.hidden = !shouldShow; + separator.hidden = !shouldShow; + + if (!shouldShow) { + return; + } + + const kMaxResults = 5; + + let options = PlacesUtils.history.getNewQueryOptions(); + options.excludeQueries = true; + options.queryType = options.QUERY_TYPE_BOOKMARKS; + options.sortingMode = options.SORT_BY_DATEADDED_DESCENDING; + options.maxResults = kMaxResults; + let query = PlacesUtils.history.getNewQuery(); + + let fragment = document.createDocumentFragment(); + let root = PlacesUtils.history.executeQuery(query, options).root; + root.containerOpen = true; + for (let i = 0; i < root.childCount; i++) { + let node = root.getChild(i); + let uri = node.uri; + let title = node.title; + let icon = node.icon; + + let item = + document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "menuitem"); + item.setAttribute("label", title || uri); + item.setAttribute("targetURI", uri); + item.setAttribute("simulated-places-node", true); + item.setAttribute("class", "menuitem-iconic menuitem-with-favicon bookmark-item " + + aExtraCSSClass); + if (icon) { + item.setAttribute("image", icon); + } + item._placesNode = node; + fragment.appendChild(item); + } + root.containerOpen = false; + aHeaderItem.parentNode.insertBefore(fragment, aHeaderItem.nextSibling); + }, + + showRecentlyBookmarked() { + Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, true); + }, + + hideRecentlyBookmarked() { + Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, false); + }, + + _updateCustomizationState: function BUI__updateCustomizationState() { + let placement = CustomizableUI.getPlacementOfWidget(this.BOOKMARK_BUTTON_ID); + this._currentAreaType = placement && CustomizableUI.getAreaType(placement.area); + }, + + _uninitView: function BUI__uninitView() { + // When an element with a placesView attached is removed and re-inserted, + // XBL reapplies the binding causing any kind of issues and possible leaks, + // so kill current view and let popupshowing generate a new one. + if (this.button._placesView) + this.button._placesView.uninit(); + // ...and do the same for the menu bar. + let menubar = document.getElementById("bookmarksMenu"); + if (menubar && menubar._placesView) + menubar._placesView.uninit(); + + // We have to do the same thing for the "special" views underneath the + // the bookmarks menu. + const kSpecialViewNodeIDs = ["BMB_bookmarksToolbar", "BMB_unsortedBookmarks"]; + for (let viewNodeID of kSpecialViewNodeIDs) { + let elem = document.getElementById(viewNodeID); + if (elem && elem._placesView) { + elem._placesView.uninit(); + } + } + }, + + onCustomizeStart: function BUI_customizeStart(aWindow) { + if (aWindow == window) { + this._uninitView(); + this._isCustomizing = true; + } + }, + + onWidgetAdded: function BUI_widgetAdded(aWidgetId) { + if (aWidgetId == this.BOOKMARK_BUTTON_ID) { + this._onWidgetWasMoved(); + } + }, + + onWidgetRemoved: function BUI_widgetRemoved(aWidgetId) { + if (aWidgetId == this.BOOKMARK_BUTTON_ID) { + this._onWidgetWasMoved(); + } + }, + + onWidgetReset: function BUI_widgetReset(aNode, aContainer) { + if (aNode == this.button) { + this._onWidgetWasMoved(); + } + }, + + onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode, aContainer) { + if (aNode == this.button) { + this._onWidgetWasMoved(); + } + }, + + _onWidgetWasMoved: function BUI_widgetWasMoved() { + let usedToUpdateStarState = this._shouldUpdateStarState(); + this._updateCustomizationState(); + if (!usedToUpdateStarState && this._shouldUpdateStarState()) { + this.updateStarState(); + } else if (usedToUpdateStarState && !this._shouldUpdateStarState()) { + this._updateStar(); + } + // If we're moved outside of customize mode, we need to uninit + // our view so it gets reconstructed. + if (!this._isCustomizing) { + this._uninitView(); + } + }, + + onCustomizeEnd: function BUI_customizeEnd(aWindow) { + if (aWindow == window) { + this._isCustomizing = false; + this.onToolbarVisibilityChange(); + } + }, + + init: function() { + CustomizableUI.addListener(this); + this._updateCustomizationState(); + }, + + _hasBookmarksObserver: false, + _itemIds: [], + uninit: function BUI_uninit() { + this._updateBookmarkPageMenuItem(true); + CustomizableUI.removeListener(this); + + this._uninitView(); + + if (this._hasBookmarksObserver) { + PlacesUtils.removeLazyBookmarkObserver(this); + } + + if (this._pendingStmt) { + this._pendingStmt.cancel(); + delete this._pendingStmt; + } + }, + + onLocationChange: function BUI_onLocationChange() { + if (this._uri && gBrowser.currentURI.equals(this._uri)) { + return; + } + this.updateStarState(); + }, + + updateStarState: function BUI_updateStarState() { + // Reset tracked values. + this._uri = gBrowser.currentURI; + this._itemIds = []; + + if (this._pendingStmt) { + this._pendingStmt.cancel(); + delete this._pendingStmt; + } + + this._pendingStmt = PlacesUtils.asyncGetBookmarkIds(this._uri, (aItemIds, aURI) => { + // Safety check that the bookmarked URI equals the tracked one. + if (!aURI.equals(this._uri)) { + Components.utils.reportError("BookmarkingUI did not receive current URI"); + return; + } + + // It's possible that onItemAdded gets called before the async statement + // calls back. For such an edge case, retain all unique entries from both + // arrays. + this._itemIds = this._itemIds.filter( + id => !aItemIds.includes(id) + ).concat(aItemIds); + + this._updateStar(); + + // Start observing bookmarks if needed. + if (!this._hasBookmarksObserver) { + try { + PlacesUtils.addLazyBookmarkObserver(this); + this._hasBookmarksObserver = true; + } catch (ex) { + Components.utils.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex); + } + } + + delete this._pendingStmt; + }); + }, + + _updateStar: function BUI__updateStar() { + if (!this._shouldUpdateStarState()) { + if (this.broadcaster.hasAttribute("starred")) { + this.broadcaster.removeAttribute("starred"); + this.broadcaster.removeAttribute("buttontooltiptext"); + } + return; + } + + if (this._itemIds.length > 0) { + this.broadcaster.setAttribute("starred", "true"); + this.broadcaster.setAttribute("buttontooltiptext", this._starredTooltip); + if (this.button.getAttribute("overflowedItem") == "true") { + this.button.setAttribute("label", this._starButtonOverflowedStarredLabel); + } + } + else { + this.broadcaster.removeAttribute("starred"); + this.broadcaster.setAttribute("buttontooltiptext", this._unstarredTooltip); + if (this.button.getAttribute("overflowedItem") == "true") { + this.button.setAttribute("label", this._starButtonOverflowedLabel); + } + } + }, + + /** + * forceReset is passed when we're destroyed and the label should go back + * to the default (Bookmark This Page) for OS X. + */ + _updateBookmarkPageMenuItem: function BUI__updateBookmarkPageMenuItem(forceReset) { + let isStarred = !forceReset && this._itemIds.length > 0; + let label = isStarred ? "editlabel" : "bookmarklabel"; + if (this.broadcaster) { + this.broadcaster.setAttribute("label", this.broadcaster.getAttribute(label)); + } + }, + + onMainMenuPopupShowing: function BUI_onMainMenuPopupShowing(event) { + // Don't handle events for submenus. + if (event.target != event.currentTarget) + return; + + this._updateBookmarkPageMenuItem(); + PlacesCommandHook.updateBookmarkAllTabsCommand(); + this._initRecentBookmarks(document.getElementById("menu_recentBookmarks")); + }, + + _showBookmarkedNotification: function BUI_showBookmarkedNotification() { + function getCenteringTransformForRects(rectToPosition, referenceRect) { + let topDiff = referenceRect.top - rectToPosition.top; + let leftDiff = referenceRect.left - rectToPosition.left; + let heightDiff = referenceRect.height - rectToPosition.height; + let widthDiff = referenceRect.width - rectToPosition.width; + return [(leftDiff + .5 * widthDiff) + "px", (topDiff + .5 * heightDiff) + "px"]; + } + + if (this._notificationTimeout) { + clearTimeout(this._notificationTimeout); + } + + if (this.notifier.style.transform == '') { + // Get all the relevant nodes and computed style objects + let dropmarker = document.getAnonymousElementByAttribute(this.button, "anonid", "dropmarker"); + let dropmarkerIcon = document.getAnonymousElementByAttribute(dropmarker, "class", "dropmarker-icon"); + let dropmarkerStyle = getComputedStyle(dropmarkerIcon); + + // Check for RTL and get bounds + let isRTL = getComputedStyle(this.button).direction == "rtl"; + let buttonRect = this.button.getBoundingClientRect(); + let notifierRect = this.notifier.getBoundingClientRect(); + let dropmarkerRect = dropmarkerIcon.getBoundingClientRect(); + let dropmarkerNotifierRect = this.dropmarkerNotifier.getBoundingClientRect(); + + // Compute, but do not set, transform for star icon + let [translateX, translateY] = getCenteringTransformForRects(notifierRect, buttonRect); + let starIconTransform = "translate(" + translateX + ", " + translateY + ")"; + if (isRTL) { + starIconTransform += " scaleX(-1)"; + } + + // Compute, but do not set, transform for dropmarker + [translateX, translateY] = getCenteringTransformForRects(dropmarkerNotifierRect, dropmarkerRect); + let dropmarkerTransform = "translate(" + translateX + ", " + translateY + ")"; + + // Do all layout invalidation in one go: + this.notifier.style.transform = starIconTransform; + this.dropmarkerNotifier.style.transform = dropmarkerTransform; + + let dropmarkerAnimationNode = this.dropmarkerNotifier.firstChild; + dropmarkerAnimationNode.style.MozImageRegion = dropmarkerStyle.MozImageRegion; + dropmarkerAnimationNode.style.listStyleImage = dropmarkerStyle.listStyleImage; + } + + let isInOverflowPanel = this.button.getAttribute("overflowedItem") == "true"; + if (!isInOverflowPanel) { + this.notifier.setAttribute("notification", "finish"); + this.button.setAttribute("notification", "finish"); + this.dropmarkerNotifier.setAttribute("notification", "finish"); + } + + this._notificationTimeout = setTimeout( () => { + this.notifier.removeAttribute("notification"); + this.dropmarkerNotifier.removeAttribute("notification"); + this.button.removeAttribute("notification"); + + this.dropmarkerNotifier.style.transform = ''; + this.notifier.style.transform = ''; + }, 1000); + }, + + _showSubview: function() { + let view = document.getElementById("PanelUI-bookmarks"); + view.addEventListener("ViewShowing", this); + view.addEventListener("ViewHiding", this); + let anchor = document.getElementById(this.BOOKMARK_BUTTON_ID); + anchor.setAttribute("closemenu", "none"); + PanelUI.showSubView("PanelUI-bookmarks", anchor, + CustomizableUI.AREA_PANEL); + }, + + onCommand: function BUI_onCommand(aEvent) { + if (aEvent.target != aEvent.currentTarget) { + return; + } + + // Handle special case when the button is in the panel. + let isBookmarked = this._itemIds.length > 0; + + if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) { + this._showSubview(); + return; + } + let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID) + .forWindow(window); + if (widget.overflowed) { + // Close the overflow panel because the Edit Bookmark panel will appear. + widget.node.removeAttribute("closemenu"); + } + + // Ignore clicks on the star if we are updating its state. + if (!this._pendingStmt) { + if (!isBookmarked) + this._showBookmarkedNotification(); + PlacesCommandHook.bookmarkCurrentPage(true); + } + }, + + onCurrentPageContextPopupShowing() { + this._updateBookmarkPageMenuItem(); + }, + + handleEvent: function BUI_handleEvent(aEvent) { + switch (aEvent.type) { + case "ViewShowing": + this.onPanelMenuViewShowing(aEvent); + break; + case "ViewHiding": + this.onPanelMenuViewHiding(aEvent); + break; + } + }, + + onPanelMenuViewShowing: function BUI_onViewShowing(aEvent) { + this._updateBookmarkPageMenuItem(); + // Update checked status of the toolbar toggle. + let viewToolbar = document.getElementById("panelMenu_viewBookmarksToolbar"); + let personalToolbar = document.getElementById("PersonalToolbar"); + if (personalToolbar.collapsed) + viewToolbar.removeAttribute("checked"); + else + viewToolbar.setAttribute("checked", "true"); + // Get all statically placed buttons to supply them with keyboard shortcuts. + let staticButtons = viewToolbar.parentNode.getElementsByTagName("toolbarbutton"); + for (let i = 0, l = staticButtons.length; i < l; ++i) + CustomizableUI.addShortcut(staticButtons[i]); + // Setup the Places view. + this._panelMenuView = new PlacesPanelMenuView("place:folder=BOOKMARKS_MENU", + "panelMenu_bookmarksMenu", + "panelMenu_bookmarksMenu", { + extraClasses: { + entry: "subviewbutton", + footer: "panel-subview-footer" + } + }); + aEvent.target.removeEventListener("ViewShowing", this); + }, + + onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) { + this._panelMenuView.uninit(); + delete this._panelMenuView; + aEvent.target.removeEventListener("ViewHiding", this); + }, + + onPanelMenuViewCommand: function BUI_onPanelMenuViewCommand(aEvent, aView) { + let target = aEvent.originalTarget; + if (!target._placesNode) + return; + if (PlacesUtils.nodeIsContainer(target._placesNode)) + PlacesCommandHook.showPlacesOrganizer([ "BookmarksMenu", target._placesNode.itemId ]); + else + PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent, aView); + PanelUI.hide(); + }, + + // nsINavBookmarkObserver + onItemAdded: function BUI_onItemAdded(aItemId, aParentId, aIndex, aItemType, + aURI) { + if (aURI && aURI.equals(this._uri)) { + // If a new bookmark has been added to the tracked uri, register it. + if (!this._itemIds.includes(aItemId)) { + this._itemIds.push(aItemId); + // Only need to update the UI if it wasn't marked as starred before: + if (this._itemIds.length == 1) { + this._updateStar(); + } + } + } + }, + + onItemRemoved: function BUI_onItemRemoved(aItemId) { + let index = this._itemIds.indexOf(aItemId); + // If one of the tracked bookmarks has been removed, unregister it. + if (index != -1) { + this._itemIds.splice(index, 1); + // Only need to update the UI if the page is no longer starred + if (this._itemIds.length == 0) { + this._updateStar(); + } + } + }, + + onItemChanged: function BUI_onItemChanged(aItemId, aProperty, + aIsAnnotationProperty, aNewValue) { + if (aProperty == "uri") { + let index = this._itemIds.indexOf(aItemId); + // If the changed bookmark was tracked, check if it is now pointing to + // a different uri and unregister it. + if (index != -1 && aNewValue != this._uri.spec) { + this._itemIds.splice(index, 1); + // Only need to update the UI if the page is no longer starred + if (this._itemIds.length == 0) { + this._updateStar(); + } + } + // If another bookmark is now pointing to the tracked uri, register it. + else if (index == -1 && aNewValue == this._uri.spec) { + this._itemIds.push(aItemId); + // Only need to update the UI if it wasn't marked as starred before: + if (this._itemIds.length == 1) { + this._updateStar(); + } + } + } + }, + + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onBeforeItemRemoved: function () {}, + onItemVisited: function () {}, + onItemMoved: function () {}, + + // CustomizableUI events: + _starButtonLabel: null, + get _starButtonOverflowedLabel() { + delete this._starButtonOverflowedLabel; + return this._starButtonOverflowedLabel = + gNavigatorBundle.getString("starButtonOverflowed.label"); + }, + get _starButtonOverflowedStarredLabel() { + delete this._starButtonOverflowedStarredLabel; + return this._starButtonOverflowedStarredLabel = + gNavigatorBundle.getString("starButtonOverflowedStarred.label"); + }, + onWidgetOverflow: function(aNode, aContainer) { + let win = aNode.ownerGlobal; + if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window) + return; + + let currentLabel = aNode.getAttribute("label"); + if (!this._starButtonLabel) + this._starButtonLabel = currentLabel; + + if (currentLabel == this._starButtonLabel) { + let desiredLabel = this._itemIds.length > 0 ? this._starButtonOverflowedStarredLabel + : this._starButtonOverflowedLabel; + aNode.setAttribute("label", desiredLabel); + } + }, + + onWidgetUnderflow: function(aNode, aContainer) { + let win = aNode.ownerGlobal; + if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window) + return; + + // The view gets broken by being removed and reinserted. Uninit + // here so popupshowing will generate a new one: + this._uninitView(); + + if (aNode.getAttribute("label") != this._starButtonLabel) + aNode.setAttribute("label", this._starButtonLabel); + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver + ]) +}; + +var AutoShowBookmarksToolbar = { + init() { + Services.obs.addObserver(this, "autoshow-bookmarks-toolbar", false); + }, + + uninit() { + Services.obs.removeObserver(this, "autoshow-bookmarks-toolbar"); + }, + + observe(subject, topic, data) { + let toolbar = document.getElementById("PersonalToolbar"); + if (!toolbar.collapsed) + return; + + let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks"); + let area = placement && placement.area; + if (area != CustomizableUI.AREA_BOOKMARKS) + return; + + setToolbarVisibility(toolbar, true); + } +}; diff --git a/application/basilisk/base/content/browser-plugins.js b/application/basilisk/base/content/browser-plugins.js new file mode 100644 index 000000000..c1bc65860 --- /dev/null +++ b/application/basilisk/base/content/browser-plugins.js @@ -0,0 +1,516 @@ +/* -*- 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 gPluginHandler = { + PREF_SESSION_PERSIST_MINUTES: "plugin.sessionPermissionNow.intervalInMinutes", + PREF_PERSISTENT_DAYS: "plugin.persistentPermissionAlways.intervalInDays", + MESSAGES: [ + "PluginContent:ShowClickToPlayNotification", + "PluginContent:RemoveNotification", + "PluginContent:UpdateHiddenPluginUI", + "PluginContent:HideNotificationBar", + "PluginContent:InstallSinglePlugin", + "PluginContent:ShowPluginCrashedNotification", + "PluginContent:SubmitReport", + "PluginContent:LinkClickCallback", + ], + + init: function () { + const mm = window.messageManager; + for (let msg of this.MESSAGES) { + mm.addMessageListener(msg, this); + } + window.addEventListener("unload", this); + }, + + uninit: function () { + const mm = window.messageManager; + for (let msg of this.MESSAGES) { + mm.removeMessageListener(msg, this); + } + window.removeEventListener("unload", this); + }, + + handleEvent: function (event) { + if (event.type == "unload") { + this.uninit(); + } + }, + + receiveMessage: function (msg) { + switch (msg.name) { + case "PluginContent:ShowClickToPlayNotification": + this.showClickToPlayNotification(msg.target, msg.data.plugins, msg.data.showNow, + msg.principal, msg.data.location); + break; + case "PluginContent:RemoveNotification": + this.removeNotification(msg.target, msg.data.name); + break; + case "PluginContent:UpdateHiddenPluginUI": + this.updateHiddenPluginUI(msg.target, msg.data.haveInsecure, msg.data.actions, + msg.principal, msg.data.location); + break; + case "PluginContent:HideNotificationBar": + this.hideNotificationBar(msg.target, msg.data.name); + break; + case "PluginContent:InstallSinglePlugin": + this.installSinglePlugin(msg.data.pluginInfo); + break; + case "PluginContent:ShowPluginCrashedNotification": + this.showPluginCrashedNotification(msg.target, msg.data.messageString, + msg.data.pluginID); + break; + case "PluginContent:SubmitReport": + // Nothing to do here + break; + case "PluginContent:LinkClickCallback": + switch (msg.data.name) { + case "managePlugins": + case "openHelpPage": + case "openPluginUpdatePage": + this[msg.data.name].call(this, msg.data.pluginTag); + break; + } + break; + default: + Cu.reportError("gPluginHandler did not expect to handle message " + msg.name); + break; + } + }, + + // Callback for user clicking on a disabled plugin + managePlugins: function () { + BrowserOpenAddonsMgr("addons://list/plugin"); + }, + + // Callback for user clicking on the link in a click-to-play plugin + // (where the plugin has an update) + openPluginUpdatePage: function(pluginTag) { + let url = Services.blocklist.getPluginInfoURL(pluginTag); + if (!url) { + url = Services.blocklist.getPluginBlocklistURL(pluginTag); + } + openUILinkIn(url, "tab"); + }, + + submitReport: function submitReport(runID, keyVals, submitURLOptIn) { + /*** STUB ***/ + return; + }, + + // Callback for user clicking a "reload page" link + reloadPage: function (browser) { + browser.reload(); + }, + + // Callback for user clicking the help icon + openHelpPage: function () { + openHelpLink("plugin-crashed", false); + }, + + _clickToPlayNotificationEventCallback: function PH_ctpEventCallback(event) { + if (event == "showing") { + Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_SHOWN") + .add(!this.options.primaryPlugin); + // Histograms always start at 0, even though our data starts at 1 + let histogramCount = this.options.pluginData.size - 1; + if (histogramCount > 4) { + histogramCount = 4; + } + Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_PLUGIN_COUNT") + .add(histogramCount); + } + else if (event == "dismissed") { + // Once the popup is dismissed, clicking the icon should show the full + // list again + this.options.primaryPlugin = null; + } + }, + + /** + * Called from the plugin doorhanger to set the new permissions for a plugin + * and activate plugins if necessary. + * aNewState should be either "allownow" "allowalways" or "block" + */ + _updatePluginPermission: function (aNotification, aPluginInfo, aNewState) { + let permission; + let expireType; + let expireTime; + let histogram = + Services.telemetry.getHistogramById("PLUGINS_NOTIFICATION_USER_ACTION"); + + // Update the permission manager. + // Also update the current state of pluginInfo.fallbackType so that + // subsequent opening of the notification shows the current state. + switch (aNewState) { + case "allownow": + permission = Ci.nsIPermissionManager.ALLOW_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_SESSION; + expireTime = Date.now() + Services.prefs.getIntPref(this.PREF_SESSION_PERSIST_MINUTES) * 60 * 1000; + histogram.add(0); + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE; + break; + + case "allowalways": + permission = Ci.nsIPermissionManager.ALLOW_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_TIME; + expireTime = Date.now() + + Services.prefs.getIntPref(this.PREF_PERSISTENT_DAYS) * 24 * 60 * 60 * 1000; + histogram.add(1); + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE; + break; + + case "block": + permission = Ci.nsIPermissionManager.PROMPT_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_NEVER; + expireTime = 0; + histogram.add(2); + switch (aPluginInfo.blocklistState) { + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE; + break; + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE; + break; + default: + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY; + } + break; + + // In case a plugin has already been allowed in another tab, the "continue allowing" button + // shouldn't change any permissions but should run the plugin-enablement code below. + case "continue": + aPluginInfo.fallbackType = Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE; + break; + default: + Cu.reportError(Error("Unexpected plugin state: " + aNewState)); + return; + } + + let browser = aNotification.browser; + if (aNewState != "continue") { + let principal = aNotification.options.principal; + Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString, + permission, expireType, expireTime); + aPluginInfo.pluginPermissionType = expireType; + } + + browser.messageManager.sendAsyncMessage("BrowserPlugins:ActivatePlugins", { + pluginInfo: aPluginInfo, + newState: aNewState, + }); + }, + + showClickToPlayNotification: function (browser, plugins, showNow, + principal, location) { + // It is possible that we've received a message from the frame script to show + // a click to play notification for a principal that no longer matches the one + // that the browser's content now has assigned (ie, the browser has browsed away + // after the message was sent, but before the message was received). In that case, + // we should just ignore the message. + if (!principal.equals(browser.contentPrincipal)) { + return; + } + + // Data URIs, when linked to from some page, inherit the principal of that + // page. That means that we also need to compare the actual locations to + // ensure we aren't getting a message from a Data URI that we're no longer + // looking at. + let receivedURI = BrowserUtils.makeURI(location); + if (!browser.documentURI.equalsExceptRef(receivedURI)) { + return; + } + + let notification = PopupNotifications.getNotification("click-to-play-plugins", browser); + + // If this is a new notification, create a pluginData map, otherwise append + let pluginData; + if (notification) { + pluginData = notification.options.pluginData; + } else { + pluginData = new Map(); + } + + for (var pluginInfo of plugins) { + if (pluginData.has(pluginInfo.permissionString)) { + continue; + } + + // If a block contains an infoURL, we should always prefer that to the default + // URL that we construct in-product, even for other blocklist types. + let url = Services.blocklist.getPluginInfoURL(pluginInfo.pluginTag); + + if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) { + if (!url) { + url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag); + } + } + else { + url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "clicktoplay"; + } + pluginInfo.detailsLink = url; + + pluginData.set(pluginInfo.permissionString, pluginInfo); + } + + let primaryPluginPermission = null; + if (showNow) { + primaryPluginPermission = plugins[0].permissionString; + } + + if (notification) { + // Don't modify the notification UI while it's on the screen, that would be + // jumpy and might allow clickjacking. + if (showNow) { + notification.options.primaryPlugin = primaryPluginPermission; + notification.reshow(); + browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown"); + } + return; + } + + let options = { + dismissed: !showNow, + eventCallback: this._clickToPlayNotificationEventCallback, + primaryPlugin: primaryPluginPermission, + pluginData: pluginData, + principal: principal, + }; + PopupNotifications.show(browser, "click-to-play-plugins", + "", "plugins-notification-icon", + null, null, options); + browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown"); + }, + + removeNotification: function (browser, name) { + let notification = PopupNotifications.getNotification(name, browser); + if (notification) + PopupNotifications.remove(notification); + }, + + hideNotificationBar: function (browser, name) { + let notificationBox = gBrowser.getNotificationBox(browser); + let notification = notificationBox.getNotificationWithValue(name); + if (notification) + notificationBox.removeNotification(notification, true); + }, + + updateHiddenPluginUI: function (browser, haveInsecure, actions, + principal, location) { + let origin = principal.originNoSuffix; + + // It is possible that we've received a message from the frame script to show + // the hidden plugin notification for a principal that no longer matches the one + // that the browser's content now has assigned (ie, the browser has browsed away + // after the message was sent, but before the message was received). In that case, + // we should just ignore the message. + if (!principal.equals(browser.contentPrincipal)) { + return; + } + + // Data URIs, when linked to from some page, inherit the principal of that + // page. That means that we also need to compare the actual locations to + // ensure we aren't getting a message from a Data URI that we're no longer + // looking at. + let receivedURI = BrowserUtils.makeURI(location); + if (!browser.documentURI.equalsExceptRef(receivedURI)) { + return; + } + + // Set up the icon + document.getElementById("plugins-notification-icon").classList. + toggle("plugin-blocked", haveInsecure); + + // Now configure the notification bar + let notificationBox = gBrowser.getNotificationBox(browser); + + function hideNotification() { + let n = notificationBox.getNotificationWithValue("plugin-hidden"); + if (n) { + notificationBox.removeNotification(n, true); + } + } + + // There are three different cases when showing an infobar: + // 1. A single type of plugin is hidden on the page. Show the UI for that + // plugin. + // 2a. Multiple types of plugins are hidden on the page. Show the multi-UI + // with the vulnerable styling. + // 2b. Multiple types of plugins are hidden on the page, but none are + // vulnerable. Show the nonvulnerable multi-UI. + function showNotification() { + let n = notificationBox.getNotificationWithValue("plugin-hidden"); + if (n) { + // If something is already shown, just keep it + return; + } + + Services.telemetry.getHistogramById("PLUGINS_INFOBAR_SHOWN"). + add(true); + + let message; + // Icons set directly cannot be manipulated using moz-image-region, so + // we use CSS classes instead. + let brand = document.getElementById("bundle_brand").getString("brandShortName"); + + if (actions.length == 1) { + let pluginInfo = actions[0]; + let pluginName = pluginInfo.pluginName; + + switch (pluginInfo.fallbackType) { + case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY: + message = gNavigatorBundle.getFormattedString( + "pluginActivateNew.message", + [pluginName, origin]); + break; + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE: + message = gNavigatorBundle.getFormattedString( + "pluginActivateOutdated.message", + [pluginName, origin, brand]); + break; + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE: + message = gNavigatorBundle.getFormattedString( + "pluginActivateVulnerable.message", + [pluginName, origin, brand]); + } + } else { + // Multi-plugin + message = gNavigatorBundle.getFormattedString( + "pluginActivateMultiple.message", [origin]); + } + + let buttons = [ + { + label: gNavigatorBundle.getString("pluginContinueBlocking.label"), + accessKey: gNavigatorBundle.getString("pluginContinueBlocking.accesskey"), + callback: function() { + Services.telemetry.getHistogramById("PLUGINS_INFOBAR_BLOCK"). + add(true); + + Services.perms.addFromPrincipal(principal, + "plugin-hidden-notification", + Services.perms.DENY_ACTION); + } + }, + { + label: gNavigatorBundle.getString("pluginActivateTrigger.label"), + accessKey: gNavigatorBundle.getString("pluginActivateTrigger.accesskey"), + callback: function() { + Services.telemetry.getHistogramById("PLUGINS_INFOBAR_ALLOW"). + add(true); + + let curNotification = + PopupNotifications.getNotification("click-to-play-plugins", + browser); + if (curNotification) { + curNotification.reshow(); + } + } + } + ]; + n = notificationBox. + appendNotification(message, "plugin-hidden", null, + notificationBox.PRIORITY_INFO_HIGH, buttons); + if (haveInsecure) { + n.classList.add('pluginVulnerable'); + } + } + + if (actions.length == 0) { + hideNotification(); + } else { + let notificationPermission = Services.perms.testPermissionFromPrincipal( + principal, "plugin-hidden-notification"); + if (notificationPermission == Ci.nsIPermissionManager.DENY_ACTION) { + hideNotification(); + } else { + showNotification(); + } + } + }, + + contextMenuCommand: function (browser, plugin, command) { + browser.messageManager.sendAsyncMessage("BrowserPlugins:ContextMenuCommand", + { command: command }, { plugin: plugin }); + }, + + // Crashed-plugin observer. Notified once per plugin crash, before events + // are dispatched to individual plugin instances. + NPAPIPluginCrashed : function(subject, topic, data) { + let propertyBag = subject; + if (!(propertyBag instanceof Ci.nsIPropertyBag2) || + !(propertyBag instanceof Ci.nsIWritablePropertyBag2) || + !propertyBag.hasKey("runID") || + !propertyBag.hasKey("pluginName")) { + Cu.reportError("A NPAPI plugin crashed, but the properties of this plugin " + + "cannot be read."); + return; + } + + let runID = propertyBag.getPropertyAsUint32("runID"); + let uglyPluginName = propertyBag.getPropertyAsAString("pluginName"); + let pluginName = BrowserUtils.makeNicePluginName(uglyPluginName); + let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID"); + + // If we don't have a minidumpID, we can't (or didn't) submit anything. + // This can happen if the plugin is killed from the task manager. + let state = "noSubmit"; + + let mm = window.getGroupMessageManager("browsers"); + mm.broadcastAsyncMessage("BrowserPlugins:NPAPIPluginProcessCrashed", + { pluginName, runID, state }); + }, + + /** + * Shows a plugin-crashed notification bar for a browser that has had an + * invisiable NPAPI plugin crash, or a GMP plugin crash. + * + * @param browser + * The browser to show the notification for. + * @param messageString + * The string to put in the notification bar + * @param pluginID + * The unique-per-process identifier for the NPAPI plugin or GMP. + * For a GMP, this is the pluginID. For NPAPI plugins (where "pluginID" + * means something different), this is the runID. + */ + showPluginCrashedNotification: function (browser, messageString, pluginID) { + // If there's already an existing notification bar, don't do anything. + let notificationBox = gBrowser.getNotificationBox(browser); + let notification = notificationBox.getNotificationWithValue("plugin-crashed"); + if (notification) { + return; + } + + // Configure the notification bar + let priority = notificationBox.PRIORITY_WARNING_MEDIUM; + let iconURL = "chrome://mozapps/skin/plugins/notifyPluginCrashed.png"; + let reloadLabel = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.label"); + let reloadKey = gNavigatorBundle.getString("crashedpluginsMessage.reloadButton.accesskey"); + + let buttons = [{ + label: reloadLabel, + accessKey: reloadKey, + popup: null, + callback: function() { browser.reload(); }, + }]; + + notification = notificationBox.appendNotification(messageString, "plugin-crashed", + iconURL, priority, buttons); + + // Add the "learn more" link. + let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let link = notification.ownerDocument.createElementNS(XULNS, "label"); + link.className = "text-link"; + link.setAttribute("value", gNavigatorBundle.getString("crashedpluginsMessage.learnMore")); + let crashurl = formatURL("app.support.baseURL", true); + crashurl += "plugin-crashed-notificationbar"; + link.href = crashurl; + let description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText"); + description.appendChild(link); + }, +}; + +gPluginHandler.init(); diff --git a/application/basilisk/base/content/browser-refreshblocker.js b/application/basilisk/base/content/browser-refreshblocker.js new file mode 100644 index 000000000..025d45421 --- /dev/null +++ b/application/basilisk/base/content/browser-refreshblocker.js @@ -0,0 +1,84 @@ +/* -*- 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/. */ + +/** + * If the user has opted into blocking refresh and redirect attempts by + * default, this handles showing the notification to the user which + * gives them the option to let the refresh or redirect proceed. + */ +var RefreshBlocker = { + init() { + gBrowser.addEventListener("RefreshBlocked", this); + }, + + uninit() { + gBrowser.removeEventListener("RefreshBlocked", this); + }, + + handleEvent: function(event) { + if (event.type == "RefreshBlocked") { + this.block(event.originalTarget, event.detail); + } + }, + + /** + * Shows the blocked refresh / redirect notification for some browser. + * + * @param browser (<xul:browser>) + * The browser that had the refresh blocked. This will be the browser + * for which we'll show the notification on. + * @param data (object) + * An object with the following properties: + * + * URI (string) + * The URI that a page is attempting to refresh or redirect to. + * + * delay (int) + * The delay (in milliseconds) before the page was going to reload + * or redirect. + * + * sameURI (bool) + * true if we're refreshing the page. false if we're redirecting. + * + * outerWindowID (int) + * The outerWindowID of the frame that requested the refresh or + * redirect. + */ + block(browser, data) { + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let message = + gNavigatorBundle.getFormattedString(data.sameURI ? "refreshBlocked.refreshLabel" + : "refreshBlocked.redirectLabel", + [brandShortName]); + + let notificationBox = gBrowser.getNotificationBox(browser); + let notification = notificationBox.getNotificationWithValue("refresh-blocked"); + + if (notification) { + notification.label = message; + } else { + let refreshButtonText = + gNavigatorBundle.getString("refreshBlocked.goButton"); + let refreshButtonAccesskey = + gNavigatorBundle.getString("refreshBlocked.goButton.accesskey"); + + let buttons = [{ + label: refreshButtonText, + accessKey: refreshButtonAccesskey, + callback: function (notification, button) { + if (browser.messageManager) { + browser.messageManager.sendAsyncMessage("RefreshBlocker:Refresh", data); + } + } + }]; + + notificationBox.appendNotification(message, "refresh-blocked", + "chrome://browser/skin/Info.png", + notificationBox.PRIORITY_INFO_MEDIUM, + buttons); + } + } +}; diff --git a/application/basilisk/base/content/browser-safebrowsing.js b/application/basilisk/base/content/browser-safebrowsing.js new file mode 100644 index 000000000..430d84f13 --- /dev/null +++ b/application/basilisk/base/content/browser-safebrowsing.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gSafeBrowsing = { + + setReportPhishingMenu: function() { + // In order to detect whether or not we're at the phishing warning + // page, we have to check the documentURI instead of the currentURI. + // This is because when the DocShell loads an error page, the + // currentURI stays at the original target, while the documentURI + // will point to the internal error page we loaded instead. + var docURI = gBrowser.selectedBrowser.documentURI; + var isPhishingPage = + docURI && docURI.spec.startsWith("about:blocked?e=deceptiveBlocked"); + + // Show/hide the appropriate menu item. + document.getElementById("menu_HelpPopup_reportPhishingtoolmenu") + .hidden = isPhishingPage; + document.getElementById("menu_HelpPopup_reportPhishingErrortoolmenu") + .hidden = !isPhishingPage; + + var broadcasterId = isPhishingPage + ? "reportPhishingErrorBroadcaster" + : "reportPhishingBroadcaster"; + + var broadcaster = document.getElementById(broadcasterId); + if (!broadcaster) + return; + + // Now look at the currentURI to learn which page we were trying + // to browse to. + let uri = gBrowser.currentURI; + if (uri && (uri.schemeIs("http") || uri.schemeIs("https"))) + broadcaster.removeAttribute("disabled"); + else + broadcaster.setAttribute("disabled", true); + }, + + /** + * Used to report a phishing page or a false positive + * @param name String One of "Phish", "Error", "Malware" or "MalwareError" + * @return String the report phishing URL. + */ + getReportURL: function(name) { + return SafeBrowsing.getReportURL(name, gBrowser.currentURI); + } +} diff --git a/application/basilisk/base/content/browser-sets.inc b/application/basilisk/base/content/browser-sets.inc new file mode 100644 index 000000000..6ea057d93 --- /dev/null +++ b/application/basilisk/base/content/browser-sets.inc @@ -0,0 +1,379 @@ +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifdef XP_UNIX +#ifndef XP_MACOSX +#define XP_GNOME 1 +#endif +#endif + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/> + <stringbundle id="bundle_shell" src="chrome://browser/locale/shellservice.properties"/> + <stringbundle id="bundle_preferences" src="chrome://browser/locale/preferences/preferences.properties"/> + </stringbundleset> + + <commandset id="mainCommandSet"> + <command id="cmd_newNavigator" oncommand="OpenBrowserWindow()" reserved="true"/> + <command id="cmd_handleBackspace" oncommand="BrowserHandleBackspace();" /> + <command id="cmd_handleShiftBackspace" oncommand="BrowserHandleShiftBackspace();" /> + + <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab(event);" reserved="true"/> + <command id="cmd_newNavigatorTabNoEvent" oncommand="BrowserOpenTab();" reserved="true"/> + <command id="Browser:OpenFile" oncommand="BrowserOpenFileWindow();"/> + <command id="Browser:SavePage" oncommand="saveBrowser(gBrowser.selectedBrowser);"/> + + <command id="Browser:SendLink" + oncommand="MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);"/> + + <command id="cmd_pageSetup" oncommand="PrintUtils.showPageSetup();"/> + <command id="cmd_print" oncommand="PrintUtils.printWindow(window.gBrowser.selectedBrowser.outerWindowID, window.gBrowser.selectedBrowser);"/> + <command id="cmd_printPreview" oncommand="PrintUtils.printPreview(PrintPreviewListener);"/> + <command id="cmd_close" oncommand="BrowserCloseTabOrWindow()" reserved="true"/> + <command id="cmd_closeWindow" oncommand="BrowserTryToCloseWindow()" reserved="true"/> + <command id="cmd_toggleMute" oncommand="gBrowser.selectedTab.toggleMuteAudio()"/> + <command id="cmd_CustomizeToolbars" oncommand="BrowserCustomizeToolbar()"/> + <command id="cmd_quitApplication" oncommand="goQuitApplication()" reserved="true"/> + + + <commandset id="editMenuCommands"/> + + <command id="View:PageSource" oncommand="BrowserViewSource(window.gBrowser.selectedBrowser);" observes="canViewSource"/> + <command id="View:PageInfo" oncommand="BrowserPageInfo();"/> + <command id="View:FullScreen" oncommand="BrowserFullScreen();"/> + <command id="View:ReaderView" oncommand="ReaderParent.toggleReaderMode(event);"/> + <command id="cmd_find" + oncommand="gFindBar.onFindCommand();" + observes="isImage"/> + <command id="cmd_findAgain" + oncommand="gFindBar.onFindAgainCommand(false);" + observes="isImage"/> + <command id="cmd_findPrevious" + oncommand="gFindBar.onFindAgainCommand(true);" + observes="isImage"/> +#ifdef XP_MACOSX + <command id="cmd_findSelection" oncommand="gFindBar.onFindSelectionCommand();"/> +#endif + <!-- work-around bug 392512 --> + <command id="Browser:AddBookmarkAs" + oncommand="PlacesCommandHook.bookmarkCurrentPage(true, PlacesUtils.bookmarksMenuFolderId);"/> + <!-- The command disabled state must be manually updated through + PlacesCommandHook.updateBookmarkAllTabsCommand() --> + <command id="Browser:BookmarkAllTabs" + oncommand="PlacesCommandHook.bookmarkCurrentPages();"/> + <command id="Browser:Home" oncommand="BrowserHome();"/> + <command id="Browser:Back" oncommand="BrowserBack();" disabled="true"/> + <command id="Browser:BackOrBackDuplicate" oncommand="BrowserBack(event);" disabled="true"> + <observes element="Browser:Back" attribute="disabled"/> + </command> + <command id="Browser:Forward" oncommand="BrowserForward();" disabled="true"/> + <command id="Browser:ForwardOrForwardDuplicate" oncommand="BrowserForward(event);" disabled="true"> + <observes element="Browser:Forward" attribute="disabled"/> + </command> + <command id="Browser:Stop" oncommand="BrowserStop();" disabled="true"/> + <command id="Browser:Reload" oncommand="if (event.shiftKey) BrowserReloadSkipCache(); else BrowserReload()" disabled="true"/> + <command id="Browser:ReloadOrDuplicate" oncommand="BrowserReloadOrDuplicate(event)" disabled="true"> + <observes element="Browser:Reload" attribute="disabled"/> + </command> + <command id="Browser:ReloadSkipCache" oncommand="BrowserReloadSkipCache()" disabled="true"> + <observes element="Browser:Reload" attribute="disabled"/> + </command> + <command id="Browser:NextTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(1, true);" reserved="true"/> + <command id="Browser:PrevTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(-1, true);" reserved="true"/> + <command id="Browser:ShowAllTabs" oncommand="allTabs.open();"/> + <command id="cmd_fullZoomReduce" oncommand="FullZoom.reduce()"/> + <command id="cmd_fullZoomEnlarge" oncommand="FullZoom.enlarge()"/> + <command id="cmd_fullZoomReset" oncommand="FullZoom.reset()"/> + <command id="cmd_fullZoomToggle" oncommand="ZoomManager.toggleZoom();"/> + <command id="cmd_gestureRotateLeft" oncommand="gGestureSupport.rotate(event.sourceEvent)"/> + <command id="cmd_gestureRotateRight" oncommand="gGestureSupport.rotate(event.sourceEvent)"/> + <command id="cmd_gestureRotateEnd" oncommand="gGestureSupport.rotateEnd()"/> + <command id="Browser:OpenLocation" oncommand="openLocation();"/> + <command id="Browser:RestoreLastSession" oncommand="restoreLastSession();" disabled="true"/> + <command id="Browser:NewUserContextTab" oncommand="openNewUserContextTab(event.sourceEvent);" reserved="true"/> + <command id="Browser:OpenAboutContainers" oncommand="openPreferences('paneContainers');"/> + + <command id="Tools:Search" oncommand="BrowserSearch.webSearch();"/> + <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/> + <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/> + <command id="Tools:ErrorConsole" oncommand="toJavaScriptConsole()"/> + <command id="Tools:Sanitize" + oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/> + <command id="Tools:PrivateBrowsing" + oncommand="OpenBrowserWindow({private: true});" reserved="true"/> + <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/> + <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/> + </commandset> + + <commandset id="placesCommands"> + <command id="Browser:ShowAllBookmarks" + oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/> + <command id="Browser:ShowAllHistory" + oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/> + </commandset> + + <broadcasterset id="mainBroadcasterSet"> + <broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;" + type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul" + oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/> + + <!-- for both places and non-places, the sidebar lives at + chrome://browser/content/history/history-panel.xul so there are no + problems when switching between versions --> + <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;" + type="checkbox" group="sidebar" + sidebarurl="chrome://browser/content/history/history-panel.xul" + oncommand="SidebarUI.toggle('viewHistorySidebar');"/> + + <broadcaster id="viewWebPanelsSidebar" autoCheck="false" + type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul" + oncommand="SidebarUI.toggle('viewWebPanelsSidebar');"/> + + <broadcaster id="bookmarkThisPageBroadcaster" + label="&bookmarkThisPageCmd.label;" + bookmarklabel="&bookmarkThisPageCmd.label;" + editlabel="&editThisBookmarkCmd.label;"/> + + <!-- popup blocking menu items --> + <broadcaster id="blockedPopupAllowSite" + accesskey="&allowPopups.accesskey;" + oncommand="gPopupBlockerObserver.toggleAllowPopupsForSite(event);"/> + <broadcaster id="blockedPopupEditSettings" +#ifdef XP_WIN + label="&editPopupSettings.label;" +#else + label="&editPopupSettingsUnix.label;" +#endif + accesskey="&editPopupSettings.accesskey;" + oncommand="gPopupBlockerObserver.editPopupSettings();"/> + <broadcaster id="blockedPopupDontShowMessage" + accesskey="&dontShowMessage.accesskey;" + type="checkbox" + oncommand="gPopupBlockerObserver.dontShowMessage();"/> + <broadcaster id="blockedPopupsSeparator"/> + <broadcaster id="isImage"/> + <broadcaster id="canViewSource"/> + <broadcaster id="isFrameImage"/> + <broadcaster id="singleFeedMenuitemState" disabled="true"/> + <broadcaster id="multipleFeedsMenuState" hidden="true"/> + + <!-- Sync broadcasters --> + <!-- A broadcaster of a number of attributes suitable for "sync now" UI - + A 'syncstatus' attribute is set while actively syncing, and the label + attribute which changes from "sync now" to "syncing" etc. --> + <broadcaster id="sync-status"/> + <!-- broadcasters of the "hidden" attribute to reflect setup state for + menus --> + <broadcaster id="sync-setup-state"/> + <broadcaster id="sync-syncnow-state" hidden="true"/> + <broadcaster id="sync-reauth-state" hidden="true"/> + <broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;" + type="checkbox" group="sidebar" + sidebarurl="chrome://browser/content/syncedtabs/sidebar.xhtml" + oncommand="SidebarUI.toggle('viewTabsSidebar');"/> + <broadcaster id="workOfflineMenuitemState"/> + + <broadcaster id="devtoolsMenuBroadcaster_ErrorConsole" + label="&errorConsoleCmd.label;" + command="Tools:ErrorConsole"/> + + <broadcaster id="devtoolsMenuBroadcaster_PageSource" + label="&pageSourceCmd.label;" + key="key_viewSource" + command="View:PageSource"> + <observes element="canViewSource" attribute="disabled"/> + + </broadcaster> + </broadcasterset> + + <keyset id="mainKeyset"> + <key id="key_newNavigator" + key="&newNavigatorCmd.key;" + command="cmd_newNavigator" + modifiers="accel"/> + <key id="key_newNavigatorTab" key="&tabCmd.commandkey;" modifiers="accel" command="cmd_newNavigatorTabNoEvent"/> + <key id="focusURLBar" key="&openCmd.commandkey;" command="Browser:OpenLocation" + modifiers="accel"/> +#ifndef XP_MACOSX + <key id="focusURLBar2" key="&urlbar.accesskey;" command="Browser:OpenLocation" + modifiers="alt"/> +#endif + +# +# Search Command Key Logic works like this: +# +# Unix: Ctrl+K (cross platform binding) +# Ctrl+J (in case of emacs Ctrl-K conflict) +# Mac: Cmd+K (cross platform binding) +# Cmd+Opt+F (platform convention) +# Win: Ctrl+K (cross platform binding) +# Ctrl+E (IE compat) +# +# We support Ctrl+K on all platforms now and advertise it in the menu since it is +# our standard - it is a "safe" choice since it is near no harmful keys like "W" as +# "E" is. People mourning the loss of Ctrl+K for emacs compat can switch their GTK +# system setting to use emacs emulation, and we should respect it. Focus-Search-Box +# is a fundamental keybinding and we are maintaining a XP binding so that it is easy +# for people to switch to Linux. +# + <key id="key_search" key="&searchFocus.commandkey;" command="Tools:Search" modifiers="accel"/> +#ifdef XP_MACOSX + <key id="key_search2" key="&findOnCmd.commandkey;" command="Tools:Search" modifiers="accel,alt"/> +#endif +#ifdef XP_WIN + <key id="key_search2" key="&searchFocus.commandkey2;" command="Tools:Search" modifiers="accel"/> +#endif +#ifdef XP_GNOME + <key id="key_search2" key="&searchFocusUnix.commandkey;" command="Tools:Search" modifiers="accel"/> + <key id="key_openDownloads" key="&downloadsUnix.commandkey;" command="Tools:Downloads" modifiers="accel,shift"/> +#else + <key id="key_openDownloads" key="&downloads.commandkey;" command="Tools:Downloads" modifiers="accel"/> +#endif + <key id="key_openAddons" key="&addons.commandkey;" command="Tools:Addons" modifiers="accel,shift"/> + <key id="openFileKb" key="&openFileCmd.commandkey;" command="Browser:OpenFile" modifiers="accel"/> + <key id="key_savePage" key="&savePageCmd.commandkey;" command="Browser:SavePage" modifiers="accel"/> + <key id="printKb" key="&printCmd.commandkey;" command="cmd_print" modifiers="accel"/> + <key id="key_close" key="&closeCmd.key;" command="cmd_close" modifiers="accel"/> + <key id="key_closeWindow" key="&closeCmd.key;" command="cmd_closeWindow" modifiers="accel,shift"/> + <key id="key_toggleMute" key="&toggleMuteCmd.key;" command="cmd_toggleMute" modifiers="control"/> + <key id="key_undo" + key="&undoCmd.key;" + modifiers="accel"/> +#ifdef XP_UNIX + <key id="key_redo" key="&undoCmd.key;" modifiers="accel,shift"/> +#else + <key id="key_redo" key="&redoCmd.key;" modifiers="accel"/> +#endif + <key id="key_cut" + key="&cutCmd.key;" + modifiers="accel"/> + <key id="key_copy" + key="©Cmd.key;" + modifiers="accel"/> + <key id="key_paste" + key="&pasteCmd.key;" + modifiers="accel"/> + <key id="key_delete" keycode="VK_DELETE" command="cmd_delete"/> + <key id="key_selectAll" key="&selectAllCmd.key;" modifiers="accel"/> + + <key keycode="VK_BACK" command="cmd_handleBackspace"/> + <key keycode="VK_BACK" command="cmd_handleShiftBackspace" modifiers="shift"/> +#ifndef XP_MACOSX + <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="alt"/> + <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="alt"/> +#else + <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="accel" /> + <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="accel" /> +#endif +#ifdef XP_UNIX + <key id="goBackKb2" key="&goBackCmd.commandKey;" command="Browser:Back" modifiers="accel"/> + <key id="goForwardKb2" key="&goForwardCmd.commandKey;" command="Browser:Forward" modifiers="accel"/> +#endif + <key id="goHome" keycode="VK_HOME" command="Browser:Home" modifiers="alt"/> + <key keycode="VK_F5" command="Browser:Reload"/> +#ifndef XP_MACOSX + <key id="showAllHistoryKb" key="&showAllHistoryCmd.commandkey;" command="Browser:ShowAllHistory" modifiers="accel,shift"/> + <key keycode="VK_F5" command="Browser:ReloadSkipCache" modifiers="accel"/> + <key id="key_fullScreen" keycode="VK_F11" command="View:FullScreen"/> +#else + <key id="key_fullScreen" key="&fullScreenCmd.macCommandKey;" command="View:FullScreen" modifiers="accel,control"/> + <key id="key_fullScreen_old" key="&fullScreenCmd.macCommandKey;" command="View:FullScreen" modifiers="accel,shift"/> + <key keycode="VK_F11" command="View:FullScreen"/> +#endif + <key id="key_toggleReaderMode" key="&toggleReaderMode.key;" command="View:ReaderView" modifiers="accel,alt" disabled="true"/> + <key key="&reloadCmd.commandkey;" command="Browser:Reload" modifiers="accel" id="key_reload"/> + <key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/> + <key id="key_viewSource" key="&pageSourceCmd.commandkey;" command="View:PageSource" modifiers="accel"/> +#ifndef XP_WIN + <key id="key_viewInfo" key="&pageInfoCmd.commandkey;" command="View:PageInfo" modifiers="accel"/> +#endif + <key id="key_find" key="&findOnCmd.commandkey;" command="cmd_find" modifiers="accel"/> + <key id="key_findAgain" key="&findAgainCmd.commandkey;" command="cmd_findAgain" modifiers="accel"/> + <key id="key_findPrevious" key="&findAgainCmd.commandkey;" command="cmd_findPrevious" modifiers="accel,shift"/> +#ifdef XP_MACOSX + <key id="key_findSelection" key="&findSelectionCmd.commandkey;" command="cmd_findSelection" modifiers="accel"/> +#endif + <key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/> + <key keycode="&findAgainCmd.commandkey2;" command="cmd_findPrevious" modifiers="shift"/> + + <key id="addBookmarkAsKb" key="&bookmarkThisPageCmd.commandkey;" command="Browser:AddBookmarkAs" modifiers="accel"/> +# Accel+Shift+A-F are reserved on GTK +#ifndef MOZ_WIDGET_GTK + <key id="bookmarkAllTabsKb" key="&bookmarkThisPageCmd.commandkey;" oncommand="PlacesCommandHook.bookmarkCurrentPages();" modifiers="accel,shift"/> + <key id="manBookmarkKb" key="&bookmarksCmd.commandkey;" command="Browser:ShowAllBookmarks" modifiers="accel,shift"/> +#else + <key id="manBookmarkKb" key="&bookmarksGtkCmd.commandkey;" command="Browser:ShowAllBookmarks" modifiers="accel,shift"/> +#endif + <key id="viewBookmarksSidebarKb" key="&bookmarksCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/> +#ifdef XP_WIN +# Cmd+I is conventially mapped to Info on MacOS X, thus it should not be +# overridden for other purposes there. + <key id="viewBookmarksSidebarWinKb" key="&bookmarksWinCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/> +#endif + + <key id="key_stop" keycode="VK_ESCAPE" command="Browser:Stop"/> + +#ifdef XP_MACOSX + <key id="key_stop_mac" modifiers="accel" key="&stopCmd.macCommandKey;" command="Browser:Stop"/> +#endif + + <key id="key_gotoHistory" + key="&historySidebarCmd.commandKey;" +#ifdef XP_MACOSX + modifiers="accel,shift" +#else + modifiers="accel" +#endif + command="viewHistorySidebar"/> + + <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/> + <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/> + <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/> + <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/> + <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/> + <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/> + <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/> + + <key id="key_showAllTabs" command="Browser:ShowAllTabs" keycode="VK_TAB" modifiers="control,shift"/> + + <key id="key_switchTextDirection" key="&bidiSwitchTextDirectionItem.commandkey;" command="cmd_switchTextDirection" modifiers="accel,shift" /> + + <key id="key_privatebrowsing" command="Tools:PrivateBrowsing" key="&privateBrowsingCmd.commandkey;" modifiers="accel,shift"/> + <key id="key_sanitize" command="Tools:Sanitize" keycode="VK_DELETE" modifiers="accel,shift"/> +#ifdef XP_MACOSX + <key id="key_sanitize_mac" command="Tools:Sanitize" keycode="VK_BACK" modifiers="accel,shift"/> + <key id="key_quitApplication" key="&quitApplicationCmdUnix.key;" modifiers="accel" reserved="true"/> +#elifdef XP_UNIX + <key id="key_quitApplication" key="&quitApplicationCmdUnix.key;" command="cmd_quitApplication" modifiers="accel"/> +#endif + +#ifdef FULL_BROWSER_WINDOW + <key id="key_undoCloseTab" command="History:UndoCloseTab" key="&tabCmd.commandkey;" modifiers="accel,shift"/> +#endif + <key id="key_undoCloseWindow" command="History:UndoCloseWindow" key="&newNavigatorCmd.key;" modifiers="accel,shift"/> + +#ifdef XP_GNOME +#define NUM_SELECT_TAB_MODIFIER alt +#else +#define NUM_SELECT_TAB_MODIFIER accel +#endif + +#expand <key id="key_selectTab1" oncommand="gBrowser.selectTabAtIndex(0, event);" key="1" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab2" oncommand="gBrowser.selectTabAtIndex(1, event);" key="2" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab3" oncommand="gBrowser.selectTabAtIndex(2, event);" key="3" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab4" oncommand="gBrowser.selectTabAtIndex(3, event);" key="4" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab5" oncommand="gBrowser.selectTabAtIndex(4, event);" key="5" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab6" oncommand="gBrowser.selectTabAtIndex(5, event);" key="6" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab7" oncommand="gBrowser.selectTabAtIndex(6, event);" key="7" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectTab8" oncommand="gBrowser.selectTabAtIndex(7, event);" key="8" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> +#expand <key id="key_selectLastTab" oncommand="gBrowser.selectTabAtIndex(-1, event);" key="9" modifiers="__NUM_SELECT_TAB_MODIFIER__"/> + + </keyset> + +# Used by baseMenuOverlay +#ifdef XP_MACOSX + <commandset id="baseMenuCommandSet" /> +#endif + <keyset id="baseMenuKeyset" /> diff --git a/application/basilisk/base/content/browser-sidebar.js b/application/basilisk/base/content/browser-sidebar.js new file mode 100644 index 000000000..5893e6015 --- /dev/null +++ b/application/basilisk/base/content/browser-sidebar.js @@ -0,0 +1,337 @@ +/* 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/. */ + +/** + * SidebarUI controls showing and hiding the browser sidebar. + * + * @note + * Some of these methods take a commandID argument - we expect to find a + * xul:broadcaster element with the specified ID. + * The following attributes on that element may be used and/or modified: + * - id (required) the string to match commandID. The convention + * is to use this naming scheme: 'view<sidebar-name>Sidebar'. + * - sidebarurl (required) specifies the URL to load in this sidebar. + * - sidebartitle or label (in that order) specify the title to + * display on the sidebar. + * - checked indicates whether the sidebar is currently displayed. + * Note that toggleSidebar updates this attribute when + * it changes the sidebar's visibility. + * - group this attribute must be set to "sidebar". + */ +var SidebarUI = { + browser: null, + + _box: null, + _title: null, + _splitter: null, + + init() { + this._box = document.getElementById("sidebar-box"); + this.browser = document.getElementById("sidebar"); + this._title = document.getElementById("sidebar-title"); + this._splitter = document.getElementById("sidebar-splitter"); + + if (!this.adoptFromWindow(window.opener)) { + let commandID = this._box.getAttribute("sidebarcommand"); + if (commandID) { + let command = document.getElementById(commandID); + if (command) { + this._delayedLoad = true; + this._box.hidden = false; + this._splitter.hidden = false; + command.setAttribute("checked", "true"); + } else { + // Remove the |sidebarcommand| attribute, because the element it + // refers to no longer exists, so we should assume this sidebar + // panel has been uninstalled. (249883) + this._box.removeAttribute("sidebarcommand"); + } + } + } + }, + + uninit() { + let enumerator = Services.wm.getEnumerator(null); + enumerator.getNext(); + if (!enumerator.hasMoreElements()) { + document.persist("sidebar-box", "sidebarcommand"); + document.persist("sidebar-box", "width"); + document.persist("sidebar-box", "src"); + document.persist("sidebar-title", "value"); + } + }, + + /** + * Try and adopt the status of the sidebar from another window. + * @param {Window} sourceWindow - Window to use as a source for sidebar status. + * @return true if we adopted the state, or false if the caller should + * initialize the state itself. + */ + adoptFromWindow(sourceWindow) { + // No source window, or it being closed, or not chrome, or in a different + // private-browsing context means we can't adopt. + if (!sourceWindow || sourceWindow.closed || + !sourceWindow.document.documentURIObject.schemeIs("chrome") || + PrivateBrowsingUtils.isWindowPrivate(window) != PrivateBrowsingUtils.isWindowPrivate(sourceWindow)) { + return false; + } + + // If the opener had a sidebar, open the same sidebar in our window. + // The opener can be the hidden window too, if we're coming from the state + // where no windows are open, and the hidden window has no sidebar box. + let sourceUI = sourceWindow.SidebarUI; + if (!sourceUI || !sourceUI._box) { + // no source UI or no _box means we also can't adopt the state. + return false; + } + if (sourceUI._box.hidden) { + // just hidden means we have adopted the hidden state. + return true; + } + + let commandID = sourceUI._box.getAttribute("sidebarcommand"); + let commandElem = document.getElementById(commandID); + + // dynamically generated sidebars will fail this check, but we still + // consider it adopted. + if (!commandElem) { + return true; + } + + this._title.setAttribute("value", + sourceUI._title.getAttribute("value")); + this._box.setAttribute("width", sourceUI._box.boxObject.width); + + this._box.setAttribute("sidebarcommand", commandID); + // Note: we're setting 'src' on this._box, which is a <vbox>, not on + // the <browser id="sidebar">. This lets us delay the actual load until + // delayedStartup(). + this._box.setAttribute("src", sourceUI.browser.getAttribute("src")); + this._delayedLoad = true; + + this._box.hidden = false; + this._splitter.hidden = false; + commandElem.setAttribute("checked", "true"); + return true; + }, + + /** + * If loading a sidebar was delayed on startup, start the load now. + */ + startDelayedLoad() { + if (!this._delayedLoad) { + return; + } + + this.browser.setAttribute("src", this._box.getAttribute("src")); + }, + + /** + * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar + * a chance to adjust focus as needed. An additional event is needed, because + * we don't want to focus the sidebar when it's opened on startup or in a new + * window, only when the user opens the sidebar. + */ + _fireFocusedEvent() { + let event = new CustomEvent("SidebarFocused", {bubbles: true}); + this.browser.contentWindow.dispatchEvent(event); + + // Run the original function for backwards compatibility. + fireSidebarFocusedEvent(); + }, + + /** + * True if the sidebar is currently open. + */ + get isOpen() { + return !this._box.hidden; + }, + + /** + * The ID of the current sidebar (ie, the ID of the broadcaster being used). + * This can be set even if the sidebar is hidden. + */ + get currentID() { + return this._box.getAttribute("sidebarcommand"); + }, + + get title() { + return this._title.value; + }, + + set title(value) { + this._title.value = value; + }, + + /** + * Toggle the visibility of the sidebar. If the sidebar is hidden or is open + * with a different commandID, then the sidebar will be opened using the + * specified commandID. Otherwise the sidebar will be hidden. + * + * @param {string} commandID ID of the xul:broadcaster element to use. + * @return {Promise} + */ + toggle(commandID = this.currentID) { + if (this.isOpen && commandID == this.currentID) { + this.hide(); + return Promise.resolve(); + } + return this.show(commandID); + }, + + /** + * Show the sidebar, using the parameters from the specified broadcaster. + * @see SidebarUI note. + * + * @param {string} commandID ID of the xul:broadcaster element to use. + */ + show(commandID) { + return new Promise((resolve, reject) => { + let sidebarBroadcaster = document.getElementById(commandID); + if (!sidebarBroadcaster || sidebarBroadcaster.localName != "broadcaster") { + reject(new Error("Invalid sidebar broadcaster specified: " + commandID)); + return; + } + + if (this.isOpen && commandID != this.currentID) { + BrowserUITelemetry.countSidebarEvent(this.currentID, "hide"); + } + + let broadcasters = document.getElementsByAttribute("group", "sidebar"); + for (let broadcaster of broadcasters) { + // skip elements that observe sidebar broadcasters and random + // other elements + if (broadcaster.localName != "broadcaster") { + continue; + } + + if (broadcaster != sidebarBroadcaster) { + broadcaster.removeAttribute("checked"); + } else { + sidebarBroadcaster.setAttribute("checked", "true"); + } + } + + this._box.hidden = false; + this._splitter.hidden = false; + + this._box.setAttribute("sidebarcommand", sidebarBroadcaster.id); + + let title = sidebarBroadcaster.getAttribute("sidebartitle"); + if (!title) { + title = sidebarBroadcaster.getAttribute("label"); + } + this._title.value = title; + + let url = sidebarBroadcaster.getAttribute("sidebarurl"); + this.browser.setAttribute("src", url); // kick off async load + + // We set this attribute here in addition to setting it on the <browser> + // element itself, because the code in SidebarUI.uninit() persists this + // attribute, not the "src" of the <browser id="sidebar">. The reason it + // does that is that we want to delay sidebar load a bit when a browser + // window opens. See delayedStartup() and SidebarUI.startDelayedLoad(). + this._box.setAttribute("src", url); + + if (this.browser.contentDocument.location.href != url) { + let onLoad = event => { + this.browser.removeEventListener("load", onLoad, true); + + // We're handling the 'load' event before it bubbles up to the usual + // (non-capturing) event handlers. Let it bubble up before firing the + // SidebarFocused event. + setTimeout(() => this._fireFocusedEvent(), 0); + + // Run the original function for backwards compatibility. + sidebarOnLoad(event); + + resolve(); + }; + + this.browser.addEventListener("load", onLoad, true); + } else { + // Older code handled this case, so we do it too. + this._fireFocusedEvent(); + resolve(); + } + + let selBrowser = gBrowser.selectedBrowser; + selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange", + {commandID: commandID, isOpen: true} + ); + BrowserUITelemetry.countSidebarEvent(commandID, "show"); + }); + }, + + /** + * Hide the sidebar. + */ + hide() { + if (!this.isOpen) { + return; + } + + let commandID = this._box.getAttribute("sidebarcommand"); + let sidebarBroadcaster = document.getElementById(commandID); + + if (sidebarBroadcaster.getAttribute("checked") != "true") { + return; + } + + // Replace the document currently displayed in the sidebar with about:blank + // so that we can free memory by unloading the page. We need to explicitly + // create a new content viewer because the old one doesn't get destroyed + // until about:blank has loaded (which does not happen as long as the + // element is hidden). + this.browser.setAttribute("src", "about:blank"); + this.browser.docShell.createAboutBlankContentViewer(null); + + sidebarBroadcaster.removeAttribute("checked"); + this._box.setAttribute("sidebarcommand", ""); + this._title.value = ""; + this._box.hidden = true; + this._splitter.hidden = true; + + let selBrowser = gBrowser.selectedBrowser; + selBrowser.focus(); + selBrowser.messageManager.sendAsyncMessage("Sidebar:VisibilityChange", + {commandID: commandID, isOpen: false} + ); + BrowserUITelemetry.countSidebarEvent(commandID, "hide"); + }, +}; + +/** + * This exists for backards compatibility - it will be called once a sidebar is + * ready, following any request to show it. + * + * @deprecated + */ +function fireSidebarFocusedEvent() {} + +/** + * This exists for backards compatibility - it gets called when a sidebar has + * been loaded. + * + * @deprecated + */ +function sidebarOnLoad(event) {} + +/** + * This exists for backards compatibility, and is equivilent to + * SidebarUI.toggle() without the forceOpen param. With forceOpen set to true, + * it is equalivent to SidebarUI.show(). + * + * @deprecated + */ +function toggleSidebar(commandID, forceOpen = false) { + Deprecated.warning("toggleSidebar() is deprecated, please use SidebarUI.toggle() or SidebarUI.show() instead", + "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Sidebar"); + + if (forceOpen) { + SidebarUI.show(commandID); + } else { + SidebarUI.toggle(commandID); + } +} diff --git a/application/basilisk/base/content/browser-syncui.js b/application/basilisk/base/content/browser-syncui.js new file mode 100644 index 000000000..51bcb15d5 --- /dev/null +++ b/application/basilisk/base/content/browser-syncui.js @@ -0,0 +1,548 @@ +/* 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/. */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +#ifdef MOZ_SERVICES_CLOUDSYNC +XPCOMUtils.defineLazyModuleGetter(this, "CloudSync", + "resource://gre/modules/CloudSync.jsm"); +#endif + +XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", + "resource://gre/modules/FxAccounts.jsm"); + +const MIN_STATUS_ANIMATION_DURATION = 1600; + +// gSyncUI handles updating the tools menu and displaying notifications. +var gSyncUI = { + _obs: ["weave:service:sync:start", + "weave:service:sync:finish", + "weave:service:sync:error", + "weave:service:setup-complete", + "weave:service:login:start", + "weave:service:login:finish", + "weave:service:login:error", + "weave:service:logout:finish", + "weave:service:start-over", + "weave:service:start-over:finish", + "weave:ui:login:error", + "weave:ui:sync:error", + "weave:ui:sync:finish", + "weave:ui:clear-error", + "weave:engine:sync:finish" + ], + + _unloaded: false, + // The last sync start time. Used to calculate the leftover animation time + // once syncing completes (bug 1239042). + _syncStartTime: 0, + _syncAnimationTimer: 0, + + init: function () { + Cu.import("resource://services-common/stringbundle.js"); + + // Proceed to set up the UI if Sync has already started up. + // Otherwise we'll do it when Sync is firing up. + if (this.weaveService.ready) { + this.initUI(); + return; + } + + // Sync isn't ready yet, but we can still update the UI with an initial + // state - we haven't called initUI() yet, but that's OK - that's more + // about observers for state changes, and will be called once Sync is + // ready to start sending notifications. + this.updateUI(); + + Services.obs.addObserver(this, "weave:service:ready", true); + Services.obs.addObserver(this, "quit-application", true); + + // Remove the observer if the window is closed before the observer + // was triggered. + window.addEventListener("unload", function onUnload() { + gSyncUI._unloaded = true; + window.removeEventListener("unload", onUnload, false); + Services.obs.removeObserver(gSyncUI, "weave:service:ready"); + Services.obs.removeObserver(gSyncUI, "quit-application"); + + if (Weave.Status.ready) { + gSyncUI._obs.forEach(function(topic) { + Services.obs.removeObserver(gSyncUI, topic); + }); + } + }, false); + }, + + initUI: function SUI_initUI() { + // If this is a browser window? + if (gBrowser) { + this._obs.push("weave:notification:added"); + } + + this._obs.forEach(function(topic) { + Services.obs.addObserver(this, topic, true); + }, this); + + // initial label for the sync buttons. + let broadcaster = document.getElementById("sync-status"); + broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label")); + + this.maybeMoveSyncedTabsButton(); + + this.updateUI(); + }, + + + // Returns a promise that resolves with true if Sync needs to be configured, + // false otherwise. + _needsSetup() { + // If Sync is configured for FxAccounts then we do that promise-dance. + if (this.weaveService.fxAccountsEnabled) { + return fxAccounts.getSignedInUser().then(user => { + // We want to treat "account needs verification" as "needs setup". + return !(user && user.verified); + }); + } + // We are using legacy sync - check that. + let firstSync = ""; + try { + firstSync = Services.prefs.getCharPref("services.sync.firstSync"); + } catch (e) { } + + return Promise.resolve(Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || + firstSync == "notReady"); + }, + + // Returns a promise that resolves with true if the user currently signed in + // to Sync needs to be verified, false otherwise. + _needsVerification() { + // For callers who care about the distinction between "needs setup" and + // "needs verification" + if (this.weaveService.fxAccountsEnabled) { + return fxAccounts.getSignedInUser().then(user => { + // If there is no user, they can't be in a "needs verification" state. + if (!user) { + return false; + } + return !user.verified; + }); + } + + // Otherwise we are configured for legacy Sync, which has no verification + // concept. + return Promise.resolve(false); + }, + + // Note that we don't show login errors in a notification bar here, but do + // still need to track a login-failed state so the "Tools" menu updates + // with the correct state. + _loginFailed: function () { + // If Sync isn't already ready, we don't want to force it to initialize + // by referencing Weave.Status - and it isn't going to be accurate before + // Sync is ready anyway. + if (!this.weaveService.ready) { + this.log.debug("_loginFailed has sync not ready, so returning false"); + return false; + } + this.log.debug("_loginFailed has sync state=${sync}", + { sync: Weave.Status.login}); + return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; + }, + + // Kick off an update of the UI - does *not* return a promise. + updateUI() { + this._promiseUpdateUI().catch(err => { + this.log.error("updateUI failed", err); + }) + }, + + // Updates the UI - returns a promise. + _promiseUpdateUI() { + return this._needsSetup().then(needsSetup => { + if (!gBrowser) + return Promise.resolve(); + + let loginFailed = this._loginFailed(); + + // Start off with a clean slate + document.getElementById("sync-reauth-state").hidden = true; + document.getElementById("sync-setup-state").hidden = true; + document.getElementById("sync-syncnow-state").hidden = true; + +#ifdef MOZ_SERVICES_CLOUDSYNC + if (CloudSync && CloudSync.ready && CloudSync().adapters.count) { + document.getElementById("sync-syncnow-state").hidden = false; + } else if (loginFailed) { +#else + if (loginFailed) { +#endif + // unhiding this element makes the menubar show the login failure state. + document.getElementById("sync-reauth-state").hidden = false; + } else if (needsSetup) { + document.getElementById("sync-setup-state").hidden = false; + } else { + document.getElementById("sync-syncnow-state").hidden = false; + } + + return this._updateSyncButtonsTooltip(); + }); + }, + + // Functions called by observers + onActivityStart() { + if (!gBrowser) + return; + + this.log.debug("onActivityStart"); + + clearTimeout(this._syncAnimationTimer); + this._syncStartTime = Date.now(); + + let broadcaster = document.getElementById("sync-status"); + broadcaster.setAttribute("syncstatus", "active"); + broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncing2.label")); + broadcaster.setAttribute("disabled", "true"); + + this.updateUI(); + }, + + _updateSyncStatus() { + if (!gBrowser) + return; + let broadcaster = document.getElementById("sync-status"); + broadcaster.removeAttribute("syncstatus"); + broadcaster.removeAttribute("disabled"); + broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label")); + this.updateUI(); + }, + + onActivityStop() { + if (!gBrowser) + return; + this.log.debug("onActivityStop"); + + let now = Date.now(); + let syncDuration = now - this._syncStartTime; + + if (syncDuration < MIN_STATUS_ANIMATION_DURATION) { + let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration; + clearTimeout(this._syncAnimationTimer); + this._syncAnimationTimer = setTimeout(() => this._updateSyncStatus(), animationTime); + } else { + this._updateSyncStatus(); + } + }, + + onLoginError: function SUI_onLoginError() { + this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status); + + // We don't show any login errors here; browser-fxaccounts shows them in + // the hamburger menu. + this.updateUI(); + }, + + onLogout: function SUI_onLogout() { + this.updateUI(); + }, + + _getAppName: function () { + let brand = new StringBundle("chrome://branding/locale/brand.properties"); + return brand.get("brandShortName"); + }, + + // Commands + // doSync forces a sync - it *does not* return a promise as it is called + // via the various UI components. + doSync() { + this._needsSetup().then(needsSetup => { + if (!needsSetup) { + setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0); + } + Services.obs.notifyObservers(null, "cloudsync:user-sync", null); + }).catch(err => { + this.log.error("Failed to force a sync", err); + }); + }, + + // Handle clicking the toolbar button - which either opens the Sync setup + // pages or forces a sync now. Does *not* return a promise as it is called + // via the UI. + handleToolbarButton() { + this._needsSetup().then(needsSetup => { + if (needsSetup || this._loginFailed()) { + this.openSetup(); + } else { + this.doSync(); + } + }).catch(err => { + this.log.error("Failed to handle toolbar button command", err); + }); + }, + + /** + * Invoke the Sync setup wizard. + * + * @param wizardType + * Indicates type of wizard to launch: + * null -- regular set up wizard + * "pair" -- pair a device first + * "reset" -- reset sync + * @param entryPoint + * Indicates the entrypoint from where this method was called. + */ + + openSetup: function SUI_openSetup(wizardType, entryPoint = "syncbutton") { + if (this.weaveService.fxAccountsEnabled) { + this.openPrefs(entryPoint); + } else { + let win = Services.wm.getMostRecentWindow("Weave:AccountSetup"); + if (win) + win.focus(); + else { + window.openDialog("chrome://browser/content/sync/setup.xul", + "weaveSetup", "centerscreen,chrome,resizable=no", + wizardType); + } + } + }, + + // Open the legacy-sync device pairing UI. Note used for FxA Sync. + openAddDevice: function () { + if (!Weave.Utils.ensureMPUnlocked()) + return; + + let win = Services.wm.getMostRecentWindow("Sync:AddDevice"); + if (win) + win.focus(); + else + window.openDialog("chrome://browser/content/sync/addDevice.xul", + "syncAddDevice", "centerscreen,chrome,resizable=no"); + }, + + openPrefs: function (entryPoint) { + openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } }); + }, + + openSignInAgainPage: function (entryPoint = "syncbutton") { + gFxAccounts.openSignInAgainPage(entryPoint); + }, + + openSyncedTabsPanel() { + let placement = CustomizableUI.getPlacementOfWidget("sync-button"); + let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR; + let anchor = document.getElementById("sync-button") || + document.getElementById("PanelUI-menu-button"); + if (area == CustomizableUI.AREA_PANEL) { + // The button is in the panel, so we need to show the panel UI, then our + // subview. + PanelUI.show().then(() => { + PanelUI.showSubView("PanelUI-remotetabs", anchor, area); + }).catch(Cu.reportError); + } else { + // It is placed somewhere else - just try and show it. + PanelUI.showSubView("PanelUI-remotetabs", anchor, area); + } + }, + + /* After Sync is initialized we perform a once-only check for the sync + button being in "customize purgatory" and if so, move it to the panel. + This is done primarily for profiles created before SyncedTabs landed, + where the button defaulted to being in that purgatory. + We use a preference to ensure we only do it once, so people can still + customize it away and have it stick. + */ + maybeMoveSyncedTabsButton() { + const prefName = "browser.migrated-sync-button"; + let migrated = false; + try { + migrated = Services.prefs.getBoolPref(prefName); + } catch (_) {} + if (migrated) { + return; + } + if (!CustomizableUI.getPlacementOfWidget("sync-button")) { + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + } + Services.prefs.setBoolPref(prefName, true); + }, + + /* Update the tooltip for the sync-status broadcaster (which will update the + Sync Toolbar button and the Sync spinner in the FxA hamburger area.) + If Sync is configured, the tooltip is when the last sync occurred, + otherwise the tooltip reflects the fact that Sync needs to be + (re-)configured. + */ + _updateSyncButtonsTooltip: Task.async(function* () { + if (!gBrowser) + return; + + let email; + try { + email = Services.prefs.getCharPref("services.sync.username"); + } catch (ex) {} + + let needsSetup = yield this._needsSetup(); + let needsVerification = yield this._needsVerification(); + let loginFailed = this._loginFailed(); + // This is a little messy as the Sync buttons are 1/2 Sync related and + // 1/2 FxA related - so for some strings we use Sync strings, but for + // others we reach into gFxAccounts for strings. + let tooltiptext; + if (needsVerification) { + // "needs verification" + tooltiptext = gFxAccounts.strings.formatStringFromName("verifyDescription", [email], 1); + } else if (needsSetup) { + // "needs setup". + tooltiptext = this._stringBundle.GetStringFromName("signInToSync.description"); + } else if (loginFailed) { + // "need to reconnect/re-enter your password" + tooltiptext = gFxAccounts.strings.formatStringFromName("reconnectDescription", [email], 1); + } else { + // Sync appears configured - format the "last synced at" time. + try { + let lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync")); + tooltiptext = this.formatLastSyncDate(lastSync); + } + catch (e) { + // pref doesn't exist (which will be the case until we've seen the + // first successful sync) or is invalid (which should be impossible!) + // Just leave tooltiptext as the empty string in these cases, which + // will cause the tooltip to be removed below. + } + } + + // We've done all our promise-y work and ready to update the UI - make + // sure it hasn't been torn down since we started. + if (!gBrowser) + return; + + let broadcaster = document.getElementById("sync-status"); + if (broadcaster) { + if (tooltiptext) { + broadcaster.setAttribute("tooltiptext", tooltiptext); + } else { + broadcaster.removeAttribute("tooltiptext"); + } + } + }), + + formatLastSyncDate: function(date) { + let dateFormat; + let sixDaysAgo = (() => { + let date = new Date(); + date.setDate(date.getDate() - 6); + date.setHours(0, 0, 0, 0); + return date; + })(); + // It may be confusing for the user to see "Last Sync: Monday" when the last sync was a indeed a Monday but 3 weeks ago + if (date < sixDaysAgo) { + dateFormat = {month: 'long', day: 'numeric'}; + } else { + dateFormat = {weekday: 'long', hour: 'numeric', minute: 'numeric'}; + } + let lastSyncDateString = date.toLocaleDateString(undefined, dateFormat); + return this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1); + }, + + onClientsSynced: function() { + let broadcaster = document.getElementById("sync-syncnow-state"); + if (broadcaster) { + if (Weave.Service.clientsEngine.stats.numClients > 1) { + broadcaster.setAttribute("devices-status", "multi"); + } else { + broadcaster.setAttribute("devices-status", "single"); + } + } + }, + + observe: function SUI_observe(subject, topic, data) { + this.log.debug("observed", topic); + if (this._unloaded) { + Cu.reportError("SyncUI observer called after unload: " + topic); + return; + } + + // Unwrap, just like Svc.Obs, but without pulling in that dependency. + if (subject && typeof subject == "object" && + ("wrappedJSObject" in subject) && + ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) { + subject = subject.wrappedJSObject.object; + } + + // First handle "activity" only. + switch (topic) { + case "weave:service:sync:start": + this.onActivityStart(); + break; + case "weave:service:sync:finish": + case "weave:service:sync:error": + this.onActivityStop(); + break; + } + // Now non-activity state (eg, enabled, errors, etc) + // Note that sync uses the ":ui:" notifications for errors because sync. + switch (topic) { + case "weave:ui:sync:finish": + // Do nothing. + break; + case "weave:ui:sync:error": + case "weave:service:setup-complete": + case "weave:service:login:finish": + case "weave:service:login:start": + case "weave:service:start-over": + this.updateUI(); + break; + case "weave:ui:login:error": + case "weave:service:login:error": + this.onLoginError(); + break; + case "weave:service:logout:finish": + this.onLogout(); + break; + case "weave:service:start-over:finish": + this.updateUI(); + break; + case "weave:service:ready": + this.initUI(); + break; + case "weave:notification:added": + this.initNotifications(); + break; + case "weave:engine:sync:finish": + if (data != "clients") { + return; + } + this.onClientsSynced(); + break; + case "quit-application": + // Stop the animation timer on shutdown, since we can't update the UI + // after this. + clearTimeout(this._syncAnimationTimer); + break; + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIObserver, + Ci.nsISupportsWeakReference + ]) +}; + +XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() { + // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381) + // but for now just make it work + return Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle("chrome://weave/locale/services/sync.properties"); +}); + +XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() { + return Log.repository.getLogger("browserwindow.syncui"); +}); + +XPCOMUtils.defineLazyGetter(gSyncUI, "weaveService", function() { + return Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; +}); diff --git a/application/basilisk/base/content/browser-tabPreviews.xml b/application/basilisk/base/content/browser-tabPreviews.xml new file mode 100644 index 000000000..f3f2ad180 --- /dev/null +++ b/application/basilisk/base/content/browser-tabPreviews.xml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> + +# -*- Mode: HTML -*- +# 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/. + +<bindings id="tabPreviews" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + <binding id="ctrlTab-preview" extends="chrome://global/content/bindings/button.xml#button-base"> + <content pack="center"> + <xul:stack> + <xul:vbox class="ctrlTab-preview-inner" align="center" pack="center" + xbl:inherits="width=canvaswidth"> + <xul:hbox class="tabPreview-canvas" xbl:inherits="style=canvasstyle"> + <children/> + </xul:hbox> + <xul:label xbl:inherits="value=label,crop" class="plain"/> + </xul:vbox> + <xul:hbox class="ctrlTab-favicon-container" xbl:inherits="hidden=noicon"> + <xul:image class="ctrlTab-favicon" xbl:inherits="src=image"/> + </xul:hbox> + </xul:stack> + </content> + <handlers> + <handler event="mouseover" action="ctrlTab._mouseOverFocus(this);"/> + <handler event="command" action="ctrlTab.pick(this);"/> + <handler event="click" button="1" action="ctrlTab.remove(this);"/> +#ifdef XP_MACOSX +# Control+click is a right click on OS X + <handler event="click" button="2" action="ctrlTab.pick(this);"/> +#endif + </handlers> + </binding> +</bindings> diff --git a/application/basilisk/base/content/browser-tabsintitlebar-stub.js b/application/basilisk/base/content/browser-tabsintitlebar-stub.js new file mode 100644 index 000000000..1e45b17dd --- /dev/null +++ b/application/basilisk/base/content/browser-tabsintitlebar-stub.js @@ -0,0 +1,17 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file is used as a stub object for platforms which +// don't have CAN_DRAW_IN_TITLEBAR defined. + +var TabsInTitlebar = { + init: function () {}, + uninit: function () {}, + allowedBy: function (condition, allow) {}, + updateAppearance: function updateAppearance(aForce) {}, + get enabled() { + return document.documentElement.getAttribute("tabsintitlebar") == "true"; + }, +}; diff --git a/application/basilisk/base/content/browser-tabsintitlebar.js b/application/basilisk/base/content/browser-tabsintitlebar.js new file mode 100644 index 000000000..5c0d94514 --- /dev/null +++ b/application/basilisk/base/content/browser-tabsintitlebar.js @@ -0,0 +1,307 @@ +/* -*- 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/. */ + +// Note: the file browser-tabsintitlebar-stub.js is used instead of +// this one on platforms which don't have CAN_DRAW_IN_TITLEBAR defined. + +var TabsInTitlebar = { + init: function () { + if (this._initialized) { + return; + } + this._readPref(); + Services.prefs.addObserver(this._prefName, this, false); + + // We need to update the appearance of the titlebar when the menu changes + // from the active to the inactive state. We can't, however, rely on + // DOMMenuBarInactive, because the menu fires this event and then removes + // the inactive attribute after an event-loop spin. + // + // Because updating the appearance involves sampling the heights and margins + // of various elements, it's important that the layout be more or less + // settled before updating the titlebar. So instead of listening to + // DOMMenuBarActive and DOMMenuBarInactive, we use a MutationObserver to + // watch the "invalid" attribute directly. + let menu = document.getElementById("toolbar-menubar"); + this._menuObserver = new MutationObserver(this._onMenuMutate); + this._menuObserver.observe(menu, {attributes: true}); + + this.onAreaReset = function(aArea) { + if (aArea == CustomizableUI.AREA_TABSTRIP || aArea == CustomizableUI.AREA_MENUBAR) + this._update(true); + }; + this.onWidgetAdded = this.onWidgetRemoved = function(aWidgetId, aArea) { + if (aArea == CustomizableUI.AREA_TABSTRIP || aArea == CustomizableUI.AREA_MENUBAR) + this._update(true); + }; + CustomizableUI.addListener(this); + + addEventListener("resolutionchange", this, false); + + this._initialized = true; + if (this._updateOnInit) { + // We don't need to call this with 'true', even if original calls + // (before init()) did, because this will be the first call and so + // we will update anyway. + this._update(); + } + }, + + allowedBy: function (condition, allow) { + if (allow) { + if (condition in this._disallowed) { + delete this._disallowed[condition]; + this._update(true); + } + } else if (!(condition in this._disallowed)) { + this._disallowed[condition] = null; + this._update(true); + } + }, + + updateAppearance: function updateAppearance(aForce) { + this._update(aForce); + }, + + get enabled() { + return document.documentElement.getAttribute("tabsintitlebar") == "true"; + }, + + observe: function (subject, topic, data) { + if (topic == "nsPref:changed") + this._readPref(); + }, + + handleEvent: function (aEvent) { + if (aEvent.type == "resolutionchange" && aEvent.target == window) { + this._update(true); + } + }, + + _onMenuMutate: function (aMutations) { + for (let mutation of aMutations) { + if (mutation.attributeName == "inactive" || + mutation.attributeName == "autohide") { + TabsInTitlebar._update(true); + return; + } + } + }, + + _initialized: false, + _updateOnInit: false, + _disallowed: {}, + _prefName: "browser.tabs.drawInTitlebar", + _lastSizeMode: null, + + _readPref: function () { + this.allowedBy("pref", + Services.prefs.getBoolPref(this._prefName)); + }, + + _update: function (aForce=false) { + let $ = id => document.getElementById(id); + let rect = ele => ele.getBoundingClientRect(); + let verticalMargins = cstyle => parseFloat(cstyle.marginBottom) + parseFloat(cstyle.marginTop); + + if (window.fullScreen) + return; + + // In some edgecases it is possible for this to fire before we've initialized. + // Don't run now, but don't forget to run it when we do initialize. + if (!this._initialized) { + this._updateOnInit = true; + return; + } + + if (!aForce) { + // _update is called on resize events, because the window is not ready + // after sizemode events. However, we only care about the event when the + // sizemode is different from the last time we updated the appearance of + // the tabs in the titlebar. + let sizemode = document.documentElement.getAttribute("sizemode"); + if (this._lastSizeMode == sizemode) { + return; + } + let oldSizeMode = this._lastSizeMode; + this._lastSizeMode = sizemode; + // Don't update right now if we are leaving fullscreen, since the UI is + // still changing in the consequent "fullscreen" event. Code there will + // call this function again when everything is ready. + // See browser-fullScreen.js: FullScreen.toggle and bug 1173768. + if (oldSizeMode == "fullscreen") { + return; + } + } + + let allowed = (Object.keys(this._disallowed)).length == 0; + + let titlebar = $("titlebar"); + let titlebarContent = $("titlebar-content"); + let menubar = $("toolbar-menubar"); + + if (allowed) { + // We set the tabsintitlebar attribute first so that our CSS for + // tabsintitlebar manifests before we do our measurements. + document.documentElement.setAttribute("tabsintitlebar", "true"); + updateTitlebarDisplay(); + + // Try to avoid reflows in this code by calculating dimensions first and + // then later set the properties affecting layout together in a batch. + + // Get the full height of the tabs toolbar: + let tabsToolbar = $("TabsToolbar"); + let tabsStyles = window.getComputedStyle(tabsToolbar); + let fullTabsHeight = rect(tabsToolbar).height + verticalMargins(tabsStyles); + // Buttons first: + let captionButtonsBoxWidth = rect($("titlebar-buttonbox-container")).width; + + let secondaryButtonsWidth, menuHeight, fullMenuHeight, menuStyles; + if (AppConstants.platform == "macosx") { + secondaryButtonsWidth = rect($("titlebar-secondary-buttonbox")).width; + // No need to look up the menubar stuff on OS X: + menuHeight = 0; + fullMenuHeight = 0; + } else { + // Otherwise, get the height and margins separately for the menubar + menuHeight = rect(menubar).height; + menuStyles = window.getComputedStyle(menubar); + fullMenuHeight = verticalMargins(menuStyles) + menuHeight; + } + + // And get the height of what's in the titlebar: + let titlebarContentHeight = rect(titlebarContent).height; + + // Begin setting CSS properties which will cause a reflow + + // If the menubar is around (menuHeight is non-zero), try to adjust + // its full height (i.e. including margins) to match the titlebar, + // by changing the menubar's bottom padding + if (menuHeight) { + // Calculate the difference between the titlebar's height and that of the menubar + let menuTitlebarDelta = titlebarContentHeight - fullMenuHeight; + let paddingBottom; + // The titlebar is bigger: + if (menuTitlebarDelta > 0) { + fullMenuHeight += menuTitlebarDelta; + // If there is already padding on the menubar, we need to add that + // to the difference so the total padding is correct: + if ((paddingBottom = menuStyles.paddingBottom)) { + menuTitlebarDelta += parseFloat(paddingBottom); + } + menubar.style.paddingBottom = menuTitlebarDelta + "px"; + // The menubar is bigger, but has bottom padding we can remove: + } else if (menuTitlebarDelta < 0 && (paddingBottom = menuStyles.paddingBottom)) { + let existingPadding = parseFloat(paddingBottom); + // menuTitlebarDelta is negative; work out what's left, but don't set negative padding: + let desiredPadding = Math.max(0, existingPadding + menuTitlebarDelta); + menubar.style.paddingBottom = desiredPadding + "px"; + // We've changed the menu height now: + fullMenuHeight += desiredPadding - existingPadding; + } + } + + // Next, we calculate how much we need to stretch the titlebar down to + // go all the way to the bottom of the tab strip, if necessary. + let tabAndMenuHeight = fullTabsHeight + fullMenuHeight; + + if (tabAndMenuHeight > titlebarContentHeight) { + // We need to increase the titlebar content's outer height (ie including margins) + // to match the tab and menu height: + let extraMargin = tabAndMenuHeight - titlebarContentHeight; + if (AppConstants.platform != "macosx") { + titlebarContent.style.marginBottom = extraMargin + "px"; + } + + titlebarContentHeight += extraMargin; + } else { + titlebarContent.style.removeProperty("margin-bottom"); + } + + // Then add a negative margin to the titlebar, so that the following elements + // will overlap it by the lesser of the titlebar height or the tabstrip+menu. + let minTitlebarOrTabsHeight = Math.min(titlebarContentHeight, tabAndMenuHeight); + titlebar.style.marginBottom = "-" + minTitlebarOrTabsHeight + "px"; + + // Finally, size the placeholders: + if (AppConstants.platform == "macosx") { + this._sizePlaceholder("fullscreen-button", secondaryButtonsWidth); + } + this._sizePlaceholder("caption-buttons", captionButtonsBoxWidth); + + } else { + document.documentElement.removeAttribute("tabsintitlebar"); + updateTitlebarDisplay(); + + if (AppConstants.platform == "macosx") { + let secondaryButtonsWidth = rect($("titlebar-secondary-buttonbox")).width; + this._sizePlaceholder("fullscreen-button", secondaryButtonsWidth); + } + + // Reset the margins and padding that might have been modified: + titlebarContent.style.marginTop = ""; + titlebarContent.style.marginBottom = ""; + titlebar.style.marginBottom = ""; + menubar.style.paddingBottom = ""; + } + + ToolbarIconColor.inferFromText(); + if (CustomizationHandler.isCustomizing()) { + gCustomizeMode.updateLWTStyling(); + } + }, + + _sizePlaceholder: function (type, width) { + Array.forEach(document.querySelectorAll(".titlebar-placeholder[type='"+ type +"']"), + function (node) { node.width = width; }); + }, + + uninit: function () { + this._initialized = false; + removeEventListener("resolutionchange", this); + Services.prefs.removeObserver(this._prefName, this); + this._menuObserver.disconnect(); + CustomizableUI.removeListener(this); + } +}; + +function updateTitlebarDisplay() { + if (AppConstants.platform == "macosx") { + // OS X and the other platforms differ enough to necessitate this kind of + // special-casing. Like the other platforms where we CAN_DRAW_IN_TITLEBAR, + // we draw in the OS X titlebar when putting the tabs up there. However, OS X + // also draws in the titlebar when a lightweight theme is applied, regardless + // of whether or not the tabs are drawn in the titlebar. + if (TabsInTitlebar.enabled) { + document.documentElement.setAttribute("chromemargin-nonlwtheme", "0,-1,-1,-1"); + document.documentElement.setAttribute("chromemargin", "0,-1,-1,-1"); + document.documentElement.removeAttribute("drawtitle"); + } else { + // We set chromemargin-nonlwtheme to "" instead of removing it as a way of + // making sure that LightweightThemeConsumer doesn't take it upon itself to + // detect this value again if and when we do a lwtheme state change. + document.documentElement.setAttribute("chromemargin-nonlwtheme", ""); + let isCustomizing = document.documentElement.hasAttribute("customizing"); + let hasLWTheme = document.documentElement.hasAttribute("lwtheme"); + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + if ((!hasLWTheme || isCustomizing) && !isPrivate) { + document.documentElement.removeAttribute("chromemargin"); + } + document.documentElement.setAttribute("drawtitle", "true"); + } + } else if (TabsInTitlebar.enabled) { + // not OS X + document.documentElement.setAttribute("chromemargin", "0,2,2,2"); + } else { + document.documentElement.removeAttribute("chromemargin"); + } +} + +function onTitlebarMaxClick() { + if (window.windowState == window.STATE_MAXIMIZED) + window.restore(); + else + window.maximize(); +} diff --git a/application/basilisk/base/content/browser-thumbnails.js b/application/basilisk/base/content/browser-thumbnails.js new file mode 100644 index 000000000..ebefb193e --- /dev/null +++ b/application/basilisk/base/content/browser-thumbnails.js @@ -0,0 +1,142 @@ +/* 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/. */ + +/** + * Keeps thumbnails of open web pages up-to-date. + */ +var gBrowserThumbnails = { + /** + * Pref that controls whether we can store SSL content on disk + */ + PREF_DISK_CACHE_SSL: "browser.cache.disk_cache_ssl", + + _captureDelayMS: 1000, + + /** + * Used to keep track of disk_cache_ssl preference + */ + _sslDiskCacheEnabled: null, + + /** + * Map of capture() timeouts assigned to their browsers. + */ + _timeouts: null, + + /** + * List of tab events we want to listen for. + */ + _tabEvents: ["TabClose", "TabSelect"], + + init: function Thumbnails_init() { + PageThumbs.addExpirationFilter(this); + gBrowser.addTabsProgressListener(this); + Services.prefs.addObserver(this.PREF_DISK_CACHE_SSL, this, false); + + this._sslDiskCacheEnabled = + Services.prefs.getBoolPref(this.PREF_DISK_CACHE_SSL); + + this._tabEvents.forEach(function (aEvent) { + gBrowser.tabContainer.addEventListener(aEvent, this, false); + }, this); + + this._timeouts = new WeakMap(); + }, + + uninit: function Thumbnails_uninit() { + PageThumbs.removeExpirationFilter(this); + gBrowser.removeTabsProgressListener(this); + Services.prefs.removeObserver(this.PREF_DISK_CACHE_SSL, this); + + this._tabEvents.forEach(function (aEvent) { + gBrowser.tabContainer.removeEventListener(aEvent, this, false); + }, this); + }, + + handleEvent: function Thumbnails_handleEvent(aEvent) { + switch (aEvent.type) { + case "scroll": + let browser = aEvent.currentTarget; + if (this._timeouts.has(browser)) + this._delayedCapture(browser); + break; + case "TabSelect": + this._delayedCapture(aEvent.target.linkedBrowser); + break; + case "TabClose": { + this._clearTimeout(aEvent.target.linkedBrowser); + break; + } + } + }, + + observe: function Thumbnails_observe() { + this._sslDiskCacheEnabled = + Services.prefs.getBoolPref(this.PREF_DISK_CACHE_SSL); + }, + + filterForThumbnailExpiration: + function Thumbnails_filterForThumbnailExpiration(aCallback) { + aCallback(this._topSiteURLs); + }, + + /** + * State change progress listener for all tabs. + */ + onStateChange: function Thumbnails_onStateChange(aBrowser, aWebProgress, + aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) + this._delayedCapture(aBrowser); + }, + + _capture: function Thumbnails_capture(aBrowser) { + // Only capture about:newtab top sites. + if (this._topSiteURLs.indexOf(aBrowser.currentURI.spec) == -1) + return; + this._shouldCapture(aBrowser, function (aResult) { + if (aResult) { + PageThumbs.captureAndStoreIfStale(aBrowser); + } + }); + }, + + _delayedCapture: function Thumbnails_delayedCapture(aBrowser) { + if (this._timeouts.has(aBrowser)) + clearTimeout(this._timeouts.get(aBrowser)); + else + aBrowser.addEventListener("scroll", this, true); + + let timeout = setTimeout(function () { + this._clearTimeout(aBrowser); + this._capture(aBrowser); + }.bind(this), this._captureDelayMS); + + this._timeouts.set(aBrowser, timeout); + }, + + _shouldCapture: function Thumbnails_shouldCapture(aBrowser, aCallback) { + // Capture only if it's the currently selected tab. + if (aBrowser != gBrowser.selectedBrowser) { + aCallback(false); + return; + } + PageThumbs.shouldStoreThumbnail(aBrowser, aCallback); + }, + + get _topSiteURLs() { + return NewTabUtils.links.getLinks().reduce((urls, link) => { + if (link) + urls.push(link.url); + return urls; + }, []); + }, + + _clearTimeout: function Thumbnails_clearTimeout(aBrowser) { + if (this._timeouts.has(aBrowser)) { + aBrowser.removeEventListener("scroll", this, false); + clearTimeout(this._timeouts.get(aBrowser)); + this._timeouts.delete(aBrowser); + } + } +}; diff --git a/application/basilisk/base/content/browser-trackingprotection.js b/application/basilisk/base/content/browser-trackingprotection.js new file mode 100644 index 000000000..20917a083 --- /dev/null +++ b/application/basilisk/base/content/browser-trackingprotection.js @@ -0,0 +1,177 @@ +/* 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 TrackingProtection = { + // If the user ignores the doorhanger, we stop showing it after some time. + PREF_ENABLED_GLOBALLY: "privacy.trackingprotection.enabled", + PREF_ENABLED_IN_PRIVATE_WINDOWS: "privacy.trackingprotection.pbmode.enabled", + enabledGlobally: false, + enabledInPrivateWindows: false, + container: null, + content: null, + icon: null, + activeTooltipText: null, + disabledTooltipText: null, + + init() { + let $ = selector => document.querySelector(selector); + this.container = $("#tracking-protection-container"); + this.content = $("#tracking-protection-content"); + this.icon = $("#tracking-protection-icon"); + + this.updateEnabled(); + Services.prefs.addObserver(this.PREF_ENABLED_GLOBALLY, this, false); + Services.prefs.addObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this, false); + + this.activeTooltipText = + gNavigatorBundle.getString("trackingProtection.icon.activeTooltip"); + this.disabledTooltipText = + gNavigatorBundle.getString("trackingProtection.icon.disabledTooltip"); + + this.enabledHistogramAdd(this.enabledGlobally); + this.disabledPBMHistogramAdd(!this.enabledInPrivateWindows); + }, + + uninit() { + Services.prefs.removeObserver(this.PREF_ENABLED_GLOBALLY, this); + Services.prefs.removeObserver(this.PREF_ENABLED_IN_PRIVATE_WINDOWS, this); + }, + + observe() { + this.updateEnabled(); + }, + + get enabled() { + return this.enabledGlobally || + (this.enabledInPrivateWindows && + PrivateBrowsingUtils.isWindowPrivate(window)); + }, + + updateEnabled() { + this.enabledGlobally = + Services.prefs.getBoolPref(this.PREF_ENABLED_GLOBALLY); + this.enabledInPrivateWindows = + Services.prefs.getBoolPref(this.PREF_ENABLED_IN_PRIVATE_WINDOWS); + this.container.hidden = !this.enabled; + }, + + enabledHistogramAdd(value) { + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + return; + } + Services.telemetry.getHistogramById("TRACKING_PROTECTION_ENABLED").add(value); + }, + + disabledPBMHistogramAdd(value) { + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + return; + } + Services.telemetry.getHistogramById("TRACKING_PROTECTION_PBM_DISABLED").add(value); + }, + + eventsHistogramAdd(value) { + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + return; + } + Services.telemetry.getHistogramById("TRACKING_PROTECTION_EVENTS").add(value); + }, + + shieldHistogramAdd(value) { + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + return; + } + Services.telemetry.getHistogramById("TRACKING_PROTECTION_SHIELD").add(value); + }, + + onSecurityChange(state, isSimulated) { + if (!this.enabled) { + return; + } + + // Only animate the shield if the event was not fired directly from + // the tabbrowser (due to a browser change). + if (isSimulated) { + this.icon.removeAttribute("animate"); + } else { + this.icon.setAttribute("animate", "true"); + } + + let isBlocking = state & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT; + let isAllowing = state & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT; + + if (isBlocking) { + this.icon.setAttribute("tooltiptext", this.activeTooltipText); + this.icon.setAttribute("state", "blocked-tracking-content"); + this.content.setAttribute("state", "blocked-tracking-content"); + + this.shieldHistogramAdd(2); + } else if (isAllowing) { + this.icon.setAttribute("tooltiptext", this.disabledTooltipText); + this.icon.setAttribute("state", "loaded-tracking-content"); + this.content.setAttribute("state", "loaded-tracking-content"); + + this.shieldHistogramAdd(1); + } else { + this.icon.removeAttribute("tooltiptext"); + this.icon.removeAttribute("state"); + this.content.removeAttribute("state"); + + // We didn't show the shield + this.shieldHistogramAdd(0); + } + + // Telemetry for state change. + this.eventsHistogramAdd(0); + }, + + disableForCurrentPage() { + // Convert document URI into the format used by + // nsChannelClassifier::ShouldEnableTrackingProtection. + // Any scheme turned into https is correct. + let normalizedUrl = Services.io.newURI( + "https://" + gBrowser.selectedBrowser.currentURI.hostPort, + null, null); + + // Add the current host in the 'trackingprotection' consumer of + // the permission manager using a normalized URI. This effectively + // places this host on the tracking protection allowlist. + if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) { + PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl); + } else { + Services.perms.add(normalizedUrl, + "trackingprotection", Services.perms.ALLOW_ACTION); + } + + // Telemetry for disable protection. + this.eventsHistogramAdd(1); + + // Hide the control center. + document.getElementById("identity-popup").hidePopup(); + + BrowserReload(); + }, + + enableForCurrentPage() { + // Remove the current host from the 'trackingprotection' consumer + // of the permission manager. This effectively removes this host + // from the tracking protection allowlist. + let normalizedUrl = Services.io.newURI( + "https://" + gBrowser.selectedBrowser.currentURI.hostPort, + null, null); + + if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) { + PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl); + } else { + Services.perms.remove(normalizedUrl, "trackingprotection"); + } + + // Telemetry for enable protection. + this.eventsHistogramAdd(2); + + // Hide the control center. + document.getElementById("identity-popup").hidePopup(); + + BrowserReload(); + }, +}; diff --git a/application/basilisk/base/content/browser.css b/application/basilisk/base/content/browser.css new file mode 100644 index 000000000..e951985dc --- /dev/null +++ b/application/basilisk/base/content/browser.css @@ -0,0 +1,1173 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); +@namespace html url("http://www.w3.org/1999/xhtml"); +@namespace svg url("http://www.w3.org/2000/svg"); + +:root { + --identity-popup-expander-width: 38px; + --panelui-subview-transition-duration: 150ms; +} + +#main-window:not([chromehidden~="toolbar"]) { +%ifdef XP_MACOSX + min-width: 335px; +%else + min-width: 300px; +%endif +} + +#main-window[customize-entered] { + min-width: -moz-fit-content; +} + +searchbar { + -moz-binding: url("chrome://browser/content/search/search.xml#searchbar"); +} + +/* Prevent shrinking the page content to 0 height and width */ +.browserStack > browser { + min-height: 25px; + min-width: 25px; +} + +.browserStack > browser { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-browser"); +} + +.browserStack > browser[remote="true"] { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-remote-browser"); +} + +toolbar[customizable="true"] { + -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#toolbar"); +} + +%ifdef XP_MACOSX +#toolbar-menubar { + -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#toolbar-menubar-stub"); +} +%endif + +#toolbar-menubar[autohide="true"] { + -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#toolbar-menubar-autohide"); +} + +#addon-bar { + -moz-binding: url("chrome://browser/content/customizableui/toolbar.xml#addonbar-delegating"); + visibility: visible; + margin: 0; + height: 0 !important; + overflow: hidden; + padding: 0; + border: 0 none; +} + +#addonbar-closebutton { + visibility: visible; + height: 0 !important; +} + +#status-bar { + height: 0 !important; + -moz-binding: none; + padding: 0; + margin: 0; +} + +panelmultiview { + -moz-binding: url("chrome://browser/content/customizableui/panelUI.xml#panelmultiview"); +} + +panelview { + -moz-binding: url("chrome://browser/content/customizableui/panelUI.xml#panelview"); + -moz-box-orient: vertical; +} + +.panel-mainview { + transition: transform var(--panelui-subview-transition-duration); +} + +panelview:not([mainview]):not([current]) { + transition: visibility 0s linear var(--panelui-subview-transition-duration); + visibility: collapse; +} + +browser[frameType="social"][remote="true"] { + -moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser"); +} + +tabbrowser { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser"); +} + +.tabbrowser-tabs { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabs"); +} + +#tabbrowser-tabs:not([overflow="true"]) ~ #alltabs-button, +#tabbrowser-tabs:not([overflow="true"]) + #new-tab-button, +#tabbrowser-tabs[overflow="true"] > .tabbrowser-arrowscrollbox > .tabs-newtab-button, +#TabsToolbar[currentset]:not([currentset*="tabbrowser-tabs,new-tab-button"]) > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .tabs-newtab-button, +#TabsToolbar[customizing="true"] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .tabs-newtab-button { + visibility: collapse; +} + +#tabbrowser-tabs:not([overflow="true"])[using-closing-tabs-spacer] ~ #alltabs-button { + visibility: hidden; /* temporary space to keep a tab's close button under the cursor */ +} + +.tabs-newtab-button > .toolbarbutton-menu-dropmarker, +#new-tab-button > .toolbarbutton-menu-dropmarker { + display: none; +} + +/* override drop marker image padding */ +.tabs-newtab-button > .toolbarbutton-icon { + margin-inline-end: 0; +} + +.tabbrowser-tab { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tab"); +} + +.tabbrowser-tab:not([pinned]) { + -moz-box-flex: 100; + max-width: 210px; + min-width: 100px; + width: 0; + transition: min-width 100ms ease-out, + max-width 100ms ease-out; +} + +.tabbrowser-tab:not([pinned]):not([fadein]) { + max-width: 0.1px; + min-width: 0.1px; + visibility: hidden; +} + +.tab-close-button, +.tab-background { + /* Explicitly set the visibility to override the value (collapsed) + * we inherit from #TabsToolbar[collapsed] upon opening a browser window. */ + visibility: visible; +} + +.tab-close-button[fadein], +.tab-background[fadein] { + /* This transition is only wanted for opening tabs. */ + transition: visibility 0ms 25ms; +} + +.tab-close-button:not([fadein]), +.tab-background:not([fadein]) { + visibility: hidden; +} + +.tab-label:not([fadein]), +.tab-throbber:not([fadein]), +.tab-icon-image:not([fadein]) { + display: none; +} + +.tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] { + position: fixed !important; + display: block; /* position:fixed already does this (bug 579776), but let's be explicit */ +} + +.tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected] { + position: relative; + z-index: 2; + pointer-events: none; /* avoid blocking dragover events on scroll buttons */ +} + +.tabbrowser-tabs[movingtab] > .tabbrowser-tab[fadein]:not([selected]) { + transition: transform 200ms ease-out; +} + +.new-tab-popup, +#alltabs-popup { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-alltabs-popup"); +} + +toolbar[printpreview="true"] { + -moz-binding: url("chrome://global/content/printPreviewBindings.xml#printpreviewtoolbar"); +} + +toolbar[overflowable] > .customization-target { + overflow: hidden; +} + +toolbar:not([overflowing]) > .overflow-button, +toolbar[customizing] > .overflow-button { + display: none; +} + +%ifdef CAN_DRAW_IN_TITLEBAR +#main-window:not([chromemargin]) > #titlebar, +#main-window[inFullscreen] > #titlebar, +#main-window[inFullscreen] .titlebar-placeholder, +#main-window:not([tabsintitlebar]) .titlebar-placeholder { + display: none; +} + +#titlebar { + -moz-binding: url("chrome://global/content/bindings/general.xml#windowdragbox"); + -moz-window-dragging: drag; +} + +#titlebar-spacer { + pointer-events: none; +} + +#main-window[tabsintitlebar] #titlebar-buttonbox { + position: relative; +} + +#titlebar-buttonbox { + -moz-appearance: -moz-window-button-box; +} + +#personal-bookmarks { + -moz-window-dragging: inherit; +} + +%ifdef XP_MACOSX +#titlebar-fullscreen-button { + -moz-appearance: -moz-mac-fullscreen-button; +} + +/* Fullscreen and caption buttons don't move with RTL on OS X so override the automatic ordering. */ +#titlebar-secondary-buttonbox:-moz-locale-dir(ltr), +#titlebar-buttonbox-container:-moz-locale-dir(rtl), +.titlebar-placeholder[type="fullscreen-button"]:-moz-locale-dir(ltr), +.titlebar-placeholder[type="caption-buttons"]:-moz-locale-dir(rtl) { + -moz-box-ordinal-group: 1000; +} + +#titlebar-secondary-buttonbox:-moz-locale-dir(rtl), +#titlebar-buttonbox-container:-moz-locale-dir(ltr), +.titlebar-placeholder[type="caption-buttons"]:-moz-locale-dir(ltr), +.titlebar-placeholder[type="fullscreen-button"]:-moz-locale-dir(rtl) { + -moz-box-ordinal-group: 0; +} + +%else +/* On non-OSX, these should be start-aligned */ +#titlebar-buttonbox-container { + -moz-box-align: start; +} +%endif + +%if !defined(MOZ_WIDGET_GTK) +#TabsToolbar > .private-browsing-indicator { + -moz-box-ordinal-group: 1000; +} +%endif + +%ifdef XP_WIN +#main-window[sizemode="maximized"] #titlebar-buttonbox { + -moz-appearance: -moz-window-button-box-maximized; +} + +#main-window[tabletmode] #titlebar-min, +#main-window[tabletmode] #titlebar-max { + display: none !important; +} + +#main-window[tabsintitlebar] #TabsToolbar, +#main-window[tabsintitlebar] #toolbar-menubar, +#main-window[tabsintitlebar] #navigator-toolbox > toolbar:-moz-lwtheme { + -moz-window-dragging: drag; +} +%endif + +%endif + +#main-window[inFullscreen][inDOMFullscreen] #navigator-toolbox, +#main-window[inFullscreen][inDOMFullscreen] #fullscr-toggler, +#main-window[inFullscreen][inDOMFullscreen] #sidebar-box, +#main-window[inFullscreen][inDOMFullscreen] #sidebar-splitter, +#main-window[inFullscreen]:not([OSXLionFullscreen]) toolbar:not([fullscreentoolbar=true]), +#main-window[inFullscreen] #global-notificationbox, +#main-window[inFullscreen] #high-priority-global-notificationbox { + visibility: collapse; +} + +#navigator-toolbox[fullscreenShouldAnimate] { + transition: 1.5s margin-top ease-out; +} + +/* Rules to help integrate SDK widgets */ +toolbaritem[sdkstylewidget="true"] > toolbarbutton, +toolbarpaletteitem > toolbaritem[sdkstylewidget="true"] > iframe, +toolbarpaletteitem > toolbaritem[sdkstylewidget="true"] > .toolbarbutton-text { + display: none; +} + +toolbarpaletteitem:-moz-any([place="palette"], [place="panel"]) > toolbaritem[sdkstylewidget="true"] > toolbarbutton { + display: -moz-box; +} + +toolbarpaletteitem > toolbaritem[sdkstylewidget="true"][cui-areatype="toolbar"] > .toolbarbutton-text { + display: -moz-box; +} + +@media not all and (min-resolution: 1.1dppx) { + .webextension-browser-action { + list-style-image: var(--webextension-toolbar-image); + } + + .webextension-browser-action[cui-areatype="menu-panel"], + toolbarpaletteitem[place="palette"] > .webextension-browser-action { + list-style-image: var(--webextension-menupanel-image); + } + + .webextension-page-action { + list-style-image: var(--webextension-urlbar-image); + } +} + +@media (min-resolution: 1.1dppx) { + .webextension-browser-action { + list-style-image: var(--webextension-toolbar-image-2x); + } + + .webextension-browser-action[cui-areatype="menu-panel"], + toolbarpaletteitem[place="palette"] > .webextension-browser-action { + list-style-image: var(--webextension-menupanel-image-2x); + } + + .webextension-page-action { + list-style-image: var(--webextension-urlbar-image-2x); + } +} + +toolbarpaletteitem[removable="false"] { + opacity: 0.5; + cursor: default; +} + +%ifndef XP_MACOSX +toolbarpaletteitem[place="palette"], +toolbarpaletteitem[place="panel"], +toolbarpaletteitem[place="toolbar"] { + -moz-user-focus: normal; +} +%endif + +#bookmarks-toolbar-placeholder, +toolbarpaletteitem > #personal-bookmarks > #PlacesToolbar, +#personal-bookmarks[cui-areatype="menu-panel"] > #PlacesToolbar, +#personal-bookmarks[cui-areatype="toolbar"][overflowedItem=true] > #PlacesToolbar { + display: none; +} + +#PlacesToolbarDropIndicatorHolder { + position: absolute; + top: 25%; +} + +toolbarpaletteitem > #personal-bookmarks > #bookmarks-toolbar-placeholder, +#personal-bookmarks[cui-areatype="menu-panel"] > #bookmarks-toolbar-placeholder, +#personal-bookmarks[cui-areatype="toolbar"][overflowedItem=true] > #bookmarks-toolbar-placeholder { + display: -moz-box; +} + +#nav-bar-customization-target > #personal-bookmarks, +toolbar:not(#TabsToolbar) > #wrapper-personal-bookmarks, +toolbar:not(#TabsToolbar) > #personal-bookmarks { + -moz-box-flex: 1; +} + +#zoom-controls[cui-areatype="toolbar"]:not([overflowedItem=true]) > #zoom-reset-button > .toolbarbutton-text { + display: -moz-box; +} + +#urlbar-reload-button:not([displaystop]) + #urlbar-stop-button, +#urlbar-reload-button[displaystop] { + visibility: collapse; +} + +#PanelUI-feeds > .feed-toolbarbutton:-moz-locale-dir(rtl) { + direction: rtl; +} + +#panelMenu_bookmarksMenu > .bookmark-item { + max-width: none; +} + +#urlbar-container { + min-width: 50ch; +} + +#search-container { + min-width: 25ch; +} + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + .searchbar-engine-image { + image-rendering: -moz-crisp-edges; + } +} + +#urlbar, +.searchbar-textbox { + /* Setting a width and min-width to let the location & search bars maintain + a constant width in case they haven't be resized manually. (bug 965772) */ + width: 1px; + min-width: 1px; +} + +#main-window:-moz-lwtheme { + background-repeat: no-repeat; + background-position: top right; +} + +%ifdef XP_MACOSX +#main-window[inFullscreen="true"] { + padding-top: 0; /* override drawintitlebar="true" */ +} +%endif + +#browser-bottombox[lwthemefooter="true"] { + background-repeat: no-repeat; + background-position: bottom left; +} + +.menuitem-tooltip { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#menuitem-tooltip"); +} + +.menuitem-iconic-tooltip, +.menuitem-tooltip[type="checkbox"], +.menuitem-tooltip[type="radio"] { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#menuitem-iconic-tooltip"); +} + +/* Hide menu elements intended for keyboard access support */ +#main-menubar[openedwithkey=false] .show-only-for-keyboard { + display: none; +} + +/* ::::: location bar ::::: */ +#urlbar { + -moz-binding: url(chrome://browser/content/urlbarBindings.xml#urlbar); +} + +/* Always show URLs LTR. */ +.ac-url-text:-moz-locale-dir(rtl), +.ac-title-text[lookslikeurl]:-moz-locale-dir(rtl) { + direction: ltr !important; +} + +/* For non-action items, hide the action text; for action items, hide the URL + text. */ +.ac-url[actiontype], +.ac-action:not([actiontype]) { + display: none; +} + +/* For action items in a noactions popup, show the URL text and hide the action + text and type icon. */ +#PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-url { + display: -moz-box; +} +#PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-action { + display: none; +} +#PopupAutoCompleteRichResult[noactions] > richlistbox > richlistitem.overridable-action > .ac-type-icon { + list-style-image: none; +} + +#urlbar:not([actiontype="switchtab"]):not([actiontype="extension"]) > #urlbar-display-box { + display: none; +} + +#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box > #switchtab { + display: none; +} + +#urlbar:not([actiontype="extension"]) > #urlbar-display-box > #extension { + display: none; +} + +#PopupAutoComplete > richlistbox > richlistitem > .ac-type-icon, +#PopupAutoComplete > richlistbox > richlistitem > .ac-site-icon, +#PopupAutoComplete > richlistbox > richlistitem > .ac-tags, +#PopupAutoComplete > richlistbox > richlistitem > .ac-separator, +#PopupAutoComplete > richlistbox > richlistitem > .ac-url { + display: none; +} + +#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] { + -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem-insecure-field"); + height: auto; +} + +#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-site-icon { + display: initial; +} + +#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > .ac-text-overflow-container > .ac-title-text { + text-overflow: initial; + white-space: initial; +} + +#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > label { + margin-inline-start: 0; +} + +#PopupSearchAutoComplete { + -moz-binding: url("chrome://browser/content/search/search.xml#browser-search-autocomplete-result-popup"); +} + +/* Overlay a badge on top of the icon of additional open search providers + in the search panel. */ +.addengine-item > .button-box > .button-icon { + -moz-binding: url("chrome://browser/content/search/search.xml#addengine-icon"); + display: -moz-stack; +} + +#PopupAutoCompleteRichResult { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup"); +} + +#PopupAutoCompleteRichResult.showSearchSuggestionsNotification { + transition: height 100ms; +} + +#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] { + visibility: collapse; + transition: margin-top 100ms; +} + +#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > hbox[anonid="search-suggestions-notification"] { + visibility: visible; +} + +#PopupAutoCompleteRichResult > richlistbox { + transition: height 100ms; +} + +#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > richlistbox { + transition: none; +} + +#DateTimePickerPanel[active="true"] { + -moz-binding: url("chrome://global/content/bindings/datetimepopup.xml#datetime-popup"); +} + +#urlbar[pageproxystate="invalid"] > #urlbar-icons > .urlbar-icon, +#urlbar[pageproxystate="invalid"][focused="true"] > #urlbar-go-button ~ toolbarbutton, +#urlbar[pageproxystate="valid"] > #urlbar-go-button, +#urlbar:not([focused="true"]) > #urlbar-go-button { + visibility: collapse; +} + +#urlbar[pageproxystate="invalid"] > #identity-box > #identity-icon-labels { + visibility: collapse; +} + +#identity-box { + -moz-user-focus: normal; +} + +#urlbar[pageproxystate="invalid"] > #identity-box { + pointer-events: none; + -moz-user-focus: ignore; +} + +#urlbar[pageproxystate="invalid"] > #identity-box > #notification-popup-box { + pointer-events: auto; +} + +#identity-icon-labels { + max-width: 18em; +} +@media (max-width: 700px) { + #urlbar-container { + min-width: 45ch; + } + #identity-icon-labels { + max-width: 70px; + } +} +@media (max-width: 600px) { + #urlbar-container { + min-width: 40ch; + } + #identity-icon-labels { + max-width: 60px; + } +} +@media (max-width: 500px) { + #urlbar-container { + min-width: 35ch; + } + #identity-icon-labels { + max-width: 50px; + } +} +@media (max-width: 400px) { + #urlbar-container { + min-width: 28ch; + } + #identity-icon-labels { + max-width: 40px; + } +} + +#identity-icon-country-label { + direction: ltr; +} + +#identity-box.verifiedIdentity > #identity-icon-labels > #identity-icon-label { + margin-inline-end: 0.25em !important; +} + +#main-window[customizing] :-moz-any(#urlbar, .searchbar-textbox) > .autocomplete-textbox-container > .textbox-input-box { + visibility: hidden; +} + +/* ::::: Unified Back-/Forward Button ::::: */ +#back-button > .toolbarbutton-menu-dropmarker, +#forward-button > .toolbarbutton-menu-dropmarker { + display: none; +} +.unified-nav-current { + font-weight: bold; +} + +toolbarbutton.bookmark-item { + max-width: 13em; +} + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + #alltabs-popup > .menuitem-iconic > .menu-iconic-left > .menu-iconic-icon, + .menuitem-with-favicon > .menu-iconic-left > .menu-iconic-icon { + image-rendering: -moz-crisp-edges; + } + + .bookmark-item > .toolbarbutton-icon, + .bookmark-item > .menu-iconic-left > .menu-iconic-icon, + #personal-bookmarks[cui-areatype="toolbar"] > #bookmarks-toolbar-placeholder > .toolbarbutton-icon { + image-rendering: -moz-crisp-edges; + } + /* Synced Tabs sidebar */ + html|*.tabs-container html|*.item-tabs-list html|*.item-icon-container { + image-rendering: -moz-crisp-edges; + } +} + +#editBMPanel_tagsSelector { + /* override default listbox width from xul.css */ + width: auto; +} + +menupopup[emptyplacesresult="true"] > .hide-if-empty-places-result { + display: none; +} +menuitem.spell-suggestion { + font-weight: bold; +} + +/* Hide extension toolbars that neglected to set the proper class */ +window[chromehidden~="location"][chromehidden~="toolbar"] toolbar:not(.chromeclass-menubar), +window[chromehidden~="toolbar"] toolbar:not(#nav-bar):not(#TabsToolbar):not(#print-preview-toolbar):not(.chromeclass-menubar) { + display: none; +} + +#navigator-toolbox , +#mainPopupSet { + min-width: 1px; +} + +/* History Swipe Animation */ + +#historySwipeAnimationContainer { + overflow: hidden; +} + +#historySwipeAnimationPreviousPage, +#historySwipeAnimationCurrentPage, +#historySwipeAnimationNextPage { + background: none top left no-repeat white; +} + +#historySwipeAnimationPreviousPage { + background-image: -moz-element(#historySwipeAnimationPreviousPageSnapshot); +} + +#historySwipeAnimationCurrentPage { + background-image: -moz-element(#historySwipeAnimationCurrentPageSnapshot); +} + +#historySwipeAnimationNextPage { + background-image: -moz-element(#historySwipeAnimationNextPageSnapshot); +} + +/* Full Screen UI */ + +#fullscr-toggler { + height: 1px; + background: black; +} + +html|*.pointerlockfswarning { + position: fixed; + z-index: 2147483647 !important; + visibility: visible; + transition: transform 300ms ease-in; + /* To center the warning box horizontally, + we use left: 50% with translateX(-50%). */ + top: 0; left: 50%; + transform: translate(-50%, -100%); + box-sizing: border-box; + width: -moz-max-content; + max-width: 95%; + pointer-events: none; +} +html|*.pointerlockfswarning:not([hidden]) { + display: flex; + will-change: transform; +} +html|*.pointerlockfswarning[onscreen] { + transform: translate(-50%, 50px); +} +html|*.pointerlockfswarning[ontop] { + /* Use -10px to hide the border and border-radius on the top */ + transform: translate(-50%, -10px); +} +#main-window[OSXLionFullscreen] html|*.pointerlockfswarning[ontop] { + transform: translate(-50%, 80px); +} + +html|*.pointerlockfswarning-domain-text, +html|*.pointerlockfswarning-generic-text { + word-wrap: break-word; + /* We must specify a min-width, otherwise word-wrap:break-word doesn't work. Bug 630864. */ + min-width: 1px +} +html|*.pointerlockfswarning-domain-text:not([hidden]) + html|*.pointerlockfswarning-generic-text { + display: none; +} + +html|*#fullscreen-exit-button { + pointer-events: auto; +} + +/* ::::: Ctrl-Tab Panel ::::: */ + +.ctrlTab-preview > html|img, +.ctrlTab-preview > html|canvas { + min-width: inherit; + max-width: inherit; + min-height: inherit; + max-height: inherit; +} + +.ctrlTab-favicon-container { + -moz-box-align: start; +%ifdef XP_MACOSX + -moz-box-pack: end; +%else + -moz-box-pack: start; +%endif +} + +.ctrlTab-favicon { + width: 16px; + height: 16px; +} + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + .ctrlTab-favicon { + image-rendering: -moz-crisp-edges; + } +} + +.ctrlTab-preview { + -moz-binding: url("chrome://browser/content/browser-tabPreviews.xml#ctrlTab-preview"); +} + + +/* notification anchors should only be visible when their associated + notifications are */ +.notification-anchor-icon { + -moz-user-focus: normal; +} + +#blocked-permissions-container > .blocked-permission-icon:not([showing]), +.notification-anchor-icon:not([showing]) { + display: none; +} + +#invalid-form-popup > description { + max-width: 280px; +} + +.popup-anchor { + /* should occupy space but not be visible */ + opacity: 0; + pointer-events: none; + -moz-stack-sizing: ignore; +} + +#addon-progress-notification { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#addon-progress-notification"); +} + +#click-to-play-plugins-notification { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#click-to-play-plugins-notification"); +} + + +.plugin-popupnotification-centeritem { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#plugin-popupnotification-center-item"); +} + +browser[tabmodalPromptShowing] { + -moz-user-focus: none !important; +} + +/* Status panel */ + +statuspanel { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#statuspanel"); + position: fixed; + margin-top: -3em; + max-width: calc(100% - 5px); + pointer-events: none; +} + +statuspanel:-moz-locale-dir(ltr)[mirror], +statuspanel:-moz-locale-dir(rtl):not([mirror]) { + left: auto; + right: 0; +} + +statuspanel[sizelimit] { + max-width: 50%; +} + +statuspanel[type=status] { + min-width: 23em; +} + +@media all and (max-width: 800px) { + statuspanel[type=status] { + min-width: 33%; + } +} + +statuspanel[type=overLink] { + transition: opacity 120ms ease-out; + direction: ltr; +} + +statuspanel[inactive] { + transition: none; + opacity: 0; +} + +statuspanel[inactive][previoustype=overLink] { + transition: opacity 200ms ease-out; +} + +.statuspanel-inner { + height: 3em; + width: 100%; + -moz-box-align: end; +} + +/* gcli */ + +html|*#gcli-tooltip-frame, +html|*#gcli-output-frame, +#gcli-output, +#gcli-tooltip { + overflow-x: hidden; +} + +.gclitoolbar-input-node, +.gclitoolbar-complete-node { + direction: ltr; +} + +#developer-toolbar-toolbox-button[error-count] > .toolbarbutton-icon { + display: none; +} + +#developer-toolbar-toolbox-button[error-count]:before { + content: attr(error-count); + display: -moz-box; + -moz-box-pack: center; +} + +/* Responsive Mode */ + +.browserContainer[responsivemode] { + overflow: auto; +} + +.devtools-responsiveui-toolbar:-moz-locale-dir(rtl) { + -moz-box-pack: end; +} + +.browserStack[responsivemode] { + transition-duration: 200ms; + transition-timing-function: linear; +} + +.browserStack[responsivemode] { + transition-property: min-width, max-width, min-height, max-height; +} + +.browserStack[responsivemode][notransition] { + transition: none; +} + +/* Translation */ +notification[value="translation"] { + -moz-binding: url("chrome://browser/content/translation-infobar.xml#translationbar"); +} + +/** See bug 872317 for why the following rule is necessary. */ + +#downloads-button { + -moz-binding: url("chrome://browser/content/downloads/download.xml#download-toolbarbutton"); +} + +/*** Visibility of downloads indicator controls ***/ + +/* Bug 924050: If we've loaded the indicator, for now we hide it in the menu panel, + and just show the icon. This is a hack to side-step very weird layout bugs that + seem to be caused by the indicator stack interacting with the menu panel. */ +#downloads-button[indicator]:not([cui-areatype="menu-panel"]) > .toolbarbutton-badge-stack > image.toolbarbutton-icon, +#downloads-button[indicator][cui-areatype="menu-panel"] > #downloads-indicator-anchor { + display: none; +} + +toolbarpaletteitem[place="palette"] > #downloads-button[indicator] > .toolbarbutton-badge-stack > image.toolbarbutton-icon { + display: -moz-box; +} + +toolbarpaletteitem[place="palette"] > #downloads-button[indicator] > #downloads-indicator-anchor { + display: none; +} + +#downloads-button:-moz-any([progress], [counter], [paused]) #downloads-indicator-icon, +#downloads-button:not(:-moz-any([progress], [counter], [paused])) + #downloads-indicator-progress-area +{ + visibility: hidden; +} + +/* Combobox dropdown renderer */ +#ContentSelectDropdown > menupopup { + /* The menupopup itself should always be rendered LTR to ensure the scrollbar aligns with + * the dropdown arrow on the dropdown widget. If a menuitem is RTL, its style will be set accordingly */ + direction: ltr; +} + +/* Indent options in optgroups */ +.contentSelectDropdown-ingroup .menu-iconic-text { + padding-inline-start: 2em; +} + +/* Give this menupopup an arrow panel styling */ +#BMB_bookmarksPopup { + -moz-appearance: none; + -moz-binding: url("chrome://browser/content/places/menu.xml#places-popup-arrow"); + background: transparent; + border: none; + /* The popup inherits -moz-image-region from the button, must reset it */ + -moz-image-region: auto; +} + +%ifndef MOZ_WIDGET_GTK + +#BMB_bookmarksPopup { + transform: scale(.4); + opacity: 0; + transition-property: transform, opacity; + transition-duration: 0.15s; + transition-timing-function: ease-out; +} + +#BMB_bookmarksPopup[animate="open"] { + transform: none; + opacity: 1.0; +} + +#BMB_bookmarksPopup[animate="cancel"] { + transform: none; +} + +#BMB_bookmarksPopup[arrowposition="after_start"]:-moz-locale-dir(ltr), +#BMB_bookmarksPopup[arrowposition="after_end"]:-moz-locale-dir(rtl) { + transform-origin: 20px top; +} + +#BMB_bookmarksPopup[arrowposition="after_end"]:-moz-locale-dir(ltr), +#BMB_bookmarksPopup[arrowposition="after_start"]:-moz-locale-dir(rtl) { + transform-origin: calc(100% - 20px) top; +} + +#BMB_bookmarksPopup[arrowposition="before_start"]:-moz-locale-dir(ltr), +#BMB_bookmarksPopup[arrowposition="before_end"]:-moz-locale-dir(rtl) { + transform-origin: 20px bottom; +} + +#BMB_bookmarksPopup[arrowposition="before_end"]:-moz-locale-dir(ltr), +#BMB_bookmarksPopup[arrowposition="before_start"]:-moz-locale-dir(rtl) { + transform-origin: calc(100% - 20px) bottom; +} + +%endif + +/* Customize mode */ +#navigator-toolbox, +#browser-bottombox, +#content-deck { + transition-property: margin-left, margin-right; + transition-duration: 200ms; + transition-timing-function: linear; +} + +#tab-view-deck[fastcustomizeanimation] #navigator-toolbox, +#tab-view-deck[fastcustomizeanimation] #content-deck { + transition-duration: 1ms; + transition-timing-function: linear; +} + +#PanelUI-contents > .panel-customization-placeholder > .panel-customization-placeholder-child { + list-style-image: none; +} + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + #PanelUI-remotetabs-tabslist > toolbarbutton > .toolbarbutton-icon, + #PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon, + #PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon, + #PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon { + image-rendering: -moz-crisp-edges; + } +} + +#customization-panelHolder { + overflow-y: hidden; +} + +#customization-panelWrapper, +#customization-panelWrapper > .panel-arrowcontent { + -moz-box-flex: 1; +} + +#customization-panelWrapper > .panel-arrowcontent { + padding: 0 !important; + overflow: hidden; +} + +#customization-panelHolder > #PanelUI-mainView { + display: flex; + flex-direction: column; + /* Hack alert - by manually setting the preferred height to 0, we convince + #PanelUI-mainView to shrink when the window gets smaller in customization + mode. Not sure why that is - might have to do with our intermingling of + XUL flex, and CSS3 Flexbox. */ + height: 0; +} + +#customization-panelHolder > #PanelUI-mainView > #PanelUI-contents-scroller { + display: flex; + flex: auto; + flex-direction: column; +} + +#customization-panel-container { + overflow-y: auto; +} + +toolbarpaletteitem[dragover] { + border-left-color: transparent; + border-right-color: transparent; +} + +#customization-palette-container { + display: flex; + flex-direction: column; +} + +#customization-palette:not([hidden]) { + display: block; + flex: 1 1 auto; + overflow: auto; + min-height: 3em; +} + +#customization-footer-spacer, +#customization-spacer { + flex: 1 1 auto; +} + +#customization-footer { + display: flex; + flex-shrink: 0; + flex-wrap: wrap; +} + +#customization-toolbar-visibility-button > .box-inherit > .button-menu-dropmarker { + display: -moz-box; +} + +toolbarpaletteitem[place="palette"] { + width: 10em; + /* icon (32) + margin (2 * 4) + button padding/border (2 * 4) + label margin (~2) + label + * line-height (1.5em): */ + height: calc(50px + 1.5em); + margin-bottom: 5px; + overflow: hidden; + display: inline-block; +} + +toolbarpaletteitem[place="palette"][hidden] { + display: none; +} + +#customization-palette .toolbarpaletteitem-box { + -moz-box-pack: center; + -moz-box-flex: 1; + width: 10em; + max-width: 10em; +} + +#main-window[customizing=true] #PanelUI-update-status { + display: none; +} + +/* Combined context-menu items */ +#context-navigation > .menuitem-iconic > .menu-iconic-text, +#context-navigation > .menuitem-iconic > .menu-accel-container { + display: none; +} + +.popup-notification-invalid-input { + box-shadow: 0 0 1.5px 1px red; +} + +.popup-notification-invalid-input[focused] { + box-shadow: 0 0 2px 2px rgba(255,0,0,0.4); +} + +.dragfeedback-tab { + -moz-appearance: none; + opacity: 0.65; + -moz-window-shadow: none; +} diff --git a/application/basilisk/base/content/browser.js b/application/basilisk/base/content/browser.js new file mode 100644 index 000000000..64c0d86f5 --- /dev/null +++ b/application/basilisk/base/content/browser.js @@ -0,0 +1,8177 @@ +/* -*- 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"], + ["BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.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:SendSSLErrorReport", this); + mm.addMessageListener("Browser:SetSSLErrorReportAuto", 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:SendSSLErrorReport", this); + mm.removeMessageListener("Browser:SetSSLErrorReportAuto", 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:SendSSLErrorReport": + this.onSSLErrorReport(msg.target, + msg.data.uri, + msg.data.securityInfo); + 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) { + if (!Services.prefs.getBoolPref("security.ssl.errorReporting.enabled")) { + Cu.reportError("User requested certificate error report sending, but certificate error reporting is disabled"); + return; + } + + let serhelper = Cc["@mozilla.org/network/serialization-helper;1"] + .getService(Ci.nsISerializationHelper); + let transportSecurityInfo = serhelper.deserializeObject(securityInfo); + transportSecurityInfo.QueryInterface(Ci.nsITransportSecurityInfo) + + let errorReporter = Cc["@mozilla.org/securityreporter;1"] + .getService(Ci.nsISecurityReporter); + errorReporter.reportTLSError(transportSecurityInfo, + uri.host, uri.port); + }, + + 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); + }, + + _getSearchEngineId: function (engine) { + if (engine && engine.identifier) { + return engine.identifier; + } + + if (!engine || (engine.name === undefined) || + !Services.prefs.getBoolPref("toolkit.telemetry.enabled")) + return "other"; + + return "other-" + engine.name; + }, + + /** + * 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={}) { + BrowserUITelemetry.countSearchEvent(source, null, details.selection); + 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) { + let id = this._getSearchEngineId(engine) + "." + source; + BrowserUITelemetry.countOneoffSearchEvent(id, 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; + }, +}; diff --git a/application/basilisk/base/content/browser.xul b/application/basilisk/base/content/browser.xul new file mode 100644 index 000000000..028df609f --- /dev/null +++ b/application/basilisk/base/content/browser.xul @@ -0,0 +1,1070 @@ +#filter substitution +<?xml version="1.0"?> +# -*- Mode: HTML -*- +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/usercontext/usercontext.css" type="text/css"?> +#ifdef MOZ_DEVTOOLS +<?xml-stylesheet href="chrome://devtools/skin/devtools-browser.css" type="text/css"?> +#endif +<?xml-stylesheet href="chrome://browser/skin/controlcenter/panel.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/customizableui/panelUI.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/browser-lightweightTheme.css" type="text/css"?> + +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +# All DTD information is stored in a separate file so that it can be shared by +# hiddenWindow.xul. +#include browser-doctype.inc + +<window id="main-window" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="gBrowserInit.onLoad()" onunload="gBrowserInit.onUnload()" onclose="return WindowIsClosing();" + title="&mainWindow.title;" + title_normal="&mainWindow.title;" +#ifdef XP_MACOSX + title_privatebrowsing="&mainWindow.title;&mainWindow.titlemodifiermenuseparator;&mainWindow.titlePrivateBrowsingSuffix;" + titledefault="&mainWindow.title;" + titlemodifier="" + titlemodifier_normal="" + titlemodifier_privatebrowsing="&mainWindow.titlePrivateBrowsingSuffix;" +#else + title_privatebrowsing="&mainWindow.titlemodifier; &mainWindow.titlePrivateBrowsingSuffix;" + titlemodifier="&mainWindow.titlemodifier;" + titlemodifier_normal="&mainWindow.titlemodifier;" + titlemodifier_privatebrowsing="&mainWindow.titlemodifier; &mainWindow.titlePrivateBrowsingSuffix;" +#endif +#ifdef CAN_DRAW_IN_TITLEBAR +#ifdef XP_WIN + chromemargin="0,2,2,2" +#else + chromemargin="0,-1,-1,-1" +#endif + tabsintitlebar="true" +#endif + titlemenuseparator="&mainWindow.titlemodifiermenuseparator;" + lightweightthemes="true" + lightweightthemesfooter="browser-bottombox" + windowtype="navigator:browser" + macanimationtype="document" + screenX="4" screenY="4" + fullscreenbutton="true" + sizemode="normal" + retargetdocumentfocus="urlbar" + persist="screenX screenY width height sizemode"> + +# All JS files which are not content (only) dependent that browser.xul +# wishes to include *must* go into the global-scripts.inc file +# so that they can be shared by macBrowserOverlay.xul. +#include global-scripts.inc +<script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/> + +<script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + +<script type="application/javascript" src="chrome://browser/content/downloads/downloads.js"/> +<script type="application/javascript" src="chrome://browser/content/downloads/indicator.js"/> +<script type="application/javascript" src="chrome://browser/content/places/editBookmarkOverlay.js"/> + +# All sets except for popupsets (commands, keys, stringbundles and broadcasters) *must* go into the +# browser-sets.inc file for sharing with hiddenWindow.xul. +#define FULL_BROWSER_WINDOW +#include browser-sets.inc +#undef FULL_BROWSER_WINDOW + + <popupset id="mainPopupSet"> + <menupopup id="tabContextMenu" + onpopupshowing="if (event.target == this) TabContextMenu.updateContextMenu(this);" + onpopuphidden="if (event.target == this) TabContextMenu.contextTab = null;"> + <menuitem id="context_reloadTab" label="&reloadTab.label;" accesskey="&reloadTab.accesskey;" + oncommand="gBrowser.reloadTab(TabContextMenu.contextTab);"/> + <menuitem id="context_toggleMuteTab" oncommand="TabContextMenu.contextTab.toggleMuteAudio();"/> + <menuseparator/> + <menuitem id="context_pinTab" label="&pinTab.label;" + accesskey="&pinTab.accesskey;" + oncommand="gBrowser.pinTab(TabContextMenu.contextTab);"/> + <menuitem id="context_unpinTab" label="&unpinTab.label;" hidden="true" + accesskey="&unpinTab.accesskey;" + oncommand="gBrowser.unpinTab(TabContextMenu.contextTab);"/> + <menuitem id="context_openTabInWindow" label="&moveToNewWindow.label;" + accesskey="&moveToNewWindow.accesskey;" + tbattr="tabbrowser-multiple" + oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/> + <menuseparator id="context_sendTabToDevice_separator" hidden="true"/> + <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;" + accesskey="&sendTabToDevice.accesskey;" hidden="true"> + <menupopup id="context_sendTabToDevicePopupMenu" + onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/> + </menu> + <menuseparator/> + <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;" + tbattr="tabbrowser-multiple-visible" + oncommand="gBrowser.reloadAllTabs();"/> + <menuitem id="context_bookmarkAllTabs" + label="&bookmarkAllTabs.label;" + accesskey="&bookmarkAllTabs.accesskey;" + command="Browser:BookmarkAllTabs"/> + <menuitem id="context_closeTabsToTheEnd" label="&closeTabsToTheEnd.label;" accesskey="&closeTabsToTheEnd.accesskey;" + oncommand="gBrowser.removeTabsToTheEndFrom(TabContextMenu.contextTab, {animate: true});"/> + <menuitem id="context_closeOtherTabs" label="&closeOtherTabs.label;" accesskey="&closeOtherTabs.accesskey;" + oncommand="gBrowser.removeAllTabsBut(TabContextMenu.contextTab);"/> + <menuseparator/> + <menuitem id="context_undoCloseTab" + label="&undoCloseTab.label;" + accesskey="&undoCloseTab.accesskey;" + observes="History:UndoCloseTab"/> + <menuitem id="context_closeTab" label="&closeTab.label;" accesskey="&closeTab.accesskey;" + oncommand="gBrowser.removeTab(TabContextMenu.contextTab, { animate: true });"/> + </menupopup> + + <!-- bug 415444/582485: event.stopPropagation is here for the cloned version + of this menupopup --> + <menupopup id="backForwardMenu" + onpopupshowing="return FillHistoryMenu(event.target);" + oncommand="gotoHistoryIndex(event); event.stopPropagation();" + onclick="checkForMiddleClick(this, event);"/> + <tooltip id="aHTMLTooltip" page="true"/> + <tooltip id="remoteBrowserTooltip"/> + + <!-- for search and content formfill/pw manager --> + + <panel type="autocomplete-richlistbox" + id="PopupAutoComplete" + noautofocus="true" + hidden="true" + overflowpadding="4" + norolluponanchor="true" /> + + <!-- for search with one-off buttons --> + <panel type="autocomplete" id="PopupSearchAutoComplete" noautofocus="true" hidden="true"/> + + <!-- for url bar autocomplete --> + <panel type="autocomplete-richlistbox" + id="PopupAutoCompleteRichResult" + noautofocus="true" + hidden="true" + flip="none" + level="parent" + overflowpadding="30" /> + + <!-- for date/time picker. consumeoutsideclicks is set to never, so that + clicks on the anchored input box are never consumed. --> + <panel id="DateTimePickerPanel" + type="arrow" + hidden="true" + orient="vertical" + noautofocus="true" + norolluponanchor="true" + consumeoutsideclicks="never" + level="parent" + tabspecific="true"> + <iframe id="dateTimePopupFrame"/> + </panel> + + <!-- for select dropdowns. The menupopup is what shows the list of options, + and the popuponly menulist makes things like the menuactive attributes + work correctly on the menupopup. ContentSelectDropdown expects the + popuponly menulist to be its immediate parent. --> + <menulist popuponly="true" id="ContentSelectDropdown" hidden="true"> + <menupopup rolluponmousewheel="true" + activateontab="true" position="after_start" +#ifdef XP_WIN + consumeoutsideclicks="false" ignorekeys="shortcuts" +#endif + /> + </menulist> + + <!-- for invalid form error message --> + <panel id="invalid-form-popup" type="arrow" orient="vertical" noautofocus="true" hidden="true" level="parent"> + <description/> + </panel> + + <panel id="editBookmarkPanel" + type="arrow" + orient="vertical" + ignorekeys="true" + hidden="true" + tabspecific="true" + onpopupshown="StarUI.panelShown(event);" + aria-labelledby="editBookmarkPanelTitle"> + <row id="editBookmarkPanelHeader" align="center" hidden="true"> + <vbox align="center"> + <image id="editBookmarkPanelStarIcon"/> + </vbox> + <vbox> + <label id="editBookmarkPanelTitle"/> + <description id="editBookmarkPanelDescription"/> + </vbox> + </row> + <vbox id="editBookmarkPanelContent" flex="1" hidden="true"/> + <hbox id="editBookmarkPanelBottomButtons" pack="end"> +#ifndef XP_UNIX + <button id="editBookmarkPanelDoneButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.done.label;" + default="true" + oncommand="StarUI.panel.hidePopup();"/> + <button id="editBookmarkPanelRemoveButton" + class="editBookmarkPanelBottomButton" + oncommand="StarUI.removeBookmarkButtonCommand();" + accesskey="&editBookmark.removeBookmark.accessKey;"/> +#else + <button id="editBookmarkPanelRemoveButton" + class="editBookmarkPanelBottomButton" + oncommand="StarUI.removeBookmarkButtonCommand();" + accesskey="&editBookmark.removeBookmark.accessKey;"/> + <button id="editBookmarkPanelDoneButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.done.label;" + default="true" + oncommand="StarUI.panel.hidePopup();"/> +#endif + </hbox> + </panel> + + <menupopup id="toolbar-context-menu" + onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator'));"> + <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)" + accesskey="&customizeMenu.moveToPanel.accesskey;" + label="&customizeMenu.moveToPanel.label;" + contexttype="toolbaritem" + class="customize-context-moveToPanel"/> + <menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode)" + accesskey="&customizeMenu.removeFromToolbar.accesskey;" + label="&customizeMenu.removeFromToolbar.label;" + contexttype="toolbaritem" + class="customize-context-removeFromToolbar"/> + <menuitem id="toolbar-context-reloadAllTabs" + class="toolbaritem-tabsmenu" + contexttype="tabbar" + oncommand="gBrowser.reloadAllTabs();" + label="&toolbarContextMenu.reloadAllTabs.label;" + accesskey="&toolbarContextMenu.reloadAllTabs.accesskey;"/> + <menuitem id="toolbar-context-bookmarkAllTabs" + class="toolbaritem-tabsmenu" + contexttype="tabbar" + command="Browser:BookmarkAllTabs" + label="&toolbarContextMenu.bookmarkAllTabs.label;" + accesskey="&toolbarContextMenu.bookmarkAllTabs.accesskey;"/> + <menuitem id="toolbar-context-undoCloseTab" + class="toolbaritem-tabsmenu" + contexttype="tabbar" + label="&toolbarContextMenu.undoCloseTab.label;" + accesskey="&toolbarContextMenu.undoCloseTab.accesskey;" + observes="History:UndoCloseTab"/> + <menuseparator/> + <menuseparator id="viewToolbarsMenuSeparator"/> + <!-- XXXgijs: we're using oncommand handler here to avoid the event being + redirected to the command element, thus preventing + listeners on the menupopup or further up the tree from + seeing the command event pass by. The observes attribute is + here so that the menuitem is still disabled and re-enabled + correctly. --> + <menuitem oncommand="BrowserCustomizeToolbar()" + observes="cmd_CustomizeToolbars" + class="viewCustomizeToolbar" + label="&viewCustomizeToolbar.label;" + accesskey="&viewCustomizeToolbar.accesskey;"/> + </menupopup> + + <menupopup id="blockedPopupOptions" + onpopupshowing="gPopupBlockerObserver.fillPopupList(event);" + onpopuphiding="gPopupBlockerObserver.onPopupHiding(event);"> + <menuitem observes="blockedPopupAllowSite"/> + <menuitem observes="blockedPopupEditSettings"/> + <menuitem observes="blockedPopupDontShowMessage"/> + <menuseparator observes="blockedPopupsSeparator"/> + </menupopup> + + <menupopup id="autohide-context" + onpopupshowing="FullScreen.getAutohide(this.firstChild);"> + <menuitem type="checkbox" label="&fullScreenAutohide.label;" + accesskey="&fullScreenAutohide.accesskey;" + oncommand="FullScreen.setAutohide();"/> + <menuseparator/> + <menuitem label="&fullScreenExit.label;" + accesskey="&fullScreenExit.accesskey;" + oncommand="BrowserFullScreen();"/> + </menupopup> + + <menupopup id="contentAreaContextMenu" pagemenu="#page-menu-separator" + onpopupshowing="if (event.target != this) + return true; + gContextMenu = new nsContextMenu(this, event.shiftKey); + if (gContextMenu.shouldDisplay) + updateEditUIVisibility(); + return gContextMenu.shouldDisplay;" + onpopuphiding="if (event.target != this) + return; + gContextMenu.hiding(); + gContextMenu = null; + updateEditUIVisibility();"> +#include browser-context.inc + </menupopup> + + <menupopup id="placesContext"> + <menuseparator id="placesContext_recentlyBookmarkedSeparator" + ignoreitem="true" + hidden="true"/> + <menuitem id="placesContext_hideRecentlyBookmarked" + label="&hideRecentlyBookmarked.label;" + accesskey="&hideRecentlyBookmarked.accesskey;" + oncommand="BookmarkingUI.hideRecentlyBookmarked();" + closemenu="single" + ignoreitem="true" + hidden="true"/> + <menuitem id="placesContext_showRecentlyBookmarked" + label="&showRecentlyBookmarked.label;" + accesskey="&showRecentlyBookmarked.accesskey;" + oncommand="BookmarkingUI.showRecentlyBookmarked();" + closemenu="single" + ignoreitem="true" + hidden="true"/> + </menupopup> + + <panel id="ctrlTab-panel" hidden="true" norestorefocus="true" level="top"> + <hbox> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + <button class="ctrlTab-preview" flex="1"/> + </hbox> + <hbox pack="center"> + <button id="ctrlTab-showAll" class="ctrlTab-preview" noicon="true"/> + </hbox> + </panel> + + <!-- Bookmarks and history tooltip --> + <tooltip id="bhTooltip"/> + + <tooltip id="tabbrowser-tab-tooltip" onpopupshowing="gBrowser.createTooltip(event);"/> + + <tooltip id="back-button-tooltip"> + <label class="tooltip-label" value="&backButton.tooltip;"/> +#ifdef XP_MACOSX + <label class="tooltip-label" value="&backForwardButtonMenuMac.tooltip;"/> +#else + <label class="tooltip-label" value="&backForwardButtonMenu.tooltip;"/> +#endif + </tooltip> + + <tooltip id="forward-button-tooltip"> + <label class="tooltip-label" value="&forwardButton.tooltip;"/> +#ifdef XP_MACOSX + <label class="tooltip-label" value="&backForwardButtonMenuMac.tooltip;"/> +#else + <label class="tooltip-label" value="&backForwardButtonMenu.tooltip;"/> +#endif + </tooltip> + +#include popup-notifications.inc + +#include ../../components/customizableui/content/panelUI.inc.xul +#include ../../components/controlcenter/content/panel.inc.xul + + <hbox id="downloads-animation-container" mousethrough="always"> + <vbox id="downloads-notification-anchor"> + <vbox id="downloads-indicator-notification"/> + </vbox> + </hbox> + + <hbox id="bookmarked-notification-container" mousethrough="always"> + <vbox id="bookmarked-notification-anchor"> + <vbox id="bookmarked-notification"/> + </vbox> + <vbox id="bookmarked-notification-dropmarker-anchor"> + <image id="bookmarked-notification-dropmarker-icon"/> + </vbox> + </hbox> + + <tooltip id="dynamic-shortcut-tooltip" + onpopupshowing="UpdateDynamicShortcutTooltipText(this);"/> + + <menupopup id="SyncedTabsSidebarContext"> + <menuitem label="&syncedTabs.context.open.label;" + accesskey="&syncedTabs.context.open.accesskey;" + id="syncedTabsOpenSelected" where="current"/> + <menuitem label="&syncedTabs.context.openInNewTab.label;" + accesskey="&syncedTabs.context.openInNewTab.accesskey;" + id="syncedTabsOpenSelectedInTab" where="tab"/> + <menuitem label="&syncedTabs.context.openInNewWindow.label;" + accesskey="&syncedTabs.context.openInNewWindow.accesskey;" + id="syncedTabsOpenSelectedInWindow" where="window"/> + <menuitem label="&syncedTabs.context.openInNewPrivateWindow.label;" + accesskey="&syncedTabs.context.openInNewPrivateWindow.accesskey;" + id="syncedTabsOpenSelectedInPrivateWindow" where="window" private="true"/> + <menuseparator/> + <menuitem label="&syncedTabs.context.bookmarkSingleTab.label;" + accesskey="&syncedTabs.context.bookmarkSingleTab.accesskey;" + id="syncedTabsBookmarkSelected"/> + <menuitem label="&syncedTabs.context.copy.label;" + accesskey="&syncedTabs.context.copy.accesskey;" + id="syncedTabsCopySelected"/> + <menuseparator/> + <menuitem label="&syncSyncNowItem.label;" + accesskey="&syncSyncNowItem.accesskey;" + id="syncedTabsRefresh"/> + </menupopup> + <menupopup id="SyncedTabsSidebarTabsFilterContext" + class="textbox-contextmenu"> + <menuitem label="&undoCmd.label;" + accesskey="&undoCmd.accesskey;" + cmd="cmd_undo"/> + <menuseparator/> + <menuitem label="&cutCmd.label;" + accesskey="&cutCmd.accesskey;" + cmd="cmd_cut"/> + <menuitem label="©Cmd.label;" + accesskey="©Cmd.accesskey;" + cmd="cmd_copy"/> + <menuitem label="&pasteCmd.label;" + accesskey="&pasteCmd.accesskey;" + cmd="cmd_paste"/> + <menuitem label="&deleteCmd.label;" + accesskey="&deleteCmd.accesskey;" + cmd="cmd_delete"/> + <menuseparator/> + <menuitem label="&selectAllCmd.label;" + accesskey="&selectAllCmd.accesskey;" + cmd="cmd_selectAll"/> + <menuseparator/> + <menuitem label="&syncSyncNowItem.label;" + accesskey="&syncSyncNowItem.accesskey;" + id="syncedTabsRefreshFilter"/> + </menupopup> + </popupset> + +#ifdef CAN_DRAW_IN_TITLEBAR +<vbox id="titlebar"> + <hbox id="titlebar-content"> + <spacer id="titlebar-spacer" flex="1"/> + <hbox id="titlebar-buttonbox-container"> +#ifdef XP_WIN + <hbox id="private-browsing-indicator-titlebar"> + <hbox class="private-browsing-indicator"/> + </hbox> +#endif + <hbox id="titlebar-buttonbox"> + <toolbarbutton class="titlebar-button" id="titlebar-min" oncommand="window.minimize();"/> + <toolbarbutton class="titlebar-button" id="titlebar-max" oncommand="onTitlebarMaxClick();"/> + <toolbarbutton class="titlebar-button" id="titlebar-close" command="cmd_closeWindow"/> + </hbox> + </hbox> +#ifdef XP_MACOSX + <!-- OS X does not natively support RTL for its titlebar items, so we prevent this secondary + buttonbox from reversing order in RTL by forcing an LTR direction. --> + <hbox id="titlebar-secondary-buttonbox" dir="ltr"> + <hbox class="private-browsing-indicator"/> + <hbox id="titlebar-fullscreen-button"/> + </hbox> +#endif + </hbox> +</vbox> +#endif + +<deck flex="1" id="tab-view-deck"> +<vbox flex="1" id="browser-panel"> + + <toolbox id="navigator-toolbox" mode="icons"> + <!-- Menu --> + <toolbar type="menubar" id="toolbar-menubar" class="chromeclass-menubar" customizable="true" + mode="icons" iconsize="small" +#ifdef MENUBAR_CAN_AUTOHIDE + toolbarname="&menubarCmd.label;" + accesskey="&menubarCmd.accesskey;" + autohide="true" +#endif + context="toolbar-context-menu"> + <toolbaritem id="menubar-items" align="center"> +# The entire main menubar is placed into browser-menubar.inc, so that it can be shared by +# hiddenWindow.xul. +#include browser-menubar.inc + </toolbaritem> + +#ifdef CAN_DRAW_IN_TITLEBAR +#ifndef XP_MACOSX + <hbox class="titlebar-placeholder" type="caption-buttons" ordinal="1000" + id="titlebar-placeholder-on-menubar-for-caption-buttons" persist="width" + skipintoolbarset="true"/> +#endif +#endif + </toolbar> + + <toolbar id="TabsToolbar" + fullscreentoolbar="true" + customizable="true" + mode="icons" + iconsize="small" + aria-label="&tabsToolbar.label;" + context="toolbar-context-menu" + collapsed="true"> + +#if defined(MOZ_WIDGET_GTK) + <hbox id="private-browsing-indicator" + skipintoolbarset="true"/> +#endif + + <tabs id="tabbrowser-tabs" + class="tabbrowser-tabs" + tabbrowser="content" + flex="1" + setfocus="false" + tooltip="tabbrowser-tab-tooltip" + stopwatchid="FX_TAB_CLICK_MS"> + <tab class="tabbrowser-tab" selected="true" visuallyselected="true" fadein="true"/> + </tabs> + + <toolbarbutton id="new-tab-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&tabCmd.label;" + command="cmd_newNavigatorTab" + onclick="checkForMiddleClick(this, event);" + tooltip="dynamic-shortcut-tooltip" + ondrop="newTabButtonObserver.onDrop(event)" + ondragover="newTabButtonObserver.onDragOver(event)" + ondragenter="newTabButtonObserver.onDragOver(event)" + ondragexit="newTabButtonObserver.onDragExit(event)" + cui-areatype="toolbar" + removable="true"/> + + <toolbarbutton id="alltabs-button" + class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button" + type="menu" + label="&listAllTabs.label;" + tooltiptext="&listAllTabs.label;" + removable="false"> + <menupopup id="alltabs-popup" + position="after_end"> + <menuitem id="alltabs_undoCloseTab" + class="menuitem-iconic" + key="key_undoCloseTab" + label="&undoCloseTab.label;" + observes="History:UndoCloseTab"/> + <menuseparator id="alltabs-popup-separator-1"/> + <menu id="alltabs_containersTab" + label="&newUserContext.label;"> + <menupopup id="alltabs_containersMenuTab" /> + </menu> + <menuseparator id="alltabs-popup-separator-2"/> + </menupopup> + </toolbarbutton> + +#if !defined(MOZ_WIDGET_GTK) + <hbox class="private-browsing-indicator" skipintoolbarset="true"/> +#endif +#ifdef CAN_DRAW_IN_TITLEBAR + <hbox class="titlebar-placeholder" type="caption-buttons" + id="titlebar-placeholder-on-TabsToolbar-for-captions-buttons" persist="width" +#ifndef XP_MACOSX + ordinal="1000" +#endif + skipintoolbarset="true"/> + +#ifdef XP_MACOSX + <hbox class="titlebar-placeholder" type="fullscreen-button" + id="titlebar-placeholder-on-TabsToolbar-for-fullscreen-button" persist="width" + skipintoolbarset="true"/> +#endif +#endif + </toolbar> + + <toolbar id="nav-bar" + aria-label="&navbarCmd.label;" + fullscreentoolbar="true" mode="icons" customizable="true" + iconsize="small" + customizationtarget="nav-bar-customization-target" + overflowable="true" + overflowbutton="nav-bar-overflow-button" + overflowtarget="widget-overflow-list" + overflowpanel="widget-overflow" + context="toolbar-context-menu"> + + <hbox id="nav-bar-customization-target" flex="1"> + <toolbaritem id="urlbar-container" flex="400" persist="width" + removable="false" + class="chromeclass-location" overflows="false"> + <toolbarbutton id="back-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&backCmd.label;" + command="Browser:BackOrBackDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltip="back-button-tooltip" + context="backForwardMenu"/> + <hbox id="urlbar-wrapper" flex="1"> + <toolbarbutton id="forward-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&forwardCmd.label;" + command="Browser:ForwardOrForwardDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltip="forward-button-tooltip" + context="backForwardMenu"/> + <textbox id="urlbar" flex="1" + placeholder="&urlbar.placeholder2;" + type="autocomplete" + autocompletesearch="unifiedcomplete" + autocompletesearchparam="enable-actions" + autocompletepopup="PopupAutoCompleteRichResult" + completeselectedindex="true" + shrinkdelay="250" + tabscrolling="true" + showcommentcolumn="true" + showimagecolumn="true" + enablehistory="true" + maxrows="10" + newlines="stripsurroundingwhitespace" + ontextentered="this.handleCommand(param);" + ontextreverted="return this.handleRevert();" + pageproxystate="invalid"> + <!-- Use onclick instead of normal popup= syntax since the popup + code fires onmousedown, and hence eats our favicon drag events. --> + <box id="identity-box" role="button" + align="center" + aria-label="&urlbar.viewSiteInfo.label;" + onclick="gIdentityHandler.handleIdentityButtonEvent(event);" + onkeypress="gIdentityHandler.handleIdentityButtonEvent(event);" + ondragstart="gIdentityHandler.onDragStart(event);"> + <image id="identity-icon" + consumeanchor="identity-box" + onclick="PageProxyClickHandler(event);"/> + <image id="sharing-icon" mousethrough="always"/> + <box id="blocked-permissions-container" align="center"> + <image data-permission-id="geo" class="blocked-permission-icon geo-icon" role="button" + tooltiptext="&urlbar.geolocationBlocked.tooltip;"/> + <image data-permission-id="desktop-notification" class="blocked-permission-icon desktop-notification-icon" role="button" + tooltiptext="&urlbar.webNotificationsBlocked.tooltip;"/> + <image data-permission-id="camera" class="blocked-permission-icon camera-icon" role="button" + tooltiptext="&urlbar.cameraBlocked.tooltip;"/> + <image data-permission-id="indexedDB" class="blocked-permission-icon indexedDB-icon" role="button" + tooltiptext="&urlbar.indexedDBBlocked.tooltip;"/> + <image data-permission-id="microphone" class="blocked-permission-icon microphone-icon" role="button" + tooltiptext="&urlbar.microphoneBlocked.tooltip;"/> + <image data-permission-id="screen" class="blocked-permission-icon screen-icon" role="button" + tooltiptext="&urlbar.screenBlocked.tooltip;"/> + </box> + <box id="notification-popup-box" + hidden="true" + onmouseover="document.getElementById('identity-icon').classList.add('no-hover');" + onmouseout="document.getElementById('identity-icon').classList.remove('no-hover');" + align="center"> + <image id="default-notification-icon" class="notification-anchor-icon" role="button" + tooltiptext="&urlbar.defaultNotificationAnchor.tooltip;"/> + <image id="geo-notification-icon" class="notification-anchor-icon geo-icon" role="button" + tooltiptext="&urlbar.geolocationNotificationAnchor.tooltip;"/> + <image id="addons-notification-icon" class="notification-anchor-icon install-icon" role="button" + tooltiptext="&urlbar.addonsNotificationAnchor.tooltip;"/> + <image id="indexedDB-notification-icon" class="notification-anchor-icon indexedDB-icon" role="button" + tooltiptext="&urlbar.indexedDBNotificationAnchor.tooltip;"/> + <image id="password-notification-icon" class="notification-anchor-icon login-icon" role="button" + tooltiptext="&urlbar.passwordNotificationAnchor.tooltip;"/> + <image id="plugins-notification-icon" class="notification-anchor-icon plugin-icon" role="button" + tooltiptext="&urlbar.pluginsNotificationAnchor.tooltip;"/> + <image id="web-notifications-notification-icon" class="notification-anchor-icon desktop-notification-icon" role="button" + tooltiptext="&urlbar.webNotificationAnchor.tooltip;"/> + <image id="webRTC-shareDevices-notification-icon" class="notification-anchor-icon camera-icon" role="button" + tooltiptext="&urlbar.webRTCShareDevicesNotificationAnchor.tooltip;"/> + <image id="webRTC-shareMicrophone-notification-icon" class="notification-anchor-icon microphone-icon" role="button" + tooltiptext="&urlbar.webRTCShareMicrophoneNotificationAnchor.tooltip;"/> + <image id="webRTC-shareScreen-notification-icon" class="notification-anchor-icon screen-icon" role="button" + tooltiptext="&urlbar.webRTCShareScreenNotificationAnchor.tooltip;"/> + <image id="servicesInstall-notification-icon" class="notification-anchor-icon service-icon" role="button" + tooltiptext="&urlbar.servicesNotificationAnchor.tooltip;"/> + <image id="translate-notification-icon" class="notification-anchor-icon translation-icon" role="button" + tooltiptext="&urlbar.translateNotificationAnchor.tooltip;"/> + <image id="translated-notification-icon" class="notification-anchor-icon translation-icon in-use" role="button" + tooltiptext="&urlbar.translatedNotificationAnchor.tooltip;"/> + <image id="eme-notification-icon" class="notification-anchor-icon drm-icon" role="button" + tooltiptext="&urlbar.emeNotificationAnchor.tooltip;"/> + </box> + <image id="tracking-protection-icon"/> + <image id="connection-icon"/> + <hbox id="identity-icon-labels"> + <label id="identity-icon-label" class="plain" flex="1"/> + <label id="identity-icon-country-label" class="plain"/> + </hbox> + </box> + <box id="urlbar-display-box" align="center"> + <label id="switchtab" class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/> + <label id="extension" class="urlbar-display urlbar-display-extension" value="&urlbar.extension.label;"/> + </box> + <hbox id="urlbar-icons"> + <image id="page-report-button" + class="urlbar-icon" + hidden="true" + tooltiptext="&pageReportIcon.tooltip;" + onmousedown="gPopupBlockerObserver.onReportButtonMousedown(event);"/> + <image id="reader-mode-button" + class="urlbar-icon" + hidden="true" + onclick="ReaderParent.buttonClick(event);"/> + <toolbarbutton id="urlbar-zoom-button" + onclick="FullZoom.reset();" + tooltiptext="&urlbar.zoomReset.tooltip;" + hidden="true"/> + </hbox> + <hbox id="userContext-icons" hidden="true"> + <label id="userContext-label"/> + <image id="userContext-indicator"/> + </hbox> + <toolbarbutton id="urlbar-go-button" + class="chromeclass-toolbar-additional" + onclick="gURLBar.handleCommand(event);" + tooltiptext="&goEndCap.tooltip;"/> + <toolbarbutton id="urlbar-reload-button" + class="chromeclass-toolbar-additional" + command="Browser:ReloadOrDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltiptext="&reloadButton.tooltip;"/> + <toolbarbutton id="urlbar-stop-button" + class="chromeclass-toolbar-additional" + command="Browser:Stop" + tooltiptext="&stopButton.tooltip;"/> + </textbox> + </hbox> + </toolbaritem> + + <toolbaritem id="search-container" title="&searchItem.title;" + align="center" class="chromeclass-toolbar-additional panel-wide-item" + cui-areatype="toolbar" + flex="100" persist="width" removable="true"> + <searchbar id="searchbar" flex="1"/> + </toolbaritem> + + <toolbarbutton id="bookmarks-menu-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + removable="true" + type="menu-button" + label="&bookmarksMenuButton.label;" + tooltip="dynamic-shortcut-tooltip" + anchor="dropmarker" + ondragenter="PlacesMenuDNDHandler.onDragEnter(event);" + ondragover="PlacesMenuDNDHandler.onDragOver(event);" + ondragleave="PlacesMenuDNDHandler.onDragLeave(event);" + ondrop="PlacesMenuDNDHandler.onDrop(event);" + cui-areatype="toolbar" + oncommand="BookmarkingUI.onCommand(event);"> + <observes element="bookmarkThisPageBroadcaster" attribute="starred"/> + <observes element="bookmarkThisPageBroadcaster" attribute="buttontooltiptext"/> + <menupopup id="BMB_bookmarksPopup" + class="cui-widget-panel cui-widget-panelview cui-widget-panelWithFooter PanelUI-subView" + placespopup="true" + context="placesContext" + openInTabs="children" + oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);" + onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);" + onpopupshowing="BookmarkingUI.onPopupShowing(event); + BookmarkingUI.attachPlacesView(event, this);" + tooltip="bhTooltip" popupsinherittooltip="true"> + <menuitem id="BMB_viewBookmarksSidebar" + class="subviewbutton" + label="&viewBookmarksSidebar2.label;" + type="checkbox" + oncommand="SidebarUI.toggle('viewBookmarksSidebar');"> + <observes element="viewBookmarksSidebar" attribute="checked"/> + </menuitem> + <!-- NB: temporary solution for bug 985024, this should go away soon. --> + <menuitem id="BMB_bookmarksShowAllTop" + class="menuitem-iconic subviewbutton" + label="&showAllBookmarks2.label;" + command="Browser:ShowAllBookmarks" + key="manBookmarkKb"/> + <menuseparator/> + <menuitem label="&recentBookmarks.label;" + id="BMB_recentBookmarks" + disabled="true" + class="menuitem-iconic subviewbutton"/> + <menuseparator/> + <menu id="BMB_bookmarksToolbar" + class="menu-iconic bookmark-item subviewbutton" + label="&personalbarCmd.label;" + container="true"> + <menupopup id="BMB_bookmarksToolbarPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=TOOLBAR', + PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);"> + <menuitem id="BMB_viewBookmarksToolbar" + placesanonid="view-toolbar" + toolbarId="PersonalToolbar" + type="checkbox" + oncommand="onViewToolbarCommand(event)" + label="&viewBookmarksToolbar.label;"/> + <menuseparator/> + <!-- Bookmarks toolbar items --> + </menupopup> + </menu> + <menu id="BMB_unsortedBookmarks" + class="menu-iconic bookmark-item subviewbutton" + label="&bookmarksMenuButton.other.label;" + container="true"> + <menupopup id="BMB_unsortedBookmarksPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=UNFILED_BOOKMARKS', + PlacesUIUtils.getViewForNode(this.parentNode.parentNode).options);"/> + </menu> + <menuseparator/> + <!-- Bookmarks menu items will go here --> + <menuitem id="BMB_bookmarksShowAll" + class="subviewbutton panel-subview-footer" + label="&showAllBookmarks2.label;" + command="Browser:ShowAllBookmarks" + key="manBookmarkKb"/> + </menupopup> + </toolbarbutton> + + <!-- This is a placeholder for the Downloads Indicator. It is visible + during the customization of the toolbar, in the palette, and before + the Downloads Indicator overlay is loaded. --> + <toolbarbutton id="downloads-button" + class="toolbarbutton-1 chromeclass-toolbar-additional badged-button" + key="key_openDownloads" + oncommand="DownloadsIndicatorView.onCommand(event);" + ondrop="DownloadsIndicatorView.onDrop(event);" + ondragover="DownloadsIndicatorView.onDragOver(event);" + ondragenter="DownloadsIndicatorView.onDragOver(event);" + label="&downloads.label;" + removable="true" + cui-areatype="toolbar" + tooltip="dynamic-shortcut-tooltip"/> + + <toolbarbutton id="home-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + removable="true" + label="&homeButton.label;" + ondragover="homeButtonObserver.onDragOver(event)" + ondragenter="homeButtonObserver.onDragOver(event)" + ondrop="homeButtonObserver.onDrop(event)" + ondragexit="homeButtonObserver.onDragExit(event)" + key="goHome" + onclick="BrowserGoHome(event);" + cui-areatype="toolbar" + aboutHomeOverrideTooltip="&abouthome.pageTitle;"/> + </hbox> + + <toolbarbutton id="nav-bar-overflow-button" + class="toolbarbutton-1 chromeclass-toolbar-additional overflow-button" + skipintoolbarset="true" + tooltiptext="&navbarOverflow.label;"/> + + <toolbaritem id="PanelUI-button" + class="chromeclass-toolbar-additional" + removable="false"> + <toolbarbutton id="PanelUI-menu-button" + class="toolbarbutton-1 badged-button" + consumeanchor="PanelUI-button" + label="&brandShortName;" + tooltiptext="&appmenu.tooltip;"/> + </toolbaritem> + + <hbox id="window-controls" hidden="true" pack="end" skipintoolbarset="true" + ordinal="1000"> + <toolbarbutton id="minimize-button" + tooltiptext="&fullScreenMinimize.tooltip;" + oncommand="window.minimize();"/> + + <toolbarbutton id="restore-button" +#ifdef XP_MACOSX +# Prior to 10.7 there wasn't a native fullscreen button so we use #restore-button +# to exit fullscreen and want it to behave like other toolbar buttons. + class="toolbarbutton-1" +#endif + tooltiptext="&fullScreenRestore.tooltip;" + oncommand="BrowserFullScreen();"/> + + <toolbarbutton id="close-button" + tooltiptext="&fullScreenClose.tooltip;" + oncommand="BrowserTryToCloseWindow();"/> + </hbox> + </toolbar> + + <toolbarset id="customToolbars" context="toolbar-context-menu"/> + + <toolbar id="PersonalToolbar" + mode="icons" iconsize="small" + class="chromeclass-directories" + context="toolbar-context-menu" + toolbarname="&personalbarCmd.label;" accesskey="&personalbarCmd.accesskey;" + collapsed="true" + customizable="true"> + <toolbaritem id="personal-bookmarks" + title="&bookmarksToolbarItem.label;" + cui-areatype="toolbar" + removable="true"> + <toolbarbutton id="bookmarks-toolbar-placeholder" + class="toolbarbutton-1" + mousethrough="never" + label="&bookmarksToolbarItem.label;" + oncommand="PlacesToolbarHelper.onPlaceholderCommand();"/> + <hbox flex="1" + id="PlacesToolbar" + context="placesContext" + onclick="BookmarksEventHandler.onClick(event, this._placesView);" + oncommand="BookmarksEventHandler.onCommand(event, this._placesView);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <hbox flex="1"> + <hbox id="PlacesToolbarDropIndicatorHolder" align="center" collapsed="true"> + <image id="PlacesToolbarDropIndicator" + mousethrough="always" + collapsed="true"/> + </hbox> + <scrollbox orient="horizontal" + id="PlacesToolbarItems" + flex="1"/> + <toolbarbutton type="menu" + id="PlacesChevron" + class="chevron" + mousethrough="never" + collapsed="true" + tooltiptext="&bookmarksToolbarChevron.tooltip;" + onpopupshowing="document.getElementById('PlacesToolbar') + ._placesView._onChevronPopupShowing(event);"> + <menupopup id="PlacesChevronPopup" + placespopup="true" + tooltip="bhTooltip" popupsinherittooltip="true" + context="placesContext"/> + </toolbarbutton> + </hbox> + </hbox> + </toolbaritem> + </toolbar> + + <!-- This is a shim which will go away ASAP. See bug 749804 for details --> + <toolbar id="addon-bar" toolbar-delegate="nav-bar" mode="icons" iconsize="small" + customizable="true"> + <hbox id="addonbar-closebutton"/> + <statusbar id="status-bar"/> + </toolbar> + + <toolbarpalette id="BrowserToolbarPalette"> + +# Update primaryToolbarButtons in browser/themes/shared/browser.inc when adding +# or removing default items with the toolbarbutton-1 class. + + <toolbarbutton id="print-button" class="toolbarbutton-1 chromeclass-toolbar-additional" +#ifdef XP_MACOSX + command="cmd_print" + tooltip="dynamic-shortcut-tooltip" +#else + command="cmd_printPreview" + tooltiptext="&printButton.tooltip;" +#endif + label="&printButton.label;"/> + + + <toolbarbutton id="new-window-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&newNavigatorCmd.label;" + command="key_newNavigator" + tooltip="dynamic-shortcut-tooltip" + ondrop="newWindowButtonObserver.onDrop(event)" + ondragover="newWindowButtonObserver.onDragOver(event)" + ondragenter="newWindowButtonObserver.onDragOver(event)" + ondragexit="newWindowButtonObserver.onDragExit(event)"/> + + <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + observes="View:FullScreen" + type="checkbox" + label="&fullScreenCmd.label;" + tooltip="dynamic-shortcut-tooltip"/> + </toolbarpalette> + </toolbox> + + <hbox id="fullscr-toggler" hidden="true"/> + + <deck id="content-deck" flex="1"> + <hbox flex="1" id="browser"> + <vbox id="browser-border-start" hidden="true" layer="true"/> + <vbox id="sidebar-box" hidden="true" class="chromeclass-extrachrome"> + <sidebarheader id="sidebar-header" align="center"> + <label id="sidebar-title" persist="value" flex="1" crop="end" control="sidebar"/> + <image id="sidebar-throbber"/> + <toolbarbutton class="close-icon tabbable" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="SidebarUI.hide();"/> + </sidebarheader> + <browser id="sidebar" flex="1" autoscroll="false" disablehistory="true" disablefullscreen="true" + style="min-width: 14em; width: 18em; max-width: 36em;" tooltip="aHTMLTooltip"/> + </vbox> + + <splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/> + <vbox id="appcontent" flex="1"> + <notificationbox id="high-priority-global-notificationbox" notificationside="top"/> + <tabbrowser id="content" + flex="1" contenttooltip="aHTMLTooltip" + tabcontainer="tabbrowser-tabs" + contentcontextmenu="contentAreaContextMenu" + autocompletepopup="PopupAutoComplete" + selectmenulist="ContentSelectDropdown" + datetimepicker="DateTimePickerPanel"/> + </vbox> + <vbox id="browser-border-end" hidden="true" layer="true"/> + </hbox> +#include ../../components/customizableui/content/customizeMode.inc.xul + </deck> + + <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true"> + <html:div class="pointerlockfswarning-domain-text"> + &fullscreenWarning.beforeDomain.label; + <html:span class="pointerlockfswarning-domain"/> + &fullscreenWarning.afterDomain.label; + </html:div> + <html:div class="pointerlockfswarning-generic-text"> + &fullscreenWarning.generic.label; + </html:div> + <html:button id="fullscreen-exit-button" + onclick="FullScreen.exitDomFullScreen();"> +#ifdef XP_MACOSX + &exitDOMFullscreenMac.button; +#else + &exitDOMFullscreen.button; +#endif + </html:button> + </html:div> + + <html:div id="pointerlock-warning" class="pointerlockfswarning" hidden="true"> + <html:div class="pointerlockfswarning-domain-text"> + &pointerlockWarning.beforeDomain.label; + <html:span class="pointerlockfswarning-domain"/> + &pointerlockWarning.afterDomain.label; + </html:div> + <html:div class="pointerlockfswarning-generic-text"> + &pointerlockWarning.generic.label; + </html:div> + </html:div> + + <vbox id="browser-bottombox" layer="true"> + <notificationbox id="global-notificationbox" notificationside="bottom"/> + </vbox> + + <svg:svg height="0"> +#include tab-shape.inc.svg + <svg:clipPath id="urlbar-back-button-clip-path"> +#ifndef XP_MACOSX + <svg:path d="M -9,-4 l 0,1 a 15 15 0 0,1 0,30 l 0,1 l 10000,0 l 0,-32 l -10000,0 z" /> +#else + <svg:path d="M -11,-5 a 16 16 0 0 1 0,34 l 10000,0 l 0,-34 l -10000,0 z"/> +#endif + </svg:clipPath> +#ifdef XP_WIN + <svg:clipPath id="urlbar-back-button-clip-path-win10"> + <svg:path d="M -6,-2 l 0,1 a 15 15 0 0,1 0,30 l 0,1 l 10000,0 l 0,-32 l -10000,0 z" /> + </svg:clipPath> +#endif + </svg:svg> + +</vbox> +# <iframe id="tab-view"> is dynamically appended as the 2nd child of #tab-view-deck. +# Introducing the iframe dynamically, as needed, was found to be better than +# starting with an empty iframe here in browser.xul from a Ts standpoint. +</deck> + +</window> diff --git a/application/basilisk/base/content/browserMountPoints.inc b/application/basilisk/base/content/browserMountPoints.inc new file mode 100644 index 000000000..e4315b04a --- /dev/null +++ b/application/basilisk/base/content/browserMountPoints.inc @@ -0,0 +1,12 @@ +<stringbundleset id="stringbundleset"/> + +<commandset id="mainCommandSet"/> +<commandset id="baseMenuCommandSet"/> +<commandset id="placesCommands"/> + +<broadcasterset id="mainBroadcasterSet"/> + +<keyset id="mainKeyset"/> +<keyset id="baseMenuKeyset"/> + +<menubar id="main-menubar"/>
\ No newline at end of file diff --git a/application/basilisk/base/content/content.js b/application/basilisk/base/content/content.js new file mode 100644 index 000000000..5758cb023 --- /dev/null +++ b/application/basilisk/base/content/content.js @@ -0,0 +1,1473 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This content script should work in any browser or iframe and should not + * depend on the frame being contained in tabbrowser. */ + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/ContentWebRTC.jsm"); +Cu.import("resource:///modules/ContentObservers.jsm"); +Cu.import("resource://gre/modules/InlineSpellChecker.jsm"); +Cu.import("resource://gre/modules/InlineSpellCheckerContent.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", + "resource:///modules/E10SUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ContentLinkHandler", + "resource:///modules/ContentLinkHandler.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", + "resource://gre/modules/LoginManagerContent.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory", + "resource://gre/modules/LoginManagerContent.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils", + "resource://gre/modules/InsecurePasswordUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PluginContent", + "resource:///modules/PluginContent.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver", + "resource:///modules/FormSubmitObserver.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata", + "resource://gre/modules/PageMetadata.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUIUtils", + "resource:///modules/PlacesUIUtils.jsm"); +XPCOMUtils.defineLazyGetter(this, "PageMenuChild", function() { + let tmp = {}; + Cu.import("resource://gre/modules/PageMenu.jsm", tmp); + return new tmp.PageMenuChild(); +}); +XPCOMUtils.defineLazyModuleGetter(this, "Feeds", + "resource:///modules/Feeds.jsm"); + +Cu.importGlobalProperties(["URL"]); + +// TabChildGlobal +var global = this; + +// Load the form validation popup handler +var formSubmitObserver = new FormSubmitObserver(content, this); + +addMessageListener("ContextMenu:DoCustomCommand", function(message) { + E10SUtils.wrapHandlingUserInput( + content, message.data.handlingUserInput, + () => PageMenuChild.executeMenu(message.data.generatedItemId)); +}); + +addMessageListener("RemoteLogins:fillForm", function(message) { + LoginManagerContent.receiveMessage(message, content); +}); +addEventListener("DOMFormHasPassword", function(event) { + LoginManagerContent.onDOMFormHasPassword(event, content); + let formLike = LoginFormFactory.createFromForm(event.target); + InsecurePasswordUtils.reportInsecurePasswords(formLike); +}); +addEventListener("DOMInputPasswordAdded", function(event) { + LoginManagerContent.onDOMInputPasswordAdded(event, content); + let formLike = LoginFormFactory.createFromField(event.target); + InsecurePasswordUtils.reportInsecurePasswords(formLike); +}); +addEventListener("pageshow", function(event) { + LoginManagerContent.onPageShow(event, content); +}); +addEventListener("DOMAutoComplete", function(event) { + LoginManagerContent.onUsernameInput(event); +}); +addEventListener("blur", function(event) { + LoginManagerContent.onUsernameInput(event); +}); + +var handleContentContextMenu = function (event) { + let defaultPrevented = event.defaultPrevented; + if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) { + let plugin = null; + try { + plugin = event.target.QueryInterface(Ci.nsIObjectLoadingContent); + } catch (e) {} + if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) { + // Don't open a context menu for plugins. + return; + } + + defaultPrevented = false; + } + + if (defaultPrevented) + return; + + let addonInfo = {}; + let subject = { + event: event, + addonInfo: addonInfo, + }; + subject.wrappedJSObject = subject; + Services.obs.notifyObservers(subject, "content-contextmenu", null); + + let doc = event.target.ownerDocument; + let docLocation = doc.mozDocumentURIIfNotForErrorPages; + docLocation = docLocation && docLocation.spec; + let charSet = doc.characterSet; + let baseURI = doc.baseURI; + let referrer = doc.referrer; + let referrerPolicy = doc.referrerPolicy; + let frameOuterWindowID = doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + let loginFillInfo = LoginManagerContent.getFieldContext(event.target); + + // The same-origin check will be done in nsContextMenu.openLinkInTab. + let parentAllowsMixedContent = !!docShell.mixedContentChannel; + + // get referrer attribute from clicked link and parse it + // if per element referrer is enabled, the element referrer overrules + // the document wide referrer + if (Services.prefs.getBoolPref("network.http.enablePerElementReferrer")) { + let referrerAttrValue = Services.netUtils.parseAttributePolicyString(event.target. + getAttribute("referrerpolicy")); + if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) { + referrerPolicy = referrerAttrValue; + } + } + + let disableSetDesktopBg = null; + // Media related cache info parent needs for saving + let contentType = null; + let contentDisposition = null; + if (event.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && + event.target instanceof Ci.nsIImageLoadingContent && + event.target.currentURI) { + disableSetDesktopBg = disableSetDesktopBackground(event.target); + + try { + let imageCache = + Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools) + .getImgCacheForDocument(doc); + let props = + imageCache.findEntryProperties(event.target.currentURI, doc); + try { + contentType = props.get("type", Ci.nsISupportsCString).data; + } catch (e) {} + try { + contentDisposition = + props.get("content-disposition", Ci.nsISupportsCString).data; + } catch (e) {} + } catch (e) {} + } + + let selectionInfo = BrowserUtils.getSelectionDetails(content); + + let loadContext = docShell.QueryInterface(Ci.nsILoadContext); + let userContextId = loadContext.originAttributes.userContextId; + + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + let editFlags = SpellCheckHelper.isEditable(event.target, content); + let spellInfo; + if (editFlags & + (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) { + spellInfo = + InlineSpellCheckerContent.initContextMenu(event, editFlags, this); + } + + // Set the event target first as the copy image command needs it to + // determine what was context-clicked on. Then, update the state of the + // commands on the context menu. + docShell.contentViewer.QueryInterface(Ci.nsIContentViewerEdit) + .setCommandNode(event.target); + event.target.ownerGlobal.updateCommands("contentcontextmenu"); + + let customMenuItems = PageMenuChild.build(event.target); + let principal = doc.nodePrincipal; + sendRpcMessage("contextmenu", + { editFlags, spellInfo, customMenuItems, addonInfo, + principal, docLocation, charSet, baseURI, referrer, + referrerPolicy, contentType, contentDisposition, + frameOuterWindowID, selectionInfo, disableSetDesktopBg, + loginFillInfo, parentAllowsMixedContent, userContextId }, + { event, popupNode: event.target }); + } + else { + // Break out to the parent window and pass the add-on info along + let browser = docShell.chromeEventHandler; + let mainWin = browser.ownerGlobal; + mainWin.gContextMenuContentData = { + isRemote: false, + event: event, + popupNode: event.target, + browser: browser, + addonInfo: addonInfo, + documentURIObject: doc.documentURIObject, + docLocation: docLocation, + charSet: charSet, + referrer: referrer, + referrerPolicy: referrerPolicy, + contentType: contentType, + contentDisposition: contentDisposition, + selectionInfo: selectionInfo, + disableSetDesktopBackground: disableSetDesktopBg, + loginFillInfo, + parentAllowsMixedContent, + userContextId, + }; + } +} + +Cc["@mozilla.org/eventlistenerservice;1"] + .getService(Ci.nsIEventListenerService) + .addSystemEventListener(global, "contextmenu", handleContentContextMenu, false); + +// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json +const TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN = 0; +const TLS_ERROR_REPORT_TELEMETRY_EXPANDED = 1; +const TLS_ERROR_REPORT_TELEMETRY_SUCCESS = 6; +const TLS_ERROR_REPORT_TELEMETRY_FAILURE = 7; + +const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; +const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE; + +const SEC_ERROR_EXPIRED_CERTIFICATE = SEC_ERROR_BASE + 11; +const SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13; +const SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE = SEC_ERROR_BASE + 30; +const SEC_ERROR_OCSP_FUTURE_RESPONSE = SEC_ERROR_BASE + 131; +const SEC_ERROR_OCSP_OLD_RESPONSE = SEC_ERROR_BASE + 132; +const MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 5; +const MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE = MOZILLA_PKIX_ERROR_BASE + 6; + +const PREF_BLOCKLIST_CLOCK_SKEW_SECONDS = "services.blocklist.clock_skew_seconds"; + +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)); +}, []); + + +function getSerializedSecurityInfo(docShell) { + let serhelper = Cc["@mozilla.org/network/serialization-helper;1"] + .getService(Ci.nsISerializationHelper); + + let securityInfo = docShell.failedChannel && docShell.failedChannel.securityInfo; + if (!securityInfo) { + return ""; + } + securityInfo.QueryInterface(Ci.nsITransportSecurityInfo) + .QueryInterface(Ci.nsISerializable); + + return serhelper.serializeToString(securityInfo); +} + +var AboutNetAndCertErrorListener = { + init: function(chromeGlobal) { + addMessageListener("CertErrorDetails", this); + addMessageListener("Browser:CaptivePortalFreed", this); + chromeGlobal.addEventListener('AboutNetErrorLoad', this, false, true); + chromeGlobal.addEventListener('AboutNetErrorOpenCaptivePortal', this, false, true); + chromeGlobal.addEventListener('AboutNetErrorSetAutomatic', this, false, true); + chromeGlobal.addEventListener('AboutNetErrorOverride', this, false, true); + chromeGlobal.addEventListener('AboutNetErrorResetPreferences', this, false, true); + }, + + get isAboutNetError() { + return content.document.documentURI.startsWith("about:neterror"); + }, + + get isAboutCertError() { + return content.document.documentURI.startsWith("about:certerror"); + }, + + receiveMessage: function(msg) { + if (!this.isAboutCertError) { + return; + } + + switch (msg.name) { + case "CertErrorDetails": + this.onCertErrorDetails(msg); + break; + case "Browser:CaptivePortalFreed": + this.onCaptivePortalFreed(msg); + break; + } + }, + + onCertErrorDetails(msg) { + let div = content.document.getElementById("certificateErrorText"); + div.textContent = msg.data.info; + let learnMoreLink = content.document.getElementById("learnMoreLink"); + let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); + + switch (msg.data.code) { + case SEC_ERROR_UNKNOWN_ISSUER: + learnMoreLink.href = baseURL + "security-error"; + break; + + // in case the certificate expired we make sure the system clock + // matches settings server (kinto) time + case SEC_ERROR_EXPIRED_CERTIFICATE: + case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE: + case SEC_ERROR_OCSP_FUTURE_RESPONSE: + case SEC_ERROR_OCSP_OLD_RESPONSE: + case MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE: + case MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE: + + let appBuildId = Services.appinfo.appBuildID; + let year = parseInt(appBuildID.substr(0, 4), 10); + let month = parseInt(appBuildID.substr(4, 2), 10) - 1; + let day = parseInt(appBuildID.substr(6, 2), 10); + let buildDate = new Date(year, month, day); + let systemDate = new Date(); + + // if the difference is more than a day + if (buildDate > systemDate) { + let formatter = new Intl.DateTimeFormat(); + + content.document.getElementById("wrongSystemTime_URL") + .textContent = content.document.location.hostname; + content.document.getElementById("wrongSystemTime_systemDate") + .textContent = formatter.format(systemDate); + + content.document.getElementById("errorShortDesc") + .style.display = "none"; + content.document.getElementById("wrongSystemTimePanel") + .style.display = "block"; + } + learnMoreLink.href = baseURL + "time-errors"; + break; + } + }, + + onCaptivePortalFreed(msg) { + content.dispatchEvent(new content.CustomEvent("AboutNetErrorCaptivePortalFreed")); + }, + + handleEvent: function(aEvent) { + if (!this.isAboutNetError && !this.isAboutCertError) { + return; + } + + switch (aEvent.type) { + case "AboutNetErrorLoad": + this.onPageLoad(aEvent); + break; + case "AboutNetErrorOpenCaptivePortal": + this.openCaptivePortalPage(aEvent); + break; + case "AboutNetErrorSetAutomatic": + this.onSetAutomatic(aEvent); + break; + case "AboutNetErrorOverride": + this.onOverride(aEvent); + break; + case "AboutNetErrorResetPreferences": + this.onResetPreferences(aEvent); + break; + } + }, + + changedCertPrefs: function () { + for (let prefName of PREF_SSL_IMPACT) { + if (Services.prefs.prefHasUserValue(prefName)) { + return true; + } + } + + return false; + }, + + onPageLoad: function(evt) { + if (this.isAboutCertError) { + let originalTarget = evt.originalTarget; + let ownerDoc = originalTarget.ownerDocument; + ClickEventHandler.onCertError(originalTarget, ownerDoc); + } + + let automatic = Services.prefs.getBoolPref("security.ssl.errorReporting.automatic"); + content.dispatchEvent(new content.CustomEvent("AboutNetErrorOptions", { + detail: JSON.stringify({ + enabled: Services.prefs.getBoolPref("security.ssl.errorReporting.enabled"), + changedCertPrefs: this.changedCertPrefs(), + automatic: automatic + }) + })); + + sendAsyncMessage("Browser:SSLErrorReportTelemetry", + {reportStatus: TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN}); + }, + + openCaptivePortalPage: function(evt) { + sendAsyncMessage("Browser:OpenCaptivePortalPage"); + }, + + + onResetPreferences: function(evt) { + sendAsyncMessage("Browser:ResetSSLPreferences"); + }, + + onSetAutomatic: function(evt) { + sendAsyncMessage("Browser:SetSSLErrorReportAuto", { + automatic: evt.detail + }); + + // if we're enabling reports, send a report for this failure + if (evt.detail) { + let {host, port} = content.document.mozDocumentURIIfNotForErrorPages; + sendAsyncMessage("Browser:SendSSLErrorReport", { + uri: { host, port }, + securityInfo: getSerializedSecurityInfo(docShell), + }); + + } + }, + + onOverride: function(evt) { + let {host, port} = content.document.mozDocumentURIIfNotForErrorPages; + sendAsyncMessage("Browser:OverrideWeakCrypto", { uri: {host, port} }); + } +} + +AboutNetAndCertErrorListener.init(this); + + +var ClickEventHandler = { + init: function init() { + Cc["@mozilla.org/eventlistenerservice;1"] + .getService(Ci.nsIEventListenerService) + .addSystemEventListener(global, "click", this, true); + }, + + handleEvent: function(event) { + if (!event.isTrusted || event.defaultPrevented || event.button == 2) { + return; + } + + let originalTarget = event.originalTarget; + let ownerDoc = originalTarget.ownerDocument; + if (!ownerDoc) { + return; + } + + // Handle click events from about pages + if (ownerDoc.documentURI.startsWith("about:certerror")) { + this.onCertError(originalTarget, ownerDoc); + return; + } else if (ownerDoc.documentURI.startsWith("about:blocked")) { + this.onAboutBlocked(originalTarget, ownerDoc); + return; + } else if (ownerDoc.documentURI.startsWith("about:neterror")) { + this.onAboutNetError(event, ownerDoc.documentURI); + return; + } + + let [href, node, principal] = this._hrefAndLinkNodeForClickEvent(event); + + // get referrer attribute from clicked link and parse it + // if per element referrer is enabled, the element referrer overrules + // the document wide referrer + let referrerPolicy = ownerDoc.referrerPolicy; + if (Services.prefs.getBoolPref("network.http.enablePerElementReferrer") && + node) { + let referrerAttrValue = Services.netUtils.parseAttributePolicyString(node. + getAttribute("referrerpolicy")); + if (referrerAttrValue !== Ci.nsIHttpChannel.REFERRER_POLICY_UNSET) { + referrerPolicy = referrerAttrValue; + } + } + + let json = { button: event.button, shiftKey: event.shiftKey, + ctrlKey: event.ctrlKey, metaKey: event.metaKey, + altKey: event.altKey, href: null, title: null, + bookmark: false, referrerPolicy: referrerPolicy, + triggeringPrincipal: principal, + originAttributes: principal ? principal.originAttributes : {}, + isContentWindowPrivate: PrivateBrowsingUtils.isContentWindowPrivate(ownerDoc.defaultView)}; + + if (href) { + try { + BrowserUtils.urlSecurityCheck(href, principal); + } catch (e) { + return; + } + + json.href = href; + if (node) { + json.title = node.getAttribute("title"); + if (event.button == 0 && !event.ctrlKey && !event.shiftKey && + !event.altKey && !event.metaKey) { + json.bookmark = node.getAttribute("rel") == "sidebar"; + if (json.bookmark) { + event.preventDefault(); // Need to prevent the pageload. + } + } + } + json.noReferrer = BrowserUtils.linkHasNoReferrer(node) + + // Check if the link needs to be opened with mixed content allowed. + // Only when the owner doc has |mixedContentChannel| and the same origin + // should we allow mixed content. + json.allowMixedContent = false; + let docshell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + if (docShell.mixedContentChannel) { + const sm = Services.scriptSecurityManager; + try { + let targetURI = BrowserUtils.makeURI(href); + sm.checkSameOriginURI(docshell.mixedContentChannel.URI, targetURI, false); + json.allowMixedContent = true; + } catch (e) {} + } + json.originPrincipal = ownerDoc.nodePrincipal; + json.triggeringPrincipal = ownerDoc.nodePrincipal; + + sendAsyncMessage("Content:Click", json); + return; + } + + // This might be middle mouse navigation. + if (event.button == 1) { + sendAsyncMessage("Content:Click", json); + } + }, + + onCertError: function (targetElement, ownerDoc) { + let docShell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + sendAsyncMessage("Browser:CertExceptionError", { + location: ownerDoc.location.href, + elementId: targetElement.getAttribute("id"), + isTopFrame: (ownerDoc.defaultView.parent === ownerDoc.defaultView), + securityInfoAsString: getSerializedSecurityInfo(docShell), + }); + }, + + onAboutBlocked: function (targetElement, ownerDoc) { + var reason = 'phishing'; + if (/e=malwareBlocked/.test(ownerDoc.documentURI)) { + reason = 'malware'; + } else if (/e=unwantedBlocked/.test(ownerDoc.documentURI)) { + reason = 'unwanted'; + } + sendAsyncMessage("Browser:SiteBlockedError", { + location: ownerDoc.location.href, + reason: reason, + elementId: targetElement.getAttribute("id"), + isTopFrame: (ownerDoc.defaultView.parent === ownerDoc.defaultView) + }); + }, + + onAboutNetError: function (event, documentURI) { + let elmId = event.originalTarget.getAttribute("id"); + if (elmId == "returnButton") { + sendAsyncMessage("Browser:SSLErrorGoBack", {}); + return; + } + if (elmId != "errorTryAgain" || !/e=netOffline/.test(documentURI)) { + return; + } + // browser front end will handle clearing offline mode and refreshing + // the page *if* we're in offline mode now. Otherwise let the error page + // handle the click. + if (Services.io.offline) { + event.preventDefault(); + sendAsyncMessage("Browser:EnableOnlineMode", {}); + } + }, + + /** + * Extracts linkNode and href for the current click target. + * + * @param event + * The click event. + * @return [href, linkNode, linkPrincipal]. + * + * @note linkNode will be null if the click wasn't on an anchor + * element. This includes SVG links, because callers expect |node| + * to behave like an <a> element, which SVG links (XLink) don't. + */ + _hrefAndLinkNodeForClickEvent: function(event) { + function isHTMLLink(aNode) { + // Be consistent with what nsContextMenu.js does. + return ((aNode instanceof content.HTMLAnchorElement && aNode.href) || + (aNode instanceof content.HTMLAreaElement && aNode.href) || + aNode instanceof content.HTMLLinkElement); + } + + let node = event.target; + while (node && !isHTMLLink(node)) { + node = node.parentNode; + } + + if (node) + return [node.href, node, node.ownerDocument.nodePrincipal]; + + // If there is no linkNode, try simple XLink. + let href, baseURI; + node = event.target; + while (node && !href) { + if (node.nodeType == content.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.ownerDocument.baseURIObject; + break; + } + } + node = node.parentNode; + } + + // In case of XLink, we don't return the node we got href from since + // callers expect <a>-like elements. + // Note: makeURI() will throw if aUri is not a valid URI. + return [href ? BrowserUtils.makeURI(href, null, baseURI).spec : null, null, + node && node.ownerDocument.nodePrincipal]; + } +}; +ClickEventHandler.init(); + +ContentLinkHandler.init(this); + +// TODO: Load this lazily so the JSM is run only if a relevant event/message fires. +var pluginContent = new PluginContent(global); + +addEventListener("DOMWebNotificationClicked", function(event) { + sendAsyncMessage("DOMWebNotificationClicked", {}); +}, false); + +addEventListener("DOMServiceWorkerFocusClient", function(event) { + sendAsyncMessage("DOMServiceWorkerFocusClient", {}); +}, false); + +ContentWebRTC.init(); +addMessageListener("rtcpeer:Allow", ContentWebRTC); +addMessageListener("rtcpeer:Deny", ContentWebRTC); +addMessageListener("webrtc:Allow", ContentWebRTC); +addMessageListener("webrtc:Deny", ContentWebRTC); +addMessageListener("webrtc:StopSharing", ContentWebRTC); +addMessageListener("webrtc:StartBrowserSharing", () => { + let windowID = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + sendAsyncMessage("webrtc:response:StartBrowserSharing", { + windowID: windowID + }); +}); + +addEventListener("pageshow", function(event) { + if (event.target == content.document) { + sendAsyncMessage("PageVisibility:Show", { + persisted: event.persisted, + }); + } +}); +addEventListener("pagehide", function(event) { + if (event.target == content.document) { + sendAsyncMessage("PageVisibility:Hide", { + persisted: event.persisted, + }); + } +}); + +var PageMetadataMessenger = { + init() { + addMessageListener("PageMetadata:GetPageData", this); + addMessageListener("PageMetadata:GetMicroformats", this); + }, + receiveMessage(message) { + switch (message.name) { + case "PageMetadata:GetPageData": { + let target = message.objects.target; + let result = PageMetadata.getData(content.document, target); + sendAsyncMessage("PageMetadata:PageDataResult", result); + break; + } + case "PageMetadata:GetMicroformats": { + let target = message.objects.target; + let result = PageMetadata.getMicroformats(content.document, target); + sendAsyncMessage("PageMetadata:MicroformatsResult", result); + break; + } + } + } +} +PageMetadataMessenger.init(); + +addMessageListener("ContextMenu:SaveVideoFrameAsImage", (message) => { + let video = message.objects.target; + let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + let ctxDraw = canvas.getContext("2d"); + ctxDraw.drawImage(video, 0, 0); + sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage:Result", { + dataURL: canvas.toDataURL("image/jpeg", ""), + }); +}); + +addMessageListener("ContextMenu:MediaCommand", (message) => { + E10SUtils.wrapHandlingUserInput( + content, message.data.handlingUserInput, + () => { + let media = message.objects.element; + switch (message.data.command) { + case "play": + media.play(); + break; + case "pause": + media.pause(); + break; + case "loop": + media.loop = !media.loop; + break; + case "mute": + media.muted = true; + break; + case "unmute": + media.muted = false; + break; + case "playbackRate": + media.playbackRate = message.data.data; + break; + case "hidecontrols": + media.removeAttribute("controls"); + break; + case "showcontrols": + media.setAttribute("controls", "true"); + break; + case "fullscreen": + if (content.document.fullscreenEnabled) + media.requestFullscreen(); + break; + } + }); +}); + +addMessageListener("ContextMenu:Canvas:ToBlobURL", (message) => { + message.objects.target.toBlob((blob) => { + let blobURL = URL.createObjectURL(blob); + sendAsyncMessage("ContextMenu:Canvas:ToBlobURL:Result", { blobURL }); + }); +}); + +addMessageListener("ContextMenu:ReloadFrame", (message) => { + message.objects.target.ownerDocument.location.reload(); +}); + +addMessageListener("ContextMenu:ReloadImage", (message) => { + let image = message.objects.target; + if (image instanceof Ci.nsIImageLoadingContent) + image.forceReload(); +}); + +addMessageListener("ContextMenu:BookmarkFrame", (message) => { + let frame = message.objects.target.ownerDocument; + sendAsyncMessage("ContextMenu:BookmarkFrame:Result", + { title: frame.title, + description: PlacesUIUtils.getDescriptionFromDocument(frame) }); +}); + +addMessageListener("ContextMenu:SearchFieldBookmarkData", (message) => { + let node = message.objects.target; + + let charset = node.ownerDocument.characterSet; + + let formBaseURI = BrowserUtils.makeURI(node.form.baseURI, + charset); + + let formURI = BrowserUtils.makeURI(node.form.getAttribute("action"), + charset, + formBaseURI); + + let spec = formURI.spec; + + let isURLEncoded = + (node.form.method.toUpperCase() == "POST" + && (node.form.enctype == "application/x-www-form-urlencoded" || + node.form.enctype == "")); + + let title = node.ownerDocument.title; + let description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument); + + let formData = []; + + function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) { + if (aIsFormUrlEncoded) { + return escape(aName + "=" + aValue); + } + return escape(aName) + "=" + escape(aValue); + } + + for (let el of node.form.elements) { + if (!el.type) // happens with fieldsets + continue; + + if (el == node) { + formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) : + // Don't escape "%s", just append + escapeNameValuePair(el.name, "", false) + "%s"); + continue; + } + + let type = el.type.toLowerCase(); + + if (((el instanceof content.HTMLInputElement && el.mozIsTextField(true)) || + type == "hidden" || type == "textarea") || + ((type == "checkbox" || type == "radio") && el.checked)) { + formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded)); + } else if (el instanceof content.HTMLSelectElement && el.selectedIndex >= 0) { + for (let j=0; j < el.options.length; j++) { + if (el.options[j].selected) + formData.push(escapeNameValuePair(el.name, el.options[j].value, + isURLEncoded)); + } + } + } + + let postData; + + if (isURLEncoded) + postData = formData.join("&"); + else { + let separator = spec.includes("?") ? "&" : "?"; + spec += separator + formData.join("&"); + } + + sendAsyncMessage("ContextMenu:SearchFieldBookmarkData:Result", + { spec, title, description, postData, charset }); +}); + +addMessageListener("Bookmarks:GetPageDetails", (message) => { + let doc = content.document; + let isErrorPage = /^about:(neterror|certerror|blocked)/.test(doc.documentURI); + sendAsyncMessage("Bookmarks:GetPageDetails:Result", + { isErrorPage: isErrorPage, + description: PlacesUIUtils.getDescriptionFromDocument(doc) }); +}); + +var LightWeightThemeWebInstallListener = { + _previewWindow: null, + + init: function() { + addEventListener("InstallBrowserTheme", this, false, true); + addEventListener("PreviewBrowserTheme", this, false, true); + addEventListener("ResetBrowserThemePreview", this, false, true); + }, + + handleEvent: function (event) { + switch (event.type) { + case "InstallBrowserTheme": { + sendAsyncMessage("LightWeightThemeWebInstaller:Install", { + baseURI: event.target.baseURI, + principal: event.target.nodePrincipal, + themeData: event.target.getAttribute("data-browsertheme"), + }); + break; + } + case "PreviewBrowserTheme": { + sendAsyncMessage("LightWeightThemeWebInstaller:Preview", { + baseURI: event.target.baseURI, + principal: event.target.nodePrincipal, + themeData: event.target.getAttribute("data-browsertheme"), + }); + this._previewWindow = event.target.ownerGlobal; + this._previewWindow.addEventListener("pagehide", this, true); + break; + } + case "pagehide": { + sendAsyncMessage("LightWeightThemeWebInstaller:ResetPreview"); + this._resetPreviewWindow(); + break; + } + case "ResetBrowserThemePreview": { + if (this._previewWindow) { + sendAsyncMessage("LightWeightThemeWebInstaller:ResetPreview", + {principal: event.target.nodePrincipal}); + this._resetPreviewWindow(); + } + break; + } + } + }, + + _resetPreviewWindow: function () { + this._previewWindow.removeEventListener("pagehide", this, true); + this._previewWindow = null; + } +}; + +LightWeightThemeWebInstallListener.init(); + +function disableSetDesktopBackground(aTarget) { + // Disable the Set as Desktop Background menu item if we're still trying + // to load the image or the load failed. + if (!(aTarget instanceof Ci.nsIImageLoadingContent)) + return true; + + if (("complete" in aTarget) && !aTarget.complete) + return true; + + if (aTarget.currentURI.schemeIs("javascript")) + return true; + + let request = aTarget.QueryInterface(Ci.nsIImageLoadingContent) + .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); + if (!request) + return true; + + return false; +} + +addMessageListener("ContextMenu:SetAsDesktopBackground", (message) => { + let target = message.objects.target; + + // Paranoia: check disableSetDesktopBackground again, in case the + // image changed since the context menu was initiated. + let disable = disableSetDesktopBackground(target); + + if (!disable) { + try { + BrowserUtils.urlSecurityCheck(target.currentURI.spec, target.ownerDocument.nodePrincipal); + let canvas = content.document.createElement("canvas"); + canvas.width = target.naturalWidth; + canvas.height = target.naturalHeight; + let ctx = canvas.getContext("2d"); + ctx.drawImage(target, 0, 0); + let dataUrl = canvas.toDataURL(); + sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result", + { dataUrl }); + } + catch (e) { + Cu.reportError(e); + disable = true; + } + } + + if (disable) + sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result", { disable }); +}); + +var PageInfoListener = { + + init: function() { + addMessageListener("PageInfo:getData", this); + }, + + receiveMessage: function(message) { + let strings = message.data.strings; + let window; + let document; + + let frameOuterWindowID = message.data.frameOuterWindowID; + + // If inside frame then get the frame's window and document. + if (frameOuterWindowID) { + window = Services.wm.getOuterWindowWithId(frameOuterWindowID); + document = window.document; + } + else { + window = content.window; + document = content.document; + } + + let imageElement = message.objects.imageElement; + + let pageInfoData = {metaViewRows: this.getMetaInfo(document), + docInfo: this.getDocumentInfo(document), + feeds: this.getFeedsInfo(document, strings), + windowInfo: this.getWindowInfo(window), + imageInfo: this.getImageInfo(imageElement)}; + + sendAsyncMessage("PageInfo:data", pageInfoData); + + // Separate step so page info dialog isn't blank while waiting for this to finish. + this.getMediaInfo(document, window, strings); + }, + + getImageInfo: function(imageElement) { + let imageInfo = null; + if (imageElement) { + imageInfo = { + currentSrc: imageElement.currentSrc, + width: imageElement.width, + height: imageElement.height, + imageText: imageElement.title || imageElement.alt + }; + } + return imageInfo; + }, + + getMetaInfo: function(document) { + let metaViewRows = []; + + // Get the meta tags from the page. + let metaNodes = document.getElementsByTagName("meta"); + + for (let metaNode of metaNodes) { + metaViewRows.push([metaNode.name || metaNode.httpEquiv || metaNode.getAttribute("property"), + metaNode.content]); + } + + return metaViewRows; + }, + + getWindowInfo: function(window) { + let windowInfo = {}; + windowInfo.isTopWindow = window == window.top; + + let hostName = null; + try { + hostName = window.location.host; + } + catch (exception) { } + + windowInfo.hostName = hostName; + return windowInfo; + }, + + getDocumentInfo: function(document) { + let docInfo = {}; + docInfo.title = document.title; + docInfo.location = document.location.toString(); + docInfo.referrer = document.referrer; + docInfo.compatMode = document.compatMode; + docInfo.contentType = document.contentType; + docInfo.characterSet = document.characterSet; + docInfo.lastModified = document.lastModified; + docInfo.principal = document.nodePrincipal; + + let documentURIObject = {}; + documentURIObject.spec = document.documentURIObject.spec; + documentURIObject.originCharset = document.documentURIObject.originCharset; + docInfo.documentURIObject = documentURIObject; + + docInfo.isContentWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(content); + + return docInfo; + }, + + getFeedsInfo: function(document, strings) { + let feeds = []; + // Get the feeds from the page. + let linkNodes = document.getElementsByTagName("link"); + let length = linkNodes.length; + for (let i = 0; i < length; i++) { + let link = linkNodes[i]; + if (!link.href) { + continue; + } + let rel = link.rel && link.rel.toLowerCase(); + let rels = {}; + + if (rel) { + for (let relVal of rel.split(/\s+/)) { + rels[relVal] = true; + } + } + + if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) { + let type = Feeds.isValidFeed(link, document.nodePrincipal, "feed" in rels); + if (type) { + type = strings[type] || strings["application/rss+xml"]; + feeds.push([link.title, type, link.href]); + } + } + } + return feeds; + }, + + // Only called once to get the media tab's media elements from the content page. + getMediaInfo: function(document, window, strings) + { + let frameList = this.goThroughFrames(document, window); + Task.spawn(() => this.processFrames(document, frameList, strings)); + }, + + goThroughFrames: function(document, window) + { + let frameList = [document]; + if (window && window.frames.length > 0) { + let num = window.frames.length; + for (let i = 0; i < num; i++) { + // Recurse through the frames. + frameList.concat(this.goThroughFrames(window.frames[i].document, + window.frames[i])); + } + } + return frameList; + }, + + processFrames: function*(document, frameList, strings) + { + let nodeCount = 0; + for (let doc of frameList) { + let iterator = doc.createTreeWalker(doc, content.NodeFilter.SHOW_ELEMENT); + + // Goes through all the elements on the doc. imageViewRows takes only the media elements. + while (iterator.nextNode()) { + let mediaItems = this.getMediaItems(document, strings, iterator.currentNode); + + if (mediaItems.length) { + sendAsyncMessage("PageInfo:mediaData", + {mediaItems, isComplete: false}); + } + + if (++nodeCount % 500 == 0) { + // setTimeout every 500 elements so we don't keep blocking the content process. + yield new Promise(resolve => setTimeout(resolve, 10)); + } + } + } + // Send that page info media fetching has finished. + sendAsyncMessage("PageInfo:mediaData", {isComplete: true}); + }, + + getMediaItems: function(document, strings, elem) + { + // Check for images defined in CSS (e.g. background, borders) + let computedStyle = elem.ownerGlobal.getComputedStyle(elem); + // A node can have multiple media items associated with it - for example, + // multiple background images. + let mediaItems = []; + + let addImage = (url, type, alt, elem, isBg) => { + let element = this.serializeElementInfo(document, url, type, alt, elem, isBg); + mediaItems.push([url, type, alt, element, isBg]); + }; + + if (computedStyle) { + let addImgFunc = (label, val) => { + if (val.primitiveType == content.CSSPrimitiveValue.CSS_URI) { + addImage(val.getStringValue(), label, strings.notSet, elem, true); + } + else if (val.primitiveType == content.CSSPrimitiveValue.CSS_STRING) { + // This is for -moz-image-rect. + // TODO: Reimplement once bug 714757 is fixed. + let strVal = val.getStringValue(); + if (strVal.search(/^.*url\(\"?/) > -1) { + let url = strVal.replace(/^.*url\(\"?/, "").replace(/\"?\).*$/, ""); + addImage(url, label, strings.notSet, elem, true); + } + } + else if (val.cssValueType == content.CSSValue.CSS_VALUE_LIST) { + // Recursively resolve multiple nested CSS value lists. + for (let i = 0; i < val.length; i++) { + addImgFunc(label, val.item(i)); + } + } + }; + + addImgFunc(strings.mediaBGImg, computedStyle.getPropertyCSSValue("background-image")); + addImgFunc(strings.mediaBorderImg, computedStyle.getPropertyCSSValue("border-image-source")); + addImgFunc(strings.mediaListImg, computedStyle.getPropertyCSSValue("list-style-image")); + addImgFunc(strings.mediaCursor, computedStyle.getPropertyCSSValue("cursor")); + } + + // One swi^H^H^Hif-else to rule them all. + if (elem instanceof content.HTMLImageElement) { + addImage(elem.src, strings.mediaImg, + (elem.hasAttribute("alt")) ? elem.alt : strings.notSet, elem, false); + } + else if (elem instanceof content.SVGImageElement) { + try { + // Note: makeURLAbsolute will throw if either the baseURI is not a valid URI + // or the URI formed from the baseURI and the URL is not a valid URI. + if (elem.href.baseVal) { + let href = Services.io.newURI(elem.href.baseVal, null, Services.io.newURI(elem.baseURI)).spec; + addImage(href, strings.mediaImg, "", elem, false); + } + } catch (e) { } + } + else if (elem instanceof content.HTMLVideoElement) { + addImage(elem.currentSrc, strings.mediaVideo, "", elem, false); + } + else if (elem instanceof content.HTMLAudioElement) { + addImage(elem.currentSrc, strings.mediaAudio, "", elem, false); + } + else if (elem instanceof content.HTMLLinkElement) { + if (elem.rel && /\bicon\b/i.test(elem.rel)) { + addImage(elem.href, strings.mediaLink, "", elem, false); + } + } + else if (elem instanceof content.HTMLInputElement || elem instanceof content.HTMLButtonElement) { + if (elem.type.toLowerCase() == "image") { + addImage(elem.src, strings.mediaInput, + (elem.hasAttribute("alt")) ? elem.alt : strings.notSet, elem, false); + } + } + else if (elem instanceof content.HTMLObjectElement) { + addImage(elem.data, strings.mediaObject, this.getValueText(elem), elem, false); + } + else if (elem instanceof content.HTMLEmbedElement) { + addImage(elem.src, strings.mediaEmbed, "", elem, false); + } + + return mediaItems; + }, + + /** + * Set up a JSON element object with all the instanceOf and other infomation that + * makePreview in pageInfo.js uses to figure out how to display the preview. + */ + + serializeElementInfo: function(document, url, type, alt, item, isBG) + { + let result = {}; + + let imageText; + if (!isBG && + !(item instanceof content.SVGImageElement) && + !(document instanceof content.ImageDocument)) { + imageText = item.title || item.alt; + + if (!imageText && !(item instanceof content.HTMLImageElement)) { + imageText = this.getValueText(item); + } + } + + result.imageText = imageText; + result.longDesc = item.longDesc; + result.numFrames = 1; + + if (item instanceof content.HTMLObjectElement || + item instanceof content.HTMLEmbedElement || + item instanceof content.HTMLLinkElement) { + result.mimeType = item.type; + } + + if (!result.mimeType && !isBG && item instanceof Ci.nsIImageLoadingContent) { + // Interface for image loading content. + let imageRequest = item.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); + if (imageRequest) { + result.mimeType = imageRequest.mimeType; + let image = !(imageRequest.imageStatus & imageRequest.STATUS_ERROR) && imageRequest.image; + if (image) { + result.numFrames = image.numFrames; + } + } + } + + // If we have a data url, get the MIME type from the url. + if (!result.mimeType && url.startsWith("data:")) { + let dataMimeType = /^data:(image\/[^;,]+)/i.exec(url); + if (dataMimeType) + result.mimeType = dataMimeType[1].toLowerCase(); + } + + result.HTMLLinkElement = item instanceof content.HTMLLinkElement; + result.HTMLInputElement = item instanceof content.HTMLInputElement; + result.HTMLImageElement = item instanceof content.HTMLImageElement; + result.HTMLObjectElement = item instanceof content.HTMLObjectElement; + result.SVGImageElement = item instanceof content.SVGImageElement; + result.HTMLVideoElement = item instanceof content.HTMLVideoElement; + result.HTMLAudioElement = item instanceof content.HTMLAudioElement; + + if (isBG) { + // Items that are showing this image as a background + // image might not necessarily have a width or height, + // so we'll dynamically generate an image and send up the + // natural dimensions. + let img = content.document.createElement("img"); + img.src = url; + result.naturalWidth = img.naturalWidth; + result.naturalHeight = img.naturalHeight; + } else { + // Otherwise, we can use the current width and height + // of the image. + result.width = item.width; + result.height = item.height; + } + + if (item instanceof content.SVGImageElement) { + result.SVGImageElementWidth = item.width.baseVal.value; + result.SVGImageElementHeight = item.height.baseVal.value; + } + + result.baseURI = item.baseURI; + + return result; + }, + + // Other Misc Stuff + // Modified from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html + // parse a node to extract the contents of the node + getValueText: function(node) + { + + let valueText = ""; + + // Form input elements don't generally contain information that is useful to our callers, so return nothing. + if (node instanceof content.HTMLInputElement || + node instanceof content.HTMLSelectElement || + node instanceof content.HTMLTextAreaElement) { + return valueText; + } + + // Otherwise recurse for each child. + let length = node.childNodes.length; + + for (let i = 0; i < length; i++) { + let childNode = node.childNodes[i]; + let nodeType = childNode.nodeType; + + // Text nodes are where the goods are. + if (nodeType == content.Node.TEXT_NODE) { + valueText += " " + childNode.nodeValue; + } + // And elements can have more text inside them. + else if (nodeType == content.Node.ELEMENT_NODE) { + // Images are special, we want to capture the alt text as if the image weren't there. + if (childNode instanceof content.HTMLImageElement) { + valueText += " " + this.getAltText(childNode); + } + else { + valueText += " " + this.getValueText(childNode); + } + } + } + + return this.stripWS(valueText); + }, + + // Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html. + // Traverse the tree in search of an img or area element and grab its alt tag. + getAltText: function(node) + { + let altText = ""; + + if (node.alt) { + return node.alt; + } + let length = node.childNodes.length; + for (let i = 0; i < length; i++) { + if ((altText = this.getAltText(node.childNodes[i]) != undefined)) { // stupid js warning... + return altText; + } + } + return ""; + }, + + // Copied from the Links Panel v2.3, http://segment7.net/mozilla/links/links.html. + // Strip leading and trailing whitespace, and replace multiple consecutive whitespace characters with a single space. + stripWS: function(text) + { + let middleRE = /\s+/g; + let endRE = /(^\s+)|(\s+$)/g; + + text = text.replace(middleRE, " "); + return text.replace(endRE, ""); + } +}; +PageInfoListener.init(); + +let OfflineApps = { + _docId: 0, + _docIdMap: new Map(), + + _docManifestSet: new Set(), + + _observerAdded: false, + registerWindow(aWindow) { + if (!this._observerAdded) { + this._observerAdded = true; + Services.obs.addObserver(this, "offline-cache-update-completed", true); + } + let manifestURI = this._getManifestURI(aWindow); + this._docManifestSet.add(manifestURI.spec); + }, + + handleEvent(event) { + if (event.type == "MozApplicationManifest") { + this.offlineAppRequested(event.originalTarget.defaultView); + } + }, + + _getManifestURI(aWindow) { + if (!aWindow.document.documentElement) + return null; + + var attr = aWindow.document.documentElement.getAttribute("manifest"); + if (!attr) + return null; + + try { + var contentURI = BrowserUtils.makeURI(aWindow.location.href, null, null); + return BrowserUtils.makeURI(attr, aWindow.document.characterSet, contentURI); + } catch (e) { + return null; + } + }, + + offlineAppRequested(aContentWindow) { + this.registerWindow(aContentWindow); + if (!Services.prefs.getBoolPref("browser.offline-apps.notify")) { + return; + } + + let currentURI = aContentWindow.document.documentURIObject; + // don't bother showing UI if the user has already made a decision + if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION) + return; + + try { + if (Services.prefs.getBoolPref("offline-apps.allow_by_default")) { + // all pages can use offline capabilities, no need to ask the user + return; + } + } catch (e) { + // this pref isn't set by default, ignore failures + } + let docId = ++this._docId; + this._docIdMap.set(docId, Cu.getWeakReference(aContentWindow.document)); + sendAsyncMessage("OfflineApps:RequestPermission", { + uri: currentURI.spec, + docId, + }); + }, + + _startFetching(aDocument) { + if (!aDocument.documentElement) + return; + + let manifestURI = this._getManifestURI(aDocument.defaultView); + if (!manifestURI) + return; + + var updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]. + getService(Ci.nsIOfflineCacheUpdateService); + updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject, + aDocument.nodePrincipal, aDocument.defaultView); + }, + + receiveMessage(aMessage) { + if (aMessage.name == "OfflineApps:StartFetching") { + let doc = this._docIdMap.get(aMessage.data.docId); + doc = doc && doc.get(); + if (doc) { + this._startFetching(doc); + } + this._docIdMap.delete(aMessage.data.docId); + } + }, + + observe(aSubject, aTopic, aState) { + if (aTopic == "offline-cache-update-completed") { + let cacheUpdate = aSubject.QueryInterface(Ci.nsIOfflineCacheUpdate); + let uri = cacheUpdate.manifestURI; + if (uri && this._docManifestSet.has(uri.spec)) { + sendAsyncMessage("OfflineApps:CheckUsage", {uri: uri.spec}); + } + } + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), +}; + +addEventListener("MozApplicationManifest", OfflineApps, false); +addMessageListener("OfflineApps:StartFetching", OfflineApps); diff --git a/application/basilisk/base/content/contentSearchUI.css b/application/basilisk/base/content/contentSearchUI.css new file mode 100644 index 000000000..cd5cf5008 --- /dev/null +++ b/application/basilisk/base/content/contentSearchUI.css @@ -0,0 +1,161 @@ +/* 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/. */ + +.contentSearchSuggestionTable { + background-color: hsla(0,0%,100%,.99); + border: 1px solid hsla(0, 0%, 0%, .2); + border-top: none; + box-shadow: 0 5px 10px hsla(0, 0%, 0%, .1); + position: absolute; + left: 0; + z-index: 1001; + -moz-user-select: none; + cursor: default; +} + +.contentSearchSuggestionTable:-moz-dir(rtl) { + left: auto; + right: 0; +} + +.contentSearchSuggestionsList { + border-bottom: 1px solid hsl(0, 0%, 92%); + width: 100%; + height: 100%; +} + +.contentSearchSuggestionTable, +.contentSearchSuggestionsList { + border-spacing: 0; + overflow: hidden; + padding: 0; + margin: 0; + text-align: start; +} + +.contentSearchHeaderRow, +.contentSearchSuggestionRow { + margin: 0; + max-width: inherit; + padding: 0; +} + +.contentSearchHeaderRow > td > img, +.contentSearchSuggestionRow > td > .historyIcon { + margin-inline-end: 8px; + margin-bottom: -3px; +} + +.contentSearchSuggestionTable .historyIcon { + width: 16px; + height: 16px; + display: inline-block; + background-image: url("chrome://browser/skin/search-history-icon.svg#search-history-icon"); +} + +.contentSearchSuggestionRow.selected > td > .historyIcon { + background-image: url("chrome://browser/skin/search-history-icon.svg#search-history-icon-active"); +} + +.contentSearchHeader > img { + height: 16px; + width: 16px; + margin: 0; + padding: 0; +} + +.contentSearchSuggestionRow.remote > td > .historyIcon { + visibility: hidden; +} + +.contentSearchSuggestionRow.selected { + background-color: Highlight; + color: HighlightText; +} + +.contentSearchHeader, +.contentSearchSuggestionEntry { + margin: 0; + max-width: inherit; + overflow: hidden; + padding: 4px 10px; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 75%; +} + +.contentSearchHeader { + background-color: hsl(0, 0%, 97%); + color: #666; + border-bottom: 1px solid hsl(0, 0%, 92%); +} + +.contentSearchSuggestionsContainer { + margin: 0; + padding: 0; + border-spacing: 0; + width: 100%; +} + +.contentSearchSearchWithHeaderSearchText { + white-space: pre; + font-weight: bold; +} + +.contentSearchOneOffItem { + -moz-appearance: none; + height: 32px; + margin: 0; + padding: 0; + border: none; + background: none; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAWCAYAAAABxvaqAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gofECQNNVW2/AAAABBJREFUGFdjOHPmzH8GehEA/KpKg9YTf4AAAAAASUVORK5CYII='); + background-repeat: no-repeat; + background-position: right center; +} + +.contentSearchOneOffItem:-moz-dir(rtl) { + background-position: left center; +} + +.contentSearchOneOffItem > img { + width: 16px; + height: 16px; + margin-bottom: -2px; +} + +.contentSearchOneOffItem:not(.last-row) { + border-bottom: 1px solid hsl(0, 0%, 92%); +} + +.contentSearchOneOffItem.end-of-row { + background-image: none; +} + +.contentSearchOneOffItem.selected { + background-color: Highlight; + background-image: none; +} + +.contentSearchOneOffsTable { + width: 100%; +} + +.contentSearchSettingsButton { + margin: 0; + padding: 0; + height: 32px; + border: none; + border-top: 1px solid hsla(0, 0%, 0%, .08); + text-align: center; + width: 100%; +} + +.contentSearchSettingsButton.selected { + background-color: hsl(0, 0%, 90%); +} + +.contentSearchSettingsButton:active { + background-color: hsl(0, 0%, 85%); +} diff --git a/application/basilisk/base/content/contentSearchUI.js b/application/basilisk/base/content/contentSearchUI.js new file mode 100644 index 000000000..9136ea8f2 --- /dev/null +++ b/application/basilisk/base/content/contentSearchUI.js @@ -0,0 +1,915 @@ +/* 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/. */ + +"use strict"; + +this.ContentSearchUIController = (function () { + +const MAX_DISPLAYED_SUGGESTIONS = 6; +const SUGGESTION_ID_PREFIX = "searchSuggestion"; +const ONE_OFF_ID_PREFIX = "oneOff"; + +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +/** + * Creates a new object that manages search suggestions and their UI for a text + * box. + * + * The UI consists of an html:table that's inserted into the DOM after the given + * text box and styled so that it appears as a dropdown below the text box. + * + * @param inputElement + * Search suggestions will be based on the text in this text box. + * Assumed to be an html:input. xul:textbox is untested but might work. + * @param tableParent + * The suggestion table is appended as a child to this element. Since + * the table is absolutely positioned and its top and left values are set + * to be relative to the top and left of the page, either the parent and + * all its ancestors should not be positioned elements (i.e., their + * positions should be "static"), or the parent's position should be the + * top left of the page. + * @param healthReportKey + * This will be sent with the search data for FHR to record the search. + * @param searchPurpose + * Sent with search data, see nsISearchEngine.getSubmission. + * @param idPrefix + * The IDs of elements created by the object will be prefixed with this + * string. + */ +function ContentSearchUIController(inputElement, tableParent, healthReportKey, + searchPurpose, idPrefix="") { + this.input = inputElement; + this._idPrefix = idPrefix; + this._healthReportKey = healthReportKey; + this._searchPurpose = searchPurpose; + + let tableID = idPrefix + "searchSuggestionTable"; + this.input.autocomplete = "off"; + this.input.setAttribute("aria-autocomplete", "true"); + this.input.setAttribute("aria-controls", tableID); + tableParent.appendChild(this._makeTable(tableID)); + + this.input.addEventListener("keypress", this); + this.input.addEventListener("input", this); + this.input.addEventListener("focus", this); + this.input.addEventListener("blur", this); + window.addEventListener("ContentSearchService", this); + + this._stickyInputValue = ""; + this._hideSuggestions(); + + this._getSearchEngines(); + this._getStrings(); +} + +ContentSearchUIController.prototype = { + + // The timeout (ms) of the remote suggestions. Corresponds to + // SearchSuggestionController.remoteTimeout. Uses + // SearchSuggestionController's default timeout if falsey. + remoteTimeout: undefined, + _oneOffButtons: [], + // Setting up the one off buttons causes an uninterruptible reflow. If we + // receive the list of engines while the newtab page is loading, this reflow + // may regress performance - so we set this flag and only set up the buttons + // if it's set when the suggestions table is actually opened. + _pendingOneOffRefresh: undefined, + + get defaultEngine() { + return this._defaultEngine; + }, + + set defaultEngine(engine) { + if (this._defaultEngine && this._defaultEngine.icon) { + URL.revokeObjectURL(this._defaultEngine.icon); + } + let icon; + if (engine.iconBuffer) { + icon = this._getFaviconURIFromBuffer(engine.iconBuffer); + } + else { + icon = this._getImageURIForCurrentResolution( + "chrome://mozapps/skin/places/defaultFavicon.png"); + } + this._defaultEngine = { + name: engine.name, + icon: icon, + }; + this._updateDefaultEngineHeader(); + + if (engine && document.activeElement == this.input) { + this._speculativeConnect(); + } + }, + + get engines() { + return this._engines; + }, + + set engines(val) { + this._engines = val; + this._pendingOneOffRefresh = true; + }, + + // The selectedIndex is the index of the element with the "selected" class in + // the list obtained by concatenating the suggestion rows, one-off buttons, and + // search settings button. + get selectedIndex() { + let allElts = [...this._suggestionsList.children, + ...this._oneOffButtons, + document.getElementById("contentSearchSettingsButton")]; + for (let i = 0; i < allElts.length; ++i) { + let elt = allElts[i]; + if (elt.classList.contains("selected")) { + return i; + } + } + return -1; + }, + + set selectedIndex(idx) { + // Update the table's rows, and the input when there is a selection. + this._table.removeAttribute("aria-activedescendant"); + this.input.removeAttribute("aria-activedescendant"); + + let allElts = [...this._suggestionsList.children, + ...this._oneOffButtons, + document.getElementById("contentSearchSettingsButton")]; + // If we are selecting a suggestion and a one-off is selected, don't deselect it. + let excludeIndex = idx < this.numSuggestions && this.selectedButtonIndex > -1 ? + this.numSuggestions + this.selectedButtonIndex : -1; + for (let i = 0; i < allElts.length; ++i) { + let elt = allElts[i]; + let ariaSelectedElt = i < this.numSuggestions ? elt.firstChild : elt; + if (i == idx) { + elt.classList.add("selected"); + ariaSelectedElt.setAttribute("aria-selected", "true"); + this.input.setAttribute("aria-activedescendant", ariaSelectedElt.id); + } + else if (i != excludeIndex) { + elt.classList.remove("selected"); + ariaSelectedElt.setAttribute("aria-selected", "false"); + } + } + }, + + get selectedButtonIndex() { + let elts = [...this._oneOffButtons, + document.getElementById("contentSearchSettingsButton")]; + for (let i = 0; i < elts.length; ++i) { + if (elts[i].classList.contains("selected")) { + return i; + } + } + return -1; + }, + + set selectedButtonIndex(idx) { + let elts = [...this._oneOffButtons, + document.getElementById("contentSearchSettingsButton")]; + for (let i = 0; i < elts.length; ++i) { + let elt = elts[i]; + if (i == idx) { + elt.classList.add("selected"); + elt.setAttribute("aria-selected", "true"); + } + else { + elt.classList.remove("selected"); + elt.setAttribute("aria-selected", "false"); + } + } + }, + + get selectedEngineName() { + let selectedElt = this._oneOffsTable.querySelector(".selected"); + if (selectedElt) { + return selectedElt.engineName; + } + return this.defaultEngine.name; + }, + + get numSuggestions() { + return this._suggestionsList.children.length; + }, + + selectAndUpdateInput: function (idx) { + this.selectedIndex = idx; + let newValue = this.suggestionAtIndex(idx) || this._stickyInputValue; + // Setting the input value when the value has not changed commits the current + // IME composition, which we don't want to do. + if (this.input.value != newValue) { + this.input.value = newValue; + } + this._updateSearchWithHeader(); + }, + + suggestionAtIndex: function (idx) { + let row = this._suggestionsList.children[idx]; + return row ? row.textContent : null; + }, + + deleteSuggestionAtIndex: function (idx) { + // Only form history suggestions can be deleted. + if (this.isFormHistorySuggestionAtIndex(idx)) { + let suggestionStr = this.suggestionAtIndex(idx); + this._sendMsg("RemoveFormHistoryEntry", suggestionStr); + this._suggestionsList.children[idx].remove(); + this.selectAndUpdateInput(-1); + } + }, + + isFormHistorySuggestionAtIndex: function (idx) { + let row = this._suggestionsList.children[idx]; + return row && row.classList.contains("formHistory"); + }, + + addInputValueToFormHistory: function () { + this._sendMsg("AddFormHistoryEntry", this.input.value); + }, + + handleEvent: function (event) { + this["_on" + event.type[0].toUpperCase() + event.type.substr(1)](event); + }, + + _onCommand: function(aEvent) { + if (this.selectedButtonIndex == this._oneOffButtons.length) { + // Settings button was selected. + this._sendMsg("ManageEngines"); + return; + } + + this.search(aEvent); + + if (aEvent) { + aEvent.preventDefault(); + } + }, + + search: function (aEvent) { + if (!this.defaultEngine) { + return; // Not initialized yet. + } + + let searchText = this.input; + let searchTerms; + if (this._table.hidden || + aEvent.originalTarget.id == "contentSearchDefaultEngineHeader" || + aEvent instanceof KeyboardEvent) { + searchTerms = searchText.value; + } + else { + searchTerms = this.suggestionAtIndex(this.selectedIndex) || searchText.value; + } + // Send an event that will perform a search and Firefox Health Report will + // record that a search from the healthReportKey passed to the constructor. + let eventData = { + engineName: this.selectedEngineName, + searchString: searchTerms, + healthReportKey: this._healthReportKey, + searchPurpose: this._searchPurpose, + originalEvent: { + shiftKey: aEvent.shiftKey, + ctrlKey: aEvent.ctrlKey, + metaKey: aEvent.metaKey, + altKey: aEvent.altKey, + }, + }; + if ("button" in aEvent) { + eventData.originalEvent.button = aEvent.button; + } + + if (this.suggestionAtIndex(this.selectedIndex)) { + eventData.selection = { + index: this.selectedIndex, + kind: undefined, + }; + if (aEvent instanceof MouseEvent) { + eventData.selection.kind = "mouse"; + } else if (aEvent instanceof KeyboardEvent) { + eventData.selection.kind = "key"; + } + } + + this._sendMsg("Search", eventData); + this.addInputValueToFormHistory(); + }, + + _onInput: function () { + if (!this.input.value) { + this._stickyInputValue = ""; + this._hideSuggestions(); + } + else if (this.input.value != this._stickyInputValue) { + // Only fetch new suggestions if the input value has changed. + this._getSuggestions(); + this.selectAndUpdateInput(-1); + } + this._updateSearchWithHeader(); + }, + + _onKeypress: function (event) { + let selectedIndexDelta = 0; + let selectedSuggestionDelta = 0; + let selectedOneOffDelta = 0; + + switch (event.keyCode) { + case event.DOM_VK_UP: + if (this._table.hidden) { + return; + } + if (event.getModifierState("Accel")) { + if (event.shiftKey) { + selectedSuggestionDelta = -1; + break; + } + this._cycleCurrentEngine(true); + break; + } + if (event.altKey) { + selectedOneOffDelta = -1; + break; + } + selectedIndexDelta = -1; + break; + case event.DOM_VK_DOWN: + if (this._table.hidden) { + this._getSuggestions(); + return; + } + if (event.getModifierState("Accel")) { + if (event.shiftKey) { + selectedSuggestionDelta = 1; + break; + } + this._cycleCurrentEngine(false); + break; + } + if (event.altKey) { + selectedOneOffDelta = 1; + break; + } + selectedIndexDelta = 1; + break; + case event.DOM_VK_TAB: + if (this._table.hidden) { + return; + } + // Shift+tab when either the first or no one-off is selected, as well as + // tab when the settings button is selected, should change focus as normal. + if ((this.selectedButtonIndex <= 0 && event.shiftKey) || + this.selectedButtonIndex == this._oneOffButtons.length && !event.shiftKey) { + return; + } + selectedOneOffDelta = event.shiftKey ? -1 : 1; + break; + case event.DOM_VK_RIGHT: + // Allow normal caret movement until the caret is at the end of the input. + if (this.input.selectionStart != this.input.selectionEnd || + this.input.selectionEnd != this.input.value.length) { + return; + } + if (this.numSuggestions && this.selectedIndex >= 0 && + this.selectedIndex < this.numSuggestions) { + this.input.value = this.suggestionAtIndex(this.selectedIndex); + this.input.setAttribute("selection-index", this.selectedIndex); + this.input.setAttribute("selection-kind", "key"); + } else { + // If we didn't select anything, make sure to remove the attributes + // in case they were populated last time. + this.input.removeAttribute("selection-index"); + this.input.removeAttribute("selection-kind"); + } + this._stickyInputValue = this.input.value; + this._hideSuggestions(); + return; + case event.DOM_VK_RETURN: + this._onCommand(event); + return; + case event.DOM_VK_DELETE: + if (this.selectedIndex >= 0) { + this.deleteSuggestionAtIndex(this.selectedIndex); + } + return; + case event.DOM_VK_ESCAPE: + if (!this._table.hidden) { + this._hideSuggestions(); + } + return; + default: + return; + } + + let currentIndex = this.selectedIndex; + if (selectedIndexDelta) { + let newSelectedIndex = currentIndex + selectedIndexDelta; + if (newSelectedIndex < -1) { + newSelectedIndex = this.numSuggestions + this._oneOffButtons.length; + } + // If are moving up from the first one off, we have to deselect the one off + // manually because the selectedIndex setter tries to exclude the selected + // one-off (which is desirable for accel+shift+up/down). + if (currentIndex == this.numSuggestions && selectedIndexDelta == -1) { + this.selectedButtonIndex = -1; + } + this.selectAndUpdateInput(newSelectedIndex); + } + + else if (selectedSuggestionDelta) { + let newSelectedIndex; + if (currentIndex >= this.numSuggestions || currentIndex == -1) { + // No suggestion already selected, select the first/last one appropriately. + newSelectedIndex = selectedSuggestionDelta == 1 ? + 0 : this.numSuggestions - 1; + } + else { + newSelectedIndex = currentIndex + selectedSuggestionDelta; + } + if (newSelectedIndex >= this.numSuggestions) { + newSelectedIndex = -1; + } + this.selectAndUpdateInput(newSelectedIndex); + } + + else if (selectedOneOffDelta) { + let newSelectedIndex; + let currentButton = this.selectedButtonIndex; + if (currentButton == -1 || currentButton == this._oneOffButtons.length) { + // No one-off already selected, select the first/last one appropriately. + newSelectedIndex = selectedOneOffDelta == 1 ? + 0 : this._oneOffButtons.length - 1; + } + else { + newSelectedIndex = currentButton + selectedOneOffDelta; + } + // Allow selection of the settings button via the tab key. + if (newSelectedIndex == this._oneOffButtons.length && + event.keyCode != event.DOM_VK_TAB) { + newSelectedIndex = -1; + } + this.selectedButtonIndex = newSelectedIndex; + } + + // Prevent the input's caret from moving. + event.preventDefault(); + }, + + _currentEngineIndex: -1, + _cycleCurrentEngine: function (aReverse) { + if ((this._currentEngineIndex == this._engines.length - 1 && !aReverse) || + (this._currentEngineIndex == 0 && aReverse)) { + return; + } + this._currentEngineIndex += aReverse ? -1 : 1; + let engineName = this._engines[this._currentEngineIndex].name; + this._sendMsg("SetCurrentEngine", engineName); + }, + + _onFocus: function () { + if (this._mousedown) { + return; + } + // When the input box loses focus to something in our table, we refocus it + // immediately. This causes the focus highlight to flicker, so we set a + // custom attribute which consumers should use for focus highlighting. This + // attribute is removed only when we do not immediately refocus the input + // box, thus eliminating flicker. + this.input.setAttribute("keepfocus", "true"); + this._speculativeConnect(); + }, + + _onBlur: function () { + if (this._mousedown) { + // At this point, this.input has lost focus, but a new element has not yet + // received it. If we re-focus this.input directly, the new element will + // steal focus immediately, so we queue it instead. + setTimeout(() => this.input.focus(), 0); + return; + } + this.input.removeAttribute("keepfocus"); + this._hideSuggestions(); + }, + + _onMousemove: function (event) { + let idx = this._indexOfTableItem(event.target); + if (idx >= this.numSuggestions) { + this.selectedButtonIndex = idx - this.numSuggestions; + return; + } + this.selectedIndex = idx; + }, + + _onMouseup: function (event) { + if (event.button == 2) { + return; + } + this._onCommand(event); + }, + + _onMouseout: function (event) { + // We only deselect one-off buttons and the settings button when they are + // moused out. + let idx = this._indexOfTableItem(event.originalTarget); + if (idx >= this.numSuggestions) { + this.selectedButtonIndex = -1; + } + }, + + _onClick: function (event) { + this._onMouseup(event); + }, + + _onContentSearchService: function (event) { + let methodName = "_onMsg" + event.detail.type; + if (methodName in this) { + this[methodName](event.detail.data); + } + }, + + _onMsgFocusInput: function (event) { + this.input.focus(); + }, + + _onMsgSuggestions: function (suggestions) { + // Ignore the suggestions if their search string or engine doesn't match + // ours. Due to the async nature of message passing, this can easily happen + // when the user types quickly. + if (this._stickyInputValue != suggestions.searchString || + this.defaultEngine.name != suggestions.engineName) { + return; + } + + this._clearSuggestionRows(); + + // Position and size the table. + let { left } = this.input.getBoundingClientRect(); + this._table.style.top = this.input.offsetHeight + "px"; + this._table.style.minWidth = this.input.offsetWidth + "px"; + this._table.style.maxWidth = (window.innerWidth - left - 40) + "px"; + + // Add the suggestions to the table. + let searchWords = + new Set(suggestions.searchString.trim().toLowerCase().split(/\s+/)); + for (let i = 0; i < MAX_DISPLAYED_SUGGESTIONS; i++) { + let type, idx; + if (i < suggestions.formHistory.length) { + [type, idx] = ["formHistory", i]; + } + else { + let j = i - suggestions.formHistory.length; + if (j < suggestions.remote.length) { + [type, idx] = ["remote", j]; + } + else { + break; + } + } + this._suggestionsList.appendChild( + this._makeTableRow(type, suggestions[type][idx], i, searchWords)); + } + + if (this._table.hidden) { + this.selectedIndex = -1; + if (this._pendingOneOffRefresh) { + this._setUpOneOffButtons(); + delete this._pendingOneOffRefresh; + } + this._currentEngineIndex = + this._engines.findIndex(aEngine => aEngine.name == this.defaultEngine.name); + this._table.hidden = false; + this.input.setAttribute("aria-expanded", "true"); + this._originalDefaultEngine = { + name: this.defaultEngine.name, + icon: this.defaultEngine.icon, + }; + } + }, + + _onMsgSuggestionsCancelled: function () { + if (!this._table.hidden) { + this._hideSuggestions(); + } + }, + + _onMsgState: function (state) { + this.engines = state.engines; + // No point updating the default engine (and the header) if there's no change. + if (this.defaultEngine && + this.defaultEngine.name == state.currentEngine.name && + this.defaultEngine.icon == state.currentEngine.icon) { + return; + } + this.defaultEngine = state.currentEngine; + }, + + _onMsgCurrentState: function (state) { + this._onMsgState(state); + }, + + _onMsgCurrentEngine: function (engine) { + this.defaultEngine = engine; + this._pendingOneOffRefresh = true; + }, + + _onMsgStrings: function (strings) { + this._strings = strings; + this._updateDefaultEngineHeader(); + this._updateSearchWithHeader(); + document.getElementById("contentSearchSettingsButton").textContent = + this._strings.searchSettings; + this.input.setAttribute("placeholder", this._strings.searchPlaceholder); + }, + + _updateDefaultEngineHeader: function () { + let header = document.getElementById("contentSearchDefaultEngineHeader"); + header.firstChild.setAttribute("src", this.defaultEngine.icon); + if (!this._strings) { + return; + } + while (header.firstChild.nextSibling) { + header.firstChild.nextSibling.remove(); + } + header.appendChild(document.createTextNode( + this._strings.searchHeader.replace("%S", this.defaultEngine.name))); + }, + + _updateSearchWithHeader: function () { + if (!this._strings) { + return; + } + let searchWithHeader = document.getElementById("contentSearchSearchWithHeader"); + if (this.input.value) { + searchWithHeader.innerHTML = this._strings.searchForSomethingWith; + searchWithHeader.querySelector('.contentSearchSearchWithHeaderSearchText').textContent = this.input.value; + } else { + searchWithHeader.textContent = this._strings.searchWithHeader; + } + }, + + _speculativeConnect: function () { + if (this.defaultEngine) { + this._sendMsg("SpeculativeConnect", this.defaultEngine.name); + } + }, + + _makeTableRow: function (type, suggestionStr, currentRow, searchWords) { + let row = document.createElementNS(HTML_NS, "tr"); + row.dir = "auto"; + row.classList.add("contentSearchSuggestionRow"); + row.classList.add(type); + row.setAttribute("role", "presentation"); + row.addEventListener("mousemove", this); + row.addEventListener("mouseup", this); + + let entry = document.createElementNS(HTML_NS, "td"); + let img = document.createElementNS(HTML_NS, "div"); + img.setAttribute("class", "historyIcon"); + entry.appendChild(img); + entry.classList.add("contentSearchSuggestionEntry"); + entry.setAttribute("role", "option"); + entry.id = this._idPrefix + SUGGESTION_ID_PREFIX + currentRow; + entry.setAttribute("aria-selected", "false"); + + let suggestionWords = suggestionStr.trim().toLowerCase().split(/\s+/); + for (let i = 0; i < suggestionWords.length; i++) { + let word = suggestionWords[i]; + let wordSpan = document.createElementNS(HTML_NS, "span"); + if (searchWords.has(word)) { + wordSpan.classList.add("typed"); + } + wordSpan.textContent = word; + entry.appendChild(wordSpan); + if (i < suggestionWords.length - 1) { + entry.appendChild(document.createTextNode(" ")); + } + } + + row.appendChild(entry); + return row; + }, + + // Converts favicon array buffer into a data URI. + _getFaviconURIFromBuffer: function (buffer) { + let blob = new Blob([buffer]); + return URL.createObjectURL(blob); + }, + + // Adds "@2x" to the name of the given PNG url for "retina" screens. + _getImageURIForCurrentResolution: function (uri) { + if (window.devicePixelRatio > 1) { + return uri.replace(/\.png$/, "@2x.png"); + } + return uri; + }, + + _getSearchEngines: function () { + this._sendMsg("GetState"); + }, + + _getStrings: function () { + this._sendMsg("GetStrings"); + }, + + _getSuggestions: function () { + this._stickyInputValue = this.input.value; + if (this.defaultEngine) { + this._sendMsg("GetSuggestions", { + engineName: this.defaultEngine.name, + searchString: this.input.value, + remoteTimeout: this.remoteTimeout, + }); + } + }, + + _clearSuggestionRows: function() { + while (this._suggestionsList.firstElementChild) { + this._suggestionsList.firstElementChild.remove(); + } + }, + + _hideSuggestions: function () { + this.input.setAttribute("aria-expanded", "false"); + this.selectedIndex = -1; + this.selectedButtonIndex = -1; + this._currentEngineIndex = -1; + this._table.hidden = true; + }, + + _indexOfTableItem: function (elt) { + if (elt.classList.contains("contentSearchOneOffItem")) { + return this.numSuggestions + this._oneOffButtons.indexOf(elt); + } + if (elt.classList.contains("contentSearchSettingsButton")) { + return this.numSuggestions + this._oneOffButtons.length; + } + while (elt && elt.localName != "tr") { + elt = elt.parentNode; + } + if (!elt) { + throw new Error("Element is not a row"); + } + return elt.rowIndex; + }, + + _makeTable: function (id) { + this._table = document.createElementNS(HTML_NS, "table"); + this._table.id = id; + this._table.hidden = true; + this._table.classList.add("contentSearchSuggestionTable"); + this._table.setAttribute("role", "presentation"); + + // When the search input box loses focus, we want to immediately give focus + // back to it if the blur was because the user clicked somewhere in the table. + // onBlur uses the _mousedown flag to detect this. + this._table.addEventListener("mousedown", () => { this._mousedown = true; }); + document.addEventListener("mouseup", () => { delete this._mousedown; }); + + // Deselect the selected element on mouseout if it wasn't a suggestion. + this._table.addEventListener("mouseout", this); + + // If a search is loaded in the same tab, ensure the suggestions dropdown + // is hidden immediately when the page starts loading and not when it first + // appears, in order to provide timely feedback to the user. + window.addEventListener("beforeunload", () => { this._hideSuggestions(); }); + + let headerRow = document.createElementNS(HTML_NS, "tr"); + let header = document.createElementNS(HTML_NS, "td"); + headerRow.setAttribute("class", "contentSearchHeaderRow"); + header.setAttribute("class", "contentSearchHeader"); + let iconImg = document.createElementNS(HTML_NS, "img"); + header.appendChild(iconImg); + header.id = "contentSearchDefaultEngineHeader"; + headerRow.appendChild(header); + headerRow.addEventListener("click", this); + this._table.appendChild(headerRow); + + let row = document.createElementNS(HTML_NS, "tr"); + row.setAttribute("class", "contentSearchSuggestionsContainer"); + let cell = document.createElementNS(HTML_NS, "td"); + cell.setAttribute("class", "contentSearchSuggestionsContainer"); + this._suggestionsList = document.createElementNS(HTML_NS, "table"); + this._suggestionsList.setAttribute("class", "contentSearchSuggestionsList"); + cell.appendChild(this._suggestionsList); + row.appendChild(cell); + this._table.appendChild(row); + this._suggestionsList.setAttribute("role", "listbox"); + + this._oneOffsTable = document.createElementNS(HTML_NS, "table"); + this._oneOffsTable.setAttribute("class", "contentSearchOneOffsTable"); + this._oneOffsTable.classList.add("contentSearchSuggestionsContainer"); + this._oneOffsTable.setAttribute("role", "group"); + this._table.appendChild(this._oneOffsTable); + + headerRow = document.createElementNS(HTML_NS, "tr"); + header = document.createElementNS(HTML_NS, "td"); + headerRow.setAttribute("class", "contentSearchHeaderRow"); + header.setAttribute("class", "contentSearchHeader"); + headerRow.appendChild(header); + header.id = "contentSearchSearchWithHeader"; + this._oneOffsTable.appendChild(headerRow); + + let button = document.createElementNS(HTML_NS, "button"); + button.setAttribute("class", "contentSearchSettingsButton"); + button.classList.add("contentSearchHeaderRow"); + button.classList.add("contentSearchHeader"); + button.id = "contentSearchSettingsButton"; + button.addEventListener("click", this); + button.addEventListener("mousemove", this); + this._table.appendChild(button); + + return this._table; + }, + + _setUpOneOffButtons: function () { + // Sometimes we receive a CurrentEngine message from the ContentSearch service + // before we've received a State message - i.e. before we have our engines. + if (!this._engines) { + return; + } + + while (this._oneOffsTable.firstChild.nextSibling) { + this._oneOffsTable.firstChild.nextSibling.remove(); + } + + this._oneOffButtons = []; + + let engines = this._engines.filter(aEngine => aEngine.name != this.defaultEngine.name) + .filter(aEngine => !aEngine.hidden); + if (!engines.length) { + this._oneOffsTable.hidden = true; + return; + } + + const kDefaultButtonWidth = 49; // 48px + 1px border. + let rowWidth = this.input.offsetWidth - 2; // 2px border. + let enginesPerRow = Math.floor(rowWidth / kDefaultButtonWidth); + let buttonWidth = Math.floor(rowWidth / enginesPerRow); + + let row = document.createElementNS(HTML_NS, "tr"); + let cell = document.createElementNS(HTML_NS, "td"); + row.setAttribute("class", "contentSearchSuggestionsContainer"); + cell.setAttribute("class", "contentSearchSuggestionsContainer"); + + for (let i = 0; i < engines.length; ++i) { + let engine = engines[i]; + if (i > 0 && i % enginesPerRow == 0) { + row.appendChild(cell); + this._oneOffsTable.appendChild(row); + row = document.createElementNS(HTML_NS, "tr"); + cell = document.createElementNS(HTML_NS, "td"); + row.setAttribute("class", "contentSearchSuggestionsContainer"); + cell.setAttribute("class", "contentSearchSuggestionsContainer"); + } + let button = document.createElementNS(HTML_NS, "button"); + button.setAttribute("class", "contentSearchOneOffItem"); + let img = document.createElementNS(HTML_NS, "img"); + let uri; + if (engine.iconBuffer) { + uri = this._getFaviconURIFromBuffer(engine.iconBuffer); + } + else { + uri = this._getImageURIForCurrentResolution( + "chrome://browser/skin/search-engine-placeholder.png"); + } + img.setAttribute("src", uri); + img.addEventListener("load", function imgLoad() { + img.removeEventListener("load", imgLoad); + URL.revokeObjectURL(uri); + }); + button.appendChild(img); + button.style.width = buttonWidth + "px"; + button.setAttribute("title", engine.name); + + button.engineName = engine.name; + button.addEventListener("click", this); + button.addEventListener("mousemove", this); + + if (engines.length - i <= enginesPerRow - (i % enginesPerRow)) { + button.classList.add("last-row"); + } + + if ((i + 1) % enginesPerRow == 0) { + button.classList.add("end-of-row"); + } + + button.id = ONE_OFF_ID_PREFIX + i; + cell.appendChild(button); + this._oneOffButtons.push(button); + } + row.appendChild(cell); + this._oneOffsTable.appendChild(row); + this._oneOffsTable.hidden = false; + }, + + _sendMsg: function (type, data=null) { + dispatchEvent(new CustomEvent("ContentSearchClient", { + detail: { + type: type, + data: data, + }, + })); + }, +}; + +return ContentSearchUIController; +})(); diff --git a/application/basilisk/base/content/defaultthemes/1.footer.jpg b/application/basilisk/base/content/defaultthemes/1.footer.jpg Binary files differnew file mode 100644 index 000000000..cb5ff2705 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/1.footer.jpg diff --git a/application/basilisk/base/content/defaultthemes/1.header.jpg b/application/basilisk/base/content/defaultthemes/1.header.jpg Binary files differnew file mode 100644 index 000000000..58c52f86a --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/1.header.jpg diff --git a/application/basilisk/base/content/defaultthemes/1.icon.jpg b/application/basilisk/base/content/defaultthemes/1.icon.jpg Binary files differnew file mode 100644 index 000000000..67b316d9f --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/1.icon.jpg diff --git a/application/basilisk/base/content/defaultthemes/1.preview.jpg b/application/basilisk/base/content/defaultthemes/1.preview.jpg Binary files differnew file mode 100644 index 000000000..1394c5936 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/1.preview.jpg diff --git a/application/basilisk/base/content/defaultthemes/2.footer.jpg b/application/basilisk/base/content/defaultthemes/2.footer.jpg Binary files differnew file mode 100644 index 000000000..a8cce0ef8 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/2.footer.jpg diff --git a/application/basilisk/base/content/defaultthemes/2.header.jpg b/application/basilisk/base/content/defaultthemes/2.header.jpg Binary files differnew file mode 100644 index 000000000..8a4aec353 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/2.header.jpg diff --git a/application/basilisk/base/content/defaultthemes/2.icon.jpg b/application/basilisk/base/content/defaultthemes/2.icon.jpg Binary files differnew file mode 100644 index 000000000..4eeed30ca --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/2.icon.jpg diff --git a/application/basilisk/base/content/defaultthemes/2.preview.jpg b/application/basilisk/base/content/defaultthemes/2.preview.jpg Binary files differnew file mode 100644 index 000000000..cc45cfc94 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/2.preview.jpg diff --git a/application/basilisk/base/content/defaultthemes/3.footer.png b/application/basilisk/base/content/defaultthemes/3.footer.png Binary files differnew file mode 100644 index 000000000..235a5ad54 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/3.footer.png diff --git a/application/basilisk/base/content/defaultthemes/3.header.png b/application/basilisk/base/content/defaultthemes/3.header.png Binary files differnew file mode 100644 index 000000000..b25d673c4 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/3.header.png diff --git a/application/basilisk/base/content/defaultthemes/3.icon.png b/application/basilisk/base/content/defaultthemes/3.icon.png Binary files differnew file mode 100644 index 000000000..186519d3e --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/3.icon.png diff --git a/application/basilisk/base/content/defaultthemes/3.preview.png b/application/basilisk/base/content/defaultthemes/3.preview.png Binary files differnew file mode 100644 index 000000000..46850f139 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/3.preview.png diff --git a/application/basilisk/base/content/defaultthemes/4.footer.png b/application/basilisk/base/content/defaultthemes/4.footer.png Binary files differnew file mode 100644 index 000000000..bd944d58b --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/4.footer.png diff --git a/application/basilisk/base/content/defaultthemes/4.header.png b/application/basilisk/base/content/defaultthemes/4.header.png Binary files differnew file mode 100644 index 000000000..1487ff10e --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/4.header.png diff --git a/application/basilisk/base/content/defaultthemes/4.icon.png b/application/basilisk/base/content/defaultthemes/4.icon.png Binary files differnew file mode 100644 index 000000000..8dd688ef1 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/4.icon.png diff --git a/application/basilisk/base/content/defaultthemes/4.preview.png b/application/basilisk/base/content/defaultthemes/4.preview.png Binary files differnew file mode 100644 index 000000000..36ac2a0bf --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/4.preview.png diff --git a/application/basilisk/base/content/defaultthemes/5.footer.png b/application/basilisk/base/content/defaultthemes/5.footer.png Binary files differnew file mode 100644 index 000000000..8e87c69a0 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/5.footer.png diff --git a/application/basilisk/base/content/defaultthemes/5.header.png b/application/basilisk/base/content/defaultthemes/5.header.png Binary files differnew file mode 100644 index 000000000..8e87c69a0 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/5.header.png diff --git a/application/basilisk/base/content/defaultthemes/5.icon.jpg b/application/basilisk/base/content/defaultthemes/5.icon.jpg Binary files differnew file mode 100644 index 000000000..b3e103ee5 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/5.icon.jpg diff --git a/application/basilisk/base/content/defaultthemes/5.preview.jpg b/application/basilisk/base/content/defaultthemes/5.preview.jpg Binary files differnew file mode 100644 index 000000000..78c2f1248 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/5.preview.jpg diff --git a/application/basilisk/base/content/defaultthemes/devedition.header.png b/application/basilisk/base/content/defaultthemes/devedition.header.png Binary files differnew file mode 100644 index 000000000..e4e8dcaa3 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/devedition.header.png diff --git a/application/basilisk/base/content/defaultthemes/devedition.icon.png b/application/basilisk/base/content/defaultthemes/devedition.icon.png Binary files differnew file mode 100644 index 000000000..04cfba796 --- /dev/null +++ b/application/basilisk/base/content/defaultthemes/devedition.icon.png diff --git a/application/basilisk/base/content/docs/sslerrorreport/dataformat.rst b/application/basilisk/base/content/docs/sslerrorreport/dataformat.rst new file mode 100644 index 000000000..f69dc7417 --- /dev/null +++ b/application/basilisk/base/content/docs/sslerrorreport/dataformat.rst @@ -0,0 +1,54 @@ +.. _sslerrorreport_dataformat: + +============== +Payload Format +============== + +An example report:: + + { + "hostname":"example.com", + "port":443, + "timestamp":1413490449, + "errorCode":-16384, + "failedCertChain":[ + ], + "userAgent":"Mozilla/5.0 (X11; Linux x86_64; rv:36.0) Gecko/20100101 Firefox/36.0", + "version":1, + "build":"20141022164419", + "product":"Firefox", + "channel":"default" + } + +Where the data represents the following: + +"hostname" + The name of the host the connection was being made to. + +"port" + The TCP port the connection was being made to. + +"timestamp" + The (local) time at which the report was generated. Seconds since 1 Jan 1970, + UTC. + +"errorCode" + The error code. This is the error code from certificate verification. Here's a small list of the most commonly-encountered errors: + https://wiki.mozilla.org/SecurityEngineering/x509Certs#Error_Codes_in_Firefox + In theory many of the errors from sslerr.h, secerr.h, and pkixnss.h could be encountered. We're starting with just MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE, which means that key pinning failed (i.e. there wasn't an intersection between the keys in any computed trusted certificate chain and the expected list of keys for the domain the user is attempting to connect to). + +"failedCertChain" + The certificate chain which caused the pinning violation (array of base64 + encoded PEM) + +"user agent" + The user agent string of the browser sending the report + +"build" + The build ID + +"product" + The product name + +"channel" + The user's release channel diff --git a/application/basilisk/base/content/docs/sslerrorreport/index.rst b/application/basilisk/base/content/docs/sslerrorreport/index.rst new file mode 100644 index 000000000..2c4210113 --- /dev/null +++ b/application/basilisk/base/content/docs/sslerrorreport/index.rst @@ -0,0 +1,15 @@ +.. _sslerrorreport: + +=================== +SSL Error Reporting +=================== + +With the introduction of HPKP, it becomes useful to be able to capture data +on pin violations. SSL Error Reporting is an opt-in mechanism to allow users +to send data on such violations to mozilla. + +.. toctree:: + :maxdepth: 1 + + dataformat + preferences diff --git a/application/basilisk/base/content/docs/sslerrorreport/preferences.rst b/application/basilisk/base/content/docs/sslerrorreport/preferences.rst new file mode 100644 index 000000000..ed6f384c2 --- /dev/null +++ b/application/basilisk/base/content/docs/sslerrorreport/preferences.rst @@ -0,0 +1,23 @@ +.. _healthreport_preferences: + +=========== +Preferences +=========== + +The following preferences are used by SSL Error reporting: + +"security.ssl.errorReporting.enabled" + Should the SSL Error Reporting UI be shown on pin violations? Default + value: ``true`` + +"security.ssl.errorReporting.url" + Where should SSL error reports be sent? Default value: + ``https://incoming.telemetry.mozilla.org/submit/sslreports/`` + +"security.ssl.errorReporting.automatic" + Should error reports be sent without user interaction. Default value: + ``false``. Note: this pref is overridden by the value of + ``security.ssl.errorReporting.enabled`` + This is only set when specifically requested by the user. The user can set + this value (or unset it) by checking the "Automatically report errors in the + future" checkbox when about:neterror is displayed for SSL Errors. diff --git a/application/basilisk/base/content/downloadManagerOverlay.xul b/application/basilisk/base/content/downloadManagerOverlay.xul new file mode 100644 index 000000000..9987820cb --- /dev/null +++ b/application/basilisk/base/content/downloadManagerOverlay.xul @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?> + +<overlay id="downloadManagerOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<window id="downloadManager"> + +#include browserMountPoints.inc + +<script type="application/javascript"><![CDATA[ + window.addEventListener("load", function(event) { + // Bug 405696: Map Edit -> Find command to the download manager's command + var findMenuItem = document.getElementById("menu_find"); + findMenuItem.setAttribute("command", "cmd_findDownload"); + findMenuItem.setAttribute("key", "key_findDownload"); + + // Bug 429614: Map Edit -> Select All command to download manager's command + let selectAllMenuItem = document.getElementById("menu_selectAll"); + selectAllMenuItem.setAttribute("command", "cmd_selectAllDownloads"); + selectAllMenuItem.setAttribute("key", "key_selectAllDownloads"); + }, false); +]]></script> + +</window> + +</overlay> diff --git a/application/basilisk/base/content/gcli_sec_bad.svg b/application/basilisk/base/content/gcli_sec_bad.svg new file mode 100644 index 000000000..4f440eb6b --- /dev/null +++ b/application/basilisk/base/content/gcli_sec_bad.svg @@ -0,0 +1,7 @@ +<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg"> + <circle cx="15" cy="15" r="15" fill="#e74c3c"/> + <g stroke="white" stroke-width="3"> + <line x1="9" y1="9" x2="21" y2="21"/> + <line x1="21" y1="9" x2="9" y2="21"/> + </g> +</svg>
\ No newline at end of file diff --git a/application/basilisk/base/content/gcli_sec_good.svg b/application/basilisk/base/content/gcli_sec_good.svg new file mode 100644 index 000000000..f1b33d073 --- /dev/null +++ b/application/basilisk/base/content/gcli_sec_good.svg @@ -0,0 +1,4 @@ +<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60"> + <circle cx="30" cy="30" r="30" fill="#2CBB0F"/> + <polygon points="17,32 25,39 26,39 44,18 45,18 48,21 27,46 25,46 14,36 13,36 16,33 16,32" fill="white"/> +</svg>
\ No newline at end of file diff --git a/application/basilisk/base/content/gcli_sec_moderate.svg b/application/basilisk/base/content/gcli_sec_moderate.svg new file mode 100644 index 000000000..3a88aa468 --- /dev/null +++ b/application/basilisk/base/content/gcli_sec_moderate.svg @@ -0,0 +1,4 @@ +<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg"> + <circle cx="15" cy="15" r="15" fill="#F5B400"/> + <rect x="7.5" y="13" width="15" height="4" fill="white"/> +</svg>
\ No newline at end of file diff --git a/application/basilisk/base/content/global-scripts.inc b/application/basilisk/base/content/global-scripts.inc new file mode 100644 index 000000000..eef21e15e --- /dev/null +++ b/application/basilisk/base/content/global-scripts.inc @@ -0,0 +1,39 @@ +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<script type="application/javascript" src="chrome://global/content/printUtils.js"/> +<script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/> +<script type="application/javascript" src="chrome://browser/content/places/browserPlacesViews.js"/> +<script type="application/javascript" src="chrome://browser/content/browser.js"/> +<script type="application/javascript" src="chrome://browser/content/customizableui/panelUI.js"/> +<script type="application/javascript" src="chrome://global/content/viewSourceUtils.js"/> + +<script type="application/javascript" src="chrome://browser/content/browser-addons.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-captivePortal.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-ctrlTab.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-customization.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-devedition.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-feeds.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-fullScreenAndPointerLock.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-fullZoom.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-gestureSupport.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-media.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-places.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-plugins.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-refreshblocker.js"/> +#ifdef MOZ_SAFE_BROWSING +<script type="application/javascript" src="chrome://browser/content/browser-safebrowsing.js"/> +#endif +<script type="application/javascript" src="chrome://browser/content/browser-sidebar.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-syncui.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-tabsintitlebar.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-thumbnails.js"/> +<script type="application/javascript" src="chrome://browser/content/browser-trackingprotection.js"/> + +#ifdef MOZ_DATA_REPORTING +<script type="application/javascript" src="chrome://browser/content/browser-data-submission-info-bar.js"/> +#endif + +<script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/> diff --git a/application/basilisk/base/content/hiddenWindow.xul b/application/basilisk/base/content/hiddenWindow.xul new file mode 100644 index 000000000..c708071cd --- /dev/null +++ b/application/basilisk/base/content/hiddenWindow.xul @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +# -*- Mode: HTML -*- +# +# 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/. + +#ifdef XP_MACOSX +<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?> +<?xml-stylesheet href="chrome://browser/skin/webRTC-indicator.css" type="text/css"?> + +<window id="main-window" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +#include browserMountPoints.inc + +</window> + +#endif diff --git a/application/basilisk/base/content/jsConsoleOverlay.xul b/application/basilisk/base/content/jsConsoleOverlay.xul new file mode 100644 index 000000000..1bc518d4f --- /dev/null +++ b/application/basilisk/base/content/jsConsoleOverlay.xul @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?> + +<overlay id="jsConsoleOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<window id="JSConsoleWindow"> + +#include browserMountPoints.inc + +</window> + +</overlay> diff --git a/application/basilisk/base/content/macBrowserOverlay.xul b/application/basilisk/base/content/macBrowserOverlay.xul new file mode 100644 index 000000000..4b2cb0d89 --- /dev/null +++ b/application/basilisk/base/content/macBrowserOverlay.xul @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +# -*- Mode: HTML -*- +# +# 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/. + +#define MAC_NON_BROWSER_WINDOW + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?> + +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +# All DTD information is stored in a separate file so that it can be shared by +# hiddenWindow.xul. +#include browser-doctype.inc + +<overlay id="hidden-overlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +# All JS files which are not content (only) dependent that browser.xul +# wishes to include *must* go into the global-scripts.inc file +# so that they can be shared by this overlay. +#include global-scripts.inc + +<script type="application/javascript"> + function OpenBrowserWindowFromDockMenu(options) { + let win = OpenBrowserWindow(options); + win.addEventListener("load", function listener() { + win.removeEventListener("load", listener); + let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"] + .getService(Ci.nsIMacDockSupport); + dockSupport.activateApplication(true); + }); + + return win; + } + + addEventListener("load", function() { gBrowserInit.nonBrowserWindowStartup() }, false); + addEventListener("unload", function() { gBrowserInit.nonBrowserWindowShutdown() }, false); +</script> + +# All sets except for popupsets (commands, keys, stringbundles and broadcasters) *must* go into the +# browser-sets.inc file for sharing with hiddenWindow.xul. +#include browser-sets.inc + +# The entire main menubar is placed into browser-menubar.inc, so that it can be shared by +# hiddenWindow.xul. +#include browser-menubar.inc + +<!-- Dock menu --> +<popupset> + <menupopup id="menu_mac_dockmenu"> + <!-- The command cannot be cmd_newNavigator because we need to activate + the application. --> + <menuitem label="&newNavigatorCmd.label;" oncommand="OpenBrowserWindowFromDockMenu();" + id="macDockMenuNewWindow" /> + <menuitem label="&newPrivateWindow.label;" oncommand="OpenBrowserWindowFromDockMenu({private: true});" /> + </menupopup> +</popupset> + +</overlay> diff --git a/application/basilisk/base/content/newtab/alternativeDefaultSites.json b/application/basilisk/base/content/newtab/alternativeDefaultSites.json new file mode 100644 index 000000000..018d3edcc --- /dev/null +++ b/application/basilisk/base/content/newtab/alternativeDefaultSites.json @@ -0,0 +1,50 @@ +{ + "directory": [ + { + "bgColor": "#ffffff", + "directoryId": 10000000, + "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAIX0lEQVR4nO1afXDbZh1+sxbWsbGvsrLbuBt36766sR3sOEgiuU4kx2vTpFGKm0SKsyyOlI8laUZTulLojQEHx4CyAaNnt72yrivklo1YSpxcW7YVbKXHgCvduCvroDt6o6OtLSWN1MRWX/5I4imyJMu2bPNHnrvfSZbf6P09z+/jfV+3ACxhCUsoAEouNWGlMRLri5GuZwUSe1Ug8bcFEjsjUPgHAoX/UyCxkyKJjQoU/iOhCd8WJSvW/8vpXFFsx7PGeaJsVaytul+g8KMiic+IFA7NTNB/rggk/jeRcu26RGFris3JEqIUtkFsdo2KFK6kI52pCSR2UmirZgY9YFmxeaYg1uyqFSj8lMXI5mofRb3rOwEAJcXmDS72ex8QvO43skjv3MeT2DtRX2150cgLzVUDIonF8xBh64KQ2FWRwncPejyFK4tYq/Nmoa1mPE/pnZ1ILe4TFxpdd+SdvOhxrhabXaeLTVjfsHfzSj5K4V8QKfyjnGs3P2UiCt/v/1LeyAsNlXebkbeRSOZ/Q+JCrBH/Yt7IX+qlbhS96dM+Y+dJLC6S+BWRwmazFizf5AEAQPRtHLEhukqMwo8KTfg2oamy8t+e0uvUczwDwDWxhopHBBLvElrc+wUSm/r/IN9a3ZtrbYqUe9eHTc7PZDLvoAcsi/lq2wQSO1M88k93r7ayjzdI76sxEv/5hdryT+foRknM696q9qMg5AEAQHx83WhWtU5i56PtG1E7fYkO+B4SKNd7AoWLBSEv9G3Asol8zOs+ddFTeWc+fDrXgq2MNmEP5+PdKZB/XR4RvRnWe8tjJ8+1ECsL4mA+EQ+vcCg8gHHuPjjZvc5S6guU6/3/POG5rdi+24IEv+KwEgFQ4QFU/rAKTu90pal5fCY60PlQsf22BefHwfUKD2YVfl6ACIAKfx2UX0QNBYj6arqL7bdtSIQBnYy+2iIAzg49Cid9bm3qv11sn21FggeHUwSILGQCgIljd8Kpreq+4Koqts+2QuGBmBL9FLsFSj+sgAKFnyq2v7ZCDoO7dKOvvc7fS3vcW4rts61I8OBxXdI6/UCJADgVvmGVHfMSAelgfUA6VBeQDqmv2nuzZ3rf6Y2r3SM7DB1RwmCnOsLaiGuenbWD/JwAcqI+IEPCL8H6gAwX7omADIn5z/Wqe0Iz1sgI3edSv6EjcR78OEk4jQiJCNhnlwD1ATlBmDqdCUHzcYRf3mXoSCIMXjdK95SeEAbfslUAk4iaEc1EsLnx0vNmAhw3q3mNAFvtFCBd2i8i4bdWAgYCDBkLwIMJw8antTDos12ARSTNM8JK5Amd8URAPmosQAS8Zdr41N/lIQOS0Z2/pkZe0iWoS35+rIY8rPfLvzcWgAdDljMgAr5tlwCEWgArjUxnnJXSmX8eMnQkPrH8OasCJMJgfyEE0KZxMrJ+i2WgyZq6gHTQ0BGFv+abujs//ab4gV0CqEvAar1n0gQXizj9PUNHEmHgTSFt1At4O3eCH+8D0tW2YUmoMkKvfyQzwC/vMHREegN8Tpe4zjIY55fD14+X9tgiwC8mV2ZrHS/LGOGXRd3y8aeKVee/3GrqjBIGMcMsmL/GJu6AW4+1QZRl/mKHADmipD4gz1jJHCIgw9oXJ+8zfVsi/ImXU7bDquvfI2ugZ7wHoiwDUY6BTrbTWRie+ti0d7rGrHTU5UD45StpX5iILPMZrQTD4Q0QG+2GKMckzcF28AXgaYiv7ZNesdokCb90PO0LP2TBpxQezKpFkPmb4Q/ebFlEHOWYZBY8FtriKwDXFDAHhLvr/bJi9TzQtPeycQNUI3HihkML5M9NfB4+caRTl/jHRsvtb333gTzzTUHDPonNZFkkfjVl7Zfr+MRtiMID+McICqtDvfrE2cWfHRxz2j321K155pxE84HLLWb7BK0odX75TEYTBI43hB1ch0HE554h7GJhKrkn/4wPMjfliXMSO4alcsIvx/UanZE17pcGMppkU2iL2zDt9QSZtwqu66/Okdbb88Qd7PydVFYfkGVthLWHHk1JyOteuHRjxpNh3JNjRtE3EgDlGIiwzLlqrq/MbvKtLwkewi/PalPc8PyQPDdIP8lqQvdw92qEZWbNyBqJgnD0VTTI7H74Je/1uRL/8mDr7VWjfYeJkecgdfC/6c8JavPL0+6fitn3JtdIzxbdqLPqiNMmwtAxR7BzZ+mgL2MnHGzHPQhL/wzlmCsLc64PbYPU4Xd1zwN6WdCwT/p61uQXsH6sL5TS/dOUgY4QCZRlxsuD9EDZsM/x4KDnk5ppSsqGfHehw3QjytHfQVj6HaP+U8F1wZbhMf1Dkerzpr3Sn4Ad/5/Yc2T7TQ6OOW2YBWZimAsVRzlaRoL0DMLRV/WW19SVh05+v5nbAxsPCPpZ4Jclwj95b87kF+BgO+5Bg8yFFHKZZkO68XqimoyvGd0FqUNnU/pA1yvTdbaRXwDCtj2CsszFjFLfSjZoRbEiqEqoqtE+6H2VT5LfvPfydtvJJ0V4jb537QhzxjLRdMSsimIhq5q538DNgSnbfqs0FoHrusUd6j2ScdTNyGZTNovfHa8Z6/fmnbwaTq5r+1wj0ziZrjlmvIKYP3ew9Hvk+NOPFpT8AjxHd6ypGOl607Z+YDUL5s4iChpkdq8b7b22KOTVQIPtGxet3bmQtVA6eKhnyBP6hvnPW8UAwvpq1nIdoeS6bpWY0fK3aMfJTCMc/csqtuf+YvNMi68Mt38WDTJPoSx9DGHpmaxLI8hcqBzpCiDB9ibnM87lxeaVLUrKg+1fRVimby3LPItw9G9Rlj6Bssw/EI4+i3DM+0iQPomy9JiD69hdztI7SoPtTWWvddry7w5LWIIx/gdCuvcjoZqlLQAAAABJRU5ErkJggg==", + "type": "affiliate", + "title": "Google", + "url": "https://www.google.com/" + }, + { + "bgColor": "#E62117", + "directoryId": 10000001, + "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAACd0lEQVR4nO3av0tVYRzH8WOIBEVDUUIGCa32FwQ5OrS42NKeRUOTdGmoqUGiLVBqa5UG+7WUREGiEqhTDkVJEA2iYZBkxKvhHumo96o3uM9z0ucNZ7zn834+XO59znm+WZZIJBKJRCKRSGwCx3AWfbiIaxjEPYzgOd7gLWbwIb8WsFjjWrU9q3U+u1C4/0yeOZ47jOROg6jkrn25e3uji27NbzC9A9n/hWnVNbVut/hOTMZ1bSqT6Ky3+A58jOsXhE/o2Lj4FryILBaSF2gpFtAT2ygCPcUCHse2icDTtcW3YSW2TQRW0JahK7ZJRLoy9Ma2iEhvhoHYFhEZyDAU2yIiwxkeBgj6FSDjXxjN8CpA0A1cxucAWY0wkeFdgKBK/pe7H1fxNUDmTpjLMB8gqLJh631A9dF6IUD2Vsxn+BYgaF0BhSIO4WYgh1osZVgOEFSzgEIRh3EL3wO4FFnOAgVtWUChiKO4gx+BvJSqgEIRx3EXP5stVsoCCkWcxH1N3EeUuoBCEafwshlipS8AJzTxW1DaAnAEtzX5B7F0BeAgrmMphFhpClB9M3UFXwI5oQQFYB8u4H0gl3VE3QninOoxVyyWozwL4AxeB8jdjqWgT4M4jUcB8nbKfKj3AUN4gN8BshphLtQbobIyEeqdYFkZzTAc2yIiw+lcQDoZSmeDe/t0ON+gPIltE4Fnxa3pnp8QacFYbKOAjCnOCOUldKhOUO12Nk+JFUroxFRkwWYypd6cYKGEVvTbfZOil2w3KVqjjHZ047xqKRWbZ4XHVed2Z/2d5a0167vYgHC9z6/df9bWs8L9uXO3RmeFE4lEIpFIJPYKfwAcall+TY3q/wAAAABJRU5ErkJggg==", + "type": "affiliate", + "title": "YouTube", + "url": "https://www.youtube.com/" + }, + { + "directoryId": 10000002, + "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAACT0lEQVR4nO3bzWsTQRgG8PlPHMVLqRVMoQiFehJyUBAFtRCFHspExaaSoFg8CMGPSg+R3kQhh3osIbOlMVrbGKtrsbYESTbB0gqNVGu1WCkWutnXg+ihQjqxi7Oz8w481533+V32iyGEEEIZ76Rho0SZ4ewOG+D3UMatXSx9nPwqb5ySPZC0dPMzhDJelD6IrDCjQijjtvRBJIWGeY3IHkJ2EED2ALKDALIHkB2lAJoujkIwnoNQwoQL96aha3AKQgkTTtyZhKM383D4+gQEoll/AQRiWbg1XILZ+VWwaw6IrE3bgZbejNoAe8+PwEC6DOsbm0Klt679lxQGCESzMD335Z+KKw+wrzcDpcVvOyqvNMCwubjj8soCnBx44Up5ZQGeFZf1BWi7/BhqjthtzpcAseRsQwVrjgNzS9/hZWUFJq3Pf6U5MqoWQHJ8Qbh8aqoKB688cWVfzwBMvP0kVN6srMCec+7t6xmAmfmvQgCRBzOu7usZgHJ1TQjgyI283gCHrj3VG6C9bwwBEAABEEAtgAPRR9A3VKibpdUfQgD9KWvba529+8pbAMF4TqicW+th/r3eAP0pS2+Anvtv9AY4dvu53gCtMfGfI74DWN+wG5rNdwDl6preAGOFj3oDJMcXvAfQHMlAKGHWTeWD2KPw1aFC3et0NPi9wDPvAq/fif0LDMZzru6LALKLIwACIAACIAACIAACIAACIAACIAACIAACIMD/BvDKwUkZAJRx2zNHZyUBFAntTp+WXV4iQCf5c3yecUsXAMq49bv8Txsl6ZCTabOAAAAAAElFTkSuQmCC", + "title": "Facebook", + "type": "affiliate", + "url": "https://www.facebook.com/" + }, + { + "bgColor": "#ffffff", + "directoryId": 10000003, + "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAA/CAYAAABQHc7KAAAEyUlEQVRoge2abYhVRRjHf7veVqSFhIQwa7E3ojXNpSiNgl0S+tAHKUjohbQohUgqKKwMSXpbsATdqOgFU4zKpAzMioploxI/WFBZmhRlL/S2lbpsudvd24dzjve5c58558ycey9B84MB95x5/vM/d+bMzDNHCAQCgUAgEAgE/rf0AOd6lJmKVrdDfI8SfxawIKNcEscfa8TOzBFrlj6Aimd5XnmAbx01TD5xiL3IiH3Y8zm4DXgaGALGMip/DDwHDACXKQ9wA/AiMJyi8T3wAnCvEr841t9tiR2JY/uB443YC4BHgA3ApyntTxD90FuAlaaBU4EfLIHvKIZtnAT8ZtE5O6fGCiNuFJiTM7YNWKe0PQScmRV8ncX4+zkbT1hq0bkyZ/wyI+5ux/bPM+I/ACbnCSyhv8tloMvBQAnYr+h8mDP+PRHzGXCMQ9sA14j4ceAMl+A70HtvjaMJ22jqy4i70Kh/sWO7AO+K+Jdcg6cCh6k3/gfQ6aBTAr5UdN7OiHtL1H3NxXhMN9Fkl2jM89BgPXrv3eKoc71Fx2ZqvqhTJv+kKRkQGjs94gE4PTZgGt9DNMvmpQR8pehst9R/Q9TZ5OG7EzgkNK720DjKNvTeu9RRZ4miMUH9bnCeuH8EOMXD881C40egw0PjKL3oP8Cbjjq2FWGrUW+HuDfg6XmP0NA2W060GYKy97odtbQVoQzMiu+fL66PACd4+O0VGqPANA+NOm5CHwVPOeqUgH2Kzub4/uvi2gOeXrcU8GdlCtG7ZBr/C5juqHWtojMOXCX+HgaO8/B5YqyV6GjZpjf3oI+C+x11JgF7FZ1/xL/v9PS4WmgMempYsW2MfsdtYwTRsqT9mBXgANGIc6WD2lG60EMjk0fRTS931GkHPrdo3ejpbZHQ2B+30XC6iNZm0/Q3RBOcC7Yc4WRPb0NC43ZPjVxsRDe+yFHHHLJJWevhabaIP0z0ujaNWdQmGUn5yEPrPkVnBPe1+0kRv97DhzPb0UdBr6OObZv9kIOGnJzLRPlL0+lFN+6yPe5BH0kV4E/yD+PlIm6bQ/uF2Um98Qlgbs74V5V4WVbl0GgDvsB/BBbiCnTj2lG5yVyqvX+QaOSYOsNk7y8WiPo+c1Ah2tFPesbJTmFfEfX7qU2BZVmRoSPnkMU+D1EUW5L0WErMHKq9P0o149NeqZ+w7wq7qO77f02p11Qmo6/laWnoVlFvnbh+uaJTAW616Dwo6rjmIw3lLnTjminZ+38DM8Q92yv1HfUnOh3AL/H9I7hnpA1lKrXnb0nRkqSXxf0nFC3bK7XMqCdT6o2NeIiirEE3Lvfks6kesI6hf1m2nTt8TW2uIeeLhub8vsxAT5IOUB2+8qTm2RStlYqOnOV7xLXBRj5EUWxJ0hKic/2k98dJ367azh32Es0Tz4hrTcn5fZE9I8s+ajc6m20CgrUWrdVEK0yFJub8RRhEN56UMjk+TVO7xtuK6yFMS1hIuuk82+SETSk6h3A/hmsJ7egfPypEh555ej/hHOzZYktyfl9keiqLzzc+LUlqWc7vSyfRJ3Rpegw4zUOrj/ofoKU5vy/91Jp+vIDWLkPL/J9h/0mmAT9TXa58vvIkzKe6ydpQ3FogEAgEAoFAIBCI+Rf5q90lTPZHIwAAAABJRU5ErkJggg==", + "title": "Wikipedia", + "type": "affiliate", + "url": "https://www.wikipedia.org/" + }, + { + "bgColor": "#400090", + "directoryId": 10000004, + "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAD3ElEQVR4nO3bS4xecxjH8WmLBVVEx63JBIkEISkhJqURpAsZQioWqpeNSxraYlKpSwiDIRY0aSakqgS9qHRBIhKxYdFFFxWXIGkilYZEUCKj4zLzsfi/LN55zpiZc/lr+/6Ss3nfc57ze77n9r88/y7swxaswgLM7jpMhTlYiNWtnPd1Ga9RfIVtuB+L0J3b/FSFeejDQ9iBvRhrTzYCUKTv8RFewlrcgAtxQsYk5+Ii3IQHsBm78ONkk5oKgIn0Mz7BO3gBA7gHS3EtLsXZre1UnITj2pI5vvV7D85BL67DCvRjEJvwHj7HcBXGqwJwyKoDILeB3OoAyG0gt8oAGMYj0qdnJ17DY3i2vK1CPYmn8Cq2YwgP4rfpBix7BwwE3+aZ2F0ybqQPMSM439NlgpYFMILzAlO9glZXCf2F+cF5LsafZQJX8Q4oujKbK4j9j4aC+EdhT9nAVb0Ebw8MduNABbF/wMlB/LUVxK4MwE84JTC5uoLYK4O48/BrBbEr/Qy+XnCbfloi5h7MCuJurcSxagGMYWFg9qr/SbxQVTeEiq7Ym9OI9UYQ52ipJ1iZ6mgJ3h0Y7zG17usvOC2I01+12ToAHBCMIGHdFGKsCo4/QwJTqerqC2wMEjgGX0zi2KLH6JU6jNYFYBSXBElc/R/HjaE3OO4y1bYs/1WdvcFd4hbilgmOie6cGa1Ytaju7vDSIKHTxc9yUYtvWZ0G6wawH8cGSa0J9r0t2G92K0ZtamJA5PEgsfaOTNHjMlC3uSYADKMnSG6B9GIr6uqehYN1m2tqSGxre4KtJDdhfcF/O5ow1hSAMVweJNmNOcHvlbb3J1KTg6K7MTO62m3Jz1LBQMdk1fSo8IpJALizSUNNA/jWBNPvOFGahG1MOeYFnpgAwHNNm8kB4CDODJI/F380bSbXzNAzAYD1OYzkAjAYABjKYaQDIMdJxQDqnFMsVC4A6wIAgzmMdADkOKkYwKM5jOQCcFcAYCqjxpUpF4Bovu+IB3BfDiO5AIzrFWJlDiO5ACzpABgP4I4cRnIBWBwAWJLDSC4AfR0A4wHcksNILgCLAgB9OYzkAnDFkQ5gDZbj3ta2XCp5bVydYuncBnKrAyC3gdzqAMhtILfqBDCCb6SZ3velatGXsUFa5NC+vdjatrf2/QAfSyUyv9dlsgyAEXyGt/G8VBl+PS7A3PaGTllJCy7n40Zp8GQD3sWXSgCaDIBRqcBxGx7GYmll57hixlySaojPx83SuqW3pLXC0wKwX7oF+3GloILjUJE03X6NtAh8J76LAOzFRtwqKGY63CStX14mLen5+m9Vgqdbrd+9cgAAAABJRU5ErkJggg==", + "title": "Yahoo!", + "type": "affiliate", + "url": "https://www.yahoo.com/" + }, + { + "directoryId": 10000005, + "imageURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAJdklEQVR4nO1ba1QTZwL9iCY8hCDxDUpYOIUDBOVZAlJ5NQXRFlEQ8YGiRaqESlhxeRQ3FAWURRC6FYglqBHTEaI8Eg+CRMRatfKwLNL6oqer1h57XEWYbwJK9sdKixiSmRCCbrnn3D8w351770wy3zd8ADCJSUxiPMHhcPS3bdv2/tq1a9MWL16MODs7t5iamt4zMjLq0dfX76VSqU8sLCzueHt7S+3t7QtjYmLWJyYmzp1o32NGSUmJd0BAQLmRkVEPAEBOhCQS6bm3t7c0MjIyHACgMyEB1AGCILTY2NgUOzu7fwGCoUfj/Pnz/x0XF/fXgoICXW3nIYTs7Owlpqam94CGgo+ktbX1j4mJiY7azIQbMTExu3V0dF6AcQo/RF1dXYggSIj2kuFAfHx8Lhjn4ODVElCxWOygpXjKkZWVxQZaDD9EOp3eXV9fb6yVkKOho6Njgb6+fp8ig9pgfHx8gjZyjgYdX1/fhtHMqSKJRHpOJpNl6o4HAMidnJxatJJUEWpraz3xmBxOExOT30JDQ/+elpZmD14+26uqqubExcVFzJs3T53H5uCJEydMtZN4BKKiovKJmA0KChLu3bt3zmh6Bw4c0F+3bl0BEU0AgLygoCByXAKqgq2tbSdek0FBQTV4dZlM5kW8ugAAOZPJ/IeGIuHH9evXp+no6DzHa7K+vt4Lr3ZiYuIOvLoAALmnp6dQQ7HwIzc31xqvQRsbmy5AYC6fnJzsjlcbACD38/M7p5lUBBEeHp5lbm7+E4lEUnonREdHpxPRLSwsNFWmN5Kurq5XNZNITfj4+EwtLCw0dXFxcebz+UFhYWFbmExm6qZNm/IjIiKyampqDIjonTp1ajp4mwrQNP70BfD5/D9PAcXFxQZcLteOwWCw9u/f/3FwcHC6paVlOfg/K4CUlpbmEB0dvSEkJCTLxsamwtnZuXXmzJmPAACDgEBYRXxjC8jJyfFYtmzZYWNj48dgjCGV8Y0qICsryyQiIiKVTqf/BMYxNHgTC8jPz/+ISqU+AVoKDt6kAnbs2MGeMmXKANByePAmFBAbG5utyJi2OKEFJCcnx+MxqYizZs361d/fvyw9PX0bg8FgZWRk2FZVVc1pampaQERnwgoQi8VLVK0DRpJMJstWrlx5hMPhvMflckmKdN+amSDRdbuenl5fRkaGnyrdt6KAsrKyD4iYpFKpTy9cuPAuHu23ogAmk9lIxGRCQsLneLVLS0tnEdHWegFisXgu0b8ANTc3W+LVT0pKsiSirfUCDh06tJyIQSsrq5tE9PPy8jyI6Gu9gMDAQA5Bg9VE9Hfu3BlFRJ/BYHSMPRUBsNnsfUQMksnkciL6q1ev5hHRNzQ07AHa3D9A9N19QEAA7lfiAAAdOp3eTUQfACBHEMR87MlwgmgBZmZm9wHOK1RUVLSFiPYQd+/eHaOheKrB4XCyiBrk8XguqnTb29vNDAwMeolqAwDkCxcuvKbBiMoREhLyKVGDy5cvr1Cm2d3dPd3Ly+sCUd3h3LNnT7xGg46GoqKiQHUMbt68uZjL5VJG6qWkpPjS6fS76miO4Ivm5maVd9qYUV5ePlPdtb+FhUV3cHBwkbm5efrGjRvz7O3tr2og+O90dHRslUqlUxX5lkq5U7HmJEtUsnTxwG0kEJ7b4Cer32ArlXIVHq8ULBarVpPGNcng4GDP4V6xloxArNq3oq+M9hTlUeSvsfwvPxMuoKGhwWO8N0RRKBSM6Bg6nf4jgiC0zk6Egl3czsYQhxsKQ48g4QIAAMDf3//0eIWn0WiPjh075jx79uyHeMfY2dl1IAgyFwAAZA1rjqE8ihw9rPscrXRp7xO5lckaN6X0t+dt7m/LiYanPI+iPPLgmApoa2t75+W7fY2GX7RoUcvx48fpAAAgEom88NwJLBbr65ycnGlD3gZuli15VhfC6JZy9Ubzj1W6to6pAAAAEAgEDsbGxv/RVHh3d/ezEomEOvwcqamp6crGsNns/NHeLg0HPPOhV98Pf2ynQRHG+ZcFDKpdAAAAFBcX+6qzF3gkly5dqjCIVCrVs7e3v6RozKpVqw6q8idrybCFonfrUB5FDqsWfzv0c1jh2IHyKHIotL0J+lsyY7CqJXmyO4hamw/Ly8vnhIeH51Op1KdEQtNotN9WrFjxpVAodFOmv337dkMmk1k9NI5CoWCZmZk7EQSZouj4bilfT/ZtUjhW4yNCDxv0ozyKHJaZ9Mq+S1sDAACdnQgF8o1RlEeRy5picsDA3dPv9QnMHqI88iAUudYNdJV4q1NEXFyc7q5duzZ4eXmdtLKyukUikV6ZL1Cp1CeOjo6tHh4eRVwudwWfzx/1MzoSXC53alRUVCaLxapjs9kBio55fC3bGPvm0wRUQL/3yjc9n9Yz0HX090zolTSP//2OPIjdEVkDAAB42nmYJjsbWjo0CEMcfoCNkXvR5lh3BAlT2LQqhIWFTZFIJFSJRELdunUrWR0NVXhc/zdj9MxHa6B4qRAemfHasx6rYUnQjn8uGD5G9k3CHpRHkcvqQk+8Joh1lX2AHl9wf+RkAWtYvx9eSvaWSn2Iz5w0jId1O6fJLn8Wila6iVCe/oCiZ3sff8YT7CLnE6BgJQoRhxtoqcmz3l8vK9629+xW6Sx4dnUmLJv55DXxo3MeQ8ThDKxlfS77Pm8F1pxkqegkmsItSZxu75nAhf1NMZFY47ov4MlFzejLz69ClhrB/vObc3uuZM5QpIf9fM4K5VHk/W25sSpP3tdaaArPrBSgJbovlM6m+LQeWO13HqtwO4i1Zn0CRZ5+2Hf7bB5cK8a9P6hbytfDOgqsMJErC7vyWRw87VUERR5X0VJDiGc2h/IocljL+rr3Klfpv9pg9xttsLOrVIcfDtltEQOtcP0CFZg/wGvmjysyvQ8VmP8ChTa3McThBiZya8NEbm3YSUYXFNrcRgXmv8Ay2jOUp0tMd9gdidWHFsiuH2QQCqUOuFwuSXY5ZT1a6dyullkNEiKMLky6NaYTeX2JrRXILqcslIkDk2GFYyP6lSE27oGPzHiKVTiJZdV+qf2dh50mJPRo6Jby9bBLycvRSpcCeNLpe7SEovw7Aw+/MpDBUx5XsNqAbHgp2buzc4KutDp4dHGXEVq3xh02fbwFq2Htg5WuQljlcwFWLOqAJ97p7hOYPewTmD2EQuu76Emn67DauxmtdEawap8ceH5TNFofznxQs5XQBstJTGISk1AX/wUPVWOeSpK50AAAAABJRU5ErkJggg==", + "title": "Amazon", + "type": "affiliate", + "url": "https://www.amazon.com/" + } + ] +} diff --git a/application/basilisk/base/content/newtab/cells.js b/application/basilisk/base/content/newtab/cells.js new file mode 100644 index 000000000..47d4ef52d --- /dev/null +++ b/application/basilisk/base/content/newtab/cells.js @@ -0,0 +1,126 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This class manages a cell's DOM node (not the actually cell content, a site). + * It's mostly read-only, i.e. all manipulation of both position and content + * aren't handled here. + */ +function Cell(aGrid, aNode) { + this._grid = aGrid; + this._node = aNode; + this._node._newtabCell = this; + + // Register drag-and-drop event handlers. + ["dragenter", "dragover", "dragexit", "drop"].forEach(function (aType) { + this._node.addEventListener(aType, this, false); + }, this); +} + +Cell.prototype = { + /** + * The grid. + */ + _grid: null, + + /** + * The cell's DOM node. + */ + get node() { return this._node; }, + + /** + * The cell's offset in the grid. + */ + get index() { + let index = this._grid.cells.indexOf(this); + + // Cache this value, overwrite the getter. + Object.defineProperty(this, "index", {value: index, enumerable: true}); + + return index; + }, + + /** + * The previous cell in the grid. + */ + get previousSibling() { + let prev = this.node.previousElementSibling; + prev = prev && prev._newtabCell; + + // Cache this value, overwrite the getter. + Object.defineProperty(this, "previousSibling", {value: prev, enumerable: true}); + + return prev; + }, + + /** + * The next cell in the grid. + */ + get nextSibling() { + let next = this.node.nextElementSibling; + next = next && next._newtabCell; + + // Cache this value, overwrite the getter. + Object.defineProperty(this, "nextSibling", {value: next, enumerable: true}); + + return next; + }, + + /** + * The site contained in the cell, if any. + */ + get site() { + let firstChild = this.node.firstElementChild; + return firstChild && firstChild._newtabSite; + }, + + /** + * Checks whether the cell contains a pinned site. + * @return Whether the cell contains a pinned site. + */ + containsPinnedSite: function Cell_containsPinnedSite() { + let site = this.site; + return site && site.isPinned(); + }, + + /** + * Checks whether the cell contains a site (is empty). + * @return Whether the cell is empty. + */ + isEmpty: function Cell_isEmpty() { + return !this.site; + }, + + /** + * Handles all cell events. + */ + handleEvent: function Cell_handleEvent(aEvent) { + // We're not responding to external drag/drop events + // when our parent window is in private browsing mode. + if (inPrivateBrowsingMode() && !gDrag.draggedSite) + return; + + if (aEvent.type != "dragexit" && !gDrag.isValid(aEvent)) + return; + + switch (aEvent.type) { + case "dragenter": + aEvent.preventDefault(); + gDrop.enter(this, aEvent); + break; + case "dragover": + aEvent.preventDefault(); + break; + case "dragexit": + gDrop.exit(this, aEvent); + break; + case "drop": + aEvent.preventDefault(); + gDrop.drop(this, aEvent); + break; + } + } +}; diff --git a/application/basilisk/base/content/newtab/customize.js b/application/basilisk/base/content/newtab/customize.js new file mode 100644 index 000000000..39724fa91 --- /dev/null +++ b/application/basilisk/base/content/newtab/customize.js @@ -0,0 +1,124 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +var gCustomize = { + _nodeIDSuffixes: [ + "blank", + "button", + "classic", + "enhanced", + "panel", + "overlay", + "learn" + ], + + _nodes: {}, + + init: function() { + for (let idSuffix of this._nodeIDSuffixes) { + this._nodes[idSuffix] = document.getElementById("newtab-customize-" + idSuffix); + } + + this._nodes.button.addEventListener("click", e => this.showPanel(e)); + this._nodes.blank.addEventListener("click", this); + this._nodes.classic.addEventListener("click", this); + this._nodes.learn.addEventListener("click", this); + + this.updateSelected(); + }, + + hidePanel: function() { + this._nodes.overlay.addEventListener("transitionend", function onTransitionEnd() { + gCustomize._nodes.overlay.removeEventListener("transitionend", onTransitionEnd); + gCustomize._nodes.overlay.style.display = "none"; + }); + this._nodes.overlay.style.opacity = 0; + this._nodes.button.removeAttribute("active"); + this._nodes.panel.removeAttribute("open"); + document.removeEventListener("click", this); + document.removeEventListener("keydown", this); + }, + + showPanel: function(event) { + if (this._nodes.panel.getAttribute("open") == "true") { + return; + } + + let {panel, button, overlay} = this._nodes; + overlay.style.display = "block"; + panel.setAttribute("open", "true"); + button.setAttribute("active", "true"); + setTimeout(() => { + // Wait for display update to take place, then animate. + overlay.style.opacity = 0.8; + }, 0); + + document.addEventListener("click", this); + document.addEventListener("keydown", this); + + // Stop the event propogation to prevent panel from immediately closing + // via the document click event that we just added. + event.stopPropagation(); + }, + + handleEvent: function(event) { + switch (event.type) { + case "click": + this.onClick(event); + break; + case "keydown": + this.onKeyDown(event); + break; + } + }, + + onClick: function(event) { + if (event.currentTarget == document) { + if (!this._nodes.panel.contains(event.target)) { + this.hidePanel(); + } + } + switch (event.currentTarget.id) { + case "newtab-customize-blank": + sendAsyncMessage("NewTab:Customize", {enabled: false, enhanced: false}); + break; + case "newtab-customize-classic": + sendAsyncMessage("NewTab:Customize", {enabled: true, enhanced: false}); + break; + case "newtab-customize-enhanced": + sendAsyncMessage("NewTab:Customize", {enabled: true, enhanced: !gAllPages.enhanced}); + break; + case "newtab-customize-learn": + this.showLearn(); + break; + } + }, + + onKeyDown: function(event) { + if (event.keyCode == event.DOM_VK_ESCAPE) { + this.hidePanel(); + } + }, + + showLearn: function() { + window.open(TILES_INTRO_LINK, 'new_window'); + this.hidePanel(); + }, + + updateSelected: function() { + let {enabled} = gAllPages; + let selected = enabled ? "classic" : "blank"; + ["classic", "blank"].forEach(id => { + let node = this._nodes[id]; + if (id == selected) { + node.setAttribute("selected", true); + } + else { + node.removeAttribute("selected"); + } + }); + }, +}; diff --git a/application/basilisk/base/content/newtab/drag.js b/application/basilisk/base/content/newtab/drag.js new file mode 100644 index 000000000..e3928ebd0 --- /dev/null +++ b/application/basilisk/base/content/newtab/drag.js @@ -0,0 +1,151 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton implements site dragging functionality. + */ +var gDrag = { + /** + * The site offset to the drag start point. + */ + _offsetX: null, + _offsetY: null, + + /** + * The site that is dragged. + */ + _draggedSite: null, + get draggedSite() { return this._draggedSite; }, + + /** + * The cell width/height at the point the drag started. + */ + _cellWidth: null, + _cellHeight: null, + get cellWidth() { return this._cellWidth; }, + get cellHeight() { return this._cellHeight; }, + + /** + * Start a new drag operation. + * @param aSite The site that's being dragged. + * @param aEvent The 'dragstart' event. + */ + start: function Drag_start(aSite, aEvent) { + this._draggedSite = aSite; + + // Mark nodes as being dragged. + let selector = ".newtab-site, .newtab-control, .newtab-thumbnail"; + let parentCell = aSite.node.parentNode; + let nodes = parentCell.querySelectorAll(selector); + for (let i = 0; i < nodes.length; i++) + nodes[i].setAttribute("dragged", "true"); + + parentCell.setAttribute("dragged", "true"); + + this._setDragData(aSite, aEvent); + + // Store the cursor offset. + let node = aSite.node; + let rect = node.getBoundingClientRect(); + this._offsetX = aEvent.clientX - rect.left; + this._offsetY = aEvent.clientY - rect.top; + + // Store the cell dimensions. + let cellNode = aSite.cell.node; + this._cellWidth = cellNode.offsetWidth; + this._cellHeight = cellNode.offsetHeight; + + gTransformation.freezeSitePosition(aSite); + }, + + /** + * Handles the 'drag' event. + * @param aSite The site that's being dragged. + * @param aEvent The 'drag' event. + */ + drag: function Drag_drag(aSite, aEvent) { + // Get the viewport size. + let {clientWidth, clientHeight} = document.documentElement; + + // We'll want a padding of 5px. + let border = 5; + + // Enforce minimum constraints to keep the drag image inside the window. + let left = Math.max(scrollX + aEvent.clientX - this._offsetX, border); + let top = Math.max(scrollY + aEvent.clientY - this._offsetY, border); + + // Enforce maximum constraints to keep the drag image inside the window. + left = Math.min(left, scrollX + clientWidth - this.cellWidth - border); + top = Math.min(top, scrollY + clientHeight - this.cellHeight - border); + + // Update the drag image's position. + gTransformation.setSitePosition(aSite, {left: left, top: top}); + }, + + /** + * Ends the current drag operation. + * @param aSite The site that's being dragged. + * @param aEvent The 'dragend' event. + */ + end: function Drag_end(aSite, aEvent) { + let nodes = gGrid.node.querySelectorAll("[dragged]") + for (let i = 0; i < nodes.length; i++) + nodes[i].removeAttribute("dragged"); + + // Slide the dragged site back into its cell (may be the old or the new cell). + gTransformation.slideSiteTo(aSite, aSite.cell, {unfreeze: true}); + + this._draggedSite = null; + }, + + /** + * Checks whether we're responsible for a given drag event. + * @param aEvent The drag event to check. + * @return Whether we should handle this drag and drop operation. + */ + isValid: function Drag_isValid(aEvent) { + let link = gDragDataHelper.getLinkFromDragEvent(aEvent); + + // Check that the drag data is non-empty. + // Can happen when dragging places folders. + if (!link || !link.url) { + return false; + } + + // Check that we're not accepting URLs which would inherit the caller's + // principal (such as javascript: or data:). + return gLinkChecker.checkLoadURI(link.url); + }, + + /** + * Initializes the drag data for the current drag operation. + * @param aSite The site that's being dragged. + * @param aEvent The 'dragstart' event. + */ + _setDragData: function Drag_setDragData(aSite, aEvent) { + let {url, title} = aSite; + + let dt = aEvent.dataTransfer; + dt.mozCursor = "default"; + dt.effectAllowed = "move"; + dt.setData("text/plain", url); + dt.setData("text/uri-list", url); + dt.setData("text/x-moz-url", url + "\n" + title); + dt.setData("text/html", "<a href=\"" + url + "\">" + url + "</a>"); + + // Create and use an empty drag element. We don't want to use the default + // drag image with its default opacity. + let dragElement = document.createElementNS(HTML_NAMESPACE, "div"); + dragElement.classList.add("newtab-drag"); + let scrollbox = document.getElementById("newtab-vertical-margin"); + scrollbox.appendChild(dragElement); + dt.setDragImage(dragElement, 0, 0); + + // After the 'dragstart' event has been processed we can remove the + // temporary drag element from the DOM. + setTimeout(() => scrollbox.removeChild(dragElement), 0); + } +}; diff --git a/application/basilisk/base/content/newtab/dragDataHelper.js b/application/basilisk/base/content/newtab/dragDataHelper.js new file mode 100644 index 000000000..675ff2671 --- /dev/null +++ b/application/basilisk/base/content/newtab/dragDataHelper.js @@ -0,0 +1,22 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +var gDragDataHelper = { + get mimeType() { + return "text/x-moz-url"; + }, + + getLinkFromDragEvent: function DragDataHelper_getLinkFromDragEvent(aEvent) { + let dt = aEvent.dataTransfer; + if (!dt || !dt.types.includes(this.mimeType)) { + return null; + } + + let data = dt.getData(this.mimeType) || ""; + let [url, title] = data.split(/[\r\n]+/); + return {url: url, title: title}; + } +}; diff --git a/application/basilisk/base/content/newtab/drop.js b/application/basilisk/base/content/newtab/drop.js new file mode 100644 index 000000000..748652455 --- /dev/null +++ b/application/basilisk/base/content/newtab/drop.js @@ -0,0 +1,150 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +// A little delay that prevents the grid from being too sensitive when dragging +// sites around. +const DELAY_REARRANGE_MS = 100; + +/** + * This singleton implements site dropping functionality. + */ +var gDrop = { + /** + * The last drop target. + */ + _lastDropTarget: null, + + /** + * Handles the 'dragenter' event. + * @param aCell The drop target cell. + */ + enter: function Drop_enter(aCell) { + this._delayedRearrange(aCell); + }, + + /** + * Handles the 'dragexit' event. + * @param aCell The drop target cell. + * @param aEvent The 'dragexit' event. + */ + exit: function Drop_exit(aCell, aEvent) { + if (aEvent.dataTransfer && !aEvent.dataTransfer.mozUserCancelled) { + this._delayedRearrange(); + } else { + // The drag operation has been cancelled. + this._cancelDelayedArrange(); + this._rearrange(); + } + }, + + /** + * Handles the 'drop' event. + * @param aCell The drop target cell. + * @param aEvent The 'dragexit' event. + */ + drop: function Drop_drop(aCell, aEvent) { + // The cell that is the drop target could contain a pinned site. We need + // to find out where that site has gone and re-pin it there. + if (aCell.containsPinnedSite()) + this._repinSitesAfterDrop(aCell); + + // Pin the dragged or insert the new site. + this._pinDraggedSite(aCell, aEvent); + + this._cancelDelayedArrange(); + + // Update the grid and move all sites to their new places. + gUpdater.updateGrid(); + }, + + /** + * Re-pins all pinned sites in their (new) positions. + * @param aCell The drop target cell. + */ + _repinSitesAfterDrop: function Drop_repinSitesAfterDrop(aCell) { + let sites = gDropPreview.rearrange(aCell); + + // Filter out pinned sites. + let pinnedSites = sites.filter(function (aSite) { + return aSite && aSite.isPinned(); + }); + + // Re-pin all shifted pinned cells. + pinnedSites.forEach(aSite => aSite.pin(sites.indexOf(aSite))); + }, + + /** + * Pins the dragged site in its new place. + * @param aCell The drop target cell. + * @param aEvent The 'dragexit' event. + */ + _pinDraggedSite: function Drop_pinDraggedSite(aCell, aEvent) { + let index = aCell.index; + let draggedSite = gDrag.draggedSite; + + if (draggedSite) { + // Pin the dragged site at its new place. + if (aCell != draggedSite.cell) + draggedSite.pin(index); + } else { + let link = gDragDataHelper.getLinkFromDragEvent(aEvent); + if (link) { + // A new link was dragged onto the grid. Create it by pinning its URL. + gPinnedLinks.pin(link, index); + + // Make sure the newly added link is not blocked. + gBlockedLinks.unblock(link); + } + } + }, + + /** + * Time a rearrange with a little delay. + * @param aCell The drop target cell. + */ + _delayedRearrange: function Drop_delayedRearrange(aCell) { + // The last drop target didn't change so there's no need to re-arrange. + if (this._lastDropTarget == aCell) + return; + + let self = this; + + function callback() { + self._rearrangeTimeout = null; + self._rearrange(aCell); + } + + this._cancelDelayedArrange(); + this._rearrangeTimeout = setTimeout(callback, DELAY_REARRANGE_MS); + + // Store the last drop target. + this._lastDropTarget = aCell; + }, + + /** + * Cancels a timed rearrange, if any. + */ + _cancelDelayedArrange: function Drop_cancelDelayedArrange() { + if (this._rearrangeTimeout) { + clearTimeout(this._rearrangeTimeout); + this._rearrangeTimeout = null; + } + }, + + /** + * Rearrange all sites in the grid depending on the current drop target. + * @param aCell The drop target cell. + */ + _rearrange: function Drop_rearrange(aCell) { + let sites = gGrid.sites; + + // We need to rearrange the grid only if there's a current drop target. + if (aCell) + sites = gDropPreview.rearrange(aCell); + + gTransformation.rearrangeSites(sites, {unfreeze: !aCell}); + } +}; diff --git a/application/basilisk/base/content/newtab/dropPreview.js b/application/basilisk/base/content/newtab/dropPreview.js new file mode 100644 index 000000000..fd7587a35 --- /dev/null +++ b/application/basilisk/base/content/newtab/dropPreview.js @@ -0,0 +1,222 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton provides the ability to re-arrange the current grid to + * indicate the transformation that results from dropping a cell at a certain + * position. + */ +var gDropPreview = { + /** + * Rearranges the sites currently contained in the grid when a site would be + * dropped onto the given cell. + * @param aCell The drop target cell. + * @return The re-arranged array of sites. + */ + rearrange: function DropPreview_rearrange(aCell) { + let sites = gGrid.sites; + + // Insert the dragged site into the current grid. + this._insertDraggedSite(sites, aCell); + + // After the new site has been inserted we need to correct the positions + // of all pinned tabs that have been moved around. + this._repositionPinnedSites(sites, aCell); + + return sites; + }, + + /** + * Inserts the currently dragged site into the given array of sites. + * @param aSites The array of sites to insert into. + * @param aCell The drop target cell. + */ + _insertDraggedSite: function DropPreview_insertDraggedSite(aSites, aCell) { + let dropIndex = aCell.index; + let draggedSite = gDrag.draggedSite; + + // We're currently dragging a site. + if (draggedSite) { + let dragCell = draggedSite.cell; + let dragIndex = dragCell.index; + + // Move the dragged site into its new position. + if (dragIndex != dropIndex) { + aSites.splice(dragIndex, 1); + aSites.splice(dropIndex, 0, draggedSite); + } + // We're handling an external drag item. + } else { + aSites.splice(dropIndex, 0, null); + } + }, + + /** + * Correct the position of all pinned sites that might have been moved to + * different positions after the dragged site has been inserted. + * @param aSites The array of sites containing the dragged site. + * @param aCell The drop target cell. + */ + _repositionPinnedSites: + function DropPreview_repositionPinnedSites(aSites, aCell) { + + // Collect all pinned sites. + let pinnedSites = this._filterPinnedSites(aSites, aCell); + + // Correct pinned site positions. + pinnedSites.forEach(function (aSite) { + aSites[aSites.indexOf(aSite)] = aSites[aSite.cell.index]; + aSites[aSite.cell.index] = aSite; + }, this); + + // There might be a pinned cell that got pushed out of the grid, try to + // sneak it in by removing a lower-priority cell. + if (this._hasOverflowedPinnedSite(aSites, aCell)) + this._repositionOverflowedPinnedSite(aSites, aCell); + }, + + /** + * Filter pinned sites out of the grid that are still on their old positions + * and have not moved. + * @param aSites The array of sites to filter. + * @param aCell The drop target cell. + * @return The filtered array of sites. + */ + _filterPinnedSites: function DropPreview_filterPinnedSites(aSites, aCell) { + let draggedSite = gDrag.draggedSite; + + // When dropping on a cell that contains a pinned site make sure that all + // pinned cells surrounding the drop target are moved as well. + let range = this._getPinnedRange(aCell); + + return aSites.filter(function (aSite, aIndex) { + // The site must be valid, pinned and not the dragged site. + if (!aSite || aSite == draggedSite || !aSite.isPinned()) + return false; + + let index = aSite.cell.index; + + // If it's not in the 'pinned range' it's a valid pinned site. + return (index > range.end || index < range.start); + }); + }, + + /** + * Determines the range of pinned sites surrounding the drop target cell. + * @param aCell The drop target cell. + * @return The range of pinned cells. + */ + _getPinnedRange: function DropPreview_getPinnedRange(aCell) { + let dropIndex = aCell.index; + let range = {start: dropIndex, end: dropIndex}; + + // We need a pinned range only when dropping on a pinned site. + if (aCell.containsPinnedSite()) { + let links = gPinnedLinks.links; + + // Find all previous siblings of the drop target that are pinned as well. + while (range.start && links[range.start - 1]) + range.start--; + + let maxEnd = links.length - 1; + + // Find all next siblings of the drop target that are pinned as well. + while (range.end < maxEnd && links[range.end + 1]) + range.end++; + } + + return range; + }, + + /** + * Checks if the given array of sites contains a pinned site that has + * been pushed out of the grid. + * @param aSites The array of sites to check. + * @param aCell The drop target cell. + * @return Whether there is an overflowed pinned cell. + */ + _hasOverflowedPinnedSite: + function DropPreview_hasOverflowedPinnedSite(aSites, aCell) { + + // If the drop target isn't pinned there's no way a pinned site has been + // pushed out of the grid so we can just exit here. + if (!aCell.containsPinnedSite()) + return false; + + let cells = gGrid.cells; + + // No cells have been pushed out of the grid, nothing to do here. + if (aSites.length <= cells.length) + return false; + + let overflowedSite = aSites[cells.length]; + + // Nothing to do if the site that got pushed out of the grid is not pinned. + return (overflowedSite && overflowedSite.isPinned()); + }, + + /** + * We have a overflowed pinned site that we need to re-position so that it's + * visible again. We try to find a lower-priority cell (empty or containing + * an unpinned site) that we can move it to. + * @param aSites The array of sites. + * @param aCell The drop target cell. + */ + _repositionOverflowedPinnedSite: + function DropPreview_repositionOverflowedPinnedSite(aSites, aCell) { + + // Try to find a lower-priority cell (empty or containing an unpinned site). + let index = this._indexOfLowerPrioritySite(aSites, aCell); + + if (index > -1) { + let cells = gGrid.cells; + let dropIndex = aCell.index; + + // Move all pinned cells to their new positions to let the overflowed + // site fit into the grid. + for (let i = index + 1, lastPosition = index; i < aSites.length; i++) { + if (i != dropIndex) { + aSites[lastPosition] = aSites[i]; + lastPosition = i; + } + } + + // Finally, remove the overflowed site from its previous position. + aSites.splice(cells.length, 1); + } + }, + + /** + * Finds the index of the last cell that is empty or contains an unpinned + * site. These are considered to be of a lower priority. + * @param aSites The array of sites. + * @param aCell The drop target cell. + * @return The cell's index. + */ + _indexOfLowerPrioritySite: + function DropPreview_indexOfLowerPrioritySite(aSites, aCell) { + + let cells = gGrid.cells; + let dropIndex = aCell.index; + + // Search (beginning with the last site in the grid) for a site that is + // empty or unpinned (an thus lower-priority) and can be pushed out of the + // grid instead of the pinned site. + for (let i = cells.length - 1; i >= 0; i--) { + // The cell that is our drop target is not a good choice. + if (i == dropIndex) + continue; + + let site = aSites[i]; + + // We can use the cell only if it's empty or the site is un-pinned. + if (!site || !site.isPinned()) + return i; + } + + return -1; + } +}; diff --git a/application/basilisk/base/content/newtab/dropTargetShim.js b/application/basilisk/base/content/newtab/dropTargetShim.js new file mode 100644 index 000000000..57a97fa00 --- /dev/null +++ b/application/basilisk/base/content/newtab/dropTargetShim.js @@ -0,0 +1,232 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton provides a custom drop target detection. We need this because + * the default DnD target detection relies on the cursor's position. We want + * to pick a drop target based on the dragged site's position. + */ +var gDropTargetShim = { + /** + * Cache for the position of all cells, cleaned after drag finished. + */ + _cellPositions: null, + + /** + * The last drop target that was hovered. + */ + _lastDropTarget: null, + + /** + * Initializes the drop target shim. + */ + init: function () { + gGrid.node.addEventListener("dragstart", this, true); + }, + + /** + * Add all event listeners needed during a drag operation. + */ + _addEventListeners: function () { + gGrid.node.addEventListener("dragend", this); + + let docElement = document.documentElement; + docElement.addEventListener("dragover", this); + docElement.addEventListener("dragenter", this); + docElement.addEventListener("drop", this); + }, + + /** + * Remove all event listeners that were needed during a drag operation. + */ + _removeEventListeners: function () { + gGrid.node.removeEventListener("dragend", this); + + let docElement = document.documentElement; + docElement.removeEventListener("dragover", this); + docElement.removeEventListener("dragenter", this); + docElement.removeEventListener("drop", this); + }, + + /** + * Handles all shim events. + */ + handleEvent: function (aEvent) { + switch (aEvent.type) { + case "dragstart": + this._dragstart(aEvent); + break; + case "dragenter": + aEvent.preventDefault(); + break; + case "dragover": + this._dragover(aEvent); + break; + case "drop": + this._drop(aEvent); + break; + case "dragend": + this._dragend(aEvent); + break; + } + }, + + /** + * Handles the 'dragstart' event. + * @param aEvent The 'dragstart' event. + */ + _dragstart: function (aEvent) { + if (aEvent.target.classList.contains("newtab-link")) { + gGrid.lock(); + this._addEventListeners(); + } + }, + + /** + * Handles the 'dragover' event. + * @param aEvent The 'dragover' event. + */ + _dragover: function (aEvent) { + // XXX bug 505521 - Use the dragover event to retrieve the + // current mouse coordinates while dragging. + let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode; + gDrag.drag(sourceNode._newtabSite, aEvent); + + // Find the current drop target, if there's one. + this._updateDropTarget(aEvent); + + // If we have a valid drop target, + // let the drag-and-drop service know. + if (this._lastDropTarget) { + aEvent.preventDefault(); + } + }, + + /** + * Handles the 'drop' event. + * @param aEvent The 'drop' event. + */ + _drop: function (aEvent) { + // We're accepting all drops. + aEvent.preventDefault(); + + // remember that drop event was seen, this explicitly + // assumes that drop event preceeds dragend event + this._dropSeen = true; + + // Make sure to determine the current drop target + // in case the dragover event hasn't been fired. + this._updateDropTarget(aEvent); + + // A site was successfully dropped. + this._dispatchEvent(aEvent, "drop", this._lastDropTarget); + }, + + /** + * Handles the 'dragend' event. + * @param aEvent The 'dragend' event. + */ + _dragend: function (aEvent) { + if (this._lastDropTarget) { + if (aEvent.dataTransfer.mozUserCancelled || !this._dropSeen) { + // The drag operation was cancelled or no drop event was generated + this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); + this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); + } + + // Clean up. + this._lastDropTarget = null; + this._cellPositions = null; + } + + this._dropSeen = false; + gGrid.unlock(); + this._removeEventListeners(); + }, + + /** + * Tries to find the current drop target and will fire + * appropriate dragenter, dragexit, and dragleave events. + * @param aEvent The current drag event. + */ + _updateDropTarget: function (aEvent) { + // Let's see if we find a drop target. + let target = this._findDropTarget(aEvent); + + if (target != this._lastDropTarget) { + if (this._lastDropTarget) + // We left the last drop target. + this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); + + if (target) + // We're now hovering a (new) drop target. + this._dispatchEvent(aEvent, "dragenter", target); + + if (this._lastDropTarget) + // We left the last drop target. + this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); + + this._lastDropTarget = target; + } + }, + + /** + * Determines the current drop target by matching the dragged site's position + * against all cells in the grid. + * @return The currently hovered drop target or null. + */ + _findDropTarget: function () { + // These are the minimum intersection values - we want to use the cell if + // the site is >= 50% hovering its position. + let minWidth = gDrag.cellWidth / 2; + let minHeight = gDrag.cellHeight / 2; + + let cellPositions = this._getCellPositions(); + let rect = gTransformation.getNodePosition(gDrag.draggedSite.node); + + // Compare each cell's position to the dragged site's position. + for (let i = 0; i < cellPositions.length; i++) { + let inter = rect.intersect(cellPositions[i].rect); + + // If the intersection is big enough we found a drop target. + if (inter.width >= minWidth && inter.height >= minHeight) + return cellPositions[i].cell; + } + + // No drop target found. + return null; + }, + + /** + * Gets the positions of all cell nodes. + * @return The (cached) cell positions. + */ + _getCellPositions: function DropTargetShim_getCellPositions() { + if (this._cellPositions) + return this._cellPositions; + + return this._cellPositions = gGrid.cells.map(function (cell) { + return {cell: cell, rect: gTransformation.getNodePosition(cell.node)}; + }); + }, + + /** + * Dispatches a custom DragEvent on the given target node. + * @param aEvent The source event. + * @param aType The event type. + * @param aTarget The target node that receives the event. + */ + _dispatchEvent: function (aEvent, aType, aTarget) { + let node = aTarget.node; + let event = document.createEvent("DragEvent"); + + // The event should not bubble to prevent recursion. + event.initDragEvent(aType, false, true, window, 0, 0, 0, 0, 0, false, false, + false, false, 0, node, aEvent.dataTransfer); + + node.dispatchEvent(event); + } +}; diff --git a/application/basilisk/base/content/newtab/grid.js b/application/basilisk/base/content/newtab/grid.js new file mode 100644 index 000000000..b6f98fa17 --- /dev/null +++ b/application/basilisk/base/content/newtab/grid.js @@ -0,0 +1,279 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * Define various fixed dimensions + */ +const GRID_BOTTOM_EXTRA = 7; // title's line-height extends 7px past the margin +const GRID_WIDTH_EXTRA = 1; // provide 1px buffer to allow for rounding error +const SPONSORED_TAG_BUFFER = 2; // 2px buffer to clip off top of sponsored tag + +/** + * This singleton represents the grid that contains all sites. + */ +var gGrid = { + /** + * The DOM node of the grid. + */ + _node: null, + _gridDefaultContent: null, + get node() { return this._node; }, + + /** + * The cached DOM fragment for sites. + */ + _siteFragment: null, + + /** + * All cells contained in the grid. + */ + _cells: [], + get cells() { return this._cells; }, + + /** + * All sites contained in the grid's cells. Sites may be empty. + */ + get sites() { return [for (cell of this.cells) cell.site]; }, + + // Tells whether the grid has already been initialized. + get ready() { return !!this._ready; }, + + // Returns whether the page has finished loading yet. + get isDocumentLoaded() { return document.readyState == "complete"; }, + + /** + * Initializes the grid. + * @param aSelector The query selector of the grid. + */ + init: function Grid_init() { + this._node = document.getElementById("newtab-grid"); + this._gridDefaultContent = this._node.lastChild; + this._createSiteFragment(); + + gLinks.populateCache(() => { + this._refreshGrid(); + this._ready = true; + + // If fetching links took longer than loading the page itself then + // we need to resize the grid as that was blocked until now. + // We also want to resize now if the page was already loaded when + // initializing the grid (the user toggled the page). + this._resizeGrid(); + + addEventListener("resize", this); + }); + + // Resize the grid as soon as the page loads. + if (!this.isDocumentLoaded) { + addEventListener("load", this); + } + }, + + /** + * Creates a new site in the grid. + * @param aLink The new site's link. + * @param aCell The cell that will contain the new site. + * @return The newly created site. + */ + createSite: function Grid_createSite(aLink, aCell) { + let node = aCell.node; + node.appendChild(this._siteFragment.cloneNode(true)); + return new Site(node.firstElementChild, aLink); + }, + + /** + * Handles all grid events. + */ + handleEvent: function Grid_handleEvent(aEvent) { + switch (aEvent.type) { + case "load": + case "resize": + this._resizeGrid(); + break; + } + }, + + /** + * Locks the grid to block all pointer events. + */ + lock: function Grid_lock() { + this.node.setAttribute("locked", "true"); + }, + + /** + * Unlocks the grid to allow all pointer events. + */ + unlock: function Grid_unlock() { + this.node.removeAttribute("locked"); + }, + + /** + * Renders and resizes the gird. _resizeGrid() call is needed to ensure + * that scrollbar disappears when the bottom row becomes empty following + * the block action, or tile display is turmed off via cog menu + */ + + refresh() { + this._refreshGrid(); + this._resizeGrid(); + }, + + /** + * Renders the grid, including cells and sites. + */ + _refreshGrid() { + let cell = document.createElementNS(HTML_NAMESPACE, "div"); + cell.classList.add("newtab-cell"); + + // Creates all the cells up to the maximum + let fragment = document.createDocumentFragment(); + for (let i = 0; i < gGridPrefs.gridColumns * gGridPrefs.gridRows; i++) { + fragment.appendChild(cell.cloneNode(true)); + } + + // Create cells. + let cells = Array.from(fragment.childNodes, (cell) => new Cell(this, cell)); + + // Fetch links. + let links = gLinks.getLinks(); + + // Create sites. + let numLinks = Math.min(links.length, cells.length); + let hasHistoryTiles = false; + for (let i = 0; i < numLinks; i++) { + if (links[i]) { + this.createSite(links[i], cells[i]); + if (links[i].type == "history") { + hasHistoryTiles = true; + } + } + } + + this._cells = cells; + while (this._gridDefaultContent.nextSibling) { + this._gridDefaultContent.nextSibling.remove(); + } + this._node.appendChild(fragment); + + document.getElementById("topsites-heading").textContent = + hasHistoryTiles ? "Your Top Sites" : "Top Sites"; + }, + + /** + * Calculate the height for a number of rows up to the maximum rows + * @param rows Number of rows defaulting to the max + */ + _computeHeight: function Grid_computeHeight(aRows) { + let {gridRows} = gGridPrefs; + aRows = aRows === undefined ? gridRows : Math.min(gridRows, aRows); + return aRows * this._cellHeight + GRID_BOTTOM_EXTRA; + }, + + /** + * Creates the DOM fragment that is re-used when creating sites. + */ + _createSiteFragment: function Grid_createSiteFragment() { + let site = document.createElementNS(HTML_NAMESPACE, "div"); + site.classList.add("newtab-site"); + site.setAttribute("draggable", "true"); + + // Create the site's inner HTML code. + site.innerHTML = + '<span class="newtab-sponsored">' + newTabString("sponsored.button") + '</span>' + + '<a class="newtab-link">' + + ' <span class="newtab-thumbnail placeholder"/>' + + ' <span class="newtab-thumbnail thumbnail"/>' + + ' <span class="newtab-thumbnail enhanced-content"/>' + + ' <span class="newtab-title"/>' + + '</a>' + + '<input type="button" title="' + newTabString("pin") + '"' + + ' class="newtab-control newtab-control-pin"/>' + + '<input type="button" title="' + newTabString("block") + '"' + + ' class="newtab-control newtab-control-block"/>' + + '<span class="newtab-suggested"/>'; + + this._siteFragment = document.createDocumentFragment(); + this._siteFragment.appendChild(site); + }, + + /** + * Test a tile at a given position for being pinned or history + * @param position Position in sites array + */ + _isHistoricalTile: function Grid_isHistoricalTile(aPos) { + let site = this.sites[aPos]; + return site && (site.isPinned() || site.link && site.link.type == "history"); + }, + + /** + * Make sure the correct number of rows and columns are visible + */ + _resizeGrid: function Grid_resizeGrid() { + // If we're somehow called before the page has finished loading, + // let's bail out to avoid caching zero heights and widths. + // We'll be called again when DOMContentLoaded fires. + // Same goes for the grid if that's not ready yet. + if (!this.isDocumentLoaded || !this._ready) { + return; + } + + // Save the cell's computed height/width including margin and border + if (this._cellHeight === undefined) { + let refCell = document.querySelector(".newtab-cell"); + let style = getComputedStyle(refCell); + this._cellHeight = refCell.offsetHeight + + parseFloat(style.marginTop) + parseFloat(style.marginBottom); + this._cellWidth = refCell.offsetWidth + + parseFloat(style.marginLeft) + parseFloat(style.marginRight); + } + + let searchContainer = document.querySelector("#newtab-search-container"); + // Save search-container margin height + if (this._searchContainerMargin === undefined) { + let style = getComputedStyle(searchContainer); + this._searchContainerMargin = parseFloat(style.marginBottom) + + parseFloat(style.marginTop); + } + + // Find the number of rows we can place into view port + let availHeight = document.documentElement.clientHeight - + searchContainer.offsetHeight - this._searchContainerMargin; + let visibleRows = Math.floor(availHeight / this._cellHeight); + + // Find the number of columns that fit into view port + let maxGridWidth = gGridPrefs.gridColumns * this._cellWidth + GRID_WIDTH_EXTRA; + // available width is current grid width, but no greater than maxGridWidth + let availWidth = Math.min(document.querySelector("#newtab-grid").clientWidth, + maxGridWidth); + // finally get the number of columns we can fit into view port + let gridColumns = Math.floor(availWidth / this._cellWidth); + // walk sites backwords until a pinned or history tile is found or visibleRows reached + let tileIndex = Math.min(gGridPrefs.gridRows * gridColumns, this.sites.length) - 1; + while (tileIndex >= visibleRows * gridColumns) { + if (this._isHistoricalTile(tileIndex)) { + break; + } + tileIndex--; + } + + // Compute the actual number of grid rows we will display (potentially + // with a scroll bar). tileIndex now points to a historical tile with + // heighest index or to the last index of the visible row, if none found + // Dividing tileIndex by number of tiles in a column gives the rows + let gridRows = Math.floor(tileIndex / gridColumns) + 1; + + // we need to set grid width, for otherwise the scrollbar may shrink + // the grid when shown and cause grid layout to be different from + // what being computed above. This, in turn, may cause scrollbar shown + // for directory tiles, and introduce jitter when grid width is aligned + // exactly on the column boundary + this._node.style.width = gridColumns * this._cellWidth + "px"; + this._node.style.maxWidth = gGridPrefs.gridColumns * this._cellWidth + + GRID_WIDTH_EXTRA + "px"; + this._node.style.height = this._computeHeight() + "px"; + this._node.style.maxHeight = this._computeHeight(gridRows) - SPONSORED_TAG_BUFFER + "px"; + } +}; diff --git a/application/basilisk/base/content/newtab/newTab.css b/application/basilisk/base/content/newtab/newTab.css new file mode 100644 index 000000000..658ad2ed3 --- /dev/null +++ b/application/basilisk/base/content/newtab/newTab.css @@ -0,0 +1,654 @@ +/* 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/. */ + +html { + width: 100%; + height: 100%; +} + +body { + font: message-box; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + background-color: #F9F9F9; + display: -moz-box; + position: relative; + -moz-box-flex: 1; + -moz-user-focus: normal; + -moz-box-orient: vertical; +} + +input { + font: message-box; + font-size: 16px; +} + +input[type=button] { + cursor: pointer; +} + +/* UNDO */ +#newtab-undo-container { + transition: opacity 100ms ease-out; + -moz-box-align: center; + -moz-box-pack: center; +} + +#newtab-undo-container[undo-disabled] { + opacity: 0; + pointer-events: none; +} + +/* CUSTOMIZE */ +#newtab-customize-button { + position: absolute; + top: 10px; + right: 20px; + z-index: 101; +} + +#newtab-customize-button:dir(rtl) { + left: 20px; + right: auto; +} + +/* MARGINS */ +#newtab-vertical-margin { + display: -moz-box; + position: relative; + -moz-box-flex: 1; + -moz-box-orient: vertical; +} + +#newtab-margin-undo-container { + display: -moz-box; + left: 6px; + position: absolute; + top: 6px; + z-index: 1; +} + +#newtab-margin-undo-container:dir(rtl) { + left: auto; + right: 6px; +} + +#newtab-undo-close-button:dir(rtl) { + float:left; +} + +#newtab-horizontal-margin { + display: -moz-box; + -moz-box-flex: 1; +} + +#newtab-margin-top, +#newtab-margin-bottom { + display: -moz-box; + position: relative; +} + +#newtab-margin-top { + -moz-box-flex: 1; +} + +#newtab-margin-bottom { + -moz-box-flex: 2; +} + +.newtab-side-margin { + min-width: 10px; + -moz-box-flex: 1; +} + +/* GRID */ +#newtab-grid { + -moz-box-flex: 5; + overflow: hidden; + text-align: center; + transition: 100ms ease-out; + transition-property: opacity; +} + +#newtab-grid[page-disabled] { + opacity: 0; +} + +#newtab-grid[locked], +#newtab-grid[page-disabled] { + pointer-events: none; +} + +body:not(.compact) #topsites-heading { + display: none; +} + +/* + * If you change the sizes here, make sure you + * change the preferences: + * toolkit.pageThumbs.minWidth + * toolkit.pageThumbs.minHeight + */ +/* CELLS */ +.newtab-cell { + display: -moz-box; + height: 210px; + margin: 20px 10px 35px; + width: 290px; +} + +body.compact .newtab-cell { + width: 110px; + height: 110px; + margin: 12px; +} + +/* SITES */ +.newtab-site { + position: relative; + -moz-box-flex: 1; + transition: 100ms ease-out; + transition-property: top, left, opacity; +} + +.newtab-site[frozen] { + position: absolute; + pointer-events: none; +} + +.newtab-site[dragged] { + transition-property: none; + z-index: 10; +} + +/* LINK + THUMBNAILS */ +.newtab-link, +.newtab-thumbnail { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; +} + +/* TITLES */ +.newtab-sponsored, +.newtab-title, +.newtab-suggested { + overflow: hidden; + position: absolute; + right: 0; + text-align: center; +} + +.newtab-sponsored, +.newtab-title { + bottom: 0; + white-space: nowrap; + text-overflow: ellipsis; + vertical-align: middle; +} + +.newtab-suggested { + border: 1px solid transparent; + border-radius: 2px; + font-size: 12px; + height: 17px; + line-height: 17px; + margin-bottom: -1px; + padding: 2px 8px; + display: none; + margin-left: auto; + margin-right: auto; + left: 0; + top: 215px; + -moz-user-select: none; +} + +.newtab-suggested-bounds { + max-height: 34px; /* 34 / 17 = 2 lines maximum */ +} + +.newtab-title { + left: 0; + padding: 0 4px; +} + +.newtab-sponsored { + background-color: #FFFFFF; + border: 1px solid #E2E2E2; + border-radius: 3px; + color: #4A4A4A; + cursor: pointer; + display: none; + font-family: Arial; + font-size: 9px; + height: 17px; + left: 0; + line-height: 6px; + padding: 4px; + right: auto; + top: -15px; +} + +.newtab-site[suggested=true] > .newtab-sponsored { + background-color: #E2E2E2; + border: none; +} + +.newtab-site > .newtab-sponsored:-moz-any(:hover, [active]) { + background-color: #4A90E2; + border: 0; + color: white; +} + +.newtab-site > .newtab-sponsored[active] { + background-color: #000000; +} + +.newtab-sponsored:dir(rtl) { + right: 0; + left: auto; +} + +.newtab-site:-moz-any([type=enhanced], [type=sponsored], [suggested]) .newtab-sponsored { + display: block; +} + +.newtab-site[suggested] .newtab-suggested { + display: table; +} + +.sponsored-explain, +.sponsored-explain a, +.suggested-explain, +.suggested-explain a { + color: white; +} + +.sponsored-explain, +.suggested-explain { + background-color: rgba(51, 51, 51, 0.95); + bottom: 30px; + line-height: 20px; + padding: 15px 10px; + position: absolute; + text-align: start; +} + +.sponsored-explain input, +.suggested-explain input { + background-size: 18px; + height: 18px; + opacity: 1; + pointer-events: none; + position: static; + width: 18px; +} + +/* CONTROLS */ +.newtab-control { + position: absolute; + opacity: 0; + transition: opacity 100ms ease-out; +} + +.newtab-control:-moz-focusring, +.newtab-cell:not([ignorehover]) > .newtab-site:hover > .newtab-control { + opacity: 1; +} + +.newtab-control[dragged] { + opacity: 0 !important; +} + +@media (-moz-touch-enabled) { + .newtab-control { + opacity: 1; + } +} + +/* DRAG & DROP */ + +/* + * This is just a temporary drag element used for dataTransfer.setDragImage() + * so that we can use custom drag images and elements. It needs an opacity of + * 0.01 so that the core code detects that it's in fact a visible element. + */ +.newtab-drag { + width: 1px; + height: 1px; + background-color: #fff; + opacity: 0.01; +} + +/* SEARCH */ +#newtab-search-container { + display: -moz-box; + position: relative; + -moz-box-pack: center; + margin: 40px 0 15px; +} + +body.compact #newtab-search-container { + margin-top: 0; + margin-bottom: 80px; +} + +#newtab-search-container[page-disabled] { + opacity: 0; + pointer-events: none; +} + +#newtab-search-form { + display: -moz-box; + position: relative; + height: 36px; + -moz-box-flex: 1; + max-width: 600px; /* 2 * (290 cell width + 10 cell margin) */ +} + +#newtab-search-icon { + border: 1px transparent; + padding: 0; + margin: 0; + width: 36px; + height: 36px; + background: url("chrome://browser/skin/search-indicator-magnifying-glass.svg") center center no-repeat; + position: absolute; +} + +#newtab-search-text { + -moz-box-flex: 1; + padding-top: 6px; + padding-bottom: 6px; + padding-inline-start: 34px; + padding-inline-end: 8px; + background: hsla(0,0%,100%,.9) padding-box; + border: 1px solid; + border-spacing: 0; + border-radius: 2px 0 0 2px; + border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2); + box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset, + 0 0 2px hsla(210,65%,9%,.1) inset, + 0 1px 0 hsla(0,0%,100%,.2); + color: inherit; + unicode-bidi: plaintext; +} + +#newtab-search-text:dir(rtl) { + border-radius: 0 2px 2px 0; +} + +#newtab-search-text[aria-expanded="true"] { + border-radius: 2px 0 0 0; +} + +#newtab-search-text[aria-expanded="true"]:dir(rtl) { + border-radius: 0 2px 0 0; +} + +#newtab-search-text[keepfocus], +#newtab-search-text:focus, +#newtab-search-text[autofocus] { + border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6); +} + +#newtab-search-submit { + margin-inline-start: -1px; + color: transparent; + background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go") center center no-repeat, linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box; + padding: 0; + border: 1px solid; + border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2); + border-radius: 0 2px 2px 0; + border-inline-start: 1px solid transparent; + box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset, + 0 1px 0 hsla(0,0%,100%,.2); + cursor: pointer; + transition-property: background-color, border-color, box-shadow; + transition-duration: 150ms; + width: 50px; +} + +#newtab-search-submit:dir(rtl) { + border-radius: 2px 0 0 2px; + background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl"), linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)); +} + +#newtab-search-text:focus + #newtab-search-submit, +#newtab-search-text + #newtab-search-submit:hover, +#newtab-search-text[autofocus] + #newtab-search-submit { + border-color: #59b5fc #45a3e7 #3294d5; +} + +#newtab-search-text:focus + #newtab-search-submit, +#newtab-search-text[keepfocus] + #newtab-search-submit, +#newtab-search-text[autofocus] + #newtab-search-submit { + background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#4cb1ff, #1793e5); + box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset, + 0 0 0 1px hsla(0,0%,100%,.1) inset, + 0 1px 0 hsla(210,54%,20%,.03); +} + +#newtab-search-text + #newtab-search-submit:hover { + background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted"), linear-gradient(#4cb1ff, #1793e5); + box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset, + 0 0 0 1px hsla(0,0%,100%,.1) inset, + 0 1px 0 hsla(210,54%,20%,.03), + 0 0 4px hsla(206,100%,20%,.2); +} + +#newtab-search-text + #newtab-search-submit:hover:active { + box-shadow: 0 1px 1px hsla(211,79%,6%,.1) inset, + 0 0 1px hsla(211,79%,6%,.2) inset; + transition-duration: 0ms; +} + +#newtab-search-text:focus + #newtab-search-submit:dir(rtl), +#newtab-search-text[keepfocus] + #newtab-search-submit:dir(rtl), +#newtab-search-text[autofocus] + #newtab-search-submit:dir(rtl), +#newtab-search-text + #newtab-search-submit:dir(rtl):hover { + background-image: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-rtl-inverted"), linear-gradient(#4cb1ff, #1793e5); +} + +/* CUSTOMIZE */ +#newtab-customize-overlay { + opacity: 0; + display: none; + width: 100%; + height: 100%; + background: #F9F9F9; + z-index: 100; + position: fixed; + transition: opacity .07s linear; +} + +.newtab-customize-panel-container { + position: absolute; + margin-right: 40px; + right: 0; +} + +.newtab-customize-panel-container:dir(rtl) { + right: auto; + left: 0; +} + +#newtab-customize-panel { + z-index: 999; + margin-top: 55px; + min-width: 270px; + position: absolute; + top: 100%; + right: -25px; + filter: drop-shadow(0 0 1px rgba(0,0,0,0.4)) drop-shadow(0 3px 4px rgba(0,0,0,0.4)); + transition: all 200ms ease-in-out; + transform-origin: top right; + transform: translate(-30px, -20px) scale(0) translate(30px, 20px); +} + +#newtab-customize-panel:dir(rtl) { + transform-origin: 40px top 20px; +} + +#newtab-customize-panel:dir(rtl), +#newtab-customize-panel-anchor:dir(rtl) { + left: 15px; + right: auto; +} + +#newtab-customize-panel[open="true"] { + transform: translate(-30px, -20px) scale(1) translate(30px, 20px); +} + +#newtab-customize-panel-anchor { + width: 18px; + height: 18px; + background-color: white; + transform: rotate(45deg); + position: absolute; + top: -6px; + right: 15px; +} + +#newtab-customize-title { + color: #7A7A7A; + font-size: 14px; + background-color: #FFFFFF; + line-height: 25px; + padding: 15px; + font-weight: 600; + cursor: default; + border-radius: 5px 5px 0px 0px; + max-width: 300px; + overflow: hidden; + display: table-cell; + border-top: none; +} + +#newtab-customize-panel-inner-wrapper { + background-color: #FFFFFF; + border-radius: 6px; + overflow: hidden; +} + +#newtab-customize-title > label { + cursor: default; +} + +#newtab-customize-panel > .panel-arrowcontainer > .panel-arrowcontent { + padding: 0; +} + +.newtab-customize-panel-item { + line-height: 25px; + padding: 15px; + padding-inline-start: 40px; + font-size: 14px; + cursor: pointer; + max-width: 300px; +} + +.newtab-customize-panel-item:not(:first-child) { + border-top: 1px solid threedshadow; +} + +.newtab-customize-panel-subitem > label, +.newtab-customize-panel-item > label, +.newtab-customize-complex-option { + padding: 0; + margin: 0; + cursor: pointer; +} + +.newtab-customize-panel-item, +.newtab-customize-complex-option { + display: block; + text-align: start; + background-color: #F9F9F9; +} + +.newtab-customize-panel-item[selected]:-moz-locale-dir(rtl) { + background-position: right 15px center; +} + +.newtab-customize-complex-option:hover > .selectable:not([selected]):-moz-locale-dir(rtl), +.selectable:not([selected]):hover:-moz-locale-dir(rtl) { + background-position: right 15px center; +} + +.newtab-customize-panel-item:not([selected]), +.newtab-customize-panel-subitem:not([selected]){ + color: #7A7A7A; +} + +.newtab-customize-panel-item:not([selected]):hover { + color: #FFFFFF; + background-color: #4A90E2 +} + +.newtab-customize-complex-option:hover > .selectable:not([selected]), +.selectable:not([selected]):hover { + background: url("chrome://global/skin/menu/shared-menu-check-hover.svg") no-repeat #FFFFFF; + background-size: 16px 16px; + background-position: 15px 15px; + color: #171F26; +} + +.newtab-customize-complex-option:hover > .selectable:not([selected]) + .newtab-customize-panel-subitem { + background-color: #FFFFFF; +} + +.newtab-customize-panel-item[selected] { + background: url("chrome://global/skin/menu/shared-menu-check-active.svg") no-repeat transparent; + background-size: 16px 16px; + background-position: 15px 15px; + color: black; + font-weight: 600; +} + +.newtab-customize-panel-subitem > .checkbox { + width: 18px; + height: 18px; + background-color: #FFFFFF; + border: solid 1px threedshadow; +} + +.newtab-customize-panel-subitem[selected] > .checkbox { + background: url("chrome://global/skin/menu/shared-menu-check-black.svg") no-repeat #FFFFFF; + background-size: 9px 9px; + background-position: center; + color: #333333; +} + +.newtab-customize-panel-subitem { + font-size: 12px; + padding: 0px 15px 15px 15px; + padding-inline-start: 40px; + display: block; + max-width: 300px; +} + +.newtab-customize-panel-subitem > label { + padding: 0px 10px; + line-height: 20px; + vertical-align: middle; + max-width: 225px; +} + +.newtab-customize-panel-superitem { + line-height: 20px; + border-bottom: medium none !important; + padding: 15px 15px 10px 15px; + padding-inline-start: 40px; + border-top: 1px solid threedshadow; +} + +.contentSearchSuggestionTable { + font: message-box; + font-size: 16px; +} diff --git a/application/basilisk/base/content/newtab/newTab.inadjacent.json b/application/basilisk/base/content/newtab/newTab.inadjacent.json new file mode 100644 index 000000000..53fb542af --- /dev/null +++ b/application/basilisk/base/content/newtab/newTab.inadjacent.json @@ -0,0 +1,3209 @@ +{ + "domains": [ + "rp5slFCxq/e7hYhXJCd0vQ==", + "2rEimAJDNX5g8HPZehOrGg==", + "nvLEpj6ZZF3LWH3wUB6lKg==", + "9Cqd4Lm3VvXuJxz79Bbqyg==", + "vNRy4LR+7TOKTixqsr5ybw==", + "N4zSgsZCo6Z4XRwZ4fu8WQ==", + "jsDtRfVbMsFg3KkEl2UiZQ==", + "TckkKpiq0a6J6NTw7uOZqw==", + "9Or7IAYuuIgZA370w9rNIg==", + "ul8WvOjCkxTz9LjT4RqTHg==", + "ZGJrbwb5878Nsqm0z+A7nQ==", + "5iT64HTeeG5SIFXG7A9o3w==", + "YSeSEghPe1kV6g8ghFcNAA==", + "0jIUl1NDmJZQkDY12VDeIQ==", + "aos6UyDyIw0R1nTK5wTawA==", + "G1xxubsq65ugK06UT2DO5A==", + "lbhavoDrDPP/8m0onwo63w==", + "ObcLsjW0SkdvY0nkZmiTGQ==", + "FHZ5084LC0nTAzZlnSKN3Q==", + "cdEr+0Fv5iaVZzalZToseg==", + "Co8WbNYbCPTFPcHpeK3hRQ==", + "qXSzhCEhByLQq9N84tqV+Q==", + "h3ufhRk5IEFaNH11rIACtQ==", + "fQ1PJ/JwazIaYoy/zy49QQ==", + "zAJqfbn54Nsm2ddGtkb59A==", + "ixPM9T8ik/gWGZ7BRIcaig==", + "/E9pwA3E3hVAZoYq3FmCyw==", + "U6ygonI8CxpruhpGB2+Q6A==", + "Igi4voB8oVMVw6WUeDSjZg==", + "jtuHIJhwoTGzavFpM7ilNw==", + "eBvTV27n6Gs+ZsBkpVynvw==", + "sFbzw0AUOGG0NEzkaSxVDg==", + "yAkIS+Ezj6woEff9YvdO7Q==", + "IP1+BwG6q60QzDADi8j7oA==", + "Q/teQEBFepHtwZ7UHa2TEA==", + "B1vDep5a1Gok5Gnth39+LA==", + "cyEIyQ2MZaPGf+K1x9Bbkg==", + "aaM+oEJnF4/nwMWyXJU8rA==", + "qpDNIpxah8FUiqXm5IRaUg==", + "ZTeJ35gMPqIv2WWbeNyIEg==", + "nzoAGQAnC/Xgg5PmOgXqkA==", + "J5pJDuNi3cqQiyaRJAJk4g==", + "2vqN53BXhXzPrKYsh6QH1A==", + "QlrzHNYxCwCBMVENvbXjQA==", + "Ou2HGn43nmsL3RWSNvMdXw==", + "3qk9lsvGTMqVMAZW+xihfw==", + "RncMe42RB2bhmUbYtGVnKQ==", + "hzNXR6dqPq1+vf4Qh5ByWA==", + "sRq3S2ZRs3H39cEQHv4Vig==", + "B4ThUBTVJOUPyOsHxikHXA==", + "A2lU9GkAdSibLO1JJfFnIA==", + "ef3HNkSvuWQrAzkuty2iqg==", + "yKDiRM6bf2xc0QXIwHYuaA==", + "AdCk4ccJuhA0bIT/61J+RQ==", + "UXvAZ7ULCVz2f505K0Wkvg==", + "ueKWblrOwVJNgiOvkXKLBQ==", + "s8u/jPuBAxu1d18HfV5Z0g==", + "hUT0Uc5YMUdNZQEGLz4hJw==", + "6jo/phmMTrEXKrNRsionGQ==", + "s/Ea/3fkyJ9honzPJkgEQQ==", + "hgu2/Jf+WrQAHfO+asW2zw==", + "kiVuTNwZ1r2lqYEZxIHyiQ==", + "24T5KVrVE2mYwJ5Goj3xJw==", + "fiWBVlfj97GGjEvf/Q9Spg==", + "5VWdlvJe7eoXMGkTtHzCUg==", + "+cFQxKa5RWVtc1z00Jujew==", + "nVa+rLH5p+yXBksLwQsjRQ==", + "5tyI6bMdb3tMIi4ewvr/SQ==", + "S6Roj31yS5bZbSFcd3f4Hg==", + "uW1Zl8iuEF8ZT/gwCBEqwA==", + "YwL+FJgxlZ8JVig+9iP5Cw==", + "ThIYK/mQsp9cMf8+rws/4Q==", + "w0oSxOhRG6kE9B868aoYVQ==", + "DJUDGQ0J32dF1kfItyxALg==", + "34/ab69lPkuAKt6WBxJPpA==", + "25jH4C9apgqWZGZP15lM6Q==", + "GxvwleSaSwILD1pG9k9buA==", + "YRAMt2ArEINo83ms6AqJ0A==", + "15HyTJNoMYzi3XCkeU5Z7A==", + "/SqjXGD+TKC90uz1vsjqUw==", + "karhKOknkhtg/LSFo9BGRA==", + "+tD1d0t3vfJvc1hUAvTT4Q==", + "rkaKbtlnyVr53D0rexLqdQ==", + "fAugw4rtnXzzRXfC1wRgOQ==", + "RgxoepF/XOwIsGat5r5HpQ==", + "Y49/EnzVz3ugXCYxFjFN7g==", + "tHMyzBm/2wNDw7TeNeujbg==", + "LlqzYV4uZpiJy4ORWPSekA==", + "M3Huar7/ded9OGgDwJhZgw==", + "QkNNATSx/PJ1XjgZyTtkUQ==", + "skVw0v6Wx00sfHAScPK1Bw==", + "v1gsIvg+C68T9wixMzL2sQ==", + "hDL75EXhl7BaYnAxkoGwbw==", + "tReG97snx2ESpXbfllCL7Q==", + "EiZBXR15dT1TMrkgkzmkvw==", + "5hRHirfD90/sdp0ILJQU8A==", + "rabElvtYtG0jW6dxAOHofg==", + "JpxoTRKWN+SEeBQ453R1YQ==", + "Faz4Lm0cpjvF0IjVkHiZMg==", + "jGErFAIoXx+50KFpVIGZiA==", + "5GzBkduKpUX7u1uwtYIFug==", + "cRBe0J9/KWRX19N2vPkCiw==", + "t/7g8t4Kr3/+SCnOn3XFWQ==", + "sd08c6jUXs5/hxND0fBkPA==", + "nTxKpqIdnHNdpDk7Dx3TEg==", + "5l+RALcce+lTDnmXI+Wqqg==", + "pzJ4QmEBGRNiiX6z0xHh8g==", + "Vfl3YbqR6JRR7SIdsUA/vA==", + "cgfhOdB376a4GAcuACADvA==", + "inAefsQM6tiIhQCMtcPcyA==", + "FTSULGL8CMhmcc7Cyf/X8A==", + "9XSWpaZyHEy7V/tuw5uZEw==", + "+VtM1opKlgb/jrCwc4YjFA==", + "oF46xheuI/NUxUOnOttzvA==", + "Qy+lvhDbJCumr6kiPLd1oA==", + "swps7UEKpIbVBJ9SnPK3zQ==", + "b7wyIiJvJs+29QePxsdWtQ==", + "x3iDZxYyuHtG/9rNW5HMYg==", + "r1dx1g5UOksywvOaQTamfA==", + "KvVF3Si/fr4JQtr7jCJiog==", + "spvZ7hhtG5QY7JXs96lBUg==", + "ECL8mA5B6CswyDH6yJ4hVw==", + "7Uu+YsdS69dMSDYUr6vTag==", + "Rnm9pSvQRRbkHpOijraLZw==", + "aQJqpnXdzqNSFhMn3EJA2Q==", + "TctnXpd7Wd5ZXKMnOFHAQA==", + "+lPqG8l6mf2FWVGWflyF/g==", + "mPmnmL2oRRJmKYjQ6TfN3g==", + "fyXFcT5ZCawDBg74n1WSpg==", + "uq5Zrxq10pO1HoPxReT5og==", + "3eoCsOKXY8RDrHSdlXqmrA==", + "9nQv2BFG56xsHViN5UpHYw==", + "RtP/nJgy/ItyuDrpBbAotg==", + "5E/drRptfHmBhJ7qplujGg==", + "cUxyZvoqXbQ0a/0I9s6Zbg==", + "womzqSigwEF30V422YmxKw==", + "FPvZqDfN8dTFHLVOuYEbUA==", + "YZMXx+scKXp/v9GaJjb1bA==", + "bjURu5MRsNIZavG5HV0eZw==", + "iY0C9uSMEOn8ikT+J7+/Eg==", + "aXkD6BzsdkMEv7A+eYqQQQ==", + "dOcOfEDGHYG2kgmrglDkPw==", + "c7GjtY05Mh+cp6SNuWY3Ig==", + "lM1uY1oVncHXNzKs/cCEtQ==", + "7jXnQJkutLsi+r9aYmrMxw==", + "NgrugWWduj2qdWnEQf9dLA==", + "faYjmy/yn5iXdS28QCIdWw==", + "68XbaOvIZpCGb4G1gaKErA==", + "Yi67HkOLtGYXeL7WD4GPrA==", + "Puo8gXuUkwcoQViaXwkdSQ==", + "L202Et5aZh60Vl20LTKNFg==", + "4agAzQ5+dnTmLZEjsZs26g==", + "LegGM1ft8Y7Ka3CUxpObvg==", + "KRdILc1QDOpow5im/qY+Kw==", + "peMW+rpwmXrSwplVuB/gTA==", + "Pic1ncr+Zn6wv75zjAdzQA==", + "ilSPlWYbiPzIC13vQUBlOw==", + "GUlDufLoTalBqrG/h3mZ6w==", + "5twANNlT57T9BG4r2D9+Hw==", + "ENrnM8HlMi+5y8Hsu4Pn4A==", + "K/DzpLEbz1MpRjA6qyYn4Q==", + "yN1cHJRHDXoFxFZacL6wsw==", + "Rc6r+KqIePH+dnj1aNYCsQ==", + "8u/z5htgqXVU5Dqwd9whJQ==", + "jV575O42EYoqDNxCm9643Q==", + "xCxGo0h3lS8N6X+ivKfpjA==", + "us+2nfpj2gjI7s14Hw0gmA==", + "bp90A/rbESwVU7eh9xRTfQ==", + "5QtMXzbTafvKDQOWZP7M8w==", + "1gFCxPLjQlQGKmSGmHwmJQ==", + "m+/dnOIe6SaIFhfvg+ybDg==", + "9Dcg87+RPq9U+swRg4dH3Q==", + "mnjbL7WFmrWp0RUqS8AMGA==", + "/0e0E+NFmq8GeE5+y2Gekw==", + "y11mbpHHtka9Ep8cr2nEvQ==", + "GdmjRyliw+W21Q+dHO4CWA==", + "X65wWQTpkg756V/Nfn92kQ==", + "xj06KvacQOxRSofbhzBNgA==", + "nVDxVhaa2o38gd1XJgE3aw==", + "4IV+JOGXrltpkQamBRXMgA==", + "jIfp8LqaYXT88r/K3a8gNw==", + "vhT4dDtbMFVyevS6yCGy0g==", + "zMs7/x8hDt8xj2FFc5+6vA==", + "1J7u2N62JGb2VrnCRlJIrw==", + "3hJs9P/RRxB0CO4q0Icb+g==", + "ZpuVY1ZyoKD3hqosdsfT6Q==", + "6KIM7C7eWgxZtqZboiJvZQ==", + "vtb6fdqirkuUkqITAmXTlw==", + "C/rEVr22mw2u/1dwUx9VTg==", + "NrY4Q5C67haCWLK8HXHq9g==", + "X9qvEftCEFWX3gBU5hXy+Q==", + "0zgw7xNB3xVGaH48TyxaNQ==", + "g7J9Jy/PJrAGRgVdvA+bEg==", + "9zb+anAyZVBzuU9rW4cJtg==", + "6Zc5FzT/m0YIjxEPYA6zDQ==", + "R2YPNlvCbVK0EodTR7czIw==", + "gsI6EGgXMtDu+1u364A8mw==", + "Bg2wBFb1/xaxeEiHfBHX+A==", + "64aapfVI6dV3LpTK56KZlg==", + "HdgcJU0W3yVnH69VYStmug==", + "qTDCcv+LK3JPFB/++t66IQ==", + "P0HEIXMnAmbvq+QYREwFzw==", + "aaU7CAmtyE35jNKTkyXOkg==", + "r9G97WKDiQ48qJHP9LBRNg==", + "8mPgQhYVDn8KshDDvvf5SA==", + "GCQiiOLDguXLiYwuLcFPsA==", + "R2Use39If2C0FVBP7KDerA==", + "23C4eh3yBb5n/RNZeTyJkA==", + "2QQtKtBAm2AjJ5c0WQ6BQA==", + "Qc+XYy2qyWJ5VVwd2PExbw==", + "zJ7ScHNxr2leCDNNcuDApA==", + "vFtC0B2oe1gck28JOM1dyg==", + "bLEntCrCHFy9pg3T3gbBzg==", + "G3PmmPGHaWHpPW30xQgm3Q==", + "me61ST+JrXM5k3/a11gRAA==", + "+LJYVZl1iPrdMU3L5+nxZw==", + "CLPzjXKGGpJ0VrkSJp7wPQ==", + "Pc+u0MAzp4lndTz4m6oQ5w==", + "cwBNvZc0u4bGABo88YUsVQ==", + "q7m/EtZySBjZNBjQ5m1hKw==", + "8ZBiwr842ZMKphlqmNngHw==", + "LMCZqd3UoF/kHHwzTdj7Tw==", + "0ODJyWKJSfObo+FNdRQkkA==", + "ViweSJuNWbx5Lc49ETEs/A==", + "x+8rwkqKCv0juoT5m1A4eg==", + "pxuSWn1u+bHtRjyh2Z8veA==", + "GKzs8mlnQQc58CyOBTlfIg==", + "Owg8qCpjZa+PmbhZew6/sw==", + "YLz+HA6qIneP+4naavq44Q==", + "9ajIS45NTicqRANzRhDWFA==", + "DjeSrUoWW2QAZOAybeLGJg==", + "qxALQrqHoDq9d91nU0DckA==", + "yPIeWcW8+3HjDagegrN8bw==", + "ocpLRASvTgqfkY20YlVFHQ==", + "RuLeQHP1wHsxhdmYMcgtrQ==", + "3WwITQML938W9+MUM56a3A==", + "ZbLVNTQSVZQWTNgC4ZGfQg==", + "X6Ln4si8G5aKar52ZH/FEQ==", + "+gbitI/gpxebN/rK7qj8Fw==", + "7cnUHeaPO8txZGGWHL9tKg==", + "epY+dsm5EMoXnZCnO4WSHw==", + "nf8x+F03kOpMhsCSUWEhVg==", + "VE4sLM5bKlLdk85sslxiLQ==", + "Hs3vUOOs2TWQdQZHs+FaQQ==", + "hkOBNoHbno2iNR7t3/d4vg==", + "Ar9N1VYgE7riwmcrM3bA2Q==", + "SbMjjI8/P8B9a9H2G0wHEQ==", + "tU31r8zla146sqczdKXufg==", + "tFmWYH82I3zb+ymk5dhepA==", + "XHjrTLXkm/bBY/BewmJcCQ==", + "FV/D5uSco+Iz8L+5t7E8SA==", + "yKLLiqzxfrCsr6+Rm6kx1Q==", + "B6reUwMkQFaCHb9BYZExpw==", + "5jyuDp82Fux+B0+zlx8EXw==", + "WGKFTWJac8uehn3N59yHJw==", + "JQf9UmutPh3tAnu7FDk3nA==", + "hv5GrLEIjPb4bGOi8RSO0w==", + "p3V7NfveB6cNxFW7+XQNeQ==", + "DinJuuBX9OKsK5fUtcaTcQ==", + "UEMwF4kwgIGxGT4jrBhMPQ==", + "Y78dviyBS3Jq9zoRD5sZtQ==", + "zbjXhZaeyMfdTb2zxvmRMg==", + "kydoXVaNcx1peR5g6i588g==", + "M2suCoFHJ5fh9oKEpUG3xA==", + "/VnKh/NDv7y/bfO6CWsLaQ==", + "S+b37XhKRm8cDwRb1gSsKQ==", + "jz7QlwxCIzysP39Cgro8jg==", + "IjmLaf3stWDAwvjzNbJpQA==", + "cHSj5dpQ04h/WyefjABfmQ==", + "+gO0bg8LY+py2dLM1sM7Ag==", + "fSANOaHD0Koaqg7AoieY9A==", + "vqYHQ3MnHrAIAr1QHwfIag==", + "Uh1mvZNGehK1AaI4a1auKQ==", + "HCbHUfsTDl6+bxPjT57lrA==", + "S7Vjy/gOWp0HozPP1RUOZw==", + "KPh6TwYpspne4KZA6NyMbw==", + "cfh5VZFmIqJH/bKboDvtlA==", + "H1zH9I8RwfEy5DGz3z+dHw==", + "2ksediOVrh4asSBxKcudTg==", + "+jVN/3ASc2O44sX6ab8/cg==", + "uvKYnKE01D5r7kR9UQyo5A==", + "BB9PTlwKAWkExt3kKC/Wog==", + "yqQPU4jT9XvRABZgNQXjgg==", + "6v3eTZtPYBfKFSjfOo2UaA==", + "49z/15Nx9Og7dN9ebVqIzg==", + "VjclDY8HN4fSpB263jsEiQ==", + "vSKsa0JhLCe9QFZKkcj58Q==", + "PolhKCedOsplEcaX4hQ0YQ==", + "D0Qt9sRlMaPnOv1xaq+XUg==", + "gBgJF0PiGEfcUnXF0RO7/w==", + "sC11Rf/mau3FG5SnON4+vQ==", + "rKb3TBM4EPx/RErFOFVCnQ==", + "+n0K7OB2ItzhySZ4rhUrMg==", + "Epm0d/DvXkOFeM4hoPCBrg==", + "K8PVQhEJCEH1ghwOdztjRw==", + "xjA21QjNdThLW3VV7SCnrg==", + "nE72uQToQFVLOzcu/nMjww==", + "2Hc5oyl0AYRy2VzcDKy+VA==", + "Y7XpxIwsGK3Lm/7jX/rRmg==", + "MK7AqlJIGqK2+K5mCvMXRQ==", + "mXycPfF5zOvcj1p4hnikWw==", + "V1fvtnJ0L3sluj9nI5KzRw==", + "TahqPgS7kEg+y6Df0HBASw==", + "EKU3OVlT4b/8j3MTBqpMNg==", + "EdvIAKdRAXj7e42mMlFOGQ==", + "uPm+cF4Jq08S5pQhYFjU8A==", + "CnIwpRVC2URVfoiymnsdYQ==", + "wyx5mnUMgP5wjykjAfTO7w==", + "OwIGvTh8FPFqa4ijNkguAw==", + "4ID0PHTzIMZz2rQqDGBVfA==", + "rlXt6zKE7DswUl0oWGOQUQ==", + "4NP8EFFJyPcuQKnBSxzKgQ==", + "bJgsuw29cO2WozqsGZxl7w==", + "b3q8kjHJPj9DWrz3yNgwjQ==", + "QGYFMpkv37CS2wmyp42ppg==", + "Kzs+/IZJO8v4uIv9mlyJ2Q==", + "ZJY+hujfd58mTKTdsmHoQQ==", + "R8FxgXWKBpEVbnl41+tWEw==", + "+CvLiih/gf2ugXAF+LgWqw==", + "BDbfe/xa9Mz1lVD82ZYRGA==", + "Dz90OhYEjpaJ/pxwg1Qxhg==", + "MLHt6Ak288G0RGhCVaOeqA==", + "r0QffVKB9OD9yGsOtqzlhA==", + "hK8KhTFcR06onlIJjTji/Q==", + "wMum67lfk5E1ohUObJgrOg==", + "JKmZqz9cUnj6eTsWnFaB0A==", + "rtJdfki8fG6CB36CADp0QA==", + "cUyqCa7Oue934riyC17F8g==", + "y4Y4mSSTw/WrIdRpktc5Hw==", + "r36kVMpF+9J+sfI3GeGqow==", + "ydVj2odhergi+2zGUwK4/A==", + "J2NFyb8cXEpZyxWDthYQiA==", + "qYuo5vY8V3tZx41Kh9/4Dw==", + "jrfRznO0nAz6tZM1mHOKIA==", + "JSr/lqDej81xqUvd/O2s7w==", + "vHGjRRSlZHJIliCwIkCAmQ==", + "sQAxqWXeiu/Su0pnnXgI9A==", + "xPe76nHyHmald6kmMQsKdg==", + "50jASqzGm4VyHJbFv8qVRA==", + "uuiJ+yB7JLDh2ulthM0mjg==", + "TI90EuS/bHq/CAlX32UFXg==", + "JgxNrUlL8wutG04ogKFPvw==", + "aMa1yVA71/w6Uf1Szc9rMA==", + "k/Aou2Jmyh8Bu3k8/+ndsQ==", + "iANKiuMqWzrHSk9nbPe3bQ==", + "7GgNLBppgAKcgJCDSsRqOQ==", + "bzVeU2qM9zHuzf7cVIsSZw==", + "rkeLYwMZ1/pW2EmIibALfA==", + "91+Yms6Oy/rP0rVjha5z9w==", + "JgXSPXDqaS1G9NqmJXZG0A==", + "ZzduJxTnXLD9EPKMn1LI4Q==", + "6W79FmpUN1ByNtv5IEXY4w==", + "Y1Nm3omeWX2MXaCjDDYnWQ==", + "ejfikwrSPMqEHjZAk3DMkA==", + "WNfDNaWUOqABQ6c6kR+eyw==", + "4BkqgraeXY7yaI1FE07Evw==", + "AjHz9GkRTFPjrqBokCDzFw==", + "T/6gSz2HwWJDFIVrmcm8Ug==", + "VWy9lB5t4fNCp4O/4n8S4w==", + "/FdZzSprPnNDPwbhV1C0Cg==", + "LUWxfy4lfgB5wUrqCOUisw==", + "r1VGXWeqGeGbfKjigaAS+Q==", + "ztULoqHvCOE6qV7ocqa4/w==", + "QCpzCTReHxGm5lcLsgwPCA==", + "Hst3yfyTB7yBUinvVzYROQ==", + "gf1Ypna/Tt+TZ08Y+GcvGg==", + "3rbml1D0gfXnwOs5jRZ3gA==", + "2vm7g3rk1ACJOTCXkLB3zA==", + "11FE2kknwYi2Qu0JUKMn3A==", + "1b2uf+CdVjufqiVpUShvHw==", + "0a4SafpDIe8V4FlFWYkMHw==", + "7btpMFgeGkUsiTtsmNxGQA==", + "dUx1REyXKiDFAABooqrKEA==", + "knYKU74onR6NkGVjQLezZg==", + "Scto+9TWxj1eZgvNKo+a9A==", + "cvZT1pvNbIL8TWg+SoTZdA==", + "1nXByug2eKq0kR3H3VjnWQ==", + "tG+rpfJBXlyGXxTmkceiKA==", + "7W9aF7dxnL+E8lbS/F7brg==", + "8vr+ERVrM99dp+IGnCWDGQ==", + "oFNMOKbQXcydxnp8fUNOHw==", + "uJZGw3IY2nCcdVeWW1geNQ==", + "q6LG0VzO1oxiogAAU63hyg==", + "f0H/AFSx2KLZi9kVx5BAZg==", + "1RQZ2pWSxT+RKyhBigtSFg==", + "scCQPl0em2Zmv/RQYar60g==", + "A2ODff+ImIkreJtDPUVrlg==", + "vRgkZZGVN7YZrlml0vxrKA==", + "68jPYo3znYoU4uWI7FH3/g==", + "iJ2nT8w8LuK11IXYqBK+YA==", + "54XELlPm8gBvx8D5bN3aUg==", + "PTAm/jGkie7OlgVOvPKpaA==", + "v7BrkRmK0FfWSHunTRHQFQ==", + "dVh/XMTUIx1nYN4q1iH1bA==", + "TSGL3iQYUgVg/O9SBKP9EA==", + "wTO49YX/ePHMWtcoxUAHpw==", + "bMb1ia0rElr2ZpZVhva0Jw==", + "sNmW2b2Ud7dZi3qOF8O8EQ==", + "3djRJvkZk9O2bZeUTe+7xQ==", + "I9KNZC1tijiG1T72C4cVqQ==", + "sQzCwNDlRsSH7iB9cTbBcg==", + "mk1CKDah7EzDJEdhL22B7w==", + "lON3WM0uMJ30F8poBMvAjQ==", + "88PNi9+yn3Bp4/upgxtWGA==", + "C+Ssp+v1r+00+qiTy2d7kA==", + "11U5XEwfMI7avx014LfC8g==", + "xsf0m31Am0W9eLhopAkfnA==", + "d13Rj3NJdcat0K/kxlHLFw==", + "UP7NXAE0uxHRXUAWPhto0w==", + "ZKXxq9yr7NGBOHidht34uQ==", + "Fd2fYFs8vtjws2kx1gf6Rw==", + "ojf6uL85EuEYgLvHoGhUrw==", + "KjnL3x+56r3M2pDj1pPihA==", + "WdCWezJU4JK43EOZ9YHVdg==", + "/jH6imhTPZ/tHI4gYz2+HA==", + "+OLntmlsMBBYPREPnS6iVw==", + "5lfLJAk1L3QzGMML3fOuSw==", + "AZs3v4KJYxdi8T1gjVjI2Q==", + "7pkUY2UzSbGnwLvyRrbxfA==", + "BjfOelfc1IBgmUxMJFjlbQ==", + "TcGhAJHRr7eMwGeFgpFBhg==", + "Y7iDCWYrO1coopM3RZWIPg==", + "mnalaO6xJucSiZ0+99r3Cg==", + "plXHHzA8X9QGwWzlJxhLRw==", + "Zqd6+81TwYuiIgLrToFOTQ==", + "1Pmnur6TbZ9cmemvu0+dSA==", + "OaNpzwshdHUZMphQXa6i8w==", + "WKehT4nGF2T7aKuzABDMlA==", + "4LvQSicqsgxQFWauqlcEjw==", + "BMZB1FwvAuEqyrd0rZrEzw==", + "YfbfE3WyYOW7083Y8sGfwQ==", + "46FCwqh+eMkf+czjhjworw==", + "734u4Y1R3u7UNUnD+wWUoA==", + "yf06Slv9l3IZEjVqvxP2aA==", + "bIk7Fa6SW7X18hfDjTKowg==", + "DnF6TYSJxlc+cwdfevLYng==", + "ionqS0piAOY2LeSReAz4zg==", + "hlMumZ7RJFpILuKs09ABtw==", + "NjeDgQ1nzH1XGRnLNqCmSg==", + "o7y4zQXQAryST2cak4gVbw==", + "29EybnMEO95Ng4l/qK4NWQ==", + "udU65VtsvJspYmamiOsgXw==", + "v1AWe5qb5y3vSKFb7ADeEw==", + "wK6Srd83eLigZ11Q20XGrg==", + "GmC+0rNDMIR+YbUudoNUXw==", + "W4utAK3ws0zjiba/3i91YA==", + "MlKWxeEh8404vXenBLq4bw==", + "Gdf4VEDLBrKJNQ8qzDsIyw==", + "Z9bDWIgcq6XwMoU2ECDR5Q==", + "VIkS30v268x+M1GCcq/A8A==", + "iPwX3SbbG9ez9HoHsrHbKw==", + "yKrsKX4/1B1C0TyvciNz5w==", + "BophnnMszW5o+ywgb+3Qbw==", + "eJLrGwPRa6NgWiOrw1pA7w==", + "eV+RwWPiGEB+76bqvw+hbA==", + "oad5SwflzN0vfNcyEyF4EA==", + "Uw6Iw+TP9ZdZGm2b/DAmkg==", + "9qWLbRLXWIBJUXYjYhY2pg==", + "dxWv00FN/2Cgmgq9U3NVDQ==", + "AX1HxQKXD12Yv5HWi39aPQ==", + "J0NauydfKsACUUEpMhQg8A==", + "mxug34EekabLz0JynutfBg==", + "bNq/hj0Cjt4lkLQeVxDVdQ==", + "nW3zZshjZEoM8KVJoVfnuQ==", + "ghp8sWGKWw20S/z1tbTxFg==", + "S4rFuiKLFKZ+cL7ldiTwpg==", + "8ZqmPJDnQSOFXvNMRQYG2Q==", + "6XYqR2WvDzx4fWO7BIOTjA==", + "Uo+FIhw1mfjF6/M8cE1c/Q==", + "bsHIShcLS134C+dTxFQHyA==", + "19yQHaBemtlgo2QkU5M6jQ==", + "sWLcS+m4aWk31BiBF+vfJQ==", + "BlCgDd7EYDIqnoAiKOXX6Q==", + "MrxR3cJaDHp0t3jQNThEyg==", + "cMo6l1EQESx1rIo+R4Vogg==", + "VOvrzqiZ1EHw+ZzzTWtpsw==", + "1/ZheMsbojazxt31j/l3iA==", + "0QxPAqRF8inBuFEEzNmLjA==", + "UXUNYEOffgW3AdBs7zTMFA==", + "lOPJhHqCtMRFZfWMX/vFZQ==", + "rXSbbRABEf4Ymtda45w8Fw==", + "jfegbZSZWkDoPulFomVntA==", + "hfcH5Az2M7rp+EjtVpPwsg==", + "VsXEBIaMkVftkxt1kIh7TA==", + "M20iX2sUfw5SXaZLZYlTaA==", + "VUDsc9RMS1fSM43c+Jo9dQ==", + "itPtn+JaO4i7wz2wOPOmDQ==", + "rCxoo4TP/+fupXMuIM0sDA==", + "cSHSg9xJz/3F6kc+hKXkwg==", + "b4BoZmzVErvuynxirLxn0w==", + "e4B3HmWjW+6hQzcOLru6Xg==", + "lTE6u9G/RzvmbuAzq2J2/Q==", + "897ptlztTjr7yk+pk8MT0Q==", + "jd6IpPJwOJW1otHKtKZ5Gw==", + "b4aFwwcWMXsSdgS1AdFOXA==", + "FltEN+7NKvzt+XAktHpfHA==", + "ZyDh3vCQWzS5DI1zSasXWA==", + "kcJ1acgBv6FtUhV8KuWoow==", + "zgEyxj/sCs63O98sZS94Yw==", + "/kGxvyEokQsVz0xlKzCn2A==", + "cxqHS4UbPolcYUwMMzgoOA==", + "62RHCbpGU8Hb+Ubn+SCTBg==", + "ePlsM/iOMme2jEUYwi15ng==", + "0fN+eHlbRS6mVZBbH/B9FQ==", + "k0XIjxp2vFG7sTrKcfAihA==", + "0rfG4gRugAwVP0i3AGVxxg==", + "M98hjSxCwvZ27aBaJTGozQ==", + "kzGNkWh3fz27cZer4BspUQ==", + "3CJbrUdW68E3Drhe4ahUnQ==", + "NGApiVkDSwzO45GT57GDQw==", + "lMjip5hbCjkD9JQjuhewDg==", + "GrSbnecYAC3j5gtoKntL0A==", + "9dbn0Kzwr9adCEfBJh78uQ==", + "64QzHOYX0A9++FqRzZRHlQ==", + "YZt6HwCvdI5DRQqndA/hBQ==", + "6GXHGF62/+jZ7PfIBlMxZw==", + "PBULPuFXb6V3Di713n3Gug==", + "8Cm19vJW8ivhFPy0oQXVNA==", + "zDSQ3NJuUGkVOlvVCATRwA==", + "6QAtjOK9enNLRhcVa2iaTg==", + "v/PshI6JjkL9nojLlMNfhg==", + "yTgN5xFIdz1MzFS6xMl5uQ==", + "SCO9nQncEcyVXGCtx30Jdg==", + "7b0oo4+qphu6HRvJq6qkHQ==", + "ol9xhVTG9e1wNo50JdZbOA==", + "hIABph+vhtSF5kkZQtOCTA==", + "k+IBS52XdOe5/hLp28ufnA==", + "6HnWgYNKohqhoa1tnjjU3A==", + "HDxGhvdQwGh0aLRYEGFqnw==", + "LDuBcL5r3PUuzKKZ9x6Kfw==", + "HPvYV94ufwiNHEImu4OYvQ==", + "h2cnQQF2/R3Mq2hWdDdrTg==", + "nqpKfidczdgrNaAyPi7BOQ==", + "2ywo4t5PPSVUCWDwUlOVwQ==", + "jZMDIu95ITTjaUX0pk4V5g==", + "bA2kaTpeXflTElTnQRp6GQ==", + "lwYQm2ynA3ik2gE1m11IEg==", + "5ugVOraop5P5z5XLlYPJyQ==", + "l2NppPcweAtmA1V2CNdk2Q==", + "DbWQI3H2tcJsVJThszfHGA==", + "H6HPFAcdHFbQUNrYnB74dA==", + "H1NJEI+fvOQbI51kaNQQjQ==", + "53UccFNzMi9mKmdeD82vAw==", + "lffapwUUgaQOIqLz2QPbAg==", + "rSvhrHyIlnIBlfNJqemEbw==", + "BLJk9wA88z6e0IQNrWJIVw==", + "5m1ijXEW+4RTNGZsDA/rxQ==", + "GG8a3BlwGrYIwZH9j3cnPA==", + "HhBHt5lQauNl7EZXpsDHJA==", + "/XjB6c5fxFGcKVAQ4o+OMw==", + "+tuUmnRDRWVLA+1k0dcUvg==", + "SM7E98MyViSSS9G0Pwzwyw==", + "c5q/8n7Oeffv3B1snHM/lA==", + "kwlAQhR2jPMmfLTAwcmoxw==", + "0b/xj6fd0x+aB8EB0LC4SA==", + "S8jlvuYuankCnvIvMVMzmg==", + "kZkmDatUOdIqs7GzH3nI1A==", + "obW3kzv2KBvuckU7F+tfjA==", + "pa8nkpAAzDKUldWjIvYMYg==", + "m+eh+ZqS74w2q0vejBkjaw==", + "LcoJBEPTlSsQwfuoKQUxEw==", + "KO2XVYyNZadcQv8aCNn5JA==", + "uvzmRcvgepW6mZbMfYgcNw==", + "KhUT2buOXavGCpcDOcbOYg==", + "fo3JL+2kPgDWfP+CCrFlFw==", + "wIfvvLKC61gOpsddUFjVog==", + "SPHU6ES1WVm0Mu2LB+YjrA==", + "LWWfRqgtph1XrpxF4N64TA==", + "LCvz/h9hbouXCmdWDPGWqg==", + "PXC6ZpdMH0ATis/jGW12iA==", + "z920R8eahJPiTsifrPYdxA==", + "GIHKW6plyLra0BmMOurFgA==", + "k6OmSlaSZ5CB0i7SD9LczQ==", + "YZ39RIXpeLAhyMgmW2vfkQ==", + "bs2QG8yYWxPzhtyMqO6u3A==", + "pKaTI+TfcV3p/sxbd2e7YQ==", + "xWYecfzAtXT9WyQ8NYY/hw==", + "Fz8EI+ZpYlbcttSHs5PfpA==", + "wfwuxn+Vja1DNwiDwL2pcQ==", + "wux5Y8AipBnc5tJapTzgEQ==", + "U+oTpcjhc0E+6UjP11OE/Q==", + "yTVJKBn72RjakMBXDoBKHg==", + "0TxcYwG72dT7Tg+eG8pP1w==", + "imZ+mwiT22sW2M9alcUFfg==", + "CkDIoAFLlIRXra78bxT/ZA==", + "4qMSNAxichi3ori/pR+o0w==", + "zNLlWGW/aKBhUwQZ4DZWoQ==", + "D31ZticrjGWAO45l5hFh7A==", + "HdXg64DBy5WcL5fRRiUVOg==", + "yhI5jHlfFJxu4eV5VJO2zQ==", + "e9GqAEnk8XI5ix6kJuieNQ==", + "EC0+iUdSZvmIEzipXgj7Gg==", + "chwv4+xbEAa93PHg8q9zgQ==", + "B1VVUbl8pU0Phyl1RYrmBg==", + "A+DLpIlYyCb9DaarpLN76g==", + "wHA+D5cObfV3kGORCdEknw==", + "+Mp+JIyO0XC5urvMyi3wvQ==", + "vUE8Iw3NyWXURpXyoNJdaw==", + "ParhxI6RtLETBSwB0vwChQ==", + "NxSdT2+MUkQN49pyNO2bJw==", + "JSyhTcHLTfzHsPrxJyiVrA==", + "PAlx9+U+yQCAc5Fi0BOG0w==", + "W/0s1x3Qm+wN8DhROk6FrQ==", + "L3Jt5dHQpWQk74IAuDOL8g==", + "VWb8U4jF/Ic0+wpoXi/y/g==", + "1wBuHqS1ciup31WTfm3NPg==", + "BDNM1u/9mefjuW1YM2DuBg==", + "SDi5+FoP9bMyKYp+vVv1XA==", + "23d9B9Gz5kUOi1I//EYsSQ==", + "/a9O7kWeXa0le45ab3+nVw==", + "PcoVtZrS1x1Q+6nfm4f80w==", + "A6TLWhipfymkjPYq8kaoDQ==", + "lzUQ1o7JAbdJYpmEqi6KnQ==", + "/2jGyMekNu7U136K+2N3Jg==", + "ZItMIn1vhGqAlpDHclg0Ig==", + "Ee4A3lTMLQ7iDQ7b8QP8Qg==", + "bO55S58bqDiRWXSAIUGJKw==", + "zeHF6fdeqcOId3fRUGscRw==", + "BxsDnI8jXr4lBwDbyHaYXw==", + "ylA6sU7Kaf9fMNIx1+sIlw==", + "ZWXfE3uGU91WpPMGyknmqw==", + "f1+fHgR5rDPsCZOzqrHM7Q==", + "8VqeoQELbCs232+Mu+HblA==", + "beSrliUu0BOadCWmx+yZyA==", + "NQVQfN3nIg9ipHiFh4BvfQ==", + "4wnUAbPT3AHRJrPwTTEjyw==", + "/cdR1i5TuQvO+u3Ov3b0KQ==", + "wtyAZIfhomcHe9dLbYoSvA==", + "ulpDxLeQnIRPnq6oaah2AA==", + "pdPwUHauXOowaq9hpL2yFw==", + "1+A9FCGP3bZhk6gU3LQtNg==", + "raYifKqev8pASjjuV+UTKQ==", + "+OERSmo7OQUUjudkccSMOA==", + "FeRovookFQIsXmHXUJhGOw==", + "USCvrMEm/Wqeu9oX6FrgcQ==", + "kly/2kE4/7ffbO34WTgoGg==", + "IindlAnepkazs5DssBCPhA==", + "Bq82MoMcDjIo/exqd/6UoA==", + "ocvA1/NbyxM0hanwwY6EiA==", + "rtd6mqFgGe98mqO0pFGbSw==", + "nvLEpj6ZZF3LWH3wUB6lKg==", + "AGd0rcLnQ0n+meYyJur1Pw==", + "wI7JrSPQwYHpv2lRsQu9nQ==", + "OnmvXbyT2BYsSDJYZhLScA==", + "CmBf5qchS1V3C2mS6Rl4bw==", + "TafM7nTE5d+tBpRCsb8TjQ==", + "wxkb8evGEaGf/rg/1XUWiA==", + "y1J+o6DC2sETFsySgpDZyA==", + "SVLHWPCCH7GPVCF7QApPbw==", + "HMWOlMmzocOIiJ7yG1YaDQ==", + "DJmrmNRKARzsTCKSMLmcNA==", + "/XC/FmMIOdhMTPqmy4DfUA==", + "63OTPaKM0xCfJOy9EDto+Q==", + "PxReytUUn/BbxYTFMu1r2Q==", + "WjDqf1LyFyhdd8qkwWk+MA==", + "/DiUApY7cVp5W9o24rkgRA==", + "alJtvTAD7dH/zss/Ek1DMQ==", + "xLm/bJBonpTs0PwsF0DvRg==", + "eAOEgF5N80A/oDVnlZYRAw==", + "LqgzKxbI6WTMz0AMIDJR5w==", + "MJ1FuK8PXcmnBAG9meU84A==", + "JLq/DrW2f26NaRwfpDXIEA==", + "fsrX00onlGvfsuiCc35pGg==", + "tXVb5f90k9l3e1oK2NGXog==", + "1JRgSHnfAQFQtSkFTttkqQ==", + "B0TaUQ6dKhPfSc5V/MjLEQ==", + "nkbLVLvh3ClKED97+nH+7Q==", + "avFTp3rS6z5zxQUZQuaBHQ==", + "lNF8PvUIN02NattcGi5u4g==", + "bBEndaOStXBpAK79FrgHaw==", + "dM9up4vKQV5LeX82j//1jQ==", + "4WO6eT0Rh6sokb29zSJQnQ==", + "RHKCMAqrPjvUYt13BVcmvw==", + "Ju4YwtPw+MKzpbC0wJsZow==", + "tzV7ixFH37ze4zuLILTlfA==", + "oPlhC4ebXdkIDazeMSn1fQ==", + "5pje7qyz8BRsa8U4a4rmoA==", + "7E6V6/zSjbtqraG7Umj+Jw==", + "8QK7emHS6rAcAF5QQemW/A==", + "LhqRc9oewY4XaaXTcnXIHQ==", + "p/7qM5+Lwzw1/lIPY91YxQ==", + "fy54Milpa7KZH/zgrDmMXQ==", + "LyPXOoOPMieqINtX8C9Zag==", + "aD4QvtMlr8Lk/zZgZ6zIMg==", + "dsueq9eygFXILDC7ZpamuA==", + "+mJLK+6qq8xFv7O/mbILTw==", + "nHUpYmfV59fe3RWaXhPs3Q==", + "VbCoGr8apEcN7xfdaVwVXw==", + "/2Chaw2M9DzsadFFkCu6WQ==", + "rKAQxu80Q8g1EEhW5Wh8tg==", + "RJJqFMeiCZHdsqs72J17MQ==", + "GF2yvI9UWf1WY7V7HXmKPA==", + "JyIDGL1m/w+pQDOyyeYupA==", + "wR2Gxb07nkaPcZHlEjr8iA==", + "PbDVq2Iw1eeM8c2o/XYdTA==", + "BL3buzSCV78rCXNEhUhuKQ==", + "i42XumprV/aDT5R0HcmfIQ==", + "DuEKxykezAvyaFO2/5ZmKQ==", + "6ACvJNfryPSjGOK39ov8Qg==", + "YaUKOTyByjUvp1XaoLiW5Q==", + "jNcMS2zX1iSZN9uYnb2EIg==", + "VRnx+kd6VdxChwsfbo1oeQ==", + "4Qinl7cWmVeLJgah8bcNkw==", + "Fiy3hkcGZQjNKSQP9vRqyA==", + "HaSc7MZphCMysTy2JbTJkw==", + "VhYGC8KYe5Up+UJ2OTLKUw==", + "K2gk9zWGd0lJFRMQ1AjQ/Q==", + "NfxVYc3RNWZwzh2RmfXpiA==", + "JGeqHRQpf4No74aCs+YTfA==", + "7VHlLw20dWck+I8tCEZilA==", + "V5HKdaTHjA8IzvHNd9C51g==", + "9TalxEyFgy6hFCM73hgb7Q==", + "R/y6+JJP8rzz1KITJ4qWBw==", + "7bM/pn4G7g7Zl6Xf1r62Lg==", + "CHsFJfsvZkPWDXkA6ZMsDQ==", + "uXuPA/2KJbb7ZX+NymN3dw==", + "o+nYS4TqJc6XOiuUzEpC3A==", + "8N3mhHt29FZDHn1P2WH1wQ==", + "uZ2gUA74/7Q33tI2TcGQlg==", + "8B12CamjOGzJDnQ+RkUf4w==", + "9FdpxlIFu11qIPdO7WC5nw==", + "G+sGF13VXPH4Ih6XgFEXxg==", + "y+1I05LDAYJ09tKMs3zW6g==", + "gnkadeCgjdmLdlu/AjBZJg==", + "1I+UVx3krrD4NhzO7dgfHQ==", + "8LNNoHe6rEQyJ0ebl151Mw==", + "yOE90OHQdyOfrAgwDvn2gA==", + "ayBGGPEy++biljvGcwIjXA==", + "o/Y4U6rWfsUCXJ72p5CUGw==", + "5kvyy902llnYGQdn2Py04w==", + "6k2cuk0McTThSMW/QRHfjA==", + "2XrR2hjDEvx8MQpHk9dnjw==", + "fv/PW8oexJYWf5De30fdLQ==", + "861mBNvjIkVgkBiocCUj/Q==", + "NKGY0ANVZ0gnUtzVx1pKSw==", + "4DIPP/yWRgRuFqVeqIyxMQ==", + "cgSEbLqqvDsNUyeA3ryJ6Q==", + "xbBxUP9JyY0wDgHDipBHeg==", + "c3WVxyC5ZFtzGeQlH5Gw+w==", + "ZKeTDCboOgCptrjSfgu0xw==", + "DjHszpS8Dgocv3oQkW/VZQ==", + "Iqszlv4R49UevjGxIPMhIA==", + "uChFnF0oCwARhAOz/d47eA==", + "0egBaMnAf0CQEXf1pCIKnA==", + "FnVNxl5AFH1AieYru2ZG+A==", + "2Ct+pLXrK6Ku1f4qehjurQ==", + "x2nSgcTjA3oGgI8mMgiqjw==", + "AUGmvZkpkKBry5bHZn4DJA==", + "x8kRVzohTdhkryvYeMvkMw==", + "rXfWkabSPN+23Ei1bdxfmQ==", + "ElTNyMR4Rg8ApKrPw88WPg==", + "9jxA/t3TQx8dQ+FBsn/YCg==", + "I07W2eDQwe6DVsm1zHKM8A==", + "0p1jMr06OyBoXQuSLYN4aQ==", + "odGhKtO4bDW5R8SYiI5yCg==", + "5Q/Y2V0iSVTK8HE8JerEig==", + "Ily2MKoFI1zr5LxBy93EmQ==", + "8dUcSkd2qnX5lD9B+fUe+Q==", + "80UE+Ivby3nwplO/HA7cPw==", + "sS6QcitMPdvUBLiMXkWQkw==", + "5VY++KiWgo7jXSdFJsPN3A==", + "aY6B28XdPnuYnbOy9uSP8A==", + "ZfRlID+pC1Rr4IY14jolMw==", + "/YuQw7oAF08KDptxJEBS9g==", + "16d+fhFlgayu3ttKVV/pbg==", + "8dBIsHMEAk7aoArLZKDZtg==", + "wRqaDZVHHurp5whOQ1kDbQ==", + "lFUq6PGk9dBRtUuiEW7Cug==", + "FoJZ61VrU8i084pAuoWhDQ==", + "4mig4AMLUw+T/ect9p4CfA==", + "Po0lhBfiMaXhl+vYh1D8gA==", + "z9cd+Qj+ueX34Zf3997MNQ==", + "1dsKN1nG6upj7kKTKuJWsQ==", + "UtLYUlQJ02oKcjNR3l+ktg==", + "O538ibsrI4gkE5tfwjxjmg==", + "G736AX070whraDxChqUrqw==", + "THs1r8ZEPChSGrrhrNTlsA==", + "pVG1hL96/+hQ+58rJJy6/A==", + "1BjsijOzgHt/0i36ZGffoQ==", + "6rIWazDEWU5WPZHLkqznuQ==", + "cdWUm6uLNzR/knuj2x75eA==", + "nsnX3tKkN1elr18E31tXDw==", + "0fnruVOCxEczscBuv4yL9A==", + "SVuEYfQ9FGyVMo1672n0Yg==", + "ZRWyfXyXqAaOEjkzWl949Q==", + "S2MAIYeDQeJ1pl9vhtYtUg==", + "vsRNZx4thFFFPneubKq1Fw==", + "kuWGANwzNRpG4XmY7KjjNg==", + "i6r+mZfyhZyqlYv56o0H+w==", + "wqWqe0KRjZlUIrGgEOG9Mg==", + "t5wh9JGSkQO78QoQoEqvXA==", + "AGoVLd0QPcXnTedT5T95JQ==", + "aRrcmH+Ud3mF1vEXcpEm4w==", + "C65PZm8rZxJ6tTEb6d08Eg==", + "oAHVGBSJ2cf4dVnb/KEYmw==", + "BuDVDLl0OGdomEcr+73XhQ==", + "bLsStF0DDebpO+xulqGNtg==", + "xukOAM0QVsA72qEy0yku9A==", + "LpoayYsTO8WLFLCSh2kf2w==", + "LEVYAE54618FrlXkDN01Kw==", + "Jm862vBTCYbv/V4T1t46+Q==", + "X4kdXUuhcUqMSduqhfLpxA==", + "cLR0Ry4/N5swqga1R6QDMw==", + "0klouNfZRHFFpdHi4ZR2hA==", + "JGx8sTyvr4bLREIhSqpFkw==", + "ZiJ/kJ9GneF3TIEm08lfvQ==", + "hP7dSa8lLn9KTE/Z0s4GVQ==", + "600bwlyhcy754W1E6tuyYg==", + "U49SfOBeqQV9wzsNkboi8Q==", + "5DDb7fFJQEb3XTc3YyOTjg==", + "6uT7LZiWjLnnqnnSEW4e/Q==", + "tq5xUJt8GtjDIh1b48SthQ==", + "eJFIQh/TR7JriMzYiTw4Sg==", + "jdRzkUJrWxrqoyNH9paHfQ==", + "RKVDdE1AkILTFndYWi9wFg==", + "AEpTVUQhIEJGlXJB6rS26A==", + "PD+yHtJxZJ2XEvjIPIJHsQ==", + "dOS+mVCy3rFX9FvpkTxGXA==", + "lz+SeifYXxamOLs1FsFmSQ==", + "QTz21WkhpPjfK8YoBrpo+w==", + "9wUIeSgNN36SFxy8v2unVg==", + "ash1r2J6B0PUxJe8P0otVQ==", + "y7yS9x3yshVhMpDbQtfYOQ==", + "f07bdNVAe9x+cAMdF1bByQ==", + "N2KovXW14hN/6+iWa1Yv3g==", + "2DNbXVgesUa7PgYQ4zX5Lw==", + "WQznrwqvMhUlM3CzmbhAOQ==", + "FpWDTLTDmkUhH/Sgo+g1Gg==", + "OVHqwV8oQMC5KSMzd5VemA==", + "Bv4mNIC72KppYw/nHQxfpQ==", + "MI+HSMRh8KTW+Afiaxd/Fw==", + "10OltdxPXOvfatJuwPVKbQ==", + "y4/HohCJxtt+cT7nLJB08w==", + "RhcqXY4OsZlVVF7ZlkTeRw==", + "/mrqas0eDX+sFUNJvCQY8g==", + "ZIZx4MehWTVXPN9cVQBmyA==", + "z20AAnvj7WsfJeOu3vemlA==", + "dL6n/JsK+Iq6UTbQuo/GOw==", + "rMm9bHK69h0fcMkMdGgeeA==", + "ftsf2qztw3NC78ep/CZXWQ==", + "/n1RLTTVpygre1dl36PDwQ==", + "/FsJYFNe+7UvsSkiotNJEQ==", + "Yy2pPhITTmkEwoudXizHqQ==", + "lizovLQxu6L9sbafNQuShQ==", + "XV5MYe0Q7YMtoBD6/iMdSw==", + "5jHgQF4SfO/zy9xy9t+9dw==", + "16iT/jCcPDrJEfi2bE5F+Q==", + "syeBfQBUmkXNWCZ1GV8xSA==", + "sr3UXbMg5zzkRduFx/as7g==", + "xUXEE7OBBCudsQnuj5ycOA==", + "ojZY7Gi2QJXE/fp6Wy31iA==", + "RlNPyhgYOIn28R4vKCVtYA==", + "KOm8PTa+ICgDrgK9QxCJZw==", + "DJoy1NSZZw87oxWGlNHhfg==", + "jEdanvXKyZdZJG6mj/3FWw==", + "Omr+zPWVucPCSfkgOzLmSQ==", + "71w3aSvuh2mBLtdqJCN3wA==", + "xjTMO2mvtpvwQrounD4e8g==", + "Zz/5VMbw1TqwazReplvsEg==", + "hIjgi20+km+Ks23NJ4VQ6Q==", + "00TVKawojyqrJkC7YqT41Q==", + "YgVpC5d5V6K/BpOD663yQA==", + "wX70jKLKJApHnhyK0r6t3A==", + "lacCCRiWdquNm4YRO7FoKA==", + "cWdlhVZD7NWHUGte24tMjg==", + "t5U+VMsTtlWAAWSW+00SfQ==", + "AMfL0rH+g8c0VqOUSgNzQw==", + "0G93AxGPVwmr66ZOleM90A==", + "9tiibT8V9VwnPOErWGNT3w==", + "+dBv88reDrjEz6a2xX3Hzw==", + "xX6atcCApI08oVLjjLteLg==", + "+YrqTEJlJCv0A2RHQ8tr1A==", + "aqcOby9QyEbizPsgO3g0yw==", + "s/BZAhh1cTV3JCDUQsV8mA==", + "x9VwDdFPp/rJ+SF16ooWYg==", + "k/OVIllJvW6BefaLEPq7DA==", + "rIMXaCaozDvrdpvpWvyZOQ==", + "qQQwJ/aF87BbnLu3okXxaw==", + "TIWSM78m0RprwgPGK/e0JA==", + "r/b5px/UImGNjT/X5sYjuA==", + "7K8l6KoP0BH82/WMLntfrg==", + "gEHGeR2F82OgBeAlnYhRSw==", + "1/SGIab+NnizimUmNDC4wA==", + "WADmxH7R6B4LR+W6HqQQ6A==", + "pcoBh5ic7baSD4TZWb3BSw==", + "es/L9iW8wsyLeC5S4Q8t+g==", + "D175i+2bZ7aWa4quSSkQpA==", + "WQMffxULFKJ+bun6NrCURA==", + "82hTTe1Nr4N2g7zwgGjxkw==", + "oyYtf08AkWLR52bXm5+sKw==", + "8uP4HUnSodw88yoiWXOIcw==", + "x2NpqNnqRihktNzpxmepkQ==", + "x5zMDuW66467ofgL3spLUQ==", + "OMO4pqzfcbQ11YO4nkTXfg==", + "N4/mQFyhDpPzmihjFJJn6w==", + "NN/ymVQNa17JOTGr6ki3eQ==", + "htDbVu1xGhCRd8qoMlBoMg==", + "S47hklz3Ow+n5aY6+qsCoA==", + "ji+1YHlRvzevs3q5Uw1gfA==", + "3Y4w0nETru3SiSVUMcWXqw==", + "XfBOCJwi2dezYzLe316ivw==", + "kMUdiwM7WR8KGOucLK4Brw==", + "V/xG5QFyx1pihimKmAo8ZA==", + "sQskMBELEq86o1SJGQqfzg==", + "6+jhreeBLfw64tJ+Nhyipw==", + "8iYdEleTXGM+Wc85/7vU9w==", + "D7piVoB2NJlBxK5owyo4+g==", + "hDGa2yLwNvgBd/v6mxmQaQ==", + "WLsh3UF4WXdHwgnbKEwRlQ==", + "D5jaV+HtXkSpSxJPmaBDXg==", + "jCgdKXsBCgf7giUKnr6paQ==", + "XqW7UBTobbV4lt1yfh0LZw==", + "EbGG4X18upaiVQmPfwKytg==", + "dXDPnL1ggEoBqR13aaW9HA==", + "Vik8tGNxO0xfdV0pFmmFDw==", + "Swjn3YkWgj0uxbZ1Idtk+A==", + "JPxEncA4IkfBDvpjHsQzig==", + "F5FcNti7lUa9DyF2iEpBug==", + "HJYgUxFZ66fRT8Ka73RaUg==", + "Jbxl8Nw1vlHO9rtu0q/Fpg==", + "fmC+85h5WBuk8fDEUWPjtQ==", + "dZgMquvZmfLqP4EcFaWCiA==", + "XF/yncdoT4ruPeXCxEhl9Q==", + "QJEbr3+42P9yiAfrekKdRQ==", + "Sr9c0ReRpkDYGAiqSy683g==", + "Nr4zGo5VUrjXbI8Lr4YVWQ==", + "NDZWIhhixq7NT8baJUR4VQ==", + "GFRJoPcXlkKSvJRuBOAYHQ==", + "WHutPin+uUEqtrA7L8878A==", + "2rhjiY0O0Lo36wTHjmlNyw==", + "XsF7R12agx/KkRWl0TyXRA==", + "R6cO8GzYfOGTIi773jtkXw==", + "zrZWcqQsUE3ocWE0fG+SOA==", + "uNzpptKjihEfKRo5A1nWmw==", + "gICaI06E9scnisonpvqCsA==", + "TA9WjiLAFgJubLN4StPwLw==", + "sBpytpE38xz0zYeT+0qc2A==", + "Ej7W3+67kCIng3yulXGpRQ==", + "nR3ACzeVF5YcLX6Gj6AGyQ==", + "b0vZfEyuTja2JYMa20Rtbg==", + "f1h+Vp+xmdZsZIziHrB2+g==", + "WzjvUJ4jZAEK7sBqw+m07A==", + "OzMR5D2LriC5yrVd5hchnA==", + "cw1gBLtxH/m4H7dSM7yvFg==", + "CZbd+UoTz0Qu1kkCS3k8Xg==", + "WtT0QAERZSiIt2SFDiAizg==", + "QsquNcCZL9wv7oZFqm64vQ==", + "FXzaxi3nAXBc8WZfFElQeA==", + "Ml3mi1lGS1IspHp3dYYClg==", + "XGAXhUFjORwKmAq9gGEcRg==", + "wOhbpTzmFla8R0kI9OiHaA==", + "qoK2keBg3hdbn7Q24kkVXg==", + "ZAQHWU6RMg4IadOxuaukyw==", + "RiahBXX2JbPzt8baPiP/8g==", + "Qx6rVv9Xj8CBjqikWI9KFA==", + "ZRnR6i+5WKMRfs3BDRBCJg==", + "91LQuW6bMSxl10J/UDX23A==", + "0dIeIM5Zvm5nSVWLy94LWg==", + "Ja3ECL7ClwDrWMTdcSQ6Ug==", + "f6iLrMpxKhFxIlfRsFAuew==", + "iSeH0JFSGK73F470Rhtesw==", + "DwOTyyCoUfaSShHZx9u6xg==", + "rdeftHE7gwAT67wwhCmkYQ==", + "kUhyc3G8Zvx8+q5q5nVEhw==", + "W8bATujVUT80v2XGJTKXDg==", + "dMRx4Mf6LrN64tiJuyWmDw==", + "9cvHJmim9e0pOaoUEtiM6A==", + "RHToSGASrwEmvzjX6VPvNQ==", + "V7eji28JSg3vTi30BCS7gw==", + "4+htiqjEz9oq0YcI/ErBVg==", + "jKJn4czwUl/6wtZklcMsSg==", + "bvyB6OEwhwCIfJ6KRhjnRw==", + "59ipbMH7cKBsF9bNf4PLeQ==", + "M/cQja3uIk1im9++brbBOA==", + "AChOz8avRYsvxlbWcorQ3w==", + "FcKjlHKfQAGoovtpf+DxWQ==", + "y+cl1/Knb9MZPz8nBB0M+w==", + "b8BZV1NfBdLi70ir4vYvZg==", + "aFJuE/s+Kbge4ppn+wulkA==", + "CWBGcRFYwZ0va6115vV/oQ==", + "glnqaRfwm6NxivtB2nySzw==", + "mPk1IsU5DmDFA/Ym5+1ojw==", + "LGwcvetzQ3QqKjNh5vA8vw==", + "yctId8ltkl3+xqi9bj+RqA==", + "spJI3xFUlpCDqzg0XCxopA==", + "V8m51xgUgywRoV6BGKUrgg==", + "rgcXxjx3pDLotH7TTfAoZw==", + "/TSsi/AwKHtP6kQaeReI3w==", + "8dbyfox/isKLsnVjQNsEXg==", + "MOrAbuJTyGKPC6MgYJlx5Q==", + "uNWFZlP7DA96sf+LWiAhtQ==", + "hNHqznsrIVRSQdII6crkww==", + "GT6WUDXiheKAM7tPg3he9A==", + "JC8Q+8yOJ52NvtVeyHo68w==", + "HMQarkPWOUDIg5+5ja2dBQ==", + "nknBKPgb7US42v8A0fTl/w==", + "fDOUzPTU2ndpbH0vgkgrJQ==", + "GTNttXfMniNhrbhn92Aykg==", + "D2JcY4zWwqaCKebLM8lPiQ==", + "/c34NtdUZAHWIwGl3JM8Tw==", + "/G26n5Xoviqldr5sg/Jl3w==", + "GF0lY77rx1NQzAsZpFtXIQ==", + "BMOi5JmFUg5sCkbTTffXHw==", + "R+beucURp/H5jLs4kW6wmg==", + "xfYZ6qhWNBqqJ0PdWRjOwA==", + "Ahpi9+nl13kPTdzL+jgqMw==", + "oIU19xAvLJwQSZzIH577aA==", + "50xwiYvGQytEDyVgeeOnMg==", + "M0ESOGwJ4WZ4Ons1ljP0bQ==", + "fS471/rN4K2m10mUwGFuLg==", + "RrE3B3X/SJi3CqCUlTYwaw==", + "oDca3JEdRb4vONT9GUUsaQ==", + "pHo1O5zrCHCiLvopP2xaWw==", + "7sCJ4RxbxRqVnF4MBoKfuQ==", + "7R5rFaXCxM3moIUtoCfM2g==", + "4rrSL6N0wyucuxeRELfAmw==", + "9Gkw+hvsR/tFY1cO89topg==", + "aw4CzX8pYbPVMuNrGCEcWg==", + "KyLQxi5UP+qOiyZl0PoHNQ==", + "T1pMWdoNDpIsHF8nKuOn2A==", + "Qv6wWP4PpycDGxe7EZNSCw==", + "ZJc7GV0Yb6MrXkpDVIuc8g==", + "aXrbsro7KLV8s4I4NMi4Eg==", + "7k5rBuh8FbTTI4TP87wBPQ==", + "NRyFx6jqO/oo9ojvbYzsAg==", + "P7eMlOz9YUcJO+pJy0Kpkw==", + "jpjpNjL1IKzJdGqWujhxCw==", + "9k1u/5TgPmXrsx3/NsYUhg==", + "c1wbFbN7AdUERO/xVPJlgw==", + "Yw4ztKv6yqxK9U1L0noFXg==", + "GnJKlRzmgKN9vWyGfMq3aA==", + "91VcAVv7YDzkC1XtluPigw==", + "h1NNwMy0RjQmLloSw1hvdg==", + "pzC8Y0Vj9MPBy3YXR32z6w==", + "UTmTgvl+vGiCDQpLXyVgOg==", + "CzWhuxwYbNB/Ffj/uSCtbw==", + "VOB+9Bcfu8aHKGdNO0iMRw==", + "X2Tawm2Cra6H7WtXi1Z4Qw==", + "6cTETZ9iebhWl+4W5CB+YQ==", + "X4hrgqMIcApsjA9qOWBoCw==", + "1buQEv2YlH/ljTgH0uJEtw==", + "FH5Z60RXXUiDk+dSZBxD3g==", + "FI2WhaSMb3guFLe3e9il8Q==", + "O/EizzJSuFY8MpusBRn7Tg==", + "b6rrRA0W247O+FfvDHbVCQ==", + "ng1Q0A7ljho3TUWWYl46sw==", + "1Ym0lyBJ9aFjhJb/GdUPvQ==", + "+OXdvbTxHtSoLg7bZMho4w==", + "cuQslgfqD2VOMhAdnApHrA==", + "pCQmlnn3BxhsV2GwqjRhXg==", + "6PzjncEw2wHZg7SP7SQk9w==", + "nqtQI1bSM7DCO9P1jGV97Q==", + "O1ckWUwuhD44MswpaD6/rw==", + "RUmhye56tQu9xXs4SRJpOQ==", + "llujnWE17U8MIHmx4SbrSA==", + "UwqBVd4Wfias4ElOjk2BzQ==", + "kBAB2PSjXwqoQOXNrv80AA==", + "w1zN28mSrI/gqHsgs4ME3A==", + "301utVPZ93AnPLYbsiJggw==", + "qIFpKKwUmztsBpJgMaVvSg==", + "QmcURiMzmVeUNaYPSOtTTg==", + "x/MpsQvziUpW40nNUHDS5Q==", + "t1O9jSNjg4DTIv/Za4NbtA==", + "1B5gxGQSGzVKoNd5Ol4N7g==", + "81iQLU+YwxNwq4of6e9z7A==", + "x0eIHCvQLd2jdDaXwSWTYQ==", + "96ORaz1JRHY1Gk8H74+C2g==", + "bNDKcFu8T5Y6OoLSV+o/Sw==", + "WrJMOuXSLKKzgmIDALkyNw==", + "+gpHnUj2GWocP74t5XWz4w==", + "z5DveTu377UW8IHnsiUGZg==", + "irnD9K8bsT+up/JUrxPw6A==", + "ginkFyNVMwkZLE49AbfqfA==", + "2hEzujfG3mR5uQJXbvOPTQ==", + "E9yeifEZtpqlD0N3pomnGw==", + "OpC/sL320wl5anx6AVEL+A==", + "D7wN7b5u5PKkMaLJBP9Ksw==", + "83WGpQGWyt6mCV+emaomog==", + "X6ulLp4noBgefQTsbuIbYQ==", + "BH+rkZWQjTp7au6vtll/CQ==", + "Ex3x5HeDPhgO2S9jjCFy4g==", + "YNqIHCmBp/EbCgaPKJ7phw==", + "312g8iTB9oJgk/OqcgR7Cw==", + "LcF0OqPWrcpHby8RwXz1Yg==", + "gaEtlJtD6ZjF5Ftx0IFt0A==", + "bvbMJZMHScwjJALxEyGIyg==", + "StoXC7TBzyRViPzytAlzyQ==", + "XqFSbgvgZn0CpaZoZiRauQ==", + "AqHVaj3JcR44hnMzUPvVYg==", + "jTg9Y6EfpON4CRFOq0QovA==", + "q/siBRjx6wNu+OTvpFKDwA==", + "goSgZ8N5UbT5NMnW3PjIlQ==", + "9onh6QKp70glZk9cX3s34A==", + "o5XVEpdP4OXH0NEO4Yfc/A==", + "a5gZ5uuRrXEAjgaoh7PXAg==", + "PaROi5U16Tk35p0EKX5JpA==", + "dtnE401dC0zRWU0S/QOTAg==", + "7J3FoFGuTIW36q0PZkgBiw==", + "hiYg+aVzdBUDCG0CXz9kCw==", + "vhdFtKVH4bVatb4n8KzeXw==", + "DWKsPfKDAtfuwgmc2dKUNg==", + "M2JMnViESVHTZaru6LDM6w==", + "G/PA+kt0N+jXDVKjR/054A==", + "6rqK8sjLPJUIp7ohkEwfZg==", + "wajwXfWz2J+O+NVaj6j2UQ==", + "C4QEzQKGxyRi2rjwioHttA==", + "N/HgDydvaXuJvTCBhG/KtA==", + "6erpZS36qZRXeZ9RN9L+kw==", + "bbBsi6tXMVWyq3SDVTIXUg==", + "aySnrShOW4/xRSzl/dtSKQ==", + "rxfACPLtKXbYua18l3WlUw==", + "L4+C6I7ausPl6JbIbmozAg==", + "R3ijnutzvK6IKV3AKHQZSA==", + "leDlMcM+B1mDE8k5SWtUeg==", + "KGI/cXVz6v6CfL8H6akcUQ==", + "NtwqUO3SKZE/9MXLbTJo/g==", + "dJHKDkfMFJeoULg7U4wwDQ==", + "IEz72W2/W8xBx5aCobUFOQ==", + "wUYhs4j3W9nIywu1HIv2JA==", + "GzbeM7snhe+M+J7X+gAsQw==", + "3/1puZTGSrD9qNKPGaUZww==", + "eKQCVzLuzoCLcB4im8147A==", + "CCK+6Dr72G3WlNCzV7nmqw==", + "CJoZn5wdTXbhrWO5LkiW0g==", + "bJ1cZW7KsXmoLw0BcoppJg==", + "OlpA9HsF8MBh7b45WZSSlg==", + "JZRjdJLgZ+S0ieWVDj8IJg==", + "uhT12XY79CtbwhcSfAmAXQ==", + "isep9d+Q7DEUf0W7CJJYzw==", + "K9A87aMlJC8XB9LuFM913g==", + "uqe3rFveJ2JIkcZQ3ZMXHQ==", + "0e8hM3E5tnABRyy29A8yFw==", + "4iiCq+HhC+hPMldNQMt0NA==", + "X4o0OkTz0ec70mzgwRfltA==", + "1E3pMgAHOnHx3ALdNoHr8Q==", + "xNilc7UOu1kyP0+nK5MrLw==", + "DQlZWBgdTCoYB1tJrNS5YQ==", + "iruDC5MeywV4yA8o1tw/KQ==", + "z+1oDVy8GJ5u/UDF+bIQdA==", + "uExgqZkkJnZj252l5dKAGg==", + "ZgdpqFrVGiaHkh9o3rDszg==", + "5N2oi2pB69NxeNt08yPLhw==", + "G37U8XTFyshfCs7qzFxATg==", + "0ZEC3hy411LkOhKblvTcqg==", + "ITZ3P47ALS0JguFms6/cDA==", + "WWN44lbUnEdHmxSfMCZc6w==", + "r2f2MyT+ww1g9uEBzdYI1w==", + "ZvvxwDd0I6MsYd7aobjLUA==", + "uQs79rbD/wEakMUxqMI48A==", + "022B0oiRMx8Xb4Af98mTvQ==", + "afMd/Hr3rYz/l7a3CfdDjg==", + "xmsYnsJq78/f9xuKuQ2pBQ==", + "dFetwmFw+D6bPMAZodUMZQ==", + "TBQpcKq2huNC5OmI2wzRQw==", + "skrQRB9xbOsiSA19YgAdIQ==", + "anyANMnNkUqr3JuPJz5Qzw==", + "6QUGE2S8oFYx4T4nW56cCw==", + "rwtF86ZAbWyKI6kLn4+KBw==", + "6txm8z4/LGCH0cpaet/Hsg==", + "wdRyYjaM11VmqkkxV/5bsA==", + "+k5lDb+QdNc9iZ01hL5yBg==", + "k/pBSWE2BvUsvJhA9Zl5uw==", + "jQjyjWCEo9nWFjP4O8lehw==", + "R6Me6sSGP5xpNI8R0xGOWw==", + "9+hjTVMQUsvVKs7Tmp52tg==", + "VQIpquUqmeyt/q6OgxzduQ==", + "KXvdjZ3rRKn60djPTCENGA==", + "5HovoyHtul8lXh+z8ywq9A==", + "1+XWdu4qCqLLVjqkKz3nmA==", + "LCj4hI520tA685Sscq6uLw==", + "b53qqLnrTBthRXmmnuXWvw==", + "WTr3q/gDkmB4Zyj7Ly20+w==", + "FbxScyuRacAQkdQ034ShTA==", + "qaTdVEeZ6S8NMOxfm+wOMA==", + "ZNrjP1fLdQpGykFXoLBNPw==", + "/Bwpt5fllzDHq2Ul6v86fA==", + "/mFp3GFkGNLhx2CiDvJv4A==", + "RppDe/WGt1Ed6Vqg1+cCkQ==", + "6M6QapJ5xtMXfiD3bMaiLA==", + "Ghuj9hAyfehmYgebBktfgA==", + "GncGQgmWpI/fZyb/6zaFCg==", + "R1TCCfgltnXBvt5AiUnCtQ==", + "5NEP7Xt7ynj6xCzWzt21hQ==", + "4yEkKp2FYZ09mAhw2IcrrA==", + "y2Tn2gmhKs5WKc01ce74rg==", + "wnfYUctNK+UPwefX5y4/Rw==", + "BV1moliPL15M14xkL+H1zw==", + "80C9TB9/XT1gGFfQDJxRoA==", + "yL1DwlIIREPuyuCFULi0uw==", + "D09afzGpwCEH0EgZUSmIZA==", + "eCy/T+a8kXggn1L8SQwgvA==", + "+dIEf5FBrHpkjmwUmGS6eg==", + "kzXsrxWRnWhkA82LsLRYog==", + "Nf9fbRHm844KZ2sqUjNgkA==", + "XAq/C+XyR6m3uzzLlMWO5Q==", + "jiV+b/1EFMnHG6J0hHpzBg==", + "HK0yf7F97bkf1VYCrEFoWA==", + "Cz1G77hsDtAjpe0WzEgQog==", + "xdCCdP8SNBOK3IsX6PiPQA==", + "8snljTGo/uICl9q0Hxy7/A==", + "sLdxIKap0ZfC3GpUk3gjog==", + "IA1jmtfpYkz/E2wD0+27WA==", + "PPa7BDMpRdxJdBxkuWCxKA==", + "CuGIxWhRLN7AalafBZLCKQ==", + "MWcV03ULc0vSt/pFPYPvFA==", + "QVwuN66yPajcjiRnVk/V8g==", + "aLY2pCT0WfFO5EJyinLpPg==", + "dGrf9SWJ13+eWS6BtmKCNw==", + "YtZ8CYfnIpMd2FFA5fJ+1Q==", + "Umd+5fTcxa3mzRFDL9Z8Ww==", + "Al8+d/dlOA5BXsUc5GL8Tg==", + "/KYZdUWrkfxSsIrp46xxow==", + "kr8tw1+3NxoPExnAtTmfxg==", + "PwvPBc+4L73xK22S9kTrdA==", + "VWNDBOtjiiI4uVNntOlu/A==", + "lJFPmPWcDzDp5B2S8Ad8AA==", + "Mofqu40zMRrlcGRLS42eBw==", + "BuENxPg7JNrWXcCxBltOPg==", + "nmD7fEU4u7/4+W/pkC4/0Q==", + "axEl7xXt/bwlvxKhI7hx4g==", + "W04GeDh+Tk/I1S85KlozRA==", + "tVw8U1AsslIFmQs4H1xshg==", + "TSPFvkgw6uLsJh66Ou0H9w==", + "IYIbEaErHoFBn8sTT9ICIQ==", + "WBu0gJmmjVdVbjDmQOkU6w==", + "ZgjifTVKmxOieco81gnccQ==", + "ZrCnZB/U/vcqEtI1cSvnww==", + "2D6yhuABiaFFoXz0Lh0C+w==", + "SfwnYZCKP1iUJyU1yq4eKg==", + "tsiqwelcBAMU/HpLGBtMGw==", + "S9L29U2P5K8wNW+sWbiH7w==", + "sGLPmr568+SalaQr8SE/PA==", + "Hm6MG6BXbAGURVJKWRM6ZA==", + "euxzbIq4vfGYoY3s1QmLcw==", + "/FchS2nPezycB8Bcqc2dbg==", + "ZKvox7BaQg4/p5jIX69Umw==", + "HkbdaMuDTPBDnt3wAn5RpQ==", + "eddhS+FkXxiUnbPoCd5JJw==", + "Muf2Eafcf9G3U2ZvQ9OgtQ==", + "a7Pv1SOWYnkhIUC22dhdDA==", + "O839JUrR+JS30/nOp428QA==", + "2qK2ZEY9LgdKSTaLf6VnLA==", + "BTiGLT6XdZIpFBc91IJY6g==", + "EqYq2aVOrdX5r7hBqUJP7g==", + "SIuKH/Qediq0TyvqUF93HQ==", + "c5ymZKqx/td1MiS2ERiz9A==", + "rqucO37p86LpzehR/asCSQ==", + "1tpM0qgdo7JDFwvT0TD78g==", + "Ar1Eb/f/LtuIjXnnVPYQlA==", + "V8q+xz4ljszLZMrOMOngug==", + "P5WPQc5NOaK7WQiRtFabkw==", + "Xo8ZjXOIoXlBjFCGdlPuZw==", + "jTmPbq+wh30+yJ/dRXk1cA==", + "KSumhnbKxMXQDkZIpDSWmQ==", + "Kh/J1NpDBGoyDU+Mrnnxkg==", + "3BjLFon1Il0SsjxHE2A1LQ==", + "dml2gqLPsKpbIZ93zTXwCQ==", + "ZyoaR1cMiKAsElmYZqKjLA==", + "vnOJ3e9Zd4wPx8PX7QgZzQ==", + "2melaInV0wnhBpiI3da6/A==", + "mUek9NkXm8HiVhQ6YXiyzA==", + "RZTpYKxOAH9JgF1QFGN+hw==", + "a/Y6IAVFv0ykRs9WD+ming==", + "yhRi5M9Etuu9HSu4d24i3w==", + "+1gcqAqaRZwCj5BGiZp3CA==", + "o1zeXHJEKevURAAbUE/Vog==", + "cvOg7N4DmTM+ok1NBLyBiQ==", + "uPdjKJIGzN7pbGZDZdCGaA==", + "REnDNe9mGfqVGZt+GdsmjQ==", + "XqTK/2QuGWj50tGmiDxysA==", + "bL2FuwsPT7a7oserJQnPcw==", + "uO+uK1DntCxVRr1KttfUIw==", + "Xconi1dtldH90Wou9swggw==", + "HRF3WL/ue3/QlYyu7NUTrA==", + "5LuFDNKzMd2BzpWEIYO2Ww==", + "dNTU+/2DdZyGGTdc+3KMhQ==", + "H+NHjk/GJDh/GaNzMQSzjg==", + "/Ph/6l/lFNVqxAje1+PgFA==", + "4WRdAjiUmOQg2MahsunjAg==", + "j+lDhAnWAyso+1N8cm85hQ==", + "nFBXCPeiwxK9mLXPScXzTA==", + "vGKknndb4j6VTV8DxeT4fQ==", + "fdqt93OrpG13KAJ5cASvkg==", + "1MIn73MLroxXirrb+vyg2Q==", + "Q7teXmTHAC5qBy+t7ugf0w==", + "bWwtTFlhO3xEh/pdw0uWaQ==", + "Omi2ZB9kdR1HrVP2nueQkA==", + "+ZozWaPWw8ws1cE5DJACeg==", + "3FH4D31nKV13sC9RpRZFIg==", + "4kXlJNuT79XXf1HuuFOlHw==", + "36XDmX6j542q+Oei1/x0gw==", + "MqqDg9Iyt4k3vYVW5F+LDw==", + "cvrGmub2LoJ+FaM5HTPt9A==", + "uC2lzm7HaMAoczJO6Z/IhQ==", + "MnStiFQAr3QlaRZ02SYGaQ==", + "ZuayB6IpbeITokKGVi9R5w==", + "FtxpWdhEmC6MT61qQv4DGA==", + "KujFdhhgB9q4oJfjYMSsLg==", + "ZV8mEgJweIYk0/l0BFKetA==", + "gDLjxT7vm07arF4SRX5/Vg==", + "/MEOgAhwb7F0nBnV4tIRZA==", + "k2KP9oPMnHmFlZO6u6tgyw==", + "fbTm027Ms0/tEzbGnKZMDA==", + "HOi+vsGAae4vhr+lJ5ATnQ==", + "9Bet5waJF5/ZvsYaHUVEjQ==", + "Wd0dOs7eIMqW5wnILTQBtg==", + "z/e5M2lE9qh3bzB97jZCKA==", + "b16O4LF7sVqB7aLU2f3F1A==", + "lsBTMnse2BgPS6wvPbe7JA==", + "0nOg18ZJ/NicqVUz5Jr0Hg==", + "MFeXfNZy6Q9wBfZmPQy3xg==", + "ksOFI9C7IrDNk4OP6SpPgw==", + "NquRbPn8fFQhBrUCQeRRoQ==", + "ccmy4GVuX967KaQyycmO0w==", + "DY0IolKTYlW+jbKLPAlYjQ==", + "aJFbBhYtMbTyMFBFIz/dTA==", + "9pdeedz1UZUlv8jPfPeZ1g==", + "qZ2q5j2gH3O56xqxkNhlIA==", + "N7fHwb397tuQHtBz1P80ZQ==", + "uOkMpYy/7DYYoethJdixfQ==", + "E9ajQQMe02gyUiW3YLjO/A==", + "dFSavcNwGd8OaLUdWq3sng==", + "TAD0Lk95CD86vbwrcRogaQ==", + "jLI3XpVfjJ6IzrwOc4g9Pw==", + "CzP13PM/mNpJcJg8JD3s6w==", + "GSWncBq4nwomZCBoxCULww==", + "9k17UqdR1HzlF7OBAjpREA==", + "TrWS+reCJ0vbrDNT5HDR9w==", + "CXMKIdGvm60bgfsNc+Imvg==", + "6NP81geiL14BeQW6TpLnUA==", + "hW9DJA1YCxHmVUAF7rhSmQ==", + "8M0kSvjn5KN8bjsMdUqKZQ==", + "eS/vTdSlMUnpmnl1PbHjyw==", + "h2B0ty0GobQhDnFqmKOpKQ==", + "n7KL1Kv027TSxBVwzt9qeA==", + "yYmnM/WOgi+48Rw7foGyXA==", + "FhthAO5IkMyW4dFwpFS7RA==", + "81ZH3SO0NrOO+xoR/Ngw1g==", + "t7HaNlXL16fVwjgSXmeOAQ==", + "N+K1ibXAOyMWdfYctNDSZQ==", + "yQCLV9IoPyXEOaj3IdFMWw==", + "3+zsjCi7TnJhti//YXK35w==", + "600mjiWke4u0CDaSQKLOOg==", + "K4VS+DDkTdBblG93l2eNkA==", + "5KOgetfZR+O2wHQSKt41BQ==", + "kj5WqpRCjWAfjM7ULMcuPQ==", + "AxEjImKz4tMFieSo7m60Sg==", + "jp5Em/0Ml4Txr1ptTUQjpg==", + "jQVlDU+HjZ2OHSDBidxX5A==", + "4NHQwbb3zWq2klqbT/pG6g==", + "PeJS+mXnAA6jQ0WxybRQ8w==", + "l6Ssc04/CnsqUua9ELu2iQ==", + "nFPDZGZowr3XXLmDVpo7hg==", + "yYBIS9PZbKo7Gram7IXWPA==", + "/HU2+fBqfWTEuqINc0UZSA==", + "adT+OjEB2kqpeYi4kQ6FPg==", + "GW1Uaq622QamiiF24QUA0g==", + "rTwJggSxTbwIYdp07ly0LA==", + "4yrFNgqWq17zVCyffULocA==", + "vvh9vAIrXjIwLVkuJb5oDQ==", + "C7UaoIEXsVRxjeA0u99Qmw==", + "x1A74vg/hwwjAx6GrkU8zw==", + "7XRiYvytcwscemlxd9iXIQ==", + "64AA4jLHXc1Dp15aMaGVcA==", + "u/QxrP1NOM/bOJlJlsi/jQ==", + "5M3dFrAOemzQ0MAbA8bI5w==", + "wyqmQGB6vgRVrYtmB2vB7w==", + "8vLA9MOdmLTo3Qg+/2GzLA==", + "/u5W2Gab4GgCMIc4KTp2mg==", + "lhAOM81Ej6YZYBu45pQYgg==", + "MArbGuIAGnw4+fw6mZIxaw==", + "ZZImGypBWwYOAW43xDRWCQ==", + "L2IeUnATZHqOPcrnW2APbA==", + "bQKkL+/KUCsAXlwwIH0N3w==", + "f09F7+1LRolRL5nZTcfKGA==", + "hPnPQOhz4QKhZi02KD6C+A==", + "78b8sDBp28zUlYPV5UTnYw==", + "iVDd2Zk7vwmEh97LkOONpQ==", + "LHQETSI5zsejvDaPpsO29g==", + "Yjm5tSq1ejZn3aWqqysNvA==", + "gkrg0NR0iCaL7edq0vtewA==", + "Lo1xTCEWSxVuIGEbBEkVxA==", + "8GyPup4QAiolFJ9v80/Nkw==", + "3L3KEBHhgDwH615w4OvgZA==", + "hJSP7CostefBkJrwVEjKHA==", + "9oQ/SVNJ4Ye9lq8AaguGAQ==", + "n7Bns42aTungqxKkRfQ5OQ==", + "K5lhaAIZkGeP5rH2ebSJFw==", + "ZaPsR9X77SNt7dLjMJUh8A==", + "18ndtDM9UaNfBR1cr3SHdA==", + "0QbH4oI8IjZ9BRcqRyvvDQ==", + "J/eAtAPswMELIj8K2ai+Xg==", + "qenHZKKlTUiEFv6goKM/Mw==", + "vjrSYGUpeKOtJ2cNgLFg2g==", + "DA+3fjr7mgpwf6BZcExj0w==", + "rh7bzsTQ1UZjG7amysr0Gg==", + "tFMJRXfWE9g78O1uBUxeqQ==", + "e/nWuo5YalCAFKsoJmFyFA==", + "gqehq46BhFX2YLknuMv02w==", + "Uudn69Kcv2CGz2FbfJSSEA==", + "Otz/PgYOEZ1CQDW54FWJIQ==", + "IwfeA6d0cT4nDTCCRhK+pA==", + "jgNijyoj2JrQNSlUv4gk4A==", + "KzWdWPP2gH0DoMYV4ndJRg==", + "pv/m2mA/RJiEQu2Qyfv9RA==", + "ATmMzriwGLl+M3ppkfcZNA==", + "tVvWdA+JqH0HR2OlNVRoag==", + "n6QVaozMGniCO0PCwGQZ6w==", + "gU3gu8Y5CYVPqHrZmLYHbQ==", + "cBBOQn7ZjxDku0CUrxq2ng==", + "w+jzM0I5DRzoUiLS/9QIMQ==", + "MLlVniZ08FHAS5xe+ZKRaA==", + "wMyJLQJdmrC2TSeFkIuSvQ==", + "dG98w8MynOoX7aWmkvt+jg==", + "zm+z+OOyHhljV2TjA3U9zw==", + "Tk5MAqd1gyHpkYi8ErlbWg==", + "g6zSo8BvLuKqdmBFM1ejLA==", + "d0VAZLbLcDUgLgIfT1GmVQ==", + "SNPYH4r/J9vpciGN2ybP5Q==", + "XA2hUgq3GVPpxtRYiqnclg==", + "fVCRaPsTCKEVLkoF4y3zEw==", + "FpgdsQ2OG+bVEy3AeuLXFQ==", + "JquDByOmaQEpFb47ZJ4+JA==", + "e369ZIQjxMZJtopA//G55Q==", + "Nsd+DfRX6L54xs+iWeMjCQ==", + "+/UCpAhZhz368iGioEO8aQ==", + "e5l9ZiNWXglpw6nVCtO8JQ==", + "Cl1u5nGyXaoGyDmNdt38Bw==", + "6sNP0rzCCm3w976I2q2s/w==", + "qcpeZWUlPllQYZU6mHVwUw==", + "kzYddqiMsY3EYrpxve2/CQ==", + "3iC21ByW/YVL+pSyppanWw==", + "3HPOzIZxoaQAmWRy9OkoSg==", + "xsCZVhCk2qJmOqvUjK3Y8Q==", + "i2sSvrTh/RdLJX0uKhbrew==", + "7Y87wVJok20UfuwkGbXxLg==", + "ibsb1ncaLZXAYgGkMO7tjQ==", + "+VfRcTBQ80KSeJRdg0cDfw==", + "kgKWQJJQKLUuD2VYKIKvxA==", + "ARKIvf4+zRF8eCvUITWPng==", + "1fztTtQWNMIMSAc5Hr6jMQ==", + "md6zNd7ZBn3qArYqQz7/fw==", + "kvAaIJb+aRAfKK104dxFAA==", + "UIXytIHyVODxlrg+eQoARA==", + "Dk0L/lQizPEb3Qud6VHb1Q==", + "64YsV2qeDxk2Q6WK/h7OqA==", + "90dtIMq0ozJXezT2r79vMQ==", + "wy/Z8505o4sVovk4UuBp1A==", + "ytDXLDBqWiU1w3sTurYmaw==", + "9pk75mBzhmcdT+koHvgDlw==", + "DQeib845UqBMEl96sqsaSg==", + "UPYR575ASaBSZIR3aX1IgQ==", + "swsVVsPi/5aPFBGP+jmPIw==", + "1cj1Fpd3+UiBAOahEhsluA==", + "ifuJCv9ZA84Vz1FYAPsyEA==", + "uu+ncs63SdQIvG6z4r7Q3Q==", + "UvC1WADanMrhT+gPp/yVqA==", + "llOvGOUDVfX68jKnAlvVRA==", + "SusSOsWNoAerAIMBVWHtfA==", + "VznvTPAAwAev+yhl9oZT0w==", + "luR/kvHLwA6tSdLeTM4TzA==", + "PcdBtV8pfKU0YbDpsjPgwg==", + "5l6kDfjtZjkTZPJvNNOVFw==", + "4FBBtWPvqJ3dv4w25tRHiQ==", + "JJbzQ/trOeqQomsKXKwUpQ==", + "0bj069wXgEJbw7dpiPr8Tg==", + "tejpAZp7y32SO2+o4OGvwQ==", + "kq26VyDyJTH/eM6QvS2cMw==", + "+zBkeHF4P8vLzk1iO1Zn3Q==", + "BzkNYH03gF/mQY71RwO3VA==", + "RnxOYPSQdHS6fw4KkDJtrA==", + "65KhGKUBFQubRRIEdh9SwQ==", + "k1DPiH6NkOFXP/r3N12GyA==", + "DqzWt1gfyu/e7RQl5zWnuQ==", + "gnez1VrH+UHT8C/SB9qGdA==", + "vZtL0yWpSIA+9v8i23bZSg==", + "FNvQqYoe0s/SogpAB7Hr1Q==", + "6nwR+e9Qw0qp8qIwH9S/Mg==", + "BPT4PQxeQcsZsUQl33VGmg==", + "rOYeIcB+Rg5V6JG2k4zS2w==", + "Je1UESovkBa9T6wS0hevLw==", + "HFHMGgfOeO0UPrray1G+Zw==", + "NBmB/cQfS+ipERd7j9+oVg==", + "iIm8c9uDotr87Aij+4vnMw==", + "S3VQa6DH+BdlSrxT/g6B5g==", + "BwRA+tMtwEvth28IwpZx+w==", + "vg3jozLXEmAnmJwdfcEN0g==", + "gW0oKhtQQ7BxozxUWw5XvQ==", + "Q6vGRQiNwoyz7bDETGvi5g==", + "Ak3rlzEOds6ykivfg39xmw==", + "G4qzBI1sFP2faN+tlRL/Bw==", + "ND9l4JWcncRaSLATsq0LVw==", + "yQmNZnp/JZywbBiZs3gecA==", + "ZoNSxARrRiKZF5Wvpg7bew==", + "GhpJfRSWZigLg/azTssyVA==", + "QyyiJ5I/OZC50o89fa5EmQ==", + "4kj0S8XlmhHXoUP7dQItUw==", + "Dt8Q5ORzTmpPR2Wdk0k+Aw==", + "/hFhjFGJx2wRfz6hyrIpvA==", + "eFimq+LuHi42byKnBeqnZQ==", + "JrKGKAKdjfAaYeQH8Y2ZRQ==", + "JFFeXsFsMA59iNtZey7LAA==", + "91SdBFJEZ65M+ixGaprY/A==", + "+S+WXgVDSU1oGmCzGwuT3g==", + "1X14kHeKwGmLeYqpe60XEA==", + "4xojeUxTFmMLGm6jiMYh/Q==", + "+1e7jvUo8f2/2l0TFrQqfA==", + "8WU1vLKV1GhrL7oS9PpABg==", + "DYWCPUq/hpjr6puBE7KBHg==", + "birqO8GOwGEI97zYaHyAuw==", + "6e8boFcyc8iF0/tHVje4eQ==", + "FLvED9nB9FEl9LqPn7OOrA==", + "ji306HRiq965zb8EZD2uig==", + "AklOdt9/2//3ylUhWebHRw==", + "VGRCSrgGTkBNb8sve0fYnQ==", + "oqlkgrYe9aCOwHXddxuyag==", + "KXuFON8tMBizNkCC48ICLA==", + "9aKH1u5+4lgYhhLztQ4KWA==", + "3hVslsq98QCDIiO40JNOuA==", + "OOS6wQCJsXH8CsWEidB35A==", + "YXHQ3JI9+oca8pc/jMH6mA==", + "V9vkAanK+Pkc4FGAokJsTA==", + "OFLn4wun6lq484I7f6yEwg==", + "3WVBP9fyAiBPZAq3DpMwOQ==", + "5gGoDPTc/sOIDLngmlEq4A==", + "E2lvMXqHdTw0x+KCKVnblg==", + "f1Gs++Iilgq9GHukcnBG3w==", + "uIkVijg7RPi/1j7c18G1qA==", + "9T7gB0ZkdWB0VpbKIXiujQ==", + "KCJJfgLe00+tjSfP6EBcUg==", + "WbAdlac/PhYUq7J2+n5f+w==", + "GLnS9wDCje7TOMvBX9jJVA==", + "VAg/aU5nl72O+cdNuPRO4g==", + "kzTl7WH/JXsX1fqgnuTOgw==", + "1HDgfU7xU7LWO/BXsODZAQ==", + "D0W5F7gKMljoG5rlue1jrg==", + "9reBKZ1Rp6xcdH1pFQacjw==", + "SSKhl2L3Mvy93DcZulADtA==", + "hlu7os0KtAkpBTBV6D2jyQ==", + "sfte/o9vVNyida/yLvqADA==", + "gYGQBLo5TdMyXks0LsZhsQ==", + "dNq2InSVDGnYXjkxPNPRxA==", + "fiv0DJivQeqUkrzDNlluRw==", + "msstzxq++XO0AqNTmA7Bmg==", + "DCjgaGV5hgSVtFY5tcwkuA==", + "aMmrAzoRWLOMPHhBuxczKg==", + "qNOSm15bdkIDSc/iUr+UTQ==", + "2nSTEYzLK77h5Rgyti+ULQ==", + "BhKO1s1O693Fjy1LItR/Jw==", + "kRnBEH6ILR5GNSmjHYOclw==", + "R97chlspND/sE9/HMScXjQ==", + "1Oykse0jQVbuR3MvW5ot4A==", + "Dmyb+a7/QFsU4d2cVQsxDw==", + "W5now3RWSzzMDAxsHSl++Q==", + "IrDuBrVu1HWm0BthAHyOLQ==", + "V6zyoX6MERIybGhhULnZiw==", + "ZQSDYgpsimK+lYGdXBWE/w==", + "lV70RNlE++04G1KFB3BMXA==", + "QmSBVvdk0tqH9RAicXq2zA==", + "qNyy6Fc0b8oOMWqqaliZ/w==", + "xvipmmwKdYt4eoKvvRnjEg==", + "Q7Df6zGwvb4rC+EtIKfaSw==", + "n1M2dgFPpmaICP+JwxHUug==", + "1k8tL2xmGFVYMgKUcmDcEw==", + "fFvXa1dbMoOOoWZdHxPGjw==", + "UP9mmAKzeQqGhod7NCqzhg==", + "PMCWKgog/G+GFZcIruSONw==", + "dnvatwSEcl73ROwcZ4bbIQ==", + "hY82j+sUQQRpCi6CCGea5A==", + "QoUC9nyK1BAzoUVnBLV2zw==", + "+aF4ilbjQbLpAuFXQEYMWQ==", + "XTCcsVfEvqxnjc0K5PLcyw==", + "ML7ipnY/g8mA1PUIju1j8Q==", + "tOkYq1BZY152/7IJ6ZYKUg==", + "2bsIpvnGcFhTCSrK9EW1FQ==", + "Af9j1naGtnZf0u1LyYmK1w==", + "ZmblZauRqO5tGysY3/0kDw==", + "PF0lpolQQXlpc3qTLMBk8w==", + "emVLJVzha7ui5OFHPJzeRQ==", + "gR0sgItXIH8hE4FVs9Q07w==", + "PTW+fhZq/ErxHqpM0DZwHQ==", + "g0kHTNRI7x/lAsr92EEppw==", + "24H9q+E8pgCEdFS7JO5kzQ==", + "HtDXgMuF8PJ1haWk88S0Ew==", + "pulldyBt2sw6QDvTrCh6zw==", + "ehwc2vvwNUAI7MxU4MWQZw==", + "enj9VEzLbmeOyYugTmdGfQ==", + "auvG6kWMnhCMi7c7e9eHrw==", + "R36O31Pj8jn0AWSuqI7X2Q==", + "3AVYtcIv7A5mVbVnQMaCeA==", + "T9WoUJNwp8h4Yydixbx6nA==", + "t0WN8TwMLgi8UVEImoFXKg==", + "mS99D+CXhwyfVt8xJ+dJZA==", + "AFdelaqvxRj6T3YdLgCFyg==", + "Lu02ic/E94s42A14m7NGCA==", + "7w3b73nN/fIBvuLuGZDCYQ==", + "O209ftgvu0vSr0UZywRFXA==", + "MQvAr+OOfnYnr/Il/2Ubkg==", + "e5txnNRcGs2a9+mBFcF1Qg==", + "YA0kMTJ82PYuLA4pkn4rfw==", + "QIKjir/ppRyS63BwUcHWmw==", + "P3y5MoXrkRTSLhCdLlnc4A==", + "WY7mCUGvpXrC8gkBB46euw==", + "g0GbRp2hFVIdc7ct7Ky7ag==", + "Cv079ZF55RnbsDT27MOQIA==", + "cvMJ714elj/HUh89a9lzOQ==", + "9inw7xzbqAnZDKOl/MfCqA==", + "F58ktE4O0f7C9HdsXYm+lw==", + "CsPkyTZADMnKcgSuNu1qxg==", + "mAzsVkijuqihhmhNTTz65g==", + "FxnbKnuDct4OWcnFMT/a5w==", + "P5wS+xB8srW4a5KDp/JVkA==", + "ctJYJegZhG42i+vnPFWAWw==", + "OrqJKjRndcZ8OjE3cSQv7g==", + "aXqiibI6BpW3qilV6izHaQ==", + "BA18GEAOOyVXO2yZt2U35w==", + "saEpnDGBSZWqeXSJm34eOA==", + "CUEueo8QXRxkfVdfNIk/gg==", + "H0UMAUfHFQH92A2AXRCBKA==", + "CT9g8mKsIN/VeHLSTFJcNQ==", + "E4NtzxQruLcetC23zKVIng==", + "203EqmJI9Q4tWxTJaBdSzA==", + "Do3aqbRKtmlQI2fXtSZfxQ==", + "JaYQXntiyznQzrTlEeZMIw==", + "VK95g27ws2C6J2h/7rC2qA==", + "CQ0PPwgdG3N6Ohfwx1C8xA==", + "/MeHciFhvFzQsCIw39xIZA==", + "u5cUPxM6/spLIV8VidPrAA==", + "OwArFF1hpdBupCkanpwT+Q==", + "PdBgXFq5mBqNxgCiqaRnkw==", + "lC5EumoIcctvxYqwELqIqw==", + "xoPSM86Se+1hHX0y3hhdkw==", + "F5bs0GGWBx9eBwcJJpXbqg==", + "1mw6LfTiirFyfjejf8QNGA==", + "daBhAvmE9shDgmciDAC5eg==", + "AvdeYb9XNOUFWiiz+XGfng==", + "JJJkp1TpuDx5wrua2Wml7g==", + "3y5Xk65ShGvWFbQxcZaQAQ==", + "l6QHU5JsJExNoOnqxBPVbw==", + "X2YfnPXgF2VHVX95ZcBaxQ==", + "g6udffWh7qUnSIo1Ldn3eA==", + "V2P75JFB4Se9h7TCUMfeNA==", + "IUZ5aGpkJ9rLgSg6oAmMlw==", + "pyrUqiZ98gVXxlXQNXv5fA==", + "83ERX2XJV3ST4XwvN7YWCg==", + "eJDUejE/Ez/7kV+S74PDYg==", + "M9oqlPb63e0kZE0zWOm+JQ==", + "0rTYcuVYdilO7zEfKrxY3A==", + "rfPTskbnoh3hRJH6ZAzQRg==", + "QtD35QhE8sAccPrDnhtQmQ==", + "jpNUgFnanr9Sxvj2xbBXZw==", + "nykEOLL/o7h0cs0yvdeT2g==", + "wX2URK6eDDHeEOF3cgPgHA==", + "jqPQ0aOuvOJte/ghI1RVng==", + "nHTsDl0xeQPC5zNRnoa0Rw==", + "mNv2Q67zePjk/jbQuvkAFA==", + "HjlPM2FQWdILUXHalIhQ5w==", + "cHkOsVd80Rgwepeweq4S1g==", + "kTCHqcb3Cos51o8cL+MXcg==", + "nvmBgp0YlUrdZ05INsEE8Q==", + "kFrRjz7Cf2KvLtz9X6oD+w==", + "Tmx0suRHzlUK4FdBivwOwA==", + "bG+P+p34t/IJ1ubRiWg6IA==", + "uESeJe/nYrHCq4RQbrNpGA==", + "ehfPlu6YctzzpQmFiQDxGA==", + "ZH5Es/4lJ+D5KEkF1BVSGg==", + "HHxn4iIQ7m0tF1rSd+BZBg==", + "DQJRsUwO1fOuGlkgJavcwQ==", + "HITIVoFoWNg04NExe13dNA==", + "MeKXnEfxeuQu9t3r/qWvcw==", + "Y7OofF9eUvp7qlpgdrzvkg==", + "XSb71ae0v+yDxNF5HJXGbQ==", + "p8W1LgFuW6JSOKjHkx3+aA==", + "y2JOIoIiT9cV1VxplZPraQ==", + "MN94B0r5CNAF9sl3Kccdbw==", + "Q1pdQadt12anX1QRmU2Y/A==", + "JIC8R48jGVqro6wmG2KXIw==", + "eWgLAqJOU+fdn8raHb9HCw==", + "5CMadLqS2KWwwMCpzlDmLw==", + "H1y2iXVaQYwP0SakN6sa+Q==", + "CUCjG2UaEBmiYWQc6+AS1Q==", + "yV3IbbTWAbHMhMGVvgb/ZQ==", + "80PCwYh4llIKAplcDvMj4g==", + "fgdUFvQPb5h+Rqz8pzLsmw==", + "2SI4F7Vvde2yjzMLAwxOog==", + "kJdY3XEdJS/hyHdR+IN0GA==", + "IKgNa2oPaFVGYnOsL+GC5Q==", + "eXFOya6x5inTdGwJx/xtUQ==", + "uTA0XbiH3fTeVV7u5z0b3w==", + "onFcHOO1c3pDdfCb5N4WkQ==", + "Slu3z535ijcs5kzDnR7kfA==", + "SElc2+YVi3afE1eG1MI7dQ==", + "ND2hYtAIQGMxBF7o7+u7nQ==", + "Pv9FWQEDLKnG/9K9EIz4Gw==", + "6CjtF1S2Y6RCbhl7hMsD+g==", + "rs2QrN4qzAHCHhkcrAvIfA==", + "eTMPXa60OTGjSPmvR4IgGw==", + "pvXHwJ3dwf9GDzfDD9JI3g==", + "CRmAj3JcasAb4iZ9ZbNIbw==", + "rcY4Ot40678ByCfqvGOGdg==", + "l4ddTxbTCW5UmZW+KRmx6A==", + "NKRzJndo2uXNiNppVnqy1g==", + "0NrvBuyjcJ2q6yaHpz/FOA==", + "3YXp1PmMldUjBz3hC6ItbA==", + "CmVD6nh8b/04/6JV9SovlA==", + "HjyxyL0db2hGDq2ZjwOOhg==", + "4PBaoeEwUj79njftnYYqLg==", + "vFFzkWgGyw6OPADONtEojQ==", + "czBWiYsQtNFrksWwoQxlOw==", + "9iB7+VwXRbi6HLkWyh9/kg==", + "zwY6tCjjya/bgrYaCncaag==", + "mW6TCje9Zg2Ep7nzmDjSYQ==", + "5LJqHFRyIwQKA4HbtqAYQQ==", + "INNBBin5ePwTyhPIyndHHg==", + "dChBe9QR29ObPFu/9PusLg==", + "1dhq3ozNCx0o4dV1syLVDA==", + "nyaekSYTKzfSeSfPrB114Q==", + "TfNHjSTV8w6Pg6+FaGlxvA==", + "m/Lp4U75AQyk9c8cX14HJg==", + "uU1TX5DoDg6EcFKgFcn0GA==", + "B+TsxQZf0IiQrU8X9S4dsQ==", + "6b7ue29cBDsvmj1VSa5njw==", + "RvXWAFwM+mUAPW1MjPBaHA==", + "pdaY6kZ8+QqkMOInvvACNA==", + "7nr3zyWL+HHtJhRrCPhYZA==", + "BXGlq54wIH6R3OdYfSSDRw==", + "b06KGv5zDYsTxyTbQ9/eyA==", + "8ylI1AS3QJpAi3I/NLMYdg==", + "0fpe9E6m3eLp/5j5rLrz2Q==", + "Qrh7OEHjp80IW+YzQwzlJg==", + "lqhgbgEqROAdfzEnJ17eXA==", + "Dulw855DfgIwiK7hr3X8vg==", + "wsp+vmW8sEqXYVURd/gjHA==", + "VoPth5hDHhkQcrQTxHXbuw==", + "TgWe70YalDPyyUz6n88ujg==", + "9lLhHcrPWI4EsA4fHIIXuw==", + "UymZUnEEQWVnLDdRemv+Tw==", + "qnkFUlJ8QT322JuCI3LQgg==", + "/p/aCTIhi1bU0/liuO/a2Q==", + "hWoxz5HhE50oYBNRoPp1JQ==", + "88tB/HgUIUnqWXEX++b5Aw==", + "Z8T1b9RsUWf59D06MUrXCQ==", + "BZTzHJGhzhs3mCXHDqMjnQ==", + "XfY+QUriCAA1+3QAsswdgg==", + "TZ3ATPOFjNqFGSKY3vP2Hw==", + "cl4t9FXabQg7tbh1g7a0OA==", + "9SgfpAY0UhNC6sYGus9GgQ==", + "d/Wd3Ma1xYyoMByPQnA9Cw==", + "DDitrRSvovaiXe2nfAtp4g==", + "s+eHg5K9zZ2Jozu5Oya9ZQ==", + "z3L2BNjQOMOfTVBUxcpnRA==", + "v4xIYrfPGILEbD/LwVDDzA==", + "HoaBBw2aPCyhh0f5GxF+/Q==", + "i9IRqAqKjBTppsxtPB7rdw==", + "cWUg7AfqhiiEmBIu+ryImA==", + "E+02smwQGBIxv42LIF2Y4Q==", + "W4CfeVp9mXgk04flryL7iA==", + "9SUOfKtfKmkGICJnvbIDMg==", + "xweGAZf+Yb3TtwR/sGmGIA==", + "EJgedRYsZPc4cT9rlwaZhg==", + "wv4NC9CIpwuGf/nOQYe/oA==", + "ZXeMG5eqQpZO/SGKC4WQkA==", + "bzXXzQGZs8ustv0K4leklA==", + "RkQK9S1ezo+dFYHQP57qrw==", + "mrinv7KooPQPrLCNTRWCFg==", + "qIUJPanWmGzTD1XxvHp+6w==", + "Js7g8Dr6XsnGURA4UNF0Ug==", + "dpSTNOCPFHN5yGoMpl1EUA==", + "ugY8rTtJkN4CXWMVcRZiZw==", + "rqHKB91H3qVuQAm+Ym5cUA==", + "UjmDFO7uzjl4RZDPeMeNyg==", + "cu4ZluwohhfIYLkWp72pqA==", + "ZydKlOpn2ySBW0G3uAqwuw==", + "LWd0+N3M94n81qd346LfJQ==", + "VbHoWmtiiPdABvkbt+3XKQ==", + "J4MC9He6oqjOWsYQh9nl3Q==", + "ahAbmGJZvUOXrcK6OydNGQ==", + "Byhi4ymFqqH8uIeoMRvPug==", + "LSN9GmT6LUHlCAMFqpuPIA==", + "IAMInfSYb76GxDlAr1dsTg==", + "qYHdgFAXhF/XcW4lxqfvWQ==", + "26+yXbqI+fmIZsYl4UhUzw==", + "AwPTZpC28NJQhf5fNiJuLA==", + "SESKbGF35rjO64gktmLTWA==", + "YVlRQHQglkbj3J2nHiP/Hw==", + "DdaT4JLC7U0EkF50LzIj9w==", + "G0LChrb0OE5YFqsfTpIL1Q==", + "5Yrj6uevT8wHRyqqgnSfeg==", + "NmWmDxwK5FpKlZbo0Rt8RA==", + "iUsUCB0mfRsE9KPEQctIzw==", + "Tm4zk2Lmg8w4ITMI31NfTA==", + "Vu0E+IJXBnc25x4n41kQig==", + "6wkfN8hyKmKU6tG3YetCmw==", + "trjM81KANPZrg9iSThWx6Q==", + "iGuY4VxcotHvMFXuXum7KA==", + "ICPdBCdONUqPwD5BXU5lrw==", + "alqHQBz8V446EdzuVfeY5Q==", + "74FW/QYTzr/P1k6QwVHMcw==", + "avZp5K7zJvRvJvpLSldNAw==", + "TIKadc6FAaRWSQUg5OATgg==", + "PfkWkSbAxIt1Iso0znW0+Q==", + "Z+bsbVP91KrJvxrujBLrrQ==", + "mrxlFD3FBqpSZr1kuuwxGg==", + "nUgYO7/oVNSX8fJqP2dbdg==", + "tVhXk9Ff3wAg56FbdNtcFg==", + "DdiNGiOSoIZxrMrGNvqkXw==", + "CDsanJz7e3r/eQe+ZYFeVQ==", + "wVfSZYjMjbTsD2gaSbwuqQ==", + "6c0iuya20Ys8BsvoI4iQaQ==", + "qCPfJTR8ecTw6u6b1yHibA==", + "fZrj3wGQSt8RXv0ykJROcQ==", + "gR3B8usSEb0NLos51BmJQg==", + "vTAmgfq3GxL4+ubXpzwk5w==", + "jLkmUZ6fV56GfhC0nkh4GA==", + "3v09RHCPTLUztqapThYaHg==", + "nULSbtw2dXbfVjZh33pDiA==", + "IHhyR6+5sZXTH+/NrghIPg==", + "tnUtJ/DQX9WaVJyTgemsUA==", + "7xTKFcog69nTmMfr5qFUTA==", + "IshzWega6zr3979khNVFQQ==", + "Ng5v/B9Z10TTfsDFQ/XrXQ==", + "hnCUnoxofUiqQvrxl73M8w==", + "VPa7DG6v7KnzMvtJPb88LQ==", + "4LtQrahKXVtsbXrEzYU1zQ==", + "Ev/xjTi7akYBI7IeZJ4Igw==", + "41WEjhYUlG6jp2UPGj11eQ==", + "JvXTdChcE3AqMbFYTT3/wg==", + "2rOkEVl90EPqfHOF5q2FYw==", + "mjFBVRJ7TgnJx+Q74xllPg==", + "Uy4QI8D2y1bq/HDNItCtAw==", + "wMOE/pEKVIklE75xjt6b6w==", + "ZcuIvc8fDI+2uF0I0uLiVA==", + "CX/N/lHckmAtHKysYtGdZA==", + "j8to4gtSIRYpCogv2TESuQ==", + "iS9wumBV5ktCTefFzKYfkA==", + "ewPT4dM12nDWEDoRfiZZnA==", + "vWn9OPnrJgfPavg4D6T/HQ==", + "J/PNYu4y6ZMWFFXsAhaoow==", + "catI+QUNk3uJ+mUBY3bY8Q==", + "F8tEIT5EhcvLNRU5f0zlXQ==", + "zyA9f5J7mw5InjhcfeumAQ==", + "MlOOZOwcRGIkifaktEq0aQ==", + "Pt3i49uweYVgWze3OjkjJA==", + "sfIClgTMtZo9CM9MHaoqhQ==", + "HeQbUuBM9sqfXFXRBDISSw==", + "SFn78uklZfMtKoz2N0xDaQ==", + "H6j2nPbBaxHecXruxiWYkA==", + "fU32wmMeD44UsFSqFY0wBA==", + "hDILjSpTLqJpiSSSGu445A==", + "ieEAgvK9LsWh2t6DsQOpWA==", + "xfjBQk3CrNjhufdPIhr91A==", + "j+8/VARfbQSYhHzj0KPurQ==", + "/zFLRvi75UL8qvg+a6zqGg==", + "U0KmEI6e5zJkaI4YJyA5Ew==", + "uXvr6vi5kazZ9BCg2PWPJA==", + "jEqP0dyHKHiUjZ9dNNGTlQ==", + "1xWx5V3G9murZP7srljFmA==", + "OIwtfdq37eQ0qoXuB2j7Hw==", + "fUAy3f9bAglLvZWvkO2Lug==", + "duRFqmvqF93uf/vWn8aOmg==", + "ysRQ+7Aq7eVLOp88KnFVMA==", + "CkZUmKBAGu0FLpgPDrybpw==", + "TrLmfgwaNATh24eSrOT+pw==", + "83wtvSoSP9FVBsdWaiWfpA==", + "pUfWmRXo70yGkUD/x5oIvA==", + "PybPZhJErbRTuAafrrkb3g==", + "8hsfXqi4uiuL+bV1VrHqCw==", + "TVlHoi8J7sOZ2Ti7Dm92cQ==", + "za4rzveYVMFe3Gw531DQJQ==", + "JKphO0UYjFqcbPr6EeBuqg==", + "hqeSvwu8eqA072iidlJBAw==", + "bUF0JIfS4uKd3JZj2xotLQ==", + "hKOsXOBoFTl/K4xE+RNHDA==", + "JHBjKpCgSgrNNACZW1W+1w==", + "Rrq0ak9YexLqqbSD4SSXlw==", + "+NmjwjsPhGJh9bM10SFkLw==", + "xMIHeno2qj3V8q9H1xezeg==", + "TcFinyBrUoAEcLzWdFymow==", + "Rvchz/xjcY9uKiDAkRBMmA==", + "TYlnrwgyeZoRgOpBYneRAg==", + "PbnxuVerGwHyshkumqAARg==", + "iFtadcw8v6betKka9yaJfg==", + "7wgT9WIiMVcrj48PVAMIgw==", + "2HHqeGRMfzf3RXwVybx+ZQ==", + "tdgI9v7cqJsgCAeW1Fii1A==", + "4ZFYKa7ZgvHyZLS6WpM8gA==", + "gB8wkuIzvuDAIhDtNT1gyA==", + "g1ELwsk6hQ+RAY1BH640Pg==", + "UZoibx+y1YJy/uRSa9Oa2w==", + "yS/yMnJDHW0iaOsbj4oPTg==", + "JzW+yhrjXW1ivKu3mUXPXg==", + "/wIZAye9h1TUiZmDW0ZmYA==", + "YK+q7uJObkQZvOwQ9hplMg==", + "Rs8deApkoosIJSfX7NXtAA==", + "MsCloSmTFoBpm7XWYb+ueQ==", + "3ltw31yJuAl4VT6MieEXXw==", + "1+qmrbC8c7MJ6pxmDMcKuA==", + "AYxGETZs477n2sa1Ulu/RQ==", + "Q0TJZxpn3jk67L7N+YDaNA==", + "OGpsXRHlaN8BvZftxh1e7A==", + "UbABE6ECnjB+9YvblE9CYw==", + "kZ0D191c/uv4YMG15yVLDw==", + "QWURrsEgxbJ8MWcaRmOWqw==", + "xiFlcSfa/gnPiO+LwbixcQ==", + "Szko0IPE7RX2+mfsWczrMg==", + "Ugt8HVC/aUzyWpiHd0gCOQ==", + "8j9GVPiFdfIRm/+ho7hpoA==", + "KR401XBdgCrtVDSaXqPEiA==", + "d0NBFiwGlQNclKObRtGVMQ==", + "XEwOJG24eaEtAuBWtMxhwg==", + "0Y6iiZjCwPDwD/CwJzfioQ==", + "MvMbvZNKbXFe2XdN+HtnpQ==", + "fsoXIbq0T0nmSpW8b+bj+g==", + "Uje3Ild84sN41JEg3PEHDg==", + "i6ZYpFwsyWyMJNgqUMSV1A==", + "+P5q4YD1Rr5SX26Xr+tzlw==", + "z4oKy2wKH+sbNSgGjbdHGw==", + "XwKWd03sAz8MmvJEuN08xA==", + "Xv0mNYedaBc57RrcbHr9OA==", + "9oUawSwUGOmb0sDn3XS6og==", + "9RGIQ2qyevNbSSEF36xk/A==", + "q8YF9G2jqydAxSqwyyys5Q==", + "m5JIUETVXcRza4VL4xlJbg==", + "aRpdnrOyu5mWB1P5YMbvOA==", + "rM/BOovNgnvebKMxZQdk7g==", + "fQS0jnQMnHBn7+JZWkiE/g==", + "gAoV4BZYdW1Wm712YXOhWQ==", + "hCzsi1yDv9ja5/o7t94j9Q==", + "CoLvjQDQGldGDqRxfQo+WQ==", + "pfGcaa49SM3S6yJIPk/EJQ==", + "yYp4iuI5f/y/l1AEJxYolQ==", + "Jj4IrSVpqQnhFrzNvylSzA==", + "4jeOFKuKpCmMXUVJSh9y0g==", + "+NMUaQ7XPsAi0rk7tTT9wQ==", + "Jt4Eg6MJn8O4Ph/K2LeSUA==", + "CiiUeJ0LeWfm7+gmEmYXtg==", + "c5Tc7rTFXNJqYyc0ppW+Iw==", + "4KJZPCE9NKTfzFxl76GWjg==", + "aXs9qTEXLTkN956ch3pnOA==", + "f5Xo7F1uaiM760Qbt978iw==", + "wpZqFkKafFpLcykN2IISqg==", + "vIORTYSHFIXk5E2NyIvWcQ==", + "prOsOG0adI4o+oz50moipw==", + "blygTgAHZJ3NzyAT33Bfww==", + "rBt6L/KLT7eybxKt5wtFdg==", + "vMuaLvAntJB5o7lmt/kVXA==", + "iujlt9fXcUXEYc+T2s5UjA==", + "LyYPOZKm8bBegMr5NTSBfg==", + "ZtWvgitOSRDWq7LAKYYd4Q==", + "kh51WUI5TRnKhur6ZEpRTQ==", + "VzQ1NwNv9btxUzxwVqvHQg==", + "8fJLQeIHaTnJ8wGqUiKU6g==", + "vvEH5A39TTe1AOC11rRCLA==", + "dihDsG7+6aocG6M9BWrCzQ==", + "3jqsY8/xTWELmu/az3Daug==", + "mpOtwBvle+nyY6lUBwTemw==", + "E1CvxFbuu9AYW604mnpGTw==", + "1LPC0BzhJbepHTSAiZ3QTw==", + "XpGXh76RDgXC4qnTCsnNHA==", + "3Gg9N7vjAfQEYOtQKuF/Eg==", + "+WpF8+poKmHPUBB4UYh/ig==", + "UNt7CNMtltJWq8giDciGyA==", + "RIZYDgXqsIdTf9o2Tp/S7g==", + "0QCQORCYfLuSbq94Sbt0bQ==", + "hvsZ5JmVevK1zclFYmxHaw==", + "3+9nURtBK3FKn0J9DQDa3g==", + "jdVMQqApseHH3fd91NFhxg==", + "VX+cVXV8p9i5EBTMoiQOQQ==", + "I5qDndyelK4Njv4YrX7S6w==", + "rWliqgfZ3/uCRBOZ9sMmdA==", + "vwno3vugCvt6ooT3CD4qIQ==", + "cffrYrBX3UQhfX1TbAF+GQ==", + "nOiwBFnXxCBfPCHYITgqNg==", + "LQttmX92SI94+hDNVd8Gtw==", + "iCF+GWw9/YGQXsOOPAnPHQ==", + "nwtCsN1xEYaHvEOPzBv+qQ==", + "CQpJFrpOvcQhsTXIlJli+Q==", + "tYeIZjIm0tVEsYxH1iIiUQ==", + "iCnm5fPmSmxsIzuRK6osrA==", + "tX8X8KoxUQ8atFSCxgwE1Q==", + "hZlX6qOfwxW5SPfqtRqaMw==", + "2aIx9UdMxxZWvrfeJ+DcTw==", + "TlJizlASbPtShZhkPww4UA==", + "p+bx+/WQWALXEBCTnIMr4w==", + "4VR5LiXLew6Nyn91zH9L4w==", + "bfUD03N2PRDT+MZ+WFVtow==", + "cTvDd8okNUx0RCMer6O8sw==", + "49jZr/mEW6fvnyzskyN40w==", + "vHmQUl4WHXs1E/Shh+TeyA==", + "fgXfRuqFfAu8qxbTi4bmhA==", + "Wn+Vj4eiWx0WPUHr3nFbyA==", + "2SwIiUwT4vRZPrg7+vZqDA==", + "nkedTagkmf6YE4tEY+0fKw==", + "8nOTDhFyZ8YUA4b6M5p84w==", + "qnzWszsyJhYtx8wkMN6b1g==", + "ka7pMp8eSiv92WgAsz2vdA==", + "pGQEWJ38hb/ZYy2P1+FIuw==", + "cVhdRFuZaW/09CYPmtNv5g==", + "prCOYlboBnzmLEBG/OeVrQ==", + "oIWwTbkVS5DDL47mY9/1KQ==", + "PKtXc4x4DEjM45dnmPWzyg==", + "f9ywiGXsz+PuEsLTV3zIbQ==", + "6G2bD3Y7qbGmfPqH9TqLFA==", + "DMHmyn2U2n+UXxkqdvKpnA==", + "XOG1PYgqoG8gVLIbVLTQgg==", + "1FSrgkUXgZot2CsmbAtkPw==", + "BxFP+4o6PSlGN78eSVT1pA==", + "EZVQGsXTZvht1qedRLF8bQ==", + "eYAQWuWZX2346VMCD6s7/A==", + "jkUpkLoIXuu7aSH8ZghIAQ==", + "mXPtbPaoNAAlGmUMmJEWBQ==", + "HLesnV3DL+FhWF3h6RXe8g==", + "nDAsSla+9XfAlQSPsXtzPA==", + "RAECgYZmcF4WxcFcZ4A0Ww==", + "W+M4BcYNmjj7xAximDGWsA==", + "ueODvMv/f9ZD8O0aIHn4sg==", + "cszpMdGbsbe6BygqMlnC9Q==", + "siHwJx6EgeB1gBT9z/vTyw==", + "FN7oLGBQGHXXn5dLnr/ElA==", + "Tud+AMyuFkWYYZ73yoJGpQ==", + "TuaG3wRdM9BWKAxh2UmAsg==", + "8CjmgWQSAAGcXX9kz3kssw==", + "ays5/F7JANIgPHN0vp2dqQ==", + "PCOGl7GIqbizAKj/sZmlwQ==", + "rZKD8oJnIj5fSNGiccfcvA==", + "gFEnTI8os2BfRGqx9p5x8w==", + "5r1ZsGkrzNQEpgt/gENibw==", + "1YO9G8qAhLIu2rShvekedw==", + "6ZKmm7IW7IdWuVytLr68CQ==", + "mMfn8OaKBxtetweulho+xQ==", + "GQJxu1SoMBH14KPV/G/KrQ==", + "IYIP2UBRyWetVfYLRsi1SQ==", + "Jit0X0srSNFnn8Ymi1EY+g==", + "ARCWkHAnVgBOIkCDQ19ZuA==", + "qA0sTaeNPNIiQbjIe1bOgQ==", + "iGI9uqMoBBAjPszpxjZBWQ==", + "+L1FDsr5VQtuYc2Is5QGjw==", + "4XNUmgwxsqDYsNmPkgNQYQ==", + "Yig+Wh18VIqdsmwtwfoUQw==", + "uqp92lAqjec8UQYfyjaEZw==", + "QiozlNcQCbqXtwItWExqJQ==", + "JFHutgSe1/SlcYKIbNNYwQ==", + "Y26jxXvl79RcffH8O8b9Ew==", + "bQ7J5mebp38rfP/fuqQOsg==", + "HI4ZIE5s8ez8Rb+Mv39FxA==", + "OzH7jTcyeM7RPVFtBdakpQ==", + "HLxROy6fx/mLXFTDSX4eLA==", + "s5RUHVRNAoKMuPR/Jkfc2Q==", + "X9QAaNjgiOeAWSphrGtyVw==", + "ALJWKUImVE40MbEooqsrng==", + "9MDG0WeBPpjGJLEmUJgBWg==", + "9RXymE9kCkDvBzWGyMgIWA==", + "vFox1d3llOeBeCUZGvTy0A==", + "r3lQAYOYhwlLnDWQIunKqg==", + "2os5s7j7Tl46ZmoZJH8FjA==", + "O5N2yd+QQggPBinQ+zIhtQ==", + "ZygAjaN62XhW5smlLkks+Q==", + "AgDJsaW0LkpGE65Kxk5+IA==", + "omAjyj1l6gyQAlBGfdxJTw==", + "fY9VATklOvceDfHZDDk57A==", + "StpQm/cQF8cT0LFzKUhC5w==", + "CYJB3qy5GalPLAv1KGFEZA==", + "coGEgMVs2b314qrXMjNumQ==", + "DQQB/l55iPN9XcySieNX3A==", + "6dshA8knH5qqD+KmR/kdSQ==", + "qyRmvxh8p4j4f+61c10ZFQ==", + "apWEPWUvMC24Y+2vTSLXoA==", + "RzX2OfSFEd//LhZwRwzBVw==", + "NdULoUDGhIolzw1PyYKV0A==", + "5w/c9WkI/FA+4lOtdPxoww==", + "bV9r7j2kNJpDCEM5E2339Q==", + "vbyiKeDCQ4q9dDRI1Q0Ong==", + "9xIgKpZGqq0/OU6wM5ZSHw==", + "RYkDwwng6eeffPHxt8iD9A==", + "w5N/aHbtOIKzcvG3GlMjGA==", + "3P2aJxV8Trll2GH9ptElYA==", + "yteeQr3ub2lDXgLziZV+DQ==", + "yqtj8GfLaUHYv/BsdjxIVw==", + "NyF+4VRog7etp90B9FuEjA==", + "uwA6N5LptSXqIBkTO0Jd7Q==", + "6lVSzYUQ/r0ep4W2eCzFpg==", + "1d7RPHdZ9qzAbG3Vi9BdFA==", + "7br49X11xc2GxQLSpZWjKQ==", + "peMW+rpwmXrSwplVuB/gTA==", + "RqYpA5AY7mKPaSxoQfI1CA==", + "dqVw2q2nhCvTcW82MT7z0g==", + "5S5/asYfWjOwnzYpbK6JDw==", + "NvkR0inSzAdetpI4SOXGhw==", + "tIqwBotg052wGBL65DZ+yA==", + "S4RvORcJ3m6WhnAgV4YfYA==", + "UAqf4owQ+EmrE45hBcUMEw==", + "4aPU6053cfMLHgLwAZJRNg==", + "3Y6/HqS1trYc9Dh778sefg==", + "ck86G8HsbXflyrK7MBntLg==", + "GLmWLXURlUOJ+PMjpWEXVA==", + "jNJQ6otieHBYIXA9LjXprg==", + "AsAHrIkMgc3RRWnklY9lJw==", + "FCLQocqxxhJeleARZ6kSPg==", + "3Leu2Sc+YOntJFlrvhaXeg==", + "hSkY45CeB6Ilvh0Io4W6cg==", + "DwrNdmU5VFFf3TwCCcptPA==", + "u2WQlcMxOACy6VbJXK4FwA==", + "E9IlDyULLdeaVUzN6eky8g==", + "EXveRXjzsjh8zbbQY2pM9g==", + "5VO1inwXMvLDBQSOahT6rg==", + "HaHTsLzx7V3G1SFknXpGxA==", + "MMaegl2Md9s/wOx5o9564w==", + "mpWNaUH9kn4WY26DWNAh3Q==", + "w3G+qXXqqKi8F5s+qvkBUg==", + "wM8tnXO4PDlLVHspZFcjYw==", + "LFcpCtnSnsCPD2gT/RA+Zg==", + "bhVbgJ4Do4v56D9mBuR/EA==", + "yU3N0HMSP5etuHPNrVkZtg==", + "FzqIpOcTsckSNHExrl+9jg==", + "BYz52gYI/Z6AbYbjWefcEA==", + "h3vYYI9yhpSZV2MQMJtwFQ==", + "adJAjAFyR2ne1puEgRiH+g==", + "eDcyiPaB954q5cPXcuxAQw==", + "40gCrW4YWi+2lkqMSPKBPg==", + "ulLuTZqhEDkX0EJ3xwRP9A==", + "y4iBxAMn/KzMmaWShdYiIw==", + "ilBBNK/IV69xKTShvI94fQ==", + "0HN6MIGtkdzNPsrGs611xA==", + "twPn6wTGqI0aR//0wP3xtA==", + "3UNJ37f+gnNyYk9yLFeoYA==", + "4SdHWowXgCpCDL28jEFpAw==", + "Mr5mCtC53+wwmwujOU/fWw==", + "81pAhreEPxcKse+++h1qBg==", + "KmcGEE0pacQ/HDUgjlt7Pg==", + "Gt4/MMrLBErhbFjGbiNqQQ==", + "lf1fwA0YoWUZaEybE+LyMQ==", + "RIVYGO2smx9rmRoDVYMPXw==", + "rJ9qVn8/2nOxexWzqIHlcQ==", + "lfOLLyZNbsWQgHRhicr4ag==", + "wgH1GlUxWi6/yLLFzE76uQ==", + "Qg1ubGl+orphvT990e5ZPA==", + "Z5B+uOmPZbpbFWHpI9WhPw==", + "snGTzo540cCqgBjxrfNpKw==", + "ZqkmoGB0p5uT5J6XBGh7Tw==", + "uPi8TsGY3vQsMVo/nsbgVQ==", + "Y5XR8Igvau/h+c1pRgKayg==", + "ZmVpw1TUVuT13Zw/MNI5hQ==", + "60suecbWRfexSh7C67RENA==", + "kZ/mZZg9YSDmk2rCGChYAg==", + "OpL+vHwPasW30s2E1TYgpA==", + "ZVnErH1Si4u51QoT0OT7pA==", + "3pi3aNVq1QNJmu1j0iyL0g==", + "tb5+2dmYALJibez1W4zXgA==", + "jOPdd330tB6+7C29a9wn0Q==", + "5oD/aGqoakxaezq43x0Tvw==", + "HdB7Se47cWjPgpJN0pZuiA==", + "6WhHPWlqEUqXC52rHGRHjA==", + "WLwpjgr9KzevuogoHZaVUw==", + "E8yMPK7W0SIGTK6gIqhxiQ==", + "1/Hxu8M9N/oNwk8bCj4FNQ==", + "Uo1ebgsOxc3eDRds1ah3ag==", + "5pqqzC/YmRIMA9tMFPi7rg==", + "ri4AOITPdB1YHyXV+5S51g==", + "HfvsiCQN/3mT0FabCU5ygQ==", + "UQTQk5rrs6lEb1a+nkLwfg==", + "VH70dN82yPCRctmAHMfCig==", + "yD3Dd4ToRrl53k/2NSCJiw==", + "fO0+6TsjL+45p9mSsMRiIg==", + "fM5uYpkvJFArnYiQ3MrQnA==", + "V+QzdKh5gxTPp2yPC9ZNEg==", + "XHHEg/8KZioW/4/wgSEkbQ==", + "2abfl3N46tznOpr+94VONQ==", + "gxwbqZDHLbQVqXjaq42BCg==", + "WnHK5ZQDR6Da5cGODXeo0A==", + "SChDh/Np1HyTPWfICfE1uA==", + "yhexr/OFKfZl0o3lS70e4w==", + "N65PqIWiQeS082D6qpfrAg==", + "RM5CpIiB94Sqxi462G7caA==", + "CBAGa5l95f3hVzNi6MPWeQ==", + "OHJBT2SEv5b5NxBpiAf7oQ==", + "p48i7AfSSAyTdJSyHvOONw==", + "/SP6pOdYFzcAl2OL05z4uQ==", + "N8dXCawxSBX40fgRRSDqlQ==", + "bMWFvjM8eVezU1ZXKmdgqw==", + "Um1ftRBycvb+363a90Osog==", + "QAz7FA+jpz9GgLvwdoNTEQ==", + "qO4HlyHMK5ygX+6HbwQe8w==", + "UgvtdE2eBZBUCAJG/6c0og==", + "q5g3c8tnQTW2EjNfb2sukw==", + "gsC/mWD8KFblxB0JxNuqJw==", + "SVFbcjXbV7HRg+7jUrzpwg==", + "bz294kSG4egZnH2dJ8HwEg==", + "ybpTgPr3SjJ12Rj5lC/IMA==", + "yDrAd1ot38soBk7zKdnT8A==", + "BB/R8oQOcoE4j63Hrh8ifg==", + "GNrMvNXQkW7PydlyJa+f1w==", + "w0PKdssv+Zc5J/BbphoxpA==", + "D5ibbo8UJMfFZ48RffuhgQ==", + "MdvhC1cuXqni/0mtQlSOCw==", + "wQKL8Ga6JQkpZ7yymDkC3w==", + "o1uhaQg5/zfne84BFAINUQ==", + "Ft2wXUokFdUf6d2Y/lwriw==", + "sLJrshdEANp0qk2xOUtTnQ==", + "jx7rpxbm1NaUMcE2ktg5sA==", + "ZQ0ZnTsZKWxbRj7Tilh24Q==", + "KhrIIHfqXl9zGE9aGrkRVg==", + "jS0JuioLGAVaHdo/96JFoQ==", + "tr+U/vt+MIGXPRQYYWJfRg==", + "TXab/hqNGWaSK+fXAoB2bg==", + "0K4NBxqEa3RYpnrkrD/XjQ==", + "3oMTbWf7Bv83KRlfjNWQZA==", + "yLAhLNezvqVHmN1SfMRrPw==", + "ZYW30FfgwHmW6nAbUGmwzA==", + "CZNoTy26VUQirvYxSPc/5A==", + "CF1sAlhjDQY/KWOBnSSveA==", + "+CLf5witKkuOvPCulTlkqw==", + "1m1yD4L9A7Q1Ot+wCsrxJQ==", + "2E41e0MgM3WhFx2oasIQeA==", + "mDXHuOmI4ayjy2kLSHku1Q==", + "sCLMrLjEUQ6P1L8tz90Kxg==", + "zDUZCzQesFjO1JI3PwDjfg==", + "x/BIDm6TKMhqu/gtb3kGyw==", + "DEaZD/8aWV6+zkiLSVN/gA==", + "7dz+W494zwU5sg63v5flCg==", + "Y5iDQySR2c3MK7RPMCgSrw==", + "GglPoW5fvr4JSM3Zv99oiA==", + "myzvc+2MfxGD9uuvZYdnqQ==", + "V9G1we3DOIQGKXjjPqIppQ==", + "gYvdNJCDDQmNhtJ6NKSuTA==", + "rXtGpN17Onx8LnccJnXwJQ==", + "/a+bLXOq02sa/s8h7PhUTg==", + "htNVAogFakQkTX6GHoCVXg==", + "eshD40tvOA6bXb0Fs/cH3A==", + "K1CGbMfhlhIuS0YHLG30PQ==", + "aOeJZUIZM9YWjIEokFPnzQ==", + "r0hAwlS0mPZVfCSB+2G6uQ==", + "0q+erphtrB+6HBnnYg7O6w==", + "bkRdUHAksJZGzE1gugizYQ==", + "J8v2f6hWFu8oLuwhOeoQjA==", + "qkvEep4vvXhc2ZJ6R449Mg==", + "6HGeEPyTAu9oiKhNVLjQnA==", + "JoATsk/aJH0UcDchFMksWA==", + "QozQL0DTtr+PXNKifv6l6g==", + "HiAgt86AyznvbI2pnLalVQ==", + "lY+tivtsfvU0LJzBQ6itYQ==", + "EfXDc6h69aBPE6qsB+6+Ig==", + "gnAIpoCyl3mQytLFgBEgGA==", + "p2JPOX8yDQ0agG+tUyyT/g==", + "zeELfk015D5krExLKRUYtg==", + "wDiGoFEfIVEDyyc4VpwhWQ==", + "7Ephy+mklG2Y3MFdqmXqlA==", + "8ZFPMJJYVJHsfRpU4DigSg==", + "ocRh5LR1ZIN9Johnht8fhQ==", + "l5f3I6osM9oxLRAwnUnc5A==", + "yxCyBXqGWA735JEyljDP7Q==", + "qE/h/Z+6buZWf+cmPdhxog==", + "HCu4ZMrcLMZbPXbTlWuvvQ==", + "TDrq23VUdzEU/8L5i8jRJQ==", + "L+N/6geuokiLPPSDXM9Qkg==", + "v6jZicMNM3ysm3U5xu0HoQ==", + "b85nxzs8xiHxaqezuDVWvg==", + "ca+kx+kf7JuZ3pfYKDwFlg==", + "KlY5TGg0pR/57TVX+ik1KQ==", + "3jmCreW5ytSuGfmeLv7NfQ==", + "ucLMWnNDSqE4NOCGWvcGWw==", + "NSrzwNlB0bde3ph8k6ZQcQ==", + "nL4iEd3b5v4Y9fHWDs+Lrw==", + "W2x0SBzSIsTRgyWUCOZ/lg==", + "ifZM0gBm9g9L09YlL+vXBg==", + "4WcFEswYU/HHQPw77DYnyA==", + "TLJbasOoVO435E5NE5JDcA==", + "WyCFB4+6lVtlzu3ExHAGbQ==", + "BW0A06zoQw7S+YMGaegT7g==", + "qP1cCE4zsKGTPhjbcpczMw==", + "UVEZPoH9cysC+17MKHFraw==", + "eQ45Mvf5in9xKrP6/qjYbg==", + "fOARCnIg/foF/6tm7m9+3w==", + "lK2xe+OuPutp4os0ZAZx5w==", + "Tug3eh+28ttyf+U7jfpg5w==", + "ENFfP93LA257G6pXQkmIdg==", + "FuWspiqu5g8Eeli5Az+BkA==", + "kIGxCUxSlNgsKZ45Al1lWw==", + "RzeH+G3gvuK1z+nJGYqARQ==", + "0ofMbUCA3/v5L8lHnX4S5w==", + "VI8pgqBZeGWNaxkuqQVe7g==", + "x6lNRGgJcRxgKTlzhc1WPg==", + "La0gzdbDyXUq6YAXeKPuJA==", + "dAq8/1JSQf1f4QPLUitp0g==", + "WN7lFJfw4lSnTCcbmt5nsg==", + "2aDK0tGNgMLyxT+BQPDE8Q==", + "9W57pTzc572EvSURqwrRhw==", + "37Nkh06O979nt7xzspOFyQ==", + "4TQkMnRsXBobbtnBmfPKnA==", + "f/BjtP5fmFw2dRHgocbFlg==", + "9vEgJVJLEfed6wJ7hBUGgQ==", + "HRWYX2XOdsOqYzCcqkwIyw==", + "StDtLMlCI75g4XC59mESEQ==", + "99+SBN45LwKCPfrjUKRPmw==", + "HbT6W1Ssd3W7ApKzrmsbcg==", + "l8/KMItWaW3n4g1Yot/rcQ==", + "s7iW1M6gkAMp+D/3jHY58w==", + "GWwJ32SZqD5wldrXUdNTLA==", + "YhLEPsi/TNyeUJw69SPYzQ==", + "g0aTR8aJ0uVy3YvGYu5xrw==", + "m6get5wjq5j1i5abnpXuZQ==", + "ymtA8EMPMgmMcimWZZ0A1Q==", + "HEcOaEd9zCoOVbEmroSvJg==", + "F8l+Qd9TZgzV+r8G584lKA==", + "3yDD+xT8iRfUVdxcc7RxKw==", + "1eRUCdIJe3YGD5jOMbkkOg==", + "DO1/jfP/xBI9N0RJNqB2Rw==", + "SiSlasZ+6U2IZYogqr2UPg==", + "tBQDfy48FnIOZI04rxfdcA==", + "HEghmKg3GN60K7otpeNhaA==", + "mTLBkP+yGHsdk5g7zLjVUw==", + "RgtwfY5pTolKrUGT+6Pp6g==", + "EyIsYQxgFa4huyo/Lomv7g==", + "HwLSUie8bzH+pOJT3XQFyg==", + "7Tauesu7bgs5lJmQROVFiQ==", + "ojugpLIfzflgU2lonfdGxA==", + "ZqjnqxZE/BjOUY0CMdVl0g==", + "oQjugfjraFziga1BcwRLRA==", + "JXCYeWjFqcdSf6QwB54G+A==", + "TeBGJCqSqbzvljIh9viAqA==", + "1Gpj4TPXhdPEI4zfQFsOCg==", + "asouSfUjJa8yfMG7BBe+fA==", + "ccy3Ke2k4+evIw0agHlh3w==", + "CzSumIcYrZlxOUwUnLR2Zw==", + "9QFYrCXsGsInUb4SClS3cQ==", + "3RTtSaMp1TZegJo5gFtwwA==", + "aTWiWjyeSDVY/q8y9xc2zg==", + "UK+R+hAoVeZ4xvsoZjdWpw==", + "rHagXw+CkF3uEWPWDKXvog==", + "MfkyURTBfkNZwB+wZKjP4g==", + "Qf7JFJJuuacSzl6djUT2EQ==", + "K1RL+tLjICBvMupe7QppIQ==", + "R2OOV18CV/YpWL1xzr/VQg==", + "o+areESiXgSO0Lby56cBeg==", + "VPqyIomYm7HbK5biVDvlpw==", + "pw1jplCdTC+b0ThX0FXOjw==", + "gTnsH3IzALFscTZ1JkA9pw==", + "JYJvOZ4CHktLrYJyAbdOnA==", + "P8lUiLFoL100c9YSQWYqDA==", + "LATQEY7f47i77M6p11wjWA==", + "U9kE50Wq5/EHO03c5hE4Ug==", + "pFKzcRHSUBqSMtkEJvrR1Q==", + "vHVXsAMQqc0qp7HA5Q+YkA==", + "3XyoREdvhmSbyvAbgw2y/A==", + "qOEIUWtGm5vx/+fg4tuazg==", + "a6IszND1m+6w+W+CvseC7g==", + "KuNY8qAJBce+yUIluW8AYw==", + "5Wcq+6hgnWsQZ/bojERpUw==", + "l2ZB9TvT68rn8AAN4MdxWw==", + "h5HsEsObPuPFqREfynVblw==", + "fvm0IQfnbfZFETg9v3z/Fg==", + "QV0OG5bpjrjku4AzDvp9yw==", + "nMuMtK/Zkb3Xr34oFuX/Lg==", + "jMZKSMP2THqwpWqJNJRWdw==", + "fX4G68hFL7DmEmjbWlCBJQ==", + "ZlBNHAiYsfaEEiPQ1z+rCA==", + "ckugAisBNX18eQz+EnEjjw==", + "Dt6hvhPJu94CJpiyJ5uUkg==", + "eYE9No9sN5kUZ5ePEyS3+Q==", + "Tp52d1NndiC9w3crFqFm9g==", + "MBjMU/17AXBK0tqyARZP5w==", + "1EI9aa955ejNo1dJepcZJw==", + "FqWLkhWl0iiD/u2cp+XK9A==", + "j8nMH8mK/0Aae7ZkqyPgdg==", + "ZtmnX24AwYAXHb2ZDC6MeQ==", + "who8uUamlHWHXnBf7dwy4A==", + "CmkmWcMK4eqPBcRbdnQvhw==", + "61V74uIjaSfZM8au1dxr1A==", + "778O1hdVKHLG2q9dycUS0Q==", + "IdadoCPmSgHDHzn1zyf8Jw==", + "Z2rwGmVEMCY6nCfHO3qOzw==", + "Q3TpCE+wnmH/1h/EPWsBtQ==", + "HnVfyqgJ+1xSsN4deTXcIA==", + "XgPHx2+ULpm14IOZU2lrDg==", + "IbN736G1Px5bsYqE5gW1JQ==", + "nY/H7vThZ+dDxoPRyql+Cg==", + "wlWxtQDJ+siGhN2fJn3qtw==", + "MrbEUlTagbesBNg0OemHpw==", + "LJtRcR70ug6UHiuqbT6NGw==", + "hSNZWNKUtDtMo6otkXA/DA==", + "LawT9ZygiVtBk0XJ+KkQgQ==", + "DLzHkTjjuH6LpWHo2ITD0Q==", + "i8XXN7jcrmhnrOVDV8a2Hw==", + "ogcuGHUZJkmv+vCz567a2g==", + "rUp5Mfc57+A8Q29SPcvH/Q==", + "6706ncrH1OANFnaK6DUMqQ==", + "gK7dhke5ChQzlYc/bcIkcg==", + "t3Txxjq43e/CtQmfQTKwWg==", + "6ZMs9vCzK9lsbS6eyzZlIA==", + "uTHBqApdKOAgdwX3cjrCYQ==", + "zirOtGUXeRL22ezfotZfQg==", + "iK0dWKHjVVexuXvMWJV9pg==", + "uzEgwx1iAXAvWPKSVwYSeQ==", + "FHvI0IVNvih8tC7JgzvCOw==", + "jjNMPXbmpFNsCpWY0cv3eg==", + "/cJ0Nn5YbXeUpOHMfWXNHQ==", + "WkSJpxBa45XJRWWZFee7hw==", + "edlXkskLx287vOBZ9+gVYg==", + "+Pl0bSMBAdXpRIA+zE02JA==", + "3xw8+0/WU51Yz4TWIMK8mw==", + "GdTanUprpE3X/YjJDPpkhQ==", + "qnsBdl050y9cUaWxbCczRw==", + "pnJnBzAJlO4j3IRqcfmhkQ==", + "USq1iF90eUv41QBebs3bhw==", + "QH3lAwOYBAJ0Fd5pULAZqw==", + "gvvyX5ATi4q9NhnwxRxC8w==", + "7xDIG/80SnhgxAYPL9YJtg==", + "WVhfn2yJZ43qCTu0TVWJwA==", + "twjiDKJM7528oIu/el4Zbg==", + "6sBemZt4qY/TBwqk3YcLOQ==", + "m3XYojKO+I6PXlVRUQBC3w==", + "gUNP5w7ANJm257qjFxSJrA==", + "mMLhjdWNnZ8zts9q+a2v3g==", + "kjWYVC7Eok2w2YT4rrI+IA==", + "ZzT5b0dYQXkQHTXySpWEaA==", + "YzTV0esAxBFVls3e0qRsnA==", + "9xmtuClkFlpz/X5E9JBWBA==", + "nhAnHuCGXcYlqzOxrrEe1g==", + "cbBXgB1WQ/i8Xul0bYY2fg==", + "AkAes5oErTaJiGD2I4A1Pw==", + "Wx9jh/teM0LJHrvTScssyQ==", + "fU5ZZ1bIVsV+eXxOpGWo/Q==", + "k8eZxqwxiN/ievXdLSEL/w==", + "E2LR1aZ3DcdCBuVT7BhReA==", + "1eCHcz4swFH+uRhiilOinQ==", + "JipruVZx4ban3Zo5nNM37g==", + "IPLD9nT5EEYG9ioaSIYuuA==", + "pHozgRyMiEmyzThtJnY4MQ==", + "p0eNK7zJd7D/HEGaVOrtrQ==", + "dGjcKAOGBd4gIjJq7fL+qQ==", + "uMq8cDVWFD+tpn8aeP8Pqg==", + "gC7gUwGumN7GNlWwfIOjJQ==", + "It+K/RCYMOfNrDZxo7lbcA==", + "4CfEP8TeMKX33ktwgifGgA==", + "nxDGRpePV3H4NChn4eLwag==", + "300hoYyMR/mk1mfWJxS8/w==", + "DmxgZsQg+Qy1GP0fPkW3VA==", + "1vqRt79ukuvdJNyIlIag8Q==", + "RWI0HfpP7643OSEZR8kxzw==", + "zZtYkKU50PPEj6qSbO5/Sw==", + "UNRlg6+CYVOt68NwgufGNA==", + "kkbX+a00dfiTgbMI+aJpMg==", + "VIC7inSiqzM6v9VqtXDyCw==", + "l+x2QhxG8wb5AQbcRxXlmA==", + "GUiinC3vgBjbQC2ybMrMNQ==", + "6uMF5i0b/xsk55DlPumT7A==", + "aK9nybtiIBUvxgs1iQFgsw==", + "BLbTFLSb4mkxMaq4/B2khg==", + "mTAqtg6oi0iytHQCaSVUsA==", + "eBapvE+hdyFTsZ0y5yrahg==", + "lHN2dn2cUKJ8ocVL3vEhUQ==", + "Mj87ajJ/yR41XwAbFzJbcA==", + "FA+nK6mpFWdD0kLFcEdhxA==", + "FrTgaF5YZCNkyfR1kVzTLQ==", + "5eHStFN7wEmIE+uuRwIlPQ==", + "AyWlT+EGzIXc395zTlEU5Q==", + "I+wVQA+jpPTJ6xEsAlYucg==", + "Y1flEyZZAYxauMo4cmtJ1w==", + "1AeReq55UQotRQVKJ66pmg==", + "xzGzN5Hhbh0m/KezjNvXbQ==", + "meHzY9dIF7llDpFQo1gyMg==", + "RnOXOygwJFqrD+DlM3R5Ew==", + "JKg64m6mU7C/CkTwVn4ASg==", + "gGLz3Ss+amU7y6JF09jq7A==", + "Pu9pEf+Tek3J+3jmQNqrKw==", + "EATnlYm0p3h04cLAL95JgA==", + "o64LDtKq/Fulf1PkVfFcyg==", + "hUWqqG1QwYgGC5uXJpCvJw==", + "RfSwpO/ywQx4lfgeYlBr2w==", + "VaJc9vtYlqJbRPGb5Tf0ow==", + "9JKIJrlQjhNSC46H3Cstcw==", + "6Z9myGCF5ylWljgIYAmhqw==", + "9bAWYElyRN1oJ6eJwPtCtQ==", + "ohK6EftXOqBzIMI+5XnESw==", + "AVjwqrTBQH1VREuBlOyUOg==", + "G2UponGde3/Z+9b2m9abpQ==", + "DoiItHSms0B9gYmunVbRkQ==", + "vUC0HlTTHj6qNHwfviDtAw==", + "hq35Fjgvrcx6I9e6egWS4w==", + "sw+bmpzqsM4gEQtnqocQLQ==", + "ApiuEPWr8UjuRyJjsYZQBw==", + "VXu4ARjq7DS2IR/gT24Pfw==", + "3TbRZtFtsh9ez8hqZuTDeA==", + "CazLJMJjQMeHhYLwXW7YNg==", + "ROSt+NlEoiPFtpRqKtDUrQ==", + "IUwVHH6+8/0c+nOrjclOWA==", + "lkzFdvtBx5bV6xZO0cxK7g==", + "4ekt4m38G9m599xJCmhlug==", + "fzkmVWKhJsxyCwiqB/ULnQ==", + "LZAKplVoNjeQgfaHqkyEJA==", + "91vfsZ7Lx9x5gqWTOdM4sg==", + "MVoxyIA+emaulH8Oks8Weg==", + "oGH7SMLI2/qjd9Vnhi3s0A==", + "vmqfGJE6r4yDahtU/HLrxw==", + "Y5KKN7t/v9JSxG/m1GMPSA==", + "gXlb7bbRqHXusTE5deolGA==", + "/2c4oNniwhL3z5IOngfggg==", + "HgIFX42oUdRPu7sKAXhNWg==", + "A3dX2ShyL9+WOi6MNJBoYQ==", + "hN9bmMHfmnVBVr+7Ibd2Ng==", + "DB706G73NpBSRS8TKQOVZw==", + "JSyq2MIuObPnEgEUDyALjQ==", + "kSUectNPXpXNg+tIveTFRw==", + "XVVy3e6dTnO3HpgD6BtwQw==", + "td7nDgTDmKPSODRusMcupw==", + "Lt/pVD4TFRoiikmgAxEWEw==", + "mmRob7iyTkTLDu8ObmTPow==", + "Fd0c8f2eykUp9GYhqOcKoA==", + "18RKixTv12q3xoBLz6eKiA==", + "RClzwwKh51rbB4ekl99EZA==", + "oONlXCW4aAqGczQ/bUllBw==", + "foPAmiABJ3IXBoed2EgQXA==", + "wEJDulZafLuXCvcqBYioFQ==", + "K1RgR6HR5uDEQgZ32TAFgA==", + "SEIZhyguLoyH7So0p1KY0A==", + "ggIfX1J4dX3xQoHnHUI7VA==", + "HBRzLacCVYfwUVGzrefZYg==", + "aWZRql2IUPVe9hS3dxgVfQ==", + "Err1mbWJud80JNsDEmXcYg==", + "Z2MkqmpQXdlctCTCUDPyzw==", + "JnE6BK0vpWIhNkaeaYNUzw==", + "5dUry23poD+0wxZ3hH6WmA==", + "DwP0MQf71VsqvAbAMtC3QQ==", + "kHcBZXoxnFJ+GMwBZ/xhfQ==", + "SUAwMWLMml8uGqagz5oqhQ==", + "79uTykH43voFC3XhHHUzKg==", + "P5fucOJhtcRIoElFJS4ffg==", + "s8NpalwgPdHPla7Zi9FJ3w==", + "8cXqZub6rjgJXmh1CYJBOg==", + "tY916jrSySzrL+YTcVmYKQ==", + "DRiFNojs7wM8sfkWcmLnhQ==", + "wqUJ1Gq1Yz2cXFkbcCmzHQ==", + "0u+0WHr7WI6IlVBBgiRi6w==", + "GCYI9Dn1h3gOuueKc7pdKA==", + "nVDxVhaa2o38gd1XJgE3aw==", + "5I/heFSQG/UpWGx0uhAqGQ==", + "1PvTn90xwZJPoVfyT5/uIQ==", + "jHOoSl3ldFYr9YErEBnD3w==", + "swJhrPwllq5JORWiP5EkDA==", + "tj2rWvF2Fl+XIccctj8Mhw==", + "QvYZxsLdu+3nV/WhY1DsYg==", + "fKalNdhsyxTt1w08bv9fJA==", + "CHLHizLruvCrVi9chj9sXA==", + "sa2DECaqYH1z1/AFhpHi+g==", + "LbPp1oL0t3K2BAlIN+l8DA==", + "5SbwLDNT6sBOy6nONtUcTg==", + "AfVPdxD3FyfwwNrQnVNQ7A==", + "jt9Ocr9D8EwGRgrXVz//aQ==", + "KkwQL0DeUM3nPFfHb2ej+A==", + "WwraoO97OTalvavjUsqhxQ==", + "fAKFfwlCOyhtdBK6yNnsNg==", + "EqMlrz1to7HG4GIFTPaehQ==", + "YmjZJyNfHN5FaTL/HAm8ww==", + "L2D7G0btrwxl9V4dP3XM5Q==", + "oUqO4HrBvkpSL781qAC9+w==", + "c6Yhwy/q3j7skXq52l36Ww==", + "FWphIPZMumqnXr1glnbK4w==", + "AcKwfS8FRVqb72uSkDNY/Q==", + "uSIiF1r9F18avZczmlEuMQ==", + "XrFDomoH2qFjQ2jJ2yp9lA==", + "N2X7KWekNN+fMmwyXgKD5w==", + "IdmcpJXyVDajzeiGZixhSA==", + "Wf2olJCYZRGTTZxZoBePuQ==", + "oVlG+0rjrg2tdFImxIeVBA==", + "7w4PDRJxptG8HMe/ijL6cQ==", + "rueNryrchijjmWaA3kljYg==", + "ZybIEGf1Rn/26vlHmuMxhw==", + "yYVW07lOZHdgtX42xJONIA==", + "4ifNsmjYf1iOn2YpMfzihg==", + "KTjwL+qswa+Bid8xLdjMTg==", + "THfzE2G2NVKKfO+A2TjeFw==", + "QoqHzpHDHTwQD5UF30NruQ==", + "dTMoNd6DDr1Tu8tuZWLudw==", + "wOc4TbwQGUwOC1B3BEZ4OQ==", + "gfhkPuMvjoC3CGcnOvki3Q==", + "vljJciS+uuIvL7XXm5688g==", + "EGLOaMe6Nvzs/cmb7pNpbg==", + "oLWWIn/2AbKRHnddr2og9g==", + "7l0RMKbONGS/goW/M+gnMQ==", + "eFkXKRd2dwu/KWI5ZFpEzw==", + "jWsC7kdp2YmIZpfXGUimiA==", + "Jcxjli2tcIAjCe+5LyvqdQ==", + "MUkRa/PjeWMhbCTq43g6Aw==", + "g2nh2xENCFOpHZfdEXnoQA==", + "x6M66krXSi0EhppwmDmsxA==", + "26Wmdp6SkKN74W0/XPcnmA==", + "ycjv4XkS5O7zcF3sqq9MwQ==", + "gfnbviaVhKvv1UvlRGznww==", + "aIPde9CtyZrhbHLK740bfw==", + "0p8YbEMxeb73HbAfvPLQRw==", + "Is3uxoSNqoIo5I15z6Z2UQ==", + "NZtcY8fIpSKPso/KA6ZfzA==", + "iQ304I1hmLZktA1d1cuOJA==", + "0QB0OUW5x2JLHfrtmpZQ+w==", + "kgyUtd8MFe0tuuxDEUZA9w==", + "AcbG0e6xN8pZfYAv7QJe1Q==", + "bb/U8UynPHwczew/hxLQxw==", + "NuBYjwlxadAH+vLWYRZ3bg==", + "Ao1Zc0h5AdSHtYt1caWZnQ==", + "FL/j3GJBuXdAo54JYiWklQ==", + "E2v8Kk60qVpQ232YzjS2ow==", + "zVupSPz7cD0v/mD/eUIIjg==", + "sEeblUmISi1HK4omrWuPTA==", + "xQpYjaAmrQudWgsdu24J0A==", + "vCekQ2nOQKiN/q8Be/qwZg==", + "8g08gjG/QtvAYer32xgNAg==", + "miiOqnhtef1ODjFzMHnxjA==", + "sXlFMSTBFnq0STHj6cS/8w==", + "+SclwwY8R2RPrnX54Z+A6w==", + "g8TcogVxHpw7uhgNFt5VCQ==", + "9viAzLFGYYudBYFu7kFamg==", + "BAJ+/jbk2HyobezZyB9LiQ==", + "/DJgKE9ouibewuZ2QEnk6w==", + "fxg/vQq9WPpmQsqQ4RFYaA==", + "lM/EhwTsbivA7MDecaVTPw==", + "pVgjGg4TeTNhKimyOu3AAw==", + "gYnznEt9r97haD/j2Cko7g==", + "/ngbFuKIAVpdSwsA3VxvNw==", + "VCL3xfPVCL5RjihQM59fgg==", + "eDWsx4isnr2xPveBOGc7Hw==", + "FIOCTEbzb2+KMCnEdJ7jZw==", + "40HzgVKYnqIb6NJhpSIF0A==", + "ccK42Lm8Tsv73YMVZRwL6A==", + "MpAwWMt7bcs4eL7hCSLudQ==", + "zxsSqovedB3HT99jVblCnQ==", + "4erEA42TqGA9K4iFKkxMMA==", + "BaRwTrc5ulyKbW4+QqD0dw==", + "CT3ldhWpS1SEEmPtjejR/Q==", + "lkl6XkrTMUpXi46dPxTPxg==", + "3EhLkC9NqD3A6ApV6idmgg==", + "fsW2DaKYTCC7gswCT+ByQQ==", + "pW4gDKtVLj48gNz6V17QdA==", + "KjfL7YyVqmCJGBGDFdJ0gw==", + "bGGUhiG9SqJMHQWitXTcYQ==", + "8RtLlzkGEiisy1v9Xo0sbw==", + "R81DX/5a7DYKkS4CU+TL+w==", + "Tu6w6DtX2RJJ3Ym3o3QAWw==", + "nx/U4Tode5ILux4DSR+QMg==", + "mjQS8CpyGnsZIDOIEdYUxg==", + "wJpepvmtQQ3sz3tVFDnFqw==", + "a4rPqbDWiMivVzaRxvAj7g==", + "6o5g9JfKLKQ2vBPqKs6kjg==", + "UzPPFSXgeV7KW4CN5GIQXA==", + "NdVyHoTbBhX6Umz/9vbi0g==", + "Fzuq+Wg7clo6DTujNrxsSA==", + "XXFr0WUuGsH5nXPas7hR3Q==", + "JVSLiwurnCelNBiG2nflpQ==", + "NiawWuMBDo0Q3P2xK/vnLQ==", + "nNaGqigseHw30DaAhjBU3g==", + "+edqJYGvcy1AH2mEjJtSIg==", + "1WIi4I62GqkjDXOYqHWJfQ==", + "rwplpbNJz0ADUHTmzAj15Q==", + "iWNlSnwrtCmVF89B+DZqOQ==", + "tHDbi43e6k6uBgO0hA+Uiw==", + "fHNpW230mNib08aB7IM3XQ==", + "OChiB4BzcRE8Qxilu6TgJg==", + "d+ctfXU0j07rpRRzb5/HDA==", + "GDMqfhPQN0PxfJPnK1Bb9A==", + "bLd38ZNkVeuhf0joEAxnBQ==", + "nvUKoKfC6j8fz3gEDQrc/w==", + "fhcbn9xE/6zobqQ2niSBgA==", + "HGxe+5/kkh6R9GXzEOOFHA==", + "mPwCyD0yrIDonVi+fhXyEQ==", + "5PfGtbH9fmVuNnq83xIIgQ==", + "XePy/hhnQwHXFeXUQQ55Vg==", + "yfAaL0MMtSXPQ37pBdmHxQ==", + "NiQ/m4DZXUbpca9aZdzWAw==", + "uT6WRh5UpVdeABssoP2VTg==", + "oxoZP897lgMg/KLcZAtkAg==", + "oKt57TPe4PogmsGssc3Cbg==", + "RxmdoO8ak8y/HzMSIm+yBQ==", + "6leyDVmC5jglAa98NQ3+Hg==", + "+QosBAnSM2h4lsKuBlqEZw==", + "hy303iin+Wm7JA6MeelwiQ==", + "m9iuy4UtsjmyPzy6FTTZvw==", + "f6Ye5F0Lkn34uLVDCzogFQ==", + "iGykaF+h4p46HhrWqL8Ffg==", + "LPYFDbTEp5nGtG6uO8epSw==", + "t2vWMIh2BvfDSQaz5T1TZw==", + "OONAvFS/kmH7+vPhAGTNSg==", + "g/z9yk94XaeBRFj4hqPzdw==", + "2wesXiib76wM9sqRZ7JYwQ==", + "n7h9v2N1gOcvMuBEf8uThw==", + "ITYL3tDwddEdWSD6J6ULaA==", + "inrUwXyKikpOW0y2Kl1wGw==", + "iwKBOGDTFzV4aXgDGfyUkw==", + "+fcjH2kZKNj8quOytUk4nQ==", + "Srl4HivgHMxMOUHyM3jvNw==", + "qngzBJbiTB4fivrdnE5gOg==", + "G0MlFNCbRjXk4ekcPO/chQ==", + "t+bYn9UqrzKiuxAYGF7RLA==", + "RVD3Ij6sRwwxTUDAxwELtA==", + "RNdyt6ZRGvwYG5Ws3QTuEA==", + "9DRHdyX8ECKHUoEsGuqR4Q==", + "oMJLQTH1wW7LvOV0KRx/dw==", + "bjLZ7ot/X/vWSVx4EYwMCg==", + "+p8pofUlwn8vV6Rp6+sz9g==", + "cchuqe+CWCJpoakjHLvUfA==", + "NvurnIHin4O+wNP7MnrZ1w==", + "RBMv0IxXEO3o7MnV47Bzow==", + "xTizUioizbMQxD0T6fy/EQ==", + "ZCdad3AwhVArttapWFwT/Q==", + "Hy1nqC40l5ItxumkIC2LAA==", + "W/5ThNLu43uT1O+fg0Fzwg==", + "b3BQG9/9qDNC/bNSTBY/sQ==", + "neQoa8pvETr07blVMN3pgA==", + "oR8rvIZoeoaZ/ufpo0htfQ==", + "zEzWZ6l7EKoVUxvk/l78Mw==", + "IHyIeMad23fSDisblwyfpA==", + "m6srF+pMehggHB1tdoxlPg==", + "kggaIvN2tlbZdZRI8S5Apw==", + "2RFaMPlSbVuoEqKXgkIa5A==", + "//eHwmDOQRSrv+k9C/k3ZQ==", + "X/Gha4Ajjm/GStp/tv+Jvw==", + "+H0Rglt/HnhZwdty2hsDHg==", + "a1aL8zQ+ie3YPogE3hyFFg==", + "HxEU37uBMeiR5y8q/pM42g==", + "68nqDtXOuxF7DSw6muEZvg==", + "s5+78jS4hQYrFtxqTW3g1Q==", + "drfODfDI6GyMW7hzkmzQvA==", + "pT1raq2fChffFSIBX3fRiA==", + "sfowXUMdN2mCoBVrUzulZg==", + "AV/YJfdoDUdRcrXVwinhQg==", + "3AKEYQqpkfW7CZMFQZoxOw==", + "PHwJ5ZAqqftZ4ypr8H1qiQ==", + "AoN/pnK4KEUaGw4V9SFjpg==", + "soBA65OmZdfBGJkBmY/4Iw==", + "mSstwJq7IkJ0JBJ5T8xDKg==", + "h13Xuonj+0dD1xH86IhSyQ==", + "HK9xG03FjgCy8vSR+hx8+Q==", + "oFanDWdePmmZN0xqwpUukA==", + "zCRZgVsHbQZcVMHd9pGD3A==", + "EvSB+rCggob2RBeXyDQRvQ==", + "tXuu7YpZOuMLTv87NjKerA==", + "DJ+a37tCaGF5OgUhG+T0NA==", + "KkXlgPJPen6HLxbNn5llBw==", + "2W6lz1Z7PhkvObEAg2XKJw==", + "n+xYzfKmMoB3lWkdZ+D3rg==", + "CPDs+We/1wvsGdaiqxzeCQ==", + "2Wvk/kouEEOY0evUkQLhOQ==", + "ezsm4aFd6+DO9FUxz0A8Pg==", + "9sYLg75/hudZaBA3FrzKHw==", + "Pp1ZMxJ8yajdbfKM4HAQxA==", + "xiyRfVG0EfBA+rCk+tgWRQ==", + "/IarsLzJB8bf0AupJJ+/Eg==", + "LJeLdqmriyAQp+QjZGFkdQ==", + "IhHyHbHGyQS+VawxteLP0w==", + "nGzPc0kI/EduVjiK7bzM6Q==", + "m06wctjNc3o7iyBHDMZs2w==", + "mSJF9dJnxZ15lTC6ilbJ2A==", + "xdmY+qyoxxuRZa9kuNpDEg==", + "oNOI17POQCAkDwj6lJsYOA==", + "p73gSu4d+4T/ZNNkIv9Nlw==", + "vOJ55zFdgPPauPyFYBf01w==", + "4A+RHIw+aDzw0rSRYfbc7g==", + "/gi3UZmunVOIXhZSktZ8zQ==", + "a6vem8n6WmRZAalDrHNP0g==", + "kGeXrHEN6o7h5qJYcThCPw==", + "wrewZ0hoHODf7qmoGcOd7g==", + "Z0sjccxzKylgEiPCFBqPSA==", + "LKyOFgUKKGUU/PxpFYMILw==", + "L2RofFWDO0fVgSz4D2mtdw==", + "KI7tQFYW38zYHOzkKp9/lQ==", + "ewe/P3pJLYu/kMb5tpvVog==", + "IADk81pIu8NIL/+9Fi94pA==", + "0L0FVcH5Dlj3oL8+e9Na7g==", + "tdiTXKrkqxstDasT0D5BPA==", + "R906Kxp2VFVR3VD+o6Vxcw==", + "wc+8ohFWgOF4VlSYiZIGwQ==", + "wJKFMqh6MGctWfasjHrPEg==", + "UHpge5Bldt9oPGo2oxnYvQ==", + "vX7RIhatQeXAMr1+OjzhZw==", + "s2AKVTwrY65/SWqQxDGJQg==", + "Q4bfQslDSqU64MOQbBQEUw==", + "mVT74Eht+gAowINoMKV7IQ==", + "EuGWtIbyKToOe6DN3NkVpQ==", + "ALlGgVDO8So71ccX0D6u2g==", + "Rww3qkF3kWSd+AaMT0kfdw==", + "hlvtFGW8r0PkbUAYXEM+Hw==", + "Oc3BqTF3ZBW3xE0QsnFn/A==", + "3j0kFUZ6g+yeeEljx+WXGg==", + "8BLkvEkfnOizJq0OTCYGzw==", + "Lqel4GdU0ZkfoJVXI5WC/Q==", + "rvE64KQGkVkbl07y7JwBqw==", + "HbXv8InyZqFT7i3VrllBgg==", + "zwQ/3MzTJ9rfBmrANIh14w==", + "gglLMohmJDPRGMY1XKndjQ==", + "lyfqic/AbEJbCiw+wA01FA==", + "XqUO7ULEYhDOuT/I2J8BOA==", + "wPhJcp7U7IVX83szbIOOxQ==", + "1gA65t5FiBTEgMELTQFUPQ==", + "ll2M0QQzBsj5OFi02fv3Yg==", + "wt+qDLU38kzNU75ZYi3Hbw==", + "a4EYNljinYTx9vb1VvUA6A==", + "T6LA+daQqRI38iDKZTdg1A==", + "gwyVIrTk5o0YMKQq4lpJ+Q==", + "bPRX2zl+K1S0iWAWUn1DZw==", + "KQw25X4LnQ9is+qdqfxo0w==", + "6tfM6dx3R5TiVKaqYQjnCg==", + "OlwHO6Sg2zIwsCOCRu0HiQ==", + "mr1qjhliRfl87wPOrJbFQg==", + "8c+lvG5sZNimvx9NKNH3ug==", + "5Nk2Z94DhlIdfG5HNgvBbQ==", + "F50iXjRo1aSTr37GQQXuJA==", + "tfgO55QqUyayjDfQh+Zo1Q==", + "h7Fc+eT/GuC8iWI+YTD0UQ==", + "3TjntNWtpG7VqBt3729L6Q==", + "+DWs0vvFGt6d3mzdcsdsyA==", + "VJt2kPVBLEBpGpgvuv1oUw==", + "XLq/nWX8lQqjxsK9jlCqUg==", + "9s3ar9q32Y5A3tla5GW/2Q==", + "51yLpfEdvqXmtB6+q27/AQ==", + "AiMtfedwGcddA+XYNc+21g==", + "p/48hurJ1kh2FFPpyChzJg==", + "CRiL6zpjfznhGXhCIbz8pQ==", + "/jDVt9dRIn+o4IQ1DPwbsg==", + "UNdKik7Vy23LjjPzEdzNsg==", + "Koiog/hpN7ew5kgJbty34A==", + "4itEKfbRCJvqlgKnyEdIOQ==", + "zi04Yc01ZheuFAQc59E45A==", + "etRjRvfL/IwceY/IJ1tgzQ==", + "3sNJJIx1NnjYcgJhjOLJOg==", + "4yVqq66iHYQjiTSxGgX2oA==", + "Q8RVI/kRbKuXa8HAQD7zUA==", + "OERGn45uzfDfglzFFn6JAg==", + "JGEy6VP3sz3LHiyT2UwNHQ==", + "1zDfWw5LdG20ClNP1HYxgw==", + "TGB+FIzzKnouLh5bAiVOQg==", + "n5GA+pA9mO/f4RN9NL9lNg==", + "bUxQBaqKyvlSHcuRL9whjg==", + "tOdlnsE3L3XCBDJRmb/OqA==", + "XdkxmYYooeDKzy7PXVigBQ==", + "PMvG4NqJP76kMRAup6TSZA==", + "qpFJZqzkklby+u1UT3c1iA==", + "fW3QZyq5UixIA1mP6eWgqQ==", + "9nMltdrrBmM5ESBY2FRjGA==", + "1Vtrv6QUAfiYQjlLTpNovg==", + "ur9JDCVNwzSH4q4ngDlHNQ==", + "4u3eyKc+y3uRnkASrgBVUw==", + "XddlSluOH6VkR7spFIFmdQ==", + "NOmu8oZc6CcKLu+Wfz2YOQ==", + "3Ejtsqw3Iep/UQd0tXnSlg==", + "y/e3HSdg7T19FanRpJ7+7Q==", + "YodhkayN5wsgPZEYN7/KNA==", + "pZfn6IiG+V28fN8E2hawDQ==", + "jGHMJqbj6X1NdTDyWmXYAQ==", + "olTSlmirL9MFhKORiOKYkQ==", + "CrJDgdfzOea2M2hVedTrIg==", + "fpXijBOM3Ai1RkmHven5Ww==", + "eLYKLr4labZeLiRrDJ9mnA==", + "9vmJUS7WIVOlhMqwipAknQ==", + "G7J/za99BFbAZH+Q+/B8WA==", + "Hb+pdSavvJ9lUXkSVZW8Og==", + "gTB2zM3RPm27mUQRXc/YRg==", + "e5KCqQ/1GAyVMRNgQpYf6g==", + "1ApqwW7pE+XUB2Cs2M6y7g==", + "/wiA2ltAuWyBhIvQAYBTQw==", + "HFCQEiZf7/SNc+oNSkkwlA==", + "JFi6N1PlrpKaYECOnI7GFg==", + "E4ojRDwGsIiyuxBuXHsKBA==", + "+25t/2lo0FUEtWYK8LdQZQ==", + "up2MVDi9ve+s83/nwNtZ7Q==", + "cXpfd6Io6Glj2/QzrDMCvA==", + "DCvI9byhw0wOFwF1uP6xIQ==", + "PibGJQNw7VHPTgqeCzGUGA==", + "0ZRGz+oj2infCAkuKKuHiQ==", + "2QS/6OBA1T01NlIbfkTYJg==", + "P14k+fyz0TG9yIPdojp52w==", + "g5EzTJ0KA4sO3+Opss3LMg==", + "R5oOM58zdbVxFSDQnNWqeA==", + "Vg2E5qEDfC+QxZTZDCu9yQ==", + "YPgMthbpcBN2CMkugV60hQ==", + "gZWTFt5CuLqMz6OhWL+hqQ==", + "YrEP9z2WPQ8l7TY1qWncDA==", + "7p4NpnoNSQR7ISg+w+4yFg==", + "9L6yLO93sRN70+3qq3ObfA==", + "QH36wzyIhh6I56Vnx79hRA==", + "9DtM1vls4rFTdrSnQ7uWXw==", + "ZlOAnCLV1PkR0kb3E+Nfuw==", + "9UhKmKtr4vMzXTEn74BEhg==", + "Ndx5LDiVyyTz/Fh3oBTgvA==", + "mXZ4JeBwT2WJQL4a/Tm4jQ==", + "N9nD7BGEM7LDwWIMDB+rEQ==", + "dmAfbd9F0OJHRAhNMEkRsA==", + "jV/D2B11NLXZRH77sG9lBw==", + "1C50kisi9nvyVJNfq2hOEQ==", + "NMbAjbnuK7EkVeY3CQI5VA==", + "J1nYqJ7tIQK1+a/3sMXI/Q==", + "m416yrrAlv+YPClGvGh+qQ==", + "rLZII1R6EGus+tYCiUtm6g==", + "xktOghh1S9nIX6fXWnT+Ug==", + "FcFcn4qmPse5mJCX5yNlsA==", + "xAAipGfHTGTjp9Qk1MR8RQ==", + "RQOlmzHwQKFpafKPJj0D8w==", + "WRjYdKdtnd1G9e/vFXCt0g==", + "z0BU//aSjYHAkGGk3ZSGNg==", + "M55eersiJuN9v61r8DoAjQ==", + "l2mAbuFF3QBIUILDODiUHQ==", + "IhpXs1TK7itQ3uTzZPRP5Q==", + "t2EkpUsLOEOsrnep0nZSmA==", + "lMaO8Yf+6YNowGyhDkPhQA==", + "UbSFw5jtyLk5MealqJw++A==", + "5u2PdDcIY3RQgtchSGDCGg==", + "MQYM3BT77i35LG9HcqxY2Q==", + "8AfCSZC0uasVON9Y/0P2Pw==", + "evaWFoxZNQcRszIRnxqB+A==", + "+8PiQt6O7pJI/nIvQpDaAg==", + "eRwaYiog2DdlGQyaltCMJg==", + "JyUJEnU6hJu8x2NCnGrYFw==", + "l0E0U/CJsyCVSTsXW4Fp+w==", + "XV13yK0QypJXmgI+dj4KYw==", + "jrRH0aTUYCOpPLZwzwPRfQ==", + "N3YDSkBUqSmrmNvZZx4a1Q==", + "0yJ7TQYzcp3DXVSvwavr+w==", + "rhgtLQh0F9bRA6IllM7AGw==", + "IWZnTJ3Hb9qw9HAK/M9gTw==", + "izeyFvXOumNgVyLrbKW45g==", + "xYD8jrCDmuQna+p1ebnKDQ==", + "SOdpdrk2ayeyv0xWdNuy9g==", + "HYylUirJRqLm+dkp39fSOQ==", + "q4z6A4l3nhX3smTmXr+Sig==", + "Zyo0fzewcqXiKe2mAwKx5g==", + "LMEtzh0+J27+4zORfcjITw==", + "LoUv/f2lcWpjftzpdivMww==", + "mXBfDUt/sBW5OUZs2sihvw==", + "PggVPQL5YKqSU/1asihcrg==", + "mI0eT4Rlr7QerMIngcu/ng==", + "NmQrsmb8PVP05qnSulPe5Q==", + "TcyyXrSsQsnz0gJ36w4Dxw==", + "y4mfEDerrhaqApDdhP5vjA==", + "ynaj4XjU27b7XbqPyxI8Ig==", + "Ua6aO6HwM+rY4sPR19CNFA==", + "3go7bJ9WqH/PPUTjNP3q/Q==", + "n1ixvP7SfwYT3L2iWpJg6A==", + "W8y32OLHihfeV0XFw7LmOg==", + "uzkNhmo2d08tv5AmnyqkoQ==", + "hJ8leLNuJ6DK5V8scnDaZQ==", + "KodYHHN62zESrXUye7M01g==", + "H+yPRiooEh5J7lAJB4RZ7Q==", + "dZg5w8rFETMp9SgW7m0gfg==", + "LsmsPokAwWNCuC74MaqFCQ==", + "1QGhj9NONF2rC44UdO+Izw==", + "uwGivY3/C9WK+dirRPJZ4A==", + "rXGWY/Gq+ZEsmvBHUfFMmQ==", + "j4FBMnNfdBwx0VsDeTvhFg==", + "81nkjWtpBhqhvOp6K8dcWg==", + "dCDaYYrgASXPMGFRV0RCGg==", + "Kj1QI+s9261S3lTtPKd9eg==", + "LblwOqNiciHmt2NXjd89tg==", + "46piyANQVvvLqcoMq5G8tQ==", + "XJihma9zSRrXLC+T+VcFDA==", + "K3NBEG8jJTJbSrYSOC3FKw==", + "cT3PwwS6ALZA/na9NjtdzA==", + "wJ4uCrl4DPg70ltw1dZO3w==", + "JATLdpQm//SQnkyCfI5x7Q==", + "X1PaCfEDScclLtOTiF5JUw==", + "444F9T6Y7J67Y9sULG81qg==", + "8JVHFRwAd/SCLU0CRJYofg==", + "aLh1XEUrfR9W82gzusKcOg==", + "U+bB5NjFIuQr/Y5UpXHwxA==", + "Egs14xVbRWjfBBX7X5Z60g==", + "KSorNz/PLR/YYkxaj1fuqw==", + "RDgGGxTtcPvRg/5KRRlz4w==", + "5T39s5CtSrK5awMPUcEWJg==", + "+PUVXkoTqHxJHO18z4KMfw==", + "Bvk8NX4l6WktLcRDRKsK/A==", + "kNGIV3+jQmJlZDTXy1pnyA==", + "E3jMjAgXwvwR8PA53g4+PQ==", + "MbI04HlTGCoc/6WDejwtaQ==", + "aEnHUfn7UE/Euh6jsMuZ7g==", + "z4Bft++f72QeDh4PWGr/sw==", + "1lCcQWGDePPYco4vYrA5vw==", + "iu5csar0IQQBOTgw5OvJwQ==", + "raKMXnnX6PFFsbloDqyVzQ==", + "uPnL9tboMZo0Kl2fe24CmA==", + "8OFxXwnPmrogpNoueZlC4Q==", + "V6CRKrKezPwsRdbm0DJ2Yg==", + "xmGgK3W5y+oCd0K2u8XjZQ==", + "Ry3zgZ6KHrpNyb7+Tt2Pkw==", + "IwLbkL33z+LdTjaFYh93kg==", + "caepyBOAFu0MxbcXrGf6TA==", + "iIWxFdolLcnXqIjPMg+5kQ==", + "P430CeF2MDkuq11YdjvV8A==", + "yCu+DVU/ceMTOZ5h/7wQTg==", + "4mQVNv7FHj+/O6XFqWFt/Q==", + "OEJ40VmMDYzc2ESEMontRA==", + "D66Suu3tWBD+eurBpPXfjA==", + "RNK9G1hfuz3ETY/RmA9+aA==", + "BYpHADmEnzBsegdYTv8B5Q==", + "DBKrdpCE0awppxST4o/zzg==", + "KOmdvm+wJuZ/nT/o1+xOuw==", + "gDxqUdxxeXDYhJk9zcrNyA==", + "UPzS4LR3p/h0u69+7YemrQ==", + "hf9HFxWRNX2ucH8FLS7ytA==", + "ozVqYsmUueKifb4lDyVyrg==", + "TfHvdbl2M4deg65QKBTPng==", + "SzCGM8ypE58FLaR1+1ccxQ==", + "3nthUmLZ30HxQrzr2d7xFA==", + "1jBaRO8Bg5l6TH7qJ8EPiw==", + "eJlcN+gJnqAnctbWSIO9uA==", + "G8LFBop8u6IIng+gQuVg3w==", + "3JhnM6G4L06NHt31lR0zXA==", + "342VOUOxoLHUqtHANt83Hw==", + "hRxbdeniAVFgKUgB9Q3Y+g==", + "cFFE2R4GztNoftYkqalqUQ==", + "YmaksRzoU+OwlpiEaBDYaQ==", + "jon1y9yMEGfiIBjsDeeJdA==", + "oSnrpW4UmmVXtUGWqLq+tQ==", + "zaqyy3GaJ7cp8qDoLJWcTw==", + "luO1R8dUM9gy1E2lojRQoA==", + "YHM6NNHjmodv+G0mRLK7kw==", + "ZSmN8mmI9lDEHkJqBBg0Nw==", + "520wTzrysiRi2Td92Zq0HQ==", + "RAAw14BA1ws5Wu/rU7oegw==", + "vb6Agwzk4JG0Nn7qRPPFMQ==", + "joDXdLpXvRjOqkRiYaD/Sw==", + "dK2DU3t1ns+DWDwfBvH3SQ==", + "gZNJ1Qq6OcnwXqc+jXzMLQ==", + "R8ULpSNu9FcCwXZM0QedSg==", + "mc45FSMtzdw2PTcEBwHWPw==", + "d0qvm3bl38rRCpYdWqolCQ==", + "o9tdzmIu+3J/EYU4YWyTkA==", + "5eXpiczlRdmqMYSaodOUiQ==", + "KYuUNrkTvjUWQovw9dNakA==", + "02im2RooJQ/9UfUrh5LO+A==", + "kWPUUi7x9kKKa6nJ+FDR5Q==", + "6z8CRivao3IMyV4p4gMh7g==", + "SmRWEzqddY9ucGAP5jXjAg==", + "DJscTYNFPyPmTb57g/1w+Q==", + "uOHrw37yF9oLLVd16nUpeg==", + "HaIRV9SNPRTPDOSX9sK/bg==", + "K4yZNVoqHjXNhrZzz2gTew==", + "bTNRjJm+FfSQVfd56nNNqQ==", + "x5lyMArsv1MuJmEFlWCnNw==", + "cxpZ4bloGv734LBf4NpVhA==", + "kUudvRfA33uJDzHIShQd3Q==", + "3Wfj05vCLFAB9vII5AU9tw==", + "FUQySDFodnRhr+NUsWt0KA==", + "eC/RcoCVQBlXdE9WtcgXIw==", + "NoX8lkY+kd2GPuGjp+s0tQ==", + "EzjbinBHx3Wr08eXpH3HXA==", + "0VsaJHR0Ms8zegsCpAKoyg==", + "e2xLFVavnZIUUtxJx+qa1g==", + "Kt6BTG1zdeBZ3nlVk+BZKQ==", + "EUXQZwLgnDG+C8qxVoBNdw==", + "0SkC/4PtnX1bMYgD6r6CLA==", + "rzj6mjHCcMEouL66083BAg==", + "V5HEaY3v9agOhsbYOAZgJA==", + "tJt6VDdAPEemBUvnoc4viA==", + "g0lWrzEYMntVIahC7i0O2g==", + "zCpibjrZOA3FQ4lYt0WoVA==", + "4Xh/B3C16rrjbES+FM1W8g==", + "GHEdXgGWOeOa6RuPMF0xXg==", + "3kREs/qaMX0AwFXN0LO5ow==", + "GLDNTSwygNBmuFwCIm7HtA==", + "JBkbaBiorCtFq9M9lSUdMg==", + "rJCuanCy51ydVD4nInf9IQ==", + "OzFRv+PzPqTNmOnvZGoo5g==", + "7mxU5fJl/c6dXss9H3vGcQ==", + "9J53kk+InE3CKa7cPyCXMw==", + "x9TIZ9Ua++3BX+MpjgTuWA==", + "h0MH5NGFfChgmRJ3E/R3HQ==", + "25w3ZRUzCvJwAVHYCIO5uw==", + "1Wc8jQlDSB4Dp32wkL2odw==", + "ipPPjxpXHS1tcykXmrHPMQ==", + "r95wJtP5rsTExKMS7QhHcw==", + "TZT86wXfzFffjt0f95UF5w==", + "VpmBstwR7qPVqPgKYQTA3g==", + "3++dZXzZ6AFEz7hK+i5hww==", + "mAiD16zf+rCc7Qzxjd5buA==", + "1JI9bT92UzxI8txjhst9LQ==", + "TNyvLixb03aP2f8cDozzfA==", + "spHVvA/pc7nF9Q4ON020+w==", + "GA8k6GQ20DGduVoC+gieRA==", + "T7waQc3PvTFr0yWGKmFQdQ==", + "P0Pc8owrqt6spdf7FgBFSw==", + "DKApp/alXiaPSRNm3MfSuA==", + "UreSZCIdDgloih8KLeX7gg==", + "xJi0T+psHOXMivSOVpMWeQ==", + "cNsC9bH30eM1EZS6IdEdtQ==", + "XjjrIpsmATV/lyln4tPb+g==", + "qt5CsMts2aD4lw/4Q6bHYQ==", + "h+KRDKIvyVUBmRjv1LcCyg==", + "2j83jrPwPfYlpJJ2clEBYQ==", + "ZrCezGLz38xKmzAom6yCTQ==", + "SEGu+cSbeeeZg4xWwsSErQ==", + "Duz/8Ebbd0w6oHwOs0Wnwg==", + "Ci7sS7Yi1+IwAM3VMAB4ew==", + "DG2Qe2DqPs5MkZPOqX363Q==", + "v0Bvws1WYVoEgDt8xmVKew==", + "CtDj/h2Q/lRey20G8dzSgA==", + "WRoJMO0BCJyn5V6qnpUi4Q==", + "RQywrOLZEKw9+kG6qTzr3g==", + "mU4CqbAwpwqegxJaOz9ofQ==", + "aN5x46Gw1VihRalwCt1CGg==", + "U6VQghxOXsydh3Naa5Nz4A==", + "YA+zdEC+yEgFWRIgS1Eiqw==", + "oPcxgoismve6+jXyIKK6AQ==", + "PqLCd/pwc+q5GkL6MB0jTg==", + "fHL+fHtDxhALZFb9W/uHuw==", + "dhTevyxTYAuKbdLWhG47Kw==", + "VllbOAjeW3Dpbj5lp2OSmA==", + "3itfXtlLPRmPCSYaSvc39Q==", + "GNak/LFeoHWlTdLW1iU4eg==", + "HuDuxs2KiGqmeyY1s1PjpQ==", + "xs8J3cesq7lDhP/dNltqOw==", + "foXSDEUwMhfHWJSmSejsQg==", + "6fWom3YoKvW6NIg6y9o9CQ==", + "NhZbSq0CjDNOAIvBHBM9zA==", + "5w4FbRhWACP7k2WnNitiHg==", + "0UeRwDID2RBIikInqFI7uw==", + "/y/jHHEpUu5TR+R2o96kXA==", + "voO3krg4sdy4Iu+MZEr8+g==", + "hdzol5dk//Q6tCm4+OndIA==", + "Nc5kiwXCAyjpzt43G5RF1A==", + "3UBYBMejKInSbCHRoJJ7dg==", + "dRFCIbVu0Y8XbjG5i+UFCQ==", + "t8pjhdyNJirkvYgWIO/eKg==", + "FAXzjjIr8l1nsQFPpgxM/g==", + "SPGpjEJrpflv1hF0qsFlPw==", + "9Y1ZmfiHJd9vCiZ6KfO1xQ==", + "7Eqzyb+Kep+dIahYJWNNxQ==", + "9rL8nC/VbSqrvnUtH9WsxQ==", + "H4FZ5Wcnb40hQM1DMGGe8A==", + "AjoXWGb/l9xH/hscgEc6kQ==", + "6nzFl41uutgDdC30oOeCqg==", + "3jo1jRy3MybXtoLR+JIbJw==", + "mXdE08dv+OlIhlcqMBH2Gg==", + "Ifd7DI6o8N5gnyAKqZTlRw==", + "JNUvg/kxL3rdcZnD4IqUxw==", + "ry8B+sAHNeFIZHCCDynFyw==", + "TXaEd5lIKhzjcncfNcBgSg==", + "Mr3ehuDMUimOSn+FlkchdA==", + "cwiGhjmX9v8I7E/ekQ0h+g==", + "I/r5+1jnqumCPprKC/2BqA==", + "S4V3MfGYk8I4fd3WH09yYw==", + "A+crVyUeynAkEMYKbnFjZw==", + "vtyHcNQPcUTRuZcQvRUX4Q==", + "UNKx1ZVv3HNp21zrUSm6ew==", + "rsAlvGLv2D0swd6ol3WlvA==", + "2qwqb8ENAR2fpQnw55sPDw==", + "xBJJuYYnsTJOeFggZSKC4Q==", + "omvtZZKruPiEt6fV0YXTdg==", + "JZEgKUhUN+USJsvtF4HZOg==", + "euG/kpJ5elSDOGNbWWDfNQ==", + "DiiVmM6/WNcp0MUjSaFq6w==", + "QCNS8gAml1M2pJ+MxZsueg==", + "M6+pggFsHfM3alFxcMOFNQ==", + "YLoWpDTwXnszEQm8FA164Q==", + "N08oUZtlXbQvO9t3vXnGog==", + "jkjuJowWuOa4CLY+RZiErQ==", + "mPf+S+6oAoVIYEVveaiNFA==", + "R0iVyo5qreP/68uZlZphDA==", + "GYlqhQgp03B0mXpUhQ+ZCA==", + "lQNbmWD7PhwNGye+zbc3GQ==", + "cNeaOJEOzUSDdRmenPQyuw==", + "Gp66/Txv6ebv5bn85TuQtA==", + "xAda6DVkcvvqhI8vWZeGyA==", + "Ggk1Qa0lEdAgCXG6SmCkBA==", + "MYuO7ZURXtyaf56q7hH4Zw==", + "RUIdZRTgJBudWUZQFgiFaQ==", + "bgFJxLirUom2zT0h7LdOpw==", + "A2gaOpIlrS7TKVQgy9XMSw==", + "zevXp0lqqnXv9X6Bgmjtqg==", + "a5iuFqWAdFFsRgp7SFYwNg==", + "TxTy0TaDsWTcRH3wdBEQLQ==", + "jephVdKDeJIhXPrdMOJ4qA==", + "C4KdamfqUPuJ3RGFdpIEdw==", + "zl6l2Ioz1qovRUIWrSyxVA==", + "+gGaDxUe0UnNrf3PPg1qQQ==", + "1HgbrlaLMHS6Qj/0kkaJxg==", + "eGxTly6Pnu7eV/MKYMmuYw==", + "RAMKfnlrzNjpyh2BWt6JHg==", + "4pZQm9ogCZ/EAR9pjJm1eA==", + "l1zv3erwXIegQFd02NlCag==", + "uHGyRZchuA4ulmuD5LqquQ==", + "/vFu89tsV+lbcoiqM/XWog==", + "63SUgqfQimrmjvy/bEDQ0w==", + "JLHuf+FlChFDa9LYfTQ4Eg==", + "I+ZnPePTFX8ZODe14bxgyA==", + "CtoK1k3U82BkvzuPfQ4pjQ==", + "6nqQm4C7y+wZ+qX0kVjwmA==", + "+C3kBxRXIjqBk0EJxe3Xfg==", + "qVu748pIxEZtiywg4/4qhw==", + "07o+sKjjRCYkwy/ACyoYhg==", + "CiLF4dkbLURekBcQbwPUVA==", + "W/N5/nkp4iQIPYfAagVV7A==", + "3PJOphhEjw0E4arTfVVwdg==", + "YdMbARHwB+bSOd0PlTlXiA==", + "41hbx5Yr7UWxsV6+bWUYUA==", + "SqJHXD0MorNwHtHL9TbWLg==", + "pWKGUzm/muwOiBtzkRMnRg==", + "az9zZ7HTa4FJGRQMcamvEw==", + "zavAAN8C9Wo8oBLyztp63Q==", + "yBAnPmwrMJ8kpPP292S/Lw==", + "E6szQhjuUAz2e0h9ffQfEQ==", + "Fs3cQxQyS9kM4T8j5R7rWw==", + "GB5fRLZxnjRUfEe0SwcePQ==", + "+9OY8xkT9dM/rb2T6ACtOQ==", + "If2xFBD1p91iDD7ZrsfgjA==", + "QCFfoMhy8EleZAOpfRY88w==", + "NobWPk1Z6bHt5s9NHXt/pg==", + "nK6T4vV4384OIcqO5tQMhA==", + "Zov1EzK+VomiuwT1+ulQ8g==", + "pF98OKDvLUlnTzo7wmlpOw==", + "Wrq9YDsieAMC3Y2DSY5Rcg==" + ] +} diff --git a/application/basilisk/base/content/newtab/newTab.js b/application/basilisk/base/content/newtab/newTab.js new file mode 100644 index 000000000..bbd2ef39d --- /dev/null +++ b/application/basilisk/base/content/newtab/newTab.js @@ -0,0 +1,71 @@ +/* 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/. */ + +"use strict"; + +var Cu = Components.utils; +var Ci = Components.interfaces; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PageThumbs.jsm"); +Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm"); +Cu.import("resource:///modules/DirectoryLinksProvider.jsm"); +Cu.import("resource://gre/modules/NewTabUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Rect", + "resource://gre/modules/Geometry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +var { + links: gLinks, + allPages: gAllPages, + linkChecker: gLinkChecker, + pinnedLinks: gPinnedLinks, + blockedLinks: gBlockedLinks, + gridPrefs: gGridPrefs +} = NewTabUtils; + +XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() { + return Services.strings. + createBundle("chrome://browser/locale/newTab.properties"); +}); + +function newTabString(name, args) { + let stringName = "newtab." + name; + if (!args) { + return gStringBundle.GetStringFromName(stringName); + } + return gStringBundle.formatStringFromName(stringName, args, args.length); +} + +function inPrivateBrowsingMode() { + return PrivateBrowsingUtils.isContentWindowPrivate(window); +} + +const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; +const XUL_NAMESPACE = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + +const TILES_EXPLAIN_LINK = "https://support.mozilla.org/kb/how-do-tiles-work-firefox"; +const TILES_INTRO_LINK = "https://www.mozilla.org/firefox/tiles/"; +const TILES_PRIVACY_LINK = "https://www.mozilla.org/privacy/"; + +#include transformations.js +#include page.js +#include grid.js +#include cells.js +#include sites.js +#include drag.js +#include dragDataHelper.js +#include drop.js +#include dropTargetShim.js +#include dropPreview.js +#include updater.js +#include undo.js +#include search.js +#include customize.js + +// Everything is loaded. Initialize the New Tab Page. +gPage.init(); diff --git a/application/basilisk/base/content/newtab/newTab.xhtml b/application/basilisk/base/content/newtab/newTab.xhtml new file mode 100644 index 000000000..eef51b4b2 --- /dev/null +++ b/application/basilisk/base/content/newtab/newTab.xhtml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html [ + <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd"> + %newTabDTD; + <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> + %browserDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>&newtab.pageTitle;</title> + + <link rel="stylesheet" type="text/css" media="all" href="chrome://global/skin/" /> + <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/content/contentSearchUI.css" /> + <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/content/newtab/newTab.css" /> + <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/newtab/newTab.css" /> +</head> + +<body dir="&locale.dir;"> + <div id="newtab-customize-overlay"></div> + + <div class="newtab-customize-panel-container"> + <div id="newtab-customize-panel" orient="vertical"> + <div id="newtab-customize-panel-anchor"></div> + <div id="newtab-customize-panel-inner-wrapper"> + <div id="newtab-customize-title" class="newtab-customize-panel-item"> + <label>&newtab.customize.cog.title2;</label> + </div> + <div id="newtab-customize-classic" class="newtab-customize-panel-item selectable"> + <label>&newtab.customize.classic;</label> + </div> + <div id="newtab-customize-blank" class="newtab-customize-panel-item selectable"> + <label>&newtab.customize.blank2;</label> + </div> + <div id="newtab-customize-learn" class="newtab-customize-panel-item"> + <label>&newtab.customize.cog.learn;</label> + </div> + </div> + </div> + </div> + + <div id="newtab-vertical-margin"> + <div id="newtab-margin-top"/> + + <div id="newtab-margin-undo-container"> + <div id="newtab-undo-container" undo-disabled="true"> + <label id="newtab-undo-label">&newtab.undo.removedLabel;</label> + <button id="newtab-undo-button" tabindex="-1" + class="newtab-undo-button">&newtab.undo.undoButton;</button> + <button id="newtab-undo-restore-button" tabindex="-1" + class="newtab-undo-button">&newtab.undo.restoreButton;</button> + <button id="newtab-undo-close-button" tabindex="-1" title="&newtab.undo.closeTooltip;"/> + </div> + </div> + + <div id="newtab-search-container"> + <div id="newtab-search-form"> + <div id="newtab-search-icon"/> + <input type="text" name="q" value="" id="newtab-search-text" + aria-label="&contentSearchInput.label;" maxlength="256"/> + <input id="newtab-search-submit" type="button" + title="&contentSearchSubmit.tooltip;"/> + </div> + </div> + + <div id="newtab-horizontal-margin"> + <div class="newtab-side-margin"/> + <div id="newtab-grid"> + <h1 id="topsites-heading"/> + </div> + <div class="newtab-side-margin"/> + </div> + + <div id="newtab-margin-bottom"/> + </div> + <input id="newtab-customize-button" type="button" dir="&locale.dir;" + value="⚙" + title="&newtab.customize.title;"/> +</body> +<script type="text/javascript;version=1.8" src="chrome://browser/content/contentSearchUI.js"/> +<script type="text/javascript;version=1.8" src="chrome://browser/content/newtab/newTab.js"/> +</html> diff --git a/application/basilisk/base/content/newtab/page.js b/application/basilisk/base/content/newtab/page.js new file mode 100644 index 000000000..f7626ced2 --- /dev/null +++ b/application/basilisk/base/content/newtab/page.js @@ -0,0 +1,297 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +// The amount of time we wait while coalescing updates for hidden pages. +const SCHEDULE_UPDATE_TIMEOUT_MS = 1000; + +/** + * This singleton represents the whole 'New Tab Page' and takes care of + * initializing all its components. + */ +var gPage = { + /** + * Initializes the page. + */ + init: function Page_init() { + // Add ourselves to the list of pages to receive notifications. + gAllPages.register(this); + + // Listen for 'unload' to unregister this page. + addEventListener("unload", this, false); + + // XXX bug 991111 - Not all click events are correctly triggered when + // listening from xhtml nodes -- in particular middle clicks on sites, so + // listen from the xul window and filter then delegate + addEventListener("click", this, false); + + // Check if the new tab feature is enabled. + let enabled = gAllPages.enabled; + if (enabled) + this._init(); + + this._updateAttributes(enabled); + + // Initialize customize controls. + gCustomize.init(); + }, + + /** + * Listens for notifications specific to this page. + */ + observe: function Page_observe(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed") { + gCustomize.updateSelected(); + + let enabled = gAllPages.enabled; + this._updateAttributes(enabled); + + // Update thumbnails to the new enhanced setting + if (aData == "browser.newtabpage.enhanced") { + this.update(); + } + + // Initialize the whole page if we haven't done that, yet. + if (enabled) { + this._init(); + } else { + gUndoDialog.hide(); + } + } else if (aTopic == "page-thumbnail:create" && gGrid.ready) { + for (let site of gGrid.sites) { + if (site && site.url === aData) { + site.refreshThumbnail(); + } + } + } + }, + + /** + * Updates the page's grid right away for visible pages. If the page is + * currently hidden, i.e. in a background tab or in the preloader, then we + * batch multiple update requests and refresh the grid once after a short + * delay. Accepts a single parameter the specifies the reason for requesting + * a page update. The page may decide to delay or prevent a requested updated + * based on the given reason. + */ + update(reason = "") { + // Update immediately if we're visible. + if (!document.hidden) { + // Ignore updates where reason=links-changed as those signal that the + // provider's set of links changed. We don't want to update visible pages + // in that case, it is ok to wait until the user opens the next tab. + if (reason != "links-changed" && gGrid.ready) { + gGrid.refresh(); + } + + return; + } + + // Bail out if we scheduled before. + if (this._scheduleUpdateTimeout) { + return; + } + + this._scheduleUpdateTimeout = setTimeout(() => { + // Refresh if the grid is ready. + if (gGrid.ready) { + gGrid.refresh(); + } + + this._scheduleUpdateTimeout = null; + }, SCHEDULE_UPDATE_TIMEOUT_MS); + }, + + /** + * Internally initializes the page. This runs only when/if the feature + * is/gets enabled. + */ + _init: function Page_init() { + if (this._initialized) + return; + + this._initialized = true; + + // Set submit button label for when CSS background are disabled (e.g. + // high contrast mode). + document.getElementById("newtab-search-submit").value = + document.body.getAttribute("dir") == "ltr" ? "\u25B6" : "\u25C0"; + + if (Services.prefs.getBoolPref("browser.newtabpage.compact")) { + document.body.classList.add("compact"); + } + + // Initialize search. + gSearch.init(); + + if (document.hidden) { + addEventListener("visibilitychange", this); + } else { + setTimeout(() => this.onPageFirstVisible()); + } + + // Initialize and render the grid. + gGrid.init(); + + // Initialize the drop target shim. + gDropTargetShim.init(); + +#ifdef XP_MACOSX + // Workaround to prevent a delay on MacOSX due to a slow drop animation. + document.addEventListener("dragover", this, false); + document.addEventListener("drop", this, false); +#endif + }, + + /** + * Updates the 'page-disabled' attributes of the respective DOM nodes. + * @param aValue Whether the New Tab Page is enabled or not. + */ + _updateAttributes: function Page_updateAttributes(aValue) { + // Set the nodes' states. + let nodeSelector = "#newtab-grid, #newtab-search-container"; + for (let node of document.querySelectorAll(nodeSelector)) { + if (aValue) + node.removeAttribute("page-disabled"); + else + node.setAttribute("page-disabled", "true"); + } + + // Enables/disables the control and link elements. + let inputSelector = ".newtab-control, .newtab-link"; + for (let input of document.querySelectorAll(inputSelector)) { + if (aValue) + input.removeAttribute("tabindex"); + else + input.setAttribute("tabindex", "-1"); + } + }, + + /** + * Handles unload event + */ + _handleUnloadEvent: function Page_handleUnloadEvent() { + gAllPages.unregister(this); + // compute page life-span and send telemetry probe: using milli-seconds will leave + // many low buckets empty. Instead we use half-second precision to make low end + // of histogram linear and not lose the change in user attention + let delta = Math.round((Date.now() - this._firstVisibleTime) / 500); + if (this._suggestedTilePresent) { + Services.telemetry.getHistogramById("NEWTAB_PAGE_LIFE_SPAN_SUGGESTED").add(delta); + } + else { + Services.telemetry.getHistogramById("NEWTAB_PAGE_LIFE_SPAN").add(delta); + } + }, + + /** + * Handles all page events. + */ + handleEvent: function Page_handleEvent(aEvent) { + switch (aEvent.type) { + case "load": + this.onPageVisibleAndLoaded(); + break; + case "unload": + this._handleUnloadEvent(); + break; + case "click": + let {button, target} = aEvent; + // Go up ancestors until we find a Site or not + while (target) { + if (target.hasOwnProperty("_newtabSite")) { + target._newtabSite.onClick(aEvent); + break; + } + target = target.parentNode; + } + break; + case "dragover": + if (gDrag.isValid(aEvent) && gDrag.draggedSite) + aEvent.preventDefault(); + break; + case "drop": + if (gDrag.isValid(aEvent) && gDrag.draggedSite) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + } + break; + case "visibilitychange": + // Cancel any delayed updates for hidden pages now that we're visible. + if (this._scheduleUpdateTimeout) { + clearTimeout(this._scheduleUpdateTimeout); + this._scheduleUpdateTimeout = null; + + // An update was pending so force an update now. + this.update(); + } + + setTimeout(() => this.onPageFirstVisible()); + removeEventListener("visibilitychange", this); + break; + } + }, + + onPageFirstVisible: function () { + // Record another page impression. + Services.telemetry.getHistogramById("NEWTAB_PAGE_SHOWN").add(true); + + for (let site of gGrid.sites) { + if (site) { + // The site may need to modify and/or re-render itself if + // something changed after newtab was created by preloader. + // For example, the suggested tile endTime may have passed. + site.onFirstVisible(); + } + } + + // save timestamp to compute page life-span delta + this._firstVisibleTime = Date.now(); + + if (document.readyState == "complete") { + this.onPageVisibleAndLoaded(); + } else { + addEventListener("load", this); + } + }, + + onPageVisibleAndLoaded() { + // Send the index of the last visible tile. + this.reportLastVisibleTileIndex(); + // Maybe tell the user they can undo an initial automigration + this.maybeShowAutoMigrationUndoNotification(); + }, + + reportLastVisibleTileIndex() { + let cwu = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let rect = cwu.getBoundsWithoutFlushing(gGrid.node); + let nodes = cwu.nodesFromRect(rect.left, rect.top, 0, rect.width, + rect.height, 0, true, false); + + let i = -1; + let lastIndex = -1; + let sites = gGrid.sites; + + for (let node of nodes) { + if (node.classList && node.classList.contains("newtab-cell")) { + if (sites[++i]) { + lastIndex = i; + if (sites[i].link.targetedSite) { + // record that suggested tile is shown to use suggested-tiles-histogram + this._suggestedTilePresent = true; + } + } + } + } + + DirectoryLinksProvider.reportSitesAction(sites, "view", lastIndex); + }, + + maybeShowAutoMigrationUndoNotification() { + sendAsyncMessage("NewTab:MaybeShowAutoMigrationUndoNotification"); + }, +}; diff --git a/application/basilisk/base/content/newtab/search.js b/application/basilisk/base/content/newtab/search.js new file mode 100644 index 000000000..cbbb6e243 --- /dev/null +++ b/application/basilisk/base/content/newtab/search.js @@ -0,0 +1,15 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +var gSearch = { + init: function () { + document.getElementById("newtab-search-submit") + .addEventListener("click", e => this._contentSearchController.search(e)); + let textbox = document.getElementById("newtab-search-text"); + this._contentSearchController = + new ContentSearchUIController(textbox, textbox.parentNode, "newtab", "newtab"); + }, +}; diff --git a/application/basilisk/base/content/newtab/sites.js b/application/basilisk/base/content/newtab/sites.js new file mode 100644 index 000000000..9d103ce9b --- /dev/null +++ b/application/basilisk/base/content/newtab/sites.js @@ -0,0 +1,440 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +const THUMBNAIL_PLACEHOLDER_ENABLED = + Services.prefs.getBoolPref("browser.newtabpage.thumbnailPlaceholder"); + +/** + * This class represents a site that is contained in a cell and can be pinned, + * moved around or deleted. + */ +function Site(aNode, aLink) { + this._node = aNode; + this._node._newtabSite = this; + + this._link = aLink; + + this._render(); + this._addEventHandlers(); +} + +Site.prototype = { + /** + * The site's DOM node. + */ + get node() { return this._node; }, + + /** + * The site's link. + */ + get link() { return this._link; }, + + /** + * The url of the site's link. + */ + get url() { return this.link.url; }, + + /** + * The title of the site's link. + */ + get title() { return this.link.title || this.link.url; }, + + /** + * The site's parent cell. + */ + get cell() { + let parentNode = this.node.parentNode; + return parentNode && parentNode._newtabCell; + }, + + /** + * Pins the site on its current or a given index. + * @param aIndex The pinned index (optional). + * @return true if link changed type after pin + */ + pin: function Site_pin(aIndex) { + if (typeof aIndex == "undefined") + aIndex = this.cell.index; + + this._updateAttributes(true); + let changed = gPinnedLinks.pin(this._link, aIndex); + if (changed) { + // render site again to remove suggested/sponsored tags + this._render(); + } + return changed; + }, + + /** + * Unpins the site and calls the given callback when done. + */ + unpin: function Site_unpin() { + if (this.isPinned()) { + this._updateAttributes(false); + gPinnedLinks.unpin(this._link); + gUpdater.updateGrid(); + } + }, + + /** + * Checks whether this site is pinned. + * @return Whether this site is pinned. + */ + isPinned: function Site_isPinned() { + return gPinnedLinks.isPinned(this._link); + }, + + /** + * Blocks the site (removes it from the grid) and calls the given callback + * when done. + */ + block: function Site_block() { + if (!gBlockedLinks.isBlocked(this._link)) { + gUndoDialog.show(this); + gBlockedLinks.block(this._link); + gUpdater.updateGrid(); + } + }, + + /** + * Gets the DOM node specified by the given query selector. + * @param aSelector The query selector. + * @return The DOM node we found. + */ + _querySelector: function Site_querySelector(aSelector) { + return this.node.querySelector(aSelector); + }, + + /** + * Updates attributes for all nodes which status depends on this site being + * pinned or unpinned. + * @param aPinned Whether this site is now pinned or unpinned. + */ + _updateAttributes: function (aPinned) { + let control = this._querySelector(".newtab-control-pin"); + + if (aPinned) { + this.node.setAttribute("pinned", true); + control.setAttribute("title", newTabString("unpin")); + } else { + this.node.removeAttribute("pinned"); + control.setAttribute("title", newTabString("pin")); + } + }, + + _newTabString: function(str, substrArr) { + let regExp = /%[0-9]\$S/g; + let matches; + while ((matches = regExp.exec(str))) { + let match = matches[0]; + let index = match.charAt(1); // Get the digit in the regExp. + str = str.replace(match, substrArr[index - 1]); + } + return str; + }, + + _getSuggestedTileExplanation: function() { + let targetedName = `<strong> ${this.link.targetedName} </strong>`; + let targetedSite = `<strong> ${this.link.targetedSite} </strong>`; + if (this.link.explanation) { + return this._newTabString(this.link.explanation, [targetedName, targetedSite]); + } + return newTabString("suggested.button", [targetedName]); + }, + + /** + * Checks for and modifies link at campaign end time + */ + _checkLinkEndTime: function Site_checkLinkEndTime() { + if (this.link.endTime && this.link.endTime < Date.now()) { + let oldUrl = this.url; + // chop off the path part from url + this.link.url = Services.io.newURI(this.url, null, null).resolve("/"); + // clear supplied images - this triggers thumbnail download for new url + delete this.link.imageURI; + delete this.link.enhancedImageURI; + // remove endTime to avoid further time checks + delete this.link.endTime; + // clear enhanced-content image that may still exist in preloaded page + this._querySelector(".enhanced-content").style.backgroundImage = ""; + gPinnedLinks.replace(oldUrl, this.link); + } + }, + + /** + * Renders the site's data (fills the HTML fragment). + */ + _render: function Site_render() { + // first check for end time, as it may modify the link + this._checkLinkEndTime(); + // setup display variables + let enhanced = gAllPages.enhanced && DirectoryLinksProvider.getEnhancedLink(this.link); + let url = this.url; + let title = enhanced && enhanced.title ? enhanced.title : + this.link.type == "history" ? this.link.baseDomain : + this.title; + let tooltip = (this.title == url ? this.title : this.title + "\n" + url); + + let link = this._querySelector(".newtab-link"); + link.setAttribute("title", tooltip); + link.setAttribute("href", url); + this.node.setAttribute("type", this.link.type); + + let titleNode = this._querySelector(".newtab-title"); + titleNode.textContent = title; + if (this.link.titleBgColor) { + titleNode.style.backgroundColor = this.link.titleBgColor; + } + + // remove "suggested" attribute to avoid showing "suggested" tag + // after site was pinned or dropped + this.node.removeAttribute("suggested"); + + if (this.link.targetedSite) { + if (this.node.getAttribute("type") != "sponsored") { + this._querySelector(".newtab-sponsored").textContent = + newTabString("suggested.tag"); + } + + this.node.setAttribute("suggested", true); + let explanation = this._getSuggestedTileExplanation(); + this._querySelector(".newtab-suggested").innerHTML = + `<div class='newtab-suggested-bounds'> ${explanation} </div>`; + } + + if (this.isPinned()) + this._updateAttributes(true); + // Capture the page if the thumbnail is missing, which will cause page.js + // to be notified and call our refreshThumbnail() method. + this.captureIfMissing(); + // but still display whatever thumbnail might be available now. + this.refreshThumbnail(); + }, + + /** + * Called when the site's tab becomes visible for the first time. + * Since the newtab may be preloaded long before it's displayed, + * check for changed conditions and re-render if needed + */ + onFirstVisible: function Site_onFirstVisible() { + if (this.link.endTime && this.link.endTime < Date.now()) { + // site needs to change landing url and background image + this._render(); + } + else { + this.captureIfMissing(); + } + }, + + /** + * Captures the site's thumbnail in the background, but only if there's no + * existing thumbnail and the page allows background captures. + */ + captureIfMissing: function Site_captureIfMissing() { + if (!document.hidden && !this.link.imageURI) { + BackgroundPageThumbs.captureIfMissing(this.url); + } + }, + + /** + * Refreshes the thumbnail for the site. + */ + refreshThumbnail: function Site_refreshThumbnail() { + // Only enhance tiles if that feature is turned on + let link = gAllPages.enhanced && DirectoryLinksProvider.getEnhancedLink(this.link) || + this.link; + + let thumbnail = this._querySelector(".newtab-thumbnail.thumbnail"); + if (link.bgColor) { + thumbnail.style.backgroundColor = link.bgColor; + } + let uri = link.imageURI || PageThumbs.getThumbnailURL(this.url); + thumbnail.style.backgroundImage = 'url("' + uri + '")'; + + if (THUMBNAIL_PLACEHOLDER_ENABLED && + link.type == "history" && + link.baseDomain) { + let placeholder = this._querySelector(".newtab-thumbnail.placeholder"); + let charCodeSum = 0; + for (let c of link.baseDomain) { + charCodeSum += c.charCodeAt(0); + } + const COLORS = 16; + let hue = Math.round((charCodeSum % COLORS) / COLORS * 360); + placeholder.style.backgroundColor = "hsl(" + hue + ",80%,40%)"; + placeholder.textContent = link.baseDomain.substr(0,1).toUpperCase(); + } + + if (link.enhancedImageURI) { + let enhanced = this._querySelector(".enhanced-content"); + enhanced.style.backgroundImage = 'url("' + link.enhancedImageURI + '")'; + + if (this.link.type != link.type) { + this.node.setAttribute("type", "enhanced"); + this.enhancedId = link.directoryId; + } + } + }, + + _ignoreHoverEvents: function(element) { + element.addEventListener("mouseover", () => { + this.cell.node.setAttribute("ignorehover", "true"); + }); + element.addEventListener("mouseout", () => { + this.cell.node.removeAttribute("ignorehover"); + }); + }, + + /** + * Adds event handlers for the site and its buttons. + */ + _addEventHandlers: function Site_addEventHandlers() { + // Register drag-and-drop event handlers. + this._node.addEventListener("dragstart", this, false); + this._node.addEventListener("dragend", this, false); + this._node.addEventListener("mouseover", this, false); + + // Specially treat the sponsored icon & suggested explanation + // text to prevent regular hover effects + let sponsored = this._querySelector(".newtab-sponsored"); + let suggested = this._querySelector(".newtab-suggested"); + this._ignoreHoverEvents(sponsored); + this._ignoreHoverEvents(suggested); + }, + + /** + * Speculatively opens a connection to the current site. + */ + _speculativeConnect: function Site_speculativeConnect() { + let sc = Services.io.QueryInterface(Ci.nsISpeculativeConnect); + let uri = Services.io.newURI(this.url, null, null); + try { + // This can throw for certain internal URLs, when they wind up in + // about:newtab. Be sure not to propagate the error. + sc.speculativeConnect(uri, null); + } catch (e) {} + }, + + /** + * Record interaction with site using telemetry. + */ + _recordSiteClicked: function Site_recordSiteClicked(aIndex) { + if (Services.prefs.prefHasUserValue("browser.newtabpage.rows") || + Services.prefs.prefHasUserValue("browser.newtabpage.columns") || + aIndex > 8) { + // We only want to get indices for the default configuration, everything + // else goes in the same bucket. + aIndex = 9; + } + Services.telemetry.getHistogramById("NEWTAB_PAGE_SITE_CLICKED") + .add(aIndex); + }, + + _toggleLegalText: function(buttonClass, explanationTextClass) { + let button = this._querySelector(buttonClass); + if (button.hasAttribute("active")) { + let explain = this._querySelector(explanationTextClass); + explain.parentNode.removeChild(explain); + + button.removeAttribute("active"); + } + else { + let explain = document.createElementNS(HTML_NAMESPACE, "div"); + explain.className = explanationTextClass.slice(1); // Slice off the first character, '.' + this.node.appendChild(explain); + + let link = '<a href="' + TILES_EXPLAIN_LINK + '">' + + newTabString("learn.link") + "</a>"; + let type = (this.node.getAttribute("suggested") && this.node.getAttribute("type") == "affiliate") ? + "suggested" : this.node.getAttribute("type"); + let icon = '<input type="button" class="newtab-control newtab-' + + (type == "enhanced" ? "customize" : "control-block") + '"/>'; + explain.innerHTML = newTabString(type + (type == "sponsored" ? ".explain2" : ".explain"), [icon, link]); + + button.setAttribute("active", "true"); + } + }, + + /** + * Handles site click events. + */ + onClick: function Site_onClick(aEvent) { + let action; + let pinned = this.isPinned(); + let tileIndex = this.cell.index; + let {button, target} = aEvent; + + // Handle tile/thumbnail link click + if (target.classList.contains("newtab-link") || + target.parentElement.classList.contains("newtab-link")) { + // Record for primary and middle clicks + if (button == 0 || button == 1) { + this._recordSiteClicked(tileIndex); + action = "click"; + } + } + // Handle sponsored explanation link click + else if (target.parentElement.classList.contains("sponsored-explain")) { + action = "sponsored_link"; + } + else if (target.parentElement.classList.contains("suggested-explain")) { + action = "suggested_link"; + } + // Only handle primary clicks for the remaining targets + else if (button == 0) { + aEvent.preventDefault(); + if (target.classList.contains("newtab-control-block")) { + // Notify DirectoryLinksProvider of suggested tile block, this may + // affect if and how suggested tiles are recommended and needs to + // be reported before pages are updated inside block() call + if (this.link.targetedSite) { + DirectoryLinksProvider.handleSuggestedTileBlock(); + } + this.block(); + action = "block"; + } + else if (target.classList.contains("sponsored-explain") || + target.classList.contains("newtab-sponsored")) { + this._toggleLegalText(".newtab-sponsored", ".sponsored-explain"); + action = "sponsored"; + } + else if (pinned && target.classList.contains("newtab-control-pin")) { + this.unpin(); + action = "unpin"; + } + else if (!pinned && target.classList.contains("newtab-control-pin")) { + if (this.pin()) { + // suggested link has changed - update rest of the pages + gAllPages.update(gPage); + } + action = "pin"; + } + } + + // Report all link click actions + if (action) { + DirectoryLinksProvider.reportSitesAction(gGrid.sites, action, tileIndex); + } + }, + + /** + * Handles all site events. + */ + handleEvent: function Site_handleEvent(aEvent) { + switch (aEvent.type) { + case "mouseover": + this._node.removeEventListener("mouseover", this, false); + this._speculativeConnect(); + break; + case "dragstart": + gDrag.start(this, aEvent); + break; + case "dragend": + gDrag.end(this, aEvent); + break; + } + } +}; diff --git a/application/basilisk/base/content/newtab/transformations.js b/application/basilisk/base/content/newtab/transformations.js new file mode 100644 index 000000000..f7db0ad84 --- /dev/null +++ b/application/basilisk/base/content/newtab/transformations.js @@ -0,0 +1,270 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton allows to transform the grid by repositioning a site's node + * in the DOM and by showing or hiding the node. It additionally provides + * convenience methods to work with a site's DOM node. + */ +var gTransformation = { + /** + * Returns the width of the left and top border of a cell. We need to take it + * into account when measuring and comparing site and cell positions. + */ + get _cellBorderWidths() { + let cstyle = window.getComputedStyle(gGrid.cells[0].node, null); + let widths = { + left: parseInt(cstyle.getPropertyValue("border-left-width")), + top: parseInt(cstyle.getPropertyValue("border-top-width")) + }; + + // Cache this value, overwrite the getter. + Object.defineProperty(this, "_cellBorderWidths", + {value: widths, enumerable: true}); + + return widths; + }, + + /** + * Gets a DOM node's position. + * @param aNode The DOM node. + * @return A Rect instance with the position. + */ + getNodePosition: function Transformation_getNodePosition(aNode) { + let {left, top, width, height} = aNode.getBoundingClientRect(); + return new Rect(left + scrollX, top + scrollY, width, height); + }, + + /** + * Fades a given node from zero to full opacity. + * @param aNode The node to fade. + * @param aCallback The callback to call when finished. + */ + fadeNodeIn: function Transformation_fadeNodeIn(aNode, aCallback) { + this._setNodeOpacity(aNode, 1, function () { + // Clear the style property. + aNode.style.opacity = ""; + + if (aCallback) + aCallback(); + }); + }, + + /** + * Fades a given node from full to zero opacity. + * @param aNode The node to fade. + * @param aCallback The callback to call when finished. + */ + fadeNodeOut: function Transformation_fadeNodeOut(aNode, aCallback) { + this._setNodeOpacity(aNode, 0, aCallback); + }, + + /** + * Fades a given site from zero to full opacity. + * @param aSite The site to fade. + * @param aCallback The callback to call when finished. + */ + showSite: function Transformation_showSite(aSite, aCallback) { + this.fadeNodeIn(aSite.node, aCallback); + }, + + /** + * Fades a given site from full to zero opacity. + * @param aSite The site to fade. + * @param aCallback The callback to call when finished. + */ + hideSite: function Transformation_hideSite(aSite, aCallback) { + this.fadeNodeOut(aSite.node, aCallback); + }, + + /** + * Allows to set a site's position. + * @param aSite The site to re-position. + * @param aPosition The desired position for the given site. + */ + setSitePosition: function Transformation_setSitePosition(aSite, aPosition) { + let style = aSite.node.style; + let {top, left} = aPosition; + + style.top = top + "px"; + style.left = left + "px"; + }, + + /** + * Freezes a site in its current position by positioning it absolute. + * @param aSite The site to freeze. + */ + freezeSitePosition: function Transformation_freezeSitePosition(aSite) { + if (this._isFrozen(aSite)) + return; + + let style = aSite.node.style; + let comp = getComputedStyle(aSite.node, null); + style.width = comp.getPropertyValue("width"); + style.height = comp.getPropertyValue("height"); + + aSite.node.setAttribute("frozen", "true"); + this.setSitePosition(aSite, this.getNodePosition(aSite.node)); + }, + + /** + * Unfreezes a site by removing its absolute positioning. + * @param aSite The site to unfreeze. + */ + unfreezeSitePosition: function Transformation_unfreezeSitePosition(aSite) { + if (!this._isFrozen(aSite)) + return; + + let style = aSite.node.style; + style.left = style.top = style.width = style.height = ""; + aSite.node.removeAttribute("frozen"); + }, + + /** + * Slides the given site to the target node's position. + * @param aSite The site to move. + * @param aTarget The slide target. + * @param aOptions Set of options (see below). + * unfreeze - unfreeze the site after sliding + * callback - the callback to call when finished + */ + slideSiteTo: function Transformation_slideSiteTo(aSite, aTarget, aOptions) { + let currentPosition = this.getNodePosition(aSite.node); + let targetPosition = this.getNodePosition(aTarget.node) + let callback = aOptions && aOptions.callback; + + let self = this; + + function finish() { + if (aOptions && aOptions.unfreeze) + self.unfreezeSitePosition(aSite); + + if (callback) + callback(); + } + + // We need to take the width of a cell's border into account. + targetPosition.left += this._cellBorderWidths.left; + targetPosition.top += this._cellBorderWidths.top; + + // Nothing to do here if the positions already match. + if (currentPosition.left == targetPosition.left && + currentPosition.top == targetPosition.top) { + finish(); + } else { + this.setSitePosition(aSite, targetPosition); + this._whenTransitionEnded(aSite.node, ["left", "top"], finish); + } + }, + + /** + * Rearranges a given array of sites and moves them to their new positions or + * fades in/out new/removed sites. + * @param aSites An array of sites to rearrange. + * @param aOptions Set of options (see below). + * unfreeze - unfreeze the site after rearranging + * callback - the callback to call when finished + */ + rearrangeSites: function Transformation_rearrangeSites(aSites, aOptions) { + let batch = []; + let cells = gGrid.cells; + let callback = aOptions && aOptions.callback; + let unfreeze = aOptions && aOptions.unfreeze; + + aSites.forEach(function (aSite, aIndex) { + // Do not re-arrange empty cells or the dragged site. + if (!aSite || aSite == gDrag.draggedSite) + return; + + batch.push(new Promise(resolve => { + if (!cells[aIndex]) { + // The site disappeared from the grid, hide it. + this.hideSite(aSite, resolve); + } else if (this._getNodeOpacity(aSite.node) != 1) { + // The site disappeared before but is now back, show it. + this.showSite(aSite, resolve); + } else { + // The site's position has changed, move it around. + this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: resolve}); + } + })); + }, this); + + if (callback) { + Promise.all(batch).then(callback); + } + }, + + /** + * Listens for the 'transitionend' event on a given node and calls the given + * callback. + * @param aNode The node that is transitioned. + * @param aProperties The properties we'll wait to be transitioned. + * @param aCallback The callback to call when finished. + */ + _whenTransitionEnded: + function Transformation_whenTransitionEnded(aNode, aProperties, aCallback) { + + let props = new Set(aProperties); + aNode.addEventListener("transitionend", function onEnd(e) { + if (props.has(e.propertyName)) { + aNode.removeEventListener("transitionend", onEnd); + aCallback(); + } + }); + }, + + /** + * Gets a given node's opacity value. + * @param aNode The node to get the opacity value from. + * @return The node's opacity value. + */ + _getNodeOpacity: function Transformation_getNodeOpacity(aNode) { + let cstyle = window.getComputedStyle(aNode, null); + return cstyle.getPropertyValue("opacity"); + }, + + /** + * Sets a given node's opacity. + * @param aNode The node to set the opacity value for. + * @param aOpacity The opacity value to set. + * @param aCallback The callback to call when finished. + */ + _setNodeOpacity: + function Transformation_setNodeOpacity(aNode, aOpacity, aCallback) { + + if (this._getNodeOpacity(aNode) == aOpacity) { + if (aCallback) + aCallback(); + } else { + if (aCallback) { + this._whenTransitionEnded(aNode, ["opacity"], aCallback); + } + + aNode.style.opacity = aOpacity; + } + }, + + /** + * Moves a site to the cell with the given index. + * @param aSite The site to move. + * @param aIndex The target cell's index. + * @param aOptions Options that are directly passed to slideSiteTo(). + */ + _moveSite: function Transformation_moveSite(aSite, aIndex, aOptions) { + this.freezeSitePosition(aSite); + this.slideSiteTo(aSite, gGrid.cells[aIndex], aOptions); + }, + + /** + * Checks whether a site is currently frozen. + * @param aSite The site to check. + * @return Whether the given site is frozen. + */ + _isFrozen: function Transformation_isFrozen(aSite) { + return aSite.node.hasAttribute("frozen"); + } +}; diff --git a/application/basilisk/base/content/newtab/undo.js b/application/basilisk/base/content/newtab/undo.js new file mode 100644 index 000000000..b856914d2 --- /dev/null +++ b/application/basilisk/base/content/newtab/undo.js @@ -0,0 +1,116 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * Dialog allowing to undo the removal of single site or to completely restore + * the grid's original state. + */ +var gUndoDialog = { + /** + * The undo dialog's timeout in miliseconds. + */ + HIDE_TIMEOUT_MS: 15000, + + /** + * Contains undo information. + */ + _undoData: null, + + /** + * Initializes the undo dialog. + */ + init: function UndoDialog_init() { + this._undoContainer = document.getElementById("newtab-undo-container"); + this._undoContainer.addEventListener("click", this, false); + this._undoButton = document.getElementById("newtab-undo-button"); + this._undoCloseButton = document.getElementById("newtab-undo-close-button"); + this._undoRestoreButton = document.getElementById("newtab-undo-restore-button"); + }, + + /** + * Shows the undo dialog. + * @param aSite The site that just got removed. + */ + show: function UndoDialog_show(aSite) { + if (this._undoData) + clearTimeout(this._undoData.timeout); + + this._undoData = { + index: aSite.cell.index, + wasPinned: aSite.isPinned(), + blockedLink: aSite.link, + timeout: setTimeout(this.hide.bind(this), this.HIDE_TIMEOUT_MS) + }; + + this._undoContainer.removeAttribute("undo-disabled"); + this._undoButton.removeAttribute("tabindex"); + this._undoCloseButton.removeAttribute("tabindex"); + this._undoRestoreButton.removeAttribute("tabindex"); + }, + + /** + * Hides the undo dialog. + */ + hide: function UndoDialog_hide() { + if (!this._undoData) + return; + + clearTimeout(this._undoData.timeout); + this._undoData = null; + this._undoContainer.setAttribute("undo-disabled", "true"); + this._undoButton.setAttribute("tabindex", "-1"); + this._undoCloseButton.setAttribute("tabindex", "-1"); + this._undoRestoreButton.setAttribute("tabindex", "-1"); + }, + + /** + * The undo dialog event handler. + * @param aEvent The event to handle. + */ + handleEvent: function UndoDialog_handleEvent(aEvent) { + switch (aEvent.target.id) { + case "newtab-undo-button": + this._undo(); + break; + case "newtab-undo-restore-button": + this._undoAll(); + break; + case "newtab-undo-close-button": + this.hide(); + break; + } + }, + + /** + * Undo the last blocked site. + */ + _undo: function UndoDialog_undo() { + if (!this._undoData) + return; + + let {index, wasPinned, blockedLink} = this._undoData; + gBlockedLinks.unblock(blockedLink); + + if (wasPinned) { + gPinnedLinks.pin(blockedLink, index); + } + + gUpdater.updateGrid(); + this.hide(); + }, + + /** + * Undo all blocked sites. + */ + _undoAll: function UndoDialog_undoAll() { + NewTabUtils.undoAll(function() { + gUpdater.updateGrid(); + this.hide(); + }.bind(this)); + } +}; + +gUndoDialog.init(); diff --git a/application/basilisk/base/content/newtab/updater.js b/application/basilisk/base/content/newtab/updater.js new file mode 100644 index 000000000..2bab74d70 --- /dev/null +++ b/application/basilisk/base/content/newtab/updater.js @@ -0,0 +1,177 @@ +#ifdef 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +#endif + +/** + * This singleton provides functionality to update the current grid to a new + * set of pinned and blocked sites. It adds, moves and removes sites. + */ +var gUpdater = { + /** + * Updates the current grid according to its pinned and blocked sites. + * This removes old, moves existing and creates new sites to fill gaps. + * @param aCallback The callback to call when finished. + */ + updateGrid: function Updater_updateGrid(aCallback) { + let links = gLinks.getLinks().slice(0, gGrid.cells.length); + + // Find all sites that remain in the grid. + let sites = this._findRemainingSites(links); + + // Remove sites that are no longer in the grid. + this._removeLegacySites(sites, () => { + // Freeze all site positions so that we can move their DOM nodes around + // without any visual impact. + this._freezeSitePositions(sites); + + // Move the sites' DOM nodes to their new position in the DOM. This will + // have no visual effect as all the sites have been frozen and will + // remain in their current position. + this._moveSiteNodes(sites); + + // Now it's time to animate the sites actually moving to their new + // positions. + this._rearrangeSites(sites, () => { + // Try to fill empty cells and finish. + this._fillEmptyCells(links, aCallback); + + // Update other pages that might be open to keep them synced. + gAllPages.update(gPage); + }); + }); + }, + + /** + * Takes an array of links and tries to correlate them to sites contained in + * the current grid. If no corresponding site can be found (i.e. the link is + * new and a site will be created) then just set it to null. + * @param aLinks The array of links to find sites for. + * @return Array of sites mapped to the given links (can contain null values). + */ + _findRemainingSites: function Updater_findRemainingSites(aLinks) { + let map = {}; + + // Create a map to easily retrieve the site for a given URL. + gGrid.sites.forEach(function (aSite) { + if (aSite) + map[aSite.url] = aSite; + }); + + // Map each link to its corresponding site, if any. + return aLinks.map(function (aLink) { + return aLink && (aLink.url in map) && map[aLink.url]; + }); + }, + + /** + * Freezes the given sites' positions. + * @param aSites The array of sites to freeze. + */ + _freezeSitePositions: function Updater_freezeSitePositions(aSites) { + aSites.forEach(function (aSite) { + if (aSite) + gTransformation.freezeSitePosition(aSite); + }); + }, + + /** + * Moves the given sites' DOM nodes to their new positions. + * @param aSites The array of sites to move. + */ + _moveSiteNodes: function Updater_moveSiteNodes(aSites) { + let cells = gGrid.cells; + + // Truncate the given array of sites to not have more sites than cells. + // This can happen when the user drags a bookmark (or any other new kind + // of link) onto the grid. + let sites = aSites.slice(0, cells.length); + + sites.forEach(function (aSite, aIndex) { + let cell = cells[aIndex]; + let cellSite = cell.site; + + // The site's position didn't change. + if (!aSite || cellSite != aSite) { + let cellNode = cell.node; + + // Empty the cell if necessary. + if (cellSite) + cellNode.removeChild(cellSite.node); + + // Put the new site in place, if any. + if (aSite) + cellNode.appendChild(aSite.node); + } + }, this); + }, + + /** + * Rearranges the given sites and slides them to their new positions. + * @param aSites The array of sites to re-arrange. + * @param aCallback The callback to call when finished. + */ + _rearrangeSites: function Updater_rearrangeSites(aSites, aCallback) { + let options = {callback: aCallback, unfreeze: true}; + gTransformation.rearrangeSites(aSites, options); + }, + + /** + * Removes all sites from the grid that are not in the given links array or + * exceed the grid. + * @param aSites The array of sites remaining in the grid. + * @param aCallback The callback to call when finished. + */ + _removeLegacySites: function Updater_removeLegacySites(aSites, aCallback) { + let batch = []; + + // Delete sites that were removed from the grid. + gGrid.sites.forEach(function (aSite) { + // The site must be valid and not in the current grid. + if (!aSite || aSites.indexOf(aSite) != -1) + return; + + batch.push(new Promise(resolve => { + // Fade out the to-be-removed site. + gTransformation.hideSite(aSite, function () { + let node = aSite.node; + + // Remove the site from the DOM. + node.parentNode.removeChild(node); + resolve(); + }); + })); + }); + + Promise.all(batch).then(aCallback); + }, + + /** + * Tries to fill empty cells with new links if available. + * @param aLinks The array of links. + * @param aCallback The callback to call when finished. + */ + _fillEmptyCells: function Updater_fillEmptyCells(aLinks, aCallback) { + let {cells, sites} = gGrid; + + // Find empty cells and fill them. + Promise.all(sites.map((aSite, aIndex) => { + if (aSite || !aLinks[aIndex]) + return null; + + return new Promise(resolve => { + // Create the new site and fade it in. + let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]); + + // Set the site's initial opacity to zero. + site.node.style.opacity = 0; + + // Flush all style changes for the dynamically inserted site to make + // the fade-in transition work. + window.getComputedStyle(site.node).opacity; + gTransformation.showSite(site, resolve); + }); + })).then(aCallback).catch(console.exception); + } +}; diff --git a/application/basilisk/base/content/nsContextMenu.js b/application/basilisk/base/content/nsContextMenu.js new file mode 100644 index 000000000..097caf367 --- /dev/null +++ b/application/basilisk/base/content/nsContextMenu.js @@ -0,0 +1,1851 @@ +/* -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et tw=80: */ +# 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/. + +Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm"); +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm"); +Components.utils.import("resource://gre/modules/LoginManagerContextMenu.jsm"); +Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + + +XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", + "resource://gre/modules/LoginHelper.jsm"); + +var gContextMenuContentData = null; + +function nsContextMenu(aXulMenu, aIsShift) { + this.shouldDisplay = true; + this.initMenu(aXulMenu, aIsShift); +} + +// Prototype for nsContextMenu "class." +nsContextMenu.prototype = { + initMenu: function CM_initMenu(aXulMenu, aIsShift) { + // Get contextual info. + this.setTarget(document.popupNode, document.popupRangeParent, + document.popupRangeOffset); + if (!this.shouldDisplay) + return; + + this.hasPageMenu = false; + this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed; + if (!aIsShift) { + if (this.isRemote) { + this.hasPageMenu = + PageMenuParent.addToPopup(gContextMenuContentData.customMenuItems, + this.browser, aXulMenu); + } + else { + this.hasPageMenu = PageMenuParent.buildAndAddToPopup(this.target, aXulMenu); + } + + let subject = { + menu: aXulMenu, + tab: gBrowser ? gBrowser.getTabForBrowser(this.browser) : undefined, + isContentSelected: this.isContentSelected, + inFrame: this.inFrame, + isTextSelected: this.isTextSelected, + onTextInput: this.onTextInput, + onLink: this.onLink, + onImage: this.onImage, + onVideo: this.onVideo, + onAudio: this.onAudio, + onCanvas: this.onCanvas, + onEditableArea: this.onEditableArea, + srcUrl: this.mediaURL, + frameUrl: gContextMenuContentData ? gContextMenuContentData.docLocation : undefined, + pageUrl: this.browser ? this.browser.currentURI.spec : undefined, + linkUrl: this.linkURL, + selectionText: this.isTextSelected ? this.selectionInfo.text : undefined, + }; + subject.wrappedJSObject = subject; + Services.obs.notifyObservers(subject, "on-build-contextmenu", null); + } + + this.isFrameImage = document.getElementById("isFrameImage"); + this.ellipsis = "\u2026"; + try { + this.ellipsis = gPrefService.getComplexValue("intl.ellipsis", + Ci.nsIPrefLocalizedString).data; + } catch (e) { } + + // Reset after "on-build-contextmenu" notification in case selection was + // changed during the notification. + this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed; + this.onPlainTextLink = false; + + let bookmarkPage = document.getElementById("context-bookmarkpage"); + if (bookmarkPage) + BookmarkingUI.onCurrentPageContextPopupShowing(); + + // Initialize (disable/remove) menu items. + this.initItems(); + + // Register this opening of the menu with telemetry: + this._checkTelemetryForMenu(aXulMenu); + }, + + hiding: function CM_hiding() { + gContextMenuContentData = null; + InlineSpellCheckerUI.clearSuggestionsFromMenu(); + InlineSpellCheckerUI.clearDictionaryListFromMenu(); + InlineSpellCheckerUI.uninit(); + LoginManagerContextMenu.clearLoginsFromMenu(document); + + // This handler self-deletes, only run it if it is still there: + if (this._onPopupHiding) { + this._onPopupHiding(); + } + }, + + initItems: function CM_initItems() { + this.initPageMenuSeparator(); + this.initOpenItems(); + this.initNavigationItems(); + this.initViewItems(); + this.initMiscItems(); + this.initSpellingItems(); + this.initSaveItems(); + this.initClipboardItems(); + this.initMediaPlayerItems(); + this.initLeaveDOMFullScreenItems(); + this.initClickToPlayItems(); + this.initPasswordManagerItems(); + this.initSyncItems(); + }, + + initPageMenuSeparator: function CM_initPageMenuSeparator() { + this.showItem("page-menu-separator", this.hasPageMenu); + }, + + initOpenItems: function CM_initOpenItems() { + var isMailtoInternal = false; + if (this.onMailtoLink) { + var mailtoHandler = Cc["@mozilla.org/uriloader/external-protocol-service;1"]. + getService(Ci.nsIExternalProtocolService). + getProtocolHandlerInfo("mailto"); + isMailtoInternal = (!mailtoHandler.alwaysAskBeforeHandling && + mailtoHandler.preferredAction == Ci.nsIHandlerInfo.useHelperApp && + (mailtoHandler.preferredApplicationHandler instanceof Ci.nsIWebHandlerApp)); + } + + if (this.isTextSelected && !this.onLink && + this.selectionInfo && this.selectionInfo.linkURL) { + this.linkURL = this.selectionInfo.linkURL; + try { + this.linkURI = makeURI(this.linkURL); + } catch (ex) {} + + this.linkTextStr = this.selectionInfo.linkText; + this.onPlainTextLink = true; + } + + var inContainer = false; + if (gContextMenuContentData.userContextId) { + inContainer = true; + var item = document.getElementById("context-openlinkincontainertab"); + + item.setAttribute("data-usercontextid", gContextMenuContentData.userContextId); + + var label = + ContextualIdentityService.getUserContextLabel(gContextMenuContentData.userContextId); + item.setAttribute("label", + gBrowserBundle.formatStringFromName("userContextOpenLink.label", + [label], 1)); + } + + var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink; + var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + var showContainers = Services.prefs.getBoolPref("privacy.userContext.enabled"); + this.showItem("context-openlink", shouldShow && !isWindowPrivate); + this.showItem("context-openlinkprivate", shouldShow); + this.showItem("context-openlinkintab", shouldShow && !inContainer); + this.showItem("context-openlinkincontainertab", shouldShow && inContainer); + this.showItem("context-openlinkinusercontext-menu", shouldShow && !isWindowPrivate && showContainers); + this.showItem("context-openlinkincurrent", this.onPlainTextLink); + this.showItem("context-sep-open", shouldShow); + }, + + initNavigationItems: function CM_initNavigationItems() { + var shouldShow = !(this.isContentSelected || this.onLink || this.onImage || + this.onCanvas || this.onVideo || this.onAudio || + this.onTextInput); + this.showItem("context-navigation", shouldShow); + this.showItem("context-sep-navigation", shouldShow); + + let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true"; + + let stopReloadItem = ""; + if (shouldShow) { + stopReloadItem = (stopped) ? "reload" : "stop"; + } + + this.showItem("context-reload", stopReloadItem == "reload"); + this.showItem("context-stop", stopReloadItem == "stop"); + + // XXX: Stop is determined in browser.js; the canStop broadcaster is broken + //this.setItemAttrFromNode( "context-stop", "disabled", "canStop" ); + }, + + initLeaveDOMFullScreenItems: function CM_initLeaveFullScreenItem() { + // only show the option if the user is in DOM fullscreen + var shouldShow = (this.target.ownerDocument.fullscreenElement != null); + this.showItem("context-leave-dom-fullscreen", shouldShow); + + // Explicitly show if in DOM fullscreen, but do not hide it has already been shown + if (shouldShow) + this.showItem("context-media-sep-commands", true); + }, + + initSaveItems: function CM_initSaveItems() { + var shouldShow = !(this.onTextInput || this.onLink || + this.isContentSelected || this.onImage || + this.onCanvas || this.onVideo || this.onAudio); + this.showItem("context-savepage", shouldShow); + + // Save link depends on whether we're in a link, or selected text matches valid URL pattern. + this.showItem("context-savelink", this.onSaveableLink || this.onPlainTextLink); + + // Save image depends on having loaded its content, video and audio don't. + this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas); + this.showItem("context-savevideo", this.onVideo); + this.showItem("context-saveaudio", this.onAudio); + this.showItem("context-video-saveimage", this.onVideo); + this.setItemAttr("context-savevideo", "disabled", !this.mediaURL); + this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL); + // Send media URL (but not for canvas, since it's a big data: URL) + this.showItem("context-sendimage", this.onImage); + this.showItem("context-sendvideo", this.onVideo); + this.showItem("context-castvideo", this.onVideo); + this.showItem("context-sendaudio", this.onAudio); + let mediaIsBlob = this.mediaURL.startsWith("blob:"); + this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL || mediaIsBlob); + this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL || mediaIsBlob); + let shouldShowCast = Services.prefs.getBoolPref("browser.casting.enabled"); + // getServicesForVideo alone would be sufficient here (it depends on + // SimpleServiceDiscovery.services), but SimpleServiceDiscovery is guaranteed + // to be already loaded, since we load it on startup in nsBrowserGlue, + // and CastingApps isn't, so check SimpleServiceDiscovery.services first + // to avoid needing to load CastingApps.jsm if we don't need to. + shouldShowCast = shouldShowCast && this.mediaURL && + SimpleServiceDiscovery.services.length > 0 && + CastingApps.getServicesForVideo(this.target).length > 0; + this.setItemAttr("context-castvideo", "disabled", !shouldShowCast); + }, + + initViewItems: function CM_initViewItems() { + // View source is always OK, unless in directory listing. + this.showItem("context-viewpartialsource-selection", + this.isContentSelected); + this.showItem("context-viewpartialsource-mathml", + this.onMathML && !this.isContentSelected); + + var shouldShow = !(this.isContentSelected || + this.onImage || this.onCanvas || + this.onVideo || this.onAudio || + this.onLink || this.onTextInput); + var showInspect = gPrefService.getBoolPref("devtools.inspector.enabled"); + this.showItem("context-viewsource", shouldShow); + this.showItem("context-viewinfo", shouldShow); + this.showItem("inspect-separator", showInspect); + this.showItem("context-inspect", showInspect); + + this.showItem("context-sep-viewsource", shouldShow); + + // Set as Desktop background depends on whether an image was clicked on, + // and only works if we have a shell service. + var haveSetDesktopBackground = false; +#ifdef HAVE_SHELL_SERVICE + // Only enable Set as Desktop Background if we can get the shell service. + var shell = getShellService(); + if (shell) + haveSetDesktopBackground = shell.canSetDesktopBackground; +#endif + this.showItem("context-setDesktopBackground", + haveSetDesktopBackground && this.onLoadedImage); + + if (haveSetDesktopBackground && this.onLoadedImage) { + document.getElementById("context-setDesktopBackground") + .disabled = gContextMenuContentData.disableSetDesktopBackground; + } + + // Reload image depends on an image that's not fully loaded + this.showItem("context-reloadimage", (this.onImage && !this.onCompletedImage)); + + // View image depends on having an image that's not standalone + // (or is in a frame), or a canvas. + this.showItem("context-viewimage", (this.onImage && + (!this.inSyntheticDoc || this.inFrame)) || this.onCanvas); + + // View video depends on not having a standalone video. + this.showItem("context-viewvideo", this.onVideo && (!this.inSyntheticDoc || this.inFrame)); + this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL); + + // View background image depends on whether there is one, but don't make + // background images of a stand-alone media document available. + this.showItem("context-viewbgimage", shouldShow && + !this._hasMultipleBGImages && + !this.inSyntheticDoc); + this.showItem("context-sep-viewbgimage", shouldShow && + !this._hasMultipleBGImages && + !this.inSyntheticDoc); + document.getElementById("context-viewbgimage") + .disabled = !this.hasBGImage; + + this.showItem("context-viewimageinfo", this.onImage); + this.showItem("context-viewimagedesc", this.onImage && this.imageDescURL !== ""); + }, + + initMiscItems: function CM_initMiscItems() { + // Use "Bookmark This Link" if on a link. + let bookmarkPage = document.getElementById("context-bookmarkpage"); + this.showItem(bookmarkPage, + !(this.isContentSelected || this.onTextInput || this.onLink || + this.onImage || this.onVideo || this.onAudio || this.onCanvas)); + bookmarkPage.setAttribute("tooltiptext", bookmarkPage.getAttribute("buttontooltiptext")); + + this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink) || + this.onPlainTextLink); + this.showItem("context-keywordfield", + this.onTextInput && this.onKeywordField); + this.showItem("frame", this.inFrame); + + let showSearchSelect = (this.isTextSelected || this.onLink) && !this.onImage; + this.showItem("context-searchselect", showSearchSelect); + if (showSearchSelect) { + this.formatSearchContextItem(); + } + + // srcdoc cannot be opened separately due to concerns about web + // content with about:srcdoc in location bar masquerading as trusted + // chrome/addon content. + // No need to also test for this.inFrame as this is checked in the parent + // submenu. + this.showItem("context-showonlythisframe", !this.inSrcdocFrame); + this.showItem("context-openframeintab", !this.inSrcdocFrame); + this.showItem("context-openframe", !this.inSrcdocFrame); + this.showItem("context-bookmarkframe", !this.inSrcdocFrame); + this.showItem("open-frame-sep", !this.inSrcdocFrame); + + this.showItem("frame-sep", this.inFrame && this.isTextSelected); + + // Hide menu entries for images, show otherwise + if (this.inFrame) { + if (BrowserUtils.mimeTypeIsTextBased(this.target.ownerDocument.contentType)) + this.isFrameImage.removeAttribute('hidden'); + else + this.isFrameImage.setAttribute('hidden', 'true'); + } + + // BiDi UI + this.showItem("context-sep-bidi", !this.onNumeric && top.gBidiUI); + this.showItem("context-bidi-text-direction-toggle", + this.onTextInput && !this.onNumeric && top.gBidiUI); + this.showItem("context-bidi-page-direction-toggle", + !this.onTextInput && top.gBidiUI); + }, + + initSpellingItems: function() { + var canSpell = InlineSpellCheckerUI.canSpellCheck && + !InlineSpellCheckerUI.initialSpellCheckPending && + this.canSpellCheck; + let showDictionaries = canSpell && InlineSpellCheckerUI.enabled; + var onMisspelling = InlineSpellCheckerUI.overMisspelling; + var showUndo = canSpell && InlineSpellCheckerUI.canUndo(); + this.showItem("spell-check-enabled", canSpell); + this.showItem("spell-separator", canSpell); + document.getElementById("spell-check-enabled") + .setAttribute("checked", canSpell && InlineSpellCheckerUI.enabled); + + this.showItem("spell-add-to-dictionary", onMisspelling); + this.showItem("spell-undo-add-to-dictionary", showUndo); + + // suggestion list + this.showItem("spell-suggestions-separator", onMisspelling || showUndo); + if (onMisspelling) { + var suggestionsSeparator = + document.getElementById("spell-add-to-dictionary"); + var numsug = + InlineSpellCheckerUI.addSuggestionsToMenu(suggestionsSeparator.parentNode, + suggestionsSeparator, 5); + this.showItem("spell-no-suggestions", numsug == 0); + } + else + this.showItem("spell-no-suggestions", false); + + // dictionary list + this.showItem("spell-dictionaries", showDictionaries); + if (canSpell) { + var dictMenu = document.getElementById("spell-dictionaries-menu"); + var dictSep = document.getElementById("spell-language-separator"); + let count = InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep); + this.showItem(dictSep, count > 0); + this.showItem("spell-add-dictionaries-main", false); + } + else if (this.onEditableArea) { + // when there is no spellchecker but we might be able to spellcheck + // add the add to dictionaries item. This will ensure that people + // with no dictionaries will be able to download them + this.showItem("spell-language-separator", showDictionaries); + this.showItem("spell-add-dictionaries-main", showDictionaries); + } + else + this.showItem("spell-add-dictionaries-main", false); + }, + + initClipboardItems: function() { + // Copy depends on whether there is selected text. + // Enabling this context menu item is now done through the global + // command updating system + // this.setItemAttr( "context-copy", "disabled", !this.isTextSelected() ); + goUpdateGlobalEditMenuItems(); + + this.showItem("context-undo", this.onTextInput); + this.showItem("context-sep-undo", this.onTextInput); + this.showItem("context-cut", this.onTextInput); + this.showItem("context-copy", + this.isContentSelected || this.onTextInput); + this.showItem("context-paste", this.onTextInput); + this.showItem("context-delete", this.onTextInput); + this.showItem("context-sep-paste", this.onTextInput); + this.showItem("context-selectall", !(this.onLink || this.onImage || + this.onVideo || this.onAudio || + this.inSyntheticDoc) || + this.isDesignMode); + this.showItem("context-sep-selectall", this.isContentSelected ); + + // XXX dr + // ------ + // nsDocumentViewer.cpp has code to determine whether we're + // on a link or an image. we really ought to be using that... + + // Copy email link depends on whether we're on an email link. + this.showItem("context-copyemail", this.onMailtoLink); + + // Copy link location depends on whether we're on a non-mailto link. + this.showItem("context-copylink", this.onLink && !this.onMailtoLink); + this.showItem("context-sep-copylink", this.onLink && + (this.onImage || this.onVideo || this.onAudio)); + +#ifdef CONTEXT_COPY_IMAGE_CONTENTS + // Copy image contents depends on whether we're on an image. + this.showItem("context-copyimage-contents", this.onImage); +#endif + // Copy image location depends on whether we're on an image. + this.showItem("context-copyimage", this.onImage); + this.showItem("context-copyvideourl", this.onVideo); + this.showItem("context-copyaudiourl", this.onAudio); + this.setItemAttr("context-copyvideourl", "disabled", !this.mediaURL); + this.setItemAttr("context-copyaudiourl", "disabled", !this.mediaURL); + this.showItem("context-sep-copyimage", this.onImage || + this.onVideo || this.onAudio); + }, + + initMediaPlayerItems: function() { + var onMedia = (this.onVideo || this.onAudio); + // Several mutually exclusive items... play/pause, mute/unmute, show/hide + this.showItem("context-media-play", onMedia && (this.target.paused || this.target.ended)); + this.showItem("context-media-pause", onMedia && !this.target.paused && !this.target.ended); + this.showItem("context-media-mute", onMedia && !this.target.muted); + this.showItem("context-media-unmute", onMedia && this.target.muted); + this.showItem("context-media-playbackrate", onMedia && this.target.duration != Number.POSITIVE_INFINITY); + this.showItem("context-media-loop", onMedia); + this.showItem("context-media-showcontrols", onMedia && !this.target.controls); + this.showItem("context-media-hidecontrols", this.target.controls && (this.onVideo || (this.onAudio && !this.inSyntheticDoc))); + this.showItem("context-video-fullscreen", this.onVideo && this.target.ownerDocument.fullscreenElement == null); + this.showItem("context-media-eme-learnmore", this.onDRMMedia); + this.showItem("context-media-eme-separator", this.onDRMMedia); + + // Disable them when there isn't a valid media source loaded. + if (onMedia) { + this.setItemAttr("context-media-playbackrate-050x", "checked", this.target.playbackRate == 0.5); + this.setItemAttr("context-media-playbackrate-100x", "checked", this.target.playbackRate == 1.0); + this.setItemAttr("context-media-playbackrate-125x", "checked", this.target.playbackRate == 1.25); + this.setItemAttr("context-media-playbackrate-150x", "checked", this.target.playbackRate == 1.5); + this.setItemAttr("context-media-playbackrate-200x", "checked", this.target.playbackRate == 2.0); + this.setItemAttr("context-media-loop", "checked", this.target.loop); + var hasError = this.target.error != null || + this.target.networkState == this.target.NETWORK_NO_SOURCE; + this.setItemAttr("context-media-play", "disabled", hasError); + this.setItemAttr("context-media-pause", "disabled", hasError); + this.setItemAttr("context-media-mute", "disabled", hasError); + this.setItemAttr("context-media-unmute", "disabled", hasError); + this.setItemAttr("context-media-playbackrate", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-050x", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-100x", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-125x", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-150x", "disabled", hasError); + this.setItemAttr("context-media-playbackrate-200x", "disabled", hasError); + this.setItemAttr("context-media-showcontrols", "disabled", hasError); + this.setItemAttr("context-media-hidecontrols", "disabled", hasError); + if (this.onVideo) { + let canSaveSnapshot = !this.onDRMMedia && this.target.readyState >= this.target.HAVE_CURRENT_DATA; + this.setItemAttr("context-video-saveimage", "disabled", !canSaveSnapshot); + this.setItemAttr("context-video-fullscreen", "disabled", hasError); + } + } + this.showItem("context-media-sep-commands", onMedia); + }, + + initClickToPlayItems: function() { + this.showItem("context-ctp-play", this.onCTPPlugin); + this.showItem("context-ctp-hide", this.onCTPPlugin); + this.showItem("context-sep-ctp", this.onCTPPlugin); + }, + + initPasswordManagerItems: function() { + let loginFillInfo = gContextMenuContentData && gContextMenuContentData.loginFillInfo; + + // If we could not find a password field we + // don't want to show the form fill option. + let showFill = loginFillInfo && loginFillInfo.passwordField.found; + + // Disable the fill option if the user has set a master password + // or if the password field or target field are disabled. + let disableFill = !loginFillInfo || + !Services.logins || + !Services.logins.isLoggedIn || + loginFillInfo.passwordField.disabled || + (!this.onPassword && loginFillInfo.usernameField.disabled); + + this.showItem("fill-login-separator", showFill); + this.showItem("fill-login", showFill); + this.setItemAttr("fill-login", "disabled", disableFill); + + // Set the correct label for the fill menu + let fillMenu = document.getElementById("fill-login"); + if (this.onPassword) { + fillMenu.setAttribute("label", fillMenu.getAttribute("label-password")); + fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-password")); + } else { + fillMenu.setAttribute("label", fillMenu.getAttribute("label-login")); + fillMenu.setAttribute("accesskey", fillMenu.getAttribute("accesskey-login")); + } + + if (!showFill || disableFill) { + return; + } + let documentURI = gContextMenuContentData.documentURIObject; + let fragment = LoginManagerContextMenu.addLoginsToMenu(this.target, this.browser, documentURI); + + this.showItem("fill-login-no-logins", !fragment); + + if (!fragment) { + return; + } + let popup = document.getElementById("fill-login-popup"); + let insertBeforeElement = document.getElementById("fill-login-no-logins"); + popup.insertBefore(fragment, insertBeforeElement); + }, + + initSyncItems: function() { + gFxAccounts.initPageContextMenu(this); + }, + + openPasswordManager: function() { + LoginHelper.openPasswordManager(window, gContextMenuContentData.documentURIObject.host); + }, + + inspectNode: function() { + let {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {}); + let gBrowser = this.browser.ownerGlobal.gBrowser; + let target = devtools.TargetFactory.forTab(gBrowser.selectedTab); + + return gDevTools.showToolbox(target, "inspector").then(toolbox => { + let inspector = toolbox.getCurrentPanel(); + + // new-node-front tells us when the node has been selected, whether the + // browser is remote or not. + let onNewNode = inspector.selection.once("new-node-front"); + + this.browser.messageManager.sendAsyncMessage("debug:inspect", {}, {node: this.target}); + inspector.walker.findInspectingNode().then(nodeFront => { + inspector.selection.setNodeFront(nodeFront, "browser-context-menu"); + }); + + return onNewNode.then(() => { + // Now that the node has been selected, wait until the inspector is + // fully updated. + return inspector.once("inspector-updated"); + }); + }); + }, + + // Set various context menu attributes based on the state of the world. + setTarget: function (aNode, aRangeParent, aRangeOffset) { + // gContextMenuContentData.isRemote tells us if the event came from a remote + // process. gContextMenuContentData can be null if something (like tests) + // opens the context menu directly. + let editFlags; + this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote; + if (this.isRemote) { + aNode = gContextMenuContentData.event.target; + aRangeParent = gContextMenuContentData.event.rangeParent; + aRangeOffset = gContextMenuContentData.event.rangeOffset; + editFlags = gContextMenuContentData.editFlags; + } + + const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + if (aNode.nodeType == Node.DOCUMENT_NODE || + // Not display on XUL element but relax for <label class="text-link"> + (aNode.namespaceURI == xulNS && !isXULTextLinkLabel(aNode))) { + this.shouldDisplay = false; + return; + } + + // Initialize contextual info. + this.onImage = false; + this.onLoadedImage = false; + this.onCompletedImage = false; + this.imageDescURL = ""; + this.onCanvas = false; + this.onVideo = false; + this.onAudio = false; + this.onDRMMedia = false; + this.onTextInput = false; + this.onNumeric = false; + this.onKeywordField = false; + this.mediaURL = ""; + this.onLink = false; + this.onMailtoLink = false; + this.onSaveableLink = false; + this.link = null; + this.linkURL = ""; + this.linkURI = null; + this.linkTextStr = ""; + this.linkProtocol = ""; + this.linkDownload = ""; + this.linkHasNoReferrer = false; + this.onMathML = false; + this.inFrame = false; + this.inSrcdocFrame = false; + this.inSyntheticDoc = false; + this.hasBGImage = false; + this.bgImageURL = ""; + this.onEditableArea = false; + this.isDesignMode = false; + this.onCTPPlugin = false; + this.canSpellCheck = false; + this.onPassword = false; + + if (this.isRemote) { + this.selectionInfo = gContextMenuContentData.selectionInfo; + } else { + this.selectionInfo = BrowserUtils.getSelectionDetails(window); + } + + this.textSelected = this.selectionInfo.text; + this.isTextSelected = this.textSelected.length != 0; + + // Remember the node that was clicked. + this.target = aNode; + + let ownerDoc = this.target.ownerDocument; + this.ownerDoc = ownerDoc; + + // If this is a remote context menu event, use the information from + // gContextMenuContentData instead. + if (this.isRemote) { + this.browser = gContextMenuContentData.browser; + this.principal = gContextMenuContentData.principal; + this.frameOuterWindowID = gContextMenuContentData.frameOuterWindowID; + } else { + editFlags = SpellCheckHelper.isEditable(this.target, window); + this.browser = ownerDoc.defaultView + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + this.principal = ownerDoc.nodePrincipal; + this.frameOuterWindowID = ownerDoc.defaultView + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + } + + // Check if we are in a synthetic document (stand alone image, video, etc.). + this.inSyntheticDoc = ownerDoc.mozSyntheticDocument; + // First, do checks for nodes that never have children. + if (this.target.nodeType == Node.ELEMENT_NODE) { + // See if the user clicked on an image. This check mirrors + // nsDocumentViewer::GetInImage. Make sure to update both if this is + // changed. + if (this.target instanceof Ci.nsIImageLoadingContent && + this.target.currentURI) { + this.onImage = true; + + var request = + this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); + if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)) + this.onLoadedImage = true; + if (request && + (request.imageStatus & request.STATUS_LOAD_COMPLETE) && + !(request.imageStatus & request.STATUS_ERROR)) { + this.onCompletedImage = true; + } + + this.mediaURL = this.target.currentURI.spec; + + var descURL = this.target.getAttribute("longdesc"); + if (descURL) { + this.imageDescURL = makeURLAbsolute(ownerDoc.body.baseURI, descURL); + } + } + else if (this.target instanceof HTMLCanvasElement) { + this.onCanvas = true; + } + else if (this.target instanceof HTMLVideoElement) { + let mediaURL = this.target.currentSrc || this.target.src; + if (this.isMediaURLReusable(mediaURL)) { + this.mediaURL = mediaURL; + } + if (this._isProprietaryDRM()) { + this.onDRMMedia = true; + } + // Firefox always creates a HTMLVideoElement when loading an ogg file + // directly. If the media is actually audio, be smarter and provide a + // context menu with audio operations. + if (this.target.readyState >= this.target.HAVE_METADATA && + (this.target.videoWidth == 0 || this.target.videoHeight == 0)) { + this.onAudio = true; + } else { + this.onVideo = true; + } + } + else if (this.target instanceof HTMLAudioElement) { + this.onAudio = true; + let mediaURL = this.target.currentSrc || this.target.src; + if (this.isMediaURLReusable(mediaURL)) { + this.mediaURL = mediaURL; + } + if (this._isProprietaryDRM()) { + this.onDRMMedia = true; + } + } + else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) { + this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0; + this.onNumeric = (editFlags & SpellCheckHelper.NUMERIC) !== 0; + this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0; + this.onPassword = (editFlags & SpellCheckHelper.PASSWORD) !== 0; + if (this.onEditableArea) { + if (this.isRemote) { + InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo); + } + else { + InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor); + InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset); + } + } + this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD); + } + else if (this.target instanceof HTMLHtmlElement) { + var bodyElt = ownerDoc.body; + if (bodyElt) { + let computedURL; + try { + computedURL = this.getComputedURL(bodyElt, "background-image"); + this._hasMultipleBGImages = false; + } catch (e) { + this._hasMultipleBGImages = true; + } + if (computedURL) { + this.hasBGImage = true; + this.bgImageURL = makeURLAbsolute(bodyElt.baseURI, + computedURL); + } + } + } + else if ((this.target instanceof HTMLEmbedElement || + this.target instanceof HTMLObjectElement || + this.target instanceof HTMLAppletElement) && + this.target.displayedType == HTMLObjectElement.TYPE_NULL && + this.target.pluginFallbackType == HTMLObjectElement.PLUGIN_CLICK_TO_PLAY) { + this.onCTPPlugin = true; + } + + this.canSpellCheck = this._isSpellCheckEnabled(this.target); + } + else if (this.target.nodeType == Node.TEXT_NODE) { + // For text nodes, look at the parent node to determine the spellcheck attribute. + this.canSpellCheck = this.target.parentNode && + this._isSpellCheckEnabled(this.target); + } + + // Second, bubble out, looking for items of interest that can have childen. + // Always pick the innermost link, background image, etc. + const XMLNS = "http://www.w3.org/XML/1998/namespace"; + var elem = this.target; + while (elem) { + if (elem.nodeType == Node.ELEMENT_NODE) { + // Link? + if (!this.onLink && + // Be consistent with what hrefAndLinkNodeForClickEvent + // does in browser.js + (isXULTextLinkLabel(elem) || + (elem instanceof HTMLAnchorElement && elem.href) || + (elem instanceof HTMLAreaElement && elem.href) || + elem instanceof HTMLLinkElement || + elem.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple")) { + + // Target is a link or a descendant of a link. + this.onLink = true; + + // Remember corresponding element. + this.link = elem; + this.linkURL = this.getLinkURL(); + this.linkURI = this.getLinkURI(); + this.linkTextStr = this.getLinkText(); + this.linkProtocol = this.getLinkProtocol(); + this.onMailtoLink = (this.linkProtocol == "mailto"); + this.onSaveableLink = this.isLinkSaveable( this.link ); + this.linkHasNoReferrer = BrowserUtils.linkHasNoReferrer(elem); + try { + if (elem.download) { + // Ignore download attribute on cross-origin links + this.principal.checkMayLoad(this.linkURI, false, true); + this.linkDownload = elem.download; + } + } + catch (ex) {} + } + + // Background image? Don't bother if we've already found a + // background image further down the hierarchy. Otherwise, + // we look for the computed background-image style. + if (!this.hasBGImage && + !this._hasMultipleBGImages) { + let bgImgUrl; + try { + bgImgUrl = this.getComputedURL(elem, "background-image"); + this._hasMultipleBGImages = false; + } catch (e) { + this._hasMultipleBGImages = true; + } + if (bgImgUrl) { + this.hasBGImage = true; + this.bgImageURL = makeURLAbsolute(elem.baseURI, + bgImgUrl); + } + } + } + + elem = elem.parentNode; + } + + // See if the user clicked on MathML + const NS_MathML = "http://www.w3.org/1998/Math/MathML"; + if ((this.target.nodeType == Node.TEXT_NODE && + this.target.parentNode.namespaceURI == NS_MathML) + || (this.target.namespaceURI == NS_MathML)) + this.onMathML = true; + + // See if the user clicked in a frame. + var docDefaultView = ownerDoc.defaultView; + if (docDefaultView != docDefaultView.top) { + this.inFrame = true; + + if (ownerDoc.isSrcdocDocument) { + this.inSrcdocFrame = true; + } + } + + // if the document is editable, show context menu like in text inputs + if (!this.onEditableArea) { + if (editFlags & SpellCheckHelper.CONTENTEDITABLE) { + // If this.onEditableArea is false but editFlags is CONTENTEDITABLE, then + // the document itself must be editable. + this.onTextInput = true; + this.onKeywordField = false; + this.onImage = false; + this.onLoadedImage = false; + this.onCompletedImage = false; + this.onMathML = false; + this.inFrame = false; + this.inSrcdocFrame = false; + this.hasBGImage = false; + this.isDesignMode = true; + this.onEditableArea = true; + if (this.isRemote) { + InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo); + } + else { + var targetWin = ownerDoc.defaultView; + var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin)); + InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset); + } + var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck; + this.showItem("spell-check-enabled", canSpell); + this.showItem("spell-separator", canSpell); + } + } + + function isXULTextLinkLabel(node) { + return node.namespaceURI == xulNS && + node.tagName == "label" && + node.classList.contains('text-link') && + node.href; + } + }, + + // Returns the computed style attribute for the given element. + getComputedStyle: function(aElem, aProp) { + return aElem.ownerDocument + .defaultView + .getComputedStyle(aElem, "").getPropertyValue(aProp); + }, + + // Returns a "url"-type computed style attribute value, with the url() stripped. + getComputedURL: function(aElem, aProp) { + var url = aElem.ownerDocument + .defaultView.getComputedStyle(aElem, "") + .getPropertyCSSValue(aProp); + if (url instanceof CSSValueList) { + if (url.length != 1) + throw "found multiple URLs"; + url = url[0]; + } + return url.primitiveType == CSSPrimitiveValue.CSS_URI ? + url.getStringValue() : null; + }, + + // Returns true if clicked-on link targets a resource that can be saved. + isLinkSaveable: function(aLink) { + // We don't do the Right Thing for news/snews yet, so turn them off + // until we do. + return this.linkProtocol && !( + this.linkProtocol == "mailto" || + this.linkProtocol == "javascript" || + this.linkProtocol == "news" || + this.linkProtocol == "snews" ); + }, + + _isSpellCheckEnabled: function(aNode) { + // We can always force-enable spellchecking on textboxes + if (this.isTargetATextBox(aNode)) { + return true; + } + // We can never spell check something which is not content editable + var editable = aNode.isContentEditable; + if (!editable && aNode.ownerDocument) { + editable = aNode.ownerDocument.designMode == "on"; + } + if (!editable) { + return false; + } + // Otherwise make sure that nothing in the parent chain disables spellchecking + return aNode.spellcheck; + }, + + _isProprietaryDRM: function() { + return this.target.isEncrypted && this.target.mediaKeys && + this.target.mediaKeys.keySystem != "org.w3.clearkey"; + }, + + _openLinkInParameters : function (extra) { + let params = { charset: gContextMenuContentData.charSet, + originPrincipal: this.principal, + triggeringPrincipal: this.principal, + referrerURI: gContextMenuContentData.documentURIObject, + referrerPolicy: gContextMenuContentData.referrerPolicy, + noReferrer: this.linkHasNoReferrer }; + for (let p in extra) { + params[p] = extra[p]; + } + + // If we want to change userContextId, we must be sure that we don't + // propagate the referrer. + if ("userContextId" in params && + params.userContextId != gContextMenuContentData.userContextId) { + params.noReferrer = true; + } + + return params; + }, + + // Open linked-to URL in a new window. + openLink : function () { + urlSecurityCheck(this.linkURL, this.principal); + openLinkIn(this.linkURL, "window", this._openLinkInParameters()); + }, + + // Open linked-to URL in a new private window. + openLinkInPrivateWindow : function () { + urlSecurityCheck(this.linkURL, this.principal); + openLinkIn(this.linkURL, "window", + this._openLinkInParameters({ private: true })); + }, + + // Open linked-to URL in a new tab. + openLinkInTab: function(event) { + urlSecurityCheck(this.linkURL, this.principal); + let referrerURI = gContextMenuContentData.documentURIObject; + + // if its parent allows mixed content 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. + let persistAllowMixedContentInChildTab = false; + + if (gContextMenuContentData.parentAllowsMixedContent) { + const sm = Services.scriptSecurityManager; + try { + let targetURI = this.linkURI; + sm.checkSameOriginURI(referrerURI, targetURI, false); + persistAllowMixedContentInChildTab = true; + } + catch (e) { } + } + + let params = { + allowMixedContent: persistAllowMixedContentInChildTab, + userContextId: parseInt(event.target.getAttribute('data-usercontextid')), + }; + + openLinkIn(this.linkURL, "tab", this._openLinkInParameters(params)); + }, + + // open URL in current tab + openLinkInCurrent: function() { + urlSecurityCheck(this.linkURL, this.principal); + openLinkIn(this.linkURL, "current", this._openLinkInParameters()); + }, + + // Open frame in a new tab. + openFrameInTab: function() { + let referrer = gContextMenuContentData.referrer; + openLinkIn(gContextMenuContentData.docLocation, "tab", + { charset: gContextMenuContentData.charSet, + referrerURI: referrer ? makeURI(referrer) : null }); + }, + + // Reload clicked-in frame. + reloadFrame: function() { + this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadFrame", + null, { target: this.target }); + }, + + // Open clicked-in frame in its own window. + openFrame: function() { + let referrer = gContextMenuContentData.referrer; + openLinkIn(gContextMenuContentData.docLocation, "window", + { charset: gContextMenuContentData.charSet, + referrerURI: referrer ? makeURI(referrer) : null }); + }, + + // Open clicked-in frame in the same window. + showOnlyThisFrame: function() { + urlSecurityCheck(gContextMenuContentData.docLocation, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + let referrer = gContextMenuContentData.referrer; + openUILinkIn(gContextMenuContentData.docLocation, "current", + { disallowInheritPrincipal: true, + referrerURI: referrer ? makeURI(referrer) : null }); + }, + + reload: function(event) { + BrowserReloadOrDuplicate(event); + }, + + // View Partial Source + viewPartialSource: function(aContext) { + let inWindow = !Services.prefs.getBoolPref("view_source.tab"); + let openSelectionFn = inWindow ? null : function() { + let tabBrowser = gBrowser; + // 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; + } + let tab = tabBrowser.loadOneTab("about:blank", { + relatedToCurrent: true, + inBackground: false + }); + return tabBrowser.getBrowserForTab(tab); + } + + let target = aContext == "mathml" ? this.target : null; + top.gViewSourceUtils.viewPartialSourceInBrowser(gBrowser.selectedBrowser, target, openSelectionFn); + }, + + // Open new "view source" window with the frame's URL. + viewFrameSource: function() { + BrowserViewSourceOfDocument({ + browser: this.browser, + URL: gContextMenuContentData.docLocation, + outerWindowID: this.frameOuterWindowID, + }); + }, + + viewInfo: function() { + BrowserPageInfo(gContextMenuContentData.docLocation, null, null, null, this.browser); + }, + + viewImageInfo: function() { + BrowserPageInfo(gContextMenuContentData.docLocation, "mediaTab", + this.target, null, this.browser); + }, + + viewImageDesc: function(e) { + urlSecurityCheck(this.imageDescURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + openUILink(this.imageDescURL, e, { disallowInheritPrincipal: true, + referrerURI: gContextMenuContentData.documentURIObject }); + }, + + viewFrameInfo: function() { + BrowserPageInfo(gContextMenuContentData.docLocation, null, null, + this.frameOuterWindowID, this.browser); + }, + + reloadImage: function() { + urlSecurityCheck(this.mediaURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + + this.browser.messageManager.sendAsyncMessage("ContextMenu:ReloadImage", + null, { target: this.target }); + }, + + _canvasToBlobURL: function(target) { + let mm = this.browser.messageManager; + return new Promise(function(resolve) { + mm.sendAsyncMessage("ContextMenu:Canvas:ToBlobURL", {}, { target }); + + let onMessage = (message) => { + mm.removeMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage); + resolve(message.data.blobURL); + }; + mm.addMessageListener("ContextMenu:Canvas:ToBlobURL:Result", onMessage); + }); + }, + + // Change current window to the URL of the image, video, or audio. + viewMedia: function(e) { + let referrerURI = gContextMenuContentData.documentURIObject; + let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + if (this.onCanvas) { + this._canvasToBlobURL(this.target).then(function(blobURL) { + openUILink(blobURL, e, { disallowInheritPrincipal: true, + referrerURI: referrerURI, + triggeringPrincipal: systemPrincipal}); + }, Cu.reportError); + } + else { + urlSecurityCheck(this.mediaURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + openUILink(this.mediaURL, e, { disallowInheritPrincipal: true, + referrerURI: referrerURI, + forceAllowDataURI: true }); + } + }, + + saveVideoFrameAsImage: function () { + let mm = this.browser.messageManager; + let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser); + + let name = ""; + if (this.mediaURL) { + try { + let uri = makeURI(this.mediaURL); + let url = uri.QueryInterface(Ci.nsIURL); + if (url.fileBaseName) + name = decodeURI(url.fileBaseName) + ".jpg"; + } catch (e) { } + } + if (!name) + name = "snapshot.jpg"; + + mm.sendAsyncMessage("ContextMenu:SaveVideoFrameAsImage", {}, { + target: this.target, + }); + + let onMessage = (message) => { + mm.removeMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage); + let dataURL = message.data.dataURL; + saveImageURL(dataURL, name, "SaveImageTitle", true, false, + document.documentURIObject, null, null, null, + isPrivate); + }; + mm.addMessageListener("ContextMenu:SaveVideoFrameAsImage:Result", onMessage); + }, + + leaveDOMFullScreen: function() { + document.exitFullscreen(); + }, + + // Change current window to the URL of the background image. + viewBGImage: function(e) { + urlSecurityCheck(this.bgImageURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true, + referrerURI: gContextMenuContentData.documentURIObject }); + }, + + setDesktopBackground: function() { + let mm = this.browser.messageManager; + + mm.sendAsyncMessage("ContextMenu:SetAsDesktopBackground", null, + { target: this.target }); + + let onMessage = (message) => { + mm.removeMessageListener("ContextMenu:SetAsDesktopBackground:Result", + onMessage); + + if (message.data.disable) + return; + + let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img'); + image.src = message.data.dataUrl; + + // Confirm since it's annoying if you hit this accidentally. + const kDesktopBackgroundURL = + "chrome://browser/content/setDesktopBackground.xul"; +#ifdef XP_MACOSX + // On Mac, the Set Desktop Background window is not modal. + // Don't open more than one Set Desktop Background window. + const wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + let dbWin = wm.getMostRecentWindow("Shell:SetDesktopBackground"); + if (dbWin) { + dbWin.gSetBackground.init(image); + dbWin.focus(); + } + else { + openDialog(kDesktopBackgroundURL, "", + "centerscreen,chrome,dialog=no,dependent,resizable=no", + image); + } +#else + // On non-Mac platforms, the Set Wallpaper dialog is modal. + openDialog(kDesktopBackgroundURL, "", + "centerscreen,chrome,dialog,modal,dependent", + image); +#endif + }; + + mm.addMessageListener("ContextMenu:SetAsDesktopBackground:Result", onMessage); + }, + + // Save URL of clicked-on frame. + saveFrame: function () { + saveBrowser(this.browser, false, this.frameOuterWindowID); + }, + + // Helper function to wait for appropriate MIME-type headers and + // then prompt the user with a file picker + saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc, docURI, + windowID, linkDownload) { + // canonical def in nsURILoader.h + const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020; + + // an object to proxy the data through to + // nsIExternalHelperAppService.doContent, which will wait for the + // appropriate MIME-type headers and then prompt the user with a + // file picker + function saveAsListener() {} + saveAsListener.prototype = { + extListener: null, + + onStartRequest: function saveLinkAs_onStartRequest(aRequest, aContext) { + + // if the timer fired, the error status will have been caused by that, + // and we'll be restarting in onStopRequest, so no reason to notify + // the user + if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT) + return; + + timer.cancel(); + + // some other error occured; notify the user... + if (!Components.isSuccessCode(aRequest.status)) { + try { + const sbs = Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService); + const bundle = sbs.createBundle( + "chrome://mozapps/locale/downloads/downloads.properties"); + + const title = bundle.GetStringFromName("downloadErrorAlertTitle"); + const msg = bundle.GetStringFromName("downloadErrorGeneric"); + + const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService); + const wm = Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(Ci.nsIWindowMediator); + let window = wm.getOuterWindowWithId(windowID); + promptSvc.alert(window, title, msg); + } catch (ex) {} + return; + } + + let extHelperAppSvc = + Cc["@mozilla.org/uriloader/external-helper-app-service;1"]. + getService(Ci.nsIExternalHelperAppService); + let channel = aRequest.QueryInterface(Ci.nsIChannel); + this.extListener = + extHelperAppSvc.doContent(channel.contentType, aRequest, + null, true, window); + this.extListener.onStartRequest(aRequest, aContext); + }, + + onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext, + aStatusCode) { + if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) { + // do it the old fashioned way, which will pick the best filename + // it can without waiting. + saveURL(linkURL, linkText, dialogTitle, bypassCache, false, docURI, + doc); + } + if (this.extListener) + this.extListener.onStopRequest(aRequest, aContext, aStatusCode); + }, + + onDataAvailable: function saveLinkAs_onDataAvailable(aRequest, aContext, + aInputStream, + aOffset, aCount) { + this.extListener.onDataAvailable(aRequest, aContext, aInputStream, + aOffset, aCount); + } + } + + function callbacks() {} + callbacks.prototype = { + getInterface: function sLA_callbacks_getInterface(aIID) { + if (aIID.equals(Ci.nsIAuthPrompt) || aIID.equals(Ci.nsIAuthPrompt2)) { + // If the channel demands authentication prompt, we must cancel it + // because the save-as-timer would expire and cancel the channel + // before we get credentials from user. Both authentication dialog + // and save as dialog would appear on the screen as we fall back to + // the old fashioned way after the timeout. + timer.cancel(); + channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT); + } + throw Cr.NS_ERROR_NO_INTERFACE; + } + } + + // if it we don't have the headers after a short time, the user + // won't have received any feedback from their click. that's bad. so + // we give up waiting for the filename. + function timerCallback() {} + timerCallback.prototype = { + notify: function sLA_timer_notify(aTimer) { + channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT); + return; + } + } + + // setting up a new channel for 'right click - save link as ...' + // ideally we should use: + // * doc - as the loadingNode, and/or + // * this.principal - as the loadingPrincipal + // for now lets use systemPrincipal to bypass mixedContentBlocker + // checks after redirects, see bug: 1136055 + var channel = NetUtil.newChannel({ + uri: makeURI(linkURL), + loadUsingSystemPrincipal: true + }); + + if (linkDownload) + channel.contentDispositionFilename = linkDownload; + if (channel instanceof Ci.nsIPrivateBrowsingChannel) { + let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser); + channel.setPrivate(docIsPrivate); + } + channel.notificationCallbacks = new callbacks(); + + let flags = Ci.nsIChannel.LOAD_CALL_CONTENT_SNIFFERS; + + if (bypassCache) + flags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + + if (channel instanceof Ci.nsICachingChannel) + flags |= Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY; + + channel.loadFlags |= flags; + + if (channel instanceof Ci.nsIHttpChannel) { + channel.referrer = docURI; + if (channel instanceof Ci.nsIHttpChannelInternal) + channel.forceAllowThirdPartyCookie = true; + } + + // fallback to the old way if we don't see the headers quickly + var timeToWait = + gPrefService.getIntPref("browser.download.saveLinkAsFilenameTimeout"); + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(new timerCallback(), timeToWait, + timer.TYPE_ONE_SHOT); + + // kick off the channel with our proxy object as the listener + channel.asyncOpen2(new saveAsListener()); + }, + + // Save URL of clicked-on link. + saveLink: function() { + urlSecurityCheck(this.linkURL, this.principal); + this.saveHelper(this.linkURL, this.linkTextStr, null, true, this.ownerDoc, + gContextMenuContentData.documentURIObject, + this.frameOuterWindowID, + this.linkDownload); + }, + + // Backwards-compatibility wrapper + saveImage : function() { + if (this.onCanvas || this.onImage) + this.saveMedia(); + }, + + // Save URL of the clicked upon image, video, or audio. + saveMedia: function() { + let doc = this.ownerDoc; + let referrerURI = gContextMenuContentData.documentURIObject; + let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.browser); + if (this.onCanvas) { + // Bypass cache, since it's a data: URL. + this._canvasToBlobURL(this.target).then(function(blobURL) { + saveImageURL(blobURL, "canvas.png", "SaveImageTitle", + true, false, referrerURI, null, null, null, + isPrivate); + }, Cu.reportError); + } + else if (this.onImage) { + urlSecurityCheck(this.mediaURL, this.principal); + saveImageURL(this.mediaURL, null, "SaveImageTitle", false, + false, referrerURI, null, gContextMenuContentData.contentType, + gContextMenuContentData.contentDisposition, isPrivate); + } + else if (this.onVideo || this.onAudio) { + urlSecurityCheck(this.mediaURL, this.principal); + var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle"; + this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI, + this.frameOuterWindowID, ""); + } + }, + + // Backwards-compatibility wrapper + sendImage : function() { + if (this.onCanvas || this.onImage) + this.sendMedia(); + }, + + sendMedia: function() { + MailIntegration.sendMessage(this.mediaURL, ""); + }, + + castVideo: function() { + CastingApps.openExternal(this.target, window); + }, + + populateCastVideoMenu: function(popup) { + let videoEl = this.target; + popup.innerHTML = null; + let doc = popup.ownerDocument; + let services = CastingApps.getServicesForVideo(videoEl); + services.forEach(service => { + let item = doc.createElement("menuitem"); + item.setAttribute("label", service.friendlyName); + item.addEventListener("command", event => { + CastingApps.sendVideoToService(videoEl, service); + }); + popup.appendChild(item); + }); + }, + + playPlugin: function() { + gPluginHandler.contextMenuCommand(this.browser, this.target, "play"); + }, + + hidePlugin: function() { + gPluginHandler.contextMenuCommand(this.browser, this.target, "hide"); + }, + + // Generate email address and put it on clipboard. + copyEmail: function() { + // Copy the comma-separated list of email addresses only. + // There are other ways of embedding email addresses in a mailto: + // link, but such complex parsing is beyond us. + var url = this.linkURL; + var qmark = url.indexOf("?"); + var addresses; + + // 7 == length of "mailto:" + addresses = qmark > 7 ? url.substring(7, qmark) : url.substr(7); + + // Let's try to unescape it using a character set + // in case the address is not ASCII. + try { + const textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"]. + getService(Ci.nsITextToSubURI); + addresses = textToSubURI.unEscapeURIForUI(gContextMenuContentData.charSet, + addresses); + } + catch(ex) { + // Do nothing. + } + + var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + clipboard.copyString(addresses); + }, + + copyLink: function() { + // If we're in a view source tab, remove the view-source: prefix + let linkURL = this.linkURL.replace(/^view-source:/, ""); + var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + clipboard.copyString(linkURL); + }, + + /////////////// + // Utilities // + /////////////// + + // Show/hide one item (specified via name or the item element itself). + showItem: function(aItemOrId, aShow) { + var item = aItemOrId.constructor == String ? + document.getElementById(aItemOrId) : aItemOrId; + if (item) + item.hidden = !aShow; + }, + + // Set given attribute of specified context-menu item. If the + // value is null, then it removes the attribute (which works + // nicely for the disabled attribute). + setItemAttr: function(aID, aAttr, aVal ) { + var elem = document.getElementById(aID); + if (elem) { + if (aVal == null) { + // null indicates attr should be removed. + elem.removeAttribute(aAttr); + } + else { + // Set attr=val. + elem.setAttribute(aAttr, aVal); + } + } + }, + + // Set context menu attribute according to like attribute of another node + // (such as a broadcaster). + setItemAttrFromNode: function(aItem_id, aAttr, aOther_id) { + var elem = document.getElementById(aOther_id); + if (elem && elem.getAttribute(aAttr) == "true") + this.setItemAttr(aItem_id, aAttr, "true"); + else + this.setItemAttr(aItem_id, aAttr, null); + }, + + // Temporary workaround for DOM api not yet implemented by XUL nodes. + cloneNode: function(aItem) { + // Create another element like the one we're cloning. + var node = document.createElement(aItem.tagName); + + // Copy attributes from argument item to the new one. + var attrs = aItem.attributes; + for (var i = 0; i < attrs.length; i++) { + var attr = attrs.item(i); + node.setAttribute(attr.nodeName, attr.nodeValue); + } + + // Voila! + return node; + }, + + // Generate fully qualified URL for clicked-on link. + getLinkURL: function() { + var href = this.link.href; + if (href) + return href; + + href = this.link.getAttribute("href") || + this.link.getAttributeNS("http://www.w3.org/1999/xlink", "href"); + + if (!href || !href.match(/\S/)) { + // Without this we try to save as the current doc, + // for example, HTML case also throws if empty + throw "Empty href"; + } + + return makeURLAbsolute(this.link.baseURI, href); + }, + + getLinkURI: function() { + try { + return makeURI(this.linkURL); + } + catch (ex) { + // e.g. empty URL string + } + + return null; + }, + + getLinkProtocol: function() { + if (this.linkURI) + return this.linkURI.scheme; // can be |undefined| + + return null; + }, + + // Get text of link. + getLinkText: function() { + var text = gatherTextUnder(this.link); + if (!text || !text.match(/\S/)) { + text = this.link.getAttribute("title"); + if (!text || !text.match(/\S/)) { + text = this.link.getAttribute("alt"); + if (!text || !text.match(/\S/)) + text = this.linkURL; + } + } + + return text; + }, + + // Kept for addon compat + linkText: function() { + return this.linkTextStr; + }, + + isMediaURLReusable: function(aURL) { + if (aURL.startsWith("blob:")) { + return URL.isValidURL(aURL); + } + return true; + }, + + toString: function () { + return "contextMenu.target = " + this.target + "\n" + + "contextMenu.onImage = " + this.onImage + "\n" + + "contextMenu.onLink = " + this.onLink + "\n" + + "contextMenu.link = " + this.link + "\n" + + "contextMenu.inFrame = " + this.inFrame + "\n" + + "contextMenu.hasBGImage = " + this.hasBGImage + "\n"; + }, + + isTargetATextBox: function(node) { + if (node instanceof HTMLInputElement) + return node.mozIsTextField(false); + + return (node instanceof HTMLTextAreaElement); + }, + + // Determines whether or not the separator with the specified ID should be + // shown or not by determining if there are any non-hidden items between it + // and the previous separator. + shouldShowSeparator: function (aSeparatorID) { + var separator = document.getElementById(aSeparatorID); + if (separator) { + var sibling = separator.previousSibling; + while (sibling && sibling.localName != "menuseparator") { + if (!sibling.hidden) + return true; + sibling = sibling.previousSibling; + } + } + return false; + }, + + addDictionaries: function() { + var uri = formatURL("browser.dictionaries.download.url", true); + + var locale = "-"; + try { + locale = gPrefService.getComplexValue("intl.accept_languages", + Ci.nsIPrefLocalizedString).data; + } + catch (e) { } + + var version = "-"; + try { + version = Cc["@mozilla.org/xre/app-info;1"]. + getService(Ci.nsIXULAppInfo).version; + } + catch (e) { } + + uri = uri.replace(/%LOCALE%/, escape(locale)).replace(/%VERSION%/, version); + + var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow"); + var where = newWindowPref == 3 ? "tab" : "window"; + + openUILinkIn(uri, where); + }, + + bookmarkThisPage: function CM_bookmarkThisPage() { + window.top.PlacesCommandHook.bookmarkPage(this.browser, PlacesUtils.bookmarksMenuFolderId, true); + }, + + bookmarkLink: function CM_bookmarkLink() { + window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId, + this.linkURL, this.linkTextStr); + }, + + addBookmarkForFrame: function CM_addBookmarkForFrame() { + let uri = gContextMenuContentData.documentURIObject; + let mm = this.browser.messageManager; + + let onMessage = (message) => { + mm.removeMessageListener("ContextMenu:BookmarkFrame:Result", onMessage); + + window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId, + uri.spec, + message.data.title, + message.data.description) + .catch(Components.utils.reportError); + }; + mm.addMessageListener("ContextMenu:BookmarkFrame:Result", onMessage); + + mm.sendAsyncMessage("ContextMenu:BookmarkFrame", null, { target: this.target }); + }, + + savePageAs: function CM_savePageAs() { + saveBrowser(this.browser); + }, + + printFrame: function CM_printFrame() { + PrintUtils.printWindow(this.frameOuterWindowID, this.browser); + }, + + switchPageDirection: function CM_switchPageDirection() { + this.browser.messageManager.sendAsyncMessage("SwitchDocumentDirection"); + }, + + mediaCommand : function CM_mediaCommand(command, data) { + let mm = this.browser.messageManager; + let win = this.browser.ownerGlobal; + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + mm.sendAsyncMessage("ContextMenu:MediaCommand", + {command: command, + data: data, + handlingUserInput: windowUtils.isHandlingUserInput}, + {element: this.target}); + }, + + copyMediaLocation : function () { + var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + clipboard.copyString(this.mediaURL); + }, + + drmLearnMore: function(aEvent) { + let drmInfoURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "drm-content"; + let dest = whereToOpenLink(aEvent); + // Don't ever want this to open in the same tab as it'll unload the + // DRM'd video, which is going to be a bad idea in most cases. + if (dest == "current") { + dest = "tab"; + } + openUILinkIn(drmInfoURL, dest); + }, + + get imageURL() { + if (this.onImage) + return this.mediaURL; + return ""; + }, + + // Formats the 'Search <engine> for "<selection or link text>"' context menu. + formatSearchContextItem: function() { + var menuItem = document.getElementById("context-searchselect"); + let selectedText = this.isTextSelected ? this.textSelected : this.linkTextStr; + + // Store searchTerms in context menu item so we know what to search onclick + menuItem.searchTerms = selectedText; + + // Copied to alert.js' prefillAlertInfo(). + // If the JS character after our truncation point is a trail surrogate, + // include it in the truncated string to avoid splitting a surrogate pair. + if (selectedText.length > 15) { + let truncLength = 15; + let truncChar = selectedText[15].charCodeAt(0); + if (truncChar >= 0xDC00 && truncChar <= 0xDFFF) + truncLength++; + selectedText = selectedText.substr(0,truncLength) + this.ellipsis; + } + + // format "Search <engine> for <selection>" string to show in menu + let engineName = Services.search.currentEngine.name; + var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch", + [engineName, + selectedText]); + menuItem.label = menuLabel; + menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey"); + }, + + _getTelemetryClickInfo: function(aXulMenu) { + this._onPopupHiding = () => { + aXulMenu.ownerDocument.removeEventListener("command", activationHandler, true); + aXulMenu.removeEventListener("popuphiding", this._onPopupHiding, true); + delete this._onPopupHiding; + + let eventKey = [ + this._telemetryPageContext, + this._telemetryHadCustomItems ? "withcustom" : "withoutcustom" + ]; + let target = this._telemetryClickID || "close-without-interaction"; + BrowserUITelemetry.registerContextMenuInteraction(eventKey, target); + }; + let activationHandler = (e) => { + // Deal with command events being routed to command elements; figure out + // what triggered the event (which will have the right e.target) + if (e.sourceEvent) { + e = e.sourceEvent; + } + // Target should be in the menu (this catches using shortcuts for items + // not in the menu while the menu is up) + if (!aXulMenu.contains(e.target)) { + return; + } + + // Check if this is a page menu item: + if (e.target.hasAttribute(PageMenuParent.GENERATEDITEMID_ATTR)) { + this._telemetryClickID = "custom-page-item"; + } else { + this._telemetryClickID = (e.target.id || "unknown").replace(/^context-/i, ""); + } + }; + aXulMenu.ownerDocument.addEventListener("command", activationHandler, true); + aXulMenu.addEventListener("popuphiding", this._onPopupHiding, true); + }, + + _getTelemetryPageContextInfo: function() { + let rv = []; + for (let k of ["isContentSelected", "onLink", "onImage", "onCanvas", "onVideo", "onAudio", + "onTextInput"]) { + if (this[k]) { + rv.push(k.replace(/^(?:is|on)(.)/, (match, firstLetter) => firstLetter.toLowerCase())); + } + } + if (!rv.length) { + rv.push('other'); + } + + return JSON.stringify(rv); + }, + + _checkTelemetryForMenu: function(aXulMenu) { + this._telemetryClickID = null; + this._telemetryPageContext = this._getTelemetryPageContextInfo(); + this._telemetryHadCustomItems = this.hasPageMenu; + this._getTelemetryClickInfo(aXulMenu); + }, + + createContainerMenu: function(aEvent) { + return createUserContextMenu(aEvent, true, + gContextMenuContentData.userContextId); + }, +}; diff --git a/application/basilisk/base/content/overrides/app-license.html b/application/basilisk/base/content/overrides/app-license.html new file mode 100644 index 000000000..0a1f0d8f5 --- /dev/null +++ b/application/basilisk/base/content/overrides/app-license.html @@ -0,0 +1,7 @@ +<!-- 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/. --> + <p><b>Binaries</b> of this product have been made available to you by the + <a href="http://www.palemoon.org/">Pale Moon project team</a> and + <a href="http://www.moonchildproductions.info/">Moonchild Productions</a> under the Mozilla + Public License 2.0 (MPL). <a href="about:rights">Know your rights</a>.</p> diff --git a/application/basilisk/base/content/pageinfo/feeds.js b/application/basilisk/base/content/pageinfo/feeds.js new file mode 100644 index 000000000..c9731b4ef --- /dev/null +++ b/application/basilisk/base/content/pageinfo/feeds.js @@ -0,0 +1,32 @@ +/* -*- 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/. */ + +function initFeedTab(feeds) +{ + for (let feed of feeds) { + let [name, type, url] = feed; + addRow(name, type, url); + } + + var feedListbox = document.getElementById("feedListbox"); + document.getElementById("feedTab").hidden = feedListbox.getRowCount() == 0; +} + +function onSubscribeFeed() +{ + var listbox = document.getElementById("feedListbox"); + openUILinkIn(listbox.selectedItem.getAttribute("feedURL"), "current", + { ignoreAlt: true }); +} + +function addRow(name, type, url) +{ + var item = document.createElement("richlistitem"); + item.setAttribute("feed", "true"); + item.setAttribute("name", name); + item.setAttribute("type", type); + item.setAttribute("feedURL", url); + document.getElementById("feedListbox").appendChild(item); +} diff --git a/application/basilisk/base/content/pageinfo/feeds.xml b/application/basilisk/base/content/pageinfo/feeds.xml new file mode 100644 index 000000000..782c05a73 --- /dev/null +++ b/application/basilisk/base/content/pageinfo/feeds.xml @@ -0,0 +1,40 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings [ + <!ENTITY % pageInfoDTD SYSTEM "chrome://browser/locale/pageInfo.dtd"> + %pageInfoDTD; +]> + +<bindings id="feedBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="feed" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:vbox flex="1"> + <xul:hbox flex="1"> + <xul:textbox flex="1" readonly="true" xbl:inherits="value=name" + class="feedTitle"/> + <xul:label xbl:inherits="value=type"/> + </xul:hbox> + <xul:vbox> + <xul:vbox align="start"> + <xul:hbox> + <xul:label xbl:inherits="value=feedURL,tooltiptext=feedURL" class="text-link" flex="1" + onclick="openUILink(this.value, event);" crop="end"/> + </xul:hbox> + </xul:vbox> + </xul:vbox> + <xul:hbox flex="1" class="feed-subscribe"> + <xul:spacer flex="1"/> + <xul:button label="&feedSubscribe;" accesskey="&feedSubscribe.accesskey;" + oncommand="onSubscribeFeed()"/> + </xul:hbox> + </xul:vbox> + </content> + </binding> +</bindings> diff --git a/application/basilisk/base/content/pageinfo/pageInfo.css b/application/basilisk/base/content/pageinfo/pageInfo.css new file mode 100644 index 000000000..622b56bb5 --- /dev/null +++ b/application/basilisk/base/content/pageinfo/pageInfo.css @@ -0,0 +1,26 @@ +/* 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/. */ + + +#viewGroup > radio { + -moz-binding: url("chrome://browser/content/pageinfo/pageInfo.xml#viewbutton"); +} + +richlistitem[feed] { + -moz-binding: url("chrome://browser/content/pageinfo/feeds.xml#feed"); +} + +richlistitem[feed]:not([selected="true"]) .feed-subscribe { + display: none; +} + +groupbox[closed="true"] > .groupbox-body { + visibility: collapse; +} + +#thepreviewimage { + display: block; +/* This following entry can be removed when Bug 522850 is fixed. */ + min-width: 1px; +} diff --git a/application/basilisk/base/content/pageinfo/pageInfo.js b/application/basilisk/base/content/pageinfo/pageInfo.js new file mode 100644 index 000000000..7a6d0a063 --- /dev/null +++ b/application/basilisk/base/content/pageinfo/pageInfo.js @@ -0,0 +1,1140 @@ +/* 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/. */ + +Components.utils.import("resource://gre/modules/LoadContextInfo.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +// define a js object to implement nsITreeView +function pageInfoTreeView(treeid, copycol) +{ + // copycol is the index number for the column that we want to add to + // the copy-n-paste buffer when the user hits accel-c + this.treeid = treeid; + this.copycol = copycol; + this.rows = 0; + this.tree = null; + this.data = [ ]; + this.selection = null; + this.sortcol = -1; + this.sortdir = false; +} + +pageInfoTreeView.prototype = { + set rowCount(c) { throw "rowCount is a readonly property"; }, + get rowCount() { return this.rows; }, + + setTree: function(tree) + { + this.tree = tree; + }, + + getCellText: function(row, column) + { + // row can be null, but js arrays are 0-indexed. + // colidx cannot be null, but can be larger than the number + // of columns in the array. In this case it's the fault of + // whoever typoed while calling this function. + return this.data[row][column.index] || ""; + }, + + setCellValue: function(row, column, value) + { + }, + + setCellText: function(row, column, value) + { + this.data[row][column.index] = value; + }, + + addRow: function(row) + { + this.rows = this.data.push(row); + this.rowCountChanged(this.rows - 1, 1); + if (this.selection.count == 0 && this.rowCount && !gImageElement) { + this.selection.select(0); + } + }, + + addRows: function(rows) + { + for (let row of rows) { + this.addRow(row); + } + }, + + rowCountChanged: function(index, count) + { + this.tree.rowCountChanged(index, count); + }, + + invalidate: function() + { + this.tree.invalidate(); + }, + + clear: function() + { + if (this.tree) + this.tree.rowCountChanged(0, -this.rows); + this.rows = 0; + this.data = [ ]; + }, + + handleCopy: function(row) + { + return (row < 0 || this.copycol < 0) ? "" : (this.data[row][this.copycol] || ""); + }, + + performActionOnRow: function(action, row) + { + if (action == "copy") { + var data = this.handleCopy(row) + this.tree.treeBody.parentNode.setAttribute("copybuffer", data); + } + }, + + onPageMediaSort : function(columnname) + { + var tree = document.getElementById(this.treeid); + var treecol = tree.columns.getNamedColumn(columnname); + + this.sortdir = + gTreeUtils.sort( + tree, + this, + this.data, + treecol.index, + function textComparator(a, b) { return (a || "").toLowerCase().localeCompare((b || "").toLowerCase()); }, + this.sortcol, + this.sortdir + ); + + Array.forEach(tree.columns, function(col) { + col.element.removeAttribute("sortActive"); + col.element.removeAttribute("sortDirection"); + }); + treecol.element.setAttribute("sortActive", "true"); + treecol.element.setAttribute("sortDirection", this.sortdir ? + "ascending" : "descending"); + + this.sortcol = treecol.index; + }, + + getRowProperties: function(row) { return ""; }, + getCellProperties: function(row, column) { return ""; }, + getColumnProperties: function(column) { return ""; }, + isContainer: function(index) { return false; }, + isContainerOpen: function(index) { return false; }, + isSeparator: function(index) { return false; }, + isSorted: function() { return this.sortcol > -1 }, + canDrop: function(index, orientation) { return false; }, + drop: function(row, orientation) { return false; }, + getParentIndex: function(index) { return 0; }, + hasNextSibling: function(index, after) { return false; }, + getLevel: function(index) { return 0; }, + getImageSrc: function(row, column) { }, + getProgressMode: function(row, column) { }, + getCellValue: function(row, column) { }, + toggleOpenState: function(index) { }, + cycleHeader: function(col) { }, + selectionChanged: function() { }, + cycleCell: function(row, column) { }, + isEditable: function(row, column) { return false; }, + isSelectable: function(row, column) { return false; }, + performAction: function(action) { }, + performActionOnCell: function(action, row, column) { } +}; + +// mmm, yummy. global variables. +var gDocInfo = null; +var gImageElement = null; + +// column number to help using the data array +const COL_IMAGE_ADDRESS = 0; +const COL_IMAGE_TYPE = 1; +const COL_IMAGE_SIZE = 2; +const COL_IMAGE_ALT = 3; +const COL_IMAGE_COUNT = 4; +const COL_IMAGE_NODE = 5; +const COL_IMAGE_BG = 6; + +// column number to copy from, second argument to pageInfoTreeView's constructor +const COPYCOL_NONE = -1; +const COPYCOL_META_CONTENT = 1; +const COPYCOL_IMAGE = COL_IMAGE_ADDRESS; + +// one nsITreeView for each tree in the window +var gMetaView = new pageInfoTreeView('metatree', COPYCOL_META_CONTENT); +var gImageView = new pageInfoTreeView('imagetree', COPYCOL_IMAGE); + +gImageView.getCellProperties = function(row, col) { + var data = gImageView.data[row]; + var item = gImageView.data[row][COL_IMAGE_NODE]; + var props = ""; + if (!checkProtocol(data) || + item instanceof HTMLEmbedElement || + (item instanceof HTMLObjectElement && !item.type.startsWith("image/"))) + props += "broken"; + + if (col.element.id == "image-address") + props += " ltr"; + + return props; +}; + +gImageView.getCellText = function(row, column) { + var value = this.data[row][column.index]; + if (column.index == COL_IMAGE_SIZE) { + if (value == -1) { + return gStrings.unknown; + } + var kbSize = Number(Math.round(value / 1024 * 100) / 100); + return gBundle.getFormattedString("mediaFileSize", [kbSize]); + } + return value || ""; +}; + +gImageView.onPageMediaSort = function(columnname) { + var tree = document.getElementById(this.treeid); + var treecol = tree.columns.getNamedColumn(columnname); + + var comparator; + var index = treecol.index; + if (index == COL_IMAGE_SIZE || index == COL_IMAGE_COUNT) { + comparator = function numComparator(a, b) { return a - b; }; + } else { + comparator = function textComparator(a, b) { return (a || "").toLowerCase().localeCompare((b || "").toLowerCase()); }; + } + + this.sortdir = + gTreeUtils.sort( + tree, + this, + this.data, + index, + comparator, + this.sortcol, + this.sortdir + ); + + Array.forEach(tree.columns, function(col) { + col.element.removeAttribute("sortActive"); + col.element.removeAttribute("sortDirection"); + }); + treecol.element.setAttribute("sortActive", "true"); + treecol.element.setAttribute("sortDirection", this.sortdir ? + "ascending" : "descending"); + + this.sortcol = index; +}; + +var gImageHash = { }; + +// localized strings (will be filled in when the document is loaded) +// this isn't all of them, these are just the ones that would otherwise have been loaded inside a loop +var gStrings = { }; +var gBundle; + +const PERMISSION_CONTRACTID = "@mozilla.org/permissionmanager;1"; +const PREFERENCES_CONTRACTID = "@mozilla.org/preferences-service;1"; +const ATOM_CONTRACTID = "@mozilla.org/atom-service;1"; + +// a number of services I'll need later +// the cache services +const nsICacheStorageService = Components.interfaces.nsICacheStorageService; +const nsICacheStorage = Components.interfaces.nsICacheStorage; +const cacheService = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"].getService(nsICacheStorageService); + +var loadContextInfo = LoadContextInfo.fromLoadContext( + window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsILoadContext), false); +var diskStorage = cacheService.diskCacheStorage(loadContextInfo, false); + +const nsICookiePermission = Components.interfaces.nsICookiePermission; +const nsIPermissionManager = Components.interfaces.nsIPermissionManager; + +const nsICertificateDialogs = Components.interfaces.nsICertificateDialogs; +const CERTIFICATEDIALOGS_CONTRACTID = "@mozilla.org/nsCertificateDialogs;1" + +// clipboard helper +function getClipboardHelper() { + try { + return Components.classes["@mozilla.org/widget/clipboardhelper;1"].getService(Components.interfaces.nsIClipboardHelper); + } catch (e) { + // do nothing, later code will handle the error + return null; + } +} +const gClipboardHelper = getClipboardHelper(); + +// Interface for image loading content +const nsIImageLoadingContent = Components.interfaces.nsIImageLoadingContent; + +// namespaces, don't need all of these yet... +const XLinkNS = "http://www.w3.org/1999/xlink"; +const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const XMLNS = "http://www.w3.org/XML/1998/namespace"; +const XHTMLNS = "http://www.w3.org/1999/xhtml"; +const XHTML2NS = "http://www.w3.org/2002/06/xhtml2" + +const XHTMLNSre = "^http\:\/\/www\.w3\.org\/1999\/xhtml$"; +const XHTML2NSre = "^http\:\/\/www\.w3\.org\/2002\/06\/xhtml2$"; +const XHTMLre = RegExp(XHTMLNSre + "|" + XHTML2NSre, ""); + +/* Overlays register functions here. + * These arrays are used to hold callbacks that Page Info will call at + * various stages. Use them by simply appending a function to them. + * For example, add a function to onLoadRegistry by invoking + * "onLoadRegistry.push(XXXLoadFunc);" + * The XXXLoadFunc should be unique to the overlay module, and will be + * invoked as "XXXLoadFunc();" + */ + +// These functions are called to build the data displayed in the Page Info window. +var onLoadRegistry = [ ]; + +// These functions are called to remove old data still displayed in +// the window when the document whose information is displayed +// changes. For example, at this time, the list of images of the Media +// tab is cleared. +var onResetRegistry = [ ]; + +// These functions are called once when all the elements in all of the target +// document (and all of its subframes, if any) have been processed +var onFinished = [ ]; + +// These functions are called once when the Page Info window is closed. +var onUnloadRegistry = [ ]; + +/* Called when PageInfo window is loaded. Arguments are: + * window.arguments[0] - (optional) an object consisting of + * - doc: (optional) document to use for source. if not provided, + * the calling window's document will be used + * - initialTab: (optional) id of the inital tab to display + */ +function onLoadPageInfo() +{ + gBundle = document.getElementById("pageinfobundle"); + gStrings.unknown = gBundle.getString("unknown"); + gStrings.notSet = gBundle.getString("notset"); + gStrings.mediaImg = gBundle.getString("mediaImg"); + gStrings.mediaBGImg = gBundle.getString("mediaBGImg"); + gStrings.mediaBorderImg = gBundle.getString("mediaBorderImg"); + gStrings.mediaListImg = gBundle.getString("mediaListImg"); + gStrings.mediaCursor = gBundle.getString("mediaCursor"); + gStrings.mediaObject = gBundle.getString("mediaObject"); + gStrings.mediaEmbed = gBundle.getString("mediaEmbed"); + gStrings.mediaLink = gBundle.getString("mediaLink"); + gStrings.mediaInput = gBundle.getString("mediaInput"); + gStrings.mediaVideo = gBundle.getString("mediaVideo"); + gStrings.mediaAudio = gBundle.getString("mediaAudio"); + + var args = "arguments" in window && + window.arguments.length >= 1 && + window.arguments[0]; + + // init media view + var imageTree = document.getElementById("imagetree"); + imageTree.view = gImageView; + + /* Select the requested tab, if the name is specified */ + loadTab(args); + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .notifyObservers(window, "page-info-dialog-loaded", null); +} + +function loadPageInfo(frameOuterWindowID, imageElement, browser) +{ + browser = browser || window.opener.gBrowser.selectedBrowser; + let mm = browser.messageManager; + + gStrings["application/rss+xml"] = gBundle.getString("feedRss"); + gStrings["application/atom+xml"] = gBundle.getString("feedAtom"); + gStrings["text/xml"] = gBundle.getString("feedXML"); + gStrings["application/xml"] = gBundle.getString("feedXML"); + gStrings["application/rdf+xml"] = gBundle.getString("feedXML"); + + // Look for pageInfoListener in content.js. Sends message to listener with arguments. + mm.sendAsyncMessage("PageInfo:getData", {strings: gStrings, + frameOuterWindowID: frameOuterWindowID}, + { imageElement }); + + let pageInfoData; + + // Get initial pageInfoData needed to display the general, feeds, permission and security tabs. + mm.addMessageListener("PageInfo:data", function onmessage(message) { + mm.removeMessageListener("PageInfo:data", onmessage); + pageInfoData = message.data; + let docInfo = pageInfoData.docInfo; + let windowInfo = pageInfoData.windowInfo; + let uri = makeURI(docInfo.documentURIObject.spec, + docInfo.documentURIObject.originCharset); + let principal = docInfo.principal; + gDocInfo = docInfo; + + gImageElement = pageInfoData.imageInfo; + + var titleFormat = windowInfo.isTopWindow ? "pageInfo.page.title" + : "pageInfo.frame.title"; + document.title = gBundle.getFormattedString(titleFormat, [docInfo.location]); + + document.getElementById("main-window").setAttribute("relatedUrl", docInfo.location); + + makeGeneralTab(pageInfoData.metaViewRows, docInfo); + initFeedTab(pageInfoData.feeds); + onLoadPermission(uri, principal); + securityOnLoad(uri, windowInfo); + }); + + // Get the media elements from content script to setup the media tab. + mm.addMessageListener("PageInfo:mediaData", function onmessage(message) { + // Page info window was closed. + if (window.closed) { + mm.removeMessageListener("PageInfo:mediaData", onmessage); + return; + } + + // The page info media fetching has been completed. + if (message.data.isComplete) { + mm.removeMessageListener("PageInfo:mediaData", onmessage); + onFinished.forEach(function(func) { func(pageInfoData); }); + return; + } + + for (let item of message.data.mediaItems) { + addImage(item); + } + + selectImage(); + }); + + /* Call registered overlay init functions */ + onLoadRegistry.forEach(function(func) { func(); }); +} + +function resetPageInfo(args) +{ + /* Reset Meta tags part */ + gMetaView.clear(); + + /* Reset Media tab */ + var mediaTab = document.getElementById("mediaTab"); + if (!mediaTab.hidden) { + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .removeObserver(imagePermissionObserver, "perm-changed"); + mediaTab.hidden = true; + } + gImageView.clear(); + gImageHash = {}; + + /* Reset Feeds Tab */ + var feedListbox = document.getElementById("feedListbox"); + while (feedListbox.firstChild) + feedListbox.removeChild(feedListbox.firstChild); + + /* Call registered overlay reset functions */ + onResetRegistry.forEach(function(func) { func(); }); + + /* Rebuild the data */ + loadTab(args); +} + +function onUnloadPageInfo() +{ + // Remove the observer, only if there is at least 1 image. + if (!document.getElementById("mediaTab").hidden) { + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .removeObserver(imagePermissionObserver, "perm-changed"); + } + + /* Call registered overlay unload functions */ + onUnloadRegistry.forEach(function(func) { func(); }); +} + +function doHelpButton() +{ + const helpTopics = { + "generalPanel": "pageinfo_general", + "mediaPanel": "pageinfo_media", + "feedPanel": "pageinfo_feed", + "permPanel": "pageinfo_permissions", + "securityPanel": "pageinfo_security" + }; + + var deck = document.getElementById("mainDeck"); + var helpdoc = helpTopics[deck.selectedPanel.id] || "pageinfo_general"; + openHelpLink(helpdoc); +} + +function showTab(id) +{ + var deck = document.getElementById("mainDeck"); + var pagel = document.getElementById(id + "Panel"); + deck.selectedPanel = pagel; +} + +function loadTab(args) +{ + // If the "View Image Info" context menu item was used, the related image + // element is provided as an argument. This can't be a background image. + let imageElement = args && args.imageElement; + let frameOuterWindowID = args && args.frameOuterWindowID; + let browser = args && args.browser; + + /* Load the page info */ + loadPageInfo(frameOuterWindowID, imageElement, browser); + + var initialTab = (args && args.initialTab) || "generalTab"; + var radioGroup = document.getElementById("viewGroup"); + initialTab = document.getElementById(initialTab) || document.getElementById("generalTab"); + radioGroup.selectedItem = initialTab; + radioGroup.selectedItem.doCommand(); + radioGroup.focus(); +} + +function toggleGroupbox(id) +{ + var elt = document.getElementById(id); + if (elt.hasAttribute("closed")) { + elt.removeAttribute("closed"); + if (elt.flexWhenOpened) + elt.flex = elt.flexWhenOpened; + } + else { + elt.setAttribute("closed", "true"); + if (elt.flex) { + elt.flexWhenOpened = elt.flex; + elt.flex = 0; + } + } +} + +function openCacheEntry(key, cb) +{ + var checkCacheListener = { + onCacheEntryCheck: function(entry, appCache) { + return Components.interfaces.nsICacheEntryOpenCallback.ENTRY_WANTED; + }, + onCacheEntryAvailable: function(entry, isNew, appCache, status) { + cb(entry); + } + }; + diskStorage.asyncOpenURI(Services.io.newURI(key, null, null), "", nsICacheStorage.OPEN_READONLY, checkCacheListener); +} + +function makeGeneralTab(metaViewRows, docInfo) +{ + var title = (docInfo.title) ? docInfo.title : gBundle.getString("noPageTitle"); + document.getElementById("titletext").value = title; + + var url = docInfo.location; + setItemValue("urltext", url); + + var referrer = ("referrer" in docInfo && docInfo.referrer); + setItemValue("refertext", referrer); + + var mode = ("compatMode" in docInfo && docInfo.compatMode == "BackCompat") ? "generalQuirksMode" : "generalStrictMode"; + document.getElementById("modetext").value = gBundle.getString(mode); + + // find out the mime type + var mimeType = docInfo.contentType; + setItemValue("typetext", mimeType); + + // get the document characterset + var encoding = docInfo.characterSet; + document.getElementById("encodingtext").value = encoding; + + let length = metaViewRows.length; + + var metaGroup = document.getElementById("metaTags"); + if (!length) + metaGroup.collapsed = true; + else { + var metaTagsCaption = document.getElementById("metaTagsCaption"); + if (length == 1) + metaTagsCaption.label = gBundle.getString("generalMetaTag"); + else + metaTagsCaption.label = gBundle.getFormattedString("generalMetaTags", [length]); + var metaTree = document.getElementById("metatree"); + metaTree.view = gMetaView; + + // Add the metaViewRows onto the general tab's meta info tree. + gMetaView.addRows(metaViewRows); + + metaGroup.collapsed = false; + } + + // get the date of last modification + var modifiedText = formatDate(docInfo.lastModified, gStrings.notSet); + document.getElementById("modifiedtext").value = modifiedText; + + // get cache info + var cacheKey = url.replace(/#.*$/, ""); + openCacheEntry(cacheKey, function(cacheEntry) { + var sizeText; + if (cacheEntry) { + var pageSize = cacheEntry.dataSize; + var kbSize = formatNumber(Math.round(pageSize / 1024 * 100) / 100); + sizeText = gBundle.getFormattedString("generalSize", [kbSize, formatNumber(pageSize)]); + } + setItemValue("sizetext", sizeText); + }); +} + +function addImage(imageViewRow) +{ + let [url, type, alt, elem, isBg] = imageViewRow; + + if (!url) + return; + + if (!gImageHash.hasOwnProperty(url)) + gImageHash[url] = { }; + if (!gImageHash[url].hasOwnProperty(type)) + gImageHash[url][type] = { }; + if (!gImageHash[url][type].hasOwnProperty(alt)) { + gImageHash[url][type][alt] = gImageView.data.length; + var row = [url, type, -1, alt, 1, elem, isBg]; + gImageView.addRow(row); + + // Fill in cache data asynchronously + openCacheEntry(url, function(cacheEntry) { + // The data at row[2] corresponds to the data size. + if (cacheEntry) { + row[2] = cacheEntry.dataSize; + // Invalidate the row to trigger a repaint. + gImageView.tree.invalidateRow(gImageView.data.indexOf(row)); + } + }); + + // Add the observer, only once. + if (gImageView.data.length == 1) { + document.getElementById("mediaTab").hidden = false; + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .addObserver(imagePermissionObserver, "perm-changed", false); + } + } + else { + var i = gImageHash[url][type][alt]; + gImageView.data[i][COL_IMAGE_COUNT]++; + // The same image can occur several times on the page at different sizes. + // If the "View Image Info" context menu item was used, ensure we select + // the correct element. + if (!gImageView.data[i][COL_IMAGE_BG] && + gImageElement && url == gImageElement.currentSrc && + gImageElement.width == elem.width && + gImageElement.height == elem.height && + gImageElement.imageText == elem.imageText) { + gImageView.data[i][COL_IMAGE_NODE] = elem; + } + } +} + +// Link Stuff +function openURL(target) +{ + var url = target.parentNode.childNodes[2].value; + window.open(url, "_blank", "chrome"); +} + +function onBeginLinkDrag(event, urlField, descField) +{ + if (event.originalTarget.localName != "treechildren") + return; + + var tree = event.target; + if (!("treeBoxObject" in tree)) + tree = tree.parentNode; + + var row = tree.treeBoxObject.getRowAt(event.clientX, event.clientY); + if (row == -1) + return; + + // Adding URL flavor + var col = tree.columns[urlField]; + var url = tree.view.getCellText(row, col); + col = tree.columns[descField]; + var desc = tree.view.getCellText(row, col); + + var dt = event.dataTransfer; + dt.setData("text/x-moz-url", url + "\n" + desc); + dt.setData("text/url-list", url); + dt.setData("text/plain", url); +} + +// Image Stuff +function getSelectedRows(tree) +{ + var start = { }; + var end = { }; + var numRanges = tree.view.selection.getRangeCount(); + + var rowArray = [ ]; + for (var t = 0; t < numRanges; t++) { + tree.view.selection.getRangeAt(t, start, end); + for (var v = start.value; v <= end.value; v++) + rowArray.push(v); + } + + return rowArray; +} + +function getSelectedRow(tree) +{ + var rows = getSelectedRows(tree); + return (rows.length == 1) ? rows[0] : -1; +} + +function selectSaveFolder(aCallback) +{ + const nsILocalFile = Components.interfaces.nsILocalFile; + const nsIFilePicker = Components.interfaces.nsIFilePicker; + let titleText = gBundle.getString("mediaSelectFolder"); + let fp = Components.classes["@mozilla.org/filepicker;1"]. + createInstance(nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK) { + aCallback(fp.file.QueryInterface(nsILocalFile)); + } else { + aCallback(null); + } + }; + + fp.init(window, titleText, nsIFilePicker.modeGetFolder); + fp.appendFilters(nsIFilePicker.filterAll); + try { + let prefs = Components.classes[PREFERENCES_CONTRACTID]. + getService(Components.interfaces.nsIPrefBranch); + let initialDir = prefs.getComplexValue("browser.download.dir", nsILocalFile); + if (initialDir) { + fp.displayDirectory = initialDir; + } + } catch (ex) { + } + fp.open(fpCallback); +} + +function saveMedia() +{ + var tree = document.getElementById("imagetree"); + var rowArray = getSelectedRows(tree); + if (rowArray.length == 1) { + var row = rowArray[0]; + var item = gImageView.data[row][COL_IMAGE_NODE]; + var url = gImageView.data[row][COL_IMAGE_ADDRESS]; + + if (url) { + var titleKey = "SaveImageTitle"; + + if (item instanceof HTMLVideoElement) + titleKey = "SaveVideoTitle"; + else if (item instanceof HTMLAudioElement) + titleKey = "SaveAudioTitle"; + + saveURL(url, null, titleKey, false, false, makeURI(item.baseURI), + null, gDocInfo.isContentWindowPrivate); + } + } else { + selectSaveFolder(function(aDirectory) { + if (aDirectory) { + var saveAnImage = function(aURIString, aChosenData, aBaseURI) { + uniqueFile(aChosenData.file); + internalSave(aURIString, null, null, null, null, false, "SaveImageTitle", + aChosenData, aBaseURI, null, false, null, gDocInfo.isContentWindowPrivate); + }; + + for (var i = 0; i < rowArray.length; i++) { + var v = rowArray[i]; + var dir = aDirectory.clone(); + var item = gImageView.data[v][COL_IMAGE_NODE]; + var uriString = gImageView.data[v][COL_IMAGE_ADDRESS]; + var uri = makeURI(uriString); + + try { + uri.QueryInterface(Components.interfaces.nsIURL); + dir.append(decodeURIComponent(uri.fileName)); + } catch (ex) { + // data:/blob: uris + // Supply a dummy filename, otherwise Download Manager + // will try to delete the base directory on failure. + dir.append(gImageView.data[v][COL_IMAGE_TYPE]); + } + + if (i == 0) { + saveAnImage(uriString, new AutoChosen(dir, uri), makeURI(item.baseURI)); + } else { + // This delay is a hack which prevents the download manager + // from opening many times. See bug 377339. + setTimeout(saveAnImage, 200, uriString, new AutoChosen(dir, uri), + makeURI(item.baseURI)); + } + } + } + }); + } +} + +function onBlockImage() +{ + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + + var checkbox = document.getElementById("blockImage"); + var uri = makeURI(document.getElementById("imageurltext").value); + if (checkbox.checked) + permissionManager.add(uri, "image", nsIPermissionManager.DENY_ACTION); + else + permissionManager.remove(uri, "image"); +} + +function onImageSelect() +{ + var previewBox = document.getElementById("mediaPreviewBox"); + var mediaSaveBox = document.getElementById("mediaSaveBox"); + var splitter = document.getElementById("mediaSplitter"); + var tree = document.getElementById("imagetree"); + var count = tree.view.selection.count; + if (count == 0) { + previewBox.collapsed = true; + mediaSaveBox.collapsed = true; + splitter.collapsed = true; + tree.flex = 1; + } + else if (count > 1) { + splitter.collapsed = true; + previewBox.collapsed = true; + mediaSaveBox.collapsed = false; + tree.flex = 1; + } + else { + mediaSaveBox.collapsed = true; + splitter.collapsed = false; + previewBox.collapsed = false; + tree.flex = 0; + makePreview(getSelectedRows(tree)[0]); + } +} + +// Makes the media preview (image, video, etc) for the selected row on the media tab. +function makePreview(row) +{ + var item = gImageView.data[row][COL_IMAGE_NODE]; + var url = gImageView.data[row][COL_IMAGE_ADDRESS]; + var isBG = gImageView.data[row][COL_IMAGE_BG]; + var isAudio = false; + + setItemValue("imageurltext", url); + setItemValue("imagetext", item.imageText); + setItemValue("imagelongdesctext", item.longDesc); + + // get cache info + var cacheKey = url.replace(/#.*$/, ""); + openCacheEntry(cacheKey, function(cacheEntry) { + // find out the file size + var sizeText; + if (cacheEntry) { + let imageSize = cacheEntry.dataSize; + var kbSize = Math.round(imageSize / 1024 * 100) / 100; + sizeText = gBundle.getFormattedString("generalSize", + [formatNumber(kbSize), formatNumber(imageSize)]); + } + else + sizeText = gBundle.getString("mediaUnknownNotCached"); + setItemValue("imagesizetext", sizeText); + + var mimeType = item.mimeType || this.getContentTypeFromHeaders(cacheEntry); + var numFrames = item.numFrames; + + var imageType; + if (mimeType) { + // We found the type, try to display it nicely + let imageMimeType = /^image\/(.*)/i.exec(mimeType); + if (imageMimeType) { + imageType = imageMimeType[1].toUpperCase(); + if (numFrames > 1) + imageType = gBundle.getFormattedString("mediaAnimatedImageType", + [imageType, numFrames]); + else + imageType = gBundle.getFormattedString("mediaImageType", [imageType]); + } + else { + // the MIME type doesn't begin with image/, display the raw type + imageType = mimeType; + } + } + else { + // We couldn't find the type, fall back to the value in the treeview + imageType = gImageView.data[row][COL_IMAGE_TYPE]; + } + setItemValue("imagetypetext", imageType); + + var imageContainer = document.getElementById("theimagecontainer"); + var oldImage = document.getElementById("thepreviewimage"); + + var isProtocolAllowed = checkProtocol(gImageView.data[row]); + + var newImage = new Image; + newImage.id = "thepreviewimage"; + var physWidth = 0, physHeight = 0; + var width = 0, height = 0; + + if ((item.HTMLLinkElement || item.HTMLInputElement || + item.HTMLImageElement || item.SVGImageElement || + (item.HTMLObjectElement && mimeType && mimeType.startsWith("image/")) || + isBG) && isProtocolAllowed) { + newImage.setAttribute("src", url); + physWidth = newImage.width || 0; + physHeight = newImage.height || 0; + + // "width" and "height" attributes must be set to newImage, + // even if there is no "width" or "height attribute in item; + // otherwise, the preview image cannot be displayed correctly. + // Since the image might have been loaded out-of-process, we expect + // the item to tell us its width / height dimensions. Failing that + // the item should tell us the natural dimensions of the image. Finally + // failing that, we'll assume that the image was never loaded in the + // other process (this can be true for favicons, for example), and so + // we'll assume that we can use the natural dimensions of the newImage + // we just created. If the natural dimensions of newImage are not known + // then the image is probably broken. + if (!isBG) { + newImage.width = ("width" in item && item.width) || newImage.naturalWidth; + newImage.height = ("height" in item && item.height) || newImage.naturalHeight; + } + else { + // the Width and Height of an HTML tag should not be used for its background image + // (for example, "table" can have "width" or "height" attributes) + newImage.width = item.naturalWidth || newImage.naturalWidth; + newImage.height = item.naturalHeight || newImage.naturalHeight; + } + + if (item.SVGImageElement) { + newImage.width = item.SVGImageElementWidth; + newImage.height = item.SVGImageElementHeight; + } + + width = newImage.width; + height = newImage.height; + + document.getElementById("theimagecontainer").collapsed = false + document.getElementById("brokenimagecontainer").collapsed = true; + } + else if (item.HTMLVideoElement && isProtocolAllowed) { + newImage = document.createElementNS("http://www.w3.org/1999/xhtml", "video"); + newImage.id = "thepreviewimage"; + newImage.src = url; + newImage.controls = true; + width = physWidth = item.videoWidth; + height = physHeight = item.videoHeight; + + document.getElementById("theimagecontainer").collapsed = false; + document.getElementById("brokenimagecontainer").collapsed = true; + } + else if (item.HTMLAudioElement && isProtocolAllowed) { + newImage = new Audio; + newImage.id = "thepreviewimage"; + newImage.src = url; + newImage.controls = true; + isAudio = true; + + document.getElementById("theimagecontainer").collapsed = false; + document.getElementById("brokenimagecontainer").collapsed = true; + } + else { + // fallback image for protocols not allowed (e.g., javascript:) + // or elements not [yet] handled (e.g., object, embed). + document.getElementById("brokenimagecontainer").collapsed = false; + document.getElementById("theimagecontainer").collapsed = true; + } + + let imageSize = ""; + if (url && !isAudio) { + if (width != physWidth || height != physHeight) { + imageSize = gBundle.getFormattedString("mediaDimensionsScaled", + [formatNumber(physWidth), + formatNumber(physHeight), + formatNumber(width), + formatNumber(height)]); + } + else { + imageSize = gBundle.getFormattedString("mediaDimensions", + [formatNumber(width), + formatNumber(height)]); + } + } + setItemValue("imagedimensiontext", imageSize); + + makeBlockImage(url); + + imageContainer.removeChild(oldImage); + imageContainer.appendChild(newImage); + }); +} + +function makeBlockImage(url) +{ + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + var prefs = Components.classes[PREFERENCES_CONTRACTID] + .getService(Components.interfaces.nsIPrefBranch); + + var checkbox = document.getElementById("blockImage"); + var imagePref = prefs.getIntPref("permissions.default.image"); + if (!(/^https?:/.test(url)) || imagePref == 2) + // We can't block the images from this host because either is is not + // for http(s) or we don't load images at all + checkbox.hidden = true; + else { + var uri = makeURI(url); + if (uri.host) { + checkbox.hidden = false; + checkbox.label = gBundle.getFormattedString("mediaBlockImage", [uri.host]); + var perm = permissionManager.testPermission(uri, "image"); + checkbox.checked = perm == nsIPermissionManager.DENY_ACTION; + } + else + checkbox.hidden = true; + } +} + +var imagePermissionObserver = { + observe: function (aSubject, aTopic, aData) + { + if (document.getElementById("mediaPreviewBox").collapsed) + return; + + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Components.interfaces.nsIPermission); + if (permission.type == "image") { + var imageTree = document.getElementById("imagetree"); + var row = getSelectedRow(imageTree); + var url = gImageView.data[row][COL_IMAGE_ADDRESS]; + if (permission.matchesURI(makeURI(url), true)) { + makeBlockImage(url); + } + } + } + } +} + +function getContentTypeFromHeaders(cacheEntryDescriptor) +{ + if (!cacheEntryDescriptor) + return null; + + let headers = cacheEntryDescriptor.getMetaDataElement("response-head"); + let type = /^Content-Type:\s*(.*?)\s*(?:\;|$)/mi.exec(headers); + return type && type[1]; +} + +function setItemValue(id, value) +{ + var item = document.getElementById(id); + if (value) { + item.parentNode.collapsed = false; + item.value = value; + } + else + item.parentNode.collapsed = true; +} + +function formatNumber(number) +{ + return (+number).toLocaleString(); // coerce number to a numeric value before calling toLocaleString() +} + +function formatDate(datestr, unknown) +{ + var date = new Date(datestr); + if (!date.valueOf()) + return unknown; + + const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"] + .getService(Components.interfaces.nsIXULChromeRegistry) + .getSelectedLocale("global", true); + const dtOptions = { year: 'numeric', month: 'long', day: 'numeric', + hour: 'numeric', minute: 'numeric', second: 'numeric' }; + return date.toLocaleString(locale, dtOptions); +} + +function doCopy() +{ + if (!gClipboardHelper) + return; + + var elem = document.commandDispatcher.focusedElement; + + if (elem && "treeBoxObject" in elem) { + var view = elem.view; + var selection = view.selection; + var text = [], tmp = ''; + var min = {}, max = {}; + + var count = selection.getRangeCount(); + + for (var i = 0; i < count; i++) { + selection.getRangeAt(i, min, max); + + for (var row = min.value; row <= max.value; row++) { + view.performActionOnRow("copy", row); + + tmp = elem.getAttribute("copybuffer"); + if (tmp) + text.push(tmp); + elem.removeAttribute("copybuffer"); + } + } + gClipboardHelper.copyString(text.join("\n")); + } +} + +function doSelectAllMedia() +{ + var tree = document.getElementById("imagetree"); + + if (tree) + tree.view.selection.selectAll(); +} + +function doSelectAll() +{ + var elem = document.commandDispatcher.focusedElement; + + if (elem && "treeBoxObject" in elem) + elem.view.selection.selectAll(); +} + +function selectImage() +{ + if (!gImageElement) + return; + + var tree = document.getElementById("imagetree"); + for (var i = 0; i < tree.view.rowCount; i++) { + // If the image row element is the image selected from the "View Image Info" context menu item. + let image = gImageView.data[i][COL_IMAGE_NODE]; + if (!gImageView.data[i][COL_IMAGE_BG] && + gImageElement.currentSrc == gImageView.data[i][COL_IMAGE_ADDRESS] && + gImageElement.width == image.width && + gImageElement.height == image.height && + gImageElement.imageText == image.imageText) { + tree.view.selection.select(i); + tree.treeBoxObject.ensureRowIsVisible(i); + tree.focus(); + return; + } + } +} + +function checkProtocol(img) +{ + var url = img[COL_IMAGE_ADDRESS]; + return /^data:image\//i.test(url) || + /^(https?|ftp|file|about|chrome|resource):/.test(url); +} diff --git a/application/basilisk/base/content/pageinfo/pageInfo.xml b/application/basilisk/base/content/pageinfo/pageInfo.xml new file mode 100644 index 000000000..62c699bf2 --- /dev/null +++ b/application/basilisk/base/content/pageinfo/pageInfo.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<bindings id="pageInfoBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <!-- based on preferences.xml paneButton --> + <binding id="viewbutton" extends="chrome://global/content/bindings/radio.xml#radio" role="xullistitem"> + <content> + <xul:image class="viewButtonIcon" xbl:inherits="src"/> + <xul:label class="viewButtonLabel" xbl:inherits="value=label"/> + </content> + </binding> + +</bindings> diff --git a/application/basilisk/base/content/pageinfo/pageInfo.xul b/application/basilisk/base/content/pageinfo/pageInfo.xul new file mode 100644 index 000000000..8352a8aa7 --- /dev/null +++ b/application/basilisk/base/content/pageinfo/pageInfo.xul @@ -0,0 +1,438 @@ +<?xml version="1.0"?> +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xml-stylesheet href="chrome://browser/content/pageinfo/pageInfo.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/pageInfo.css" type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % pageInfoDTD SYSTEM "chrome://browser/locale/pageInfo.dtd"> + %pageInfoDTD; +]> + +#ifdef XP_MACOSX +<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?> +#endif + +<window id="main-window" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + windowtype="Browser:page-info" + onload="onLoadPageInfo()" + onunload="onUnloadPageInfo()" + align="stretch" + screenX="10" screenY="10" + width="&pageInfoWindow.width;" height="&pageInfoWindow.height;" + persist="screenX screenY width height sizemode"> + + <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + <script type="application/javascript" src="chrome://global/content/treeUtils.js"/> + <script type="application/javascript" src="chrome://browser/content/pageinfo/pageInfo.js"/> + <script type="application/javascript" src="chrome://browser/content/pageinfo/feeds.js"/> + <script type="application/javascript" src="chrome://browser/content/pageinfo/permissions.js"/> + <script type="application/javascript" src="chrome://browser/content/pageinfo/security.js"/> + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + + <stringbundleset id="pageinfobundleset"> + <stringbundle id="pageinfobundle" src="chrome://browser/locale/pageInfo.properties"/> + <stringbundle id="pkiBundle" src="chrome://pippki/locale/pippki.properties"/> + <stringbundle id="browserBundle" src="chrome://browser/locale/browser.properties"/> + </stringbundleset> + + <commandset id="pageInfoCommandSet"> + <command id="cmd_close" oncommand="window.close();"/> + <command id="cmd_help" oncommand="doHelpButton();"/> + <command id="cmd_copy" oncommand="doCopy();"/> + <command id="cmd_selectall" oncommand="doSelectAll();"/> + + <!-- permissions tab --> + <command id="cmd_pluginsDef" oncommand="onCheckboxClick('plugins');"/> + <command id="cmd_pluginsToggle" oncommand="onPluginRadioClick(event);"/> + </commandset> + + <keyset id="pageInfoKeySet"> + <key key="&closeWindow.key;" modifiers="accel" command="cmd_close"/> + <key keycode="VK_ESCAPE" command="cmd_close"/> +#ifdef XP_MACOSX + <key key="." modifiers="meta" command="cmd_close"/> +#else + <key keycode="VK_F1" command="cmd_help"/> +#endif + <key key="©.key;" modifiers="accel" command="cmd_copy"/> + <key key="&selectall.key;" modifiers="accel" command="cmd_selectall"/> + <key key="&selectall.key;" modifiers="alt" command="cmd_selectall"/> + </keyset> + + <menupopup id="picontext"> + <menuitem id="menu_selectall" label="&selectall.label;" command="cmd_selectall" accesskey="&selectall.accesskey;"/> + <menuitem id="menu_copy" label="©.label;" command="cmd_copy" accesskey="©.accesskey;"/> + </menupopup> + + <windowdragbox id="topBar" class="viewGroupWrapper"> + <radiogroup id="viewGroup" class="chromeclass-toolbar" orient="horizontal"> + <radio id="generalTab" label="&generalTab;" accesskey="&generalTab.accesskey;" + oncommand="showTab('general');"/> + <radio id="mediaTab" label="&mediaTab;" accesskey="&mediaTab.accesskey;" + oncommand="showTab('media');" hidden="true"/> + <radio id="feedTab" label="&feedTab;" accesskey="&feedTab.accesskey;" + oncommand="showTab('feed');" hidden="true"/> + <radio id="permTab" label="&permTab;" accesskey="&permTab.accesskey;" + oncommand="showTab('perm');"/> + <radio id="securityTab" label="&securityTab;" accesskey="&securityTab.accesskey;" + oncommand="showTab('security');"/> + <!-- Others added by overlay --> + </radiogroup> + </windowdragbox> + + <deck id="mainDeck" flex="1"> + <!-- General page information --> + <vbox id="generalPanel"> + <grid id="generalGrid"> + <columns> + <column/> + <column class="gridSeparator"/> + <column flex="1"/> + </columns> + <rows id="generalRows"> + <row id="generalTitle"> + <label control="titletext" value="&generalTitle;"/> + <separator/> + <textbox readonly="true" id="titletext"/> + </row> + <row id="generalURLRow"> + <label control="urltext" value="&generalURL;"/> + <separator/> + <textbox readonly="true" id="urltext"/> + </row> + <row id="generalSeparatorRow1"> + <separator class="thin"/> + </row> + <row id="generalTypeRow"> + <label control="typetext" value="&generalType;"/> + <separator/> + <textbox readonly="true" id="typetext"/> + </row> + <row id="generalModeRow"> + <label control="modetext" value="&generalMode;"/> + <separator/> + <textbox readonly="true" crop="end" id="modetext"/> + </row> + <row id="generalEncodingRow"> + <label control="encodingtext" value="&generalEncoding2;"/> + <separator/> + <textbox readonly="true" id="encodingtext"/> + </row> + <row id="generalSizeRow"> + <label control="sizetext" value="&generalSize;"/> + <separator/> + <textbox readonly="true" id="sizetext"/> + </row> + <row id="generalReferrerRow"> + <label control="refertext" value="&generalReferrer;"/> + <separator/> + <textbox readonly="true" id="refertext"/> + </row> + <row id="generalSeparatorRow2"> + <separator class="thin"/> + </row> + <row id="generalModifiedRow"> + <label control="modifiedtext" value="&generalModified;"/> + <separator/> + <textbox readonly="true" id="modifiedtext"/> + </row> + </rows> + </grid> + <separator class="thin"/> + <groupbox id="metaTags" flex="1" class="collapsable treebox"> + <caption id="metaTagsCaption" onclick="toggleGroupbox('metaTags');"/> + <tree id="metatree" flex="1" hidecolumnpicker="true" contextmenu="picontext"> + <treecols> + <treecol id="meta-name" label="&generalMetaName;" + persist="width" flex="1" + onclick="gMetaView.onPageMediaSort('meta-name');"/> + <splitter class="tree-splitter"/> + <treecol id="meta-content" label="&generalMetaContent;" + persist="width" flex="4" + onclick="gMetaView.onPageMediaSort('meta-content');"/> + </treecols> + <treechildren id="metatreechildren" flex="1"/> + </tree> + </groupbox> + <hbox pack="end"> + <button command="cmd_help" label="&helpButton.label;" dlgtype="help"/> + </hbox> + </vbox> + + <!-- Media information --> + <vbox id="mediaPanel"> + <tree id="imagetree" onselect="onImageSelect();" contextmenu="picontext" + ondragstart="onBeginLinkDrag(event,'image-address','image-alt')"> + <treecols> + <treecol sortSeparators="true" primary="true" persist="width" flex="10" + width="10" id="image-address" label="&mediaAddress;" + onclick="gImageView.onPageMediaSort('image-address');"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" persist="hidden width" flex="2" + width="2" id="image-type" label="&mediaType;" + onclick="gImageView.onPageMediaSort('image-type');"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="2" + width="2" id="image-size" label="&mediaSize;" value="size" + onclick="gImageView.onPageMediaSort('image-size');"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="4" + width="4" id="image-alt" label="&mediaAltHeader;" + onclick="gImageView.onPageMediaSort('image-alt');"/> + <splitter class="tree-splitter"/> + <treecol sortSeparators="true" hidden="true" persist="hidden width" flex="1" + width="1" id="image-count" label="&mediaCount;" + onclick="gImageView.onPageMediaSort('image-count');"/> + </treecols> + <treechildren id="imagetreechildren" flex="1"/> + </tree> + <splitter orient="vertical" id="mediaSplitter"/> + <vbox flex="1" id="mediaPreviewBox" collapsed="true"> + <grid id="mediaGrid"> + <columns> + <column id="mediaLabelColumn"/> + <column class="gridSeparator"/> + <column flex="1"/> + </columns> + <rows id="mediaRows"> + <row id="mediaLocationRow"> + <label control="imageurltext" value="&mediaLocation;"/> + <separator/> + <textbox readonly="true" id="imageurltext"/> + </row> + <row id="mediaTypeRow"> + <label control="imagetypetext" value="&generalType;"/> + <separator/> + <textbox readonly="true" id="imagetypetext"/> + </row> + <row id="mediaSizeRow"> + <label control="imagesizetext" value="&generalSize;"/> + <separator/> + <textbox readonly="true" id="imagesizetext"/> + </row> + <row id="mediaDimensionRow"> + <label control="imagedimensiontext" value="&mediaDimension;"/> + <separator/> + <textbox readonly="true" id="imagedimensiontext"/> + </row> + <row id="mediaTextRow"> + <label control="imagetext" value="&mediaText;"/> + <separator/> + <textbox readonly="true" id="imagetext"/> + </row> + <row id="mediaLongdescRow"> + <label control="imagelongdesctext" value="&mediaLongdesc;"/> + <separator/> + <textbox readonly="true" id="imagelongdesctext"/> + </row> + </rows> + </grid> + <hbox id="imageSaveBox" align="end"> + <vbox id="blockImageBox"> + <checkbox id="blockImage" hidden="true" oncommand="onBlockImage()" + accesskey="&mediaBlockImage.accesskey;"/> + <label control="thepreviewimage" value="&mediaPreview;" class="header"/> + </vbox> + <spacer id="imageSaveBoxSpacer" flex="1"/> + <button label="&selectall.label;" accesskey="&selectall.accesskey;" + id="selectallbutton" + oncommand="doSelectAllMedia();"/> + <button label="&mediaSaveAs;" accesskey="&mediaSaveAs.accesskey;" + icon="save" id="imagesaveasbutton" + oncommand="saveMedia();"/> + </hbox> + <vbox id="imagecontainerbox" class="inset iframe" flex="1" pack="center"> + <hbox id="theimagecontainer" pack="center"> + <image id="thepreviewimage"/> + </hbox> + <hbox id="brokenimagecontainer" pack="center" collapsed="true"> + <image id="brokenimage" src="resource://gre-resources/broken-image.png"/> + </hbox> + </vbox> + </vbox> + <hbox id="mediaSaveBox" collapsed="true"> + <spacer id="mediaSaveBoxSpacer" flex="1"/> + <button label="&mediaSaveAs;" accesskey="&mediaSaveAs2.accesskey;" + icon="save" id="mediasaveasbutton" + oncommand="saveMedia();"/> + </hbox> + <hbox pack="end"> + <button command="cmd_help" label="&helpButton.label;" dlgtype="help"/> + </hbox> + </vbox> + + <!-- Feeds --> + <vbox id="feedPanel"> + <richlistbox id="feedListbox" flex="1"/> + </vbox> + + <!-- Permissions --> + <vbox id="permPanel"> + <hbox id="permHostBox"> + <label value="&permissionsFor;" control="hostText" /> + <textbox id="hostText" class="header" readonly="true" + crop="end" flex="1"/> + </hbox> + + <vbox id="permList" flex="1"> + <hbox id="perm-indexedDB-extras"> + <spacer flex="1"/> + <vbox id="permIndexedDBStatusBox" pack="center"> + <label id="indexedDBStatus" control="indexedDBClear" hidden="true"/> + </vbox> + <button id="indexedDBClear" label="&permClearStorage;" hidden="true" + accesskey="&permClearStorage.accesskey;" onclick="onIndexedDBClear();"/> + </hbox> + <vbox class="permission" id="perm-plugins-row"> + <label class="permissionLabel" id="permPluginsLabel" + value="&permPlugins;" control="pluginsRadioGroup"/> + <hbox id="permPluginTemplate" role="group" aria-labelledby="permPluginsLabel" align="baseline"> + <label class="permPluginTemplateLabel"/> + <spacer flex="1"/> + <radiogroup class="permPluginTemplateRadioGroup" orient="horizontal" command="cmd_pluginsToggle"> + <radio class="permPluginTemplateRadioDefault" label="&permUseDefault;"/> + <radio class="permPluginTemplateRadioAsk" label="&permAskAlways;"/> + <radio class="permPluginTemplateRadioAllow" label="&permAllow;"/> + <radio class="permPluginTemplateRadioBlock" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + </vbox> + <hbox pack="end"> + <button command="cmd_help" label="&helpButton.label;" dlgtype="help"/> + </hbox> + </vbox> + + <!-- Security & Privacy --> + <vbox id="securityPanel"> + <!-- Identity Section --> + <groupbox id="security-identity-groupbox" flex="1"> + <caption id="security-identity" label="&securityView.identity.header;"/> + <grid id="security-identity-grid" flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows id="security-identity-rows"> + <!-- Domain --> + <row id="security-identity-domain-row"> + <label id="security-identity-domain-label" + class="fieldLabel" + value="&securityView.identity.domain;" + control="security-identity-domain-value"/> + <textbox id="security-identity-domain-value" + class="fieldValue" readonly="true"/> + </row> + <!-- Owner --> + <row id="security-identity-owner-row"> + <label id="security-identity-owner-label" + class="fieldLabel" + value="&securityView.identity.owner;" + control="security-identity-owner-value"/> + <textbox id="security-identity-owner-value" + class="fieldValue" readonly="true"/> + </row> + <!-- Verifier --> + <row id="security-identity-verifier-row"> + <label id="security-identity-verifier-label" + class="fieldLabel" + value="&securityView.identity.verifier;" + control="security-identity-verifier-value"/> + <textbox id="security-identity-verifier-value" + class="fieldValue" readonly="true" /> + </row> + </rows> + </grid> + <spacer flex="1"/> + <!-- Cert button --> + <hbox id="security-view-cert-box" pack="end"> + <button id="security-view-cert" label="&securityView.certView;" + accesskey="&securityView.accesskey;" + oncommand="security.viewCert();"/> + </hbox> + </groupbox> + + <!-- Privacy & History section --> + <groupbox id="security-privacy-groupbox" flex="1"> + <caption id="security-privacy" label="&securityView.privacy.header;" /> + <grid id="security-privacy-grid"> + <columns> + <column flex="1"/> + <column flex="1"/> + </columns> + <rows id="security-privacy-rows"> + <!-- History --> + <row id="security-privacy-history-row"> + <label id="security-privacy-history-label" + control="security-privacy-history-value" + class="fieldLabel">&securityView.privacy.history;</label> + <textbox id="security-privacy-history-value" + class="fieldValue" + value="&securityView.unknown;" + readonly="true"/> + </row> + <!-- Cookies --> + <row id="security-privacy-cookies-row"> + <label id="security-privacy-cookies-label" + control="security-privacy-cookies-value" + class="fieldLabel">&securityView.privacy.cookies;</label> + <hbox id="security-privacy-cookies-box" align="center"> + <textbox id="security-privacy-cookies-value" + class="fieldValue" + value="&securityView.unknown;" + flex="1" + readonly="true"/> + <button id="security-view-cookies" + label="&securityView.privacy.viewCookies;" + accesskey="&securityView.privacy.viewCookies.accessKey;" + oncommand="security.viewCookies();"/> + </hbox> + </row> + <!-- Passwords --> + <row id="security-privacy-passwords-row"> + <label id="security-privacy-passwords-label" + control="security-privacy-passwords-value" + class="fieldLabel">&securityView.privacy.passwords;</label> + <hbox id="security-privacy-passwords-box" align="center"> + <textbox id="security-privacy-passwords-value" + class="fieldValue" + value="&securityView.unknown;" + flex="1" + readonly="true"/> + <button id="security-view-password" + label="&securityView.privacy.viewPasswords;" + accesskey="&securityView.privacy.viewPasswords.accessKey;" + oncommand="security.viewPasswords();"/> + </hbox> + </row> + </rows> + </grid> + </groupbox> + + <!-- Technical Details section --> + <groupbox id="security-technical-groupbox" flex="1"> + <caption id="security-technical" label="&securityView.technical.header;" /> + <vbox id="security-technical-box" flex="1"> + <label id="security-technical-shortform" class="fieldValue"/> + <description id="security-technical-longform1" class="fieldLabel"/> + <description id="security-technical-longform2" class="fieldLabel"/> + <description id="security-technical-certificate-transparency" class="fieldLabel"/> + </vbox> + </groupbox> + <hbox pack="end"> + <button command="cmd_help" label="&helpButton.label;" dlgtype="help"/> + </hbox> + </vbox> + <!-- Others added by overlay --> + </deck> + +#ifdef XP_MACOSX +#include ../browserMountPoints.inc +#endif + +</window> diff --git a/application/basilisk/base/content/pageinfo/permissions.js b/application/basilisk/base/content/pageinfo/permissions.js new file mode 100644 index 000000000..0e6b9cba1 --- /dev/null +++ b/application/basilisk/base/content/pageinfo/permissions.js @@ -0,0 +1,334 @@ +/* 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/. */ + +Components.utils.import("resource:///modules/SitePermissions.jsm"); +Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); + +const nsIQuotaManagerService = Components.interfaces.nsIQuotaManagerService; + +var gPermURI; +var gPermPrincipal; +var gUsageRequest; + +// Array of permissionIDs sorted alphabetically by label. +var gPermissions = SitePermissions.listPermissions().sort((a, b) => { + let firstLabel = SitePermissions.getPermissionLabel(a); + let secondLabel = SitePermissions.getPermissionLabel(b); + return firstLabel.localeCompare(secondLabel); +}); +gPermissions.push("plugins"); + +var permissionObserver = { + observe: function (aSubject, aTopic, aData) + { + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Components.interfaces.nsIPermission); + if (permission.matchesURI(gPermURI, true)) { + if (gPermissions.indexOf(permission.type) > -1) + initRow(permission.type); + else if (permission.type.startsWith("plugin")) + setPluginsRadioState(); + } + } + } +}; + +function onLoadPermission(uri, principal) +{ + var permTab = document.getElementById("permTab"); + if (SitePermissions.isSupportedURI(uri)) { + gPermURI = uri; + gPermPrincipal = principal; + var hostText = document.getElementById("hostText"); + hostText.value = gPermURI.prePath; + + for (var i of gPermissions) + initRow(i); + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.addObserver(permissionObserver, "perm-changed", false); + onUnloadRegistry.push(onUnloadPermission); + permTab.hidden = false; + } + else + permTab.hidden = true; +} + +function onUnloadPermission() +{ + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.removeObserver(permissionObserver, "perm-changed"); + + if (gUsageRequest) { + gUsageRequest.cancel(); + gUsageRequest = null; + } +} + +function initRow(aPartId) +{ + if (aPartId == "plugins") { + initPluginsRow(); + return; + } + + createRow(aPartId); + + var checkbox = document.getElementById(aPartId + "Def"); + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + var perm = SitePermissions.get(gPermURI, aPartId); + + if (perm) { + checkbox.checked = false; + command.removeAttribute("disabled"); + } + else { + checkbox.checked = true; + command.setAttribute("disabled", "true"); + perm = SitePermissions.getDefault(aPartId); + } + setRadioState(aPartId, perm); + + if (aPartId == "indexedDB") { + initIndexedDBRow(); + } +} + +function createRow(aPartId) { + let rowId = "perm-" + aPartId + "-row"; + if (document.getElementById(rowId)) + return; + + let commandId = "cmd_" + aPartId + "Toggle"; + let labelId = "perm-" + aPartId + "-label"; + let radiogroupId = aPartId + "RadioGroup"; + + let command = document.createElement("command"); + command.setAttribute("id", commandId); + command.setAttribute("oncommand", "onRadioClick('" + aPartId + "');"); + document.getElementById("pageInfoCommandSet").appendChild(command); + + let row = document.createElement("vbox"); + row.setAttribute("id", rowId); + row.setAttribute("class", "permission"); + + let label = document.createElement("label"); + label.setAttribute("id", labelId); + label.setAttribute("control", radiogroupId); + label.setAttribute("value", SitePermissions.getPermissionLabel(aPartId)); + label.setAttribute("class", "permissionLabel"); + row.appendChild(label); + + let controls = document.createElement("hbox"); + controls.setAttribute("role", "group"); + controls.setAttribute("aria-labelledby", labelId); + + let checkbox = document.createElement("checkbox"); + checkbox.setAttribute("id", aPartId + "Def"); + checkbox.setAttribute("oncommand", "onCheckboxClick('" + aPartId + "');"); + checkbox.setAttribute("label", gBundle.getString("permissions.useDefault")); + controls.appendChild(checkbox); + + let spacer = document.createElement("spacer"); + spacer.setAttribute("flex", "1"); + controls.appendChild(spacer); + + let radiogroup = document.createElement("radiogroup"); + radiogroup.setAttribute("id", radiogroupId); + radiogroup.setAttribute("orient", "horizontal"); + for (let state of SitePermissions.getAvailableStates(aPartId)) { + let radio = document.createElement("radio"); + radio.setAttribute("id", aPartId + "#" + state); + radio.setAttribute("label", SitePermissions.getStateLabel(aPartId, state)); + radio.setAttribute("command", commandId); + radiogroup.appendChild(radio); + } + controls.appendChild(radiogroup); + + row.appendChild(controls); + + document.getElementById("permList").appendChild(row); +} + +function onCheckboxClick(aPartId) +{ + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + var checkbox = document.getElementById(aPartId + "Def"); + if (checkbox.checked) { + SitePermissions.remove(gPermURI, aPartId); + command.setAttribute("disabled", "true"); + var perm = SitePermissions.getDefault(aPartId); + setRadioState(aPartId, perm); + } + else { + onRadioClick(aPartId); + command.removeAttribute("disabled"); + } +} + +function onPluginRadioClick(aEvent) { + onRadioClick(aEvent.originalTarget.getAttribute("id").split('#')[0]); +} + +function onRadioClick(aPartId) +{ + var radioGroup = document.getElementById(aPartId + "RadioGroup"); + var id = radioGroup.selectedItem.id; + var permission = id.split('#')[1]; + SitePermissions.set(gPermURI, aPartId, permission); +} + +function setRadioState(aPartId, aValue) +{ + var radio = document.getElementById(aPartId + "#" + aValue); + if (radio) { + radio.radioGroup.selectedItem = radio; + } +} + +function initIndexedDBRow() +{ + let row = document.getElementById("perm-indexedDB-row"); + let extras = document.getElementById("perm-indexedDB-extras"); + + row.appendChild(extras); + + var quotaManagerService = + Components.classes["@mozilla.org/dom/quota-manager-service;1"] + .getService(nsIQuotaManagerService); + gUsageRequest = + quotaManagerService.getUsageForPrincipal(gPermPrincipal, + onIndexedDBUsageCallback); + + var status = document.getElementById("indexedDBStatus"); + var button = document.getElementById("indexedDBClear"); + + status.value = ""; + status.setAttribute("hidden", "true"); + button.setAttribute("hidden", "true"); +} + +function onIndexedDBClear() +{ + Components.classes["@mozilla.org/dom/quota-manager-service;1"] + .getService(nsIQuotaManagerService) + .clearStoragesForPrincipal(gPermPrincipal); + + Components.classes["@mozilla.org/serviceworkers/manager;1"] + .getService(Components.interfaces.nsIServiceWorkerManager) + .removeAndPropagate(gPermURI.host); + + SitePermissions.remove(gPermURI, "indexedDB"); + initIndexedDBRow(); +} + +function onIndexedDBUsageCallback(request) +{ + let uri = request.principal.URI; + if (!uri.equals(gPermURI)) { + throw new Error("Callback received for bad URI: " + uri); + } + + let usage = request.result.usage; + if (usage) { + if (!("DownloadUtils" in window)) { + Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); + } + + var status = document.getElementById("indexedDBStatus"); + var button = document.getElementById("indexedDBClear"); + + status.value = + gBundle.getFormattedString("indexedDBUsage", + DownloadUtils.convertByteUnits(usage)); + status.removeAttribute("hidden"); + button.removeAttribute("hidden"); + } +} + +function fillInPluginPermissionTemplate(aPluginName, aPermissionString) { + let permPluginTemplate = document.getElementById("permPluginTemplate").cloneNode(true); + permPluginTemplate.setAttribute("permString", aPermissionString); + let attrs = [ + [ ".permPluginTemplateLabel", "value", aPluginName ], + [ ".permPluginTemplateRadioGroup", "id", aPermissionString + "RadioGroup" ], + [ ".permPluginTemplateRadioDefault", "id", aPermissionString + "#0" ], + [ ".permPluginTemplateRadioAsk", "id", aPermissionString + "#3" ], + [ ".permPluginTemplateRadioAllow", "id", aPermissionString + "#1" ], + [ ".permPluginTemplateRadioBlock", "id", aPermissionString + "#2" ] + ]; + + for (let attr of attrs) { + permPluginTemplate.querySelector(attr[0]).setAttribute(attr[1], attr[2]); + } + + return permPluginTemplate; +} + +function clearPluginPermissionTemplate() { + let permPluginTemplate = document.getElementById("permPluginTemplate"); + permPluginTemplate.hidden = true; + permPluginTemplate.removeAttribute("permString"); + document.querySelector(".permPluginTemplateLabel").removeAttribute("value"); + document.querySelector(".permPluginTemplateRadioGroup").removeAttribute("id"); + document.querySelector(".permPluginTemplateRadioAsk").removeAttribute("id"); + document.querySelector(".permPluginTemplateRadioAllow").removeAttribute("id"); + document.querySelector(".permPluginTemplateRadioBlock").removeAttribute("id"); +} + +function initPluginsRow() { + let vulnerableLabel = document.getElementById("browserBundle").getString("pluginActivateVulnerable.label"); + let pluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost); + + let permissionMap = new Map(); + + for (let plugin of pluginHost.getPluginTags()) { + if (plugin.disabled) { + continue; + } + for (let mimeType of plugin.getMimeTypes()) { + let permString = pluginHost.getPermissionStringForType(mimeType); + if (!permissionMap.has(permString)) { + let name = BrowserUtils.makeNicePluginName(plugin.name); + if (permString.startsWith("plugin-vulnerable:")) { + name += " \u2014 " + vulnerableLabel; + } + permissionMap.set(permString, name); + } + } + } + + let entries = Array.from(permissionMap, item => ({ name: item[1], permission: item[0] })); + + entries.sort(function(a, b) { + return a.name.localeCompare(b.name); + }); + + let permissionEntries = entries.map(p => fillInPluginPermissionTemplate(p.name, p.permission)); + + let permPluginsRow = document.getElementById("perm-plugins-row"); + clearPluginPermissionTemplate(); + if (permissionEntries.length < 1) { + permPluginsRow.hidden = true; + return; + } + + for (let permissionEntry of permissionEntries) { + permPluginsRow.appendChild(permissionEntry); + } + + setPluginsRadioState(); +} + +function setPluginsRadioState() { + let box = document.getElementById("perm-plugins-row"); + for (let permissionEntry of box.childNodes) { + if (permissionEntry.hasAttribute("permString")) { + let permString = permissionEntry.getAttribute("permString"); + let permission = SitePermissions.get(gPermURI, permString); + setRadioState(permString, permission); + } + } +} diff --git a/application/basilisk/base/content/pageinfo/security.js b/application/basilisk/base/content/pageinfo/security.js new file mode 100644 index 000000000..5295a8fe6 --- /dev/null +++ b/application/basilisk/base/content/pageinfo/security.js @@ -0,0 +1,388 @@ +/* -*- 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/. */ + +Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper", + "resource://gre/modules/LoginHelper.jsm"); + +var security = { + init: function(uri, windowInfo) { + this.uri = uri; + this.windowInfo = windowInfo; + }, + + // Display the server certificate (static) + viewCert : function () { + var cert = security._cert; + viewCertHelper(window, cert); + }, + + _getSecurityInfo : function() { + const nsISSLStatusProvider = Components.interfaces.nsISSLStatusProvider; + const nsISSLStatus = Components.interfaces.nsISSLStatus; + + // We don't have separate info for a frame, return null until further notice + // (see bug 138479) + if (!this.windowInfo.isTopWindow) + return null; + + var hostName = this.windowInfo.hostName; + + var ui = security._getSecurityUI(); + if (!ui) + return null; + + var isBroken = + (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IS_BROKEN); + var isMixed = + (ui.state & (Components.interfaces.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT | + Components.interfaces.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT)); + var isInsecure = + (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IS_INSECURE); + var isEV = + (ui.state & Components.interfaces.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL); + ui.QueryInterface(nsISSLStatusProvider); + var status = ui.SSLStatus; + + if (!isInsecure && status) { + status.QueryInterface(nsISSLStatus); + var cert = status.serverCert; + var issuerName = + this.mapIssuerOrganization(cert.issuerOrganization) || cert.issuerName; + + var retval = { + hostName : hostName, + cAName : issuerName, + encryptionAlgorithm : undefined, + encryptionStrength : undefined, + version: undefined, + isBroken : isBroken, + isMixed : isMixed, + isEV : isEV, + cert : cert, + certificateTransparency : undefined + }; + + var version; + try { + retval.encryptionAlgorithm = status.cipherName; + retval.encryptionStrength = status.secretKeyLength; + version = status.protocolVersion; + } + catch (e) { + } + + switch (version) { + case nsISSLStatus.SSL_VERSION_3: + retval.version = "SSL 3"; + break; + case nsISSLStatus.TLS_VERSION_1: + retval.version = "TLS 1.0"; + break; + case nsISSLStatus.TLS_VERSION_1_1: + retval.version = "TLS 1.1"; + break; + case nsISSLStatus.TLS_VERSION_1_2: + retval.version = "TLS 1.2" + break; + case nsISSLStatus.TLS_VERSION_1_3: + retval.version = "TLS 1.3" + break; + } + + // Select status text to display for Certificate Transparency. + switch (status.certificateTransparencyStatus) { + case nsISSLStatus.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE: + // CT compliance checks were not performed, + // do not display any status text. + retval.certificateTransparency = null; + break; + case nsISSLStatus.CERTIFICATE_TRANSPARENCY_NONE: + retval.certificateTransparency = "None"; + break; + case nsISSLStatus.CERTIFICATE_TRANSPARENCY_OK: + retval.certificateTransparency = "OK"; + break; + case nsISSLStatus.CERTIFICATE_TRANSPARENCY_UNKNOWN_LOG: + retval.certificateTransparency = "UnknownLog"; + break; + case nsISSLStatus.CERTIFICATE_TRANSPARENCY_INVALID: + retval.certificateTransparency = "Invalid"; + break; + } + + return retval; + } + return { + hostName : hostName, + cAName : "", + encryptionAlgorithm : "", + encryptionStrength : 0, + version: "", + isBroken : isBroken, + isMixed : isMixed, + isEV : isEV, + cert : null, + certificateTransparency : null + }; + }, + + // Find the secureBrowserUI object (if present) + _getSecurityUI : function() { + if (window.opener.gBrowser) + return window.opener.gBrowser.securityUI; + return null; + }, + + // Interface for mapping a certificate issuer organization to + // the value to be displayed. + // Bug 82017 - this implementation should be moved to pipnss C++ code + mapIssuerOrganization: function(name) { + if (!name) return null; + + if (name == "RSA Data Security, Inc.") return "Verisign, Inc."; + + // No mapping required + return name; + }, + + /** + * Open the cookie manager window + */ + viewCookies : function() + { + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var win = wm.getMostRecentWindow("Browser:Cookies"); + var eTLDService = Components.classes["@mozilla.org/network/effective-tld-service;1"]. + getService(Components.interfaces.nsIEffectiveTLDService); + + var eTLD; + try { + eTLD = eTLDService.getBaseDomain(this.uri); + } + catch (e) { + // getBaseDomain will fail if the host is an IP address or is empty + eTLD = this.uri.asciiHost; + } + + if (win) { + win.gCookiesWindow.setFilter(eTLD); + win.focus(); + } + else + window.openDialog("chrome://browser/content/preferences/cookies.xul", + "Browser:Cookies", "", {filterString : eTLD}); + }, + + /** + * Open the login manager window + */ + viewPasswords : function() { + LoginHelper.openPasswordManager(window, this._getSecurityInfo().hostName); + }, + + _cert : null +}; + +function securityOnLoad(uri, windowInfo) { + security.init(uri, windowInfo); + + var info = security._getSecurityInfo(); + if (!info) { + document.getElementById("securityTab").hidden = true; + return; + } + document.getElementById("securityTab").hidden = false; + + const pageInfoBundle = document.getElementById("pageinfobundle"); + + /* Set Identity section text */ + setText("security-identity-domain-value", info.hostName); + + var owner, verifier; + if (info.cert && !info.isBroken) { + // Try to pull out meaningful values. Technically these fields are optional + // so we'll employ fallbacks where appropriate. The EV spec states that Org + // fields must be specified for subject and issuer so that case is simpler. + if (info.isEV) { + owner = info.cert.organization; + verifier = security.mapIssuerOrganization(info.cAName); + } + else { + // Technically, a non-EV cert might specify an owner in the O field or not, + // depending on the CA's issuing policies. However we don't have any programmatic + // way to tell those apart, and no policy way to establish which organization + // vetting standards are good enough (that's what EV is for) so we default to + // treating these certs as domain-validated only. + owner = pageInfoBundle.getString("securityNoOwner"); + verifier = security.mapIssuerOrganization(info.cAName || + info.cert.issuerCommonName || + info.cert.issuerName); + } + } + else { + // We don't have valid identity credentials. + owner = pageInfoBundle.getString("securityNoOwner"); + verifier = pageInfoBundle.getString("notset"); + } + + setText("security-identity-owner-value", owner); + setText("security-identity-verifier-value", verifier); + + /* Manage the View Cert button*/ + var viewCert = document.getElementById("security-view-cert"); + if (info.cert) { + security._cert = info.cert; + viewCert.collapsed = false; + } + else + viewCert.collapsed = true; + + /* Set Privacy & History section text */ + var yesStr = pageInfoBundle.getString("yes"); + var noStr = pageInfoBundle.getString("no"); + + setText("security-privacy-cookies-value", + hostHasCookies(uri) ? yesStr : noStr); + setText("security-privacy-passwords-value", + realmHasPasswords(uri) ? yesStr : noStr); + + var visitCount = previousVisitCount(info.hostName); + if (visitCount > 1) { + setText("security-privacy-history-value", + pageInfoBundle.getFormattedString("securityNVisits", [visitCount.toLocaleString()])); + } + else if (visitCount == 1) { + setText("security-privacy-history-value", + pageInfoBundle.getString("securityOneVisit")); + } + else { + setText("security-privacy-history-value", noStr); + } + + /* Set the Technical Detail section messages */ + const pkiBundle = document.getElementById("pkiBundle"); + var hdr; + var msg1; + var msg2; + + if (info.isBroken) { + if (info.isMixed) { + hdr = pkiBundle.getString("pageInfo_MixedContent"); + msg1 = pkiBundle.getString("pageInfo_MixedContent2"); + } else { + hdr = pkiBundle.getFormattedString("pageInfo_BrokenEncryption", + [info.encryptionAlgorithm, + info.encryptionStrength + "", + info.version]); + msg1 = pkiBundle.getString("pageInfo_WeakCipher"); + } + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } + else if (info.encryptionStrength > 0) { + hdr = pkiBundle.getFormattedString("pageInfo_EncryptionWithBitsAndProtocol", + [info.encryptionAlgorithm, + info.encryptionStrength + "", + info.version]); + msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1"); + msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2"); + security._cert = info.cert; + } + else { + hdr = pkiBundle.getString("pageInfo_NoEncryption"); + if (info.hostName != null) + msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [info.hostName]); + else + msg1 = pkiBundle.getString("pageInfo_Privacy_None4"); + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } + setText("security-technical-shortform", hdr); + setText("security-technical-longform1", msg1); + setText("security-technical-longform2", msg2); + + const ctStatus = + document.getElementById("security-technical-certificate-transparency"); + if (info.certificateTransparency) { + ctStatus.hidden = false; + ctStatus.value = pkiBundle.getString( + "pageInfo_CertificateTransparency_" + info.certificateTransparency); + } else { + ctStatus.hidden = true; + } +} + +function setText(id, value) +{ + var element = document.getElementById(id); + if (!element) + return; + if (element.localName == "textbox" || element.localName == "label") + element.value = value; + else { + if (element.hasChildNodes()) + element.removeChild(element.firstChild); + var textNode = document.createTextNode(value); + element.appendChild(textNode); + } +} + +function viewCertHelper(parent, cert) +{ + if (!cert) + return; + + var cd = Components.classes[CERTIFICATEDIALOGS_CONTRACTID].getService(nsICertificateDialogs); + cd.viewCert(parent, cert); +} + +/** + * Return true iff we have cookies for uri + */ +function hostHasCookies(uri) { + var cookieManager = Components.classes["@mozilla.org/cookiemanager;1"] + .getService(Components.interfaces.nsICookieManager2); + + return cookieManager.countCookiesFromHost(uri.asciiHost) > 0; +} + +/** + * Return true iff realm (proto://host:port) (extracted from uri) has + * saved passwords + */ +function realmHasPasswords(uri) { + var passwordManager = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + return passwordManager.countLogins(uri.prePath, "", "") > 0; +} + +/** + * Return the number of previous visits recorded for host before today. + * + * @param host - the domain name to look for in history + */ +function previousVisitCount(host, endTimeReference) { + if (!host) + return false; + + var historyService = Components.classes["@mozilla.org/browser/nav-history-service;1"] + .getService(Components.interfaces.nsINavHistoryService); + + var options = historyService.getNewQueryOptions(); + options.resultType = options.RESULTS_AS_VISIT; + + // Search for visits to this host before today + var query = historyService.getNewQuery(); + query.endTimeReference = query.TIME_RELATIVE_TODAY; + query.endTime = 0; + query.domain = host; + + var result = historyService.executeQuery(query, options); + result.root.containerOpen = true; + var cc = result.root.childCount; + result.root.containerOpen = false; + return cc; +} diff --git a/application/basilisk/base/content/popup-notifications.inc b/application/basilisk/base/content/popup-notifications.inc new file mode 100644 index 000000000..bdc2d0bd3 --- /dev/null +++ b/application/basilisk/base/content/popup-notifications.inc @@ -0,0 +1,81 @@ +# to be included inside a popupset element + + <panel id="notification-popup" + type="arrow" + position="after_start" + hidden="true" + orient="vertical" + noautofocus="true" + role="alert"/> + + <popupnotification id="webRTC-shareDevices-notification" hidden="true"> + <popupnotificationcontent id="webRTC-selectCamera" orient="vertical"> + <label value="&getUserMedia.selectCamera.label;" + accesskey="&getUserMedia.selectCamera.accesskey;" + control="webRTC-selectCamera-menulist"/> + <menulist id="webRTC-selectCamera-menulist"> + <menupopup id="webRTC-selectCamera-menupopup"/> + </menulist> + </popupnotificationcontent> + + <popupnotificationcontent id="webRTC-selectWindowOrScreen" orient="vertical"> + <label id="webRTC-selectWindow-label" + control="webRTC-selectWindow-menulist"/> + <menulist id="webRTC-selectWindow-menulist" + oncommand="webrtcUI.updateMainActionLabel(this);"> + <menupopup id="webRTC-selectWindow-menupopup"/> + </menulist> + <description id="webRTC-all-windows-shared" hidden="true">&getUserMedia.allWindowsShared.message;</description> + </popupnotificationcontent> + + <popupnotificationcontent id="webRTC-preview" hidden="true"> + <html:video id="webRTC-previewVideo"/> + <vbox id="webRTC-previewWarningBox"> + <spacer flex="1"/> + <description id="webRTC-previewWarning"/> + </vbox> + </popupnotificationcontent> + + <popupnotificationcontent id="webRTC-selectMicrophone" orient="vertical"> + <label value="&getUserMedia.selectMicrophone.label;" + accesskey="&getUserMedia.selectMicrophone.accesskey;" + control="webRTC-selectMicrophone-menulist"/> + <menulist id="webRTC-selectMicrophone-menulist"> + <menupopup id="webRTC-selectMicrophone-menupopup"/> + </menulist> + </popupnotificationcontent> + </popupnotification> + + <popupnotification id="servicesInstall-notification" hidden="true"> + <popupnotificationcontent orient="vertical" align="start"> + <!-- XXX bug 974146, tests are looking for this, can't remove yet. --> + </popupnotificationcontent> + </popupnotification> + + <popupnotification id="password-notification" hidden="true"> + <popupnotificationcontent orient="vertical"> + <textbox id="password-notification-username"/> + <textbox id="password-notification-password" type="password" show-content=""/> + <checkbox id="password-notification-visibilityToggle" hidden="true"/> + </popupnotificationcontent> + </popupnotification> + + + <popupnotification id="addon-progress-notification" hidden="true"> + <popupnotificationcontent orient="vertical"> + <progressmeter id="addon-progress-notification-progressmeter"/> + <label id="addon-progress-notification-progresstext" crop="end"/> + </popupnotificationcontent> + <button id="addon-progress-cancel" + oncommand="this.parentNode.cancel();"/> + <button id="addon-progress-accept" disabled="true"/> + </popupnotification> + + <popupnotification id="addon-install-confirmation-notification" hidden="true"> + <popupnotificationcontent id="addon-install-confirmation-content" orient="vertical"/> + <button id="addon-install-confirmation-cancel" + oncommand="PopupNotifications.getNotification('addon-install-confirmation').remove();"/> + <button id="addon-install-confirmation-accept" + oncommand="gXPInstallObserver.acceptInstallation(); + PopupNotifications.getNotification('addon-install-confirmation').remove();"/> + </popupnotification> diff --git a/application/basilisk/base/content/report-phishing-overlay.xul b/application/basilisk/base/content/report-phishing-overlay.xul new file mode 100644 index 000000000..712079f82 --- /dev/null +++ b/application/basilisk/base/content/report-phishing-overlay.xul @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ +<!ENTITY % reportphishDTD SYSTEM "chrome://browser/locale/safebrowsing/report-phishing.dtd"> +%reportphishDTD; +<!ENTITY % safebrowsingDTD SYSTEM "chrome://browser/locale/safebrowsing/phishing-afterload-warning-message.dtd"> +%safebrowsingDTD; +]> + +<overlay id="reportPhishingMenuOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <broadcasterset id="mainBroadcasterSet"> + <broadcaster id="reportPhishingBroadcaster" disabled="true"/> + <broadcaster id="reportPhishingErrorBroadcaster" disabled="true"/> + </broadcasterset> + <menupopup id="menu_HelpPopup"> + <menuitem id="menu_HelpPopup_reportPhishingtoolmenu" + label="&reportDeceptiveSiteMenu.title;" + accesskey="&reportDeceptiveSiteMenu.accesskey;" + insertbefore="aboutSeparator" + observes="reportPhishingBroadcaster" + oncommand="openUILink(gSafeBrowsing.getReportURL('Phish'), event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="menu_HelpPopup_reportPhishingErrortoolmenu" + label="&safeb.palm.notdeceptive.label;" + accesskey="&safeb.palm.notdeceptive.accesskey;" + insertbefore="aboutSeparator" + observes="reportPhishingErrorBroadcaster" + oncommand="openUILinkIn(gSafeBrowsing.getReportURL('PhishMistake'), 'tab');" + onclick="checkForMiddleClick(this, event);"/> + </menupopup> +</overlay> diff --git a/application/basilisk/base/content/safeMode.css b/application/basilisk/base/content/safeMode.css new file mode 100644 index 000000000..4f093a452 --- /dev/null +++ b/application/basilisk/base/content/safeMode.css @@ -0,0 +1,8 @@ +/* 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/. */ + +#resetProfileFooter { + font-weight: bold; +} + diff --git a/application/basilisk/base/content/safeMode.js b/application/basilisk/base/content/safeMode.js new file mode 100644 index 000000000..7f34c2c58 --- /dev/null +++ b/application/basilisk/base/content/safeMode.js @@ -0,0 +1,82 @@ +/* -*- 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 Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +const appStartup = Services.startup; + +Cu.import("resource://gre/modules/ResetProfile.jsm"); + +var defaultToReset = false; + +function restartApp() { + appStartup.quit(appStartup.eForceQuit | appStartup.eRestart); +} + +function resetProfile() { + // Set the reset profile environment variable. + let env = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment); + env.set("MOZ_RESET_PROFILE_RESTART", "1"); +} + +function showResetDialog() { + // Prompt the user to confirm the reset. + let retVals = { + reset: false, + }; + window.openDialog("chrome://global/content/resetProfile.xul", null, + "chrome,modal,centerscreen,titlebar,dialog=yes", retVals); + if (!retVals.reset) + return; + resetProfile(); + restartApp(); +} + +function onDefaultButton() { + if (defaultToReset) { + // Restart to reset the profile. + resetProfile(); + restartApp(); + // Return false to prevent starting into safe mode while restarting. + return false; + } + // Continue in safe mode. No restart needed. + return true; +} + +function onCancel() { + appStartup.quit(appStartup.eForceQuit); +} + +function onExtra1() { + if (defaultToReset) { + // Continue in safe mode + window.close(); + return true; + } + // The reset dialog will handle starting the reset process if the user confirms. + showResetDialog(); + return false; +} + +function onLoad() { + if (appStartup.automaticSafeModeNecessary) { + document.getElementById("autoSafeMode").hidden = false; + document.getElementById("safeMode").hidden = true; + if (ResetProfile.resetSupported()) { + document.getElementById("resetProfile").hidden = false; + } else { + // Hide the reset button is it's not supported. + document.documentElement.getButton("extra1").hidden = true; + } + } else if (!ResetProfile.resetSupported()) { + // Hide the reset button and text if it's not supported. + document.documentElement.getButton("extra1").hidden = true; + document.getElementById("resetProfileInstead").hidden = true; + } +} diff --git a/application/basilisk/base/content/safeMode.xul b/application/basilisk/base/content/safeMode.xul new file mode 100644 index 000000000..a94de5fba --- /dev/null +++ b/application/basilisk/base/content/safeMode.xul @@ -0,0 +1,51 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE prefwindow [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % safeModeDTD SYSTEM "chrome://browser/locale/safeMode.dtd" > +%safeModeDTD; +<!ENTITY % resetProfileDTD SYSTEM "chrome://global/locale/resetProfile.dtd" > +%resetProfileDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/content/safeMode.css"?> + +<dialog id="safeModeDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&safeModeDialog.title;" + buttons="accept,extra1" + buttonlabelaccept="&startSafeMode.label;" + buttonlabelextra1="&refreshProfile.label;" + maxwidth="&window.maxWidth;" + ondialogaccept="return onDefaultButton()" + ondialogcancel="onCancel();" + ondialogextra1="return onExtra1()" + onload="onLoad()"> + + <script type="application/javascript" src="chrome://global/content/resetProfile.js"/> + <script type="application/javascript" src="chrome://browser/content/safeMode.js"/> + + <vbox id="autoSafeMode" hidden="true"> + <description>&autoSafeModeDescription3.label;</description> + </vbox> + + <vbox id="safeMode"> + <label>&safeModeDescription3.label;</label> + <separator class="thin"/> + <label>&safeModeDescription4.label;</label> + <separator class="thin"/> + <label id="resetProfileInstead">&refreshProfileInstead.label;</label> + </vbox> + + <vbox id="resetProfile" hidden="true"> + <label id="resetProfileInstead">&refreshProfileInstead.label;</label> + </vbox> + + <separator class="thin"/> +</dialog> diff --git a/application/basilisk/base/content/sanitize.js b/application/basilisk/base/content/sanitize.js new file mode 100644 index 000000000..841376580 --- /dev/null +++ b/application/basilisk/base/content/sanitize.js @@ -0,0 +1,910 @@ +// -*- 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/. */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", + "resource://gre/modules/AppConstants.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", + "resource://gre/modules/TelemetryStopwatch.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/Console.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Preferences", + "resource://gre/modules/Preferences.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "setTimeout", + "resource://gre/modules/Timer.jsm"); + + +XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager"); +XPCOMUtils.defineLazyServiceGetter(this, "quotaManagerService", + "@mozilla.org/dom/quota-manager-service;1", + "nsIQuotaManagerService"); + +var {classes: Cc, interfaces: Ci, results: Cr} = Components; + +/** + * A number of iterations after which to yield time back + * to the system. + */ +const YIELD_PERIOD = 10; + +function Sanitizer() { +} +Sanitizer.prototype = { + // warning to the caller: this one may raise an exception (e.g. bug #265028) + clearItem: function (aItemName) + { + this.items[aItemName].clear(); + }, + + prefDomain: "", + + getNameFromPreference: function (aPreferenceName) + { + return aPreferenceName.substr(this.prefDomain.length); + }, + + /** + * Deletes privacy sensitive data in a batch, according to user preferences. + * Returns a promise which is resolved if no errors occurred. If an error + * occurs, a message is reported to the console and all other items are still + * cleared before the promise is finally rejected. + * + * @param [optional] itemsToClear + * Array of items to be cleared. if specified only those + * items get cleared, irrespectively of the preference settings. + * @param [optional] options + * Object whose properties are options for this sanitization. + * TODO (bug 1167238) document options here. + */ + sanitize: Task.async(function*(itemsToClear = null, options = {}) { + let progress = options.progress || {}; + let promise = this._sanitize(itemsToClear, progress); + + // Depending on preferences, the sanitizer may perform asynchronous + // work before it starts cleaning up the Places database (e.g. closing + // windows). We need to make sure that the connection to that database + // hasn't been closed by the time we use it. + // Though, if this is a sanitize on shutdown, we already have a blocker. + if (!progress.isShutdown) { + let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"] + .getService(Ci.nsPIPlacesDatabase) + .shutdownClient + .jsclient; + shutdownClient.addBlocker("sanitize.js: Sanitize", + promise, + { + fetchState: () => ({ progress }) + } + ); + } + + try { + yield promise; + } finally { + Services.obs.notifyObservers(null, "sanitizer-sanitization-complete", ""); + } + }), + + _sanitize: Task.async(function*(aItemsToClear, progress = {}) { + let seenError = false; + let itemsToClear; + if (Array.isArray(aItemsToClear)) { + // Shallow copy the array, as we are going to modify + // it in place later. + itemsToClear = [...aItemsToClear]; + } else { + let branch = Services.prefs.getBranch(this.prefDomain); + itemsToClear = Object.keys(this.items).filter(itemName => { + try { + return branch.getBoolPref(itemName); + } catch (ex) { + return false; + } + }); + } + + // Store the list of items to clear, in case we are killed before we + // get a chance to complete. + Preferences.set(Sanitizer.PREF_SANITIZE_IN_PROGRESS, + JSON.stringify(itemsToClear)); + + // Store the list of items to clear, for debugging/forensics purposes + for (let k of itemsToClear) { + progress[k] = "ready"; + } + + // Ensure open windows get cleared first, if they're in our list, so that they don't stick + // around in the recently closed windows list, and so we can cancel the whole thing + // if the user selects to keep a window open from a beforeunload prompt. + let openWindowsIndex = itemsToClear.indexOf("openWindows"); + if (openWindowsIndex != -1) { + itemsToClear.splice(openWindowsIndex, 1); + yield this.items.openWindows.clear(); + progress.openWindows = "cleared"; + } + + // Cache the range of times to clear + let range = null; + // If we ignore timespan, clear everything, + // otherwise, pick a range. + if (!this.ignoreTimespan) { + range = this.range || Sanitizer.getClearRange(); + } + + // For performance reasons we start all the clear tasks at once, then wait + // for their promises later. + // Some of the clear() calls may raise exceptions (for example bug 265028), + // we catch and store them, but continue to sanitize as much as possible. + // Callers should check returned errors and give user feedback + // about items that could not be sanitized + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_TOTAL", refObj); + + let annotateError = (name, ex) => { + progress[name] = "failed"; + seenError = true; + console.error("Error sanitizing " + name, ex); + }; + + // Array of objects in form { name, promise }. + // `name` is the item's name and `promise` may be a promise, if the + // sanitization is asynchronous, or the function return value, otherwise. + let handles = []; + for (let itemName of itemsToClear) { + // Workaround for bug 449811. + let name = itemName; + let item = this.items[name]; + try { + // Catch errors here, so later we can just loop through these. + handles.push({ name, + promise: item.clear(range) + .then(() => progress[name] = "cleared", + ex => annotateError(name, ex)) + }); + } catch (ex) { + annotateError(name, ex); + } + } + for (let handle of handles) { + progress[handle.name] = "blocking"; + yield handle.promise; + } + + // Sanitization is complete. + TelemetryStopwatch.finish("FX_SANITIZE_TOTAL", refObj); + // Reset the inProgress preference since we were not killed during + // sanitization. + Preferences.reset(Sanitizer.PREF_SANITIZE_IN_PROGRESS); + progress = {}; + if (seenError) { + throw new Error("Error sanitizing"); + } + }), + + // Time span only makes sense in certain cases. Consumers who want + // to only clear some private data can opt in by setting this to false, + // and can optionally specify a specific range. If timespan is not ignored, + // and range is not set, sanitize() will use the value of the timespan + // pref to determine a range + ignoreTimespan : true, + range : null, + + items: { + cache: { + clear: Task.async(function* (range) { + let seenException; + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_CACHE", refObj); + + try { + // Cache doesn't consult timespan, nor does it have the + // facility for timespan-based eviction. Wipe it. + let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Ci.nsICacheStorageService); + cache.clear(); + } catch (ex) { + seenException = ex; + } + + try { + let imageCache = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .getImgCacheForDocument(null); + imageCache.clearCache(false); // true=chrome, false=content + } catch (ex) { + seenException = ex; + } + + TelemetryStopwatch.finish("FX_SANITIZE_CACHE", refObj); + if (seenException) { + throw seenException; + } + }) + }, + + cookies: { + clear: Task.async(function* (range) { + let seenException; + let yieldCounter = 0; + let refObj = {}; + + // Clear cookies. + TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj); + try { + let cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"] + .getService(Ci.nsICookieManager); + if (range) { + // Iterate through the cookies and delete any created after our cutoff. + let cookiesEnum = cookieMgr.enumerator; + while (cookiesEnum.hasMoreElements()) { + let cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2); + + if (cookie.creationTime > range[0]) { + // This cookie was created after our cutoff, clear it + cookieMgr.remove(cookie.host, cookie.name, cookie.path, + false, cookie.originAttributes); + + if (++yieldCounter % YIELD_PERIOD == 0) { + yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long + } + } + } + } + else { + // Remove everything + cookieMgr.removeAll(); + yield new Promise(resolve => setTimeout(resolve, 0)); // Don't block the main thread too long + } + } catch (ex) { + seenException = ex; + } finally { + TelemetryStopwatch.finish("FX_SANITIZE_COOKIES_2", refObj); + } + + // Clear deviceIds. Done asynchronously (returns before complete). + try { + let mediaMgr = Components.classes["@mozilla.org/mediaManagerService;1"] + .getService(Ci.nsIMediaManagerService); + mediaMgr.sanitizeDeviceIds(range && range[0]); + } catch (ex) { + seenException = ex; + } + + // Clear plugin data. + // As evidenced in bug 1253204, clearing plugin data can sometimes be + // very, very long, for mysterious reasons. Unfortunately, this is not + // something actionable by Mozilla, so crashing here serves no purpose. + // + // For this reason, instead of waiting for sanitization to always + // complete, we introduce a soft timeout. Once this timeout has + // elapsed, we proceed with the shutdown of Firefox. + let promiseClearPluginCookies; + try { + // We don't want to wait for this operation to complete... + promiseClearPluginCookies = this.promiseClearPluginCookies(range); + + // ... at least, not for more than 10 seconds. + yield Promise.race([ + promiseClearPluginCookies, + new Promise(resolve => setTimeout(resolve, 10000 /* 10 seconds */)) + ]); + } catch (ex) { + seenException = ex; + } + + // Detach waiting for plugin cookies to be cleared. + promiseClearPluginCookies.catch(() => { + // If this exception is raised before the soft timeout, it + // will appear in `seenException`. Otherwise, it's too late + // to do anything about it. + }); + + if (seenException) { + throw seenException; + } + }), + + promiseClearPluginCookies: Task.async(function* (range) { + const FLAG_CLEAR_ALL = Ci.nsIPluginHost.FLAG_CLEAR_ALL; + let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + + // Determine age range in seconds. (-1 means clear all.) We don't know + // that range[1] is actually now, so we compute age range based + // on the lower bound. If range results in a negative age, do nothing. + let age = range ? (Date.now() / 1000 - range[0] / 1000000) : -1; + if (!range || age >= 0) { + let tags = ph.getPluginTags(); + for (let tag of tags) { + let refObj = {}; + let probe = ""; + if (/\bFlash\b/.test(tag.name)) { + probe = tag.loaded ? "FX_SANITIZE_LOADED_FLASH" + : "FX_SANITIZE_UNLOADED_FLASH"; + TelemetryStopwatch.start(probe, refObj); + } + try { + let rv = yield new Promise(resolve => + ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, age, resolve) + ); + // If the plugin doesn't support clearing by age, clear everything. + if (rv == Components.results.NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) { + yield new Promise(resolve => + ph.clearSiteData(tag, null, FLAG_CLEAR_ALL, -1, resolve) + ); + } + if (probe) { + TelemetryStopwatch.finish(probe, refObj); + } + } catch (ex) { + // Ignore errors from plug-ins + if (probe) { + TelemetryStopwatch.cancel(probe, refObj); + } + } + } + } + }) + }, + + offlineApps: { + clear: Task.async(function* (range) { + // AppCache + Components.utils.import("resource:///modules/offlineAppCache.jsm"); + // This doesn't wait for the cleanup to be complete. + OfflineAppCacheHelper.clear(); + + // LocalStorage + Services.obs.notifyObservers(null, "extension:purge-localStorage", null); + + // ServiceWorkers + let serviceWorkers = serviceWorkerManager.getAllRegistrations(); + for (let i = 0; i < serviceWorkers.length; i++) { + let sw = serviceWorkers.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo); + let host = sw.principal.URI.host; + serviceWorkerManager.removeAndPropagate(host); + } + + // QuotaManager + let promises = []; + yield new Promise(resolve => { + quotaManagerService.getUsage(request => { + if (request.resultCode != Cr.NS_OK) { + // We are probably shutting down. We don't want to propagate the + // error, rejecting the promise. + resolve(); + return; + } + + for (let item of request.result) { + let principal = Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(item.origin); + let uri = principal.URI; + if (uri.scheme == "http" || uri.scheme == "https" || uri.scheme == "file") { + promises.push(new Promise(r => { + let req = quotaManagerService.clearStoragesForPrincipal(principal, null, true); + req.callback = () => { r(); }; + })); + } + } + resolve(); + }); + }); + + yield Promise.all(promises); + }) + }, + + history: { + clear: Task.async(function* (range) { + let seenException; + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj); + try { + if (range) { + yield PlacesUtils.history.removeVisitsByFilter({ + beginDate: new Date(range[0] / 1000), + endDate: new Date(range[1] / 1000) + }); + } else { + // Remove everything. + yield PlacesUtils.history.clear(); + } + } catch (ex) { + seenException = ex; + } finally { + TelemetryStopwatch.finish("FX_SANITIZE_HISTORY", refObj); + } + + try { + let clearStartingTime = range ? String(range[0]) : ""; + Services.obs.notifyObservers(null, "browser:purge-session-history", clearStartingTime); + } catch (ex) { + seenException = ex; + } + + try { + let predictor = Components.classes["@mozilla.org/network/predictor;1"] + .getService(Components.interfaces.nsINetworkPredictor); + predictor.reset(); + } catch (ex) { + seenException = ex; + } + + if (seenException) { + throw seenException; + } + }) + }, + + formdata: { + clear: Task.async(function* (range) { + let seenException; + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_FORMDATA", refObj); + try { + // Clear undo history of all searchBars + let windows = Services.wm.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + let currentWindow = windows.getNext(); + let currentDocument = currentWindow.document; + let searchBar = currentDocument.getElementById("searchbar"); + if (searchBar) + searchBar.textbox.reset(); + let tabBrowser = currentWindow.gBrowser; + if (!tabBrowser) { + // No tab browser? This means that it's too early during startup (typically, + // Session Restore hasn't completed yet). Since we don't have find + // bars at that stage and since Session Restore will not restore + // find bars further down during startup, we have nothing to clear. + continue; + } + for (let tab of tabBrowser.tabs) { + if (tabBrowser.isFindBarInitialized(tab)) + tabBrowser.getFindBar(tab).clear(); + } + // Clear any saved find value + tabBrowser._lastFindValue = ""; + } + } catch (ex) { + seenException = ex; + } + + try { + let change = { op: "remove" }; + if (range) { + [ change.firstUsedStart, change.firstUsedEnd ] = range; + } + yield new Promise(resolve => { + FormHistory.update(change, { + handleError(e) { + seenException = new Error("Error " + e.result + ": " + e.message); + }, + handleCompletion() { + resolve(); + } + }); + }); + } catch (ex) { + seenException = ex; + } + + TelemetryStopwatch.finish("FX_SANITIZE_FORMDATA", refObj); + if (seenException) { + throw seenException; + } + }) + }, + + downloads: { + clear: Task.async(function* (range) { + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_DOWNLOADS", refObj); + try { + let filterByTime = null; + if (range) { + // Convert microseconds back to milliseconds for date comparisons. + let rangeBeginMs = range[0] / 1000; + let rangeEndMs = range[1] / 1000; + filterByTime = download => download.startTime >= rangeBeginMs && + download.startTime <= rangeEndMs; + } + + // Clear all completed/cancelled downloads + let list = yield Downloads.getList(Downloads.ALL); + list.removeFinished(filterByTime); + } finally { + TelemetryStopwatch.finish("FX_SANITIZE_DOWNLOADS", refObj); + } + }) + }, + + sessions: { + clear: Task.async(function* (range) { + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_SESSIONS", refObj); + + try { + // clear all auth tokens + let sdr = Components.classes["@mozilla.org/security/sdr;1"] + .getService(Components.interfaces.nsISecretDecoderRing); + sdr.logoutAndTeardown(); + + // clear FTP and plain HTTP auth sessions + Services.obs.notifyObservers(null, "net:clear-active-logins", null); + } finally { + TelemetryStopwatch.finish("FX_SANITIZE_SESSIONS", refObj); + } + }) + }, + + siteSettings: { + clear: Task.async(function* (range) { + let seenException; + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_SITESETTINGS", refObj); + + let startDateMS = range ? range[0] / 1000 : null; + + try { + // Clear site-specific permissions like "Allow this site to open popups" + // we ignore the "end" range and hope it is now() - none of the + // interfaces used here support a true range anyway. + if (startDateMS == null) { + Services.perms.removeAll(); + } else { + Services.perms.removeAllSince(startDateMS); + } + } catch (ex) { + seenException = ex; + } + + try { + // Clear site-specific settings like page-zoom level + let cps = Components.classes["@mozilla.org/content-pref/service;1"] + .getService(Components.interfaces.nsIContentPrefService2); + if (startDateMS == null) { + cps.removeAllDomains(null); + } else { + cps.removeAllDomainsSince(startDateMS, null); + } + } catch (ex) { + seenException = ex; + } + + try { + // Clear site security settings - no support for ranges in this + // interface either, so we clearAll(). + let sss = Cc["@mozilla.org/ssservice;1"] + .getService(Ci.nsISiteSecurityService); + sss.clearAll(); + } catch (ex) { + seenException = ex; + } + + // Clear all push notification subscriptions + try { + yield new Promise((resolve, reject) => { + let push = Cc["@mozilla.org/push/Service;1"] + .getService(Ci.nsIPushService); + push.clearForDomain("*", status => { + if (Components.isSuccessCode(status)) { + resolve(); + } else { + reject(new Error("Error clearing push subscriptions: " + + status)); + } + }); + }); + } catch (ex) { + seenException = ex; + } + + TelemetryStopwatch.finish("FX_SANITIZE_SITESETTINGS", refObj); + if (seenException) { + throw seenException; + } + }) + }, + + openWindows: { + privateStateForNewWindow: "non-private", + _canCloseWindow: function(aWindow) { + if (aWindow.CanCloseWindow()) { + // We already showed PermitUnload for the window, so let's + // make sure we don't do it again when we actually close the + // window. + aWindow.skipNextCanClose = true; + return true; + } + return false; + }, + _resetAllWindowClosures: function(aWindowList) { + for (let win of aWindowList) { + win.skipNextCanClose = false; + } + }, + clear: Task.async(function* () { + // NB: this closes all *browser* windows, not other windows like the library, about window, + // browser console, etc. + + // Keep track of the time in case we get stuck in la-la-land because of onbeforeunload + // dialogs + let existingWindow = Services.appShell.hiddenDOMWindow; + let startDate = existingWindow.performance.now(); + + // First check if all these windows are OK with being closed: + let windowEnumerator = Services.wm.getEnumerator("navigator:browser"); + let windowList = []; + while (windowEnumerator.hasMoreElements()) { + let someWin = windowEnumerator.getNext(); + windowList.push(someWin); + // If someone says "no" to a beforeunload prompt, we abort here: + if (!this._canCloseWindow(someWin)) { + this._resetAllWindowClosures(windowList); + throw new Error("Sanitize could not close windows: cancelled by user"); + } + + // ...however, beforeunload prompts spin the event loop, and so the code here won't get + // hit until the prompt has been dismissed. If more than 1 minute has elapsed since we + // started prompting, stop, because the user might not even remember initiating the + // 'forget', and the timespans will be all wrong by now anyway: + if (existingWindow.performance.now() > (startDate + 60 * 1000)) { + this._resetAllWindowClosures(windowList); + throw new Error("Sanitize could not close windows: timeout"); + } + } + + // If/once we get here, we should actually be able to close all windows. + + let refObj = {}; + TelemetryStopwatch.start("FX_SANITIZE_OPENWINDOWS", refObj); + + // First create a new window. We do this first so that on non-mac, we don't + // accidentally close the app by closing all the windows. + let handler = Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler); + let defaultArgs = handler.defaultArgs; + let features = "chrome,all,dialog=no," + this.privateStateForNewWindow; + let newWindow = existingWindow.openDialog("chrome://browser/content/", "_blank", + features, defaultArgs); + + let onFullScreen = null; + if (AppConstants.platform == "macosx") { + onFullScreen = function(e) { + newWindow.removeEventListener("fullscreen", onFullScreen); + let docEl = newWindow.document.documentElement; + let sizemode = docEl.getAttribute("sizemode"); + if (!newWindow.fullScreen && sizemode == "fullscreen") { + docEl.setAttribute("sizemode", "normal"); + e.preventDefault(); + e.stopPropagation(); + return false; + } + return undefined; + } + newWindow.addEventListener("fullscreen", onFullScreen); + } + + let promiseReady = new Promise(resolve => { + // Window creation and destruction is asynchronous. We need to wait + // until all existing windows are fully closed, and the new window is + // fully open, before continuing. Otherwise the rest of the sanitizer + // could run too early (and miss new cookies being set when a page + // closes) and/or run too late (and not have a fully-formed window yet + // in existence). See bug 1088137. + let newWindowOpened = false; + let onWindowOpened = function(subject, topic, data) { + if (subject != newWindow) + return; + + Services.obs.removeObserver(onWindowOpened, "browser-delayed-startup-finished"); + if (AppConstants.platform == "macosx") { + newWindow.removeEventListener("fullscreen", onFullScreen); + } + newWindowOpened = true; + // If we're the last thing to happen, invoke callback. + if (numWindowsClosing == 0) { + TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj); + resolve(); + } + } + + let numWindowsClosing = windowList.length; + let onWindowClosed = function() { + numWindowsClosing--; + if (numWindowsClosing == 0) { + Services.obs.removeObserver(onWindowClosed, "xul-window-destroyed"); + // If we're the last thing to happen, invoke callback. + if (newWindowOpened) { + TelemetryStopwatch.finish("FX_SANITIZE_OPENWINDOWS", refObj); + resolve(); + } + } + } + Services.obs.addObserver(onWindowOpened, "browser-delayed-startup-finished", false); + Services.obs.addObserver(onWindowClosed, "xul-window-destroyed", false); + }); + + // Start the process of closing windows + while (windowList.length) { + windowList.pop().close(); + } + newWindow.focus(); + yield promiseReady; + }) + }, + } +}; + +// The preferences branch for the sanitizer. +Sanitizer.PREF_DOMAIN = "privacy.sanitize."; +// Whether we should sanitize on shutdown. +Sanitizer.PREF_SANITIZE_ON_SHUTDOWN = "privacy.sanitize.sanitizeOnShutdown"; +// During a sanitization this is set to a json containing the array of items +// being sanitized, then cleared once the sanitization is complete. +// This allows to retry a sanitization on startup in case it was interrupted +// by a crash. +Sanitizer.PREF_SANITIZE_IN_PROGRESS = "privacy.sanitize.sanitizeInProgress"; +// Whether the previous shutdown sanitization completed successfully. +// This is used to detect cases where we were supposed to sanitize on shutdown +// but due to a crash we were unable to. In such cases there may not be any +// sanitization in progress, cause we didn't have a chance to start it yet. +Sanitizer.PREF_SANITIZE_DID_SHUTDOWN = "privacy.sanitize.didShutdownSanitize"; + +// Time span constants corresponding to values of the privacy.sanitize.timeSpan +// pref. Used to determine how much history to clear, for various items +Sanitizer.TIMESPAN_EVERYTHING = 0; +Sanitizer.TIMESPAN_HOUR = 1; +Sanitizer.TIMESPAN_2HOURS = 2; +Sanitizer.TIMESPAN_4HOURS = 3; +Sanitizer.TIMESPAN_TODAY = 4; +Sanitizer.TIMESPAN_5MIN = 5; +Sanitizer.TIMESPAN_24HOURS = 6; + +// Return a 2 element array representing the start and end times, +// in the uSec-since-epoch format that PRTime likes. If we should +// clear everything, return null. Use ts if it is defined; otherwise +// use the timeSpan pref. +Sanitizer.getClearRange = function (ts) { + if (ts === undefined) + ts = Sanitizer.prefs.getIntPref("timeSpan"); + if (ts === Sanitizer.TIMESPAN_EVERYTHING) + return null; + + // PRTime is microseconds while JS time is milliseconds + var endDate = Date.now() * 1000; + switch (ts) { + case Sanitizer.TIMESPAN_5MIN : + var startDate = endDate - 300000000; // 5*60*1000000 + break; + case Sanitizer.TIMESPAN_HOUR : + startDate = endDate - 3600000000; // 1*60*60*1000000 + break; + case Sanitizer.TIMESPAN_2HOURS : + startDate = endDate - 7200000000; // 2*60*60*1000000 + break; + case Sanitizer.TIMESPAN_4HOURS : + startDate = endDate - 14400000000; // 4*60*60*1000000 + break; + case Sanitizer.TIMESPAN_TODAY : + var d = new Date(); // Start with today + d.setHours(0); // zero us back to midnight... + d.setMinutes(0); + d.setSeconds(0); + startDate = d.valueOf() * 1000; // convert to epoch usec + break; + case Sanitizer.TIMESPAN_24HOURS : + startDate = endDate - 86400000000; // 24*60*60*1000000 + break; + default: + throw "Invalid time span for clear private data: " + ts; + } + return [startDate, endDate]; +}; + +Sanitizer._prefs = null; +Sanitizer.__defineGetter__("prefs", function() +{ + return Sanitizer._prefs ? Sanitizer._prefs + : Sanitizer._prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService) + .getBranch(Sanitizer.PREF_DOMAIN); +}); + +// Shows sanitization UI +Sanitizer.showUI = function(aParentWindow) +{ + let win = AppConstants.platform == "macosx" ? + null: // make this an app-modal window on Mac + aParentWindow; + Services.ww.openWindow(win, + "chrome://browser/content/sanitize.xul", + "Sanitize", + "chrome,titlebar,dialog,centerscreen,modal", + null); +}; + +/** + * Deletes privacy sensitive data in a batch, optionally showing the + * sanitize UI, according to user preferences + */ +Sanitizer.sanitize = function(aParentWindow) +{ + Sanitizer.showUI(aParentWindow); +}; + +Sanitizer.onStartup = Task.async(function*() { + // Check if we were interrupted during the last shutdown sanitization. + let shutownSanitizationWasInterrupted = + Preferences.get(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, false) && + !Preferences.has(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN); + + if (Preferences.has(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN)) { + // Reset the pref, so that if we crash before having a chance to + // sanitize on shutdown, we will do at the next startup. + // Flushing prefs has a cost, so do this only if necessary. + Preferences.reset(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN); + Services.prefs.savePrefFile(null); + } + + // Make sure that we are triggered during shutdown. + let shutdownClient = Cc["@mozilla.org/browser/nav-history-service;1"] + .getService(Ci.nsPIPlacesDatabase) + .shutdownClient + .jsclient; + // We need to pass to sanitize() (through sanitizeOnShutdown) a state object + // that tracks the status of the shutdown blocker. This `progress` object + // will be updated during sanitization and reported with the crash in case of + // a shutdown timeout. + // We use the `options` argument to pass the `progress` object to sanitize(). + let progress = { isShutdown: true }; + shutdownClient.addBlocker("sanitize.js: Sanitize on shutdown", + () => sanitizeOnShutdown({ progress }), + { + fetchState: () => ({ progress }) + } + ); + + // Check if Firefox crashed during a sanitization. + let lastInterruptedSanitization = Preferences.get(Sanitizer.PREF_SANITIZE_IN_PROGRESS, ""); + if (lastInterruptedSanitization) { + let s = new Sanitizer(); + // If the json is invalid this will just throw and reject the Task. + let itemsToClear = JSON.parse(lastInterruptedSanitization); + yield s.sanitize(itemsToClear); + } else if (shutownSanitizationWasInterrupted) { + // Otherwise, could be we were supposed to sanitize on shutdown but we + // didn't have a chance, due to an earlier crash. + // In such a case, just redo a shutdown sanitize now, during startup. + yield sanitizeOnShutdown(); + } +}); + +var sanitizeOnShutdown = Task.async(function*(options = {}) { + if (!Preferences.get(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN)) { + return; + } + // Need to sanitize upon shutdown + let s = new Sanitizer(); + s.prefDomain = "privacy.clearOnShutdown."; + yield s.sanitize(null, options); + // We didn't crash during shutdown sanitization, so annotate it to avoid + // sanitizing again on startup. + Preferences.set(Sanitizer.PREF_SANITIZE_DID_SHUTDOWN, true); + Services.prefs.savePrefFile(null); +}); diff --git a/application/basilisk/base/content/sanitize.xul b/application/basilisk/base/content/sanitize.xul new file mode 100644 index 000000000..c00c6cda7 --- /dev/null +++ b/application/basilisk/base/content/sanitize.xul @@ -0,0 +1,183 @@ +<?xml version="1.0"?> + +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/sanitizeDialog.css"?> + +#ifdef CRH_DIALOG_TREE_VIEW +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> +#endif + +<?xml-stylesheet href="chrome://browser/content/sanitizeDialog.css"?> + +<!DOCTYPE prefwindow [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % sanitizeDTD SYSTEM "chrome://browser/locale/sanitize.dtd"> + %brandDTD; + %sanitizeDTD; +]> + +<prefwindow id="SanitizeDialog" type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + dlgbuttons="accept,cancel" + title="&sanitizeDialog2.title;" + noneverythingtitle="&sanitizeDialog2.title;" + style="width: &sanitizeDialog2.width;;" + ondialogaccept="return gSanitizePromptDialog.sanitize();"> + + <prefpane id="SanitizeDialogPane" onpaneload="gSanitizePromptDialog.init();"> + <stringbundle id="bundleBrowser" + src="chrome://browser/locale/browser.properties"/> + + <script type="application/javascript" + src="chrome://browser/content/sanitize.js"/> + +#ifdef CRH_DIALOG_TREE_VIEW + <script type="application/javascript" + src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" + src="chrome://browser/content/places/treeView.js"/> + <script type="application/javascript"><![CDATA[ + Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + Components.utils.import("resource:///modules/PlacesUIUtils.jsm"); + ]]></script> +#endif + + <script type="application/javascript" + src="chrome://browser/content/sanitizeDialog.js"/> + + <preferences id="sanitizePreferences"> + <preference id="privacy.cpd.history" name="privacy.cpd.history" type="bool"/> + <preference id="privacy.cpd.formdata" name="privacy.cpd.formdata" type="bool"/> + <preference id="privacy.cpd.downloads" name="privacy.cpd.downloads" type="bool" disabled="true"/> + <preference id="privacy.cpd.cookies" name="privacy.cpd.cookies" type="bool"/> + <preference id="privacy.cpd.cache" name="privacy.cpd.cache" type="bool"/> + <preference id="privacy.cpd.sessions" name="privacy.cpd.sessions" type="bool"/> + <preference id="privacy.cpd.offlineApps" name="privacy.cpd.offlineApps" type="bool"/> + <preference id="privacy.cpd.siteSettings" name="privacy.cpd.siteSettings" type="bool"/> + </preferences> + + <preferences id="nonItemPreferences"> + <preference id="privacy.sanitize.timeSpan" + name="privacy.sanitize.timeSpan" + type="int"/> + </preferences> + + <hbox id="SanitizeDurationBox" align="center"> + <label value="&clearTimeDuration.label;" + accesskey="&clearTimeDuration.accesskey;" + control="sanitizeDurationChoice" + id="sanitizeDurationLabel"/> + <menulist id="sanitizeDurationChoice" + preference="privacy.sanitize.timeSpan" + onselect="gSanitizePromptDialog.selectByTimespan();" + flex="1"> + <menupopup id="sanitizeDurationPopup"> +#ifdef CRH_DIALOG_TREE_VIEW + <menuitem label="" value="-1" id="sanitizeDurationCustom"/> +#endif + <menuitem label="&clearTimeDuration.lastHour;" value="1"/> + <menuitem label="&clearTimeDuration.last2Hours;" value="2"/> + <menuitem label="&clearTimeDuration.last4Hours;" value="3"/> + <menuitem label="&clearTimeDuration.today;" value="4"/> + <menuseparator/> + <menuitem label="&clearTimeDuration.everything;" value="0"/> + </menupopup> + </menulist> + <label id="sanitizeDurationSuffixLabel" + value="&clearTimeDuration.suffix;"/> + </hbox> + + <separator class="thin"/> + +#ifdef CRH_DIALOG_TREE_VIEW + <deck id="durationDeck"> + <tree id="placesTree" flex="1" hidecolumnpicker="true" rows="10" + disabled="true" disableKeyNavigation="true"> + <treecols> + <treecol id="date" label="&clearTimeDuration.dateColumn;" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="title" label="&clearTimeDuration.nameColumn;" flex="5"/> + </treecols> + <treechildren id="placesTreechildren" + ondragstart="gSanitizePromptDialog.grippyMoved('ondragstart', event);" + ondragover="gSanitizePromptDialog.grippyMoved('ondragover', event);" + onkeypress="gSanitizePromptDialog.grippyMoved('onkeypress', event);" + onmousedown="gSanitizePromptDialog.grippyMoved('onmousedown', event);"/> + </tree> +#endif + + <vbox id="sanitizeEverythingWarningBox"> + <spacer flex="1"/> + <hbox align="center"> + <image id="sanitizeEverythingWarningIcon"/> + <vbox id="sanitizeEverythingWarningDescBox" flex="1"> + <description id="sanitizeEverythingWarning"/> + <description id="sanitizeEverythingUndoWarning">&sanitizeEverythingUndoWarning;</description> + </vbox> + </hbox> + <spacer flex="1"/> + </vbox> + +#ifdef CRH_DIALOG_TREE_VIEW + </deck> +#endif + + <separator class="thin"/> + + <hbox id="detailsExpanderWrapper" align="center"> + <button type="image" + id="detailsExpander" + class="expander-down" + persist="class" + oncommand="gSanitizePromptDialog.toggleItemList();"/> + <label id="detailsExpanderLabel" + value="&detailsProgressiveDisclosure.label;" + accesskey="&detailsProgressiveDisclosure.accesskey;" + control="detailsExpander"/> + </hbox> + <listbox id="itemList" rows="7" collapsed="true" persist="collapsed"> + <listitem label="&itemHistoryAndDownloads.label;" + type="checkbox" + accesskey="&itemHistoryAndDownloads.accesskey;" + preference="privacy.cpd.history" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemFormSearchHistory.label;" + type="checkbox" + accesskey="&itemFormSearchHistory.accesskey;" + preference="privacy.cpd.formdata" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemCookies.label;" + type="checkbox" + accesskey="&itemCookies.accesskey;" + preference="privacy.cpd.cookies" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemCache.label;" + type="checkbox" + accesskey="&itemCache.accesskey;" + preference="privacy.cpd.cache" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemActiveLogins.label;" + type="checkbox" + accesskey="&itemActiveLogins.accesskey;" + preference="privacy.cpd.sessions" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemOfflineApps.label;" + type="checkbox" + accesskey="&itemOfflineApps.accesskey;" + preference="privacy.cpd.offlineApps" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemSitePreferences.label;" + type="checkbox" + accesskey="&itemSitePreferences.accesskey;" + preference="privacy.cpd.siteSettings" + noduration="true" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + </listbox> + + </prefpane> +</prefwindow> diff --git a/application/basilisk/base/content/sanitizeDialog.css b/application/basilisk/base/content/sanitizeDialog.css new file mode 100644 index 000000000..a7c17f094 --- /dev/null +++ b/application/basilisk/base/content/sanitizeDialog.css @@ -0,0 +1,23 @@ +/* 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/. */ + +/* Places tree */ + +#placesTreechildren { + -moz-user-focus: normal; +} + +#placesTreechildren::-moz-tree-cell(grippyRow), +#placesTreechildren::-moz-tree-cell-text(grippyRow), +#placesTreechildren::-moz-tree-image(grippyRow) { + cursor: grab; +} + + +/* Sanitize everything warnings */ + +#sanitizeEverythingWarning, +#sanitizeEverythingUndoWarning { + white-space: pre-wrap; +} diff --git a/application/basilisk/base/content/sanitizeDialog.js b/application/basilisk/base/content/sanitizeDialog.js new file mode 100644 index 000000000..279f1efd6 --- /dev/null +++ b/application/basilisk/base/content/sanitizeDialog.js @@ -0,0 +1,889 @@ +/* -*- 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 Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +var {Sanitizer} = Cu.import("resource:///modules/Sanitizer.jsm", {}); + +var gSanitizePromptDialog = { + + get bundleBrowser() + { + if (!this._bundleBrowser) + this._bundleBrowser = document.getElementById("bundleBrowser"); + return this._bundleBrowser; + }, + + get selectedTimespan() + { + var durList = document.getElementById("sanitizeDurationChoice"); + return parseInt(durList.value); + }, + + get sanitizePreferences() + { + if (!this._sanitizePreferences) { + this._sanitizePreferences = + document.getElementById("sanitizePreferences"); + } + return this._sanitizePreferences; + }, + + get warningBox() + { + return document.getElementById("sanitizeEverythingWarningBox"); + }, + + init: function () + { + // This is used by selectByTimespan() to determine if the window has loaded. + this._inited = true; + + var s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + + document.documentElement.getButton("accept").label = + this.bundleBrowser.getString("sanitizeButtonOK"); + + if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) { + this.prepareWarning(); + this.warningBox.hidden = false; + document.title = + this.bundleBrowser.getString("sanitizeDialog2.everything.title"); + } + else + this.warningBox.hidden = true; + }, + + selectByTimespan: function () + { + // This method is the onselect handler for the duration dropdown. As a + // result it's called a couple of times before onload calls init(). + if (!this._inited) + return; + + var warningBox = this.warningBox; + + // If clearing everything + if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) { + this.prepareWarning(); + if (warningBox.hidden) { + warningBox.hidden = false; + window.resizeBy(0, warningBox.boxObject.height); + } + window.document.title = + this.bundleBrowser.getString("sanitizeDialog2.everything.title"); + return; + } + + // If clearing a specific time range + if (!warningBox.hidden) { + window.resizeBy(0, -warningBox.boxObject.height); + warningBox.hidden = true; + } + window.document.title = + window.document.documentElement.getAttribute("noneverythingtitle"); + }, + + sanitize: function () + { + // Update pref values before handing off to the sanitizer (bug 453440) + this.updatePrefs(); + var s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + + s.range = Sanitizer.getClearRange(this.selectedTimespan); + s.ignoreTimespan = !s.range; + + // As the sanitize is async, we disable the buttons, update the label on + // the 'accept' button to indicate things are happening and return false - + // once the async operation completes (either with or without errors) + // we close the window. + let docElt = document.documentElement; + let acceptButton = docElt.getButton("accept"); + acceptButton.disabled = true; + acceptButton.setAttribute("label", + this.bundleBrowser.getString("sanitizeButtonClearing")); + docElt.getButton("cancel").disabled = true; + + try { + s.sanitize().then(null, Components.utils.reportError) + .then(() => window.close()) + .then(null, Components.utils.reportError); + } catch (er) { + Components.utils.reportError("Exception during sanitize: " + er); + return true; // We *do* want to close immediately on error. + } + }, + + /** + * If the panel that displays a warning when the duration is "Everything" is + * not set up, sets it up. Otherwise does nothing. + * + * @param aDontShowItemList Whether only the warning message should be updated. + * True means the item list visibility status should not + * be changed. + */ + prepareWarning: function (aDontShowItemList) { + // If the date and time-aware locale warning string is ever used again, + // initialize it here. Currently we use the no-visits warning string, + // which does not include date and time. See bug 480169 comment 48. + + var warningStringID; + if (this.hasNonSelectedItems()) { + warningStringID = "sanitizeSelectedWarning"; + if (!aDontShowItemList) + this.showItemList(); + } + else { + warningStringID = "sanitizeEverythingWarning2"; + } + + var warningDesc = document.getElementById("sanitizeEverythingWarning"); + warningDesc.textContent = + this.bundleBrowser.getString(warningStringID); + }, + + /** + * Called when the value of a preference element is synced from the actual + * pref. Enables or disables the OK button appropriately. + */ + onReadGeneric: function () + { + var found = false; + + // Find any other pref that's checked and enabled. + var i = 0; + while (!found && i < this.sanitizePreferences.childNodes.length) { + var preference = this.sanitizePreferences.childNodes[i]; + + found = !!preference.value && + !preference.disabled; + i++; + } + + try { + document.documentElement.getButton("accept").disabled = !found; + } + catch (e) { } + + // Update the warning prompt if needed + this.prepareWarning(true); + + return undefined; + }, + + /** + * Sanitizer.prototype.sanitize() requires the prefs to be up-to-date. + * Because the type of this prefwindow is "child" -- and that's needed because + * without it the dialog has no OK and Cancel buttons -- the prefs are not + * updated on dialogaccept on platforms that don't support instant-apply + * (i.e., Windows). We must therefore manually set the prefs from their + * corresponding preference elements. + */ + updatePrefs : function () + { + var tsPref = document.getElementById("privacy.sanitize.timeSpan"); + Sanitizer.prefs.setIntPref("timeSpan", this.selectedTimespan); + + // Keep the pref for the download history in sync with the history pref. + document.getElementById("privacy.cpd.downloads").value = + document.getElementById("privacy.cpd.history").value; + + // Now manually set the prefs from their corresponding preference + // elements. + var prefs = this.sanitizePreferences.rootBranch; + for (let i = 0; i < this.sanitizePreferences.childNodes.length; ++i) { + var p = this.sanitizePreferences.childNodes[i]; + prefs.setBoolPref(p.name, p.value); + } + }, + + /** + * Check if all of the history items have been selected like the default status. + */ + hasNonSelectedItems: function () { + let checkboxes = document.querySelectorAll("#itemList > [preference]"); + for (let i = 0; i < checkboxes.length; ++i) { + let pref = document.getElementById(checkboxes[i].getAttribute("preference")); + if (!pref.value) + return true; + } + return false; + }, + + /** + * Show the history items list. + */ + showItemList: function () { + var itemList = document.getElementById("itemList"); + var expanderButton = document.getElementById("detailsExpander"); + + if (itemList.collapsed) { + expanderButton.className = "expander-up"; + itemList.setAttribute("collapsed", "false"); + if (document.documentElement.boxObject.height) + window.resizeBy(0, itemList.boxObject.height); + } + }, + + /** + * Hide the history items list. + */ + hideItemList: function () { + var itemList = document.getElementById("itemList"); + var expanderButton = document.getElementById("detailsExpander"); + + if (!itemList.collapsed) { + expanderButton.className = "expander-down"; + window.resizeBy(0, -itemList.boxObject.height); + itemList.setAttribute("collapsed", "true"); + } + }, + + /** + * Called by the item list expander button to toggle the list's visibility. + */ + toggleItemList: function () + { + var itemList = document.getElementById("itemList"); + + if (itemList.collapsed) + this.showItemList(); + else + this.hideItemList(); + }, + +#ifdef CRH_DIALOG_TREE_VIEW + // A duration value; used in the same context as Sanitizer.TIMESPAN_HOUR, + // Sanitizer.TIMESPAN_2HOURS, et al. This should match the value attribute + // of the sanitizeDurationCustom menuitem. + get TIMESPAN_CUSTOM() + { + return -1; + }, + + get placesTree() + { + if (!this._placesTree) + this._placesTree = document.getElementById("placesTree"); + return this._placesTree; + }, + + init: function () + { + // This is used by selectByTimespan() to determine if the window has loaded. + this._inited = true; + + var s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + + document.documentElement.getButton("accept").label = + this.bundleBrowser.getString("sanitizeButtonOK"); + + this.selectByTimespan(); + }, + + /** + * Sets up the hashes this.durationValsToRows, which maps duration values + * to rows in the tree, this.durationRowsToVals, which maps rows in + * the tree to duration values, and this.durationStartTimes, which maps + * duration values to their corresponding start times. + */ + initDurationDropdown: function () + { + // First, calculate the start times for each duration. + this.durationStartTimes = {}; + var durVals = []; + var durPopup = document.getElementById("sanitizeDurationPopup"); + var durMenuitems = durPopup.childNodes; + for (let i = 0; i < durMenuitems.length; i++) { + let durMenuitem = durMenuitems[i]; + let durVal = parseInt(durMenuitem.value); + if (durMenuitem.localName === "menuitem" && + durVal !== Sanitizer.TIMESPAN_EVERYTHING && + durVal !== this.TIMESPAN_CUSTOM) { + durVals.push(durVal); + let durTimes = Sanitizer.getClearRange(durVal); + this.durationStartTimes[durVal] = durTimes[0]; + } + } + + // Sort the duration values ascending. Because one tree index can map to + // more than one duration, this ensures that this.durationRowsToVals maps + // a row index to the largest duration possible in the code below. + durVals.sort(); + + // Now calculate the rows in the tree of the durations' start times. For + // each duration, we are looking for the node in the tree whose time is the + // smallest time greater than or equal to the duration's start time. + this.durationRowsToVals = {}; + this.durationValsToRows = {}; + var view = this.placesTree.view; + // For all rows in the tree except the grippy row... + for (let i = 0; i < view.rowCount - 1; i++) { + let unfoundDurVals = []; + let nodeTime = view.QueryInterface(Ci.nsINavHistoryResultTreeViewer). + nodeForTreeIndex(i).time; + // For all durations whose rows have not yet been found in the tree, see + // if index i is their index. An index may map to more than one duration, + // in which case the final duration (the largest) wins. + for (let j = 0; j < durVals.length; j++) { + let durVal = durVals[j]; + let durStartTime = this.durationStartTimes[durVal]; + if (nodeTime < durStartTime) { + this.durationValsToRows[durVal] = i - 1; + this.durationRowsToVals[i - 1] = durVal; + } + else + unfoundDurVals.push(durVal); + } + durVals = unfoundDurVals; + } + + // If any durations were not found above, then every node in the tree has a + // time greater than or equal to the duration. In other words, those + // durations include the entire tree (except the grippy row). + for (let i = 0; i < durVals.length; i++) { + let durVal = durVals[i]; + this.durationValsToRows[durVal] = view.rowCount - 2; + this.durationRowsToVals[view.rowCount - 2] = durVal; + } + }, + + /** + * If the Places tree is not set up, sets it up. Otherwise does nothing. + */ + ensurePlacesTreeIsInited: function () + { + if (this._placesTreeIsInited) + return; + + this._placesTreeIsInited = true; + + // Either "Last Four Hours" or "Today" will have the most history. If + // it's been more than 4 hours since today began, "Today" will. Otherwise + // "Last Four Hours" will. + var times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_TODAY); + + // If it's been less than 4 hours since today began, use the past 4 hours. + if (times[1] - times[0] < 14400000000) { // 4*60*60*1000000 + times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_4HOURS); + } + + var histServ = Cc["@mozilla.org/browser/nav-history-service;1"]. + getService(Ci.nsINavHistoryService); + var query = histServ.getNewQuery(); + query.beginTimeReference = query.TIME_RELATIVE_EPOCH; + query.beginTime = times[0]; + query.endTimeReference = query.TIME_RELATIVE_EPOCH; + query.endTime = times[1]; + var opts = histServ.getNewQueryOptions(); + opts.sortingMode = opts.SORT_BY_DATE_DESCENDING; + opts.queryType = opts.QUERY_TYPE_HISTORY; + var result = histServ.executeQuery(query, opts); + + var view = gContiguousSelectionTreeHelper.setTree(this.placesTree, + new PlacesTreeView()); + result.addObserver(view, false); + this.initDurationDropdown(); + }, + + /** + * Called on select of the duration dropdown and when grippyMoved() sets a + * duration based on the location of the grippy row. Selects all the nodes in + * the tree that are contained in the selected duration. If clearing + * everything, the warning panel is shown instead. + */ + selectByTimespan: function () + { + // This method is the onselect handler for the duration dropdown. As a + // result it's called a couple of times before onload calls init(). + if (!this._inited) + return; + + var durDeck = document.getElementById("durationDeck"); + var durList = document.getElementById("sanitizeDurationChoice"); + var durVal = parseInt(durList.value); + var durCustom = document.getElementById("sanitizeDurationCustom"); + + // If grippy row is not at a duration boundary, show the custom menuitem; + // otherwise, hide it. Since the user cannot specify a custom duration by + // using the dropdown, this conditional is true only when this method is + // called onselect from grippyMoved(), so no selection need be made. + if (durVal === this.TIMESPAN_CUSTOM) { + durCustom.hidden = false; + return; + } + durCustom.hidden = true; + + // If clearing everything, show the warning and change the dialog's title. + if (durVal === Sanitizer.TIMESPAN_EVERYTHING) { + this.prepareWarning(); + durDeck.selectedIndex = 1; + window.document.title = + this.bundleBrowser.getString("sanitizeDialog2.everything.title"); + document.documentElement.getButton("accept").disabled = false; + return; + } + + // Otherwise -- if clearing a specific time range -- select that time range + // in the tree. + this.ensurePlacesTreeIsInited(); + durDeck.selectedIndex = 0; + window.document.title = + window.document.documentElement.getAttribute("noneverythingtitle"); + var durRow = this.durationValsToRows[durVal]; + gContiguousSelectionTreeHelper.rangedSelect(durRow); + gContiguousSelectionTreeHelper.scrollToGrippy(); + + // If duration is empty (there are no selected rows), disable the dialog's + // OK button. + document.documentElement.getButton("accept").disabled = durRow < 0; + }, + + sanitize: function () + { + // Update pref values before handing off to the sanitizer (bug 453440) + this.updatePrefs(); + var s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + + var durList = document.getElementById("sanitizeDurationChoice"); + var durValue = parseInt(durList.value); + s.ignoreTimespan = durValue === Sanitizer.TIMESPAN_EVERYTHING; + + // Set the sanitizer's time range if we're not clearing everything. + if (!s.ignoreTimespan) { + // If user selected a custom timespan, use that. + if (durValue === this.TIMESPAN_CUSTOM) { + var view = this.placesTree.view; + var now = Date.now() * 1000; + // We disable the dialog's OK button if there's no selection, but we'll + // handle that case just in... case. + if (view.selection.getRangeCount() === 0) + s.range = [now, now]; + else { + var startIndexRef = {}; + // Tree sorted by visit date DEscending, so start time time comes last. + view.selection.getRangeAt(0, {}, startIndexRef); + view.QueryInterface(Ci.nsINavHistoryResultTreeViewer); + var startNode = view.nodeForTreeIndex(startIndexRef.value); + s.range = [startNode.time, now]; + } + } + // Otherwise use the predetermined range. + else + s.range = [this.durationStartTimes[durValue], Date.now() * 1000]; + } + + try { + s.sanitize(); // We ignore the resulting Promise + } catch (er) { + Components.utils.reportError("Exception during sanitize: " + er); + } + return true; + }, + + /** + * In order to mark the custom Places tree view and its nsINavHistoryResult + * for garbage collection, we need to break the reference cycle between the + * two. + */ + unload: function () + { + let result = this.placesTree.getResult(); + result.removeObserver(this.placesTree.view); + this.placesTree.view = null; + }, + + /** + * Called when the user moves the grippy by dragging it, clicking in the tree, + * or on keypress. Updates the duration dropdown so that it displays the + * appropriate specific or custom duration. + * + * @param aEventName + * The name of the event whose handler called this method, e.g., + * "ondragstart", "onkeypress", etc. + * @param aEvent + * The event captured in the event handler. + */ + grippyMoved: function (aEventName, aEvent) + { + gContiguousSelectionTreeHelper[aEventName](aEvent); + var lastSelRow = gContiguousSelectionTreeHelper.getGrippyRow() - 1; + var durList = document.getElementById("sanitizeDurationChoice"); + var durValue = parseInt(durList.value); + + // Multiple durations can map to the same row. Don't update the dropdown + // if the current duration is valid for lastSelRow. + if ((durValue !== this.TIMESPAN_CUSTOM || + lastSelRow in this.durationRowsToVals) && + (durValue === this.TIMESPAN_CUSTOM || + this.durationValsToRows[durValue] !== lastSelRow)) { + // Setting durList.value causes its onselect handler to fire, which calls + // selectByTimespan(). + if (lastSelRow in this.durationRowsToVals) + durList.value = this.durationRowsToVals[lastSelRow]; + else + durList.value = this.TIMESPAN_CUSTOM; + } + + // If there are no selected rows, disable the dialog's OK button. + document.documentElement.getButton("accept").disabled = lastSelRow < 0; + } +#endif + +}; + + +#ifdef CRH_DIALOG_TREE_VIEW +/** + * A helper for handling contiguous selection in the tree. + */ +var gContiguousSelectionTreeHelper = { + + /** + * Gets the tree associated with this helper. + */ + get tree() + { + return this._tree; + }, + + /** + * Sets the tree that this module handles. The tree is assigned a new view + * that is equipped to handle contiguous selection. You can pass in an + * object that will be used as the prototype of the new view. Otherwise + * the tree's current view is used as the prototype. + * + * @param aTreeElement + * The tree element + * @param aProtoTreeView + * If defined, this will be used as the prototype of the tree's new + * view + * @return The new view + */ + setTree: function CSTH_setTree(aTreeElement, aProtoTreeView) + { + this._tree = aTreeElement; + var newView = this._makeTreeView(aProtoTreeView || aTreeElement.view); + aTreeElement.view = newView; + return newView; + }, + + /** + * The index of the row that the grippy occupies. Note that the index of the + * last selected row is getGrippyRow() - 1. If getGrippyRow() is 0, then + * no selection exists. + * + * @return The row index of the grippy + */ + getGrippyRow: function CSTH_getGrippyRow() + { + var sel = this.tree.view.selection; + var rangeCount = sel.getRangeCount(); + if (rangeCount === 0) + return 0; + if (rangeCount !== 1) { + throw "contiguous selection tree helper: getGrippyRow called with " + + "multiple selection ranges"; + } + var max = {}; + sel.getRangeAt(0, {}, max); + return max.value + 1; + }, + + /** + * Helper function for the dragover event. Your dragover listener should + * call this. It updates the selection in the tree under the mouse. + * + * @param aEvent + * The observed dragover event + */ + ondragover: function CSTH_ondragover(aEvent) + { + // Without this when dragging on Windows the mouse cursor is a "no" sign. + // This makes it a drop symbol. + var ds = Cc["@mozilla.org/widget/dragservice;1"]. + getService(Ci.nsIDragService). + getCurrentSession(); + ds.canDrop = true; + ds.dragAction = 0; + + var tbo = this.tree.treeBoxObject; + aEvent.QueryInterface(Ci.nsIDOMMouseEvent); + var hoverRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY); + + if (hoverRow < 0) + return; + + this.rangedSelect(hoverRow - 1); + }, + + /** + * Helper function for the dragstart event. Your dragstart listener should + * call this. It starts a drag session. + * + * @param aEvent + * The observed dragstart event + */ + ondragstart: function CSTH_ondragstart(aEvent) + { + var tbo = this.tree.treeBoxObject; + var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY); + + if (clickedRow !== this.getGrippyRow()) + return; + + // This part is a hack. What we really want is a grab and slide, not + // drag and drop. Start a move drag session with dummy data and a + // dummy region. Set the region's coordinates to (Infinity, Infinity) + // so it's drawn offscreen and its size to (1, 1). + var arr = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + var trans = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + trans.init(null); + trans.setTransferData('dummy-flavor', null, 0); + arr.appendElement(trans, /* weak = */ false); + var reg = Cc["@mozilla.org/gfx/region;1"]. + createInstance(Ci.nsIScriptableRegion); + reg.setToRect(Infinity, Infinity, 1, 1); + var ds = Cc["@mozilla.org/widget/dragservice;1"]. + getService(Ci.nsIDragService); + ds.invokeDragSession(aEvent.target, arr, reg, ds.DRAGDROP_ACTION_MOVE); + }, + + /** + * Helper function for the keypress event. Your keypress listener should + * call this. Users can use Up, Down, Page Up/Down, Home, and End to move + * the bottom of the selection window. + * + * @param aEvent + * The observed keypress event + */ + onkeypress: function CSTH_onkeypress(aEvent) + { + var grippyRow = this.getGrippyRow(); + var tbo = this.tree.treeBoxObject; + var rangeEnd; + switch (aEvent.keyCode) { + case aEvent.DOM_VK_HOME: + rangeEnd = 0; + break; + case aEvent.DOM_VK_PAGE_UP: + rangeEnd = grippyRow - tbo.getPageLength(); + break; + case aEvent.DOM_VK_UP: + rangeEnd = grippyRow - 2; + break; + case aEvent.DOM_VK_DOWN: + rangeEnd = grippyRow; + break; + case aEvent.DOM_VK_PAGE_DOWN: + rangeEnd = grippyRow + tbo.getPageLength(); + break; + case aEvent.DOM_VK_END: + rangeEnd = this.tree.view.rowCount - 2; + break; + default: + return; + break; + } + + aEvent.stopPropagation(); + + // First, clip rangeEnd. this.rangedSelect() doesn't clip the range if we + // select past the ends of the tree. + if (rangeEnd < 0) + rangeEnd = -1; + else if (this.tree.view.rowCount - 2 < rangeEnd) + rangeEnd = this.tree.view.rowCount - 2; + + // Next, (de)select. + this.rangedSelect(rangeEnd); + + // Finally, scroll the tree. We always want one row above and below the + // grippy row to be visible if possible. + if (rangeEnd < grippyRow) // moved up + tbo.ensureRowIsVisible(rangeEnd < 0 ? 0 : rangeEnd); + else { // moved down + if (rangeEnd + 2 < this.tree.view.rowCount) + tbo.ensureRowIsVisible(rangeEnd + 2); + else if (rangeEnd + 1 < this.tree.view.rowCount) + tbo.ensureRowIsVisible(rangeEnd + 1); + } + }, + + /** + * Helper function for the mousedown event. Your mousedown listener should + * call this. Users can click on individual rows to make the selection + * jump to them immediately. + * + * @param aEvent + * The observed mousedown event + */ + onmousedown: function CSTH_onmousedown(aEvent) + { + var tbo = this.tree.treeBoxObject; + var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY); + + if (clickedRow < 0 || clickedRow >= this.tree.view.rowCount) + return; + + if (clickedRow < this.getGrippyRow()) + this.rangedSelect(clickedRow); + else if (clickedRow > this.getGrippyRow()) + this.rangedSelect(clickedRow - 1); + }, + + /** + * Selects range [0, aEndRow] in the tree. The grippy row will then be at + * index aEndRow + 1. aEndRow may be -1, in which case the selection is + * cleared and the grippy row will be at index 0. + * + * @param aEndRow + * The range [0, aEndRow] will be selected. + */ + rangedSelect: function CSTH_rangedSelect(aEndRow) + { + var tbo = this.tree.treeBoxObject; + if (aEndRow < 0) + this.tree.view.selection.clearSelection(); + else + this.tree.view.selection.rangedSelect(0, aEndRow, false); + tbo.invalidateRange(tbo.getFirstVisibleRow(), tbo.getLastVisibleRow()); + }, + + /** + * Scrolls the tree so that the grippy row is in the center of the view. + */ + scrollToGrippy: function CSTH_scrollToGrippy() + { + var rowCount = this.tree.view.rowCount; + var tbo = this.tree.treeBoxObject; + var pageLen = tbo.getPageLength() || + parseInt(this.tree.getAttribute("rows")) || + 10; + + // All rows fit on a single page. + if (rowCount <= pageLen) + return; + + var scrollToRow = this.getGrippyRow() - Math.ceil(pageLen / 2.0); + + // Grippy row is in first half of first page. + if (scrollToRow < 0) + scrollToRow = 0; + + // Grippy row is in last half of last page. + else if (rowCount < scrollToRow + pageLen) + scrollToRow = rowCount - pageLen; + + tbo.scrollToRow(scrollToRow); + }, + + /** + * Creates a new tree view suitable for contiguous selection. If + * aProtoTreeView is specified, it's used as the new view's prototype. + * Otherwise the tree's current view is used as the prototype. + * + * @param aProtoTreeView + * Used as the new view's prototype if specified + */ + _makeTreeView: function CSTH__makeTreeView(aProtoTreeView) + { + var view = aProtoTreeView; + var that = this; + + //XXXadw: When Alex gets the grippy icon done, this may or may not change, + // depending on how we style it. + view.isSeparator = function CSTH_View_isSeparator(aRow) + { + return aRow === that.getGrippyRow(); + }; + + // rowCount includes the grippy row. + view.__defineGetter__("_rowCount", view.__lookupGetter__("rowCount")); + view.__defineGetter__("rowCount", + function CSTH_View_rowCount() + { + return this._rowCount + 1; + }); + + // This has to do with visual feedback in the view itself, e.g., drawing + // a small line underneath the dropzone. Not what we want. + view.canDrop = function CSTH_View_canDrop() { return false; }; + + // No clicking headers to sort the tree or sort feedback on columns. + view.cycleHeader = function CSTH_View_cycleHeader() {}; + view.sortingChanged = function CSTH_View_sortingChanged() {}; + + // Override a bunch of methods to account for the grippy row. + + view._getCellProperties = view.getCellProperties; + view.getCellProperties = + function CSTH_View_getCellProperties(aRow, aCol) + { + var grippyRow = that.getGrippyRow(); + if (aRow === grippyRow) + return "grippyRow"; + if (aRow < grippyRow) + return this._getCellProperties(aRow, aCol); + + return this._getCellProperties(aRow - 1, aCol); + }; + + view._getRowProperties = view.getRowProperties; + view.getRowProperties = + function CSTH_View_getRowProperties(aRow) + { + var grippyRow = that.getGrippyRow(); + if (aRow === grippyRow) + return "grippyRow"; + + if (aRow < grippyRow) + return this._getRowProperties(aRow); + + return this._getRowProperties(aRow - 1); + }; + + view._getCellText = view.getCellText; + view.getCellText = + function CSTH_View_getCellText(aRow, aCol) + { + var grippyRow = that.getGrippyRow(); + if (aRow === grippyRow) + return ""; + aRow = aRow < grippyRow ? aRow : aRow - 1; + return this._getCellText(aRow, aCol); + }; + + view._getImageSrc = view.getImageSrc; + view.getImageSrc = + function CSTH_View_getImageSrc(aRow, aCol) + { + var grippyRow = that.getGrippyRow(); + if (aRow === grippyRow) + return ""; + aRow = aRow < grippyRow ? aRow : aRow - 1; + return this._getImageSrc(aRow, aCol); + }; + + view.isContainer = function CSTH_View_isContainer(aRow) { return false; }; + view.getParentIndex = function CSTH_View_getParentIndex(aRow) { return -1; }; + view.getLevel = function CSTH_View_getLevel(aRow) { return 0; }; + view.hasNextSibling = function CSTH_View_hasNextSibling(aRow, aAfterIndex) + { + return aRow < this.rowCount - 1; + }; + + return view; + } +}; +#endif diff --git a/application/basilisk/base/content/softwareUpdateOverlay.xul b/application/basilisk/base/content/softwareUpdateOverlay.xul new file mode 100644 index 000000000..01170e46c --- /dev/null +++ b/application/basilisk/base/content/softwareUpdateOverlay.xul @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xul-overlay href="chrome://browser/content/macBrowserOverlay.xul"?> + +<overlay id="softwareUpdateOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<window id="updates"> + +#include browserMountPoints.inc + +</window> + +</overlay> diff --git a/application/basilisk/base/content/sync/aboutSyncTabs-bindings.xml b/application/basilisk/base/content/sync/aboutSyncTabs-bindings.xml new file mode 100644 index 000000000..e6108209a --- /dev/null +++ b/application/basilisk/base/content/sync/aboutSyncTabs-bindings.xml @@ -0,0 +1,46 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<bindings id="tabBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="tab-listing" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:hbox flex="1"> + <xul:vbox pack="start"> + <xul:image class="tabIcon" + xbl:inherits="src=icon"/> + </xul:vbox> + <xul:vbox pack="start" flex="1"> + <xul:label xbl:inherits="value=title,selected" + crop="end" flex="1" class="title"/> + <xul:label xbl:inherits="value=url,selected" + crop="end" flex="1" class="url"/> + </xul:vbox> + </xul:hbox> + </content> + <handlers> + <handler event="dblclick" button="0"> + <![CDATA[ + RemoteTabViewer.openSelected(); + ]]> + </handler> + </handlers> + </binding> + + <binding id="client-listing" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> + <content> + <xul:hbox pack="start" align="center" onfocus="event.target.blur()" onselect="return false;"> + <xul:image/> + <xul:label xbl:inherits="value=clientName" + class="clientName" + crop="center" flex="1"/> + </xul:hbox> + </content> + </binding> +</bindings> diff --git a/application/basilisk/base/content/sync/aboutSyncTabs.css b/application/basilisk/base/content/sync/aboutSyncTabs.css new file mode 100644 index 000000000..5a353175b --- /dev/null +++ b/application/basilisk/base/content/sync/aboutSyncTabs.css @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +richlistitem[type="tab"] { + -moz-binding: url(chrome://browser/content/sync/aboutSyncTabs-bindings.xml#tab-listing); +} + +richlistitem[type="client"] { + -moz-binding: url(chrome://browser/content/sync/aboutSyncTabs-bindings.xml#client-listing); +} diff --git a/application/basilisk/base/content/sync/aboutSyncTabs.js b/application/basilisk/base/content/sync/aboutSyncTabs.js new file mode 100644 index 000000000..f4bb607ea --- /dev/null +++ b/application/basilisk/base/content/sync/aboutSyncTabs.js @@ -0,0 +1,364 @@ +/* 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 Cu = Components.utils; + +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://services-sync/main.js"); +Cu.import("resource:///modules/PlacesUIUtils.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm", this); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + +#ifdef MOZ_SERVICES_CLOUDSYNC +XPCOMUtils.defineLazyModuleGetter(this, "CloudSync", + "resource://gre/modules/CloudSync.jsm"); +#endif + +var RemoteTabViewer = { + _tabsList: null, + + init: function () { + Services.obs.addObserver(this, "weave:service:login:finish", false); + Services.obs.addObserver(this, "weave:engine:sync:finish", false); + + Services.obs.addObserver(this, "cloudsync:tabs:update", false); + + this._tabsList = document.getElementById("tabsList"); + + this.buildList(true); + }, + + uninit: function () { + Services.obs.removeObserver(this, "weave:service:login:finish"); + Services.obs.removeObserver(this, "weave:engine:sync:finish"); + + Services.obs.removeObserver(this, "cloudsync:tabs:update"); + }, + + createItem: function (attrs) { + let item = document.createElement("richlistitem"); + + // Copy the attributes from the argument into the item. + for (let attr in attrs) { + item.setAttribute(attr, attrs[attr]); + } + + if (attrs["type"] == "tab") { + item.label = attrs.title != "" ? attrs.title : attrs.url; + } + + return item; + }, + + filterTabs: function (event) { + let val = event.target.value.toLowerCase(); + let numTabs = this._tabsList.getRowCount(); + let clientTabs = 0; + let currentClient = null; + + for (let i = 0; i < numTabs; i++) { + let item = this._tabsList.getItemAtIndex(i); + let hide = false; + if (item.getAttribute("type") == "tab") { + if (!item.getAttribute("url").toLowerCase().includes(val) && + !item.getAttribute("title").toLowerCase().includes(val)) { + hide = true; + } else { + clientTabs++; + } + } + else if (item.getAttribute("type") == "client") { + if (currentClient) { + if (clientTabs == 0) { + currentClient.hidden = true; + } + } + currentClient = item; + clientTabs = 0; + } + item.hidden = hide; + } + if (clientTabs == 0) { + currentClient.hidden = true; + } + }, + + openSelected: function () { + let items = this._tabsList.selectedItems; + let urls = []; + for (let i = 0; i < items.length; i++) { + if (items[i].getAttribute("type") == "tab") { + urls.push(items[i].getAttribute("url")); + let index = this._tabsList.getIndexOfItem(items[i]); + this._tabsList.removeItemAt(index); + } + } + if (urls.length) { + getTopWin().gBrowser.loadTabs(urls); + this._tabsList.clearSelection(); + } + }, + + bookmarkSingleTab: function () { + let item = this._tabsList.selectedItems[0]; + let uri = Weave.Utils.makeURI(item.getAttribute("url")); + let title = item.getAttribute("title"); + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: uri + , title: title + , hiddenRows: [ "description" + , "location" + , "loadInSidebar" + , "keyword" ] + }, window.top); + }, + + bookmarkSelectedTabs: function () { + let items = this._tabsList.selectedItems; + let URIs = []; + for (let i = 0; i < items.length; i++) { + if (items[i].getAttribute("type") == "tab") { + let uri = Weave.Utils.makeURI(items[i].getAttribute("url")); + if (!uri) { + continue; + } + + URIs.push(uri); + } + } + if (URIs.length) { + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "folder" + , URIList: URIs + , hiddenRows: [ "description" ] + }, window.top); + } + }, + + getIcon: function (iconUri, defaultIcon) { + try { + let iconURI = Weave.Utils.makeURI(iconUri); + return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec; + } catch (ex) { + // Do nothing. + } + + // Just give the provided default icon or the system's default. + return defaultIcon || PlacesUtils.favicons.defaultFavicon.spec; + }, + + _waitingForBuildList: false, + + _buildListRequested: false, + + buildList: function (forceSync) { + if (this._waitingForBuildList) { + this._buildListRequested = true; + return; + } + + this._waitingForBuildList = true; + this._buildListRequested = false; + + this._clearTabList(); + + if (Weave.Service.isLoggedIn) { + this._refetchTabs(forceSync); + this._generateWeaveTabList(); + } else { + // XXXzpao We should say something about not being logged in & not having data + // or tell the appropriate condition. (bug 583344) + } + + let complete = () => { + this._waitingForBuildList = false; + if (this._buildListRequested) { + CommonUtils.nextTick(this.buildList, this); + } + } + +#ifdef MOZ_SERVICES_CLOUDSYNC + if (CloudSync && CloudSync.ready && CloudSync().tabsReady && CloudSync().tabs.hasRemoteTabs()) { + this._generateCloudSyncTabList() + .then(complete, complete); + } else { + complete(); + } +#else + complete(); +#endif + }, + + _clearTabList: function () { + let list = this._tabsList; + + // Clear out existing richlistitems. + let count = list.getRowCount(); + if (count > 0) { + for (let i = count - 1; i >= 0; i--) { + list.removeItemAt(i); + } + } + }, + + _generateWeaveTabList: function () { + let engine = Weave.Service.engineManager.get("tabs"); + let list = this._tabsList; + + let seenURLs = new Set(); + let localURLs = engine.getOpenURLs(); + + for (let [, client] of Object.entries(engine.getAllClients())) { + // Create the client node, but don't add it in-case we don't show any tabs + let appendClient = true; + + client.tabs.forEach(function({title, urlHistory, icon}) { + let url = urlHistory[0]; + if (!url || localURLs.has(url) || seenURLs.has(url)) { + return; + } + seenURLs.add(url); + + if (appendClient) { + let attrs = { + type: "client", + clientName: client.clientName, + class: Weave.Service.clientsEngine.isMobile(client.id) ? "mobile" : "desktop" + }; + let clientEnt = this.createItem(attrs); + list.appendChild(clientEnt); + appendClient = false; + clientEnt.disabled = true; + } + let attrs = { + type: "tab", + title: title || url, + url: url, + icon: this.getIcon(icon), + } + let tab = this.createItem(attrs); + list.appendChild(tab); + }, this); + } + }, + + _generateCloudSyncTabList: function () { + let updateTabList = function (remoteTabs) { + let list = this._tabsList; + + for (let client of remoteTabs) { + let clientAttrs = { + type: "client", + clientName: client.name, + }; + + let clientEnt = this.createItem(clientAttrs); + list.appendChild(clientEnt); + + for (let tab of client.tabs) { + let tabAttrs = { + type: "tab", + title: tab.title, + url: tab.url, + icon: this.getIcon(tab.icon), + }; + let tabEnt = this.createItem(tabAttrs); + list.appendChild(tabEnt); + } + } + }.bind(this); + + return CloudSync().tabs.getRemoteTabs() + .then(updateTabList, Promise.reject.bind(Promise)); + }, + + adjustContextMenu: function (event) { + let mode = "all"; + switch (this._tabsList.selectedItems.length) { + case 0: + break; + case 1: + mode = "single" + break; + default: + mode = "multiple"; + break; + } + + let menu = document.getElementById("tabListContext"); + let el = menu.firstChild; + while (el) { + let showFor = el.getAttribute("showFor"); + if (showFor) { + el.hidden = showFor != mode && showFor != "all"; + } + + el = el.nextSibling; + } + }, + + _refetchTabs: function (force) { + if (!force) { + // Don't bother refetching tabs if we already did so recently + let lastFetch = 0; + try { + lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch"); + } + catch (e) { + /* Just use the default value of 0 */ + } + + let now = Math.floor(Date.now() / 1000); + if (now - lastFetch < 30) { + return false; + } + } + + // Ask Sync to just do the tabs engine if it can. + Weave.Service.sync(["tabs"]); + Services.prefs.setIntPref("services.sync.lastTabFetch", + Math.floor(Date.now() / 1000)); + + return true; + }, + + observe: function (subject, topic, data) { + switch (topic) { + case "weave:service:login:finish": + // A login has finished, which means that a Sync is about to start and + // we will eventually get to the "tabs" engine - but try and force the + // tab engine to sync first by passing |true| for the forceSync param. + this.buildList(true); + break; + case "weave:engine:sync:finish": + if (data == "tabs") { + // The tabs engine just finished, so re-build the list without + // forcing a new sync of the tabs engine. + this.buildList(false); + } + break; + case "cloudsync:tabs:update": + this.buildList(false); + break; + } + }, + + handleClick: function (event) { + if (event.target.getAttribute("type") != "tab") { + return; + } + + if (event.button == 1) { + let url = event.target.getAttribute("url"); + openUILink(url, event); + let index = this._tabsList.getIndexOfItem(event.target); + this._tabsList.removeItemAt(index); + } + } +} diff --git a/application/basilisk/base/content/sync/aboutSyncTabs.xul b/application/basilisk/base/content/sync/aboutSyncTabs.xul new file mode 100644 index 000000000..a4aa0032f --- /dev/null +++ b/application/basilisk/base/content/sync/aboutSyncTabs.xul @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/aboutSyncTabs.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/sync/aboutSyncTabs.css" type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % aboutSyncTabsDTD SYSTEM "chrome://browser/locale/aboutSyncTabs.dtd"> + %aboutSyncTabsDTD; +]> + +<window id="tabs-display" + onload="RemoteTabViewer.init()" + onunload="RemoteTabViewer.uninit()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="&tabs.otherDevices.label;"> + <script type="application/javascript;version=1.8" src="chrome://browser/content/sync/aboutSyncTabs.js"/> + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + <html:head> + <html:link rel="icon" href="chrome://browser/skin/sync-16.png"/> + </html:head> + + <popupset id="contextmenus"> + <menupopup id="tabListContext"> + <menuitem label="&tabs.context.openTab.label;" + accesskey="&tabs.context.openTab.accesskey;" + oncommand="RemoteTabViewer.openSelected()" + showFor="single"/> + <menuitem label="&tabs.context.bookmarkSingleTab.label;" + accesskey="&tabs.context.bookmarkSingleTab.accesskey;" + oncommand="RemoteTabViewer.bookmarkSingleTab(event)" + showFor="single"/> + <menuitem label="&tabs.context.openMultipleTabs.label;" + accesskey="&tabs.context.openMultipleTabs.accesskey;" + oncommand="RemoteTabViewer.openSelected()" + showFor="multiple"/> + <menuitem label="&tabs.context.bookmarkMultipleTabs.label;" + accesskey="&tabs.context.bookmarkMultipleTabs.accesskey;" + oncommand="RemoteTabViewer.bookmarkSelectedTabs()" + showFor="multiple"/> + <menuseparator/> + <menuitem label="&tabs.context.refreshList.label;" + accesskey="&tabs.context.refreshList.accesskey;" + oncommand="RemoteTabViewer.buildList()" + showFor="all"/> + </menupopup> + </popupset> + <richlistbox context="tabListContext" id="tabsList" seltype="multiple" + align="center" flex="1" + onclick="RemoteTabViewer.handleClick(event)" + oncontextmenu="RemoteTabViewer.adjustContextMenu(event)"> + <hbox id="headers" align="center"> + <label id="tabsListHeading" + value="&tabs.otherDevices.label;"/> + <spacer flex="1"/> + <textbox type="search" + emptytext="&tabs.searchText.label;" + oncommand="RemoteTabViewer.filterTabs(event)"/> + </hbox> + + </richlistbox> +</window> + diff --git a/application/basilisk/base/content/sync/addDevice.js b/application/basilisk/base/content/sync/addDevice.js new file mode 100644 index 000000000..0390d4397 --- /dev/null +++ b/application/basilisk/base/content/sync/addDevice.js @@ -0,0 +1,157 @@ +/* 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 Cc = Components.classes; +var Cu = Components.utils; + +Cu.import("resource://services-sync/main.js"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const PIN_PART_LENGTH = 4; + +const ADD_DEVICE_PAGE = 0; +const SYNC_KEY_PAGE = 1; +const DEVICE_CONNECTED_PAGE = 2; + +var gSyncAddDevice = { + + init: function init() { + this.pin1.setAttribute("maxlength", PIN_PART_LENGTH); + this.pin2.setAttribute("maxlength", PIN_PART_LENGTH); + this.pin3.setAttribute("maxlength", PIN_PART_LENGTH); + + this.nextFocusEl = {pin1: this.pin2, + pin2: this.pin3, + pin3: this.wizard.getButton("next")}; + + this.throbber = document.getElementById("pairDeviceThrobber"); + this.errorRow = document.getElementById("errorRow"); + + // Kick off a sync. That way the server will have the most recent data from + // this computer and it will show up immediately on the new device. + Weave.Service.scheduler.scheduleNextSync(0); + }, + + onPageShow: function onPageShow() { + this.wizard.getButton("back").hidden = true; + + switch (this.wizard.pageIndex) { + case ADD_DEVICE_PAGE: + this.onTextBoxInput(); + this.wizard.canRewind = false; + this.wizard.getButton("next").hidden = false; + this.pin1.focus(); + break; + case SYNC_KEY_PAGE: + this.wizard.canAdvance = false; + this.wizard.canRewind = true; + this.wizard.getButton("back").hidden = false; + this.wizard.getButton("next").hidden = true; + document.getElementById("weavePassphrase").value = + Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey); + break; + case DEVICE_CONNECTED_PAGE: + this.wizard.canAdvance = true; + this.wizard.canRewind = false; + this.wizard.getButton("cancel").hidden = true; + break; + } + }, + + onWizardAdvance: function onWizardAdvance() { + switch (this.wizard.pageIndex) { + case ADD_DEVICE_PAGE: + this.startTransfer(); + return false; + case DEVICE_CONNECTED_PAGE: + window.close(); + return false; + } + return true; + }, + + startTransfer: function startTransfer() { + this.errorRow.hidden = true; + // When onAbort is called, Weave may already be gone. + const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT; + + let self = this; + let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({ + onPaired: function onPaired() { + let credentials = {account: Weave.Service.identity.account, + password: Weave.Service.identity.basicPassword, + synckey: Weave.Service.identity.syncKey, + serverURL: Weave.Service.serverURL}; + jpakeclient.sendAndComplete(credentials); + }, + onComplete: function onComplete() { + delete self._jpakeclient; + self.wizard.pageIndex = DEVICE_CONNECTED_PAGE; + + // Schedule a Sync for soonish to fetch the data uploaded by the + // device with which we just paired. + Weave.Service.scheduler.scheduleNextSync(Weave.Service.scheduler.activeInterval); + }, + onAbort: function onAbort(error) { + delete self._jpakeclient; + + // Aborted by user, ignore. + if (error == JPAKE_ERROR_USERABORT) { + return; + } + + self.errorRow.hidden = false; + self.throbber.hidden = true; + self.pin1.value = self.pin2.value = self.pin3.value = ""; + self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false; + self.pin1.focus(); + } + }); + this.throbber.hidden = false; + this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true; + this.wizard.canAdvance = false; + + let pin = this.pin1.value + this.pin2.value + this.pin3.value; + let expectDelay = false; + jpakeclient.pairWithPIN(pin, expectDelay); + }, + + onWizardBack: function onWizardBack() { + if (this.wizard.pageIndex != SYNC_KEY_PAGE) + return true; + + this.wizard.pageIndex = ADD_DEVICE_PAGE; + return false; + }, + + onWizardCancel: function onWizardCancel() { + if (this._jpakeclient) { + this._jpakeclient.abort(); + delete this._jpakeclient; + } + return true; + }, + + onTextBoxInput: function onTextBoxInput(textbox) { + if (textbox && textbox.value.length == PIN_PART_LENGTH) + this.nextFocusEl[textbox.id].focus(); + + this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH + && this.pin2.value.length == PIN_PART_LENGTH + && this.pin3.value.length == PIN_PART_LENGTH); + }, + + goToSyncKeyPage: function goToSyncKeyPage() { + this.wizard.pageIndex = SYNC_KEY_PAGE; + } + +}; +// onWizardAdvance() and onPageShow() are run before init() so we'll set +// these up as lazy getters. +["wizard", "pin1", "pin2", "pin3"].forEach(function (id) { + XPCOMUtils.defineLazyGetter(gSyncAddDevice, id, function() { + return document.getElementById(id); + }); +}); diff --git a/application/basilisk/base/content/sync/addDevice.xul b/application/basilisk/base/content/sync/addDevice.xul new file mode 100644 index 000000000..83c3b7b3c --- /dev/null +++ b/application/basilisk/base/content/sync/addDevice.xul @@ -0,0 +1,129 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd"> +%brandDTD; +%syncBrandDTD; +%syncSetupDTD; +]> +<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + id="wizard" + title="&pairDevice.title.label;" + windowtype="Sync:AddDevice" + persist="screenX screenY" + onwizardnext="return gSyncAddDevice.onWizardAdvance();" + onwizardback="return gSyncAddDevice.onWizardBack();" + onwizardcancel="gSyncAddDevice.onWizardCancel();" + onload="gSyncAddDevice.init();"> + + <script type="application/javascript" + src="chrome://browser/content/sync/addDevice.js"/> + <script type="application/javascript" + src="chrome://browser/content/sync/utils.js"/> + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/printUtils.js"/> + + <wizardpage id="addDevicePage" + label="&pairDevice.title.label;" + onpageshow="gSyncAddDevice.onPageShow();"> + <description> + &pairDevice.dialog.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/add-device"/> + </description> + <separator class="groove-thin"/> + <description> + &addDevice.dialog.enterCode.label; + </description> + <separator class="groove-thin"/> + <vbox align="center"> + <textbox id="pin1" + class="pin" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();" + /> + <textbox id="pin2" + class="pin" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();" + /> + <textbox id="pin3" + class="pin" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();" + /> + </vbox> + <separator class="groove-thin"/> + <vbox id="pairDeviceThrobber" align="center" hidden="true"> + <image/> + </vbox> + <hbox id="errorRow" pack="center" hidden="true"> + <image class="statusIcon" status="error"/> + <label class="status" + value="&addDevice.dialog.tryAgain.label;"/> + </hbox> + <spacer flex="3"/> + <label class="text-link" + value="&addDevice.dontHaveDevice.label;" + onclick="gSyncAddDevice.goToSyncKeyPage();"/> + </wizardpage> + + <!-- Need a non-empty label here, otherwise we get a default label on Mac --> + <wizardpage id="syncKeyPage" + label=" " + onpageshow="gSyncAddDevice.onPageShow();"> + <description> + &addDevice.dialog.recoveryKey.label; + </description> + <spacer/> + + <groupbox> + <label value="&recoveryKeyEntry.label;" + accesskey="&recoveryKeyEntry.accesskey;" + control="weavePassphrase"/> + <textbox id="weavePassphrase" + readonly="true"/> + </groupbox> + + <groupbox align="center"> + <description>&recoveryKeyBackup.description;</description> + <hbox> + <button id="printSyncKeyButton" + label="&button.syncKeyBackup.print.label;" + accesskey="&button.syncKeyBackup.print.accesskey;" + oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/> + <button id="saveSyncKeyButton" + label="&button.syncKeyBackup.save.label;" + accesskey="&button.syncKeyBackup.save.accesskey;" + oncommand="gSyncUtils.passphraseSave('weavePassphrase');"/> + </hbox> + </groupbox> + </wizardpage> + + <wizardpage id="deviceConnectedPage" + label="&addDevice.dialog.connected.label;" + onpageshow="gSyncAddDevice.onPageShow();"> + <vbox align="center"> + <image id="successPageIcon"/> + </vbox> + <separator/> + <description class="normal"> + &addDevice.dialog.successful.label; + </description> + </wizardpage> + +</wizard> diff --git a/application/basilisk/base/content/sync/customize.css b/application/basilisk/base/content/sync/customize.css new file mode 100644 index 000000000..2bb62595d --- /dev/null +++ b/application/basilisk/base/content/sync/customize.css @@ -0,0 +1,28 @@ +/* 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/. */ + +:root { + font-size: 80%; +} + +#sync-customize-pane { + padding-inline-start: 74px; + background: top left url(chrome://browser/skin/sync-128.png) no-repeat; + background-size: 64px; +} + +#sync-customize-title { + margin-inline-start: 0; + padding-bottom: 0.5em; + font-weight: bold; +} + +#sync-customize-subtitle { + font-size: 90%; +} + +checkbox { + margin: 0; + padding: 0.5em 0 0; +} diff --git a/application/basilisk/base/content/sync/customize.js b/application/basilisk/base/content/sync/customize.js new file mode 100644 index 000000000..f431ac58c --- /dev/null +++ b/application/basilisk/base/content/sync/customize.js @@ -0,0 +1,25 @@ +/* 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/. */ + +"use strict"; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +addEventListener("dialogaccept", function () { + let pane = document.getElementById("sync-customize-pane"); + // First determine what the preference for the "global" sync enabled pref + // should be based on the engines selected. + let prefElts = pane.querySelectorAll("preferences > preference"); + let syncEnabled = false; + for (let elt of prefElts) { + if (elt.name.startsWith("services.sync.") && elt.value) { + syncEnabled = true; + break; + } + } + Services.prefs.setBoolPref("services.sync.enabled", syncEnabled); + // and write the individual prefs. + pane.writePreferences(true); + window.arguments[0].accepted = true; +}); diff --git a/application/basilisk/base/content/sync/customize.xul b/application/basilisk/base/content/sync/customize.xul new file mode 100644 index 000000000..827edf565 --- /dev/null +++ b/application/basilisk/base/content/sync/customize.xul @@ -0,0 +1,62 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/sync/customize.css" type="text/css"?> + +<!DOCTYPE dialog [ +<!ENTITY % syncCustomizeDTD SYSTEM "chrome://browser/locale/syncCustomize.dtd"> +%syncCustomizeDTD; +]> +<dialog id="sync-customize" + windowtype="Sync:Customize" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="&syncCustomize.dialog.title;" + buttonlabelaccept="&syncCustomize.acceptButton.label;" + buttons="accept"> + + <prefpane id="sync-customize-pane"> + <preferences> + <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/> + <preference id="engine.history" name="services.sync.engine.history" type="bool"/> + <preference id="engine.tabs" name="services.sync.engine.tabs" type="bool"/> + <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/> + <preference id="engine.addons" name="services.sync.engine.addons" type="bool"/> + <preference id="engine.prefs" name="services.sync.engine.prefs" type="bool"/> + </preferences> + + <label id="sync-customize-title" value="&syncCustomize.title;"/> + <description id="sync-customize-subtitle" + value="&syncCustomize.description;"/> + + <vbox align="start"> + <checkbox label="&engine.tabs.label;" + accesskey="&engine.tabs.accesskey;" + preference="engine.tabs"/> + <checkbox label="&engine.bookmarks.label;" + accesskey="&engine.bookmarks.accesskey;" + preference="engine.bookmarks"/> + <checkbox label="&engine.passwords.label;" + accesskey="&engine.passwords.accesskey;" + preference="engine.passwords"/> + <checkbox label="&engine.history.label;" + accesskey="&engine.history.accesskey;" + preference="engine.history"/> + <checkbox label="&engine.addons.label;" + accesskey="&engine.addons.accesskey;" + preference="engine.addons"/> + <checkbox label="&engine.prefs.label;" + accesskey="&engine.prefs.accesskey;" + preference="engine.prefs"/> + </vbox> + + </prefpane> + + <script type="application/javascript" + src="chrome://browser/content/sync/customize.js" /> + +</dialog> diff --git a/application/basilisk/base/content/sync/genericChange.js b/application/basilisk/base/content/sync/genericChange.js new file mode 100644 index 000000000..51a74f1b1 --- /dev/null +++ b/application/basilisk/base/content/sync/genericChange.js @@ -0,0 +1,233 @@ +/* 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 Cc = Components.classes; + +Components.utils.import("resource://services-sync/main.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +var Change = { + _dialog: null, + _dialogType: null, + _status: null, + _statusIcon: null, + _firstBox: null, + _secondBox: null, + + get _passphraseBox() { + delete this._passphraseBox; + return this._passphraseBox = document.getElementById("passphraseBox"); + }, + + get _currentPasswordInvalid() { + return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; + }, + + get _updatingPassphrase() { + return this._dialogType == "UpdatePassphrase"; + }, + + onLoad: function Change_onLoad() { + /* Load labels */ + let introText = document.getElementById("introText"); + let warningText = document.getElementById("warningText"); + + // load some other elements & info from the window + this._dialog = document.getElementById("change-dialog"); + this._dialogType = window.arguments[0]; + this._duringSetup = window.arguments[1]; + this._status = document.getElementById("status"); + this._statusIcon = document.getElementById("statusIcon"); + this._statusRow = document.getElementById("statusRow"); + this._firstBox = document.getElementById("textBox1"); + this._secondBox = document.getElementById("textBox2"); + + this._dialog.getButton("finish").disabled = true; + this._dialog.getButton("back").hidden = true; + + this._stringBundle = + Services.strings.createBundle("chrome://browser/locale/syncGenericChange.properties"); + + switch (this._dialogType) { + case "UpdatePassphrase": + case "ResetPassphrase": + document.getElementById("textBox1Row").hidden = true; + document.getElementById("textBox2Row").hidden = true; + document.getElementById("passphraseLabel").value + = this._str("new.recoverykey.label"); + document.getElementById("passphraseSpacer").hidden = false; + + if (this._updatingPassphrase) { + document.getElementById("passphraseHelpBox").hidden = false; + document.title = this._str("new.recoverykey.title"); + introText.textContent = this._str("new.recoverykey.introText"); + this._dialog.getButton("finish").label + = this._str("new.recoverykey.acceptButton"); + } + else { + document.getElementById("generatePassphraseButton").hidden = false; + document.getElementById("passphraseBackupButtons").hidden = false; + this._passphraseBox.setAttribute("readonly", "true"); + let pp = Weave.Service.identity.syncKey; + if (Weave.Utils.isPassphrase(pp)) + pp = Weave.Utils.hyphenatePassphrase(pp); + this._passphraseBox.value = pp; + this._passphraseBox.focus(); + document.title = this._str("change.recoverykey.title"); + introText.textContent = this._str("change.synckey.introText2"); + warningText.textContent = this._str("change.recoverykey.warningText"); + this._dialog.getButton("finish").label + = this._str("change.recoverykey.acceptButton"); + if (this._duringSetup) { + this._dialog.getButton("finish").disabled = false; + } + } + break; + case "ChangePassword": + document.getElementById("passphraseRow").hidden = true; + let box1label = document.getElementById("textBox1Label"); + let box2label = document.getElementById("textBox2Label"); + box1label.value = this._str("new.password.label"); + + if (this._currentPasswordInvalid) { + document.title = this._str("new.password.title"); + introText.textContent = this._str("new.password.introText"); + this._dialog.getButton("finish").label + = this._str("new.password.acceptButton"); + document.getElementById("textBox2Row").hidden = true; + } + else { + document.title = this._str("change.password.title"); + box2label.value = this._str("new.password.confirm"); + introText.textContent = this._str("change.password3.introText"); + warningText.textContent = this._str("change.password.warningText"); + this._dialog.getButton("finish").label + = this._str("change.password.acceptButton"); + } + break; + } + document.getElementById("change-page") + .setAttribute("label", document.title); + }, + + _clearStatus: function _clearStatus() { + this._status.value = ""; + this._statusIcon.removeAttribute("status"); + }, + + _updateStatus: function Change__updateStatus(str, state) { + this._updateStatusWithString(this._str(str), state); + }, + + _updateStatusWithString: function Change__updateStatusWithString(string, state) { + this._statusRow.hidden = false; + this._status.value = string; + this._statusIcon.setAttribute("status", state); + + let error = state == "error"; + this._dialog.getButton("cancel").disabled = !error; + this._dialog.getButton("finish").disabled = !error; + document.getElementById("printSyncKeyButton").disabled = !error; + document.getElementById("saveSyncKeyButton").disabled = !error; + + if (state == "success") + window.setTimeout(window.close, 1500); + }, + + onDialogAccept: function() { + switch (this._dialogType) { + case "UpdatePassphrase": + case "ResetPassphrase": + return this.doChangePassphrase(); + case "ChangePassword": + return this.doChangePassword(); + } + return undefined; + }, + + doGeneratePassphrase: function () { + let passphrase = Weave.Utils.generatePassphrase(); + this._passphraseBox.value = Weave.Utils.hyphenatePassphrase(passphrase); + this._dialog.getButton("finish").disabled = false; + }, + + doChangePassphrase: function Change_doChangePassphrase() { + let pp = Weave.Utils.normalizePassphrase(this._passphraseBox.value); + if (this._updatingPassphrase) { + Weave.Service.identity.syncKey = pp; + if (Weave.Service.login()) { + this._updateStatus("change.recoverykey.success", "success"); + Weave.Service.persistLogin(); + Weave.Service.scheduler.delayedAutoConnect(0); + } + else { + this._updateStatus("new.passphrase.status.incorrect", "error"); + } + } + else { + this._updateStatus("change.recoverykey.label", "active"); + + if (Weave.Service.changePassphrase(pp)) + this._updateStatus("change.recoverykey.success", "success"); + else + this._updateStatus("change.recoverykey.error", "error"); + } + + return false; + }, + + doChangePassword: function Change_doChangePassword() { + if (this._currentPasswordInvalid) { + Weave.Service.identity.basicPassword = this._firstBox.value; + if (Weave.Service.login()) { + this._updateStatus("change.password.status.success", "success"); + Weave.Service.persistLogin(); + } + else { + this._updateStatus("new.password.status.incorrect", "error"); + } + } + else { + this._updateStatus("change.password.status.active", "active"); + + if (Weave.Service.changePassword(this._firstBox.value)) + this._updateStatus("change.password.status.success", "success"); + else + this._updateStatus("change.password.status.error", "error"); + } + + return false; + }, + + validate: function (event) { + let valid = false; + let errorString = ""; + + if (this._dialogType == "ChangePassword") { + if (this._currentPasswordInvalid) + [valid, errorString] = gSyncUtils.validatePassword(this._firstBox); + else + [valid, errorString] = gSyncUtils.validatePassword(this._firstBox, this._secondBox); + } + else { + if (!this._updatingPassphrase) + return; + + valid = this._passphraseBox.value != ""; + } + + if (errorString == "") + this._clearStatus(); + else + this._updateStatusWithString(errorString, "error"); + + this._statusRow.hidden = valid; + this._dialog.getButton("finish").disabled = !valid; + }, + + _str: function Change__string(str) { + return this._stringBundle.GetStringFromName(str); + } +}; diff --git a/application/basilisk/base/content/sync/genericChange.xul b/application/basilisk/base/content/sync/genericChange.xul new file mode 100644 index 000000000..db74a1b31 --- /dev/null +++ b/application/basilisk/base/content/sync/genericChange.xul @@ -0,0 +1,123 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd"> +%brandDTD; +%syncBrandDTD; +%syncSetupDTD; +]> +<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + id="change-dialog" + windowtype="Weave:ChangeSomething" + persist="screenX screenY" + onwizardnext="Change.onLoad()" + onwizardfinish="return Change.onDialogAccept();"> + + <script type="application/javascript" + src="chrome://browser/content/sync/genericChange.js"/> + <script type="application/javascript" + src="chrome://browser/content/sync/utils.js"/> + <script type="application/javascript" + src="chrome://global/content/printUtils.js"/> + + <wizardpage id="change-page" + label=""> + + <description id="introText"> + </description> + + <separator class="thin"/> + + <groupbox> + <grid> + <columns> + <column align="right"/> + <column flex="3"/> + <column flex="1"/> + </columns> + <rows> + <row id="textBox1Row" align="center"> + <label id="textBox1Label" control="textBox1"/> + <textbox id="textBox1" type="password" oninput="Change.validate()"/> + <spacer/> + </row> + <row id="textBox2Row" align="center"> + <label id="textBox2Label" control="textBox2"/> + <textbox id="textBox2" type="password" oninput="Change.validate()"/> + <spacer/> + </row> + </rows> + </grid> + + <vbox id="passphraseRow"> + <hbox flex="1"> + <label id="passphraseLabel" control="passphraseBox"/> + <spacer flex="1"/> + <label id="generatePassphraseButton" + hidden="true" + value="&syncGenerateNewKey.label;" + class="text-link" + onclick="event.stopPropagation(); + Change.doGeneratePassphrase();"/> + </hbox> + <textbox id="passphraseBox" + flex="1" + onfocus="this.select()" + oninput="Change.validate()"/> + </vbox> + + <vbox id="feedback" pack="center"> + <hbox id="statusRow" align="center"> + <image id="statusIcon" class="statusIcon"/> + <label id="status" class="status" value=" "/> + </hbox> + </vbox> + </groupbox> + + <separator class="thin"/> + + <hbox id="passphraseBackupButtons" + hidden="true" + pack="center"> + <button id="printSyncKeyButton" + label="&button.syncKeyBackup.print.label;" + accesskey="&button.syncKeyBackup.print.accesskey;" + oncommand="gSyncUtils.passphrasePrint('passphraseBox');"/> + <button id="saveSyncKeyButton" + label="&button.syncKeyBackup.save.label;" + accesskey="&button.syncKeyBackup.save.accesskey;" + oncommand="gSyncUtils.passphraseSave('passphraseBox');"/> + </hbox> + + <vbox id="passphraseHelpBox" + hidden="true"> + <description> + &existingRecoveryKey.description; + <label class="text-link" + href="https://services.mozilla.com/sync/help/manual-setup"> + &addDevice.showMeHow.label; + </label> + </description> + </vbox> + + <spacer id="passphraseSpacer" + flex="1" + hidden="true"/> + + <description id="warningText" class="data"> + </description> + + <spacer flex="1"/> + </wizardpage> +</wizard> diff --git a/application/basilisk/base/content/sync/key.xhtml b/application/basilisk/base/content/sync/key.xhtml new file mode 100644 index 000000000..1363132e7 --- /dev/null +++ b/application/basilisk/base/content/sync/key.xhtml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> + %syncBrandDTD; + <!ENTITY % syncKeyDTD SYSTEM "chrome://browser/locale/syncKey.dtd"> + %syncKeyDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" > + %globalDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>&syncKey.page.title;</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="robots" content="noindex"/> + <style type="text/css"> + #synckey { font-size: 150% } + footer { font-size: 70% } + /* Bug 575675: Need to have an a:visited rule in a chrome document. */ + a:visited { color: purple; } + </style> +</head> + +<body dir="&locale.dir;"> +<h1>&syncKey.page.title;</h1> + +<p id="synckey" dir="ltr">SYNCKEY</p> + +<p>&syncKey.page.description2;</p> + +<div id="column1"> + <h2>&syncKey.keepItSecret.heading;</h2> + <p>&syncKey.keepItSecret.description;</p> +</div> + +<div id="column2"> + <h2>&syncKey.keepItSafe.heading;</h2> + <p><em>&syncKey.keepItSafe1.description;</em>&syncKey.keepItSafe2.description;<em>&syncKey.keepItSafe3.description;</em>&syncKey.keepItSafe4a.description;</p> +</div> + +<p>&syncKey.findOutMore1.label;<a href="https://services.mozilla.com">https://services.mozilla.com</a>&syncKey.findOutMore2.label;</p> + +<footer> + &syncKey.footer1.label;<a id="tosLink" href="termsURL">termsURL</a>&syncKey.footer2.label;<a id="ppLink" href="privacyURL">privacyURL</a>&syncKey.footer3.label; +</footer> + +</body> +</html> diff --git a/application/basilisk/base/content/sync/setup.js b/application/basilisk/base/content/sync/setup.js new file mode 100644 index 000000000..f9dae1bd4 --- /dev/null +++ b/application/basilisk/base/content/sync/setup.js @@ -0,0 +1,1060 @@ +// -*- 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 Cc = Components.classes; +var Cr = Components.results; +var Cu = Components.utils; + +// page consts + +const PAIR_PAGE = 0; +const INTRO_PAGE = 1; +const NEW_ACCOUNT_START_PAGE = 2; +const EXISTING_ACCOUNT_CONNECT_PAGE = 3; +const EXISTING_ACCOUNT_LOGIN_PAGE = 4; +const OPTIONS_PAGE = 5; +const OPTIONS_CONFIRM_PAGE = 6; + +// Broader than we'd like, but after this changed from api-secure.recaptcha.net +// we had no choice. At least we only do this for the duration of setup. +// See discussion in Bugs 508112 and 653307. +const RECAPTCHA_DOMAIN = "https://www.google.com"; + +const PIN_PART_LENGTH = 4; + +Cu.import("resource://services-sync/main.js"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm"); +Cu.import("resource://gre/modules/PluralForm.jsm"); + + +function setVisibility(element, visible) { + element.style.visibility = visible ? "visible" : "hidden"; +} + +var gSyncSetup = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, + Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + + captchaBrowser: null, + wizard: null, + _disabledSites: [], + + status: { + password: false, + email: false, + server: false + }, + + get _remoteSites() { + return [Weave.Service.serverURL, RECAPTCHA_DOMAIN]; + }, + + get _usingMainServers() { + if (this._settingUpNew) + return document.getElementById("server").selectedIndex == 0; + return document.getElementById("existingServer").selectedIndex == 0; + }, + + init: function () { + let obs = [ + ["weave:service:change-passphrase", "onResetPassphrase"], + ["weave:service:login:start", "onLoginStart"], + ["weave:service:login:error", "onLoginEnd"], + ["weave:service:login:finish", "onLoginEnd"]]; + + // Add the observers now and remove them on unload + let self = this; + let addRem = function(add) { + obs.forEach(function([topic, func]) { + // XXXzpao This should use Services.obs.* but Weave's Obs does nice handling + // of `this`. Fix in a followup. (bug 583347) + if (add) + Weave.Svc.Obs.add(topic, self[func], self); + else + Weave.Svc.Obs.remove(topic, self[func], self); + }); + }; + addRem(true); + window.addEventListener("unload", () => addRem(false), false); + + window.setTimeout(function () { + // Force Service to be loaded so that engines are registered. + // See Bug 670082. + Weave.Service; + }, 0); + + this.captchaBrowser = document.getElementById("captcha"); + + this.wizardType = null; + if (window.arguments && window.arguments[0]) { + this.wizardType = window.arguments[0]; + } + switch (this.wizardType) { + case null: + this.wizard.pageIndex = INTRO_PAGE; + // Fall through! + case "pair": + this.captchaBrowser.addProgressListener(this); + Weave.Svc.Prefs.set("firstSync", "notReady"); + break; + case "reset": + this._resettingSync = true; + this.wizard.pageIndex = OPTIONS_PAGE; + break; + } + + this.wizard.getButton("extra1").label = + this._stringBundle.GetStringFromName("button.syncOptions.label"); + + // Remember these values because the options pages change them temporarily. + this._nextButtonLabel = this.wizard.getButton("next").label; + this._nextButtonAccesskey = this.wizard.getButton("next") + .getAttribute("accesskey"); + this._backButtonLabel = this.wizard.getButton("back").label; + this._backButtonAccesskey = this.wizard.getButton("back") + .getAttribute("accesskey"); + }, + + startNewAccountSetup: function () { + if (!Weave.Utils.ensureMPUnlocked()) + return; + this._settingUpNew = true; + this.wizard.pageIndex = NEW_ACCOUNT_START_PAGE; + }, + + useExistingAccount: function () { + if (!Weave.Utils.ensureMPUnlocked()) + return; + this._settingUpNew = false; + if (this.wizardType == "pair") { + // We're already pairing, so there's no point in pairing again. + // Go straight to the manual login page. + this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE; + } else { + this.wizard.pageIndex = EXISTING_ACCOUNT_CONNECT_PAGE; + } + }, + + resetPassphrase: function resetPassphrase() { + // Apply the existing form fields so that + // Weave.Service.changePassphrase() has the necessary credentials. + Weave.Service.identity.account = document.getElementById("existingAccountName").value; + Weave.Service.identity.basicPassword = document.getElementById("existingPassword").value; + + // Generate a new passphrase so that Weave.Service.login() will + // actually do something. + let passphrase = Weave.Utils.generatePassphrase(); + Weave.Service.identity.syncKey = passphrase; + + // Only open the dialog if username + password are actually correct. + Weave.Service.login(); + if ([Weave.LOGIN_FAILED_INVALID_PASSPHRASE, + Weave.LOGIN_FAILED_NO_PASSPHRASE, + Weave.LOGIN_SUCCEEDED].indexOf(Weave.Status.login) == -1) { + return; + } + + // Hide any errors about the passphrase, we know it's not right. + let feedback = document.getElementById("existingPassphraseFeedbackRow"); + feedback.hidden = true; + let el = document.getElementById("existingPassphrase"); + el.value = Weave.Utils.hyphenatePassphrase(passphrase); + + // changePassphrase() will sync, make sure we set the "firstSync" pref + // according to the user's pref. + Weave.Svc.Prefs.reset("firstSync"); + this.setupInitialSync(); + gSyncUtils.resetPassphrase(true); + }, + + onResetPassphrase: function () { + document.getElementById("existingPassphrase").value = + Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey); + this.checkFields(); + this.wizard.advance(); + }, + + onLoginStart: function () { + this.toggleLoginFeedback(false); + }, + + onLoginEnd: function () { + this.toggleLoginFeedback(true); + }, + + sendCredentialsAfterSync: function () { + let send = function() { + Services.obs.removeObserver("weave:service:sync:finish", send); + Services.obs.removeObserver("weave:service:sync:error", send); + let credentials = {account: Weave.Service.identity.account, + password: Weave.Service.identity.basicPassword, + synckey: Weave.Service.identity.syncKey, + serverURL: Weave.Service.serverURL}; + this._jpakeclient.sendAndComplete(credentials); + }.bind(this); + Services.obs.addObserver("weave:service:sync:finish", send, false); + Services.obs.addObserver("weave:service:sync:error", send, false); + }, + + toggleLoginFeedback: function (stop) { + document.getElementById("login-throbber").hidden = stop; + let password = document.getElementById("existingPasswordFeedbackRow"); + let server = document.getElementById("existingServerFeedbackRow"); + let passphrase = document.getElementById("existingPassphraseFeedbackRow"); + + if (!stop || (Weave.Status.login == Weave.LOGIN_SUCCEEDED)) { + password.hidden = server.hidden = passphrase.hidden = true; + return; + } + + let feedback; + switch (Weave.Status.login) { + case Weave.LOGIN_FAILED_NETWORK_ERROR: + case Weave.LOGIN_FAILED_SERVER_ERROR: + feedback = server; + break; + case Weave.LOGIN_FAILED_LOGIN_REJECTED: + case Weave.LOGIN_FAILED_NO_USERNAME: + case Weave.LOGIN_FAILED_NO_PASSWORD: + feedback = password; + break; + case Weave.LOGIN_FAILED_INVALID_PASSPHRASE: + feedback = passphrase; + break; + } + this._setFeedbackMessage(feedback, false, Weave.Status.login); + }, + + setupInitialSync: function () { + let action = document.getElementById("mergeChoiceRadio").selectedItem.id; + switch (action) { + case "resetClient": + // if we're not resetting sync, we don't need to explicitly + // call resetClient + if (!this._resettingSync) + return; + // otherwise, fall through + case "wipeClient": + case "wipeRemote": + Weave.Svc.Prefs.set("firstSync", action); + break; + } + }, + + // fun with validation! + checkFields: function () { + this.wizard.canAdvance = this.readyToAdvance(); + }, + + readyToAdvance: function () { + switch (this.wizard.pageIndex) { + case INTRO_PAGE: + return false; + case NEW_ACCOUNT_START_PAGE: + for (let i in this.status) { + if (!this.status[i]) + return false; + } + if (this._usingMainServers) + return document.getElementById("tos").checked; + + return true; + case EXISTING_ACCOUNT_LOGIN_PAGE: + let hasUser = document.getElementById("existingAccountName").value != ""; + let hasPass = document.getElementById("existingPassword").value != ""; + let hasKey = document.getElementById("existingPassphrase").value != ""; + + if (hasUser && hasPass && hasKey) { + if (this._usingMainServers) + return true; + + if (this._validateServer(document.getElementById("existingServer"))) { + return true; + } + } + return false; + } + // Default, e.g. wizard's special page -1 etc. + return true; + }, + + onPINInput: function onPINInput(textbox) { + if (textbox && textbox.value.length == PIN_PART_LENGTH) { + this.nextFocusEl[textbox.id].focus(); + } + this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH && + this.pin2.value.length == PIN_PART_LENGTH && + this.pin3.value.length == PIN_PART_LENGTH); + }, + + onEmailInput: function () { + // Check account validity when the user stops typing for 1 second. + if (this._checkAccountTimer) + window.clearTimeout(this._checkAccountTimer); + this._checkAccountTimer = window.setTimeout(function () { + gSyncSetup.checkAccount(); + }, 1000); + }, + + checkAccount: function() { + delete this._checkAccountTimer; + let value = Weave.Utils.normalizeAccount( + document.getElementById("weaveEmail").value); + if (!value) { + this.status.email = false; + this.checkFields(); + return; + } + + let re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + let feedback = document.getElementById("emailFeedbackRow"); + let valid = re.test(value); + + let str = ""; + if (!valid) { + str = "invalidEmail.label"; + } else { + let availCheck = Weave.Service.checkAccount(value); + valid = availCheck == "available"; + if (!valid) { + if (availCheck == "notAvailable") + str = "usernameNotAvailable.label"; + else + str = availCheck; + } + } + + this._setFeedbackMessage(feedback, valid, str); + this.status.email = valid; + if (valid) + Weave.Service.identity.account = value; + this.checkFields(); + }, + + onPasswordChange: function () { + let password = document.getElementById("weavePassword"); + let pwconfirm = document.getElementById("weavePasswordConfirm"); + let [valid, errorString] = gSyncUtils.validatePassword(password, pwconfirm); + + let feedback = document.getElementById("passwordFeedbackRow"); + this._setFeedback(feedback, valid, errorString); + + this.status.password = valid; + this.checkFields(); + }, + + onPageShow: function() { + switch (this.wizard.pageIndex) { + case PAIR_PAGE: + this.wizard.getButton("back").hidden = true; + this.wizard.getButton("extra1").hidden = true; + this.onPINInput(); + this.pin1.focus(); + break; + case INTRO_PAGE: + // We may not need the captcha in the Existing Account branch of the + // wizard. However, we want to preload it to avoid any flickering while + // the Create Account page is shown. + this.loadCaptcha(); + this.wizard.getButton("next").hidden = true; + this.wizard.getButton("back").hidden = true; + this.wizard.getButton("extra1").hidden = true; + this.checkFields(); + break; + case NEW_ACCOUNT_START_PAGE: + this.wizard.getButton("extra1").hidden = false; + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("back").hidden = false; + this.onServerCommand(); + this.wizard.canRewind = true; + this.checkFields(); + break; + case EXISTING_ACCOUNT_CONNECT_PAGE: + Weave.Svc.Prefs.set("firstSync", "existingAccount"); + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("back").hidden = false; + this.wizard.getButton("extra1").hidden = false; + this.wizard.canAdvance = false; + this.wizard.canRewind = true; + this.startEasySetup(); + break; + case EXISTING_ACCOUNT_LOGIN_PAGE: + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("back").hidden = false; + this.wizard.getButton("extra1").hidden = false; + this.wizard.canRewind = true; + this.checkFields(); + break; + case OPTIONS_PAGE: + this.wizard.canRewind = false; + this.wizard.canAdvance = true; + if (!this._resettingSync) { + this.wizard.getButton("next").label = + this._stringBundle.GetStringFromName("button.syncOptionsDone.label"); + this.wizard.getButton("next").removeAttribute("accesskey"); + } + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("back").hidden = true; + this.wizard.getButton("cancel").hidden = !this._resettingSync; + this.wizard.getButton("extra1").hidden = true; + document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName; + document.getElementById("syncOptions").collapsed = this._resettingSync; + document.getElementById("mergeOptions").collapsed = this._settingUpNew; + break; + case OPTIONS_CONFIRM_PAGE: + this.wizard.canRewind = true; + this.wizard.canAdvance = true; + this.wizard.getButton("back").label = + this._stringBundle.GetStringFromName("button.syncOptionsCancel.label"); + this.wizard.getButton("back").removeAttribute("accesskey"); + this.wizard.getButton("back").hidden = this._resettingSync; + this.wizard.getButton("next").hidden = false; + this.wizard.getButton("finish").hidden = true; + break; + } + }, + + onWizardAdvance: function () { + // Check pageIndex so we don't prompt before the Sync setup wizard appears. + // This is a fallback in case the Master Password gets locked mid-wizard. + if ((this.wizard.pageIndex >= 0) && + !Weave.Utils.ensureMPUnlocked()) { + return false; + } + + switch (this.wizard.pageIndex) { + case PAIR_PAGE: + this.startPairing(); + return false; + case NEW_ACCOUNT_START_PAGE: + // If the user selects Next (e.g. by hitting enter) when we haven't + // executed the delayed checks yet, execute them immediately. + if (this._checkAccountTimer) { + this.checkAccount(); + } + if (this._checkServerTimer) { + this.checkServer(); + } + if (!this.wizard.canAdvance) { + return false; + } + + let doc = this.captchaBrowser.contentDocument; + let getField = function getField(field) { + let node = doc.getElementById("recaptcha_" + field + "_field"); + return node && node.value; + }; + + // Display throbber + let feedback = document.getElementById("captchaFeedback"); + let image = feedback.firstChild; + let label = image.nextSibling; + image.setAttribute("status", "active"); + label.value = this._stringBundle.GetStringFromName("verifying.label"); + setVisibility(feedback, true); + + let password = document.getElementById("weavePassword").value; + let email = Weave.Utils.normalizeAccount( + document.getElementById("weaveEmail").value); + let challenge = getField("challenge"); + let response = getField("response"); + + let error = Weave.Service.createAccount(email, password, + challenge, response); + + if (error == null) { + Weave.Service.identity.account = email; + Weave.Service.identity.basicPassword = password; + Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase(); + this._handleNoScript(false); + Weave.Svc.Prefs.set("firstSync", "newAccount"); + this.wizardFinish(); + return false; + } + + image.setAttribute("status", "error"); + label.value = Weave.Utils.getErrorString(error); + return false; + case EXISTING_ACCOUNT_LOGIN_PAGE: + Weave.Service.identity.account = Weave.Utils.normalizeAccount( + document.getElementById("existingAccountName").value); + Weave.Service.identity.basicPassword = + document.getElementById("existingPassword").value; + let pp = document.getElementById("existingPassphrase").value; + Weave.Service.identity.syncKey = Weave.Utils.normalizePassphrase(pp); + if (Weave.Service.login()) { + this.wizardFinish(); + } + return false; + case OPTIONS_PAGE: + let desc = document.getElementById("mergeChoiceRadio").selectedIndex; + // No confirmation needed on new account setup or merge option + // with existing account. + if (this._settingUpNew || (!this._resettingSync && desc == 0)) + return this.returnFromOptions(); + return this._handleChoice(); + case OPTIONS_CONFIRM_PAGE: + if (this._resettingSync) { + this.wizardFinish(); + return false; + } + return this.returnFromOptions(); + } + return true; + }, + + onWizardBack: function () { + switch (this.wizard.pageIndex) { + case NEW_ACCOUNT_START_PAGE: + this.wizard.pageIndex = INTRO_PAGE; + return false; + case EXISTING_ACCOUNT_CONNECT_PAGE: + this.abortEasySetup(); + this.wizard.pageIndex = INTRO_PAGE; + return false; + case EXISTING_ACCOUNT_LOGIN_PAGE: + // If we were already pairing on entry, we went straight to the manual + // login page. If subsequently we go back, return to the page that lets + // us choose whether we already have an account. + if (this.wizardType == "pair") { + this.wizard.pageIndex = INTRO_PAGE; + return false; + } + return true; + case OPTIONS_CONFIRM_PAGE: + // Backing up from the confirmation page = resetting first sync to merge. + document.getElementById("mergeChoiceRadio").selectedIndex = 0; + return this.returnFromOptions(); + } + return true; + }, + + wizardFinish: function () { + this.setupInitialSync(); + + if (this.wizardType == "pair") { + this.completePairing(); + } + + if (!this._resettingSync) { + function isChecked(element) { + return document.getElementById(element).hasAttribute("checked"); + } + + let prefs = ["engine.bookmarks", "engine.passwords", "engine.history", + "engine.tabs", "engine.prefs", "engine.addons"]; + for (let i = 0;i < prefs.length;i++) { + Weave.Svc.Prefs.set(prefs[i], isChecked(prefs[i])); + } + this._handleNoScript(false); + if (Weave.Svc.Prefs.get("firstSync", "") == "notReady") + Weave.Svc.Prefs.reset("firstSync"); + + Weave.Service.persistLogin(); + Weave.Svc.Obs.notify("weave:service:setup-complete"); + } + Weave.Utils.nextTick(Weave.Service.sync, Weave.Service); + window.close(); + }, + + onWizardCancel: function () { + if (this._resettingSync) + return; + + this.abortEasySetup(); + this._handleNoScript(false); + Weave.Service.startOver(); + }, + + onSyncOptions: function () { + this._beforeOptionsPage = this.wizard.pageIndex; + this.wizard.pageIndex = OPTIONS_PAGE; + }, + + returnFromOptions: function() { + this.wizard.getButton("next").label = this._nextButtonLabel; + this.wizard.getButton("next").setAttribute("accesskey", + this._nextButtonAccesskey); + this.wizard.getButton("back").label = this._backButtonLabel; + this.wizard.getButton("back").setAttribute("accesskey", + this._backButtonAccesskey); + this.wizard.getButton("cancel").hidden = false; + this.wizard.getButton("extra1").hidden = false; + this.wizard.pageIndex = this._beforeOptionsPage; + return false; + }, + + startPairing: function startPairing() { + this.pairDeviceErrorRow.hidden = true; + // When onAbort is called, Weave may already be gone. + const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT; + + let self = this; + let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({ + onPaired: function onPaired() { + self.wizard.pageIndex = INTRO_PAGE; + }, + onComplete: function onComplete() { + // This method will never be called since SendCredentialsController + // will take over after the wizard completes. + }, + onAbort: function onAbort(error) { + delete self._jpakeclient; + + // Aborted by user, ignore. The window is almost certainly going to close + // or is already closed. + if (error == JPAKE_ERROR_USERABORT) { + return; + } + + self.pairDeviceErrorRow.hidden = false; + self.pairDeviceThrobber.hidden = true; + self.pin1.value = self.pin2.value = self.pin3.value = ""; + self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false; + if (self.wizard.pageIndex == PAIR_PAGE) { + self.pin1.focus(); + } + } + }); + this.pairDeviceThrobber.hidden = false; + this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true; + this.wizard.canAdvance = false; + + let pin = this.pin1.value + this.pin2.value + this.pin3.value; + let expectDelay = true; + jpakeclient.pairWithPIN(pin, expectDelay); + }, + + completePairing: function completePairing() { + if (!this._jpakeclient) { + // The channel was aborted while we were setting up the account + // locally. XXX TODO should we do anything here, e.g. tell + // the user on the last wizard page that it's ok, they just + // have to pair again? + return; + } + let controller = new Weave.SendCredentialsController(this._jpakeclient, + Weave.Service); + this._jpakeclient.controller = controller; + }, + + startEasySetup: function () { + // Don't do anything if we have a client already (e.g. we went to + // Sync Options and just came back). + if (this._jpakeclient) + return; + + // When onAbort is called, Weave may already be gone + const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT; + + let self = this; + this._jpakeclient = new Weave.JPAKEClient({ + displayPIN: function displayPIN(pin) { + document.getElementById("easySetupPIN1").value = pin.slice(0, 4); + document.getElementById("easySetupPIN2").value = pin.slice(4, 8); + document.getElementById("easySetupPIN3").value = pin.slice(8); + }, + + onPairingStart: function onPairingStart() {}, + + onComplete: function onComplete(credentials) { + Weave.Service.identity.account = credentials.account; + Weave.Service.identity.basicPassword = credentials.password; + Weave.Service.identity.syncKey = credentials.synckey; + Weave.Service.serverURL = credentials.serverURL; + gSyncSetup.wizardFinish(); + }, + + onAbort: function onAbort(error) { + delete self._jpakeclient; + + // Ignore if wizard is aborted. + if (error == JPAKE_ERROR_USERABORT) + return; + + // Automatically go to manual setup if we couldn't acquire a channel. + if (error == Weave.JPAKE_ERROR_CHANNEL) { + self.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE; + return; + } + + // Restart on all other errors. + self.startEasySetup(); + } + }); + this._jpakeclient.receiveNoPIN(); + }, + + abortEasySetup: function () { + document.getElementById("easySetupPIN1").value = ""; + document.getElementById("easySetupPIN2").value = ""; + document.getElementById("easySetupPIN3").value = ""; + if (!this._jpakeclient) + return; + + this._jpakeclient.abort(); + delete this._jpakeclient; + }, + + manualSetup: function () { + this.abortEasySetup(); + this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE; + }, + + // _handleNoScript is needed because it blocks the captcha. So we temporarily + // allow the necessary sites so that we can verify the user is in fact a human. + // This was done with the help of Giorgio (NoScript author). See bug 508112. + _handleNoScript: function (addExceptions) { + // if NoScript isn't installed, or is disabled, bail out. + let ns = Cc["@maone.net/noscript-service;1"]; + if (ns == null) + return; + + ns = ns.getService().wrappedJSObject; + if (addExceptions) { + this._remoteSites.forEach(function(site) { + site = ns.getSite(site); + if (!ns.isJSEnabled(site)) { + this._disabledSites.push(site); // save status + ns.setJSEnabled(site, true); // allow site + } + }, this); + } + else { + this._disabledSites.forEach(function(site) { + ns.setJSEnabled(site, false); + }); + this._disabledSites = []; + } + }, + + onExistingServerCommand: function () { + let control = document.getElementById("existingServer"); + if (control.selectedIndex == 0) { + control.removeAttribute("editable"); + Weave.Svc.Prefs.reset("serverURL"); + } else { + control.setAttribute("editable", "true"); + // Force a style flush to ensure that the binding is attached. + control.clientTop; + control.value = ""; + control.inputField.focus(); + } + document.getElementById("existingServerFeedbackRow").hidden = true; + this.checkFields(); + }, + + onExistingServerInput: function () { + // Check custom server validity when the user stops typing for 1 second. + if (this._existingServerTimer) + window.clearTimeout(this._existingServerTimer); + this._existingServerTimer = window.setTimeout(function () { + gSyncSetup.checkFields(); + }, 1000); + }, + + onServerCommand: function () { + setVisibility(document.getElementById("TOSRow"), this._usingMainServers); + let control = document.getElementById("server"); + if (!this._usingMainServers) { + control.setAttribute("editable", "true"); + // Force a style flush to ensure that the binding is attached. + control.clientTop; + control.value = ""; + control.inputField.focus(); + // checkServer() will call checkAccount() and checkFields(). + this.checkServer(); + return; + } + control.removeAttribute("editable"); + Weave.Svc.Prefs.reset("serverURL"); + if (this._settingUpNew) { + this.loadCaptcha(); + } + this.checkAccount(); + this.status.server = true; + document.getElementById("serverFeedbackRow").hidden = true; + this.checkFields(); + }, + + onServerInput: function () { + // Check custom server validity when the user stops typing for 1 second. + if (this._checkServerTimer) + window.clearTimeout(this._checkServerTimer); + this._checkServerTimer = window.setTimeout(function () { + gSyncSetup.checkServer(); + }, 1000); + }, + + checkServer: function () { + delete this._checkServerTimer; + let el = document.getElementById("server"); + let valid = false; + let feedback = document.getElementById("serverFeedbackRow"); + if (el.value) { + valid = this._validateServer(el); + let str = valid ? "" : "serverInvalid.label"; + this._setFeedbackMessage(feedback, valid, str); + } + else + this._setFeedbackMessage(feedback, true); + + // Recheck account against the new server. + if (valid) + this.checkAccount(); + + this.status.server = valid; + this.checkFields(); + }, + + _validateServer: function (element) { + let valid = false; + let val = element.value; + if (!val) + return false; + + let uri = Weave.Utils.makeURI(val); + + if (!uri) + uri = Weave.Utils.makeURI("https://" + val); + + if (uri && this._settingUpNew) { + function isValid(uri) { + Weave.Service.serverURL = uri.spec; + let check = Weave.Service.checkAccount("a"); + return (check == "available" || check == "notAvailable"); + } + + if (uri.schemeIs("http")) { + uri.scheme = "https"; + if (isValid(uri)) + valid = true; + else + // setting the scheme back to http + uri.scheme = "http"; + } + if (!valid) + valid = isValid(uri); + + if (valid) { + this.loadCaptcha(); + } + } + else if (uri) { + valid = true; + Weave.Service.serverURL = uri.spec; + } + + if (valid) + element.value = Weave.Service.serverURL; + else + Weave.Svc.Prefs.reset("serverURL"); + + return valid; + }, + + _handleChoice: function () { + let desc = document.getElementById("mergeChoiceRadio").selectedIndex; + document.getElementById("chosenActionDeck").selectedIndex = desc; + switch (desc) { + case 1: + if (this._case1Setup) + break; + + let places_db = PlacesUtils.history + .QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; + if (Weave.Service.engineManager.get("history").enabled) { + let daysOfHistory = 0; + let stm = places_db.createStatement( + "SELECT ROUND(( " + + "strftime('%s','now','localtime','utc') - " + + "( " + + "SELECT visit_date FROM moz_historyvisits " + + "ORDER BY visit_date ASC LIMIT 1 " + + ")/1000000 " + + ")/86400) AS daysOfHistory "); + + if (stm.step()) + daysOfHistory = stm.getInt32(0); + // Support %S for historical reasons (see bug 600141) + document.getElementById("historyCount").value = + PluralForm.get(daysOfHistory, + this._stringBundle.GetStringFromName("historyDaysCount.label")) + .replace("%S", daysOfHistory) + .replace("#1", daysOfHistory); + } else { + document.getElementById("historyCount").hidden = true; + } + + if (Weave.Service.engineManager.get("bookmarks").enabled) { + let bookmarks = 0; + let stm = places_db.createStatement( + "SELECT count(*) AS bookmarks " + + "FROM moz_bookmarks b " + + "LEFT JOIN moz_bookmarks t ON " + + "b.parent = t.id WHERE b.type = 1 AND t.parent <> :tag"); + stm.params.tag = PlacesUtils.tagsFolderId; + if (stm.executeStep()) + bookmarks = stm.row.bookmarks; + // Support %S for historical reasons (see bug 600141) + document.getElementById("bookmarkCount").value = + PluralForm.get(bookmarks, + this._stringBundle.GetStringFromName("bookmarksCount.label")) + .replace("%S", bookmarks) + .replace("#1", bookmarks); + } else { + document.getElementById("bookmarkCount").hidden = true; + } + + if (Weave.Service.engineManager.get("passwords").enabled) { + let logins = Services.logins.getAllLogins({}); + // Support %S for historical reasons (see bug 600141) + document.getElementById("passwordCount").value = + PluralForm.get(logins.length, + this._stringBundle.GetStringFromName("passwordsCount.label")) + .replace("%S", logins.length) + .replace("#1", logins.length); + } else { + document.getElementById("passwordCount").hidden = true; + } + + if (!Weave.Service.engineManager.get("prefs").enabled) { + document.getElementById("prefsWipe").hidden = true; + } + + let addonsEngine = Weave.Service.engineManager.get("addons"); + if (addonsEngine.enabled) { + let ids = addonsEngine._store.getAllIDs(); + let blessedcount = Object.keys(ids).filter(id => ids[id]).length; + // bug 600141 does not apply, as this does not have to support existing strings + document.getElementById("addonCount").value = + PluralForm.get(blessedcount, + this._stringBundle.GetStringFromName("addonsCount.label")) + .replace("#1", blessedcount); + } else { + document.getElementById("addonCount").hidden = true; + } + + this._case1Setup = true; + break; + case 2: + if (this._case2Setup) + break; + let count = 0; + function appendNode(label) { + let box = document.getElementById("clientList"); + let node = document.createElement("label"); + node.setAttribute("value", label); + node.setAttribute("class", "data indent"); + box.appendChild(node); + } + + for (let name of Weave.Service.clientsEngine.stats.names) { + // Don't list the current client + if (name == Weave.Service.clientsEngine.localName) + continue; + + // Only show the first several client names + if (++count <= 5) + appendNode(name); + } + if (count > 5) { + // Support %S for historical reasons (see bug 600141) + let label = + PluralForm.get(count - 5, + this._stringBundle.GetStringFromName("additionalClientCount.label")) + .replace("%S", count - 5) + .replace("#1", count - 5); + appendNode(label); + } + this._case2Setup = true; + break; + } + + return true; + }, + + // sets class and string on a feedback element + // if no property string is passed in, we clear label/style + _setFeedback: function (element, success, string) { + element.hidden = success || !string; + let classname = success ? "success" : "error"; + let image = element.getElementsByAttribute("class", "statusIcon")[0]; + image.setAttribute("status", classname); + let label = element.getElementsByAttribute("class", "status")[0]; + label.value = string; + }, + + // shim + _setFeedbackMessage: function (element, success, string) { + let str = ""; + if (string) { + try { + str = this._stringBundle.GetStringFromName(string); + } catch (e) {} + + if (!str) + str = Weave.Utils.getErrorString(string); + } + this._setFeedback(element, success, str); + }, + + loadCaptcha: function loadCaptcha() { + let captchaURI = Weave.Service.miscAPI + "captcha_html"; + // First check for NoScript and whitelist the right sites. + this._handleNoScript(true); + if (this.captchaBrowser.currentURI.spec != captchaURI) { + this.captchaBrowser.loadURI(captchaURI); + } + }, + + onStateChange: function(webProgress, request, stateFlags, status) { + // We're only looking for the end of the frame load + if ((stateFlags & Ci.nsIWebProgressListener.STATE_STOP) == 0) + return; + if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) == 0) + return; + if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) == 0) + return; + + // If we didn't find a captcha, assume it's not needed and don't show it. + let responseStatus = request.QueryInterface(Ci.nsIHttpChannel).responseStatus; + setVisibility(this.captchaBrowser, responseStatus != 404); + // XXX TODO we should really log any responseStatus other than 200 + }, + onProgressChange: function() {}, + onStatusChange: function() {}, + onSecurityChange: function() {}, + onLocationChange: function () {} +}; + +// Define lazy getters for various XUL elements. +// +// onWizardAdvance() and onPageShow() are run before init(), so we'll even +// define things that will almost certainly be used (like 'wizard') as a lazy +// getter here. +["wizard", + "pin1", + "pin2", + "pin3", + "pairDeviceErrorRow", + "pairDeviceThrobber"].forEach(function (id) { + XPCOMUtils.defineLazyGetter(gSyncSetup, id, function() { + return document.getElementById(id); + }); +}); +XPCOMUtils.defineLazyGetter(gSyncSetup, "nextFocusEl", function () { + return {pin1: this.pin2, + pin2: this.pin3, + pin3: this.wizard.getButton("next")}; +}); +XPCOMUtils.defineLazyGetter(gSyncSetup, "_stringBundle", function() { + return Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); +}); diff --git a/application/basilisk/base/content/sync/setup.xul b/application/basilisk/base/content/sync/setup.xul new file mode 100644 index 000000000..11c085931 --- /dev/null +++ b/application/basilisk/base/content/sync/setup.xul @@ -0,0 +1,490 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd"> +%brandDTD; +%syncBrandDTD; +%syncSetupDTD; +]> +<wizard id="wizard" + title="&accountSetupTitle.label;" + windowtype="Weave:AccountSetup" + persist="screenX screenY" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onwizardnext="return gSyncSetup.onWizardAdvance()" + onwizardback="return gSyncSetup.onWizardBack()" + onwizardcancel="gSyncSetup.onWizardCancel()" + onload="gSyncSetup.init()"> + + <script type="application/javascript" + src="chrome://browser/content/sync/setup.js"/> + <script type="application/javascript" + src="chrome://browser/content/sync/utils.js"/> + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/printUtils.js"/> + + <wizardpage id="addDevicePage" + label="&pairDevice.title.label;" + onpageshow="gSyncSetup.onPageShow()"> + <description> + &pairDevice.dialog.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/add-device"/> + </description> + <separator class="groove-thin"/> + <description> + &addDevice.dialog.enterCode.label; + </description> + <separator class="groove-thin"/> + <vbox align="center"> + <textbox id="pin1" + class="pin" + oninput="gSyncSetup.onPINInput(this);" + onfocus="this.select();" + /> + <textbox id="pin2" + class="pin" + oninput="gSyncSetup.onPINInput(this);" + onfocus="this.select();" + /> + <textbox id="pin3" + class="pin" + oninput="gSyncSetup.onPINInput(this);" + onfocus="this.select();" + /> + </vbox> + <separator class="groove-thin"/> + <vbox id="pairDeviceThrobber" align="center" hidden="true"> + <image/> + </vbox> + <hbox id="pairDeviceErrorRow" pack="center" hidden="true"> + <image class="statusIcon" status="error"/> + <label class="status" + value="&addDevice.dialog.tryAgain.label;"/> + </hbox> + </wizardpage> + + <wizardpage id="pickSetupType" + label="&syncBrand.fullName.label;" + onpageshow="gSyncSetup.onPageShow()"> + <vbox align="center" flex="1"> + <description style="padding: 0 7em;"> + &setup.pickSetupType.description2; + </description> + <spacer flex="3"/> + <button id="newAccount" + class="accountChoiceButton" + label="&button.createNewAccount.label;" + oncommand="gSyncSetup.startNewAccountSetup()" + align="center"/> + <spacer flex="1"/> + </vbox> + <separator class="groove"/> + <vbox align="center" flex="1"> + <spacer flex="1"/> + <button id="existingAccount" + class="accountChoiceButton" + label="&button.haveAccount.label;" + oncommand="gSyncSetup.useExistingAccount()"/> + <spacer flex="3"/> + </vbox> + </wizardpage> + + <wizardpage label="&setup.newAccountDetailsPage.title.label;" + id="newAccountStart" + onextra1="gSyncSetup.onSyncOptions()" + onpageshow="gSyncSetup.onPageShow();"> + <grid> + <columns> + <column/> + <column class="inputColumn" flex="1"/> + </columns> + <rows> + <row id="emailRow" align="center"> + <label value="&setup.emailAddress.label;" + accesskey="&setup.emailAddress.accesskey;" + control="weaveEmail"/> + <textbox id="weaveEmail" + oninput="gSyncSetup.onEmailInput()"/> + </row> + <row id="emailFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row id="passwordRow" align="center"> + <label value="&setup.choosePassword.label;" + accesskey="&setup.choosePassword.accesskey;" + control="weavePassword"/> + <textbox id="weavePassword" + type="password" + onchange="gSyncSetup.onPasswordChange()"/> + </row> + <row id="confirmRow" align="center"> + <label value="&setup.confirmPassword.label;" + accesskey="&setup.confirmPassword.accesskey;" + control="weavePasswordConfirm"/> + <textbox id="weavePasswordConfirm" + type="password" + onchange="gSyncSetup.onPasswordChange()"/> + </row> + <row id="passwordFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row align="center"> + <label control="server" + value="&server.label;"/> + <menulist id="server" + oncommand="gSyncSetup.onServerCommand()" + oninput="gSyncSetup.onServerInput()"> + <menupopup> + <menuitem label="&serverType.default.label;" + value="main"/> + <menuitem label="&serverType.custom2.label;" + value="custom"/> + </menupopup> + </menulist> + </row> + <row id="serverFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row id="TOSRow" align="center"> + <spacer/> + <hbox align="center"> + <checkbox id="tos" + accesskey="&setup.tosAgree1.accesskey;" + oncommand="this.focus(); gSyncSetup.checkFields();"/> + <description id="tosDesc" + flex="1" + onclick="document.getElementById('tos').focus(); + document.getElementById('tos').click()"> + &setup.tosAgree1.label; + <label class="text-link" + onclick="event.stopPropagation();gSyncUtils.openToS();"> + &setup.tosLink.label; + </label> + &setup.tosAgree2.label; + <label class="text-link" + onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();"> + &setup.ppLink.label; + </label> + &setup.tosAgree3.label; + </description> + </hbox> + </row> + </rows> + </grid> + <spacer flex="1"/> + <vbox flex="1" align="center"> + <browser height="150" + width="500" + id="captcha" + type="content" + disablehistory="true"/> + <spacer flex="1"/> + <hbox id="captchaFeedback"> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </vbox> + </wizardpage> + + <wizardpage id="addDevice" + label="&pairDevice.title.label;" + onextra1="gSyncSetup.onSyncOptions()" + onpageshow="gSyncSetup.onPageShow()"> + <description> + &pairDevice.setup.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/easy-setup"/> + </description> + <label value="&addDevice.setup.enterCode.label;" + control="easySetupPIN1"/> + <spacer flex="1"/> + <vbox align="center" flex="1"> + <textbox id="easySetupPIN1" + class="pin" + value="" + readonly="true" + /> + <textbox id="easySetupPIN2" + class="pin" + value="" + readonly="true" + /> + <textbox id="easySetupPIN3" + class="pin" + value="" + readonly="true" + /> + </vbox> + <spacer flex="3"/> + <label class="text-link" + value="&addDevice.dontHaveDevice.label;" + onclick="gSyncSetup.manualSetup();"/> + </wizardpage> + + <wizardpage id="existingAccount" + label="&setup.signInPage.title.label;" + onextra1="gSyncSetup.onSyncOptions()" + onpageshow="gSyncSetup.onPageShow()"> + <grid> + <columns> + <column/> + <column class="inputColumn" flex="1"/> + </columns> + <rows> + <row id="existingAccountRow" align="center"> + <label id="existingAccountLabel" + value="&signIn.account2.label;" + accesskey="&signIn.account2.accesskey;" + control="existingAccount"/> + <textbox id="existingAccountName" + oninput="gSyncSetup.checkFields(event)" + onchange="gSyncSetup.checkFields(event)"/> + </row> + <row id="existingPasswordRow" align="center"> + <label id="existingPasswordLabel" + value="&signIn.password.label;" + accesskey="&signIn.password.accesskey;" + control="existingPassword"/> + <textbox id="existingPassword" + type="password" + onkeyup="gSyncSetup.checkFields(event)" + onchange="gSyncSetup.checkFields(event)"/> + </row> + <row id="existingPasswordFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row align="center"> + <spacer/> + <label class="text-link" + value="&resetPassword.label;" + onclick="gSyncUtils.resetPassword(); return false;"/> + </row> + <row align="center"> + <label control="existingServer" + value="&server.label;"/> + <menulist id="existingServer" + oncommand="gSyncSetup.onExistingServerCommand()" + oninput="gSyncSetup.onExistingServerInput()"> + <menupopup> + <menuitem label="&serverType.default.label;" + value="main"/> + <menuitem label="&serverType.custom2.label;" + value="custom"/> + </menupopup> + </menulist> + </row> + <row id="existingServerFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <vbox> + <label class="status" value=" "/> + </vbox> + </hbox> + </row> + </rows> + </grid> + + <groupbox> + <label id="existingPassphraseLabel" + value="&signIn.recoveryKey.label;" + accesskey="&signIn.recoveryKey.accesskey;" + control="existingPassphrase"/> + <textbox id="existingPassphrase" + oninput="gSyncSetup.checkFields()"/> + <hbox id="login-throbber" hidden="true"> + <image/> + <label value="&verifying.label;"/> + </hbox> + <vbox align="left" id="existingPassphraseFeedbackRow" hidden="true"> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </vbox> + </groupbox> + + <vbox id="passphraseHelpBox"> + <description> + &existingRecoveryKey.description; + <label class="text-link" + href="https://services.mozilla.com/sync/help/manual-setup"> + &addDevice.showMeHow.label; + </label> + <spacer id="passphraseHelpSpacer"/> + <label class="text-link" + onclick="gSyncSetup.resetPassphrase(); return false;"> + &resetSyncKey.label; + </label> + </description> + </vbox> + </wizardpage> + + <wizardpage id="syncOptionsPage" + label="&setup.optionsPage.title;" + onpageshow="gSyncSetup.onPageShow()"> + <groupbox id="syncOptions"> + <grid> + <columns> + <column/> + <column flex="1" style="margin-inline-end: 2px"/> + </columns> + <rows> + <row align="center"> + <label value="&syncDeviceName.label;" + accesskey="&syncDeviceName.accesskey;" + control="syncComputerName"/> + <textbox id="syncComputerName" flex="1" + onchange="gSyncUtils.changeName(this)"/> + </row> + <row> + <label value="&syncMy.label;" /> + <vbox> + <checkbox label="&engine.addons.label;" + accesskey="&engine.addons.accesskey;" + id="engine.addons" + checked="true"/> + <checkbox label="&engine.bookmarks.label;" + accesskey="&engine.bookmarks.accesskey;" + id="engine.bookmarks" + checked="true"/> + <checkbox label="&engine.passwords.label;" + accesskey="&engine.passwords.accesskey;" + id="engine.passwords" + checked="true"/> + <checkbox label="&engine.prefs.label;" + accesskey="&engine.prefs.accesskey;" + id="engine.prefs" + checked="true"/> + <checkbox label="&engine.history.label;" + accesskey="&engine.history.accesskey;" + id="engine.history" + checked="true"/> + <checkbox label="&engine.tabs.label;" + accesskey="&engine.tabs.accesskey;" + id="engine.tabs" + checked="true"/> + </vbox> + </row> + </rows> + </grid> + </groupbox> + + <groupbox id="mergeOptions"> + <radiogroup id="mergeChoiceRadio" pack="start"> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows flex="1"> + <row align="center"> + <radio id="resetClient" + class="mergeChoiceButton" + aria-labelledby="resetClientLabel"/> + <label id="resetClientLabel" control="resetClient"> + <html:strong>&choice2.merge.recommended.label;</html:strong> + &choice2a.merge.main.label; + </label> + </row> + <row align="center"> + <radio id="wipeClient" + class="mergeChoiceButton" + aria-labelledby="wipeClientLabel"/> + <label id="wipeClientLabel" + control="wipeClient"> + &choice2a.client.main.label; + </label> + </row> + <row align="center"> + <radio id="wipeRemote" + class="mergeChoiceButton" + aria-labelledby="wipeRemoteLabel"/> + <label id="wipeRemoteLabel" + control="wipeRemote"> + &choice2a.server.main.label; + </label> + </row> + </rows> + </grid> + </radiogroup> + </groupbox> + </wizardpage> + + <wizardpage id="syncOptionsConfirm" + label="&setup.optionsConfirmPage.title;" + onpageshow="gSyncSetup.onPageShow()"> + <deck id="chosenActionDeck"> + <vbox id="chosenActionMerge" class="confirm"> + <description class="normal"> + &confirm.merge2.label; + </description> + </vbox> + <vbox id="chosenActionWipeClient" class="confirm"> + <description class="normal"> + &confirm.client3.label; + </description> + <separator class="thin"/> + <vbox id="dataList"> + <label class="data indent" id="bookmarkCount"/> + <label class="data indent" id="historyCount"/> + <label class="data indent" id="passwordCount"/> + <label class="data indent" id="addonCount"/> + <label class="data indent" id="prefsWipe" + value="&engine.prefs.label;"/> + </vbox> + <separator class="thin"/> + <description class="normal"> + &confirm.client2.moreinfo.label; + </description> + </vbox> + <vbox id="chosenActionWipeServer" class="confirm"> + <description class="normal"> + &confirm.server2.label; + </description> + <separator class="thin"/> + <vbox id="clientList"> + </vbox> + </vbox> + </deck> + </wizardpage> + <!-- In terms of the wizard flow shown to the user, the 'syncOptionsConfirm' + page above is not the last wizard page. To prevent the wizard binding from + assuming that it is, we're inserting this dummy page here. This also means + that the wizard needs to always be closed manually via wizardFinish(). --> + <wizardpage> + </wizardpage> +</wizard> + diff --git a/application/basilisk/base/content/sync/utils.js b/application/basilisk/base/content/sync/utils.js new file mode 100644 index 000000000..92981f7b4 --- /dev/null +++ b/application/basilisk/base/content/sync/utils.js @@ -0,0 +1,231 @@ +/* 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/. */ + +// Equivalent to 0o600 permissions; used for saved Sync Recovery Key. +// This constant can be replaced when the equivalent values are available to +// chrome JS; see Bug 433295 and Bug 757351. +const PERMISSIONS_RWUSR = 0x180; + +// Weave should always exist before before this file gets included. +var gSyncUtils = { + get bundle() { + delete this.bundle; + return this.bundle = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); + }, + + get fxAccountsEnabled() { + let service = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + return service.fxAccountsEnabled; + }, + + // opens in a new window if we're in a modal prefwindow world, in a new tab otherwise + _openLink: function (url) { + let thisDocEl = document.documentElement, + openerDocEl = window.opener && window.opener.document.documentElement; + if (thisDocEl.id == "accountSetup" && window.opener && + openerDocEl.id == "BrowserPreferences" && !openerDocEl.instantApply) + openUILinkIn(url, "window"); + else if (thisDocEl.id == "BrowserPreferences" && !thisDocEl.instantApply) + openUILinkIn(url, "window"); + else if (document.documentElement.id == "change-dialog") + Services.wm.getMostRecentWindow("navigator:browser") + .openUILinkIn(url, "tab"); + else + openUILinkIn(url, "tab"); + }, + + changeName: function changeName(input) { + // Make sure to update to a modified name, e.g., empty-string -> default + Weave.Service.clientsEngine.localName = input.value; + input.value = Weave.Service.clientsEngine.localName; + }, + + openChange: function openChange(type, duringSetup) { + // Just re-show the dialog if it's already open + let openedDialog = Services.wm.getMostRecentWindow("Sync:" + type); + if (openedDialog != null) { + openedDialog.focus(); + return; + } + + // Open up the change dialog + let changeXUL = "chrome://browser/content/sync/genericChange.xul"; + let changeOpt = "centerscreen,chrome,resizable=no"; + Services.ww.activeWindow.openDialog(changeXUL, "", changeOpt, + type, duringSetup); + }, + + changePassword: function () { + if (Weave.Utils.ensureMPUnlocked()) + this.openChange("ChangePassword"); + }, + + resetPassphrase: function (duringSetup) { + if (Weave.Utils.ensureMPUnlocked()) + this.openChange("ResetPassphrase", duringSetup); + }, + + updatePassphrase: function () { + if (Weave.Utils.ensureMPUnlocked()) + this.openChange("UpdatePassphrase"); + }, + + resetPassword: function () { + this._openLink(Weave.Service.pwResetURL); + }, + + get tosURL() { + let root = this.fxAccountsEnabled ? "fxa." : ""; + return Weave.Svc.Prefs.get(root + "termsURL"); + }, + + openToS: function () { + this._openLink(this.tosURL); + }, + + get privacyPolicyURL() { + let root = this.fxAccountsEnabled ? "fxa." : ""; + return Weave.Svc.Prefs.get(root + "privacyURL"); + }, + + openPrivacyPolicy: function () { + this._openLink(this.privacyPolicyURL); + }, + + /** + * Prepare an invisible iframe with the passphrase backup document. + * Used by both the print and saving methods. + * + * @param elid : ID of the form element containing the passphrase. + * @param callback : Function called once the iframe has loaded. + */ + _preparePPiframe: function(elid, callback) { + let pp = document.getElementById(elid).value; + + // Create an invisible iframe whose contents we can print. + let iframe = document.createElement("iframe"); + iframe.setAttribute("src", "chrome://browser/content/sync/key.xhtml"); + iframe.collapsed = true; + document.documentElement.appendChild(iframe); + iframe.contentWindow.addEventListener("load", function() { + iframe.contentWindow.removeEventListener("load", arguments.callee, false); + + // Insert the Sync Key into the page. + let el = iframe.contentDocument.getElementById("synckey"); + el.firstChild.nodeValue = pp; + + // Insert the TOS and Privacy Policy URLs into the page. + let termsURL = Weave.Svc.Prefs.get("termsURL"); + el = iframe.contentDocument.getElementById("tosLink"); + el.setAttribute("href", termsURL); + el.firstChild.nodeValue = termsURL; + + let privacyURL = Weave.Svc.Prefs.get("privacyURL"); + el = iframe.contentDocument.getElementById("ppLink"); + el.setAttribute("href", privacyURL); + el.firstChild.nodeValue = privacyURL; + + callback(iframe); + }, false); + }, + + /** + * Print passphrase backup document. + * + * @param elid : ID of the form element containing the passphrase. + */ + passphrasePrint: function(elid) { + this._preparePPiframe(elid, function(iframe) { + let webBrowserPrint = iframe.contentWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebBrowserPrint); + let printSettings = PrintUtils.getPrintSettings(); + + // Display no header/footer decoration except for the date. + printSettings.headerStrLeft + = printSettings.headerStrCenter + = printSettings.headerStrRight + = printSettings.footerStrLeft + = printSettings.footerStrCenter = ""; + printSettings.footerStrRight = "&D"; + + try { + webBrowserPrint.print(printSettings, null); + } catch (ex) { + // print()'s return codes are expressed as exceptions. Ignore. + } + }); + }, + + /** + * Save passphrase backup document to disk as HTML file. + * + * @param elid : ID of the form element containing the passphrase. + */ + passphraseSave: function(elid) { + let dialogTitle = this.bundle.GetStringFromName("save.recoverykey.title"); + let defaultSaveName = this.bundle.GetStringFromName("save.recoverykey.defaultfilename"); + this._preparePPiframe(elid, function(iframe) { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == Ci.nsIFilePicker.returnOK || + aResult == Ci.nsIFilePicker.returnReplace) { + let stream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + stream.init(fp.file, -1, PERMISSIONS_RWUSR, 0); + + let serializer = new XMLSerializer(); + let output = serializer.serializeToString(iframe.contentDocument); + output = output.replace(/<!DOCTYPE (.|\n)*?]>/, + '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' + + '"DTD/xhtml1-strict.dtd">'); + output = Weave.Utils.encodeUTF8(output); + stream.write(output, output.length); + } + }; + + fp.init(window, dialogTitle, Ci.nsIFilePicker.modeSave); + fp.appendFilters(Ci.nsIFilePicker.filterHTML); + fp.defaultString = defaultSaveName; + fp.open(fpCallback); + return false; + }); + }, + + /** + * validatePassword + * + * @param el1 : the first textbox element in the form + * @param el2 : the second textbox element, if omitted it's an update form + * + * returns [valid, errorString] + */ + validatePassword: function (el1, el2) { + let valid = false; + let val1 = el1.value; + let val2 = el2 ? el2.value : ""; + let error = ""; + + if (!el2) + valid = val1.length >= Weave.MIN_PASS_LENGTH; + else if (val1 && val1 == Weave.Service.identity.username) + error = "change.password.pwSameAsUsername"; + else if (val1 && val1 == Weave.Service.identity.account) + error = "change.password.pwSameAsEmail"; + else if (val1 && val1 == Weave.Service.identity.basicPassword) + error = "change.password.pwSameAsPassword"; + else if (val1 && val2) { + if (val1 == val2 && val1.length >= Weave.MIN_PASS_LENGTH) + valid = true; + else if (val1.length < Weave.MIN_PASS_LENGTH) + error = "change.password.tooShort"; + else if (val1 != val2) + error = "change.password.mismatch"; + } + let errorString = error ? Weave.Utils.getErrorString(error) : ""; + return [valid, errorString]; + } +}; diff --git a/application/basilisk/base/content/tab-content.js b/application/basilisk/base/content/tab-content.js new file mode 100644 index 000000000..11a9fabce --- /dev/null +++ b/application/basilisk/base/content/tab-content.js @@ -0,0 +1,943 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This content script contains code that requires a tab browser. */ + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +#ifdef MOZ_WEBEXTENSIONS +Cu.import("resource://gre/modules/ExtensionContent.jsm"); +#endif + +XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", + "resource:///modules/E10SUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AboutReader", + "resource://gre/modules/AboutReader.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", + "resource://gre/modules/ReaderMode.jsm"); +XPCOMUtils.defineLazyGetter(this, "SimpleServiceDiscovery", function() { + let ssdp = Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm", {}).SimpleServiceDiscovery; + // Register targets + ssdp.registerDevice({ + id: "roku:ecp", + target: "roku:ecp", + factory: function(aService) { + Cu.import("resource://gre/modules/RokuApp.jsm"); + return new RokuApp(aService); + }, + types: ["video/mp4"], + extensions: ["mp4"] + }); + return ssdp; +}); + +// TabChildGlobal +var global = this; + + +addEventListener("MozDOMPointerLock:Entered", function(aEvent) { + sendAsyncMessage("PointerLock:Entered", { + originNoSuffix: aEvent.target.nodePrincipal.originNoSuffix + }); +}); + +addEventListener("MozDOMPointerLock:Exited", function(aEvent) { + sendAsyncMessage("PointerLock:Exited"); +}); + + +addMessageListener("Browser:HideSessionRestoreButton", function (message) { + // Hide session restore button on about:home + let doc = content.document; + let container; + if (doc.documentURI.toLowerCase() == "about:home" && + (container = doc.getElementById("sessionRestoreContainer"))) { + container.hidden = true; + } +}); + + +addMessageListener("Browser:Reload", function(message) { + /* First, we'll try to use the session history object to reload so + * that framesets are handled properly. If we're in a special + * window (such as view-source) that has no session history, fall + * back on using the web navigation's reload method. + */ + + let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); + try { + let sh = webNav.sessionHistory; + if (sh) + webNav = sh.QueryInterface(Ci.nsIWebNavigation); + } catch (e) { + } + + let reloadFlags = message.data.flags; + try { + E10SUtils.wrapHandlingUserInput(content, message.data.handlingUserInput, + () => webNav.reload(reloadFlags)); + } catch (e) { + } +}); + +addMessageListener("MixedContent:ReenableProtection", function() { + docShell.mixedContentChannel = null; +}); + +addMessageListener("SecondScreen:tab-mirror", function(message) { + if (!Services.prefs.getBoolPref("browser.casting.enabled")) { + return; + } + let app = SimpleServiceDiscovery.findAppForService(message.data.service); + if (app) { + let width = content.innerWidth; + let height = content.innerHeight; + let viewport = {cssWidth: width, cssHeight: height, width: width, height: height}; + app.mirror(function() {}, content, viewport, function() {}, content); + } +}); + +var AboutHomeListener = { + init: function(chromeGlobal) { + chromeGlobal.addEventListener('AboutHomeLoad', this, false, true); + }, + + get isAboutHome() { + return content.document.documentURI.toLowerCase() == "about:home"; + }, + + handleEvent: function(aEvent) { + if (!this.isAboutHome) { + return; + } + switch (aEvent.type) { + case "AboutHomeLoad": + this.onPageLoad(); + break; + case "click": + this.onClick(aEvent); + break; + case "pagehide": + this.onPageHide(aEvent); + break; + } + }, + + receiveMessage: function(aMessage) { + if (!this.isAboutHome) { + return; + } + switch (aMessage.name) { + case "AboutHome:Update": + this.onUpdate(aMessage.data); + break; + } + }, + + onUpdate: function(aData) { + let doc = content.document; + if (aData.showRestoreLastSession && !PrivateBrowsingUtils.isContentWindowPrivate(content)) + doc.getElementById("launcher").setAttribute("session", "true"); + + // Inject search engine URL. + let docElt = doc.documentElement; + if (aData.showKnowYourRights) + docElt.setAttribute("showKnowYourRights", "true"); + }, + + onPageLoad: function() { + addMessageListener("AboutHome:Update", this); + addEventListener("click", this, true); + addEventListener("pagehide", this, true); + + sendAsyncMessage("AboutHome:MaybeShowAutoMigrationUndoNotification"); + sendAsyncMessage("AboutHome:RequestUpdate"); + }, + + onClick: function(aEvent) { + if (!aEvent.isTrusted || // Don't trust synthetic events + aEvent.button == 2 || aEvent.target.localName != "button") { + return; + } + + let originalTarget = aEvent.originalTarget; + let ownerDoc = originalTarget.ownerDocument; + if (ownerDoc.documentURI != "about:home") { + // This shouldn't happen, but we're being defensive. + return; + } + + let elmId = originalTarget.getAttribute("id"); + + switch (elmId) { + case "restorePreviousSession": + sendAsyncMessage("AboutHome:RestorePreviousSession"); + ownerDoc.getElementById("launcher").removeAttribute("session"); + break; + + case "downloads": + sendAsyncMessage("AboutHome:Downloads"); + break; + + case "bookmarks": + sendAsyncMessage("AboutHome:Bookmarks"); + break; + + case "history": + sendAsyncMessage("AboutHome:History"); + break; + + case "addons": + sendAsyncMessage("AboutHome:Addons"); + break; + + case "sync": + sendAsyncMessage("AboutHome:Sync"); + break; + + case "settings": + sendAsyncMessage("AboutHome:Settings"); + break; + } + }, + + onPageHide: function(aEvent) { + if (aEvent.target.defaultView.frameElement) { + return; + } + removeMessageListener("AboutHome:Update", this); + removeEventListener("click", this, true); + removeEventListener("pagehide", this, true); + }, +}; +AboutHomeListener.init(this); + +var AboutPrivateBrowsingListener = { + init(chromeGlobal) { + chromeGlobal.addEventListener("AboutPrivateBrowsingOpenWindow", this, + false, true); + chromeGlobal.addEventListener("AboutPrivateBrowsingToggleTrackingProtection", this, + false, true); + }, + + get isAboutPrivateBrowsing() { + return content.document.documentURI.toLowerCase() == "about:privatebrowsing"; + }, + + handleEvent(aEvent) { + if (!this.isAboutPrivateBrowsing) { + return; + } + switch (aEvent.type) { + case "AboutPrivateBrowsingOpenWindow": + sendAsyncMessage("AboutPrivateBrowsing:OpenPrivateWindow"); + break; + case "AboutPrivateBrowsingToggleTrackingProtection": + sendAsyncMessage("AboutPrivateBrowsing:ToggleTrackingProtection"); + break; + } + }, +}; +AboutPrivateBrowsingListener.init(this); + +var AboutReaderListener = { + + _articlePromise: null, + + _isLeavingReaderMode: false, + + init: function() { + addEventListener("AboutReaderContentLoaded", this, false, true); + addEventListener("DOMContentLoaded", this, false); + addEventListener("pageshow", this, false); + addEventListener("pagehide", this, false); + addMessageListener("Reader:ToggleReaderMode", this); + addMessageListener("Reader:PushState", this); + }, + + receiveMessage: function(message) { + switch (message.name) { + case "Reader:ToggleReaderMode": + if (!this.isAboutReader) { + this._articlePromise = ReaderMode.parseDocument(content.document).catch(Cu.reportError); + ReaderMode.enterReaderMode(docShell, content); + } else { + this._isLeavingReaderMode = true; + ReaderMode.leaveReaderMode(docShell, content); + } + break; + + case "Reader:PushState": + this.updateReaderButton(!!(message.data && message.data.isArticle)); + break; + } + }, + + get isAboutReader() { + if (!content) { + return false; + } + return content.document.documentURI.startsWith("about:reader"); + }, + + handleEvent: function(aEvent) { + if (aEvent.originalTarget.defaultView != content) { + return; + } + + switch (aEvent.type) { + case "AboutReaderContentLoaded": + if (!this.isAboutReader) { + return; + } + + if (content.document.body) { + // Update the toolbar icon to show the "reader active" icon. + sendAsyncMessage("Reader:UpdateReaderButton"); + new AboutReader(content, this._articlePromise); + this._articlePromise = null; + } + break; + + case "pagehide": + this.cancelPotentialPendingReadabilityCheck(); + // this._isLeavingReaderMode is used here to keep the Reader Mode icon + // visible in the location bar when transitioning from reader-mode page + // back to the source page. + sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: this._isLeavingReaderMode }); + if (this._isLeavingReaderMode) { + this._isLeavingReaderMode = false; + } + break; + + case "pageshow": + // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded" + // event, so we need to rely on "pageshow" in this case. + if (aEvent.persisted) { + this.updateReaderButton(); + } + break; + case "DOMContentLoaded": + this.updateReaderButton(); + break; + + } + }, + + /** + * NB: this function will update the state of the reader button asynchronously + * after the next mozAfterPaint call (assuming reader mode is enabled and + * this is a suitable document). Calling it on things which won't be + * painted is not going to work. + */ + updateReaderButton: function(forceNonArticle) { + if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader || + !content || !(content.document instanceof content.HTMLDocument) || + content.document.mozSyntheticDocument) { + return; + } + + this.scheduleReadabilityCheckPostPaint(forceNonArticle); + }, + + cancelPotentialPendingReadabilityCheck: function() { + if (this._pendingReadabilityCheck) { + removeEventListener("MozAfterPaint", this._pendingReadabilityCheck); + delete this._pendingReadabilityCheck; + } + }, + + scheduleReadabilityCheckPostPaint: function(forceNonArticle) { + if (this._pendingReadabilityCheck) { + // We need to stop this check before we re-add one because we don't know + // if forceNonArticle was true or false last time. + this.cancelPotentialPendingReadabilityCheck(); + } + this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(this, forceNonArticle); + addEventListener("MozAfterPaint", this._pendingReadabilityCheck); + }, + + onPaintWhenWaitedFor: function(forceNonArticle, event) { + // In non-e10s, we'll get called for paints other than ours, and so it's + // possible that this page hasn't been laid out yet, in which case we + // should wait until we get an event that does relate to our layout. We + // determine whether any of our content got painted by checking if there + // are any painted rects. + if (!event.clientRects.length) { + return; + } + + this.cancelPotentialPendingReadabilityCheck(); + // Only send updates when there are articles; there's no point updating with + // |false| all the time. + if (ReaderMode.isProbablyReaderable(content.document)) { + sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true }); + } else if (forceNonArticle) { + sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false }); + } + }, +}; +AboutReaderListener.init(); + + +var ContentSearchMediator = { + + whitelist: new Set([ + "about:home", + "about:newtab", + ]), + + init: function (chromeGlobal) { + chromeGlobal.addEventListener("ContentSearchClient", this, true, true); + addMessageListener("ContentSearch", this); + }, + + handleEvent: function (event) { + if (this._contentWhitelisted) { + this._sendMsg(event.detail.type, event.detail.data); + } + }, + + receiveMessage: function (msg) { + if (msg.data.type == "AddToWhitelist") { + for (let uri of msg.data.data) { + this.whitelist.add(uri); + } + this._sendMsg("AddToWhitelistAck"); + return; + } + if (this._contentWhitelisted) { + this._fireEvent(msg.data.type, msg.data.data); + } + }, + + get _contentWhitelisted() { + return this.whitelist.has(content.document.documentURI); + }, + + _sendMsg: function (type, data=null) { + sendAsyncMessage("ContentSearch", { + type: type, + data: data, + }); + }, + + _fireEvent: function (type, data=null) { + let event = Cu.cloneInto({ + detail: { + type: type, + data: data, + }, + }, content); + content.dispatchEvent(new content.CustomEvent("ContentSearchService", + event)); + }, +}; +ContentSearchMediator.init(this); + +var PageStyleHandler = { + init: function() { + addMessageListener("PageStyle:Switch", this); + addMessageListener("PageStyle:Disable", this); + addEventListener("pageshow", () => this.sendStyleSheetInfo()); + }, + + get markupDocumentViewer() { + return docShell.contentViewer; + }, + + sendStyleSheetInfo: function() { + let filteredStyleSheets = this._filterStyleSheets(this.getAllStyleSheets()); + + sendAsyncMessage("PageStyle:StyleSheets", { + filteredStyleSheets: filteredStyleSheets, + authorStyleDisabled: this.markupDocumentViewer.authorStyleDisabled, + preferredStyleSheetSet: content.document.preferredStyleSheetSet + }); + }, + + getAllStyleSheets: function(frameset = content) { + let selfSheets = Array.slice(frameset.document.styleSheets); + let subSheets = Array.map(frameset.frames, frame => this.getAllStyleSheets(frame)); + return selfSheets.concat(...subSheets); + }, + + receiveMessage: function(msg) { + switch (msg.name) { + case "PageStyle:Switch": + this.markupDocumentViewer.authorStyleDisabled = false; + this._stylesheetSwitchAll(content, msg.data.title); + break; + + case "PageStyle:Disable": + this.markupDocumentViewer.authorStyleDisabled = true; + break; + } + + this.sendStyleSheetInfo(); + }, + + _stylesheetSwitchAll: function (frameset, title) { + if (!title || this._stylesheetInFrame(frameset, title)) { + this._stylesheetSwitchFrame(frameset, title); + } + + for (let i = 0; i < frameset.frames.length; i++) { + // Recurse into sub-frames. + this._stylesheetSwitchAll(frameset.frames[i], title); + } + }, + + _stylesheetSwitchFrame: function (frame, title) { + var docStyleSheets = frame.document.styleSheets; + + for (let i = 0; i < docStyleSheets.length; ++i) { + let docStyleSheet = docStyleSheets[i]; + if (docStyleSheet.title) { + docStyleSheet.disabled = (docStyleSheet.title != title); + } else if (docStyleSheet.disabled) { + docStyleSheet.disabled = false; + } + } + }, + + _stylesheetInFrame: function (frame, title) { + return Array.some(frame.document.styleSheets, (styleSheet) => styleSheet.title == title); + }, + + _filterStyleSheets: function(styleSheets) { + let result = []; + + for (let currentStyleSheet of styleSheets) { + if (!currentStyleSheet.title) + continue; + + // Skip any stylesheets that don't match the screen media type. + if (currentStyleSheet.media.length > 0) { + let mediaQueryList = currentStyleSheet.media.mediaText; + if (!content.matchMedia(mediaQueryList).matches) { + continue; + } + } + + let URI; + try { + if (!currentStyleSheet.ownerNode || + // special-case style nodes, which have no href + currentStyleSheet.ownerNode.nodeName.toLowerCase() != "style") { + URI = Services.io.newURI(currentStyleSheet.href, null, null); + } + } catch (e) { + if (e.result != Cr.NS_ERROR_MALFORMED_URI) { + throw e; + } + continue; + } + + // We won't send data URIs all of the way up to the parent, as these + // can be arbitrarily large. + let sentURI = (!URI || URI.scheme == "data") ? null : URI.spec; + + result.push({ + title: currentStyleSheet.title, + disabled: currentStyleSheet.disabled, + href: sentURI, + }); + } + + return result; + }, +}; +PageStyleHandler.init(); + +// Keep a reference to the translation content handler to avoid it it being GC'ed. +var trHandler = null; +if (Services.prefs.getBoolPref("browser.translation.detectLanguage")) { + Cu.import("resource:///modules/translation/TranslationContentHandler.jsm"); + trHandler = new TranslationContentHandler(global, docShell); +} + +function gKeywordURIFixup(fixupInfo) { + fixupInfo.QueryInterface(Ci.nsIURIFixupInfo); + if (!fixupInfo.consumer) { + return; + } + + // Ignore info from other docshells + let parent = fixupInfo.consumer.QueryInterface(Ci.nsIDocShellTreeItem).sameTypeRootTreeItem; + if (parent != docShell) + return; + + let data = {}; + for (let f of Object.keys(fixupInfo)) { + if (f == "consumer" || typeof fixupInfo[f] == "function") + continue; + + if (fixupInfo[f] && fixupInfo[f] instanceof Ci.nsIURI) { + data[f] = fixupInfo[f].spec; + } else { + data[f] = fixupInfo[f]; + } + } + + sendAsyncMessage("Browser:URIFixup", data); +} +Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup", false); +addEventListener("unload", () => { + Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup"); +}, false); + +addMessageListener("Browser:AppTab", function(message) { + if (docShell) { + docShell.isAppTab = message.data.isAppTab; + } +}); + +var WebBrowserChrome = { + 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 (!E10SUtils.shouldLoadURI(aDocShell, aURI, aReferrer)) { + E10SUtils.redirectLoad(aDocShell, aURI, aReferrer); + return false; + } + + return true; + }, + + // Try to reload the currently active or currently loading page in a new process. + reloadInFreshProcess: function(aDocShell, aURI, aReferrer) { + E10SUtils.redirectLoad(aDocShell, aURI, aReferrer, true); + return true; + } +}; + +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsITabChild); + tabchild.webBrowserChrome = WebBrowserChrome; +} + + +var DOMFullscreenHandler = { + + init: function() { + addMessageListener("DOMFullscreen:Entered", this); + addMessageListener("DOMFullscreen:CleanUp", this); + addEventListener("MozDOMFullscreen:Request", this); + addEventListener("MozDOMFullscreen:Entered", this); + addEventListener("MozDOMFullscreen:NewOrigin", this); + addEventListener("MozDOMFullscreen:Exit", this); + addEventListener("MozDOMFullscreen:Exited", this); + }, + + get _windowUtils() { + if (!content) { + return null; + } + return content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + }, + + receiveMessage: function(aMessage) { + let windowUtils = this._windowUtils; + switch (aMessage.name) { + case "DOMFullscreen:Entered": { + this._lastTransactionId = windowUtils.lastTransactionId; + if (!windowUtils.handleFullscreenRequests() && + !content.document.fullscreenElement) { + // If we don't actually have any pending fullscreen request + // to handle, neither we have been in fullscreen, tell the + // parent to just exit. + sendAsyncMessage("DOMFullscreen:Exit"); + } + break; + } + case "DOMFullscreen:CleanUp": { + // If we've exited fullscreen at this point, no need to record + // transaction id or call exit fullscreen. This is especially + // important for non-e10s, since in that case, it is possible + // that no more paint would be triggered after this point. + if (content.document.fullscreenElement && windowUtils) { + this._lastTransactionId = windowUtils.lastTransactionId; + windowUtils.exitFullscreen(); + } + break; + } + } + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "MozDOMFullscreen:Request": { + sendAsyncMessage("DOMFullscreen:Request"); + break; + } + case "MozDOMFullscreen:NewOrigin": { + sendAsyncMessage("DOMFullscreen:NewOrigin", { + originNoSuffix: aEvent.target.nodePrincipal.originNoSuffix, + }); + break; + } + case "MozDOMFullscreen:Exit": { + sendAsyncMessage("DOMFullscreen:Exit"); + break; + } + case "MozDOMFullscreen:Entered": + case "MozDOMFullscreen:Exited": { + addEventListener("MozAfterPaint", this); + if (!content || !content.document.fullscreenElement) { + // If we receive any fullscreen change event, and find we are + // actually not in fullscreen, also ask the parent to exit to + // ensure that the parent always exits fullscreen when we do. + sendAsyncMessage("DOMFullscreen:Exit"); + } + break; + } + case "MozAfterPaint": { + // Only send Painted signal after we actually finish painting + // the transition for the fullscreen change. + // Note that this._lastTransactionId is not set when in non-e10s + // mode, so we need to check that explicitly. + if (!this._lastTransactionId || + aEvent.transactionId > this._lastTransactionId) { + removeEventListener("MozAfterPaint", this); + sendAsyncMessage("DOMFullscreen:Painted"); + } + break; + } + } + } +}; +DOMFullscreenHandler.init(); + +var RefreshBlocker = { + PREF: "accessibility.blockautorefresh", + + // Bug 1247100 - When a refresh is caused by an HTTP header, + // onRefreshAttempted will be fired before onLocationChange. + // When a refresh is caused by a <meta> tag in the document, + // onRefreshAttempted will be fired after onLocationChange. + // + // We only ever want to send a message to the parent after + // onLocationChange has fired, since the parent uses the + // onLocationChange update to clear transient notifications. + // Sending the message before onLocationChange will result in + // us creating the notification, and then clearing it very + // soon after. + // + // To account for both cases (onRefreshAttempted before + // onLocationChange, and onRefreshAttempted after onLocationChange), + // we'll hold a mapping of DOM Windows that we see get + // sent through both onLocationChange and onRefreshAttempted. + // When either run, they'll check the WeakMap for the existence + // of the DOM Window. If it doesn't exist, it'll add it. If + // it finds it, it'll know that it's safe to send the message + // to the parent, since we know that both have fired. + // + // The DOM Window is removed from blockedWindows when we notice + // the nsIWebProgress change state to STATE_STOP for the + // STATE_IS_WINDOW case. + // + // DOM Windows are mapped to a JS object that contains the data + // to be sent to the parent to show the notification. Since that + // data is only known when onRefreshAttempted is fired, it's only + // ever stashed in the map if onRefreshAttempted fires first - + // otherwise, null is set as the value of the mapping. + blockedWindows: new WeakMap(), + + init() { + if (Services.prefs.getBoolPref(this.PREF)) { + this.enable(); + } + + Services.prefs.addObserver(this.PREF, this, false); + }, + + uninit() { + if (Services.prefs.getBoolPref(this.PREF)) { + this.disable(); + } + + Services.prefs.removeObserver(this.PREF, this); + }, + + observe(subject, topic, data) { + if (topic == "nsPref:changed" && data == this.PREF) { + if (Services.prefs.getBoolPref(this.PREF)) { + this.enable(); + } else { + this.disable(); + } + } + }, + + enable() { + this._filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + this._filter.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_ALL); + + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener(this._filter, Ci.nsIWebProgress.NOTIFY_ALL); + + addMessageListener("RefreshBlocker:Refresh", this); + }, + + disable() { + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.removeProgressListener(this._filter); + + this._filter.removeProgressListener(this); + this._filter = null; + + removeMessageListener("RefreshBlocker:Refresh", this); + }, + + send(data) { + sendAsyncMessage("RefreshBlocker:Blocked", data); + }, + + /** + * Notices when the nsIWebProgress transitions to STATE_STOP for + * the STATE_IS_WINDOW case, which will clear any mappings from + * blockedWindows. + */ + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + this.blockedWindows.delete(aWebProgress.DOMWindow); + } + }, + + /** + * Notices when the location has changed. If, when running, + * onRefreshAttempted has already fired for this DOM Window, will + * send the appropriate refresh blocked data to the parent. + */ + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + let win = aWebProgress.DOMWindow; + if (this.blockedWindows.has(win)) { + let data = this.blockedWindows.get(win); + if (data) { + // We saw onRefreshAttempted before onLocationChange, so + // send the message to the parent to show the notification. + this.send(data); + } + } else { + this.blockedWindows.set(win, null); + } + }, + + /** + * Notices when a refresh / reload was attempted. If, when running, + * onLocationChange has not yet run, will stash the appropriate data + * into the blockedWindows map to be sent when onLocationChange fires. + */ + onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) { + let win = aWebProgress.DOMWindow; + let outerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + + let data = { + URI: aURI.spec, + originCharset: aURI.originCharset, + delay: aDelay, + sameURI: aSameURI, + outerWindowID, + }; + + if (this.blockedWindows.has(win)) { + // onLocationChange must have fired before, so we can tell the + // parent to show the notification. + this.send(data); + } else { + // onLocationChange hasn't fired yet, so stash the data in the + // map so that onLocationChange can send it when it fires. + this.blockedWindows.set(win, data); + } + + return false; + }, + + receiveMessage(message) { + let data = message.data; + + if (message.name == "RefreshBlocker:Refresh") { + let win = Services.wm.getOuterWindowWithId(data.outerWindowID); + let refreshURI = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIRefreshURI); + + let URI = BrowserUtils.makeURI(data.URI, data.originCharset, null); + + refreshURI.forceRefreshURI(URI, data.delay, true); + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener2, + Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference, + Ci.nsISupports]), +}; + +RefreshBlocker.init(); + +var UserContextIdNotifier = { + init() { + addEventListener("DOMWindowCreated", this); + }, + + uninit() { + removeEventListener("DOMWindowCreated", this); + }, + + handleEvent(aEvent) { + // When the window is created, we want to inform the tabbrowser about + // the userContextId in use in order to update the UI correctly. + // Just because we cannot change the userContextId from an active docShell, + // we don't need to check DOMContentLoaded again. + this.uninit(); + + // We use the docShell because content.document can have been loaded before + // setting the originAttributes. + let loadContext = docShell.QueryInterface(Ci.nsILoadContext); + let userContextId = loadContext.originAttributes.userContextId; + + sendAsyncMessage("Browser:WindowCreated", { userContextId }); + } +}; + +UserContextIdNotifier.init(); + +#ifdef MOZ_WEBEXTENSIONS +ExtensionContent.init(this); +addEventListener("unload", () => { + ExtensionContent.uninit(this); + RefreshBlocker.uninit(); +}); +#endif + +addMessageListener("AllowScriptsToClose", () => { + content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .allowScriptsToClose(); +}); + +addEventListener("MozAfterPaint", function onFirstPaint() { + removeEventListener("MozAfterPaint", onFirstPaint); + sendAsyncMessage("Browser:FirstPaint"); +}); diff --git a/application/basilisk/base/content/tab-shape.inc.svg b/application/basilisk/base/content/tab-shape.inc.svg new file mode 100644 index 000000000..f97889389 --- /dev/null +++ b/application/basilisk/base/content/tab-shape.inc.svg @@ -0,0 +1,11 @@ +<!-- 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/. --> + +<svg:clipPath id="tab-curve-clip-path-start" clipPathUnits="objectBoundingBox"> + <svg:path d="m 1,0.0625 0.05,0 0,0.938 -1,0 0,-0.028 C 0.32082458,0.95840561 0.4353096,0.81970962 0.48499998,0.5625 0.51819998,0.3905 0.535,0.0659 1,0.0625 z"/> +</svg:clipPath> + +<svg:clipPath id="tab-curve-clip-path-end" clipPathUnits="objectBoundingBox"> + <svg:path d="m 0,0.0625 -0.05,0 0,0.938 1,0 0,-0.028 C 0.67917542,0.95840561 0.56569036,0.81970962 0.51599998,0.5625 0.48279998,0.3905 0.465,0.0659 0,0.0625 z"/> +</svg:clipPath> diff --git a/application/basilisk/base/content/tabbrowser.css b/application/basilisk/base/content/tabbrowser.css new file mode 100644 index 000000000..9085304f6 --- /dev/null +++ b/application/basilisk/base/content/tabbrowser.css @@ -0,0 +1,98 @@ +/* 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/. */ + +.tabbrowser-tabbox { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabbox"); +} + +.tabbrowser-tabpanels { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabpanels"); +} + +.tabbrowser-arrowscrollbox { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-arrowscrollbox"); +} + +.tab-close-button { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-close-tab-button"); +} + +.tab-close-button[pinned], +.tabbrowser-tabs[closebuttons="activetab"] > * > * > * > .tab-close-button:not([selected="true"]), +.tab-icon-image:not([src]):not([pinned]):not([crashed])[selected], +.tab-icon-image:not([src]):not([pinned]):not([crashed]):not([sharing]), +.tab-icon-image[busy], +.tab-throbber:not([busy]), +.tab-icon-sound:not([soundplaying]):not([muted]):not([blocked]), +.tab-icon-sound[pinned], +.tab-sharing-icon-overlay, +.tab-icon-overlay { + display: none; +} + +.tab-sharing-icon-overlay[sharing]:not([selected]), +.tab-icon-overlay[soundplaying][pinned], +.tab-icon-overlay[muted][pinned], +.tab-icon-overlay[blocked][pinned], +.tab-icon-overlay[crashed] { + display: -moz-box; +} + +.tab-label[pinned] { + width: 0; + margin-left: 0 !important; + margin-right: 0 !important; + padding-left: 0 !important; + padding-right: 0 !important; +} + +.tab-stack { + vertical-align: top; /* for pinned tabs */ +} + +tabpanels { + background-color: transparent; +} + +.tab-drop-indicator { + position: relative; + z-index: 2; +} + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + .tab-icon-image { + image-rendering: -moz-crisp-edges; + } +} + +.closing-tabs-spacer { + pointer-events: none; +} + +.tabbrowser-tabs:not(:hover) > .tabbrowser-arrowscrollbox > .closing-tabs-spacer { + transition: width .15s ease-out; +} + +/** + * Optimization for tabs that are restored lazily. We can save a good amount of + * memory that to-be-restored tabs would otherwise consume simply by setting + * their browsers to 'display: none' as that will prevent them from having to + * create a presentation and the like. + */ +browser[pending] { + display: none; +} + +browser[pendingpaint] { + opacity: 0; +} + +tabbrowser[pendingpaint] { + background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png); + background-repeat: no-repeat; + background-position: center center; + background-color: #f9f9f9 !important; + background-size: 30px; +} diff --git a/application/basilisk/base/content/tabbrowser.xml b/application/basilisk/base/content/tabbrowser.xml new file mode 100644 index 000000000..463e74a52 --- /dev/null +++ b/application/basilisk/base/content/tabbrowser.xml @@ -0,0 +1,7389 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<bindings id="tabBrowserBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="tabbrowser"> + <resources> + <stylesheet src="chrome://browser/content/tabbrowser.css"/> + </resources> + + <content> + <xul:stringbundle anonid="tbstringbundle" src="chrome://browser/locale/tabbrowser.properties"/> + <xul:tabbox anonid="tabbox" class="tabbrowser-tabbox" + flex="1" eventnode="document" xbl:inherits="handleCtrlPageUpDown" + onselect="if (event.target.localName == 'tabpanels') this.parentNode.updateCurrentBrowser();"> + <xul:tabpanels flex="1" class="plain" selectedIndex="0" anonid="panelcontainer"> + <xul:notificationbox flex="1" notificationside="top"> + <xul:hbox flex="1" class="browserSidebarContainer"> + <xul:vbox flex="1" class="browserContainer"> + <xul:stack flex="1" class="browserStack" anonid="browserStack"> + <xul:browser anonid="initialBrowser" type="content-primary" message="true" messagemanagergroup="browsers" + xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,selectmenulist,datetimepicker"/> + </xul:stack> + </xul:vbox> + </xul:hbox> + </xul:notificationbox> + </xul:tabpanels> + </xul:tabbox> + <children/> + </content> + <implementation implements="nsIDOMEventListener, nsIMessageListener, nsIObserver"> + + <property name="tabContextMenu" readonly="true" + onget="return this.tabContainer.contextMenu;"/> + + <field name="tabContainer" readonly="true"> + document.getElementById(this.getAttribute("tabcontainer")); + </field> + <field name="tabs" readonly="true"> + this.tabContainer.childNodes; + </field> + + <property name="visibleTabs" readonly="true"> + <getter><![CDATA[ + if (!this._visibleTabs) + this._visibleTabs = Array.filter(this.tabs, + tab => !tab.hidden && !tab.closing); + return this._visibleTabs; + ]]></getter> + </property> + + <field name="closingTabsEnum" readonly="true">({ ALL: 0, OTHER: 1, TO_END: 2 });</field> + + <field name="_visibleTabs">null</field> + + <field name="mURIFixup" readonly="true"> + Components.classes["@mozilla.org/docshell/urifixup;1"] + .getService(Components.interfaces.nsIURIFixup); + </field> + <field name="_unifiedComplete" readonly="true"> + Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] + .getService(Components.interfaces.mozIPlacesAutoComplete); + </field> + <field name="AppConstants" readonly="true"> + (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants; + </field> + <field name="mTabBox" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "tabbox"); + </field> + <field name="mPanelContainer" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer"); + </field> + <field name="mStringBundle"> + document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle"); + </field> + <field name="mCurrentTab"> + null + </field> + <field name="_lastRelatedTab"> + null + </field> + <field name="mCurrentBrowser"> + null + </field> + <field name="mProgressListeners"> + [] + </field> + <field name="mActiveResizeDisplayportSuppression"> + null + </field> + <field name="mTabsProgressListeners"> + [] + </field> + <field name="_tabListeners"> + new Map() + </field> + <field name="_tabFilters"> + new Map() + </field> + <field name="mIsBusy"> + false + </field> + <field name="_outerWindowIDBrowserMap"> + new Map(); + </field> + <field name="arrowKeysShouldWrap" readonly="true"> + this.AppConstants.platform == "macosx"; + </field> + + <field name="_autoScrollPopup"> + null + </field> + + <field name="_previewMode"> + false + </field> + + <field name="_lastFindValue"> + "" + </field> + + <field name="_contentWaitingCount"> + 0 + </field> + + <property name="_numPinnedTabs" readonly="true"> + <getter><![CDATA[ + for (var i = 0; i < this.tabs.length; i++) { + if (!this.tabs[i].pinned) + break; + } + return i; + ]]></getter> + </property> + + <property name="popupAnchor" readonly="true"> + <getter><![CDATA[ + if (this.mCurrentTab._popupAnchor) { + return this.mCurrentTab._popupAnchor; + } + let stack = this.mCurrentBrowser.parentNode; + // Create an anchor for the popup + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let popupAnchor = document.createElementNS(NS_XUL, "hbox"); + popupAnchor.className = "popup-anchor"; + popupAnchor.hidden = true; + stack.appendChild(popupAnchor); + return this.mCurrentTab._popupAnchor = popupAnchor; + ]]></getter> + </property> + + <method name="isFindBarInitialized"> + <parameter name="aTab"/> + <body><![CDATA[ + return (aTab || this.selectedTab)._findBar != undefined; + ]]></body> + </method> + + <method name="getFindBar"> + <parameter name="aTab"/> + <body><![CDATA[ + if (!aTab) + aTab = this.selectedTab; + + if (aTab._findBar) + return aTab._findBar; + + let findBar = document.createElementNS(this.namespaceURI, "findbar"); + let browser = this.getBrowserForTab(aTab); + let browserContainer = this.getBrowserContainer(browser); + browserContainer.appendChild(findBar); + + // Force a style flush to ensure that our binding is attached. + findBar.clientTop; + + findBar.browser = browser; + findBar._findField.value = this._lastFindValue; + + aTab._findBar = findBar; + + let event = document.createEvent("Events"); + event.initEvent("TabFindInitialized", true, false); + aTab.dispatchEvent(event); + + return findBar; + ]]></body> + </method> + + <method name="getStatusPanel"> + <body><![CDATA[ + if (!this._statusPanel) { + this._statusPanel = document.createElementNS(this.namespaceURI, "statuspanel"); + this._statusPanel.setAttribute("inactive", "true"); + this._statusPanel.setAttribute("layer", "true"); + this._appendStatusPanel(); + } + return this._statusPanel; + ]]></body> + </method> + + <method name="_appendStatusPanel"> + <body><![CDATA[ + if (this._statusPanel) { + let browser = this.selectedBrowser; + let browserContainer = this.getBrowserContainer(browser); + browserContainer.insertBefore(this._statusPanel, browser.parentNode.nextSibling); + } + ]]></body> + </method> + + <method name="_setCloseKeyState"> + <parameter name="aEnabled"/> + <body><![CDATA[ + let keyClose = document.getElementById("key_close"); + let closeKeyEnabled = keyClose.getAttribute("disabled") != "true"; + if (closeKeyEnabled == aEnabled) + return; + + if (aEnabled) + keyClose.removeAttribute("disabled"); + else + keyClose.setAttribute("disabled", "true"); + + // We also want to remove the keyboard shortcut from the file menu + // when the shortcut is disabled, and bring it back when it's + // renabled. + // + // Fixing bug 630826 could make that happen automatically. + // Fixing bug 630830 could avoid the ugly hack below. + + let closeMenuItem = document.getElementById("menu_close"); + let parentPopup = closeMenuItem.parentNode; + let nextItem = closeMenuItem.nextSibling; + let clonedItem = closeMenuItem.cloneNode(true); + + parentPopup.removeChild(closeMenuItem); + + if (aEnabled) + clonedItem.setAttribute("key", "key_close"); + else + clonedItem.removeAttribute("key"); + + parentPopup.insertBefore(clonedItem, nextItem); + ]]></body> + </method> + + <method name="pinTab"> + <parameter name="aTab"/> + <body><![CDATA[ + if (aTab.pinned) + return; + + if (aTab.hidden) + this.showTab(aTab); + + this.moveTabTo(aTab, this._numPinnedTabs); + aTab.setAttribute("pinned", "true"); + this.tabContainer._unlockTabSizing(); + this.tabContainer._positionPinnedTabs(); + this.tabContainer.adjustTabstrip(); + + this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: true }) + + if (aTab.selected) + this._setCloseKeyState(false); + + let event = document.createEvent("Events"); + event.initEvent("TabPinned", true, false); + aTab.dispatchEvent(event); + ]]></body> + </method> + + <method name="unpinTab"> + <parameter name="aTab"/> + <body><![CDATA[ + if (!aTab.pinned) + return; + + this.moveTabTo(aTab, this._numPinnedTabs - 1); + aTab.removeAttribute("pinned"); + aTab.style.marginInlineStart = ""; + this.tabContainer._unlockTabSizing(); + this.tabContainer._positionPinnedTabs(); + this.tabContainer.adjustTabstrip(); + + this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: false }) + + if (aTab.selected) + this._setCloseKeyState(true); + + let event = document.createEvent("Events"); + event.initEvent("TabUnpinned", true, false); + aTab.dispatchEvent(event); + ]]></body> + </method> + + <method name="previewTab"> + <parameter name="aTab"/> + <parameter name="aCallback"/> + <body> + <![CDATA[ + let currentTab = this.selectedTab; + try { + // Suppress focus, ownership and selected tab changes + this._previewMode = true; + this.selectedTab = aTab; + aCallback(); + } finally { + this.selectedTab = currentTab; + this._previewMode = false; + } + ]]> + </body> + </method> + + <method name="getBrowserAtIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + return this.browsers[aIndex]; + ]]> + </body> + </method> + + <method name="getBrowserIndexForDocument"> + <parameter name="aDocument"/> + <body> + <![CDATA[ + var tab = this._getTabForContentWindow(aDocument.defaultView); + return tab ? tab._tPos : -1; + ]]> + </body> + </method> + + <method name="getBrowserForDocument"> + <parameter name="aDocument"/> + <body> + <![CDATA[ + var tab = this._getTabForContentWindow(aDocument.defaultView); + return tab ? tab.linkedBrowser : null; + ]]> + </body> + </method> + + <method name="getBrowserForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + var tab = this._getTabForContentWindow(aWindow); + return tab ? tab.linkedBrowser : null; + ]]> + </body> + </method> + + <method name="getBrowserForOuterWindowID"> + <parameter name="aID"/> + <body> + <![CDATA[ + return this._outerWindowIDBrowserMap.get(aID); + ]]> + </body> + </method> + + <method name="_getTabForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + // When not using remote browsers, we can take a fast path by getting + // directly from the content window to the browser without looping + // over all browsers. + if (!gMultiProcessBrowser) { + let browser = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + return this.getTabForBrowser(browser); + } + + for (let i = 0; i < this.browsers.length; i++) { + // NB: We use contentWindowAsCPOW so that this code works both + // for remote browsers as well. aWindow may be a CPOW. + if (this.browsers[i].contentWindowAsCPOW == aWindow) + return this.tabs[i]; + } + return null; + ]]> + </body> + </method> + + <!-- Binding from browser to tab --> + <field name="_tabForBrowser" readonly="true"> + <![CDATA[ + new WeakMap(); + ]]> + </field> + + <method name="_getTabForBrowser"> + <parameter name="aBrowser" /> + <body> + <![CDATA[ + let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated; + let text = "_getTabForBrowser` is now deprecated, please use `getTabForBrowser"; + let url = "https://developer.mozilla.org/docs/Mozilla/Tech/XUL/Method/getTabForBrowser"; + Deprecated.warning(text, url); + return this.getTabForBrowser(aBrowser); + ]]> + </body> + </method> + + <method name="getTabForBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return this._tabForBrowser.get(aBrowser); + ]]> + </body> + </method> + + <method name="getNotificationBox"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return this.getSidebarContainer(aBrowser).parentNode; + ]]> + </body> + </method> + + <method name="getSidebarContainer"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return this.getBrowserContainer(aBrowser).parentNode; + ]]> + </body> + </method> + + <method name="getBrowserContainer"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return (aBrowser || this.mCurrentBrowser).parentNode.parentNode; + ]]> + </body> + </method> + + <method name="getTabModalPromptBox"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + let browser = (aBrowser || this.mCurrentBrowser); + if (!browser.tabModalPromptBox) { + browser.tabModalPromptBox = new TabModalPromptBox(browser); + } + return browser.tabModalPromptBox; + ]]> + </body> + </method> + + <method name="getTabFromAudioEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") || + !aEvent.isTrusted) { + return null; + } + + var browser = aEvent.originalTarget; + var tab = this.getTabForBrowser(browser); + return tab; + ]]> + </body> + </method> + + <method name="_callProgressListeners"> + <parameter name="aBrowser"/> + <parameter name="aMethod"/> + <parameter name="aArguments"/> + <parameter name="aCallGlobalListeners"/> + <parameter name="aCallTabsListeners"/> + <body><![CDATA[ + var rv = true; + + function callListeners(listeners, args) { + for (let p of listeners) { + if (aMethod in p) { + try { + if (!p[aMethod].apply(p, args)) + rv = false; + } catch (e) { + // don't inhibit other listeners + Components.utils.reportError(e); + } + } + } + } + + if (!aBrowser) + aBrowser = this.mCurrentBrowser; + + if (aCallGlobalListeners != false && + aBrowser == this.mCurrentBrowser) { + callListeners(this.mProgressListeners, aArguments); + } + + if (aCallTabsListeners != false) { + aArguments.unshift(aBrowser); + + callListeners(this.mTabsProgressListeners, aArguments); + } + + return rv; + ]]></body> + </method> + + <!-- A web progress listener object definition for a given tab. --> + <method name="mTabProgressListener"> + <parameter name="aTab"/> + <parameter name="aBrowser"/> + <parameter name="aStartsBlank"/> + <parameter name="aWasPreloadedBrowser"/> + <parameter name="aOrigStateFlags"/> + <body> + <![CDATA[ + let stateFlags = aOrigStateFlags || 0; + // Initialize mStateFlags to non-zero e.g. when creating a progress + // listener for preloaded browsers as there was no progress listener + // around when the content started loading. If the content didn't + // quite finish loading yet, mStateFlags will very soon be overridden + // with the correct value and end up at STATE_STOP again. + if (aWasPreloadedBrowser) { + stateFlags = Ci.nsIWebProgressListener.STATE_STOP | + Ci.nsIWebProgressListener.STATE_IS_REQUEST; + } + + return ({ + mTabBrowser: this, + mTab: aTab, + mBrowser: aBrowser, + mBlank: aStartsBlank, + + // cache flags for correct status UI update after tab switching + mStateFlags: stateFlags, + mStatus: 0, + mMessage: "", + mTotalProgress: 0, + + // count of open requests (should always be 0 or 1) + mRequestCount: 0, + + destroy: function () { + delete this.mTab; + delete this.mBrowser; + delete this.mTabBrowser; + }, + + _callProgressListeners: function () { + Array.unshift(arguments, this.mBrowser); + return this.mTabBrowser._callProgressListeners.apply(this.mTabBrowser, arguments); + }, + + _shouldShowProgress: function (aRequest) { + if (this.mBlank) + return false; + + // Don't show progress indicators in tabs for about: URIs + // pointing to local resources. + if ((aRequest instanceof Ci.nsIChannel) && + aRequest.originalURI.schemeIs("about") && + (aRequest.URI.schemeIs("jar") || aRequest.URI.schemeIs("file"))) + return false; + + return true; + }, + + _isForInitialAboutBlank: function (aWebProgress, aLocation) { + if (!this.mBlank || !aWebProgress.isTopLevel) { + return false; + } + + let location = aLocation ? aLocation.spec : ""; + return location == "about:blank"; + }, + + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0; + + if (!this._shouldShowProgress(aRequest)) + return; + + if (this.mTotalProgress) + this.mTab.setAttribute("progress", "true"); + + this._callProgressListeners("onProgressChange", + [aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress]); + }, + + onProgressChange64: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + return this.onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress); + }, + + onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { + if (!aRequest) + return; + + const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + const nsIChannel = Components.interfaces.nsIChannel; + let location, originalLocation; + try { + aRequest.QueryInterface(nsIChannel) + location = aRequest.URI; + originalLocation = aRequest.originalURI; + } catch (ex) {} + + let ignoreBlank = this._isForInitialAboutBlank(aWebProgress, location); + // If we were ignoring some messages about the initial about:blank, and we + // got the STATE_STOP for it, we'll want to pay attention to those messages + // from here forward. Similarly, if we conclude that this state change + // is one that we shouldn't be ignoring, then stop ignoring. + if ((ignoreBlank && + aStateFlags & nsIWebProgressListener.STATE_STOP && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) || + !ignoreBlank && this.mBlank) { + this.mBlank = false; + } + + if (aStateFlags & nsIWebProgressListener.STATE_START) { + this.mRequestCount++; + } + else if (aStateFlags & nsIWebProgressListener.STATE_STOP) { + const NS_ERROR_UNKNOWN_HOST = 2152398878; + if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) { + // to prevent bug 235825: wait for the request handled + // by the automatic keyword resolver + return; + } + // since we (try to) only handle STATE_STOP of the last request, + // the count of open requests should now be 0 + this.mRequestCount = 0; + } + + if (aStateFlags & nsIWebProgressListener.STATE_START && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + if (aWebProgress.isTopLevel) { + // Need to use originalLocation rather than location because things + // like about:home and about:privatebrowsing arrive with nsIRequest + // pointing to their resolved jar: or file: URIs. + if (!(originalLocation && gInitialPages.includes(originalLocation.spec) && + originalLocation != "about:blank" && + this.mBrowser.initialPageLoadedFromURLBar != originalLocation.spec && + this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) { + // Indicating that we started a load will allow the location + // bar to be cleared when the load finishes. + // In order to not overwrite user-typed content, we avoid it + // (see if condition above) in a very specific case: + // If the load is of an 'initial' page (e.g. about:privatebrowsing, + // about:newtab, etc.), was not explicitly typed in the location + // bar by the user, is not about:blank (because about:blank can be + // loaded by websites under their principal), and the current + // page in the browser is about:blank (indicating it is a newly + // created or re-created browser, e.g. because it just switched + // remoteness or is a new tab/window). + this.mBrowser.urlbarChangeTracker.startedLoad(); + } + delete this.mBrowser.initialPageLoadedFromURLBar; + // If the browser is loading it must not be crashed anymore + this.mTab.removeAttribute("crashed"); + } + + if (this._shouldShowProgress(aRequest)) { + if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { + this.mTab.setAttribute("busy", "true"); + + if (aWebProgress.isTopLevel && + !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD)) + this.mTabBrowser.setTabTitleLoading(this.mTab); + } + + if (this.mTab.selected) + this.mTabBrowser.mIsBusy = true; + } + } + else if (aStateFlags & nsIWebProgressListener.STATE_STOP && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + + if (this.mTab.hasAttribute("busy")) { + this.mTab.removeAttribute("busy"); + this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]); + if (!this.mTab.selected) + this.mTab.setAttribute("unread", "true"); + } + this.mTab.removeAttribute("progress"); + + if (aWebProgress.isTopLevel) { + let isSuccessful = Components.isSuccessCode(aStatus); + if (!isSuccessful && !isTabEmpty(this.mTab)) { + // Restore the current document's location in case the + // request was stopped (possibly from a content script) + // before the location changed. + + this.mBrowser.userTypedValue = null; + + let inLoadURI = this.mBrowser.inLoadURI; + if (this.mTab.selected && gURLBar && !inLoadURI) { + URLBarSetURI(); + } + } else if (isSuccessful) { + this.mBrowser.urlbarChangeTracker.finishedLoad(); + } + + if (!this.mBrowser.mIconURL) + this.mTabBrowser.useDefaultIcon(this.mTab); + } + + // For keyword URIs clear the user typed value since they will be changed into real URIs + if (location.scheme == "keyword") + this.mBrowser.userTypedValue = null; + + if (this.mTab.label == this.mTabBrowser.mStringBundle.getString("tabs.connecting")) + this.mTabBrowser.setTabTitle(this.mTab); + + if (this.mTab.selected) + this.mTabBrowser.mIsBusy = false; + } + + if (ignoreBlank) { + this._callProgressListeners("onUpdateCurrentBrowser", + [aStateFlags, aStatus, "", 0], + true, false); + } else { + this._callProgressListeners("onStateChange", + [aWebProgress, aRequest, aStateFlags, aStatus], + true, false); + } + + this._callProgressListeners("onStateChange", + [aWebProgress, aRequest, aStateFlags, aStatus], + false); + + if (aStateFlags & (nsIWebProgressListener.STATE_START | + nsIWebProgressListener.STATE_STOP)) { + // reset cached temporary values at beginning and end + this.mMessage = ""; + this.mTotalProgress = 0; + } + this.mStateFlags = aStateFlags; + this.mStatus = aStatus; + }, + + onLocationChange: function (aWebProgress, aRequest, aLocation, + aFlags) { + // OnLocationChange is called for both the top-level content + // and the subframes. + let topLevel = aWebProgress.isTopLevel; + + if (topLevel) { + let isSameDocument = + !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); + // We need to clear the typed value + // if the document failed to load, to make sure the urlbar reflects the + // failed URI (particularly for SSL errors). However, don't clear the value + // if the error page's URI is about:blank, because that causes complete + // loss of urlbar contents for invalid URI errors (see bug 867957). + // Another reason to clear the userTypedValue is if this was an anchor + // navigation initiated by the user. + if (this.mBrowser.didStartLoadSinceLastUserTyping() || + ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) && + aLocation.spec != "about:blank") || + (isSameDocument && this.mBrowser.inLoadURI)) { + this.mBrowser.userTypedValue = null; + } + + // If the browser was playing audio, we should remove the playing state. + if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) { + clearTimeout(this.mTab._soundPlayingAttrRemovalTimer); + this.mTab._soundPlayingAttrRemovalTimer = 0; + this.mTab.removeAttribute("soundplaying"); + this.mTabBrowser._tabAttrModified(this.mTab, ["soundplaying"]); + } + + // If the browser was previously muted, we should restore the muted state. + if (this.mTab.hasAttribute("muted")) { + this.mTab.linkedBrowser.mute(); + } + + if (this.mTabBrowser.isFindBarInitialized(this.mTab)) { + let findBar = this.mTabBrowser.getFindBar(this.mTab); + + // Close the Find toolbar if we're in old-style TAF mode + if (findBar.findMode != findBar.FIND_NORMAL) { + findBar.close(); + } + } + + // Don't clear the favicon if this onLocationChange was + // triggered by a pushState or a replaceState (bug 550565) or + // a hash change (bug 408415). + if (aWebProgress.isLoadingDocument && !isSameDocument) { + this.mBrowser.mIconURL = null; + } + + let unifiedComplete = this.mTabBrowser._unifiedComplete; + let userContextId = this.mBrowser.getAttribute("usercontextid") || 0; + if (this.mBrowser.registeredOpenURI) { + unifiedComplete.unregisterOpenPage(this.mBrowser.registeredOpenURI, + userContextId); + delete this.mBrowser.registeredOpenURI; + } + // Tabs in private windows aren't registered as "Open" so + // that they don't appear as switch-to-tab candidates. + if (!isBlankPageURL(aLocation.spec) && + (!PrivateBrowsingUtils.isWindowPrivate(window) || + PrivateBrowsingUtils.permanentPrivateBrowsing)) { + unifiedComplete.registerOpenPage(aLocation, userContextId); + this.mBrowser.registeredOpenURI = aLocation; + } + } + + if (!this.mBlank) { + this._callProgressListeners("onLocationChange", + [aWebProgress, aRequest, aLocation, + aFlags]); + } + + if (topLevel) { + this.mBrowser.lastURI = aLocation; + this.mBrowser.lastLocationChange = Date.now(); + } + }, + + onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) { + if (this.mBlank) + return; + + this._callProgressListeners("onStatusChange", + [aWebProgress, aRequest, aStatus, aMessage]); + + this.mMessage = aMessage; + }, + + onSecurityChange: function (aWebProgress, aRequest, aState) { + this._callProgressListeners("onSecurityChange", + [aWebProgress, aRequest, aState]); + }, + + onRefreshAttempted: function (aWebProgress, aURI, aDelay, aSameURI) { + return this._callProgressListeners("onRefreshAttempted", + [aWebProgress, aURI, aDelay, aSameURI]); + }, + + QueryInterface: function (aIID) { + if (aIID.equals(Components.interfaces.nsIWebProgressListener) || + aIID.equals(Components.interfaces.nsIWebProgressListener2) || + aIID.equals(Components.interfaces.nsISupportsWeakReference) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + } + }); + ]]> + </body> + </method> + + <field name="serializationHelper"> + Cc["@mozilla.org/network/serialization-helper;1"] + .getService(Ci.nsISerializationHelper); + </field> + + <field name="mIconLoadingPrincipal"> + null + </field> + + <method name="setIcon"> + <parameter name="aTab"/> + <parameter name="aURI"/> + <parameter name="aLoadingPrincipal"/> + <body> + <![CDATA[ + let browser = this.getBrowserForTab(aTab); + browser.mIconURL = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + let loadingPrincipal = aLoadingPrincipal + ? aLoadingPrincipal + : Services.scriptSecurityManager.getSystemPrincipal(); + + if (aURI) { + if (!(aURI instanceof Ci.nsIURI)) { + aURI = makeURI(aURI); + } + PlacesUIUtils.loadFavicon(browser, loadingPrincipal, aURI); + } + + let sizedIconUrl = browser.mIconURL || ""; + if (sizedIconUrl != aTab.getAttribute("image")) { + if (sizedIconUrl) { + aTab.setAttribute("image", sizedIconUrl); + if (!browser.mIconLoadingPrincipal || + !browser.mIconLoadingPrincipal.equals(loadingPrincipal)) { + aTab.setAttribute("iconLoadingPrincipal", + this.serializationHelper.serializeToString(loadingPrincipal)); + browser.mIconLoadingPrincipal = loadingPrincipal; + } + } + else { + aTab.removeAttribute("image"); + aTab.removeAttribute("iconLoadingPrincipal"); + delete browser.mIconLoadingPrincipal; + } + this._tabAttrModified(aTab, ["image"]); + } + + this._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]); + ]]> + </body> + </method> + + <method name="getIcon"> + <parameter name="aTab"/> + <body> + <![CDATA[ + let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser; + return browser.mIconURL; + ]]> + </body> + </method> + + <method name="shouldLoadFavIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + return (aURI && + Services.prefs.getBoolPref("browser.chrome.site_icons") && + Services.prefs.getBoolPref("browser.chrome.favicons") && + ("schemeIs" in aURI) && (aURI.schemeIs("http") || aURI.schemeIs("https"))); + ]]> + </body> + </method> + + <method name="useDefaultIcon"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var browser = this.getBrowserForTab(aTab); + var documentURI = browser.documentURI; + var icon = null; + + if (browser.imageDocument) { + if (Services.prefs.getBoolPref("browser.chrome.site_icons")) { + let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size"); + if (browser.imageDocument.width <= sz && + browser.imageDocument.height <= sz) { + icon = browser.currentURI; + } + } + } + + // Use documentURIObject in the check for shouldLoadFavIcon so that we + // do the right thing with about:-style error pages. Bug 453442 + if (!icon && this.shouldLoadFavIcon(documentURI)) { + let url = documentURI.prePath + "/favicon.ico"; + if (!this.isFailedIcon(url)) + icon = url; + } + this.setIcon(aTab, icon, browser.contentPrincipal); + ]]> + </body> + </method> + + <method name="isFailedIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + if (!(aURI instanceof Ci.nsIURI)) + aURI = makeURI(aURI); + return PlacesUtils.favicons.isFailedFavicon(aURI); + ]]> + </body> + </method> + + <method name="getWindowTitleForBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + var newTitle = ""; + var docElement = this.ownerDocument.documentElement; + var sep = docElement.getAttribute("titlemenuseparator"); + + // Strip out any null bytes in the content title, since the + // underlying widget implementations of nsWindow::SetTitle pass + // null-terminated strings to system APIs. + var docTitle = aBrowser.contentTitle.replace(/\0/g, ""); + + if (!docTitle) + docTitle = docElement.getAttribute("titledefault"); + + var modifier = docElement.getAttribute("titlemodifier"); + if (docTitle) { + newTitle += docElement.getAttribute("titlepreface"); + newTitle += docTitle; + if (modifier) + newTitle += sep; + } + newTitle += modifier; + + // If location bar is hidden and the URL type supports a host, + // add the scheme and host to the title to prevent spoofing. + // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239 + try { + if (docElement.getAttribute("chromehidden").includes("location")) { + var uri = this.mURIFixup.createExposableURI( + aBrowser.currentURI); + if (uri.scheme == "about") + newTitle = uri.spec + sep + newTitle; + else + newTitle = uri.prePath + sep + newTitle; + } + } catch (e) {} + + return newTitle; + ]]> + </body> + </method> + + <method name="updateTitlebar"> + <body> + <![CDATA[ + this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser); + ]]> + </body> + </method> + + <!-- Holds a unique ID for the tab change that's currently being timed. + Used to make sure that multiple, rapid tab switches do not try to + create overlapping timers. --> + <field name="_tabSwitchID">null</field> + + <method name="updateCurrentBrowser"> + <parameter name="aForceUpdate"/> + <body> + <![CDATA[ + var newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex); + if (this.mCurrentBrowser == newBrowser && !aForceUpdate) + return; + + if (!aForceUpdate) { + TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS"); + if (!gMultiProcessBrowser) { + // old way of measuring tab paint which is not valid with e10s. + // Waiting until the next MozAfterPaint ensures that we capture + // the time it takes to paint, upload the textures to the compositor, + // and then composite. + if (this._tabSwitchID) { + TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_MS"); + } + + let tabSwitchID = Symbol(); + + TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_MS"); + this._tabSwitchID = tabSwitchID; + + let onMozAfterPaint = () => { + if (this._tabSwitchID === tabSwitchID) { + TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS"); + this._tabSwitchID = null; + } + window.removeEventListener("MozAfterPaint", onMozAfterPaint); + } + window.addEventListener("MozAfterPaint", onMozAfterPaint); + } + } + + var oldTab = this.mCurrentTab; + + // Preview mode should not reset the owner + if (!this._previewMode && !oldTab.selected) + oldTab.owner = null; + + if (this._lastRelatedTab) { + if (!this._lastRelatedTab.selected) + this._lastRelatedTab.owner = null; + this._lastRelatedTab = null; + } + + var oldBrowser = this.mCurrentBrowser; + + if (!gMultiProcessBrowser) { + oldBrowser.setAttribute("type", "content-targetable"); + oldBrowser.docShellIsActive = false; + newBrowser.setAttribute("type", "content-primary"); + newBrowser.docShellIsActive = + (window.windowState != window.STATE_MINIMIZED); + } + + var updateBlockedPopups = false; + if ((oldBrowser.blockedPopups && !newBrowser.blockedPopups) || + (!oldBrowser.blockedPopups && newBrowser.blockedPopups)) + updateBlockedPopups = true; + + this.mCurrentBrowser = newBrowser; + this.mCurrentTab = this.tabContainer.selectedItem; + this.showTab(this.mCurrentTab); + + var forwardButtonContainer = document.getElementById("urlbar-wrapper"); + if (forwardButtonContainer) { + forwardButtonContainer.setAttribute("switchingtabs", "true"); + window.addEventListener("MozAfterPaint", function removeSwitchingtabsAttr() { + window.removeEventListener("MozAfterPaint", removeSwitchingtabsAttr); + forwardButtonContainer.removeAttribute("switchingtabs"); + }); + } + + this._appendStatusPanel(); + + if (updateBlockedPopups) + this.mCurrentBrowser.updateBlockedPopups(); + + // Update the URL bar. + var loc = this.mCurrentBrowser.currentURI; + + var webProgress = this.mCurrentBrowser.webProgress; + var securityUI = this.mCurrentBrowser.securityUI; + + this._callProgressListeners(null, "onLocationChange", + [webProgress, null, loc, 0], true, + false); + + if (securityUI) { + // Include the true final argument to indicate that this event is + // simulated (instead of being observed by the webProgressListener). + this._callProgressListeners(null, "onSecurityChange", + [webProgress, null, securityUI.state, true], + true, false); + } + + var listener = this._tabListeners.get(this.mCurrentTab); + if (listener && listener.mStateFlags) { + this._callProgressListeners(null, "onUpdateCurrentBrowser", + [listener.mStateFlags, listener.mStatus, + listener.mMessage, listener.mTotalProgress], + true, false); + } + + if (!this._previewMode) { + this._recordTabAccess(this.mCurrentTab); + + this.mCurrentTab.updateLastAccessed(); + this.mCurrentTab.removeAttribute("unread"); + oldTab.updateLastAccessed(); + + let oldFindBar = oldTab._findBar; + if (oldFindBar && + oldFindBar.findMode == oldFindBar.FIND_NORMAL && + !oldFindBar.hidden) + this._lastFindValue = oldFindBar._findField.value; + + this.updateTitlebar(); + + this.mCurrentTab.removeAttribute("titlechanged"); + this.mCurrentTab.removeAttribute("attention"); + } + + // If the new tab is busy, and our current state is not busy, then + // we need to fire a start to all progress listeners. + const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + if (this.mCurrentTab.hasAttribute("busy") && !this.mIsBusy) { + this.mIsBusy = true; + this._callProgressListeners(null, "onStateChange", + [webProgress, null, + nsIWebProgressListener.STATE_START | + nsIWebProgressListener.STATE_IS_NETWORK, 0], + true, false); + } + + // If the new tab is not busy, and our current state is busy, then + // we need to fire a stop to all progress listeners. + if (!this.mCurrentTab.hasAttribute("busy") && this.mIsBusy) { + this.mIsBusy = false; + this._callProgressListeners(null, "onStateChange", + [webProgress, null, + nsIWebProgressListener.STATE_STOP | + nsIWebProgressListener.STATE_IS_NETWORK, 0], + true, false); + } + + this._setCloseKeyState(!this.mCurrentTab.pinned); + + // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code + // that might rely upon the other changes suppressed. + // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window + if (!this._previewMode) { + // We've selected the new tab, so go ahead and notify listeners. + let event = new CustomEvent("TabSelect", { + bubbles: true, + cancelable: false, + detail: { + previousTab: oldTab + } + }); + this.mCurrentTab.dispatchEvent(event); + + this._tabAttrModified(oldTab, ["selected"]); + this._tabAttrModified(this.mCurrentTab, ["selected"]); + + if (oldBrowser != newBrowser && + oldBrowser.getInPermitUnload) { + oldBrowser.getInPermitUnload(inPermitUnload => { + if (!inPermitUnload) { + return; + } + // Since the user is switching away from a tab that has + // a beforeunload prompt active, we remove the prompt. + // This prevents confusing user flows like the following: + // 1. User attempts to close Firefox + // 2. User switches tabs (ingoring a beforeunload prompt) + // 3. User returns to tab, presses "Leave page" + let promptBox = this.getTabModalPromptBox(oldBrowser); + let prompts = promptBox.listPrompts(); + // There might not be any prompts here if the tab was closed + // while in an onbeforeunload prompt, which will have + // destroyed aforementioned prompt already, so check there's + // something to remove, first: + if (prompts.length) { + // NB: This code assumes that the beforeunload prompt + // is the top-most prompt on the tab. + prompts[prompts.length - 1].abortPrompt(); + } + }); + } + + oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused); + if (this.isFindBarInitialized(oldTab)) { + let findBar = this.getFindBar(oldTab); + oldTab._findBarFocused = (!findBar.hidden && + findBar._findField.getAttribute("focused") == "true"); + } + + // If focus is in the tab bar, retain it there. + if (document.activeElement == oldTab) { + // We need to explicitly focus the new tab, because + // tabbox.xml does this only in some cases. + this.mCurrentTab.focus(); + } else if (gMultiProcessBrowser && document.activeElement !== newBrowser) { + // Clear focus so that _adjustFocusAfterTabSwitch can detect if + // some element has been focused and respect that. + document.activeElement.blur(); + } + + if (!gMultiProcessBrowser) + this._adjustFocusAfterTabSwitch(this.mCurrentTab); + } + + updateUserContextUIIndicator(); + gIdentityHandler.updateSharingIndicator(); + + this.tabContainer._setPositionalAttributes(); + + if (!gMultiProcessBrowser) { + let event = new CustomEvent("TabSwitchDone", { + bubbles: true, + cancelable: true + }); + this.dispatchEvent(event); + } + + if (!aForceUpdate) + TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS"); + ]]> + </body> + </method> + + <method name="_adjustFocusAfterTabSwitch"> + <parameter name="newTab"/> + <body><![CDATA[ + // Don't steal focus from the tab bar. + if (document.activeElement == newTab) + return; + + let newBrowser = this.getBrowserForTab(newTab); + + // If there's a tabmodal prompt showing, focus it. + if (newBrowser.hasAttribute("tabmodalPromptShowing")) { + let XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let prompts = newBrowser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"); + let prompt = prompts[prompts.length - 1]; + prompt.Dialog.setDefaultFocus(); + return; + } + + // Focus the location bar if it was previously focused for that tab. + // In full screen mode, only bother making the location bar visible + // if the tab is a blank one. + if (newBrowser._urlbarFocused && gURLBar) { + // Explicitly close the popup if the URL bar retains focus + gURLBar.closePopup(); + + if (!window.fullScreen) { + gURLBar.focus(); + return; + } + + if (isTabEmpty(this.mCurrentTab)) { + focusAndSelectUrlBar(); + return; + } + } + + // Focus the find bar if it was previously focused for that tab. + if (gFindBarInitialized && !gFindBar.hidden && + this.selectedTab._findBarFocused) { + gFindBar._findField.focus(); + return; + } + + // Don't focus the content area if something has been focused after the + // tab switch was initiated. + if (gMultiProcessBrowser && + document.activeElement != document.documentElement) + return; + + // We're now committed to focusing the content area. + let fm = Services.focus; + let focusFlags = fm.FLAG_NOSCROLL; + + if (!gMultiProcessBrowser) { + let newFocusedElement = fm.getFocusedElementForWindow(window.content, true, {}); + + // for anchors, use FLAG_SHOWRING so that it is clear what link was + // last clicked when switching back to that tab + if (newFocusedElement && + (newFocusedElement instanceof HTMLAnchorElement || + newFocusedElement.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple")) + focusFlags |= fm.FLAG_SHOWRING; + } + + fm.setFocus(newBrowser, focusFlags); + ]]></body> + </method> + + <!-- + This function assumes we have an LRU cache of tabs (either + images of tab content or their layers). The goal is to find + out how far into the cache we need to look in order to find + aTab. We record this number in telemetry and also move aTab to + the front of the cache. + + A newly created tab has position Infinity in the cache. + If a tab is closed, it has no effect on the position of other + tabs in the cache since we assume that closing a tab doesn't + cause us to load in any other tabs. + + We ignore the effect of dragging tabs between windows. + --> + <method name="_recordTabAccess"> + <parameter name="aTab"/> + <body><![CDATA[ + if (!Services.telemetry.canRecordExtended) { + return; + } + + let tabs = Array.from(this.visibleTabs); + + let pos = aTab.cachePosition; + for (let i = 0; i < tabs.length; i++) { + // If aTab is moving to the front, everything that was + // previously in front of it is bumped up one position. + if (tabs[i].cachePosition < pos) { + tabs[i].cachePosition++; + } + } + aTab.cachePosition = 0; + + if (isFinite(pos)) { + Services.telemetry.getHistogramById("TAB_SWITCH_CACHE_POSITION").add(pos); + } + ]]></body> + </method> + + <method name="_tabAttrModified"> + <parameter name="aTab"/> + <parameter name="aChanged"/> + <body><![CDATA[ + if (aTab.closing) + return; + + let event = new CustomEvent("TabAttrModified", { + bubbles: true, + cancelable: false, + detail: { + changed: aChanged, + } + }); + aTab.dispatchEvent(event); + ]]></body> + </method> + + <method name="setBrowserSharing"> + <parameter name="aBrowser"/> + <parameter name="aState"/> + <body><![CDATA[ + let tab = this.getTabForBrowser(aBrowser); + if (!tab) + return; + + let sharing; + if (aState.screen) { + sharing = "screen"; + } else if (aState.camera) { + sharing = "camera"; + } else if (aState.microphone) { + sharing = "microphone"; + } + + if (sharing) { + tab.setAttribute("sharing", sharing); + tab._sharingState = aState; + } else { + tab.removeAttribute("sharing"); + tab._sharingState = null; + } + this._tabAttrModified(tab, ["sharing"]); + + if (aBrowser == this.mCurrentBrowser) + gIdentityHandler.updateSharingIndicator(); + ]]></body> + </method> + + + <method name="setTabTitleLoading"> + <parameter name="aTab"/> + <body> + <![CDATA[ + aTab.label = this.mStringBundle.getString("tabs.connecting"); + aTab.crop = "end"; + this._tabAttrModified(aTab, ["label", "crop"]); + ]]> + </body> + </method> + + <method name="setTabTitle"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var browser = this.getBrowserForTab(aTab); + var crop = "end"; + var title = browser.contentTitle; + + if (!title) { + if (browser.currentURI.spec) { + try { + title = this.mURIFixup.createExposableURI(browser.currentURI).spec; + } catch (ex) { + title = browser.currentURI.spec; + } + } + + if (title && !isBlankPageURL(title)) { + // At this point, we now have a URI. + // Let's try to unescape it using a character set + // in case the URI is not ASCII. + try { + var characterSet = browser.characterSet; + const textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"] + .getService(Components.interfaces.nsITextToSubURI); + title = textToSubURI.unEscapeNonAsciiURI(characterSet, title); + } catch (ex) { /* Do nothing. */ } + + crop = "center"; + + } else if (aTab.hasAttribute("customizemode")) { + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + title = gNavigatorBundle.getFormattedString("customizeMode.tabTitle", + [ brandShortName ]); + } else // Still no title? Fall back to our untitled string. + title = this.mStringBundle.getString("tabs.emptyTabTitle"); + } + + if (aTab.label == title && + aTab.crop == crop) + return false; + + aTab.label = title; + aTab.crop = crop; + this._tabAttrModified(aTab, ["label", "crop"]); + + if (aTab.selected) + this.updateTitlebar(); + + return true; + ]]> + </body> + </method> + + <method name="loadOneTab"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <parameter name="aLoadInBackground"/> + <parameter name="aAllowThirdPartyFixup"/> + <body> + <![CDATA[ + var aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aRelatedToCurrent; + var aAllowMixedContent; + var aSkipAnimation; + var aForceNotRemote; + var aNoReferrer; + var aUserContextId; + var aRelatedBrowser; + var aOriginPrincipal; + var aOpener; + if (arguments.length == 2 && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aTriggeringPrincipal = params.triggeringPrincipal + aReferrerURI = params.referrerURI; + aReferrerPolicy = params.referrerPolicy; + aCharset = params.charset; + aPostData = params.postData; + aLoadInBackground = params.inBackground; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + aAllowMixedContent = params.allowMixedContent; + aSkipAnimation = params.skipAnimation; + aForceNotRemote = params.forceNotRemote; + aNoReferrer = params.noReferrer; + aUserContextId = params.userContextId; + aRelatedBrowser = params.relatedBrowser; + aOriginPrincipal = params.originPrincipal; + aOpener = params.opener; + } + + var bgLoad = (aLoadInBackground != null) ? aLoadInBackground : + Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + var owner = bgLoad ? null : this.selectedTab; + var tab = this.addTab(aURI, { + triggeringPrincipal: aTriggeringPrincipal, + referrerURI: aReferrerURI, + referrerPolicy: aReferrerPolicy, + charset: aCharset, + postData: aPostData, + ownerTab: owner, + allowThirdPartyFixup: aAllowThirdPartyFixup, + fromExternal: aFromExternal, + relatedToCurrent: aRelatedToCurrent, + skipAnimation: aSkipAnimation, + allowMixedContent: aAllowMixedContent, + forceNotRemote: aForceNotRemote, + noReferrer: aNoReferrer, + userContextId: aUserContextId, + originPrincipal: aOriginPrincipal, + relatedBrowser: aRelatedBrowser, + opener: aOpener }); + if (!bgLoad) + this.selectedTab = tab; + + return tab; + ]]> + </body> + </method> + + <method name="loadTabs"> + <parameter name="aURIs"/> + <parameter name="aLoadInBackground"/> + <parameter name="aReplace"/> + <body><![CDATA[ + let aAllowThirdPartyFixup; + let aTargetTab; + let aNewIndex = -1; + let aPostDatas = []; + let aUserContextId; + if (arguments.length == 2 && + typeof arguments[1] == "object") { + let params = arguments[1]; + aLoadInBackground = params.inBackground; + aReplace = params.replace; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aTargetTab = params.targetTab; + aNewIndex = typeof params.newIndex === "number" ? + params.newIndex : aNewIndex; + aPostDatas = params.postDatas || aPostDatas; + aUserContextId = params.userContextId; + } + + if (!aURIs.length) + return; + + // The tab selected after this new tab is closed (i.e. the new tab's + // "owner") is the next adjacent tab (i.e. not the previously viewed tab) + // when several urls are opened here (i.e. closing the first should select + // the next of many URLs opened) or if the pref to have UI links opened in + // the background is set (i.e. the link is not being opened modally) + // + // i.e. + // Number of URLs Load UI Links in BG Focus Last Viewed? + // == 1 false YES + // == 1 true NO + // > 1 false/true NO + var multiple = aURIs.length > 1; + var owner = multiple || aLoadInBackground ? null : this.selectedTab; + var firstTabAdded = null; + var targetTabIndex = -1; + + if (aReplace) { + let browser; + if (aTargetTab) { + browser = this.getBrowserForTab(aTargetTab); + targetTabIndex = aTargetTab._tPos; + } else { + browser = this.mCurrentBrowser; + targetTabIndex = this.tabContainer.selectedIndex; + } + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (aAllowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | + Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + try { + browser.loadURIWithFlags(aURIs[0], { + flags, postData: aPostDatas[0] + }); + } catch (e) { + // Ignore failure in case a URI is wrong, so we can continue + // opening the next ones. + } + } else { + firstTabAdded = this.addTab(aURIs[0], { + ownerTab: owner, + skipAnimation: multiple, + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[0], + userContextId: aUserContextId + }); + if (aNewIndex !== -1) { + this.moveTabTo(firstTabAdded, aNewIndex); + targetTabIndex = firstTabAdded._tPos; + } + } + + let tabNum = targetTabIndex; + for (let i = 1; i < aURIs.length; ++i) { + let tab = this.addTab(aURIs[i], { + skipAnimation: true, + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[i], + userContextId: aUserContextId + }); + if (targetTabIndex !== -1) + this.moveTabTo(tab, ++tabNum); + } + + if (!aLoadInBackground) { + if (firstTabAdded) { + // .selectedTab setter focuses the content area + this.selectedTab = firstTabAdded; + } + else + this.selectedBrowser.focus(); + } + ]]></body> + </method> + + <method name="updateBrowserRemoteness"> + <parameter name="aBrowser"/> + <parameter name="aShouldBeRemote"/> + <parameter name="aOpener"/> + <parameter name="aFreshProcess"/> + <body> + <![CDATA[ + let isRemote = aBrowser.getAttribute("remote") == "true"; + + // If we are passed an opener, we must be making the browser non-remote, and + // if the browser is _currently_ non-remote, we need the openers to match, + // because it is already too late to change it. + if (aOpener) { + if (aShouldBeRemote) { + throw new Exception("Cannot set an opener on a browser which should be remote!"); + } + if (!isRemote && aBrowser.contentWindow.opener != aOpener) { + throw new Exception("Cannot change opener on an already non-remote browser!"); + } + } + + // Abort if we're not going to change anything + if (isRemote == aShouldBeRemote && !aFreshProcess) { + return false; + } + + let tab = this.getTabForBrowser(aBrowser); + let evt = document.createEvent("Events"); + evt.initEvent("BeforeTabRemotenessChange", true, false); + tab.dispatchEvent(evt); + + let wasActive = document.activeElement == aBrowser; + + // Unmap the old outerWindowID. + this._outerWindowIDBrowserMap.delete(aBrowser.outerWindowID); + + // Unhook our progress listener. + let filter = this._tabFilters.get(tab); + let listener = this._tabListeners.get(tab); + aBrowser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(listener); + + // We'll be creating a new listener, so destroy the old one. + listener.destroy(); + + let oldUserTypedValue = aBrowser.userTypedValue; + let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping(); + + // Make sure the browser is destroyed so it unregisters from observer notifications + aBrowser.destroy(); + + // Make sure to restore the original droppedLinkHandler and + // relatedBrowser. + let droppedLinkHandler = aBrowser.droppedLinkHandler; + let relatedBrowser = aBrowser.relatedBrowser; + + // Change the "remote" attribute. + let parent = aBrowser.parentNode; + parent.removeChild(aBrowser); + aBrowser.setAttribute("remote", aShouldBeRemote ? "true" : "false"); + + // NB: This works with the hack in the browser constructor that + // turns this normal property into a field. + aBrowser.relatedBrowser = relatedBrowser; + + // Set the opener window on the browser, such that when the frame + // loader is created the opener is set correctly. + aBrowser.presetOpenerWindow(aOpener); + + // Set the freshProcess attribute so that the frameloader knows to + // create a new process + if (aFreshProcess) { + aBrowser.setAttribute("freshProcess", "true"); + } + + parent.appendChild(aBrowser); + + // Remove the freshProcess attribute if we set it, as we don't + // want it to apply for the next time the frameloader is created + aBrowser.removeAttribute("freshProcess"); + + aBrowser.userTypedValue = oldUserTypedValue; + if (hadStartedLoad) { + aBrowser.urlbarChangeTracker.startedLoad(); + } + + aBrowser.droppedLinkHandler = droppedLinkHandler; + + // Switching a browser's remoteness will create a new frameLoader. + // As frameLoaders start out with an active docShell we have to + // deactivate it if this is not the selected tab's browser or the + // browser window is minimized. + aBrowser.docShellIsActive = this.shouldActivateDocShell(aBrowser); + + // Create a new tab progress listener for the new browser we just injected, + // since tab progress listeners have logic for handling the initial about:blank + // load + listener = this.mTabProgressListener(tab, aBrowser, true, false); + this._tabListeners.set(tab, listener); + filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL); + + // Restore the progress listener. + aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); + + // Restore the securityUI state. + let securityUI = aBrowser.securityUI; + let state = securityUI ? securityUI.state + : Ci.nsIWebProgressListener.STATE_IS_INSECURE; + // Include the true final argument to indicate that this event is + // simulated (instead of being observed by the webProgressListener). + this._callProgressListeners(aBrowser, "onSecurityChange", + [aBrowser.webProgress, null, state, true], + true, false); + + if (aShouldBeRemote) { + // Switching the browser to be remote will connect to a new child + // process so the browser can no longer be considered to be + // crashed. + tab.removeAttribute("crashed"); + } else { + aBrowser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned }) + + // Register the new outerWindowID. + this._outerWindowIDBrowserMap.set(aBrowser.outerWindowID, aBrowser); + } + + if (wasActive) + aBrowser.focus(); + + // If the findbar has been initialised, reset its browser reference. + if (this.isFindBarInitialized(tab)) { + this.getFindBar(tab).browser = aBrowser; + } + + evt = document.createEvent("Events"); + evt.initEvent("TabRemotenessChange", true, false); + tab.dispatchEvent(evt); + + return true; + ]]> + </body> + </method> + + <method name="switchBrowserIntoFreshProcess"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + if (!gMultiProcessBrowser) { + return this.updateBrowserRemoteness(aBrowser, false); + } + + return this.updateBrowserRemoteness(aBrowser, + /* aShouldBeRemote */ true, + /* aOpener */ null, + /* aFreshProcess */ true); + ]]> + </body> + </method> + + <method name="updateBrowserRemotenessByURL"> + <parameter name="aBrowser"/> + <parameter name="aURL"/> + <body> + <![CDATA[ + if (!gMultiProcessBrowser) + return this.updateBrowserRemoteness(aBrowser, false); + + let process = aBrowser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT + : Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + + // If this URL can't load in the browser's current process then flip + // it to the other process + if (!E10SUtils.canLoadURIInProcess(aURL, process)) + return this.updateBrowserRemoteness(aBrowser, !aBrowser.isRemoteBrowser); + + return false; + ]]> + </body> + </method> + + <field name="_preloadedBrowser">null</field> + <method name="_getPreloadedBrowser"> + <body> + <![CDATA[ + if (!this._isPreloadingEnabled()) { + return null; + } + + // The preloaded browser might be null. + let browser = this._preloadedBrowser; + + // Consume the browser. + this._preloadedBrowser = null; + + // Attach the nsIFormFillController now that we know the browser + // will be used. If we do that before and the preloaded browser + // won't be consumed until shutdown then we leak a docShell. + // Also, we do not need to take care of attaching nsIFormFillControllers + // in the case that the browser is remote, as remote browsers take + // care of that themselves. + if (browser && this.hasAttribute("autocompletepopup")) { + browser.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup")); + } + + return browser; + ]]> + </body> + </method> + + <method name="_isPreloadingEnabled"> + <body> + <![CDATA[ + // Preloading for the newtab page is enabled when the pref is true + // and the URL is "about:newtab". We do not support preloading for + // custom newtab URLs. + return Services.prefs.getBoolPref("browser.newtab.preload") && + !aboutNewTabService.overridden; + ]]> + </body> + </method> + + <method name="_createPreloadBrowser"> + <body> + <![CDATA[ + // Do nothing if we have a preloaded browser already + // or preloading of newtab pages is disabled. + if (this._preloadedBrowser || !this._isPreloadingEnabled()) { + return; + } + + let remote = gMultiProcessBrowser && + E10SUtils.canLoadURIInProcess(BROWSER_NEW_TAB_URL, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT); + let browser = this._createBrowser({isPreloadBrowser: true, remote: remote}); + this._preloadedBrowser = browser; + + let notificationbox = this.getNotificationBox(browser); + this.mPanelContainer.appendChild(notificationbox); + + if (remote) { + // For remote browsers, we need to make sure that the webProgress is + // instantiated, otherwise the parent won't get informed about the state + // of the preloaded browser until it gets attached to a tab. + browser.webProgress; + } + + browser.loadURI(BROWSER_NEW_TAB_URL); + browser.docShellIsActive = false; + ]]> + </body> + </method> + + <method name="_createBrowser"> + <parameter name="aParams"/> + <body> + <![CDATA[ + // Supported parameters: + // userContextId, remote, isPreloadBrowser, uriIsAboutBlank, permanentKey + + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + let b = document.createElementNS(NS_XUL, "browser"); + b.permanentKey = aParams.permanentKey || {}; + b.setAttribute("type", "content-targetable"); + b.setAttribute("message", "true"); + b.setAttribute("messagemanagergroup", "browsers"); + b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu")); + b.setAttribute("tooltip", this.getAttribute("contenttooltip")); + + if (aParams.userContextId) { + b.setAttribute("usercontextid", aParams.userContextId); + } + + if (aParams.remote) { + b.setAttribute("remote", "true"); + } + + if (aParams.opener) { + if (aParams.remote) { + throw new Exception("Cannot set opener window on a remote browser!"); + } + b.QueryInterface(Ci.nsIFrameLoaderOwner).presetOpenerWindow(aParams.opener); + } + + if (!aParams.isPreloadBrowser && this.hasAttribute("autocompletepopup")) { + b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup")); + } + + if (this.hasAttribute("selectmenulist")) + b.setAttribute("selectmenulist", this.getAttribute("selectmenulist")); + + if (this.hasAttribute("datetimepicker")) { + b.setAttribute("datetimepicker", this.getAttribute("datetimepicker")); + } + + b.setAttribute("autoscrollpopup", this._autoScrollPopup.id); + + if (aParams.relatedBrowser) { + b.relatedBrowser = aParams.relatedBrowser; + } + + // Create the browserStack container + var stack = document.createElementNS(NS_XUL, "stack"); + stack.className = "browserStack"; + stack.appendChild(b); + stack.setAttribute("flex", "1"); + + // Create the browserContainer + var browserContainer = document.createElementNS(NS_XUL, "vbox"); + browserContainer.className = "browserContainer"; + browserContainer.appendChild(stack); + browserContainer.setAttribute("flex", "1"); + + // Create the sidebar container + var browserSidebarContainer = document.createElementNS(NS_XUL, + "hbox"); + browserSidebarContainer.className = "browserSidebarContainer"; + browserSidebarContainer.appendChild(browserContainer); + browserSidebarContainer.setAttribute("flex", "1"); + + // Add the Message and the Browser to the box + var notificationbox = document.createElementNS(NS_XUL, + "notificationbox"); + notificationbox.setAttribute("flex", "1"); + notificationbox.setAttribute("notificationside", "top"); + notificationbox.appendChild(browserSidebarContainer); + + // Prevent the superfluous initial load of a blank document + // if we're going to load something other than about:blank. + if (!aParams.uriIsAboutBlank) { + b.setAttribute("nodefaultsrc", "true"); + } + + return b; + ]]> + </body> + </method> + + <method name="_linkBrowserToTab"> + <parameter name="aTab"/> + <parameter name="aURI"/> + <parameter name="aParams"/> + <body> + <![CDATA[ + "use strict"; + + // Supported parameters: + // forceNotRemote, userContextId + + let uriIsAboutBlank = !aURI || aURI == "about:blank"; + + // The new browser should be remote if this is an e10s window and + // the uri to load can be loaded remotely. + let remote = gMultiProcessBrowser && + !aParams.forceNotRemote && + E10SUtils.canLoadURIInProcess(aURI, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT); + + let browser; + let usingPreloadedContent = false; + + // If we open a new tab with the newtab URL in the default + // userContext, check if there is a preloaded browser ready. + // Private windows are not included because both the label and the + // icon for the tab would be set incorrectly (see bug 1195981). + if (aURI == BROWSER_NEW_TAB_URL && + !aParams.userContextId && + !PrivateBrowsingUtils.isWindowPrivate(window)) { + browser = this._getPreloadedBrowser(); + if (browser) { + usingPreloadedContent = true; + aTab.permanentKey = browser.permanentKey; + } + } + + if (!browser) { + // No preloaded browser found, create one. + browser = this._createBrowser({permanentKey: aTab.permanentKey, + remote: remote, + uriIsAboutBlank: uriIsAboutBlank, + userContextId: aParams.userContextId, + relatedBrowser: aParams.relatedBrowser, + opener: aParams.opener}); + } + + let notificationbox = this.getNotificationBox(browser); + let uniqueId = this._generateUniquePanelID(); + notificationbox.id = uniqueId; + aTab.linkedPanel = uniqueId; + aTab.linkedBrowser = browser; + aTab.hasBrowser = true; + this._tabForBrowser.set(browser, aTab); + + // Inject the <browser> into the DOM if necessary. + if (!notificationbox.parentNode) { + // NB: this appendChild call causes us to run constructors for the + // browser element, which fires off a bunch of notifications. Some + // of those notifications can cause code to run that inspects our + // state, so it is important that the tab element is fully + // initialized by this point. + this.mPanelContainer.appendChild(notificationbox); + } + + // wire up a progress listener for the new browser object. + let tabListener = this.mTabProgressListener(aTab, browser, uriIsAboutBlank, usingPreloadedContent); + const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); + browser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); + this._tabListeners.set(aTab, tabListener); + this._tabFilters.set(aTab, filter); + + browser.droppedLinkHandler = handleDroppedLink; + + // We start our browsers out as inactive, and then maintain + // activeness in the tab switcher. + browser.docShellIsActive = false; + + // When addTab() is called with an URL that is not "about:blank" we + // set the "nodefaultsrc" attribute that prevents a frameLoader + // from being created as soon as the linked <browser> is inserted + // into the DOM. We thus have to register the new outerWindowID + // for non-remote browsers after we have called browser.loadURI(). + if (!remote) { + this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser); + } + + var evt = new CustomEvent("TabBrowserInserted", { bubbles: true, detail: {} }); + aTab.dispatchEvent(evt); + + return { usingPreloadedContent: usingPreloadedContent }; + ]]> + </body> + </method> + + <method name="addTab"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <parameter name="aOwner"/> + <parameter name="aAllowThirdPartyFixup"/> + <body> + <![CDATA[ + "use strict"; + + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aRelatedToCurrent; + var aSkipAnimation; + var aAllowMixedContent; + var aForceNotRemote; + var aNoReferrer; + var aUserContextId; + var aEventDetail; + var aRelatedBrowser; + var aOriginPrincipal; + var aOpener; + if (arguments.length == 2 && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aTriggeringPrincipal = params.triggeringPrincipal; + aReferrerURI = params.referrerURI; + aReferrerPolicy = params.referrerPolicy; + aCharset = params.charset; + aPostData = params.postData; + aOwner = params.ownerTab; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + aSkipAnimation = params.skipAnimation; + aAllowMixedContent = params.allowMixedContent; + aForceNotRemote = params.forceNotRemote; + aNoReferrer = params.noReferrer; + aUserContextId = params.userContextId; + aEventDetail = params.eventDetail; + aRelatedBrowser = params.relatedBrowser; + aOriginPrincipal = params.originPrincipal; + aOpener = params.opener; + } + + // if we're adding tabs, we're past interrupt mode, ditch the owner + if (this.mCurrentTab.owner) + this.mCurrentTab.owner = null; + + var t = document.createElementNS(NS_XUL, "tab"); + + var uriIsAboutBlank = !aURI || aURI == "about:blank"; + let aURIObject = null; + try { + aURIObject = Services.io.newURI(aURI || "about:blank"); + } catch (ex) { /* we'll try to fix up this URL later */ } + + if (!aURI || isBlankPageURL(aURI)) { + t.setAttribute("label", this.mStringBundle.getString("tabs.emptyTabTitle")); + } else if (aURI.toLowerCase().startsWith("javascript:")) { + // This can go away when bug 672618 or bug 55696 are fixed. + t.setAttribute("label", aURI); + } + + if (aUserContextId) { + t.setAttribute("usercontextid", aUserContextId); + ContextualIdentityService.setTabStyle(t); + } + + t.setAttribute("crop", "end"); + t.setAttribute("onerror", "this.removeAttribute('image');"); + t.className = "tabbrowser-tab"; + + this.tabContainer._unlockTabSizing(); + + // When overflowing, new tabs are scrolled into view smoothly, which + // doesn't go well together with the width transition. So we skip the + // transition in that case. + let animate = !aSkipAnimation && + this.tabContainer.getAttribute("overflow") != "true" && + Services.prefs.getBoolPref("browser.tabs.animate"); + if (!animate) { + t.setAttribute("fadein", "true"); + setTimeout(function (tabContainer) { + tabContainer._handleNewTab(t); + }, 0, this.tabContainer); + } + + // invalidate cache + this._visibleTabs = null; + + this.tabContainer.appendChild(t); + + // If this new tab is owned by another, assert that relationship + if (aOwner) + t.owner = aOwner; + + var position = this.tabs.length - 1; + t._tPos = position; + t.permanentKey = {}; + this.tabContainer._setPositionalAttributes(); + + this.tabContainer.updateVisibility(); + + // Currently in this incarnation of bug 906076, we are forcing the + // browser to immediately be linked. In future incarnations of this + // bug this will be removed so we can leave the tab in its "lazy" + // state to be exploited for startup optimization. Note that for + // now this must occur before "TabOpen" event is fired, as that will + // trigger SessionStore.jsm to run code that expects the existence + // of tab.linkedBrowser. + let browserParams = { + forceNotRemote: aForceNotRemote, + userContextId: aUserContextId, + relatedBrowser: aRelatedBrowser, + opener: aOpener, + }; + let { usingPreloadedContent } = this._linkBrowserToTab(t, aURI, browserParams); + let b = t.linkedBrowser; + + // Dispatch a new tab notification. We do this once we're + // entirely done, so that things are in a consistent state + // even if the event listener opens or closes tabs. + var detail = aEventDetail || {}; + var evt = new CustomEvent("TabOpen", { bubbles: true, detail }); + t.dispatchEvent(evt); + + if (!usingPreloadedContent && aOriginPrincipal && aURI) { + let {URI_INHERITS_SECURITY_CONTEXT} = Ci.nsIProtocolHandler; + // Unless we know for sure we're not inheriting principals, + // force the about:blank viewer to have the right principal: + if (!aURIObject || + (Services.io.getProtocolFlags(aURIObject.scheme) & URI_INHERITS_SECURITY_CONTEXT)) { + b.createAboutBlankContentViewer(aOriginPrincipal); + } + } + + // If we didn't swap docShells with a preloaded browser + // then let's just continue loading the page normally. + if (!usingPreloadedContent && !uriIsAboutBlank) { + // pretend the user typed this so it'll be available till + // the document successfully loads + if (aURI && gInitialPages.indexOf(aURI) == -1) + b.userTypedValue = aURI; + + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (aAllowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + if (aFromExternal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL; + if (aAllowMixedContent) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT; + try { + b.loadURIWithFlags(aURI, { + flags, + triggeringPrincipal: aTriggeringPrincipal, + referrerURI: aNoReferrer ? null: aReferrerURI, + referrerPolicy: aReferrerPolicy, + charset: aCharset, + postData: aPostData, + }); + } catch (ex) { + Cu.reportError(ex); + } + } + + // Check if we're opening a tab related to the current tab and + // move it to after the current tab. + // aReferrerURI is null or undefined if the tab is opened from + // an external application or bookmark, i.e. somewhere other + // than the current tab. + if ((aRelatedToCurrent == null ? aReferrerURI : aRelatedToCurrent) && + Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) { + let newTabPos = (this._lastRelatedTab || + this.selectedTab)._tPos + 1; + if (this._lastRelatedTab) + this._lastRelatedTab.owner = null; + else + t.owner = this.selectedTab; + this.moveTabTo(t, newTabPos); + this._lastRelatedTab = t; + } + + if (animate) { + requestAnimationFrame(function () { + this.tabContainer._handleTabTelemetryStart(t, aURI); + + // kick the animation off + t.setAttribute("fadein", "true"); + }.bind(this)); + } + + return t; + ]]> + </body> + </method> + + <method name="warnAboutClosingTabs"> + <parameter name="aCloseTabs"/> + <parameter name="aTab"/> + <body> + <![CDATA[ + var tabsToClose; + switch (aCloseTabs) { + case this.closingTabsEnum.ALL: + tabsToClose = this.tabs.length - this._removingTabs.length - + gBrowser._numPinnedTabs; + break; + case this.closingTabsEnum.OTHER: + tabsToClose = this.visibleTabs.length - 1 - gBrowser._numPinnedTabs; + break; + case this.closingTabsEnum.TO_END: + if (!aTab) + throw new Error("Required argument missing: aTab"); + + tabsToClose = this.getTabsToTheEndFrom(aTab).length; + break; + default: + throw new Error("Invalid argument: " + aCloseTabs); + } + + if (tabsToClose <= 1) + return true; + + const pref = aCloseTabs == this.closingTabsEnum.ALL ? + "browser.tabs.warnOnClose" : "browser.tabs.warnOnCloseOtherTabs"; + var shouldPrompt = Services.prefs.getBoolPref(pref); + if (!shouldPrompt) + return true; + + var ps = Services.prompt; + + // default to true: if it were false, we wouldn't get this far + var warnOnClose = { value: true }; + var bundle = this.mStringBundle; + + // focus the window before prompting. + // this will raise any minimized window, which will + // make it obvious which window the prompt is for and will + // solve the problem of windows "obscuring" the prompt. + // see bug #350299 for more details + window.focus(); + var warningMessage = + PluralForm.get(tabsToClose, bundle.getString("tabs.closeWarningMultiple")) + .replace("#1", tabsToClose); + var buttonPressed = + ps.confirmEx(window, + bundle.getString("tabs.closeWarningTitle"), + warningMessage, + (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) + + (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1), + bundle.getString("tabs.closeButtonMultiple"), + null, null, + aCloseTabs == this.closingTabsEnum.ALL ? + bundle.getString("tabs.closeWarningPromptMe") : null, + warnOnClose); + var reallyClose = (buttonPressed == 0); + + // don't set the pref unless they press OK and it's false + if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value) + Services.prefs.setBoolPref(pref, false); + + return reallyClose; + ]]> + </body> + </method> + + <method name="getTabsToTheEndFrom"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var tabsToEnd = []; + let tabs = this.visibleTabs; + for (let i = tabs.length - 1; tabs[i] != aTab && i >= 0; --i) { + tabsToEnd.push(tabs[i]); + } + return tabsToEnd.reverse(); + ]]> + </body> + </method> + + <method name="removeTabsToTheEndFrom"> + <parameter name="aTab"/> + <parameter name="aParams"/> + <body> + <![CDATA[ + if (this.warnAboutClosingTabs(this.closingTabsEnum.TO_END, aTab)) { + let tabs = this.getTabsToTheEndFrom(aTab); + for (let i = tabs.length - 1; i >= 0; --i) { + this.removeTab(tabs[i], aParams); + } + } + ]]> + </body> + </method> + + <method name="removeAllTabsBut"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (aTab.pinned) + return; + + if (this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) { + let tabs = this.visibleTabs; + this.selectedTab = aTab; + + for (let i = tabs.length - 1; i >= 0; --i) { + if (tabs[i] != aTab && !tabs[i].pinned) + this.removeTab(tabs[i], {animate: true}); + } + } + ]]> + </body> + </method> + + <method name="removeCurrentTab"> + <parameter name="aParams"/> + <body> + <![CDATA[ + this.removeTab(this.mCurrentTab, aParams); + ]]> + </body> + </method> + + <field name="_removingTabs"> + [] + </field> + + <method name="removeTab"> + <parameter name="aTab"/> + <parameter name="aParams"/> + <body> + <![CDATA[ + if (aParams) { + var animate = aParams.animate; + var byMouse = aParams.byMouse; + var skipPermitUnload = aParams.skipPermitUnload; + } + + // Handle requests for synchronously removing an already + // asynchronously closing tab. + if (!animate && + aTab.closing) { + this._endRemoveTab(aTab); + return; + } + + var isLastTab = (this.tabs.length - this._removingTabs.length == 1); + + if (!this._beginRemoveTab(aTab, null, null, true, skipPermitUnload)) + return; + + if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse) + this.tabContainer._lockTabSizing(aTab); + else + this.tabContainer._unlockTabSizing(); + + if (!animate /* the caller didn't opt in */ || + isLastTab || + aTab.pinned || + aTab.hidden || + this._removingTabs.length > 3 /* don't want lots of concurrent animations */ || + aTab.getAttribute("fadein") != "true" /* fade-in transition hasn't been triggered yet */ || + window.getComputedStyle(aTab).maxWidth == "0.1px" /* fade-in transition hasn't moved yet */ || + !Services.prefs.getBoolPref("browser.tabs.animate")) { + this._endRemoveTab(aTab); + return; + } + + this.tabContainer._handleTabTelemetryStart(aTab); + + this._blurTab(aTab); + aTab.style.maxWidth = ""; // ensure that fade-out transition happens + aTab.removeAttribute("fadein"); + + setTimeout(function (tab, tabbrowser) { + if (tab.parentNode && + window.getComputedStyle(tab).maxWidth == "0.1px") { + NS_ASSERT(false, "Giving up waiting for the tab closing animation to finish (bug 608589)"); + tabbrowser._endRemoveTab(tab); + } + }, 3000, aTab, this); + ]]> + </body> + </method> + + <!-- Tab close requests are ignored if the window is closing anyway, + e.g. when holding Ctrl+W. --> + <field name="_windowIsClosing"> + false + </field> + + <method name="_beginRemoveTab"> + <parameter name="aTab"/> + <parameter name="aAdoptedByTab"/> + <parameter name="aCloseWindowWithLastTab"/> + <parameter name="aCloseWindowFastpath"/> + <parameter name="aSkipPermitUnload"/> + <body> + <![CDATA[ + if (aTab.closing || + this._windowIsClosing) + return false; + + var browser = this.getBrowserForTab(aTab); + + if (!aTab._pendingPermitUnload && !aAdoptedByTab && !aSkipPermitUnload) { + // We need to block while calling permitUnload() because it + // processes the event queue and may lead to another removeTab() + // call before permitUnload() returns. + aTab._pendingPermitUnload = true; + let {permitUnload, timedOut} = browser.permitUnload(); + delete aTab._pendingPermitUnload; + // If we were closed during onbeforeunload, we return false now + // so we don't (try to) close the same tab again. Of course, we + // also stop if the unload was cancelled by the user: + if (aTab.closing || (!timedOut && !permitUnload)) { + // NB: deliberately keep the _closedDuringPermitUnload set to + // true so we keep exiting early in case of multiple calls. + return false; + } + } + + + var closeWindow = false; + var newTab = false; + if (this.tabs.length - this._removingTabs.length == 1) { + closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab : + !window.toolbar.visible || + Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab"); + + if (closeWindow) { + // We've already called beforeunload on all the relevant tabs if we get here, + // so avoid calling it again: + window.skipNextCanClose = true; + } + + // Closing the tab and replacing it with a blank one is notably slower + // than closing the window right away. If the caller opts in, take + // the fast path. + if (closeWindow && + aCloseWindowFastpath && + this._removingTabs.length == 0) { + // This call actually closes the window, unless the user + // cancels the operation. We are finished here in both cases. + this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow); + return null; + } + + newTab = true; + } + + aTab.closing = true; + this._removingTabs.push(aTab); + this._visibleTabs = null; // invalidate cache + + // Invalidate hovered tab state tracking for this closing tab. + if (this.tabContainer._hoveredTab == aTab) + aTab._mouseleave(); + + if (newTab) + this.addTab(BROWSER_NEW_TAB_URL, {skipAnimation: true}); + else + this.tabContainer.updateVisibility(); + + // We're committed to closing the tab now. + // Dispatch a notification. + // We dispatch it before any teardown so that event listeners can + // inspect the tab that's about to close. + var evt = new CustomEvent("TabClose", { bubbles: true, detail: { adoptedBy: aAdoptedByTab } }); + aTab.dispatchEvent(evt); + + if (!aAdoptedByTab && !gMultiProcessBrowser) { + // Prevent this tab from showing further dialogs, since we're closing it + var windowUtils = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils); + windowUtils.disableDialogs(); + } + + // Remove the tab's filter and progress listener. + const filter = this._tabFilters.get(aTab); + + browser.webProgress.removeProgressListener(filter); + + const listener = this._tabListeners.get(aTab); + filter.removeProgressListener(listener); + listener.destroy(); + + if (browser.registeredOpenURI && !aAdoptedByTab) { + this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI, + browser.getAttribute("usercontextid") || 0); + delete browser.registeredOpenURI; + } + + // We are no longer the primary content area. + browser.setAttribute("type", "content-targetable"); + + // Remove this tab as the owner of any other tabs, since it's going away. + for (let tab of this.tabs) { + if ("owner" in tab && tab.owner == aTab) + // |tab| is a child of the tab we're removing, make it an orphan + tab.owner = null; + } + + aTab._endRemoveArgs = [closeWindow, newTab]; + return true; + ]]> + </body> + </method> + + <method name="_endRemoveTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (!aTab || !aTab._endRemoveArgs) + return; + + var [aCloseWindow, aNewTab] = aTab._endRemoveArgs; + aTab._endRemoveArgs = null; + + if (this._windowIsClosing) { + aCloseWindow = false; + aNewTab = false; + } + + this._lastRelatedTab = null; + + // update the UI early for responsiveness + aTab.collapsed = true; + this.tabContainer._fillTrailingGap(); + this._blurTab(aTab); + + this._removingTabs.splice(this._removingTabs.indexOf(aTab), 1); + + if (aCloseWindow) { + this._windowIsClosing = true; + while (this._removingTabs.length) + this._endRemoveTab(this._removingTabs[0]); + } else if (!this._windowIsClosing) { + if (aNewTab) + focusAndSelectUrlBar(); + + // workaround for bug 345399 + this.tabContainer.mTabstrip._updateScrollButtonsDisabledState(); + } + + // We're going to remove the tab and the browser now. + this._tabFilters.delete(aTab); + this._tabListeners.delete(aTab); + + var browser = this.getBrowserForTab(aTab); + this._outerWindowIDBrowserMap.delete(browser.outerWindowID); + + // Because of the way XBL works (fields just set JS + // properties on the element) and the code we have in place + // to preserve the JS objects for any elements that have + // JS properties set on them, the browser element won't be + // destroyed until the document goes away. So we force a + // cleanup ourselves. + // This has to happen before we remove the child so that the + // XBL implementation of nsIObserver still works. + browser.destroy(); + + var wasPinned = aTab.pinned; + + // Remove the tab ... + this.tabContainer.removeChild(aTab); + + // ... and fix up the _tPos properties immediately. + for (let i = aTab._tPos; i < this.tabs.length; i++) + this.tabs[i]._tPos = i; + + if (!this._windowIsClosing) { + if (wasPinned) + this.tabContainer._positionPinnedTabs(); + + // update tab close buttons state + this.tabContainer.adjustTabstrip(); + + setTimeout(function(tabs) { + tabs._lastTabClosedByMouse = false; + }, 0, this.tabContainer); + } + + // update tab positional properties and attributes + this.selectedTab._selected = true; + this.tabContainer._setPositionalAttributes(); + + // Removing the panel requires fixing up selectedPanel immediately + // (see below), which would be hindered by the potentially expensive + // browser removal. So we remove the browser and the panel in two + // steps. + + var panel = this.getNotificationBox(browser); + + // In the multi-process case, it's possible an asynchronous tab switch + // is still underway. If so, then it's possible that the last visible + // browser is the one we're in the process of removing. There's the + // risk of displaying preloaded browsers that are at the end of the + // deck if we remove the browser before the switch is complete, so + // we alert the switcher in order to show a spinner instead. + if (this._switcher) { + this._switcher.onTabRemoved(aTab); + } + + // This will unload the document. An unload handler could remove + // dependant tabs, so it's important that the tabbrowser is now in + // a consistent state (tab removed, tab positions updated, etc.). + browser.parentNode.removeChild(browser); + + // Release the browser in case something is erroneously holding a + // reference to the tab after its removal. + this._tabForBrowser.delete(aTab.linkedBrowser); + aTab.linkedBrowser = null; + + // As the browser is removed, the removal of a dependent document can + // cause the whole window to close. So at this point, it's possible + // that the binding is destructed. + if (this.mTabBox) { + this.mPanelContainer.removeChild(panel); + } + + if (aCloseWindow) + this._windowIsClosing = closeWindow(true, window.warnAboutClosingWindow); + ]]> + </body> + </method> + + <method name="_blurTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (!aTab.selected) + return; + + if (aTab.owner && + !aTab.owner.hidden && + !aTab.owner.closing && + Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) { + this.selectedTab = aTab.owner; + return; + } + + // Switch to a visible tab unless there aren't any others remaining + let remainingTabs = this.visibleTabs; + let numTabs = remainingTabs.length; + if (numTabs == 0 || numTabs == 1 && remainingTabs[0] == aTab) { + remainingTabs = Array.filter(this.tabs, function(tab) { + return !tab.closing; + }, this); + } + + // Try to find a remaining tab that comes after the given tab + var tab = aTab; + do { + tab = tab.nextSibling; + } while (tab && remainingTabs.indexOf(tab) == -1); + + if (!tab) { + tab = aTab; + + do { + tab = tab.previousSibling; + } while (tab && remainingTabs.indexOf(tab) == -1); + } + + this.selectedTab = tab; + ]]> + </body> + </method> + + <method name="swapBrowsersAndCloseOther"> + <parameter name="aOurTab"/> + <parameter name="aOtherTab"/> + <body> + <![CDATA[ + // Do not allow transfering a private tab to a non-private window + // and vice versa. + if (PrivateBrowsingUtils.isWindowPrivate(window) != + PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerDocument.defaultView)) + return; + + let ourBrowser = this.getBrowserForTab(aOurTab); + let otherBrowser = aOtherTab.linkedBrowser; + + // Can't swap between chrome and content processes. + if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser) + return; + + // Keep the userContextId if set on other browser + if (otherBrowser.hasAttribute("usercontextid")) { + ourBrowser.setAttribute("usercontextid", otherBrowser.getAttribute("usercontextid")); + } + + // That's gBrowser for the other window, not the tab's browser! + var remoteBrowser = aOtherTab.ownerDocument.defaultView.gBrowser; + var isPending = aOtherTab.hasAttribute("pending"); + + let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab); + let stateFlags = otherTabListener.mStateFlags; + + // Expedite the removal of the icon if it was already scheduled. + if (aOtherTab._soundPlayingAttrRemovalTimer) { + clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer); + aOtherTab._soundPlayingAttrRemovalTimer = 0; + aOtherTab.removeAttribute("soundplaying"); + remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]); + } + + // First, start teardown of the other browser. Make sure to not + // fire the beforeunload event in the process. Close the other + // window if this was its last tab. + if (!remoteBrowser._beginRemoveTab(aOtherTab, aOurTab, true)) + return; + + let modifiedAttrs = []; + if (aOtherTab.hasAttribute("muted")) { + aOurTab.setAttribute("muted", "true"); + aOurTab.muteReason = aOtherTab.muteReason; + ourBrowser.mute(); + modifiedAttrs.push("muted"); + } + if (aOtherTab.hasAttribute("soundplaying")) { + aOurTab.setAttribute("soundplaying", "true"); + modifiedAttrs.push("soundplaying"); + } + if (aOtherTab.hasAttribute("usercontextid")) { + aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid")); + modifiedAttrs.push("usercontextid"); + } + if (aOtherTab.hasAttribute("sharing")) { + aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing")); + modifiedAttrs.push("sharing"); + aOurTab._sharingState = aOtherTab._sharingState; + webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser); + } + + // If the other tab is pending (i.e. has not been restored, yet) + // then do not switch docShells but retrieve the other tab's state + // and apply it to our tab. + if (isPending) { + SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab)); + + // Make sure to unregister any open URIs. + this._swapRegisteredOpenURIs(ourBrowser, otherBrowser); + } else { + // Workarounds for bug 458697 + // Icon might have been set on DOMLinkAdded, don't override that. + if (!ourBrowser.mIconURL && otherBrowser.mIconURL) + this.setIcon(aOurTab, otherBrowser.mIconURL, otherBrowser.contentPrincipal); + var isBusy = aOtherTab.hasAttribute("busy"); + if (isBusy) { + aOurTab.setAttribute("busy", "true"); + modifiedAttrs.push("busy"); + if (aOurTab.selected) + this.mIsBusy = true; + } + + this._swapBrowserDocShells(aOurTab, otherBrowser, stateFlags); + } + + // Handle findbar data (if any) + let otherFindBar = aOtherTab._findBar; + if (otherFindBar && + otherFindBar.findMode == otherFindBar.FIND_NORMAL) { + let ourFindBar = this.getFindBar(aOurTab); + ourFindBar._findField.value = otherFindBar._findField.value; + if (!otherFindBar.hidden) + ourFindBar.onFindCommand(); + } + + // Finish tearing down the tab that's going away. + remoteBrowser._endRemoveTab(aOtherTab); + + if (isBusy) + this.setTabTitleLoading(aOurTab); + else + this.setTabTitle(aOurTab); + + // If the tab was already selected (this happpens in the scenario + // of replaceTabWithWindow), notify onLocationChange, etc. + if (aOurTab.selected) + this.updateCurrentBrowser(true); + + if (modifiedAttrs.length) { + this._tabAttrModified(aOurTab, modifiedAttrs); + } + ]]> + </body> + </method> + + <method name="_swapBrowserDocShells"> + <parameter name="aOurTab"/> + <parameter name="aOtherBrowser"/> + <parameter name="aStateFlags"/> + <body> + <![CDATA[ + // Unhook our progress listener + const filter = this._tabFilters.get(aOurTab); + let tabListener = this._tabListeners.get(aOurTab); + let ourBrowser = this.getBrowserForTab(aOurTab); + ourBrowser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(tabListener); + + // Make sure to unregister any open URIs. + this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser); + + // Unmap old outerWindowIDs. + this._outerWindowIDBrowserMap.delete(ourBrowser.outerWindowID); + let remoteBrowser = aOtherBrowser.ownerDocument.defaultView.gBrowser; + if (remoteBrowser) { + remoteBrowser._outerWindowIDBrowserMap.delete(aOtherBrowser.outerWindowID); + } + + // If switcher is active, it will intercept swap events and + // react as needed. + if (!this._switcher) { + aOtherBrowser.docShellIsActive = this.shouldActivateDocShell(ourBrowser); + } + + // Swap the docshells + ourBrowser.swapDocShells(aOtherBrowser); + + if (ourBrowser.isRemoteBrowser) { + // Switch outerWindowIDs for remote browsers. + let ourOuterWindowID = ourBrowser._outerWindowID; + ourBrowser._outerWindowID = aOtherBrowser._outerWindowID; + aOtherBrowser._outerWindowID = ourOuterWindowID; + } + + // Register new outerWindowIDs. + this._outerWindowIDBrowserMap.set(ourBrowser.outerWindowID, ourBrowser); + if (remoteBrowser) { + remoteBrowser._outerWindowIDBrowserMap.set(aOtherBrowser.outerWindowID, aOtherBrowser); + } + + // Swap permanentKey properties. + let ourPermanentKey = ourBrowser.permanentKey; + ourBrowser.permanentKey = aOtherBrowser.permanentKey; + aOtherBrowser.permanentKey = ourPermanentKey; + aOurTab.permanentKey = ourBrowser.permanentKey; + if (remoteBrowser) { + let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser); + if (otherTab) { + otherTab.permanentKey = aOtherBrowser.permanentKey; + } + } + + // Restore the progress listener + tabListener = this.mTabProgressListener(aOurTab, ourBrowser, false, false, + aStateFlags); + this._tabListeners.set(aOurTab, tabListener); + + const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL; + filter.addProgressListener(tabListener, notifyAll); + ourBrowser.webProgress.addProgressListener(filter, notifyAll); + ]]> + </body> + </method> + + <method name="_swapRegisteredOpenURIs"> + <parameter name="aOurBrowser"/> + <parameter name="aOtherBrowser"/> + <body> + <![CDATA[ + // If the current URI is registered as open remove it from the list. + if (aOurBrowser.registeredOpenURI) { + this._unifiedComplete.unregisterOpenPage(aOurBrowser.registeredOpenURI, + aOurBrowser.getAttribute("usercontextid") || 0); + delete aOurBrowser.registeredOpenURI; + } + + // If the other/new URI is registered as open then copy it over. + if (aOtherBrowser.registeredOpenURI) { + aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI; + delete aOtherBrowser.registeredOpenURI; + } + ]]> + </body> + </method> + + <method name="reloadAllTabs"> + <body> + <![CDATA[ + let tabs = this.visibleTabs; + let l = tabs.length; + for (var i = 0; i < l; i++) { + try { + this.getBrowserForTab(tabs[i]).reload(); + } catch (e) { + // ignore failure to reload so others will be reloaded + } + } + ]]> + </body> + </method> + + <method name="reloadTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + this.getBrowserForTab(aTab).reload(); + ]]> + </body> + </method> + + <method name="addProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (arguments.length != 1) { + Components.utils.reportError("gBrowser.addProgressListener was " + + "called with a second argument, " + + "which is not supported. See bug " + + "608628. Call stack: " + new Error().stack); + } + + this.mProgressListeners.push(aListener); + ]]> + </body> + </method> + + <method name="removeProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + this.mProgressListeners = + this.mProgressListeners.filter(l => l != aListener); + ]]> + </body> + </method> + + <method name="addTabsProgressListener"> + <parameter name="aListener"/> + <body> + this.mTabsProgressListeners.push(aListener); + </body> + </method> + + <method name="removeTabsProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + this.mTabsProgressListeners = + this.mTabsProgressListeners.filter(l => l != aListener); + ]]> + </body> + </method> + + <method name="getBrowserForTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + return aTab.linkedBrowser; + ]]> + </body> + </method> + + <method name="showOnlyTheseTabs"> + <parameter name="aTabs"/> + <body> + <![CDATA[ + for (let tab of this.tabs) { + if (aTabs.indexOf(tab) == -1) + this.hideTab(tab); + else + this.showTab(tab); + } + + this.tabContainer._handleTabSelect(false); + ]]> + </body> + </method> + + <method name="showTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (aTab.hidden) { + aTab.removeAttribute("hidden"); + this._visibleTabs = null; // invalidate cache + + this.tabContainer.adjustTabstrip(); + + this.tabContainer._setPositionalAttributes(); + + let event = document.createEvent("Events"); + event.initEvent("TabShow", true, false); + aTab.dispatchEvent(event); + } + ]]> + </body> + </method> + + <method name="hideTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (!aTab.hidden && !aTab.pinned && !aTab.selected && + !aTab.closing) { + aTab.setAttribute("hidden", "true"); + this._visibleTabs = null; // invalidate cache + + this.tabContainer.adjustTabstrip(); + + this.tabContainer._setPositionalAttributes(); + + let event = document.createEvent("Events"); + event.initEvent("TabHide", true, false); + aTab.dispatchEvent(event); + } + ]]> + </body> + </method> + + <method name="selectTabAtIndex"> + <parameter name="aIndex"/> + <parameter name="aEvent"/> + <body> + <![CDATA[ + let tabs = this.visibleTabs; + + // count backwards for aIndex < 0 + if (aIndex < 0) { + aIndex += tabs.length; + // clamp at index 0 if still negative. + if (aIndex < 0) + aIndex = 0; + } else if (aIndex >= tabs.length) { + // clamp at right-most tab if out of range. + aIndex = tabs.length - 1; + } + + this.selectedTab = tabs[aIndex]; + + if (aEvent) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + } + ]]> + </body> + </method> + + <property name="selectedTab"> + <getter> + return this.mCurrentTab; + </getter> + <setter> + <![CDATA[ + if (gNavToolbox.collapsed) { + return this.mTabBox.selectedTab; + } + // Update the tab + this.mTabBox.selectedTab = val; + return val; + ]]> + </setter> + </property> + + <property name="selectedBrowser" + onget="return this.mCurrentBrowser;" + readonly="true"/> + + <field name="browsers" readonly="true"> + <![CDATA[ + // This defines a proxy which allows us to access browsers by + // index without actually creating a full array of browsers. + new Proxy([], { + has: (target, name) => { + if (typeof name == "string" && Number.isInteger(parseInt(name))) { + return (name in this.tabs); + } + return false; + }, + get: (target, name) => { + if (name == "length") { + return this.tabs.length; + } + if (typeof name == "string" && Number.isInteger(parseInt(name))) { + if (!(name in this.tabs)) { + return undefined; + } + return this.tabs[name].linkedBrowser; + } + return target[name]; + } + }); + ]]> + </field> + + <!-- Moves a tab to a new browser window, unless it's already the only tab + in the current window, in which case this will do nothing. --> + <method name="replaceTabWithWindow"> + <parameter name="aTab"/> + <parameter name="aOptions"/> + <body> + <![CDATA[ + if (this.tabs.length == 1) + return null; + + var options = "chrome,dialog=no,all"; + for (var name in aOptions) + options += "," + name + "=" + aOptions[name]; + + // tell a new window to take the "dropped" tab + return window.openDialog(getBrowserURL(), "_blank", options, aTab); + ]]> + </body> + </method> + + <method name="moveTabTo"> + <parameter name="aTab"/> + <parameter name="aIndex"/> + <body> + <![CDATA[ + var oldPosition = aTab._tPos; + if (oldPosition == aIndex) + return; + + // Don't allow mixing pinned and unpinned tabs. + if (aTab.pinned) + aIndex = Math.min(aIndex, this._numPinnedTabs - 1); + else + aIndex = Math.max(aIndex, this._numPinnedTabs); + if (oldPosition == aIndex) + return; + + this._lastRelatedTab = null; + + let wasFocused = (document.activeElement == this.mCurrentTab); + + aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1; + + // invalidate cache + this._visibleTabs = null; + + // use .item() instead of [] because dragging to the end of the strip goes out of + // bounds: .item() returns null (so it acts like appendChild), but [] throws + this.tabContainer.insertBefore(aTab, this.tabs.item(aIndex)); + + for (let i = 0; i < this.tabs.length; i++) { + this.tabs[i]._tPos = i; + this.tabs[i]._selected = false; + } + + // If we're in the midst of an async tab switch while calling + // moveTabTo, we can get into a case where _visuallySelected + // is set to true on two different tabs. + // + // What we want to do in moveTabTo is to remove logical selection + // from all tabs, and then re-add logical selection to mCurrentTab + // (and visual selection as well if we're not running with e10s, which + // setting _selected will do automatically). + // + // If we're running with e10s, then the visual selection will not + // be changed, which is fine, since if we weren't in the midst of a + // tab switch, the previously visually selected tab should still be + // correct, and if we are in the midst of a tab switch, then the async + // tab switcher will set the visually selected tab once the tab switch + // has completed. + this.mCurrentTab._selected = true; + + if (wasFocused) + this.mCurrentTab.focus(); + + this.tabContainer._handleTabSelect(false); + + if (aTab.pinned) + this.tabContainer._positionPinnedTabs(); + + this.tabContainer._setPositionalAttributes(); + + var evt = document.createEvent("UIEvents"); + evt.initUIEvent("TabMove", true, false, window, oldPosition); + aTab.dispatchEvent(evt); + ]]> + </body> + </method> + + <method name="moveTabForward"> + <body> + <![CDATA[ + let nextTab = this.mCurrentTab.nextSibling; + while (nextTab && nextTab.hidden) + nextTab = nextTab.nextSibling; + + if (nextTab) + this.moveTabTo(this.mCurrentTab, nextTab._tPos); + else if (this.arrowKeysShouldWrap) + this.moveTabToStart(); + ]]> + </body> + </method> + + <!-- Adopts a tab from another browser window, and inserts it at aIndex --> + <method name="adoptTab"> + <parameter name="aTab"/> + <parameter name="aIndex"/> + <parameter name="aSelectTab"/> + <body> + <![CDATA[ + // Swap the dropped tab with a new one we create and then close + // it in the other window (making it seem to have moved between + // windows). + let params = { eventDetail: { adoptedTab: aTab } }; + if (aTab.hasAttribute("usercontextid")) { + // new tab must have the same usercontextid as the old one + params.userContextId = aTab.getAttribute("usercontextid"); + } + let newTab = this.addTab("about:blank", params); + let newBrowser = this.getBrowserForTab(newTab); + let newURL = aTab.linkedBrowser.currentURI.spec; + + // If we're an e10s browser window, an exception will be thrown + // if we attempt to drag a non-remote browser in, so we need to + // ensure that the remoteness of the newly created browser is + // appropriate for the URL of the tab being dragged in. + this.updateBrowserRemotenessByURL(newBrowser, newURL); + + // Stop the about:blank load. + newBrowser.stop(); + // Make sure it has a docshell. + newBrowser.docShell; + + let numPinned = this._numPinnedTabs; + if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) { + this.pinTab(newTab); + } + + this.moveTabTo(newTab, aIndex); + + // We need to select the tab before calling swapBrowsersAndCloseOther + // so that window.content in chrome windows points to the right tab + // when pagehide/show events are fired. This is no longer necessary + // for any exiting browser code, but it may be necessary for add-on + // compatibility. + if (aSelectTab) { + this.selectedTab = newTab; + } + + aTab.parentNode._finishAnimateTabMove(); + this.swapBrowsersAndCloseOther(newTab, aTab); + + if (aSelectTab) { + // Call updateCurrentBrowser to make sure the URL bar is up to date + // for our new tab after we've done swapBrowsersAndCloseOther. + this.updateCurrentBrowser(true); + } + + return newTab; + ]]> + </body> + </method> + + + <method name="moveTabBackward"> + <body> + <![CDATA[ + let previousTab = this.mCurrentTab.previousSibling; + while (previousTab && previousTab.hidden) + previousTab = previousTab.previousSibling; + + if (previousTab) + this.moveTabTo(this.mCurrentTab, previousTab._tPos); + else if (this.arrowKeysShouldWrap) + this.moveTabToEnd(); + ]]> + </body> + </method> + + <method name="moveTabToStart"> + <body> + <![CDATA[ + var tabPos = this.mCurrentTab._tPos; + if (tabPos > 0) + this.moveTabTo(this.mCurrentTab, 0); + ]]> + </body> + </method> + + <method name="moveTabToEnd"> + <body> + <![CDATA[ + var tabPos = this.mCurrentTab._tPos; + if (tabPos < this.browsers.length - 1) + this.moveTabTo(this.mCurrentTab, this.browsers.length - 1); + ]]> + </body> + </method> + + <method name="moveTabOver"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var direction = window.getComputedStyle(this.parentNode, null).direction; + if ((direction == "ltr" && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) || + (direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT)) + this.moveTabForward(); + else + this.moveTabBackward(); + ]]> + </body> + </method> + + <method name="duplicateTab"> + <parameter name="aTab"/><!-- can be from a different window as well --> + <parameter name="aRestoreTabImmediately"/><!-- can defer loading of the tab contents --> + <body> + <![CDATA[ + return SessionStore.duplicateTab(window, aTab, 0, aRestoreTabImmediately); + ]]> + </body> + </method> + + <!-- + List of browsers whose docshells must be active in order for print preview + to work. + --> + <field name="_printPreviewBrowsers"> + new Set() + </field> + + <method name="activateBrowserForPrintPreview"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + this._printPreviewBrowsers.add(aBrowser); + if (this._switcher) { + this._switcher.activateBrowserForPrintPreview(aBrowser); + } + aBrowser.docShellIsActive = true; + ]]> + </body> + </method> + + <method name="deactivatePrintPreviewBrowsers"> + <body> + <![CDATA[ + let browsers = this._printPreviewBrowsers; + this._printPreviewBrowsers = new Set(); + for (let browser of browsers) { + browser.docShellIsActive = this.shouldActivateDocShell(browser); + } + ]]> + </body> + </method> + + <!-- + Returns true if a given browser's docshell should be active. + --> + <method name="shouldActivateDocShell"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + if (this._switcher) { + return this._switcher.shouldActivateDocShell(aBrowser); + } + return (aBrowser == this.selectedBrowser && + window.windowState != window.STATE_MINIMIZED) || + this._printPreviewBrowsers.has(aBrowser); + ]]> + </body> + </method> + + <!-- + The tab switcher is responsible for asynchronously switching + tabs in e10s. It waits until the new tab is ready (i.e., the + layer tree is available) before switching to it. Then it + unloads the layer tree for the old tab. + + The tab switcher is a state machine. For each tab, it + maintains state about whether the layer tree for the tab is + available, being loaded, being unloaded, or unavailable. It + also keeps track of the tab currently being displayed, the tab + it's trying to load, and the tab the user has asked to switch + to. The switcher object is created upon tab switch. It is + released when there are no pending tabs to load or unload. + + The following general principles have guided the design: + + 1. We only request one layer tree at a time. If the user + switches to a different tab while waiting, we don't request + the new layer tree until the old tab has loaded or timed out. + + 2. If loading the layers for a tab times out, we show the + spinner and possibly request the layer tree for another tab if + the user has requested one. + + 3. We discard layer trees on a delay. This way, if the user is + switching among the same tabs frequently, we don't continually + load the same tabs. + + It's important that we always show either the spinner or a tab + whose layers are available. Otherwise the compositor will draw + an entirely black frame, which is very jarring. To ensure this + never happens when switching away from a tab, we assume the + old tab might still be drawn until a MozAfterPaint event + occurs. Because layout and compositing happen asynchronously, + we don't have any other way of knowing when the switch + actually takes place. Therefore, we don't unload the old tab + until the next MozAfterPaint event. + --> + <field name="_switcher">null</field> + <method name="_getSwitcher"> + <body><![CDATA[ + if (this._switcher) { + return this._switcher; + } + + let switcher = { + // How long to wait for a tab's layers to load. After this + // time elapses, we're free to put up the spinner and start + // trying to load a different tab. + TAB_SWITCH_TIMEOUT: 400 /* ms */, + + // When the user hasn't switched tabs for this long, we unload + // layers for all tabs that aren't in use. + UNLOAD_DELAY: 300 /* ms */, + + // The next three tabs form the principal state variables. + // See the assertions in postActions for their invariants. + + // Tab the user requested most recently. + requestedTab: this.selectedTab, + + // Tab we're currently trying to load. + loadingTab: null, + + // We show this tab in case the requestedTab hasn't loaded yet. + lastVisibleTab: this.selectedTab, + + // Auxilliary state variables: + + visibleTab: this.selectedTab, // Tab that's on screen. + spinnerTab: null, // Tab showing a spinner. + originalTab: this.selectedTab, // Tab that we started on. + + tabbrowser: this, // Reference to gBrowser. + loadTimer: null, // TAB_SWITCH_TIMEOUT nsITimer instance. + unloadTimer: null, // UNLOAD_DELAY nsITimer instance. + + // Map from tabs to STATE_* (below). + tabState: new Map(), + + // True if we're in the midst of switching tabs. + switchInProgress: false, + + // Keep an exact list of content processes (tabParent) in which + // we're actively suppressing the display port. This gives a robust + // way to make sure we don't forget to un-suppress. + activeSuppressDisplayport: new Set(), + + // Set of tabs that might be visible right now. We maintain + // this set because we can't be sure when a tab is actually + // drawn. A tab is added to this set when we ask to make it + // visible. All tabs but the most recently shown tab are + // removed from the set upon MozAfterPaint. + maybeVisibleTabs: new Set([this.selectedTab]), + + STATE_UNLOADED: 0, + STATE_LOADING: 1, + STATE_LOADED: 2, + STATE_UNLOADING: 3, + + // re-entrancy guard: + _processing: false, + + // Wraps nsITimer. Must not use the vanilla setTimeout and + // clearTimeout, because they will be blocked by nsIPromptService + // dialogs. + setTimer: function(callback, timeout) { + let event = { + notify: callback + }; + + var timer = Cc["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT); + return timer; + }, + + clearTimer: function(timer) { + timer.cancel(); + }, + + getTabState: function(tab) { + let state = this.tabState.get(tab); + if (state === undefined) { + return this.STATE_UNLOADED; + } + return state; + }, + + setTabStateNoAction(tab, state) { + if (state == this.STATE_UNLOADED) { + this.tabState.delete(tab); + } else { + this.tabState.set(tab, state); + } + }, + + setTabState: function(tab, state) { + this.setTabStateNoAction(tab, state); + + let browser = tab.linkedBrowser; + let {tabParent} = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; + if (state == this.STATE_LOADING) { + this.assert(!this.minimized); + browser.docShellIsActive = true; + if (!tabParent) { + this.onLayersReady(browser); + } + } else if (state == this.STATE_UNLOADING) { + browser.docShellIsActive = false; + if (!tabParent) { + this.onLayersCleared(browser); + } + } + }, + + get minimized() { + return window.windowState == window.STATE_MINIMIZED; + }, + + init: function() { + this.log("START"); + + // If we minimized the window before the switcher was activated, + // we might have set the preserveLayers flag for the current + // browser. Let's clear it. + this.tabbrowser.mCurrentBrowser.preserveLayers(false); + + window.addEventListener("MozAfterPaint", this); + window.addEventListener("MozLayerTreeReady", this); + window.addEventListener("MozLayerTreeCleared", this); + window.addEventListener("TabRemotenessChange", this); + window.addEventListener("sizemodechange", this); + window.addEventListener("SwapDocShells", this, true); + window.addEventListener("EndSwapDocShells", this, true); + if (!this.minimized) { + this.setTabState(this.requestedTab, this.STATE_LOADED); + } + }, + + destroy: function() { + if (this.unloadTimer) { + this.clearTimer(this.unloadTimer); + this.unloadTimer = null; + } + if (this.loadTimer) { + this.clearTimer(this.loadTimer); + this.loadTimer = null; + } + + window.removeEventListener("MozAfterPaint", this); + window.removeEventListener("MozLayerTreeReady", this); + window.removeEventListener("MozLayerTreeCleared", this); + window.removeEventListener("TabRemotenessChange", this); + window.removeEventListener("sizemodechange", this); + window.removeEventListener("SwapDocShells", this, true); + window.removeEventListener("EndSwapDocShells", this, true); + + this.tabbrowser._switcher = null; + + this.activeSuppressDisplayport.forEach(function(tabParent) { + tabParent.suppressDisplayport(false); + }); + this.activeSuppressDisplayport.clear(); + }, + + finish: function() { + this.log("FINISH"); + + this.assert(this.tabbrowser._switcher); + this.assert(this.tabbrowser._switcher === this); + this.assert(!this.spinnerTab); + this.assert(!this.loadTimer); + this.assert(!this.loadingTab); + this.assert(this.lastVisibleTab === this.requestedTab); + this.assert(this.minimized || this.getTabState(this.requestedTab) == this.STATE_LOADED); + + this.destroy(); + + let toBrowser = this.requestedTab.linkedBrowser; + toBrowser.setAttribute("type", "content-primary"); + + this.tabbrowser._adjustFocusAfterTabSwitch(this.requestedTab); + + let fromBrowser = this.originalTab.linkedBrowser; + // It's possible that the tab we're switching from closed + // before we were able to finalize, in which case, fromBrowser + // doesn't exist. + if (fromBrowser) { + fromBrowser.setAttribute("type", "content-targetable"); + } + + let event = new CustomEvent("TabSwitchDone", { + bubbles: true, + cancelable: true + }); + this.tabbrowser.dispatchEvent(event); + }, + + // This function is called after all the main state changes to + // make sure we display the right tab. + updateDisplay: function() { + // Figure out which tab we actually want visible right now. + let showTab = null; + if (this.getTabState(this.requestedTab) != this.STATE_LOADED && + this.lastVisibleTab && this.loadTimer) { + // If we can't show the requestedTab, and lastVisibleTab is + // available, show it. + showTab = this.lastVisibleTab; + } else { + // Show the requested tab. If it's not available, we'll show the spinner. + showTab = this.requestedTab; + } + + // Show or hide the spinner as needed. + let needSpinner = this.getTabState(showTab) != this.STATE_LOADED && !this.minimized; + if (!needSpinner && this.spinnerTab) { + this.spinnerHidden(); + this.tabbrowser.removeAttribute("pendingpaint"); + this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint"); + this.spinnerTab = null; + } else if (needSpinner && this.spinnerTab !== showTab) { + if (this.spinnerTab) { + this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint"); + } else { + this.spinnerDisplayed(); + } + this.spinnerTab = showTab; + this.tabbrowser.setAttribute("pendingpaint", "true"); + this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true"); + } + + // Switch to the tab we've decided to make visible. + if (this.visibleTab !== showTab) { + this.visibleTab = showTab; + + this.maybeVisibleTabs.add(showTab); + + let tabs = this.tabbrowser.mTabBox.tabs; + let tabPanel = this.tabbrowser.mPanelContainer; + let showPanel = tabs.getRelatedElement(showTab); + let index = Array.indexOf(tabPanel.childNodes, showPanel); + if (index != -1) { + this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`); + tabPanel.setAttribute("selectedIndex", index); + if (showTab === this.requestedTab) { + this.tabbrowser._adjustFocusAfterTabSwitch(showTab); + } + } + + // This doesn't necessarily exist if we're a new window and haven't switched tabs yet + if (this.lastVisibleTab) + this.lastVisibleTab._visuallySelected = false; + + this.visibleTab._visuallySelected = true; + } + + this.lastVisibleTab = this.visibleTab; + }, + + assert: function(cond) { + if (!cond) { + dump("Assertion failure\n" + Error().stack); + + // Don't break a user's browser if an assertion fails. + if (this.tabbrowser.AppConstants.DEBUG) { + throw new Error("Assertion failure"); + } + } + }, + + // We've decided to try to load requestedTab. + loadRequestedTab: function() { + this.assert(!this.loadTimer); + this.assert(!this.minimized); + + // loadingTab can be non-null here if we timed out loading the current tab. + // In that case we just overwrite it with a different tab; it's had its chance. + this.loadingTab = this.requestedTab; + this.log("Loading tab " + this.tinfo(this.loadingTab)); + + this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT); + this.setTabState(this.requestedTab, this.STATE_LOADING); + }, + + // This function runs before every event. It fixes up the state + // to account for closed tabs. + preActions: function() { + this.assert(this.tabbrowser._switcher); + this.assert(this.tabbrowser._switcher === this); + + for (let [tab, ] of this.tabState) { + if (!tab.linkedBrowser) { + this.tabState.delete(tab); + } + } + + if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) { + this.lastVisibleTab = null; + } + if (this.spinnerTab && !this.spinnerTab.linkedBrowser) { + this.spinnerHidden(); + this.spinnerTab = null; + } + if (this.loadingTab && !this.loadingTab.linkedBrowser) { + this.loadingTab = null; + this.clearTimer(this.loadTimer); + this.loadTimer = null; + } + }, + + // This code runs after we've responded to an event or requested a new + // tab. It's expected that we've already updated all the principal + // state variables. This function takes care of updating any auxilliary + // state. + postActions: function() { + // Once we finish loading loadingTab, we null it out. So the state should + // always be LOADING. + this.assert(!this.loadingTab || + this.getTabState(this.loadingTab) == this.STATE_LOADING); + + // We guarantee that loadingTab is non-null iff loadTimer is non-null. So + // the timer is set only when we're loading something. + this.assert(!this.loadTimer || this.loadingTab); + this.assert(!this.loadingTab || this.loadTimer); + + // If we're not loading anything, try loading the requested tab. + let requestedState = this.getTabState(this.requestedTab); + if (!this.loadTimer && !this.minimized && + (requestedState == this.STATE_UNLOADED || + requestedState == this.STATE_UNLOADING)) { + this.loadRequestedTab(); + } + + // See how many tabs still have work to do. + let numPending = 0; + for (let [tab, state] of this.tabState) { + // Skip print preview browsers since they shouldn't affect tab switching. + if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) { + continue; + } + + if (state == this.STATE_LOADED && tab !== this.requestedTab) { + numPending++; + } + if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) { + numPending++; + } + } + + this.updateDisplay(); + + // It's possible for updateDisplay to trigger one of our own event + // handlers, which might cause finish() to already have been called. + // Check for that before calling finish() again. + if (!this.tabbrowser._switcher) { + return; + } + + if (numPending == 0) { + this.finish(); + } + + this.logState("done"); + }, + + // Fires when we're ready to unload unused tabs. + onUnloadTimeout: function() { + this.logState("onUnloadTimeout"); + this.unloadTimer = null; + this.preActions(); + + let numPending = 0; + + // Unload any tabs that can be unloaded. + for (let [tab, state] of this.tabState) { + if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) { + continue; + } + + if (state == this.STATE_LOADED && + !this.maybeVisibleTabs.has(tab) && + tab !== this.lastVisibleTab && + tab !== this.loadingTab && + tab !== this.requestedTab) + { + this.setTabState(tab, this.STATE_UNLOADING); + } + + if (state != this.STATE_UNLOADED && tab !== this.requestedTab) { + numPending++; + } + } + + if (numPending) { + // Keep the timer going since there may be more tabs to unload. + this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY); + } + + this.postActions(); + }, + + // Fires when an ongoing load has taken too long. + onLoadTimeout: function() { + this.logState("onLoadTimeout"); + this.preActions(); + this.loadTimer = null; + this.loadingTab = null; + this.postActions(); + }, + + // Fires when the layers become available for a tab. + onLayersReady: function(browser) { + let tab = this.tabbrowser.getTabForBrowser(browser); + this.logState(`onLayersReady(${tab._tPos})`); + + this.assert(this.getTabState(tab) == this.STATE_LOADING || + this.getTabState(tab) == this.STATE_LOADED); + this.setTabState(tab, this.STATE_LOADED); + + this.maybeFinishTabSwitch(); + + if (this.loadingTab === tab) { + this.clearTimer(this.loadTimer); + this.loadTimer = null; + this.loadingTab = null; + } + }, + + // Fires when we paint the screen. Any tab switches we initiated + // previously are done, so there's no need to keep the old layers + // around. + onPaint: function() { + this.maybeVisibleTabs.clear(); + this.maybeFinishTabSwitch(); + }, + + // Called when we're done clearing the layers for a tab. + onLayersCleared: function(browser) { + let tab = this.tabbrowser.getTabForBrowser(browser); + if (tab) { + this.logState(`onLayersCleared(${tab._tPos})`); + this.assert(this.getTabState(tab) == this.STATE_UNLOADING || + this.getTabState(tab) == this.STATE_UNLOADED); + this.setTabState(tab, this.STATE_UNLOADED); + } + }, + + // Called when a tab switches from remote to non-remote. In this case + // a MozLayerTreeReady notification that we requested may never fire, + // so we need to simulate it. + onRemotenessChange: function(tab) { + this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`); + if (!tab.linkedBrowser.isRemoteBrowser) { + if (this.getTabState(tab) == this.STATE_LOADING) { + this.onLayersReady(tab.linkedBrowser); + } else if (this.getTabState(tab) == this.STATE_UNLOADING) { + this.onLayersCleared(tab.linkedBrowser); + } + } + }, + + // Called when a tab has been removed, and the browser node is + // about to be removed from the DOM. + onTabRemoved: function(tab) { + if (this.lastVisibleTab == tab) { + // The browser that was being presented to the user is + // going to be removed during this tick of the event loop. + // This will cause us to show a tab spinner instead. + this.preActions(); + this.lastVisibleTab = null; + this.postActions(); + } + }, + + onSizeModeChange() { + if (this.minimized) { + for (let [tab, state] of this.tabState) { + // Skip print preview browsers since they shouldn't affect tab switching. + if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) { + continue; + } + + if (state == this.STATE_LOADING || state == this.STATE_LOADED) { + this.setTabState(tab, this.STATE_UNLOADING); + } + } + if (this.loadTimer) { + this.clearTimer(this.loadTimer); + this.loadTimer = null; + } + this.loadingTab = null; + } else { + // Do nothing. We'll automatically start loading the requested tab in + // postActions. + } + }, + + onSwapDocShells(ourBrowser, otherBrowser) { + // This event fires before the swap. ourBrowser is from + // our window. We save the state of otherBrowser since ourBrowser + // needs to take on that state at the end of the swap. + + let otherTabbrowser = otherBrowser.ownerDocument.defaultView.gBrowser; + let otherState; + if (otherTabbrowser && otherTabbrowser._switcher) { + let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser); + otherState = otherTabbrowser._switcher.getTabState(otherTab); + } else { + otherState = (otherBrowser.docShellIsActive + ? this.STATE_LOADED + : this.STATE_UNLOADED); + } + + if (!this.swapMap) { + this.swapMap = new WeakMap(); + } + this.swapMap.set(otherBrowser, otherState); + }, + + onEndSwapDocShells(ourBrowser, otherBrowser) { + // The swap has happened. We reset the loadingTab in + // case it has been swapped. We also set ourBrowser's state + // to whatever otherBrowser's state was before the swap. + + if (this.loadTimer) { + // Clearing the load timer means that we will + // immediately display a spinner if ourBrowser isn't + // ready yet. Typically it will already be ready + // though. If it's not, we're probably in a new window, + // in which case we have no other tabs to display anyway. + this.clearTimer(this.loadTimer); + this.loadTimer = null; + } + this.loadingTab = null; + + let otherState = this.swapMap.get(otherBrowser); + this.swapMap.delete(otherBrowser); + + let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser); + if (ourTab) { + this.setTabStateNoAction(ourTab, otherState); + } + }, + + shouldActivateDocShell(browser) { + let tab = this.tabbrowser.getTabForBrowser(browser); + let state = this.getTabState(tab); + return state == this.STATE_LOADING || state == this.STATE_LOADED; + }, + + activateBrowserForPrintPreview(browser) { + let tab = this.tabbrowser.getTabForBrowser(browser); + this.setTabState(tab, this.STATE_LOADING); + }, + + // Called when the user asks to switch to a given tab. + requestTab: function(tab) { + if (tab === this.requestedTab) { + return; + } + + this.logState("requestTab " + this.tinfo(tab)); + this.startTabSwitch(); + + this.requestedTab = tab; + + let browser = this.requestedTab.linkedBrowser; + let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; + if (fl && fl.tabParent && !this.activeSuppressDisplayport.has(fl.tabParent)) { + fl.tabParent.suppressDisplayport(true); + this.activeSuppressDisplayport.add(fl.tabParent); + } + + this.preActions(); + + if (this.unloadTimer) { + this.clearTimer(this.unloadTimer); + } + this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY); + + this.postActions(); + }, + + handleEvent: function(event, delayed = false) { + if (this._processing) { + this.setTimer(() => this.handleEvent(event, true), 0); + return; + } + if (delayed && this.tabbrowser._switcher != this) { + // if we delayed processing this event, we might be out of date, in which + // case we drop the delayed events + return; + } + this._processing = true; + this.preActions(); + + if (event.type == "MozLayerTreeReady") { + this.onLayersReady(event.originalTarget); + } if (event.type == "MozAfterPaint") { + this.onPaint(); + } else if (event.type == "MozLayerTreeCleared") { + this.onLayersCleared(event.originalTarget); + } else if (event.type == "TabRemotenessChange") { + this.onRemotenessChange(event.target); + } else if (event.type == "sizemodechange") { + this.onSizeModeChange(); + } else if (event.type == "SwapDocShells") { + this.onSwapDocShells(event.originalTarget, event.detail); + } else if (event.type == "EndSwapDocShells") { + this.onEndSwapDocShells(event.originalTarget, event.detail); + } + + this.postActions(); + this._processing = false; + }, + + /* + * Telemetry and Profiler related helpers for recording tab switch + * timing. + */ + + startTabSwitch: function () { + TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", window); + TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", window); + this.addMarker("AsyncTabSwitch:Start"); + this.switchInProgress = true; + }, + + /** + * Something has occurred that might mean that we've completed + * the tab switch (layers are ready, paints are done, spinners + * are hidden). This checks to make sure all conditions are + * satisfied, and then records the tab switch as finished. + */ + maybeFinishTabSwitch: function () { + if (this.switchInProgress && this.requestedTab && + this.getTabState(this.requestedTab) == this.STATE_LOADED) { + // After this point the tab has switched from the content thread's point of view. + // The changes will be visible after the next refresh driver tick + composite. + let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", window); + if (time != -1) { + TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window); + this.log("DEBUG: tab switch time = " + time); + this.addMarker("AsyncTabSwitch:Finish"); + } + this.switchInProgress = false; + } + }, + + spinnerDisplayed: function () { + this.assert(!this.spinnerTab); + TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window); + // We have a second, similar probe for capturing recordings of + // when the spinner is displayed for very long periods. + TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window); + this.addMarker("AsyncTabSwitch:SpinnerShown"); + }, + + spinnerHidden: function () { + this.assert(this.spinnerTab); + this.log("DEBUG: spinner time = " + + TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window)); + TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window); + TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window); + this.addMarker("AsyncTabSwitch:SpinnerHidden"); + // we do not get a onPaint after displaying the spinner + this.maybeFinishTabSwitch(); + }, + + addMarker: function(marker) { + if (Services.profiler) { + Services.profiler.AddMarker(marker); + } + }, + + /* + * Debug related logging for switcher. + */ + + _useDumpForLogging: false, + _logInit: false, + + logging: function () { + if (this._useDumpForLogging) + return true; + if (this._logInit) + return this._shouldLog; + let result = false; + try { + result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming"); + } catch (ex) { + } + this._shouldLog = result; + this._logInit = true; + return this._shouldLog; + }, + + tinfo: function(tab) { + if (tab) { + return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")"; + } + return "null"; + }, + + log: function(s) { + if (!this.logging()) + return; + if (this._useDumpForLogging) { + dump(s + "\n"); + } else { + Services.console.logStringMessage(s); + } + }, + + logState: function(prefix) { + if (!this.logging()) + return; + + let accum = prefix + " "; + for (let i = 0; i < this.tabbrowser.tabs.length; i++) { + let tab = this.tabbrowser.tabs[i]; + let state = this.getTabState(tab); + + accum += i + ":"; + if (tab === this.lastVisibleTab) accum += "V"; + if (tab === this.loadingTab) accum += "L"; + if (tab === this.requestedTab) accum += "R"; + if (state == this.STATE_LOADED) accum += "(+)"; + if (state == this.STATE_LOADING) accum += "(+?)"; + if (state == this.STATE_UNLOADED) accum += "(-)"; + if (state == this.STATE_UNLOADING) accum += "(-?)"; + accum += " "; + } + if (this._useDumpForLogging) { + dump(accum + "\n"); + } else { + Services.console.logStringMessage(accum); + } + }, + }; + this._switcher = switcher; + switcher.init(); + return switcher; + ]]></body> + </method> + + <!-- BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT + MAKE SURE TO ADD IT HERE AS WELL. --> + <property name="canGoBack" + onget="return this.mCurrentBrowser.canGoBack;" + readonly="true"/> + + <property name="canGoForward" + onget="return this.mCurrentBrowser.canGoForward;" + readonly="true"/> + + <method name="goBack"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goBack(); + ]]> + </body> + </method> + + <method name="goForward"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goForward(); + ]]> + </body> + </method> + + <method name="reload"> + <body> + <![CDATA[ + return this.mCurrentBrowser.reload(); + ]]> + </body> + </method> + + <method name="reloadWithFlags"> + <parameter name="aFlags"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.reloadWithFlags(aFlags); + ]]> + </body> + </method> + + <method name="stop"> + <body> + <![CDATA[ + return this.mCurrentBrowser.stop(); + ]]> + </body> + </method> + + <!-- throws exception for unknown schemes --> + <method name="loadURI"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.loadURI(aURI, aReferrerURI, aCharset); + ]]> + </body> + </method> + + <!-- throws exception for unknown schemes --> + <method name="loadURIWithFlags"> + <parameter name="aURI"/> + <parameter name="aFlags"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <body> + <![CDATA[ + // Note - the callee understands both: + // (a) loadURIWithFlags(aURI, aFlags, ...) + // (b) loadURIWithFlags(aURI, { flags: aFlags, ... }) + // Forwarding it as (a) here actually supports both (a) and (b), + // so you can call us either way too. + return this.mCurrentBrowser.loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData); + ]]> + </body> + </method> + + <method name="goHome"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goHome(); + ]]> + </body> + </method> + + <property name="homePage"> + <getter> + <![CDATA[ + return this.mCurrentBrowser.homePage; + ]]> + </getter> + <setter> + <![CDATA[ + this.mCurrentBrowser.homePage = val; + return val; + ]]> + </setter> + </property> + + <method name="gotoIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.gotoIndex(aIndex); + ]]> + </body> + </method> + + <property name="currentURI" + onget="return this.mCurrentBrowser.currentURI;" + readonly="true"/> + + <property name="finder" + onget="return this.mCurrentBrowser.finder" + readonly="true"/> + + <property name="docShell" + onget="return this.mCurrentBrowser.docShell" + readonly="true"/> + + <property name="webNavigation" + onget="return this.mCurrentBrowser.webNavigation" + readonly="true"/> + + <property name="webBrowserFind" + readonly="true" + onget="return this.mCurrentBrowser.webBrowserFind"/> + + <property name="webProgress" + readonly="true" + onget="return this.mCurrentBrowser.webProgress"/> + + <property name="contentWindow" + readonly="true" + onget="return this.mCurrentBrowser.contentWindow"/> + + <property name="contentWindowAsCPOW" + readonly="true" + onget="return this.mCurrentBrowser.contentWindowAsCPOW"/> + + <property name="sessionHistory" + onget="return this.mCurrentBrowser.sessionHistory;" + readonly="true"/> + + <property name="markupDocumentViewer" + onget="return this.mCurrentBrowser.markupDocumentViewer;" + readonly="true"/> + + <property name="contentViewerEdit" + onget="return this.mCurrentBrowser.contentViewerEdit;" + readonly="true"/> + + <property name="contentViewerFile" + onget="return this.mCurrentBrowser.contentViewerFile;" + readonly="true"/> + + <property name="contentDocument" + onget="return this.mCurrentBrowser.contentDocument;" + readonly="true"/> + + <property name="contentDocumentAsCPOW" + onget="return this.mCurrentBrowser.contentDocumentAsCPOW;" + readonly="true"/> + + <property name="contentTitle" + onget="return this.mCurrentBrowser.contentTitle;" + readonly="true"/> + + <property name="contentPrincipal" + onget="return this.mCurrentBrowser.contentPrincipal;" + readonly="true"/> + + <property name="securityUI" + onget="return this.mCurrentBrowser.securityUI;" + readonly="true"/> + + <property name="fullZoom" + onget="return this.mCurrentBrowser.fullZoom;" + onset="this.mCurrentBrowser.fullZoom = val;"/> + + <property name="textZoom" + onget="return this.mCurrentBrowser.textZoom;" + onset="this.mCurrentBrowser.textZoom = val;"/> + + <property name="isSyntheticDocument" + onget="return this.mCurrentBrowser.isSyntheticDocument;" + readonly="true"/> + + <method name="_handleKeyDownEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!aEvent.isTrusted) { + // Don't let untrusted events mess with tabs. + return; + } + + if (aEvent.altKey) + return; + + // Don't check if the event was already consumed because tab + // navigation should always work for better user experience. + + if (aEvent.ctrlKey && aEvent.shiftKey && !aEvent.metaKey) { + switch (aEvent.keyCode) { + case aEvent.DOM_VK_PAGE_UP: + this.moveTabBackward(); + aEvent.preventDefault(); + return; + case aEvent.DOM_VK_PAGE_DOWN: + this.moveTabForward(); + aEvent.preventDefault(); + return; + } + } + + if (this.AppConstants.platform != "macosx") { + if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey && + aEvent.keyCode == KeyEvent.DOM_VK_F4 && + !this.mCurrentTab.pinned) { + this.removeCurrentTab({animate: true}); + aEvent.preventDefault(); + } + } + ]]></body> + </method> + + <method name="_handleKeyPressEventMac"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!aEvent.isTrusted) { + // Don't let untrusted events mess with tabs. + return; + } + + if (aEvent.altKey) + return; + + if (this.AppConstants.platform == "macosx") { + if (!aEvent.metaKey) + return; + + var offset = 1; + switch (aEvent.charCode) { + case '}'.charCodeAt(0): + offset = -1; + case '{'.charCodeAt(0): + if (window.getComputedStyle(this, null).direction == "ltr") + offset *= -1; + this.tabContainer.advanceSelectedTab(offset, true); + aEvent.preventDefault(); + } + } + ]]></body> + </method> + + <property name="userTypedValue" + onget="return this.mCurrentBrowser.userTypedValue;" + onset="return this.mCurrentBrowser.userTypedValue = val;"/> + + <method name="createTooltip"> + <parameter name="event"/> + <body><![CDATA[ + event.stopPropagation(); + var tab = document.tooltipNode; + if (tab.localName != "tab") { + event.preventDefault(); + return; + } + + let stringWithShortcut = (stringId, keyElemId) => { + let keyElem = document.getElementById(keyElemId); + let shortcut = ShortcutUtils.prettifyShortcut(keyElem); + return this.mStringBundle.getFormattedString(stringId, [shortcut]); + }; + + var label; + if (tab.mOverCloseButton) { + label = tab.selected ? + stringWithShortcut("tabs.closeSelectedTab.tooltip", "key_close") : + this.mStringBundle.getString("tabs.closeTab.tooltip"); + } else if (tab._overPlayingIcon) { + let stringID; + if (tab.selected) { + stringID = tab.linkedBrowser.audioMuted ? + "tabs.unmuteAudio.tooltip" : + "tabs.muteAudio.tooltip"; + label = stringWithShortcut(stringID, "key_toggleMute"); + } else { + if (tab.linkedBrowser.audioBlocked) { + stringID = "tabs.unblockAudio.tooltip"; + } else { + stringID = tab.linkedBrowser.audioMuted ? + "tabs.unmuteAudio.background.tooltip" : + "tabs.muteAudio.background.tooltip"; + } + + label = this.mStringBundle.getString(stringID); + } + } else { + label = tab.getAttribute("label"); + } + event.target.setAttribute("label", label); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "keydown": + this._handleKeyDownEvent(aEvent); + break; + case "keypress": + this._handleKeyPressEventMac(aEvent); + break; + case "sizemodechange": + if (aEvent.target == window && !this._switcher) { + this.mCurrentBrowser.preserveLayers(window.windowState == window.STATE_MINIMIZED); + this.mCurrentBrowser.docShellIsActive = this.shouldActivateDocShell(this.mCurrentBrowser); + } + break; + } + ]]></body> + </method> + + <method name="receiveMessage"> + <parameter name="aMessage"/> + <body><![CDATA[ + let data = aMessage.data; + let browser = aMessage.target; + + switch (aMessage.name) { + case "DOMTitleChanged": { + let tab = this.getTabForBrowser(browser); + if (!tab || tab.hasAttribute("pending")) + return undefined; + let titleChanged = this.setTabTitle(tab); + if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) + tab.setAttribute("titlechanged", "true"); + break; + } + case "DOMWindowClose": { + if (this.tabs.length == 1) { + // We already did PermitUnload in the content process + // for this tab (the only one in the window). So we don't + // need to do it again for any tabs. + window.skipNextCanClose = true; + window.close(); + return undefined; + } + + let tab = this.getTabForBrowser(browser); + if (tab) { + // Skip running PermitUnload since it already happened in + // the content process. + this.removeTab(tab, {skipPermitUnload: true}); + } + break; + } + case "contextmenu": { + let spellInfo = data.spellInfo; + if (spellInfo) + spellInfo.target = aMessage.target.messageManager; + let documentURIObject = makeURI(data.docLocation, + data.charSet, + makeURI(data.baseURI)); + gContextMenuContentData = { isRemote: true, + event: aMessage.objects.event, + popupNode: aMessage.objects.popupNode, + browser: browser, + editFlags: data.editFlags, + spellInfo: spellInfo, + principal: data.principal, + customMenuItems: data.customMenuItems, + addonInfo: data.addonInfo, + documentURIObject: documentURIObject, + docLocation: data.docLocation, + charSet: data.charSet, + referrer: data.referrer, + referrerPolicy: data.referrerPolicy, + contentType: data.contentType, + contentDisposition: data.contentDisposition, + frameOuterWindowID: data.frameOuterWindowID, + selectionInfo: data.selectionInfo, + disableSetDesktopBackground: data.disableSetDesktopBg, + loginFillInfo: data.loginFillInfo, + parentAllowsMixedContent: data.parentAllowsMixedContent, + userContextId: data.userContextId, + }; + let popup = browser.ownerDocument.getElementById("contentAreaContextMenu"); + let event = gContextMenuContentData.event; + popup.openPopupAtScreen(event.screenX, event.screenY, true); + break; + } + case "DOMServiceWorkerFocusClient": + case "DOMWebNotificationClicked": { + let tab = this.getTabForBrowser(browser); + if (!tab) + return undefined; + this.selectedTab = tab; + window.focus(); + break; + } + case "Browser:Init": { + let tab = this.getTabForBrowser(browser); + if (!tab) + return undefined; + + this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser); + browser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned }) + break; + } + case "Browser:WindowCreated": { + let tab = this.getTabForBrowser(browser); + if (tab && data.userContextId) { + ContextualIdentityService.telemetry(data.userContextId); + tab.setUserContextId(data.userContextId); + } + + // We don't want to update the container icon and identifier if + // this is not the selected browser. + if (browser == gBrowser.selectedBrowser) { + updateUserContextUIIndicator(); + } + + break; + } + case "Findbar:Keypress": { + let tab = this.getTabForBrowser(browser); + // If the find bar for this tab is not yet alive, only initialize + // it if there's a possibility FindAsYouType will be used. + // There's no point in doing it for most random keypresses. + if (!this.isFindBarInitialized(tab) && + data.shouldFastFind) { + let shouldFastFind = this._findAsYouType; + if (!shouldFastFind) { + // Please keep in sync with toolkit/content/widgets/findbar.xml + const FAYT_LINKS_KEY = "'"; + const FAYT_TEXT_KEY = "/"; + let charCode = data.fakeEvent.charCode; + let key = charCode ? String.fromCharCode(charCode) : null; + shouldFastFind = key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY; + } + if (shouldFastFind) { + // Make sure we return the result. + return this.getFindBar(tab).receiveMessage(aMessage); + } + } + break; + } + case "RefreshBlocker:Blocked": { + let event = new CustomEvent("RefreshBlocked", { + bubbles: true, + cancelable: false, + detail: data, + }); + + browser.dispatchEvent(event); + + break; + } + + } + return undefined; + ]]></body> + </method> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + let browser; + switch (aTopic) { + case "live-resize-start": + browser = this.mCurrentTab.linkedBrowser; + let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; + if (fl && fl.tabParent && !this.mActiveResizeDisplayportSuppression) { + fl.tabParent.suppressDisplayport(true); + this.mActiveResizeDisplayportSuppression = browser; + } + break; + case "live-resize-end": + browser = this.mActiveResizeDisplayportSuppression; + if (browser) { + let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; + if (fl && fl.tabParent) { + fl.tabParent.suppressDisplayport(false); + this.mActiveResizeDisplayportSuppression = null; + } + } + break; + case "nsPref:changed": + // This is the only pref observed. + this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind"); + break; + } + ]]></body> + </method> + + <constructor> + <![CDATA[ + this.mCurrentBrowser = document.getAnonymousElementByAttribute(this, "anonid", "initialBrowser"); + this.mCurrentBrowser.permanentKey = {}; + + Services.obs.addObserver(this, "live-resize-start", false); + Services.obs.addObserver(this, "live-resize-end", false); + + this.mCurrentTab = this.tabContainer.firstChild; + const nsIEventListenerService = + Components.interfaces.nsIEventListenerService; + let els = Components.classes["@mozilla.org/eventlistenerservice;1"] + .getService(nsIEventListenerService); + els.addSystemEventListener(document, "keydown", this, false); + if (this.AppConstants.platform == "macosx") { + els.addSystemEventListener(document, "keypress", this, false); + } + window.addEventListener("sizemodechange", this, false); + + var uniqueId = this._generateUniquePanelID(); + this.mPanelContainer.childNodes[0].id = uniqueId; + this.mCurrentTab.linkedPanel = uniqueId; + this.mCurrentTab.permanentKey = this.mCurrentBrowser.permanentKey; + this.mCurrentTab._tPos = 0; + this.mCurrentTab._fullyOpen = true; + this.mCurrentTab.cachePosition = 0; + this.mCurrentTab.linkedBrowser = this.mCurrentBrowser; + this.mCurrentTab.hasBrowser = true; + this._tabForBrowser.set(this.mCurrentBrowser, this.mCurrentTab); + + // set up the shared autoscroll popup + this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup(); + this._autoScrollPopup.id = "autoscroller"; + this.appendChild(this._autoScrollPopup); + this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id); + this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink; + + // Hook up the event listeners to the first browser + var tabListener = this.mTabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true, false); + const nsIWebProgress = Components.interfaces.nsIWebProgress; + const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(nsIWebProgress); + filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL); + this._tabListeners.set(this.mCurrentTab, tabListener); + this._tabFilters.set(this.mCurrentTab, filter); + this.webProgress.addProgressListener(filter, nsIWebProgress.NOTIFY_ALL); + + this.style.backgroundColor = + Services.prefs.getBoolPref("browser.display.use_system_colors") ? + "-moz-default-background-color" : + Services.prefs.getCharPref("browser.display.background_color"); + + let remote = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext) + .useRemoteTabs; + if (remote) { + messageManager.addMessageListener("DOMTitleChanged", this); + messageManager.addMessageListener("DOMWindowClose", this); + messageManager.addMessageListener("contextmenu", this); + messageManager.addMessageListener("Browser:Init", this); + + // If this window has remote tabs, switch to our tabpanels fork + // which does asynchronous tab switching. + this.mPanelContainer.classList.add("tabbrowser-tabpanels"); + } else { + this._outerWindowIDBrowserMap.set(this.mCurrentBrowser.outerWindowID, + this.mCurrentBrowser); + } + messageManager.addMessageListener("DOMWebNotificationClicked", this); + messageManager.addMessageListener("DOMServiceWorkerFocusClient", this); + messageManager.addMessageListener("RefreshBlocker:Blocked", this); + messageManager.addMessageListener("Browser:WindowCreated", this); + + // To correctly handle keypresses for potential FindAsYouType, while + // the tab's find bar is not yet initialized. + this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind"); + Services.prefs.addObserver("accessibility.typeaheadfind", this, false); + messageManager.addMessageListener("Findbar:Keypress", this); + ]]> + </constructor> + + <method name="_generateUniquePanelID"> + <body><![CDATA[ + if (!this._uniquePanelIDCounter) { + this._uniquePanelIDCounter = 0; + } + + let outerID = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + + // We want panel IDs to be globally unique, that's why we include the + // window ID. We switched to a monotonic counter as Date.now() lead + // to random failures because of colliding IDs. + return "panel-" + outerID + "-" + (++this._uniquePanelIDCounter); + ]]></body> + </method> + + <destructor> + <![CDATA[ + Services.obs.removeObserver(this, "live-resize-start", false); + Services.obs.removeObserver(this, "live-resize-end", false); + + for (let tab of this.tabs) { + let browser = tab.linkedBrowser; + if (browser.registeredOpenURI) { + this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI, + browser.getAttribute("usercontextid") || 0); + delete browser.registeredOpenURI; + } + let filter = this._tabFilters.get(tab); + let listener = this._tabListeners.get(tab); + + browser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(listener); + listener.destroy(); + + this._tabFilters.delete(tab); + this._tabListeners.delete(tab); + } + const nsIEventListenerService = + Components.interfaces.nsIEventListenerService; + let els = Components.classes["@mozilla.org/eventlistenerservice;1"] + .getService(nsIEventListenerService); + els.removeSystemEventListener(document, "keydown", this, false); + if (this.AppConstants.platform == "macosx") { + els.removeSystemEventListener(document, "keypress", this, false); + } + window.removeEventListener("sizemodechange", this, false); + + if (gMultiProcessBrowser) { + messageManager.removeMessageListener("DOMTitleChanged", this); + messageManager.removeMessageListener("contextmenu", this); + + if (this._switcher) { + this._switcher.destroy(); + } + } + + Services.prefs.removeObserver("accessibility.typeaheadfind", this); + ]]> + </destructor> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <method name="enterTabbedMode"> + <body> + Services.console.logStringMessage("enterTabbedMode is an obsolete method and " + + "will be removed in a future release."); + </body> + </method> + <field name="mTabbedMode" readonly="true">true</field> + <method name="setStripVisibilityTo"> + <parameter name="aShow"/> + <body> + this.tabContainer.visible = aShow; + </body> + </method> + <method name="getStripVisibility"> + <body> + return this.tabContainer.visible; + </body> + </method> + + <property name="mContextTab" readonly="true" + onget="return TabContextMenu.contextTab;"/> + <property name="mPrefs" readonly="true" + onget="return Services.prefs;"/> + <property name="mTabContainer" readonly="true" + onget="return this.tabContainer;"/> + <property name="mTabs" readonly="true" + onget="return this.tabs;"/> + <!-- + - Compatibility hack: several extensions depend on this property to + - access the tab context menu or tab container, so keep that working for + - now. Ideally we can remove this once extensions are using + - tabbrowser.tabContextMenu and tabbrowser.tabContainer directly. + --> + <property name="mStrip" readonly="true"> + <getter> + <![CDATA[ + return ({ + self: this, + childNodes: [null, this.tabContextMenu, this.tabContainer], + firstChild: { nextSibling: this.tabContextMenu }, + getElementsByAttribute: function (attr, attrValue) { + if (attr == "anonid" && attrValue == "tabContextMenu") + return [this.self.tabContextMenu]; + return []; + }, + // Also support adding event listeners (forward to the tab container) + addEventListener: function (a, b, c) { this.self.tabContainer.addEventListener(a, b, c); }, + removeEventListener: function (a, b, c) { this.self.tabContainer.removeEventListener(a, b, c); } + }); + ]]> + </getter> + </property> + <field name="_soundPlayingAttrRemovalTimer">0</field> + </implementation> + + <handlers> + <handler event="DOMWindowClose" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + if (this.tabs.length == 1) { + // We already did PermitUnload in nsGlobalWindow::Close + // for this tab. There are no other tabs we need to do + // PermitUnload for. + window.skipNextCanClose = true; + return; + } + + var tab = this._getTabForContentWindow(event.target); + if (tab) { + // Skip running PermitUnload since it already happened. + this.removeTab(tab, {skipPermitUnload: true}); + event.preventDefault(); + } + ]]> + </handler> + <handler event="DOMWillOpenModalDialog" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + let targetIsWindow = event.target instanceof Window; + + // We're about to open a modal dialog, so figure out for which tab: + // If this is a same-process modal dialog, then we're given its DOM + // window as the event's target. For remote dialogs, we're given the + // browser, but that's in the originalTarget and not the target, + // because it's across the tabbrowser's XBL boundary. + let tabForEvent = targetIsWindow ? + this._getTabForContentWindow(event.target.top) : + this.getTabForBrowser(event.originalTarget); + + // Don't need to act if the tab is already selected: + if (tabForEvent.selected) + return; + + // If this is a tabprompt, we won't switch tabs, unless: + // - this is a beforeunload prompt + // - this behaviour has been disabled entirely using the pref + if (event.detail && event.detail.tabPrompt && + !event.detail.inPermitUnload && + Services.prefs.getBoolPref("browser.tabs.dontfocusfordialogs")) { + let docPrincipal = targetIsWindow ? event.target.document.nodePrincipal : null; + // At least one of these should/will be non-null: + let promptPrincipal = event.detail.promptPrincipal || docPrincipal || + tabForEvent.linkedBrowser.contentPrincipal; + // For null principals, we bail immediately and don't show the checkbox: + if (!promptPrincipal || promptPrincipal.isNullPrincipal) { + tabForEvent.setAttribute("attention", "true"); + return; + } + + // For non-system/expanded principals, we bail and show the checkbox + if (promptPrincipal.URI && + !Services.scriptSecurityManager.isSystemPrincipal(promptPrincipal)) { + let permission = Services.perms.testPermissionFromPrincipal(promptPrincipal, + "focus-tab-by-prompt"); + if (permission != Services.perms.ALLOW_ACTION) { + // Tell the prompt box we want to show the user a checkbox: + let tabPrompt = this.getTabModalPromptBox(tabForEvent.linkedBrowser); + tabPrompt.onNextPromptShowAllowFocusCheckboxFor(promptPrincipal); + tabForEvent.setAttribute("attention", "true"); + return; + } + } + // ... so system and expanded principals, as well as permitted "normal" + // URI-based principals, always get to steal focus for the tab when prompting. + } + + // if prefs/permissions/origins so dictate, bring tab to the front: + this.selectedTab = tabForEvent; + ]]> + </handler> + <handler event="DOMTitleChanged"> + <![CDATA[ + if (!event.isTrusted) + return; + + var contentWin = event.target.defaultView; + if (contentWin != contentWin.top) + return; + + var tab = this._getTabForContentWindow(contentWin); + if (!tab || tab.hasAttribute("pending")) + return; + + var titleChanged = this.setTabTitle(tab); + if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) + tab.setAttribute("titlechanged", "true"); + ]]> + </handler> + <handler event="oop-browser-crashed"> + <![CDATA[ + if (!event.isTrusted) + return; + + let browser = event.originalTarget; + let icon = browser.mIconURL; + let tab = this.getTabForBrowser(browser); + + if (this.selectedBrowser == browser) { + TabCrashHandler.onSelectedBrowserCrash(browser); + } else { + this.updateBrowserRemoteness(browser, false); + SessionStore.reviveCrashedTab(tab); + } + + tab.removeAttribute("soundplaying"); + this.setIcon(tab, icon, browser.contentPrincipal); + ]]> + </handler> + <handler event="DOMAudioPlaybackStarted"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + clearTimeout(tab._soundPlayingAttrRemovalTimer); + tab._soundPlayingAttrRemovalTimer = 0; + + let modifiedAttrs = []; + if (tab.hasAttribute("soundplaying-scheduledremoval")) { + tab.removeAttribute("soundplaying-scheduledremoval"); + modifiedAttrs.push("soundplaying-scheduledremoval"); + } + + if (!tab.hasAttribute("soundplaying")) { + tab.setAttribute("soundplaying", true); + modifiedAttrs.push("soundplaying"); + } + + this._tabAttrModified(tab, modifiedAttrs); + ]]> + </handler> + <handler event="DOMAudioPlaybackStopped"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (tab.hasAttribute("soundplaying")) { + let removalDelay = Services.prefs.getIntPref("browser.tabs.delayHidingAudioPlayingIconMS"); + + tab.style.setProperty("--soundplaying-removal-delay", `${removalDelay - 300}ms`); + tab.setAttribute("soundplaying-scheduledremoval", "true"); + this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]); + + tab._soundPlayingAttrRemovalTimer = setTimeout(() => { + tab.removeAttribute("soundplaying-scheduledremoval"); + tab.removeAttribute("soundplaying"); + this._tabAttrModified(tab, ["soundplaying", "soundplaying-scheduledremoval"]); + }, removalDelay); + } + ]]> + </handler> + <handler event="DOMAudioPlaybackBlockStarted"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (!tab.hasAttribute("blocked")) { + tab.setAttribute("blocked", true); + this._tabAttrModified(tab, ["blocked"]); + } + ]]> + </handler> + <handler event="DOMAudioPlaybackBlockStopped"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (tab.hasAttribute("blocked")) { + tab.removeAttribute("blocked"); + this._tabAttrModified(tab, ["blocked"]); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-tabbox" + extends="chrome://global/content/bindings/tabbox.xml#tabbox"> + <implementation> + <property name="tabs" readonly="true" + onget="return document.getBindingParent(this).tabContainer;"/> + </implementation> + </binding> + + <binding id="tabbrowser-arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll"> + <implementation> + <!-- Override scrollbox.xml method, since our scrollbox's children are + inherited from the binding parent --> + <method name="_getScrollableElements"> + <body><![CDATA[ + return Array.filter(document.getBindingParent(this).childNodes, + this._canScrollToElement, this); + ]]></body> + </method> + <method name="_canScrollToElement"> + <parameter name="tab"/> + <body><![CDATA[ + return !tab.pinned && !tab.hidden; + ]]></body> + </method> + <field name="_tabMarginLeft">null</field> + <field name="_tabMarginRight">null</field> + <method name="_calcTabMargins"> + <parameter name="aTab"/> + <body><![CDATA[ + if (this._tabMarginLeft === null || this._tabMarginRight === null) { + let tabMiddle = document.getAnonymousElementByAttribute(aTab, "class", "tab-background-middle"); + let tabMiddleStyle = window.getComputedStyle(tabMiddle, null); + this._tabMarginLeft = parseFloat(tabMiddleStyle.marginLeft); + this._tabMarginRight = parseFloat(tabMiddleStyle.marginRight); + } + ]]></body> + </method> + <method name="_adjustElementStartAndEnd"> + <parameter name="aTab"/> + <parameter name="tabStart"/> + <parameter name="tabEnd"/> + <body><![CDATA[ + this._calcTabMargins(aTab); + if (this._tabMarginLeft < 0) { + tabStart = tabStart + this._tabMarginLeft; + } + if (this._tabMarginRight < 0) { + tabEnd = tabEnd - this._tabMarginRight; + } + return [tabStart, tabEnd]; + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="underflow" phase="capturing"><![CDATA[ + if (event.detail == 0) + return; // Ignore vertical events + + var tabs = document.getBindingParent(this); + tabs.removeAttribute("overflow"); + + if (tabs._lastTabClosedByMouse) + tabs._expandSpacerBy(this._scrollButtonDown.clientWidth); + + for (let tab of Array.from(tabs.tabbrowser._removingTabs)) + tabs.tabbrowser.removeTab(tab); + + tabs._positionPinnedTabs(); + ]]></handler> + <handler event="overflow"><![CDATA[ + if (event.detail == 0) + return; // Ignore vertical events + + var tabs = document.getBindingParent(this); + tabs.setAttribute("overflow", "true"); + tabs._positionPinnedTabs(); + tabs._handleTabSelect(false); + ]]></handler> + </handlers> + </binding> + + <binding id="tabbrowser-tabs" + extends="chrome://global/content/bindings/tabbox.xml#tabs"> + <resources> + <stylesheet src="chrome://browser/content/tabbrowser.css"/> + </resources> + + <content> + <xul:hbox align="end"> + <xul:image class="tab-drop-indicator" anonid="tab-drop-indicator" collapsed="true"/> + </xul:hbox> + <xul:arrowscrollbox anonid="arrowscrollbox" orient="horizontal" flex="1" + style="min-width: 1px;" + class="tabbrowser-arrowscrollbox"> +<!-- + This is a hack to circumvent bug 472020, otherwise the tabs show up on the + right of the newtab button. +--> + <children includes="tab"/> +<!-- + This is to ensure anything extensions put here will go before the newtab + button, necessary due to the previous hack. +--> + <children/> + <xul:toolbarbutton class="tabs-newtab-button" + anonid="tabs-newtab-button" + command="cmd_newNavigatorTab" + onclick="checkForMiddleClick(this, event);" + onmouseover="document.getBindingParent(this)._enterNewTab();" + onmouseout="document.getBindingParent(this)._leaveNewTab();" + tooltip="dynamic-shortcut-tooltip"/> + <xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer" + style="width: 0;"/> + </xul:arrowscrollbox> + </content> + + <implementation implements="nsIDOMEventListener, nsIObserver"> + <constructor> + <![CDATA[ + this.mTabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth"); + + var tab = this.firstChild; + tab.label = this.tabbrowser.mStringBundle.getString("tabs.emptyTabTitle"); + tab.setAttribute("crop", "end"); + tab.setAttribute("onerror", "this.removeAttribute('image');"); + + window.addEventListener("resize", this, false); + window.addEventListener("load", this, false); + + try { + this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled"); + } catch (ex) { + this._tabAnimationLoggingEnabled = false; + } + this._browserNewtabpageEnabled = Services.prefs.getBoolPref("browser.newtabpage.enabled"); + this.observe(null, "nsPref:changed", "privacy.userContext.enabled"); + Services.prefs.addObserver("privacy.userContext.enabled", this, false); + ]]> + </constructor> + + <destructor> + <![CDATA[ + Services.prefs.removeObserver("privacy.userContext.enabled", this); + ]]> + </destructor> + + <field name="tabbrowser" readonly="true"> + document.getElementById(this.getAttribute("tabbrowser")); + </field> + + <field name="tabbox" readonly="true"> + this.tabbrowser.mTabBox; + </field> + + <field name="contextMenu" readonly="true"> + document.getElementById("tabContextMenu"); + </field> + + <field name="mTabstripWidth">0</field> + + <field name="mTabstrip"> + document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox"); + </field> + + <field name="_firstTab">null</field> + <field name="_lastTab">null</field> + <field name="_afterSelectedTab">null</field> + <field name="_beforeHoveredTab">null</field> + <field name="_afterHoveredTab">null</field> + <field name="_hoveredTab">null</field> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + switch (aTopic) { + case "nsPref:changed": + // This is the only pref observed. + let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled"); + + const newTab = document.getElementById("new-tab-button"); + const newTab2 = document.getAnonymousElementByAttribute(this, "anonid", "tabs-newtab-button") + + if (containersEnabled) { + for (let parent of [newTab, newTab2]) { + if (!parent) + continue; + let popup = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "menupopup"); + if (parent.id) { + popup.id = "newtab-popup"; + } else { + popup.setAttribute("anonid", "newtab-popup"); + } + popup.className = "new-tab-popup"; + popup.setAttribute("position", "after_end"); + parent.appendChild(popup); + + gClickAndHoldListenersOnElement.add(parent); + parent.setAttribute("type", "menu"); + } + } else { + for (let parent of [newTab, newTab2]) { + if (!parent) + continue; + gClickAndHoldListenersOnElement.remove(parent); + parent.removeAttribute("type"); + if (!parent.firstChild) + continue; + parent.firstChild.remove(); + } + } + + break; + } + ]]></body> + </method> + + <property name="_isCustomizing" readonly="true"> + <getter> + let root = document.documentElement; + return root.getAttribute("customizing") == "true" || + root.getAttribute("customize-exiting") == "true"; + </getter> + </property> + + <method name="_setPositionalAttributes"> + <body><![CDATA[ + let visibleTabs = this.tabbrowser.visibleTabs; + + if (!visibleTabs.length) + return; + + let selectedIndex = visibleTabs.indexOf(this.selectedItem); + + let lastVisible = visibleTabs.length - 1; + + if (this._afterSelectedTab) + this._afterSelectedTab.removeAttribute("afterselected-visible"); + if (this.selectedItem.closing || selectedIndex == lastVisible) { + this._afterSelectedTab = null; + } else { + this._afterSelectedTab = visibleTabs[selectedIndex + 1]; + this._afterSelectedTab.setAttribute("afterselected-visible", + "true"); + } + + if (this._firstTab) + this._firstTab.removeAttribute("first-visible-tab"); + this._firstTab = visibleTabs[0]; + this._firstTab.setAttribute("first-visible-tab", "true"); + if (this._lastTab) + this._lastTab.removeAttribute("last-visible-tab"); + this._lastTab = visibleTabs[lastVisible]; + this._lastTab.setAttribute("last-visible-tab", "true"); + + let hoveredTab = this._hoveredTab; + if (hoveredTab) { + hoveredTab._mouseleave(); + } + hoveredTab = this.querySelector("tab:hover"); + if (hoveredTab) { + hoveredTab._mouseenter(); + } + ]]></body> + </method> + + <field name="_blockDblClick">false</field> + + <field name="_tabDropIndicator"> + document.getAnonymousElementByAttribute(this, "anonid", "tab-drop-indicator"); + </field> + + <field name="_dragOverDelay">350</field> + <field name="_dragTime">0</field> + + <field name="_container" readonly="true"><![CDATA[ + this.parentNode && this.parentNode.localName == "toolbar" ? this.parentNode : this; + ]]></field> + + <field name="_propagatedVisibilityOnce">false</field> + + <property name="visible" + onget="return !this._container.collapsed;"> + <setter><![CDATA[ + if (val == this.visible && + this._propagatedVisibilityOnce) + return val; + + this._container.collapsed = !val; + + this._propagateVisibility(); + this._propagatedVisibilityOnce = true; + + return val; + ]]></setter> + </property> + + <method name="_enterNewTab"> + <body><![CDATA[ + let visibleTabs = this.tabbrowser.visibleTabs; + let candidate = visibleTabs[visibleTabs.length - 1]; + if (!candidate.selected) { + this._beforeHoveredTab = candidate; + candidate.setAttribute("beforehovered", "true"); + } + ]]></body> + </method> + + <method name="_leaveNewTab"> + <body><![CDATA[ + if (this._beforeHoveredTab) { + this._beforeHoveredTab.removeAttribute("beforehovered"); + this._beforeHoveredTab = null; + } + ]]></body> + </method> + + <method name="_propagateVisibility"> + <body><![CDATA[ + let visible = this.visible; + + document.getElementById("menu_closeWindow").hidden = !visible; + document.getElementById("menu_close").setAttribute("label", + this.tabbrowser.mStringBundle.getString(visible ? "tabs.closeTab" : "tabs.close")); + + TabsInTitlebar.allowedBy("tabs-visible", visible); + ]]></body> + </method> + + <method name="updateVisibility"> + <body><![CDATA[ + if (this.childNodes.length - this.tabbrowser._removingTabs.length == 1) + this.visible = window.toolbar.visible; + else + this.visible = true; + ]]></body> + </method> + + <method name="adjustTabstrip"> + <body><![CDATA[ + let numTabs = this.childNodes.length - + this.tabbrowser._removingTabs.length; + if (numTabs > 2) { + // This is an optimization to avoid layout flushes by calling + // getBoundingClientRect() when we just opened a second tab. In + // this case it's highly unlikely that the tab width is smaller + // than mTabClipWidth and the tab close button obscures too much + // of the tab's label. In the edge case of the window being too + // narrow (or if tabClipWidth has been set to a way higher value), + // we'll correct the 'closebuttons' attribute after the tabopen + // animation has finished. + + let tab = this.tabbrowser.visibleTabs[this.tabbrowser._numPinnedTabs]; + if (tab && tab.getBoundingClientRect().width <= this.mTabClipWidth) { + this.setAttribute("closebuttons", "activetab"); + return; + } + } + this.removeAttribute("closebuttons"); + ]]></body> + </method> + + <method name="_handleTabSelect"> + <parameter name="aSmoothScroll"/> + <body><![CDATA[ + if (this.getAttribute("overflow") == "true") + this.mTabstrip.ensureElementIsVisible(this.selectedItem, aSmoothScroll); + ]]></body> + </method> + + <method name="_fillTrailingGap"> + <body><![CDATA[ + try { + // if we're at the right side (and not the logical end, + // which is why this works for both LTR and RTL) + // of the tabstrip, we need to ensure that we stay + // completely scrolled to the right side + var tabStrip = this.mTabstrip; + if (tabStrip.scrollPosition + tabStrip.scrollClientSize > + tabStrip.scrollSize) + tabStrip.scrollByPixels(-1); + } catch (e) {} + ]]></body> + </method> + + <field name="_closingTabsSpacer"> + document.getAnonymousElementByAttribute(this, "anonid", "closing-tabs-spacer"); + </field> + + <field name="_tabDefaultMaxWidth">NaN</field> + <field name="_lastTabClosedByMouse">false</field> + <field name="_hasTabTempMaxWidth">false</field> + + <!-- Try to keep the active tab's close button under the mouse cursor --> + <method name="_lockTabSizing"> + <parameter name="aTab"/> + <body><![CDATA[ + var tabs = this.tabbrowser.visibleTabs; + if (!tabs.length) + return; + + var isEndTab = (aTab._tPos > tabs[tabs.length-1]._tPos); + var tabWidth = aTab.getBoundingClientRect().width; + + if (!this._tabDefaultMaxWidth) + this._tabDefaultMaxWidth = + parseFloat(window.getComputedStyle(aTab).maxWidth); + this._lastTabClosedByMouse = true; + + if (this.getAttribute("overflow") == "true") { + // Don't need to do anything if we're in overflow mode and aren't scrolled + // all the way to the right, or if we're closing the last tab. + if (isEndTab || !this.mTabstrip._scrollButtonDown.disabled) + return; + + // If the tab has an owner that will become the active tab, the owner will + // be to the left of it, so we actually want the left tab to slide over. + // This can't be done as easily in non-overflow mode, so we don't bother. + if (aTab.owner) + return; + + this._expandSpacerBy(tabWidth); + } else { // non-overflow mode + // Locking is neither in effect nor needed, so let tabs expand normally. + if (isEndTab && !this._hasTabTempMaxWidth) + return; + + let numPinned = this.tabbrowser._numPinnedTabs; + // Force tabs to stay the same width, unless we're closing the last tab, + // which case we need to let them expand just enough so that the overall + // tabbar width is the same. + if (isEndTab) { + let numNormalTabs = tabs.length - numPinned; + tabWidth = tabWidth * (numNormalTabs + 1) / numNormalTabs; + if (tabWidth > this._tabDefaultMaxWidth) + tabWidth = this._tabDefaultMaxWidth; + } + tabWidth += "px"; + for (let i = numPinned; i < tabs.length; i++) { + let tab = tabs[i]; + tab.style.setProperty("max-width", tabWidth, "important"); + if (!isEndTab) { // keep tabs the same width + tab.style.transition = "none"; + tab.clientTop; // flush styles to skip animation; see bug 649247 + tab.style.transition = ""; + } + } + this._hasTabTempMaxWidth = true; + this.tabbrowser.addEventListener("mousemove", this, false); + window.addEventListener("mouseout", this, false); + } + ]]></body> + </method> + + <method name="_expandSpacerBy"> + <parameter name="pixels"/> + <body><![CDATA[ + let spacer = this._closingTabsSpacer; + spacer.style.width = parseFloat(spacer.style.width) + pixels + "px"; + this.setAttribute("using-closing-tabs-spacer", "true"); + this.tabbrowser.addEventListener("mousemove", this, false); + window.addEventListener("mouseout", this, false); + ]]></body> + </method> + + <method name="_unlockTabSizing"> + <body><![CDATA[ + this.tabbrowser.removeEventListener("mousemove", this, false); + window.removeEventListener("mouseout", this, false); + + if (this._hasTabTempMaxWidth) { + this._hasTabTempMaxWidth = false; + let tabs = this.tabbrowser.visibleTabs; + for (let i = 0; i < tabs.length; i++) + tabs[i].style.maxWidth = ""; + } + + if (this.hasAttribute("using-closing-tabs-spacer")) { + this.removeAttribute("using-closing-tabs-spacer"); + this._closingTabsSpacer.style.width = 0; + } + ]]></body> + </method> + + <field name="_lastNumPinned">0</field> + <method name="_positionPinnedTabs"> + <body><![CDATA[ + var numPinned = this.tabbrowser._numPinnedTabs; + var doPosition = this.getAttribute("overflow") == "true" && + numPinned > 0; + + if (doPosition) { + this.setAttribute("positionpinnedtabs", "true"); + + let scrollButtonWidth = this.mTabstrip._scrollButtonDown.getBoundingClientRect().width; + let paddingStart = this.mTabstrip.scrollboxPaddingStart; + let width = 0; + + for (let i = numPinned - 1; i >= 0; i--) { + let tab = this.childNodes[i]; + width += tab.getBoundingClientRect().width; + tab.style.marginInlineStart = - (width + scrollButtonWidth + paddingStart) + "px"; + } + + this.style.paddingInlineStart = width + paddingStart + "px"; + + } else { + this.removeAttribute("positionpinnedtabs"); + + for (let i = 0; i < numPinned; i++) { + let tab = this.childNodes[i]; + tab.style.marginInlineStart = ""; + } + + this.style.paddingInlineStart = ""; + } + + if (this._lastNumPinned != numPinned) { + this._lastNumPinned = numPinned; + this._handleTabSelect(false); + } + ]]></body> + </method> + + <method name="_animateTabMove"> + <parameter name="event"/> + <body><![CDATA[ + let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0); + + if (this.getAttribute("movingtab") != "true") { + this.setAttribute("movingtab", "true"); + this.selectedItem = draggedTab; + } + + if (!("animLastScreenX" in draggedTab._dragData)) + draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX; + + let screenX = event.screenX; + if (screenX == draggedTab._dragData.animLastScreenX) + return; + + draggedTab._dragData.animLastScreenX = screenX; + + let rtl = (window.getComputedStyle(this).direction == "rtl"); + let pinned = draggedTab.pinned; + let numPinned = this.tabbrowser._numPinnedTabs; + let tabs = this.tabbrowser.visibleTabs + .slice(pinned ? 0 : numPinned, + pinned ? numPinned : undefined); + if (rtl) + tabs.reverse(); + let tabWidth = draggedTab.getBoundingClientRect().width; + + // Move the dragged tab based on the mouse position. + + let leftTab = tabs[0]; + let rightTab = tabs[tabs.length - 1]; + let tabScreenX = draggedTab.boxObject.screenX; + let translateX = screenX - draggedTab._dragData.screenX; + if (!pinned) + translateX += this.mTabstrip.scrollPosition - draggedTab._dragData.scrollX; + let leftBound = leftTab.boxObject.screenX - tabScreenX; + let rightBound = (rightTab.boxObject.screenX + rightTab.boxObject.width) - + (tabScreenX + tabWidth); + translateX = Math.max(translateX, leftBound); + translateX = Math.min(translateX, rightBound); + draggedTab.style.transform = "translateX(" + translateX + "px)"; + + // Determine what tab we're dragging over. + // * Point of reference is the center of the dragged tab. If that + // point touches a background tab, the dragged tab would take that + // tab's position when dropped. + // * We're doing a binary search in order to reduce the amount of + // tabs we need to check. + + let tabCenter = tabScreenX + translateX + tabWidth / 2; + let newIndex = -1; + let oldIndex = "animDropIndex" in draggedTab._dragData ? + draggedTab._dragData.animDropIndex : draggedTab._tPos; + let low = 0; + let high = tabs.length - 1; + while (low <= high) { + let mid = Math.floor((low + high) / 2); + if (tabs[mid] == draggedTab && + ++mid > high) + break; + let boxObject = tabs[mid].boxObject; + let screenX = boxObject.screenX + getTabShift(tabs[mid], oldIndex); + if (screenX > tabCenter) { + high = mid - 1; + } else if (screenX + boxObject.width < tabCenter) { + low = mid + 1; + } else { + newIndex = tabs[mid]._tPos; + break; + } + } + if (newIndex >= oldIndex) + newIndex++; + if (newIndex < 0 || newIndex == oldIndex) + return; + draggedTab._dragData.animDropIndex = newIndex; + + // Shift background tabs to leave a gap where the dragged tab + // would currently be dropped. + + for (let tab of tabs) { + if (tab != draggedTab) { + let shift = getTabShift(tab, newIndex); + tab.style.transform = shift ? "translateX(" + shift + "px)" : ""; + } + } + + function getTabShift(tab, dropIndex) { + if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex) + return rtl ? -tabWidth : tabWidth; + if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex) + return rtl ? tabWidth : -tabWidth; + return 0; + } + ]]></body> + </method> + + <method name="_finishAnimateTabMove"> + <body><![CDATA[ + if (this.getAttribute("movingtab") != "true") + return; + + for (let tab of this.tabbrowser.visibleTabs) + tab.style.transform = ""; + + this.removeAttribute("movingtab"); + + this._handleTabSelect(); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "load": + this.updateVisibility(); + TabsInTitlebar.init(); + break; + case "resize": + if (aEvent.target != window) + break; + + TabsInTitlebar.updateAppearance(); + + var width = this.mTabstrip.boxObject.width; + if (width != this.mTabstripWidth) { + this.adjustTabstrip(); + this._fillTrailingGap(); + this._handleTabSelect(); + this.mTabstripWidth = width; + } + break; + case "mouseout": + // If the "related target" (the node to which the pointer went) is not + // a child of the current document, the mouse just left the window. + let relatedTarget = aEvent.relatedTarget; + if (relatedTarget && relatedTarget.ownerDocument == document) + break; + case "mousemove": + if (document.getElementById("tabContextMenu").state != "open") + this._unlockTabSizing(); + break; + } + ]]></body> + </method> + + <field name="_animateElement"> + this.mTabstrip._scrollButtonDown; + </field> + + <method name="_notifyBackgroundTab"> + <parameter name="aTab"/> + <body><![CDATA[ + if (aTab.pinned || aTab.hidden) + return; + + var scrollRect = this.mTabstrip.scrollClientRect; + var tab = aTab.getBoundingClientRect(); + this.mTabstrip._calcTabMargins(aTab); + + // DOMRect left/right properties are immutable. + tab = {left: tab.left, right: tab.right}; + + // Is the new tab already completely visible? + if (scrollRect.left <= tab.left && tab.right <= scrollRect.right) + return; + + if (this.mTabstrip.smoothScroll) { + let selected = !this.selectedItem.pinned && + this.selectedItem.getBoundingClientRect(); + if (selected) { + selected = {left: selected.left, right: selected.right}; + // Need to take in to account the width of the left/right margins on tabs. + selected.left = selected.left + this.mTabstrip._tabMarginLeft; + selected.right = selected.right - this.mTabstrip._tabMarginRight; + } + + tab.left += this.mTabstrip._tabMarginLeft; + tab.right -= this.mTabstrip._tabMarginRight; + + // Can we make both the new tab and the selected tab completely visible? + if (!selected || + Math.max(tab.right - selected.left, selected.right - tab.left) <= + scrollRect.width) { + this.mTabstrip.ensureElementIsVisible(aTab); + return; + } + + this.mTabstrip._smoothScrollByPixels(this.mTabstrip._isRTLScrollbox ? + selected.right - scrollRect.right : + selected.left - scrollRect.left); + } + + if (!this._animateElement.hasAttribute("notifybgtab")) { + this._animateElement.setAttribute("notifybgtab", "true"); + setTimeout(function (ele) { + ele.removeAttribute("notifybgtab"); + }, 150, this._animateElement); + } + ]]></body> + </method> + + <method name="_getDragTargetTab"> + <parameter name="event"/> + <parameter name="isLink"/> + <body><![CDATA[ + let tab = event.target.localName == "tab" ? event.target : null; + if (tab && isLink) { + let boxObject = tab.boxObject; + if (event.screenX < boxObject.screenX + boxObject.width * .25 || + event.screenX > boxObject.screenX + boxObject.width * .75) + return null; + } + return tab; + ]]></body> + </method> + + <method name="_getDropIndex"> + <parameter name="event"/> + <parameter name="isLink"/> + <body><![CDATA[ + var tabs = this.childNodes; + var tab = this._getDragTargetTab(event, isLink); + if (window.getComputedStyle(this, null).direction == "ltr") { + for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) + if (event.screenX < tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2) + return i; + } else { + for (let i = tab ? tab._tPos : 0; i < tabs.length; i++) + if (event.screenX > tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2) + return i; + } + return tabs.length; + ]]></body> + </method> + + <method name="_getDropEffectForTabDrag"> + <parameter name="event"/> + <body><![CDATA[ + var dt = event.dataTransfer; + if (dt.mozItemCount == 1) { + var types = dt.mozTypesAt(0); + // tabs are always added as the first type + if (types[0] == TAB_DROP_TYPE) { + let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0); + if (sourceNode instanceof XULElement && + sourceNode.localName == "tab" && + sourceNode.ownerDocument.defaultView instanceof ChromeWindow && + sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser" && + sourceNode.ownerDocument.defaultView.gBrowser.tabContainer == sourceNode.parentNode) { + // Do not allow transfering a private tab to a non-private window + // and vice versa. + if (PrivateBrowsingUtils.isWindowPrivate(window) != + PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerDocument.defaultView)) + return "none"; + + if (window.gMultiProcessBrowser != + sourceNode.ownerDocument.defaultView.gMultiProcessBrowser) + return "none"; + + return dt.dropEffect == "copy" ? "copy" : "move"; + } + } + } + + if (browserDragAndDrop.canDropLink(event)) { + return "link"; + } + return "none"; + ]]></body> + </method> + + <method name="_handleNewTab"> + <parameter name="tab"/> + <body><![CDATA[ + if (tab.parentNode != this) + return; + tab._fullyOpen = true; + + this.adjustTabstrip(); + + if (tab.getAttribute("selected") == "true") { + this._fillTrailingGap(); + this._handleTabSelect(); + } else { + this._notifyBackgroundTab(tab); + } + + // XXXmano: this is a temporary workaround for bug 345399 + // We need to manually update the scroll buttons disabled state + // if a tab was inserted to the overflow area or removed from it + // without any scrolling and when the tabbar has already + // overflowed. + this.mTabstrip._updateScrollButtonsDisabledState(); + + // Preload the next about:newtab if there isn't one already. + this.tabbrowser._createPreloadBrowser(); + ]]></body> + </method> + + <method name="_canAdvanceToTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + return !aTab.closing; + ]]> + </body> + </method> + + <method name="_handleTabTelemetryStart"> + <parameter name="aTab"/> + <parameter name="aURI"/> + <body> + <![CDATA[ + // Animation-smoothness telemetry/logging + if (Services.telemetry.canRecordExtended || this._tabAnimationLoggingEnabled) { + if (aURI == "about:newtab" && (aTab._tPos == 1 || aTab._tPos == 2)) { + // Indicate newtab page animation where other tabs are unaffected + // (for which case, the 2nd or 3rd tabs are good representatives, even if not absolute) + aTab._recordingTabOpenPlain = true; + } + aTab._recordingHandle = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .startFrameTimeRecording(); + } + + // Overall animation duration + aTab._animStartTime = Date.now(); + ]]> + </body> + </method> + + <method name="_handleTabTelemetryEnd"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (!aTab._animStartTime) { + return; + } + + aTab._animStartTime = 0; + + // Handle tab animation smoothness telemetry/logging of frame intervals and paint times + if (!("_recordingHandle" in aTab)) { + return; + } + + let intervals = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .stopFrameTimeRecording(aTab._recordingHandle); + delete aTab._recordingHandle; + let frameCount = intervals.length; + + if (this._tabAnimationLoggingEnabled) { + let msg = "Tab " + (aTab.closing ? "close" : "open") + " (Frame-interval):\n"; + for (let i = 0; i < frameCount; i++) { + msg += Math.round(intervals[i]) + "\n"; + } + Services.console.logStringMessage(msg); + } + + // For telemetry, the first frame interval is not useful since it may represent an interval + // to a relatively old frame (prior to recording start). So we'll ignore it for the average. + if (frameCount > 1) { + let averageInterval = 0; + for (let i = 1; i < frameCount; i++) { + averageInterval += intervals[i]; + } + averageInterval = averageInterval / (frameCount - 1); + + Services.telemetry.getHistogramById("FX_TAB_ANIM_ANY_FRAME_INTERVAL_MS").add(averageInterval); + + if (aTab._recordingTabOpenPlain) { + delete aTab._recordingTabOpenPlain; + // While we do have a telemetry probe NEWTAB_PAGE_ENABLED to monitor newtab preview, it'll be + // easier to overview the data without slicing by it. Hence the additional histograms with _PREVIEW. + let preview = this._browserNewtabpageEnabled ? "_PREVIEW" : ""; + Services.telemetry.getHistogramById("FX_TAB_ANIM_OPEN" + preview + "_FRAME_INTERVAL_MS").add(averageInterval); + } + } + ]]> + </body> + </method> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <property name="mAllTabsPopup" readonly="true" + onget="return document.getElementById('alltabs-popup');"/> + </implementation> + + <handlers> + <handler event="TabSelect" action="this._handleTabSelect();"/> + + <handler event="transitionend"><![CDATA[ + if (event.propertyName != "max-width") + return; + + var tab = event.target; + + this._handleTabTelemetryEnd(tab); + + if (tab.getAttribute("fadein") == "true") { + if (tab._fullyOpen) + this.adjustTabstrip(); + else + this._handleNewTab(tab); + } else if (tab.closing) { + this.tabbrowser._endRemoveTab(tab); + } + ]]></handler> + + <handler event="dblclick"><![CDATA[ + // When the tabbar has an unified appearance with the titlebar + // and menubar, a double-click in it should have the same behavior + // as double-clicking the titlebar + if (TabsInTitlebar.enabled || this.parentNode._dragBindingAlive) + return; + + if (event.button != 0 || + event.originalTarget.localName != "box") + return; + + // See hack note in the tabbrowser-close-tab-button binding + if (!this._blockDblClick) + BrowserOpenTab(); + + event.preventDefault(); + ]]></handler> + + <handler event="click" button="0" phase="capturing"><![CDATA[ + /* Catches extra clicks meant for the in-tab close button. + * Placed here to avoid leaking (a temporary handler added from the + * in-tab close button binding would close over the tab and leak it + * until the handler itself was removed). (bug 897751) + * + * The only sequence in which a second click event (i.e. dblclik) + * can be dispatched on an in-tab close button is when it is shown + * after the first click (i.e. the first click event was dispatched + * on the tab). This happens when we show the close button only on + * the active tab. (bug 352021) + * The only sequence in which a third click event can be dispatched + * on an in-tab close button is when the tab was opened with a + * double click on the tabbar. (bug 378344) + * In both cases, it is most likely that the close button area has + * been accidentally clicked, therefore we do not close the tab. + * + * We don't want to ignore processing of more than one click event, + * though, since the user might actually be repeatedly clicking to + * close many tabs at once. + */ + let target = event.originalTarget; + if (target.classList.contains('tab-close-button')) { + // We preemptively set this to allow the closing-multiple-tabs- + // in-a-row case. + if (this._blockDblClick) { + target._ignoredCloseButtonClicks = true; + } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) { + target._ignoredCloseButtonClicks = true; + event.stopPropagation(); + return; + } else { + // Reset the "ignored click" flag + target._ignoredCloseButtonClicks = false; + } + } + + /* Protects from close-tab-button errant doubleclick: + * Since we're removing the event target, if the user + * double-clicks the button, the dblclick event will be dispatched + * with the tabbar as its event target (and explicit/originalTarget), + * which treats that as a mouse gesture for opening a new tab. + * In this context, we're manually blocking the dblclick event + * (see tabbrowser-close-tab-button dblclick handler). + */ + if (this._blockDblClick) { + if (!("_clickedTabBarOnce" in this)) { + this._clickedTabBarOnce = true; + return; + } + delete this._clickedTabBarOnce; + this._blockDblClick = false; + } + ]]></handler> + + <handler event="click"><![CDATA[ + if (event.button != 1) + return; + + if (event.target.localName == "tab") { + this.tabbrowser.removeTab(event.target, {animate: true, + byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE}); + } else if (event.originalTarget.localName == "box") { + // The user middleclicked an open space on the tabstrip. This could + // be because they intend to open a new tab, but it could also be + // because they just removed a tab and they now middleclicked on the + // resulting space while that tab is closing. In that case, we don't + // want to open a tab. So if we're removing one or more tabs, and + // the tab click is before the end of the last visible tab, we do + // nothing. + if (this.tabbrowser._removingTabs.length) { + let visibleTabs = this.tabbrowser.visibleTabs; + let ltr = (window.getComputedStyle(this, null).direction == "ltr"); + let lastTab = visibleTabs[visibleTabs.length - 1]; + let endOfTab = lastTab.getBoundingClientRect()[ltr ? "right" : "left"]; + if ((ltr && event.clientX > endOfTab) || + (!ltr && event.clientX < endOfTab)) { + BrowserOpenTab(); + } + } else { + BrowserOpenTab(); + } + } else { + return; + } + + event.stopPropagation(); + ]]></handler> + + <handler event="keydown" group="system"><![CDATA[ + if (event.altKey || event.shiftKey) + return; + + let wrongModifiers; + if (this.tabbrowser.AppConstants.platform == "macosx") { + wrongModifiers = !event.metaKey; + } else { + wrongModifiers = !event.ctrlKey || event.metaKey; + } + + if (wrongModifiers) + return; + + // Don't check if the event was already consumed because tab navigation + // should work always for better user experience. + + switch (event.keyCode) { + case KeyEvent.DOM_VK_UP: + this.tabbrowser.moveTabBackward(); + break; + case KeyEvent.DOM_VK_DOWN: + this.tabbrowser.moveTabForward(); + break; + case KeyEvent.DOM_VK_RIGHT: + case KeyEvent.DOM_VK_LEFT: + this.tabbrowser.moveTabOver(event); + break; + case KeyEvent.DOM_VK_HOME: + this.tabbrowser.moveTabToStart(); + break; + case KeyEvent.DOM_VK_END: + this.tabbrowser.moveTabToEnd(); + break; + default: + // Consume the keydown event for the above keyboard + // shortcuts only. + return; + } + event.preventDefault(); + ]]></handler> + + <handler event="dragstart"><![CDATA[ + var tab = this._getDragTargetTab(event, false); + if (!tab || this._isCustomizing) + return; + + let dt = event.dataTransfer; + dt.mozSetDataAt(TAB_DROP_TYPE, tab, 0); + let browser = tab.linkedBrowser; + + // We must not set text/x-moz-url or text/plain data here, + // otherwise trying to deatch the tab by dropping it on the desktop + // may result in an "internet shortcut" + dt.mozSetDataAt("text/x-moz-text-internal", browser.currentURI.spec, 0); + + // Set the cursor to an arrow during tab drags. + dt.mozCursor = "default"; + + // Create a canvas to which we capture the current tab. + // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired + // canvas size (in CSS pixels) to the window's backing resolution in order + // to get a full-resolution drag image for use on HiDPI displays. + let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils); + let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom; + let canvas = this._dndCanvas ? this._dndCanvas + : document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.mozOpaque = true; + canvas.width = 160 * scale; + canvas.height = 90 * scale; + let toDrag; + let dragImageOffset = -16; + if (gMultiProcessBrowser) { + var context = canvas.getContext('2d'); + context.fillStyle = "white"; + context.fillRect(0, 0, canvas.width, canvas.height); + // Create a panel to use it in setDragImage + // which will tell xul to render a panel that follows + // the pointer while a dnd session is on. + if (!this._dndPanel) { + this._dndCanvas = canvas; + this._dndPanel = document.createElement("panel"); + this._dndPanel.className = "dragfeedback-tab"; + this._dndPanel.setAttribute("type", "drag"); + let wrapper = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + wrapper.style.width = "160px"; + wrapper.style.height = "90px"; + wrapper.appendChild(canvas); + canvas.style.width = "100%"; + canvas.style.height = "100%"; + this._dndPanel.appendChild(wrapper); + document.documentElement.appendChild(this._dndPanel); + } + // PageThumb is async with e10s but that's fine + // since we can update the panel during the dnd. + PageThumbs.captureToCanvas(browser, canvas); + toDrag = this._dndPanel; + } else { + // For the non e10s case we can just use PageThumbs + // sync. No need for xul magic, the native dnd will + // be fine, so let's use the canvas for setDragImage. + PageThumbs.captureToCanvas(browser, canvas); + toDrag = canvas; + dragImageOffset = dragImageOffset * scale; + } + dt.setDragImage(toDrag, dragImageOffset, dragImageOffset); + + // _dragData.offsetX/Y give the coordinates that the mouse should be + // positioned relative to the corner of the new window created upon + // dragend such that the mouse appears to have the same position + // relative to the corner of the dragged tab. + function clientX(ele) { + return ele.getBoundingClientRect().left; + } + let tabOffsetX = clientX(tab) - clientX(this); + tab._dragData = { + offsetX: event.screenX - window.screenX - tabOffsetX, + offsetY: event.screenY - window.screenY, + scrollX: this.mTabstrip.scrollPosition, + screenX: event.screenX + }; + + event.stopPropagation(); + ]]></handler> + + <handler event="dragover"><![CDATA[ + var effects = this._getDropEffectForTabDrag(event); + + var ind = this._tabDropIndicator; + if (effects == "" || effects == "none") { + ind.collapsed = true; + return; + } + event.preventDefault(); + event.stopPropagation(); + + var tabStrip = this.mTabstrip; + var ltr = (window.getComputedStyle(this, null).direction == "ltr"); + + // autoscroll the tab strip if we drag over the scroll + // buttons, even if we aren't dragging a tab, but then + // return to avoid drawing the drop indicator + var pixelsToScroll = 0; + if (this.getAttribute("overflow") == "true") { + var targetAnonid = event.originalTarget.getAttribute("anonid"); + switch (targetAnonid) { + case "scrollbutton-up": + pixelsToScroll = tabStrip.scrollIncrement * -1; + break; + case "scrollbutton-down": + pixelsToScroll = tabStrip.scrollIncrement; + break; + } + if (pixelsToScroll) + tabStrip.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll); + } + + if (effects == "move" && + this == event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0).parentNode) { + ind.collapsed = true; + this._animateTabMove(event); + return; + } + + this._finishAnimateTabMove(); + + if (effects == "link") { + let tab = this._getDragTargetTab(event, true); + if (tab) { + if (!this._dragTime) + this._dragTime = Date.now(); + if (Date.now() >= this._dragTime + this._dragOverDelay) + this.selectedItem = tab; + ind.collapsed = true; + return; + } + } + + var rect = tabStrip.getBoundingClientRect(); + var newMargin; + if (pixelsToScroll) { + // if we are scrolling, put the drop indicator at the edge + // so that it doesn't jump while scrolling + let scrollRect = tabStrip.scrollClientRect; + let minMargin = scrollRect.left - rect.left; + let maxMargin = Math.min(minMargin + scrollRect.width, + scrollRect.right); + if (!ltr) + [minMargin, maxMargin] = [this.clientWidth - maxMargin, + this.clientWidth - minMargin]; + newMargin = (pixelsToScroll > 0) ? maxMargin : minMargin; + } + else { + let newIndex = this._getDropIndex(event, effects == "link"); + if (newIndex == this.childNodes.length) { + let tabRect = this.childNodes[newIndex-1].getBoundingClientRect(); + if (ltr) + newMargin = tabRect.right - rect.left; + else + newMargin = rect.right - tabRect.left; + } + else { + let tabRect = this.childNodes[newIndex].getBoundingClientRect(); + if (ltr) + newMargin = tabRect.left - rect.left; + else + newMargin = rect.right - tabRect.right; + } + } + + ind.collapsed = false; + + newMargin += ind.clientWidth / 2; + if (!ltr) + newMargin *= -1; + + ind.style.transform = "translate(" + Math.round(newMargin) + "px)"; + ind.style.marginInlineStart = (-ind.clientWidth) + "px"; + ]]></handler> + + <handler event="drop"><![CDATA[ + var dt = event.dataTransfer; + var dropEffect = dt.dropEffect; + var draggedTab; + if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { // tab copy or move + draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0); + // not our drop then + if (!draggedTab) + return; + } + + this._tabDropIndicator.collapsed = true; + event.stopPropagation(); + if (draggedTab && dropEffect == "copy") { + // copy the dropped tab (wherever it's from) + let newIndex = this._getDropIndex(event, false); + let newTab = this.tabbrowser.duplicateTab(draggedTab); + this.tabbrowser.moveTabTo(newTab, newIndex); + if (draggedTab.parentNode != this || event.shiftKey) + this.selectedItem = newTab; + } else if (draggedTab && draggedTab.parentNode == this) { + this._finishAnimateTabMove(); + + // actually move the dragged tab + if ("animDropIndex" in draggedTab._dragData) { + let newIndex = draggedTab._dragData.animDropIndex; + if (newIndex > draggedTab._tPos) + newIndex--; + this.tabbrowser.moveTabTo(draggedTab, newIndex); + } + } else if (draggedTab) { + let newIndex = this._getDropIndex(event, false); + this.tabbrowser.adoptTab(draggedTab, newIndex, true); + } else { + // Pass true to disallow dropping javascript: or data: urls + let links; + try { + links = browserDragAndDrop.dropLinks(event, true); + } catch (ex) {} + + if (!links || links.length === 0) + return; + + let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + + if (event.shiftKey) + inBackground = !inBackground; + + let targetTab = this._getDragTargetTab(event, true); + let userContextId = this.selectedItem.getAttribute("usercontextid"); + let replace = !!targetTab; + let newIndex = this._getDropIndex(event, true); + let urls = links.map(link => link.url); + this.tabbrowser.loadTabs(urls, { + inBackground, + replace, + allowThirdPartyFixup: true, + targetTab, + newIndex, + userContextId, + }); + } + + if (draggedTab) { + delete draggedTab._dragData; + } + ]]></handler> + + <handler event="dragend"><![CDATA[ + // Note: while this case is correctly handled here, this event + // isn't dispatched when the tab is moved within the tabstrip, + // see bug 460801. + + this._finishAnimateTabMove(); + + var dt = event.dataTransfer; + var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0); + if (dt.mozUserCancelled || dt.dropEffect != "none" || this._isCustomizing) { + delete draggedTab._dragData; + return; + } + + // Disable detach within the browser toolbox + var eX = event.screenX; + var eY = event.screenY; + var wX = window.screenX; + // check if the drop point is horizontally within the window + if (eX > wX && eX < (wX + window.outerWidth)) { + let bo = this.mTabstrip.boxObject; + // also avoid detaching if the the tab was dropped too close to + // the tabbar (half a tab) + let endScreenY = bo.screenY + 1.5 * bo.height; + if (eY < endScreenY && eY > window.screenY) + return; + } + + // screen.availLeft et. al. only check the screen that this window is on, + // but we want to look at the screen the tab is being dropped onto. + var screen = Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager) + .screenForRect(eX, eY, 1, 1); + var fullX = {}, fullY = {}, fullWidth = {}, fullHeight = {}; + var availX = {}, availY = {}, availWidth = {}, availHeight = {}; + // get full screen rect and available rect, both in desktop pix + screen.GetRectDisplayPix(fullX, fullY, fullWidth, fullHeight); + screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight); + + // scale factor to convert desktop pixels to CSS px + var scaleFactor = + screen.contentsScaleFactor / screen.defaultCSSScaleFactor; + // synchronize CSS-px top-left coordinates with the screen's desktop-px + // coordinates, to ensure uniqueness across multiple screens + // (compare the equivalent adjustments in nsGlobalWindow::GetScreenXY() + // and related methods) + availX.value = (availX.value - fullX.value) * scaleFactor + fullX.value; + availY.value = (availY.value - fullY.value) * scaleFactor + fullY.value; + availWidth.value *= scaleFactor; + availHeight.value *= scaleFactor; + + // ensure new window entirely within screen + var winWidth = Math.min(window.outerWidth, availWidth.value); + var winHeight = Math.min(window.outerHeight, availHeight.value); + var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, availX.value), + availX.value + availWidth.value - winWidth); + var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, availY.value), + availY.value + availHeight.value - winHeight); + + delete draggedTab._dragData; + + if (this.tabbrowser.tabs.length == 1) { + // resize _before_ move to ensure the window fits the new screen. if + // the window is too large for its screen, the window manager may do + // automatic repositioning. + window.resizeTo(winWidth, winHeight); + window.moveTo(left, top); + window.focus(); + } else { + let props = { screenX: left, screenY: top }; + if (this.tabbrowser.AppConstants.platform != "win") { + props.outerWidth = winWidth; + props.outerHeight = winHeight; + } + this.tabbrowser.replaceTabWithWindow(draggedTab, props); + } + event.stopPropagation(); + ]]></handler> + + <handler event="dragexit"><![CDATA[ + this._dragTime = 0; + + // This does not work at all (see bug 458613) + var target = event.relatedTarget; + while (target && target != this) + target = target.parentNode; + if (target) + return; + + this._tabDropIndicator.collapsed = true; + event.stopPropagation(); + ]]></handler> + </handlers> + </binding> + + <!-- close-tab-button binding + This binding relies on the structure of the tabbrowser binding. + Therefore it should only be used as a child of the tab or the tabs + element (in both cases, when they are anonymous nodes of <tabbrowser>). + --> + <binding id="tabbrowser-close-tab-button" + extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-image"> + <handlers> + <handler event="click" button="0"><![CDATA[ + var bindingParent = document.getBindingParent(this); + var tabContainer = bindingParent.parentNode; + tabContainer.tabbrowser.removeTab(bindingParent, {animate: true, + byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE}); + // This enables double-click protection for the tab container + // (see tabbrowser-tabs 'click' handler). + tabContainer._blockDblClick = true; + ]]></handler> + + <handler event="dblclick" button="0" phase="capturing"> + // for the one-close-button case + event.stopPropagation(); + </handler> + + <handler event="dragstart"> + event.stopPropagation(); + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-tab" display="xul:hbox" + extends="chrome://global/content/bindings/tabbox.xml#tab"> + <resources> + <stylesheet src="chrome://browser/content/tabbrowser.css"/> + </resources> + + <content context="tabContextMenu"> + <xul:stack class="tab-stack" flex="1"> + <xul:hbox xbl:inherits="pinned,selected=visuallyselected,fadein" + class="tab-background"> + <xul:hbox xbl:inherits="pinned,selected=visuallyselected" + class="tab-background-start"/> + <xul:hbox xbl:inherits="pinned,selected=visuallyselected" + class="tab-background-middle"/> + <xul:hbox xbl:inherits="pinned,selected=visuallyselected" + class="tab-background-end"/> + </xul:hbox> + <xul:hbox xbl:inherits="pinned,selected=visuallyselected,titlechanged,attention" + class="tab-content" align="center"> + <xul:image xbl:inherits="fadein,pinned,busy,progress,selected=visuallyselected" + class="tab-throbber" + role="presentation" + layer="true" /> + <xul:image xbl:inherits="src=image,loadingprincipal=iconLoadingPrincipal,fadein,pinned,selected=visuallyselected,busy,crashed,sharing" + anonid="tab-icon-image" + class="tab-icon-image" + validate="never" + role="presentation"/> + <xul:image xbl:inherits="sharing,selected=visuallyselected" + anonid="sharing-icon" + class="tab-sharing-icon-overlay" + role="presentation"/> + <xul:image xbl:inherits="crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected" + anonid="overlay-icon" + class="tab-icon-overlay" + role="presentation"/> + <xul:label flex="1" + xbl:inherits="value=label,crop,accesskey,fadein,pinned,selected=visuallyselected,attention" + class="tab-text tab-label" + role="presentation"/> + <xul:image xbl:inherits="soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected" + anonid="soundplaying-icon" + class="tab-icon-sound" + role="presentation"/> + <xul:toolbarbutton anonid="close-button" + xbl:inherits="fadein,pinned,selected=visuallyselected" + class="tab-close-button close-icon"/> + </xul:hbox> + </xul:stack> + </content> + + <implementation> + <constructor><![CDATA[ + if (!("_lastAccessed" in this)) { + this.updateLastAccessed(); + } + ]]></constructor> + + <property name="_visuallySelected"> + <setter> + <![CDATA[ + if (val) + this.setAttribute("visuallyselected", "true"); + else + this.removeAttribute("visuallyselected"); + this.parentNode.tabbrowser._tabAttrModified(this, ["visuallyselected"]); + + this._setPositionAttributes(val); + + return val; + ]]> + </setter> + </property> + + <property name="_selected"> + <setter> + <![CDATA[ + // in e10s we want to only pseudo-select a tab before its rendering is done, so that + // the rest of the system knows that the tab is selected, but we don't want to update its + // visual status to selected until after we receive confirmation that its content has painted. + if (val) + this.setAttribute("selected", "true"); + else + this.removeAttribute("selected"); + + // If we're non-e10s we should update the visual selection as well at the same time, + // *or* if we're e10s and the visually selected tab isn't changing, in which case the + // tab switcher code won't run and update anything else (like the before- and after- + // selected attributes). + if (!gMultiProcessBrowser || (val && this.hasAttribute("visuallyselected"))) { + this._visuallySelected = val; + } + + return val; + ]]> + </setter> + </property> + + <property name="pinned" readonly="true"> + <getter> + return this.getAttribute("pinned") == "true"; + </getter> + </property> + <property name="hidden" readonly="true"> + <getter> + return this.getAttribute("hidden") == "true"; + </getter> + </property> + <property name="muted" readonly="true"> + <getter> + return this.getAttribute("muted") == "true"; + </getter> + </property> + <property name="blocked" readonly="true"> + <getter> + return this.getAttribute("blocked") == "true"; + </getter> + </property> + <!-- + Describes how the tab ended up in this mute state. May be any of: + + - undefined: The tabs mute state has never changed. + - null: The mute state was last changed through the UI. + - Any string: The ID was changed through an extension API. The string + must be the ID of the extension which changed it. + --> + <field name="muteReason">undefined</field> + + <property name="userContextId" readonly="true"> + <getter> + return this.hasAttribute("usercontextid") + ? parseInt(this.getAttribute("usercontextid")) + : 0; + </getter> + </property> + + <property name="soundPlaying" readonly="true"> + <getter> + return this.getAttribute("soundplaying") == "true"; + </getter> + </property> + + <property name="lastAccessed"> + <getter> + return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed; + </getter> + </property> + <method name="updateLastAccessed"> + <parameter name="aDate"/> + <body><![CDATA[ + this._lastAccessed = this.selected ? Infinity : (aDate || Date.now()); + ]]></body> + </method> + + <field name="cachePosition">Infinity</field> + + <field name="mOverCloseButton">false</field> + <property name="_overPlayingIcon" readonly="true"> + <getter><![CDATA[ + let iconVisible = this.hasAttribute("soundplaying") || + this.hasAttribute("muted") || + this.hasAttribute("blocked"); + let soundPlayingIcon = + document.getAnonymousElementByAttribute(this, "anonid", "soundplaying-icon"); + let overlayIcon = + document.getAnonymousElementByAttribute(this, "anonid", "overlay-icon"); + + return soundPlayingIcon && soundPlayingIcon.matches(":hover") || + (overlayIcon && overlayIcon.matches(":hover") && iconVisible); + ]]></getter> + </property> + <field name="mCorrespondingMenuitem">null</field> + + <!-- + While it would make sense to track this in a field, the field will get nuked + once the node is gone from the DOM, which causes us to think the tab is not + closed, which causes us to make wrong decisions. So we use an expando instead. + <field name="closing">false</field> + --> + + <method name="_mouseenter"> + <body><![CDATA[ + if (this.hidden || this.closing) + return; + + let tabContainer = this.parentNode; + let visibleTabs = tabContainer.tabbrowser.visibleTabs; + let tabIndex = visibleTabs.indexOf(this); + if (tabIndex == 0) { + tabContainer._beforeHoveredTab = null; + } else { + let candidate = visibleTabs[tabIndex - 1]; + if (!candidate.selected) { + tabContainer._beforeHoveredTab = candidate; + candidate.setAttribute("beforehovered", "true"); + } + } + + if (tabIndex == visibleTabs.length - 1) { + tabContainer._afterHoveredTab = null; + } else { + let candidate = visibleTabs[tabIndex + 1]; + if (!candidate.selected) { + tabContainer._afterHoveredTab = candidate; + candidate.setAttribute("afterhovered", "true"); + } + } + + tabContainer._hoveredTab = this; + ]]></body> + </method> + + <method name="_mouseleave"> + <body><![CDATA[ + let tabContainer = this.parentNode; + if (tabContainer._beforeHoveredTab) { + tabContainer._beforeHoveredTab.removeAttribute("beforehovered"); + tabContainer._beforeHoveredTab = null; + } + if (tabContainer._afterHoveredTab) { + tabContainer._afterHoveredTab.removeAttribute("afterhovered"); + tabContainer._afterHoveredTab = null; + } + + tabContainer._hoveredTab = null; + ]]></body> + </method> + + <method name="toggleMuteAudio"> + <parameter name="aMuteReason"/> + <body> + <![CDATA[ + let tabContainer = this.parentNode; + let browser = this.linkedBrowser; + let modifiedAttrs = []; + if (browser.audioBlocked) { + this.removeAttribute("blocked"); + modifiedAttrs.push("blocked"); + + // We don't want sound icon flickering between "blocked", "none" and + // "sound-playing", here adding the "soundplaying" is to keep the + // transition smoothly. + if (!this.hasAttribute("soundplaying")) { + this.setAttribute("soundplaying", true); + modifiedAttrs.push("soundplaying"); + } + + browser.resumeMedia(); + } else { + if (browser.audioMuted) { + browser.unmute(); + this.removeAttribute("muted"); + BrowserUITelemetry.countTabMutingEvent("unmute", aMuteReason); + } else { + browser.mute(); + this.setAttribute("muted", "true"); + BrowserUITelemetry.countTabMutingEvent("mute", aMuteReason); + } + this.muteReason = aMuteReason || null; + modifiedAttrs.push("muted"); + } + tabContainer.tabbrowser._tabAttrModified(this, modifiedAttrs); + ]]> + </body> + </method> + + <method name="setUserContextId"> + <parameter name="aUserContextId"/> + <body> + <![CDATA[ + if (aUserContextId) { + if (this.linkedBrowser) { + this.linkedBrowser.setAttribute("usercontextid", aUserContextId); + } + this.setAttribute("usercontextid", aUserContextId); + } else { + if (this.linkedBrowser) { + this.linkedBrowser.removeAttribute("usercontextid"); + } + this.removeAttribute("usercontextid"); + } + + ContextualIdentityService.setTabStyle(this); + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="mouseover"><![CDATA[ + let anonid = event.originalTarget.getAttribute("anonid"); + if (anonid == "close-button") + this.mOverCloseButton = true; + + this._mouseenter(); + ]]></handler> + <handler event="mouseout"><![CDATA[ + let anonid = event.originalTarget.getAttribute("anonid"); + if (anonid == "close-button") + this.mOverCloseButton = false; + + this._mouseleave(); + ]]></handler> + <handler event="dragstart" phase="capturing"> + this.style.MozUserFocus = ''; + </handler> + <handler event="mousedown" phase="capturing"> + <![CDATA[ + if (this.selected) { + this.style.MozUserFocus = 'ignore'; + this.clientTop; // just using this to flush style updates + } else if (this.mOverCloseButton || + this._overPlayingIcon) { + // Prevent tabbox.xml from selecting the tab. + event.stopPropagation(); + } + ]]> + </handler> + <handler event="mouseup"> + this.style.MozUserFocus = ''; + </handler> + <handler event="click"> + <![CDATA[ + if (event.button != 0) { + return; + } + + if (this._overPlayingIcon) { + this.toggleMuteAudio(); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-alltabs-popup" + extends="chrome://global/content/bindings/popup.xml#popup"> + <implementation implements="nsIDOMEventListener"> + <method name="_tabOnAttrModified"> + <parameter name="aEvent"/> + <body><![CDATA[ + var tab = aEvent.target; + if (tab.mCorrespondingMenuitem) + this._setMenuitemAttributes(tab.mCorrespondingMenuitem, tab); + ]]></body> + </method> + + <method name="_tabOnTabClose"> + <parameter name="aEvent"/> + <body><![CDATA[ + var tab = aEvent.target; + if (tab.mCorrespondingMenuitem) + this.removeChild(tab.mCorrespondingMenuitem); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "TabAttrModified": + this._tabOnAttrModified(aEvent); + break; + case "TabClose": + this._tabOnTabClose(aEvent); + break; + case "scroll": + this._updateTabsVisibilityStatus(); + break; + } + ]]></body> + </method> + + <method name="_updateTabsVisibilityStatus"> + <body><![CDATA[ + var tabContainer = gBrowser.tabContainer; + // We don't want menu item decoration unless there is overflow. + if (tabContainer.getAttribute("overflow") != "true") + return; + + var tabstripBO = tabContainer.mTabstrip.scrollBoxObject; + for (var i = 0; i < this.childNodes.length; i++) { + let curTab = this.childNodes[i].tab; + if (!curTab) // "Undo close tab", menuseparator, or entries put here by addons. + continue; + let curTabBO = curTab.boxObject; + if (curTabBO.screenX >= tabstripBO.screenX && + curTabBO.screenX + curTabBO.width <= tabstripBO.screenX + tabstripBO.width) + this.childNodes[i].setAttribute("tabIsVisible", "true"); + else + this.childNodes[i].removeAttribute("tabIsVisible"); + } + ]]></body> + </method> + + <method name="_createTabMenuItem"> + <parameter name="aTab"/> + <body><![CDATA[ + var menuItem = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "menuitem"); + + menuItem.setAttribute("class", "menuitem-iconic alltabs-item menuitem-with-favicon"); + + this._setMenuitemAttributes(menuItem, aTab); + + aTab.mCorrespondingMenuitem = menuItem; + menuItem.tab = aTab; + + this.appendChild(menuItem); + ]]></body> + </method> + + <method name="_setMenuitemAttributes"> + <parameter name="aMenuitem"/> + <parameter name="aTab"/> + <body><![CDATA[ + aMenuitem.setAttribute("label", aTab.label); + aMenuitem.setAttribute("crop", aTab.getAttribute("crop")); + + if (aTab.hasAttribute("busy")) { + aMenuitem.setAttribute("busy", aTab.getAttribute("busy")); + aMenuitem.removeAttribute("image"); + } else { + aMenuitem.setAttribute("image", aTab.getAttribute("image")); + aMenuitem.removeAttribute("busy"); + } + + if (aTab.hasAttribute("pending")) + aMenuitem.setAttribute("pending", aTab.getAttribute("pending")); + else + aMenuitem.removeAttribute("pending"); + + if (aTab.selected) + aMenuitem.setAttribute("selected", "true"); + else + aMenuitem.removeAttribute("selected"); + + function addEndImage() { + let endImage = document.createElement("image"); + endImage.setAttribute("class", "alltabs-endimage"); + let endImageContainer = document.createElement("hbox"); + endImageContainer.setAttribute("align", "center"); + endImageContainer.setAttribute("pack", "center"); + endImageContainer.appendChild(endImage); + aMenuitem.appendChild(endImageContainer); + return endImage; + } + + if (aMenuitem.firstChild) + aMenuitem.firstChild.remove(); + if (aTab.hasAttribute("muted")) + addEndImage().setAttribute("muted", "true"); + else if (aTab.hasAttribute("soundplaying")) + addEndImage().setAttribute("soundplaying", "true"); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="popupshowing"> + <![CDATA[ + if (event.target.getAttribute("id") == "alltabs_containersMenuTab") { + createUserContextMenu(event); + return; + } + + let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled"); + + if (event.target.getAttribute("anonid") == "newtab-popup" || + event.target.id == "newtab-popup") { + createUserContextMenu(event); + } else { + document.getElementById("alltabs-popup-separator-1").hidden = !containersEnabled; + let containersTab = document.getElementById("alltabs_containersTab"); + + containersTab.hidden = !containersEnabled; + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + containersTab.setAttribute("disabled", "true"); + } + + document.getElementById("alltabs_undoCloseTab").disabled = + SessionStore.getClosedTabCount(window) == 0; + + var tabcontainer = gBrowser.tabContainer; + + // Listen for changes in the tab bar. + tabcontainer.addEventListener("TabAttrModified", this, false); + tabcontainer.addEventListener("TabClose", this, false); + tabcontainer.mTabstrip.addEventListener("scroll", this, false); + + let tabs = gBrowser.visibleTabs; + for (var i = 0; i < tabs.length; i++) { + if (!tabs[i].pinned) + this._createTabMenuItem(tabs[i]); + } + this._updateTabsVisibilityStatus(); + } + ]]></handler> + + <handler event="popuphidden"> + <![CDATA[ + if (event.target.getAttribute("id") == "alltabs_containersMenuTab") { + return; + } + + // clear out the menu popup and remove the listeners + for (let i = this.childNodes.length - 1; i > 0; i--) { + let menuItem = this.childNodes[i]; + if (menuItem.tab) { + menuItem.tab.mCorrespondingMenuitem = null; + this.removeChild(menuItem); + } + if (menuItem.hasAttribute("usercontextid")) { + this.removeChild(menuItem); + } + } + var tabcontainer = gBrowser.tabContainer; + tabcontainer.mTabstrip.removeEventListener("scroll", this, false); + tabcontainer.removeEventListener("TabAttrModified", this, false); + tabcontainer.removeEventListener("TabClose", this, false); + ]]></handler> + + <handler event="DOMMenuItemActive"> + <![CDATA[ + var tab = event.target.tab; + if (tab) { + let overLink = tab.linkedBrowser.currentURI.spec; + if (overLink == "about:blank") + overLink = ""; + XULBrowserWindow.setOverLink(overLink, null); + } + ]]></handler> + + <handler event="DOMMenuItemInactive"> + <![CDATA[ + XULBrowserWindow.setOverLink("", null); + ]]></handler> + + <handler event="command"><![CDATA[ + if (event.target.tab) + gBrowser.selectedTab = event.target.tab; + ]]></handler> + + </handlers> + </binding> + + <binding id="statuspanel" display="xul:hbox"> + <content> + <xul:hbox class="statuspanel-inner"> + <xul:label class="statuspanel-label" + role="status" + aria-live="off" + xbl:inherits="value=label,crop,mirror" + flex="1" + crop="end"/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor><![CDATA[ + window.addEventListener("resize", this, false); + ]]></constructor> + + <destructor><![CDATA[ + window.removeEventListener("resize", this, false); + MousePosTracker.removeListener(this); + ]]></destructor> + + <property name="label"> + <setter><![CDATA[ + if (!this.label) { + this.removeAttribute("mirror"); + this.removeAttribute("sizelimit"); + } + + this.style.minWidth = this.getAttribute("type") == "status" && + this.getAttribute("previoustype") == "status" + ? getComputedStyle(this).width : ""; + + if (val) { + this.setAttribute("label", val); + this.removeAttribute("inactive"); + this._calcMouseTargetRect(); + MousePosTracker.addListener(this); + } else { + this.setAttribute("inactive", "true"); + MousePosTracker.removeListener(this); + } + + return val; + ]]></setter> + <getter> + return this.hasAttribute("inactive") ? "" : this.getAttribute("label"); + </getter> + </property> + + <method name="getMouseTargetRect"> + <body><![CDATA[ + return this._mouseTargetRect; + ]]></body> + </method> + + <method name="onMouseEnter"> + <body> + this._mirror(); + </body> + </method> + + <method name="onMouseLeave"> + <body> + this._mirror(); + </body> + </method> + + <method name="handleEvent"> + <parameter name="event"/> + <body><![CDATA[ + if (!this.label) + return; + + switch (event.type) { + case "resize": + this._calcMouseTargetRect(); + break; + } + ]]></body> + </method> + + <method name="_calcMouseTargetRect"> + <body><![CDATA[ + let container = this.parentNode; + let alignRight = (getComputedStyle(container).direction == "rtl"); + let panelRect = this.getBoundingClientRect(); + let containerRect = container.getBoundingClientRect(); + + this._mouseTargetRect = { + top: panelRect.top, + bottom: panelRect.bottom, + left: alignRight ? containerRect.right - panelRect.width : containerRect.left, + right: alignRight ? containerRect.right : containerRect.left + panelRect.width + }; + ]]></body> + </method> + + <method name="_mirror"> + <body> + if (this.hasAttribute("mirror")) + this.removeAttribute("mirror"); + else + this.setAttribute("mirror", "true"); + + if (!this.hasAttribute("sizelimit")) { + this.setAttribute("sizelimit", "true"); + this._calcMouseTargetRect(); + } + </body> + </method> + </implementation> + </binding> + + <binding id="tabbrowser-tabpanels" + extends="chrome://global/content/bindings/tabbox.xml#tabpanels"> + <implementation> + <field name="_selectedIndex">0</field> + + <property name="selectedIndex"> + <getter> + <![CDATA[ + return this._selectedIndex; + ]]> + </getter> + + <setter> + <![CDATA[ + if (val < 0 || val >= this.childNodes.length) + return val; + + let toTab = this.getRelatedElement(this.childNodes[val]); + + gBrowser._getSwitcher().requestTab(toTab); + + var panel = this._selectedPanel; + var newPanel = this.childNodes[val]; + this._selectedPanel = newPanel; + if (this._selectedPanel != panel) { + var event = document.createEvent("Events"); + event.initEvent("select", true, true); + this.dispatchEvent(event); + + this._selectedIndex = val; + } + + return val; + ]]> + </setter> + </property> + </implementation> + </binding> + + <binding id="tabbrowser-browser" + extends="chrome://global/content/bindings/browser.xml#browser"> + <implementation> + <field name="tabModalPromptBox">null</field> + + <!-- throws exception for unknown schemes --> + <method name="loadURIWithFlags"> + <parameter name="aURI"/> + <parameter name="aFlags"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <body> + <![CDATA[ + var params = arguments[1]; + if (typeof(params) == "number") { + params = { + flags: aFlags, + referrerURI: aReferrerURI, + charset: aCharset, + postData: aPostData, + }; + } + _loadURIWithFlags(this, aURI, params); + ]]> + </body> + </method> + </implementation> + </binding> + + <binding id="tabbrowser-remote-browser" + extends="chrome://global/content/bindings/remote-browser.xml#remote-browser"> + <implementation> + <field name="tabModalPromptBox">null</field> + + <!-- throws exception for unknown schemes --> + <method name="loadURIWithFlags"> + <parameter name="aURI"/> + <parameter name="aFlags"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <body> + <![CDATA[ + var params = arguments[1]; + if (typeof(params) == "number") { + params = { + flags: aFlags, + referrerURI: aReferrerURI, + charset: aCharset, + postData: aPostData, + }; + } + _loadURIWithFlags(this, aURI, params); + ]]> + </body> + </method> + </implementation> + </binding> + +</bindings> diff --git a/application/basilisk/base/content/urlbarBindings.xml b/application/basilisk/base/content/urlbarBindings.xml new file mode 100644 index 000000000..eb3150581 --- /dev/null +++ b/application/basilisk/base/content/urlbarBindings.xml @@ -0,0 +1,2758 @@ +<?xml version="1.0"?> + +<!-- +-*- Mode: HTML -*- +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. +--> + +<!DOCTYPE bindings [ +<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd"> +%notificationDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> +%browserDTD; +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +]> + +<bindings id="urlbarBindings" xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:xbl="http://www.mozilla.org/xbl"> + + <binding id="urlbar" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete"> + + <content sizetopopup="pref"> + <xul:hbox anonid="textbox-container" + class="autocomplete-textbox-container urlbar-textbox-container" + flex="1" xbl:inherits="focused"> + <children includes="image|deck|stack|box"> + <xul:image class="autocomplete-icon" allowevents="true"/> + </children> + <xul:hbox anonid="textbox-input-box" + class="textbox-input-box urlbar-input-box" + flex="1" xbl:inherits="tooltiptext=inputtooltiptext"> + <children/> + <html:input anonid="input" + class="autocomplete-textbox urlbar-input textbox-input uri-element-right-align" + allowevents="true" + inputmode="url" + xbl:inherits="tooltiptext=inputtooltiptext,value,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/> + </xul:hbox> + <xul:dropmarker anonid="historydropmarker" + class="autocomplete-history-dropmarker urlbar-history-dropmarker" + tooltiptext="&urlbar.openHistoryPopup.tooltip;" + allowevents="true" + xbl:inherits="open,enablehistory,parentfocused=focused"/> + <children includes="hbox"/> + </xul:hbox> + <xul:popupset anonid="popupset" + class="autocomplete-result-popupset"/> + <children includes="toolbarbutton"/> + </content> + + <implementation implements="nsIObserver, nsIDOMEventListener"> + <field name="AppConstants" readonly="true"> + (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants; + </field> +#ifdef MOZ_WEBEXTENSIONS + <field name="ExtensionSearchHandler" readonly="true"> + (Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler; + </field> +#endif + + <constructor><![CDATA[ + this._prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService) + .getBranch("browser.urlbar."); + + this._prefs.addObserver("", this, false); + this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll"); + this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll"); + this.completeDefaultIndex = this._prefs.getBoolPref("autoFill"); + this.timeout = this._prefs.getIntPref("delay"); + this._formattingEnabled = this._prefs.getBoolPref("formatting.enabled"); + this._mayTrimURLs = this._prefs.getBoolPref("trimURLs"); + this._cacheUserMadeSearchSuggestionsChoice(); + this.inputField.controllers.insertControllerAt(0, this._copyCutController); + this.inputField.addEventListener("paste", this, false); + this.inputField.addEventListener("mousedown", this, false); + this.inputField.addEventListener("mousemove", this, false); + this.inputField.addEventListener("mouseout", this, false); + this.inputField.addEventListener("overflow", this, false); + this.inputField.addEventListener("underflow", this, false); + + var textBox = document.getAnonymousElementByAttribute(this, + "anonid", "textbox-input-box"); + var cxmenu = document.getAnonymousElementByAttribute(textBox, + "anonid", "input-box-contextmenu"); + var pasteAndGo; + cxmenu.addEventListener("popupshowing", function() { + if (!pasteAndGo) + return; + var controller = document.commandDispatcher.getControllerForCommand("cmd_paste"); + var enabled = controller.isCommandEnabled("cmd_paste"); + if (enabled) + pasteAndGo.removeAttribute("disabled"); + else + pasteAndGo.setAttribute("disabled", "true"); + }, false); + + var insertLocation = cxmenu.firstChild; + while (insertLocation.nextSibling && + insertLocation.getAttribute("cmd") != "cmd_paste") + insertLocation = insertLocation.nextSibling; + if (insertLocation) { + pasteAndGo = document.createElement("menuitem"); + let label = Services.strings.createBundle("chrome://browser/locale/browser.properties"). + GetStringFromName("pasteAndGo.label"); + pasteAndGo.setAttribute("label", label); + pasteAndGo.setAttribute("anonid", "paste-and-go"); + pasteAndGo.setAttribute("oncommand", + "gURLBar.select(); goDoCommand('cmd_paste'); gURLBar.handleCommand();"); + cxmenu.insertBefore(pasteAndGo, insertLocation.nextSibling); + } + + this._enableOrDisableOneOffSearches(); + ]]></constructor> + + <destructor><![CDATA[ + this._prefs.removeObserver("", this); + this._prefs = null; + this.inputField.controllers.removeController(this._copyCutController); + this.inputField.removeEventListener("paste", this, false); + this.inputField.removeEventListener("mousedown", this, false); + this.inputField.removeEventListener("mousemove", this, false); + this.inputField.removeEventListener("mouseout", this, false); + this.inputField.removeEventListener("overflow", this, false); + this.inputField.removeEventListener("underflow", this, false); + ]]></destructor> + + <field name="_value">""</field> + <field name="gotResultForCurrentQuery">false</field> + + <!-- + This is set around HandleHenter so it can be used in handleCommand. + It is also used to track whether we must handle a delayed handleEnter, + by checking if it has been cleared. + --> + <field name="handleEnterInstance">null</field> + + <!-- + For performance reasons we want to limit the size of the text runs we + build and show to the user. + --> + <field name="textRunsMaxLen">255</field> + + <!-- + onBeforeValueGet is called by the base-binding's .value getter. + It can return an object with a "value" property, to override the + return value of the getter. + --> + <method name="onBeforeValueGet"> + <body><![CDATA[ + return { value: this._value }; + ]]></body> + </method> + + <!-- + onBeforeValueSet is called by the base-binding's .value setter. + It should return the value that the setter should use. + --> + <method name="onBeforeValueSet"> + <parameter name="aValue"/> + <body><![CDATA[ + this._value = aValue; + var returnValue = aValue; + var action = this._parseActionUrl(aValue); + + if (action) { + switch (action.type) { + case "switchtab": // Fall through. + case "remotetab": // Fall through. + case "visiturl": { + returnValue = action.params.displayUrl; + break; + } + case "keyword": // Fall through. + case "searchengine": { + returnValue = action.params.input; + break; + } + case "extension": { + returnValue = action.params.content; + break; + } + } + } else { + let originalUrl = ReaderMode.getOriginalUrl(aValue); + if (originalUrl) { + returnValue = originalUrl; + } + } + + // Set the actiontype only if the user is not overriding actions. + if (action && this._pressedNoActionKeys.size == 0) { + this.setAttribute("actiontype", action.type); + } else { + this.removeAttribute("actiontype"); + } + return returnValue; + ]]></body> + </method> + + <method name="onKeyPress"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_LEFT: + case KeyEvent.DOM_VK_RIGHT: + case KeyEvent.DOM_VK_HOME: + // Reset the selected index so that nsAutoCompleteController + // simply closes the popup without trying to fill anything. + this.popup.selectedIndex = -1; + break; + } + if (this.popup.popupOpen && + !this.popup.disableKeyNavigation && + this.popup.handleKeyPress(aEvent)) { + return true; + } + return this.handleKeyPress(aEvent); + ]]></body> + </method> + + <field name="_mayTrimURLs">true</field> + <method name="trimValue"> + <parameter name="aURL"/> + <body><![CDATA[ + // This method must not modify the given URL such that calling + // nsIURIFixup::createFixupURI with the result will produce a different URI. + return this._mayTrimURLs ? trimURL(aURL) : aURL; + ]]></body> + </method> + + <field name="_formattingEnabled">true</field> + <method name="formatValue"> + <body><![CDATA[ + if (!this._formattingEnabled || !this.editor) + return; + + let controller = this.editor.selectionController; + let strikeOut = controller.getSelection(controller.SELECTION_URLSTRIKEOUT); + strikeOut.removeAllRanges(); + + let selection = controller.getSelection(controller.SELECTION_URLSECONDARY); + selection.removeAllRanges(); + + if (this.focused) + return; + + let textNode = this.editor.rootElement.firstChild; + let value = textNode.textContent; + if (!value) + return; + + // Get the URL from the fixup service: + let flags = Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | + Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP; + let uriInfo; + try { + uriInfo = Services.uriFixup.getFixupURIInfo(value, flags); + } catch (ex) {} + // Ignore if we couldn't make a URI out of this, the URI resulted in a search, + // or the URI has a non-http(s)/ftp protocol. + if (!uriInfo || + !uriInfo.fixedURI || + uriInfo.keywordProviderName || + ["http", "https", "ftp"].indexOf(uriInfo.fixedURI.scheme) == -1) { + return; + } + + // If we trimmed off the http scheme, ensure we stick it back on before + // trying to figure out what domain we're accessing, so we don't get + // confused by user:pass@host http URLs. We later use + // trimmedLength to ensure we don't count the length of a trimmed protocol + // when determining which parts of the URL to highlight as "preDomain". + let trimmedLength = 0; + if (uriInfo.fixedURI.scheme == "http" && !value.startsWith("http://")) { + value = "http://" + value; + trimmedLength = "http://".length; + } + + let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(\S+?)(?::\d+)?\s*(?:[\/#?]|$)/); + if (!matchedURL) + return; + + // Strike out the "https" part if mixed active content is loaded. + if (this.getAttribute("pageproxystate") == "valid" && + value.startsWith("https:") && + gBrowser.securityUI.state & + Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) { + let range = document.createRange(); + range.setStart(textNode, 0); + range.setEnd(textNode, 5); + strikeOut.addRange(range); + } + + let [, preDomain, domain] = matchedURL; + let baseDomain = domain; + let subDomain = ""; + try { + baseDomain = Services.eTLD.getBaseDomainFromHost(uriInfo.fixedURI.host); + if (!domain.endsWith(baseDomain)) { + // getBaseDomainFromHost converts its resultant to ACE. + let IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + baseDomain = IDNService.convertACEtoUTF8(baseDomain); + } + } catch (e) {} + if (baseDomain != domain) { + subDomain = domain.slice(0, -baseDomain.length); + } + + let rangeLength = preDomain.length + subDomain.length - trimmedLength; + if (rangeLength) { + let range = document.createRange(); + range.setStart(textNode, 0); + range.setEnd(textNode, rangeLength); + selection.addRange(range); + } + + let startRest = preDomain.length + domain.length - trimmedLength; + if (startRest < value.length - trimmedLength) { + let range = document.createRange(); + range.setStart(textNode, startRest); + range.setEnd(textNode, value.length - trimmedLength); + selection.addRange(range); + } + ]]></body> + </method> + + <method name="handleRevert"> + <body><![CDATA[ + var isScrolling = this.popupOpen; + + gBrowser.userTypedValue = null; + + // don't revert to last valid url unless page is NOT loading + // and user is NOT key-scrolling through autocomplete list + if (!XULBrowserWindow.isBusy && !isScrolling) { + URLBarSetURI(); + + // If the value isn't empty and the urlbar has focus, select the value. + if (this.value && this.hasAttribute("focused")) + this.select(); + } + + // tell widget to revert to last typed text only if the user + // was scrolling when they hit escape + return !isScrolling; + ]]></body> + </method> + + <!-- + This is ultimately called by the autocomplete controller as the result + of handleEnter when the Return key is pressed in the textbox. Since + onPopupClick also calls handleEnter, this is also called as a result in + that case. + + @param event + The event that triggered the command. + @param openUILinkWhere + Optional. The "where" to pass to openUILinkIn. This method + computes the appropriate "where" given the event, but you can + use this to override it. + @param openUILinkParams + Optional. The parameters to pass to openUILinkIn. As with + "where", this method computes the appropriate parameters, but + any parameters you supply here will override those. + --> + <method name="handleCommand"> + <parameter name="event"/> + <parameter name="openUILinkWhere"/> + <parameter name="openUILinkParams"/> + <body><![CDATA[ + let isMouseEvent = event instanceof MouseEvent; + if (isMouseEvent && event.button == 2) { + // Do nothing for right clicks. + return; + } + + // Determine whether to use the selected one-off search button. In + // one-off search buttons parlance, "selected" means that the button + // has been navigated to via the keyboard. So we want to use it if + // the triggering event is not a mouse click -- i.e., it's a Return + // key -- or if the one-off was mouse-clicked. + let selectedOneOff = this.popup.oneOffSearchButtons.selectedButton; + if (selectedOneOff && + isMouseEvent && + event.originalTarget != selectedOneOff) { + selectedOneOff = null; + } + + // Do the command of the selected one-off if it's not an engine. + if (selectedOneOff && !selectedOneOff.engine) { + selectedOneOff.doCommand(); + return; + } + + let where = openUILinkWhere; + if (!where) { + if (isMouseEvent) { + where = whereToOpenLink(event, false, false); + } else { + // If the current tab is empty, ignore Alt+Enter (reuse this tab) + let altEnter = !isMouseEvent && + event && + event.altKey && + !isTabEmpty(gBrowser.selectedTab); + where = altEnter ? "tab" : "current"; + } + } + + let url = this.value; + if (!url) { + return; + } + + let mayInheritPrincipal = false; + let postData = null; + let browser = gBrowser.selectedBrowser; + let action = this._parseActionUrl(url); + + if (selectedOneOff && selectedOneOff.engine) { + // If there's a selected one-off button then load a search using + // the one-off's engine. + [url, postData] = + this._parseAndRecordSearchEngineLoad(selectedOneOff.engine, + this.oneOffSearchQuery, + event, where, + openUILinkParams); + } else if (action) { + switch (action.type) { + case "visiturl": + // Unifiedcomplete uses fixupURI to tell if something is a visit + // or a search, and passes out the fixedURI as the url param. + // By using that uri we would end up passing a different string + // to the docshell that may run a different not-found heuristic. + // For example, "mozilla/run" would be fixed by unifiedcomplete + // to "http://mozilla/run". The docshell, once it can't resolve + // mozilla, would note the string has a scheme, and try to load + // http://mozilla.com/run instead of searching "mozilla/run". + // So, if we have the original input at hand, we pass it through + // and let the docshell handle it. + if (action.params.input) { + url = action.params.input; + break; + } + url = action.params.url; + break; + case "remotetab": + url = action.params.url; + break; + case "keyword": + if (action.params.postData) { + postData = getPostDataStream(action.params.postData); + } + mayInheritPrincipal = true; + url = action.params.url; + break; + case "switchtab": + url = action.params.url; + if (this.hasAttribute("actiontype")) { + this.handleRevert(); + let prevTab = gBrowser.selectedTab; + if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) { + gBrowser.removeTab(prevTab); + } + return; + } + break; + case "searchengine": + if (selectedOneOff && selectedOneOff.engine) { + // Replace the engine with the selected one-off engine. + action.params.engineName = selectedOneOff.engine.name; + } + const actionDetails = { + isSuggestion: !!action.params.searchSuggestion, + isAlias: !!action.params.alias + }; + [url, postData] = this._parseAndRecordSearchEngineLoad( + action.params.engineName, + action.params.searchSuggestion || action.params.searchQuery, + event, + where, + openUILinkParams, + actionDetails + ); + break; +#ifdef MOZ_WEBEXTENSIONS + case "extension": + this.handleRevert(); + // Give the extension control of handling the command. + let searchString = action.params.content; + let keyword = action.params.keyword; + this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where); + return; +#endif + } + } else { + // This is a fallback for add-ons and old testing code that directly + // set value and try to confirm it. UnifiedComplete should always + // resolve to a valid url. + try { + new URL(url); + } catch (ex) { + let lastLocationChange = browser.lastLocationChange; + getShortcutOrURIAndPostData(url).then(data => { + if (where != "current" || + browser.lastLocationChange == lastLocationChange) { + this._loadURL(data.url, browser, data.postData, where, + openUILinkParams, data.mayInheritPrincipal); + } + }); + return; + } + } + + this._loadURL(url, browser, postData, where, openUILinkParams, + mayInheritPrincipal); + ]]></body> + </method> + + <property name="oneOffSearchQuery"> + <getter><![CDATA[ + // this.textValue may be an autofilled string. Search only with the + // portion that the user typed, if any, by preferring the autocomplete + // controller's searchString (including handleEnterInstance.searchString). + return (this.handleEnterInstance && this.handleEnterInstance.searchString) || + this.mController.searchString || + this.textValue; + ]]></getter> + </property> + + <method name="_loadURL"> + <parameter name="url"/> + <parameter name="browser"/> + <parameter name="postData"/> + <parameter name="openUILinkWhere"/> + <parameter name="openUILinkParams"/> + <parameter name="mayInheritPrincipal"/> + <body><![CDATA[ + this.value = url; + browser.userTypedValue = url; + if (gInitialPages.includes(url)) { + browser.initialPageLoadedFromURLBar = url; + } + try { + addToUrlbarHistory(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); + } + + let params = { + postData, + allowThirdPartyFixup: true, + currentBrowser: browser, + }; + if (openUILinkWhere == "current") { + params.indicateErrorPageLoad = true; + params.allowPinnedTabHostChange = true; + params.disallowInheritPrincipal = !mayInheritPrincipal; + params.allowPopups = url.startsWith("javascript:"); + } else { + params.initiatingDoc = document; + } + + if (openUILinkParams) { + for (let key in openUILinkParams) { + params[key] = openUILinkParams[key]; + } + } + + // Focus the content area before triggering loads, since if the load + // occurs in a new tab, we want focus to be restored to the content + // area when the current tab is re-selected. + browser.focus(); + + if (openUILinkWhere != "current") { + this.handleRevert(); + } + + try { + openUILinkIn(url, openUILinkWhere, params); + } catch (ex) { + // This load can throw an exception in certain cases, which means + // we'll want to replace the URL with the loaded URL: + if (ex.result != Cr.NS_ERROR_LOAD_SHOWED_ERRORPAGE) { + this.handleRevert(); + } + } + + if (openUILinkWhere == "current") { + // Ensure the start of the URL is visible for usability reasons. + this.selectionStart = this.selectionEnd = 0; + } + ]]></body> + </method> + + <method name="_parseAndRecordSearchEngineLoad"> + <parameter name="engineOrEngineName"/> + <parameter name="query"/> + <parameter name="event"/> + <parameter name="openUILinkWhere"/> + <parameter name="openUILinkParams"/> + <parameter name="searchActionDetails"/> + <body><![CDATA[ + let engine = + typeof(engineOrEngineName) == "string" ? + Services.search.getEngineByName(engineOrEngineName) : + engineOrEngineName; + let isOneOff = this.popup.oneOffSearchButtons + .maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams); + // Infer the type of the event which triggered the search. + let eventType = "unknown"; + if (event instanceof KeyboardEvent) { + eventType = "key"; + } else if (event instanceof MouseEvent) { + eventType = "mouse"; + } + // Augment the search action details object. + let details = searchActionDetails || {}; + details.isOneOff = isOneOff; + details.type = eventType; + + BrowserSearch.recordSearchInTelemetry(engine, "urlbar", details); + let submission = engine.getSubmission(query, null, "keyword"); + return [submission.uri.spec, submission.postData]; + ]]></body> + </method> + + <method name="maybeCanonizeURL"> + <parameter name="aTriggeringEvent"/> + <parameter name="aUrl"/> + <body><![CDATA[ + // Only add the suffix when the URL bar value isn't already "URL-like", + // and only if we get a keyboard event, to match user expectations. + if (!/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(aUrl) || + !(aTriggeringEvent instanceof KeyEvent)) { + return; + } + + let url = aUrl; + let accel = this.AppConstants.platform == "macosx" ? + aTriggeringEvent.metaKey : + aTriggeringEvent.ctrlKey; + let shift = aTriggeringEvent.shiftKey; + let suffix = ""; + + switch (true) { + case (accel && shift): + suffix = ".org/"; + break; + case (shift): + suffix = ".net/"; + break; + case (accel): + try { + suffix = gPrefService.getCharPref("browser.fixup.alternate.suffix"); + if (suffix.charAt(suffix.length - 1) != "/") + suffix += "/"; + } catch (e) { + suffix = ".com/"; + } + break; + } + + if (!suffix) + return; + + // trim leading/trailing spaces (bug 233205) + url = url.trim(); + + // Tack www. and suffix on. If user has appended directories, insert + // suffix before them (bug 279035). Be careful not to get two slashes. + let firstSlash = url.indexOf("/"); + if (firstSlash >= 0) { + url = url.substring(0, firstSlash) + suffix + + url.substring(firstSlash + 1); + } else { + url = url + suffix; + } + + this.popup.overrideValue = "http://www." + url; + ]]></body> + </method> + + <field name="_contentIsCropped">false</field> + + <method name="_initURLTooltip"> + <body><![CDATA[ + if (this.focused || !this._contentIsCropped) + return; + this.inputField.setAttribute("tooltiptext", this.value); + ]]></body> + </method> + + <method name="_hideURLTooltip"> + <body><![CDATA[ + this.inputField.removeAttribute("tooltiptext"); + ]]></body> + </method> + + <method name="_getDroppableLink"> + <parameter name="aEvent"/> + <body><![CDATA[ + let links = browserDragAndDrop.dropLinks(aEvent); + // The URL bar automatically handles inputs with newline characters, + // so we can get away with treating text/x-moz-url flavours as text/plain. + if (links.length > 0 && links[0].url) { + aEvent.preventDefault(); + let url = links[0].url; + let strippedURL = stripUnsafeProtocolOnPaste(url); + if (strippedURL != url) { + aEvent.stopImmediatePropagation(); + return null; + } + try { + urlSecurityCheck(url, + gBrowser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + } catch (ex) { + return null; + } + return url; + } + return null; + ]]></body> + </method> + + <method name="onDragOver"> + <parameter name="aEvent"/> + <body><![CDATA[ + // We don't need the link here, so we ignore the return value. + if (!this._getDroppableLink(aEvent)) { + aEvent.dataTransfer.dropEffect = "none"; + } + ]]></body> + </method> + + <method name="onDrop"> + <parameter name="aEvent"/> + <body><![CDATA[ + let url = this._getDroppableLink(aEvent); + if (url) { + this.value = url; + SetPageProxyState("invalid"); + this.focus(); + this.handleCommand(); + // Force not showing the dropped URI immediately. + gBrowser.userTypedValue = null; + URLBarSetURI(); + } + ]]></body> + </method> + + <method name="_getSelectedValueForClipboard"> + <body><![CDATA[ + // Grab the actual input field's value, not our value, which could include moz-action: + var inputVal = this.inputField.value; + var selectedVal = inputVal.substring(this.selectionStart, this.selectionEnd); + + // If the selection doesn't start at the beginning or doesn't span the full domain or + // the URL bar is modified or there is no text at all, nothing else to do here. + if (this.selectionStart > 0 || this.valueIsTyped || selectedVal == "") + return selectedVal; + // The selection doesn't span the full domain if it doesn't contain a slash and is + // followed by some character other than a slash. + if (!selectedVal.includes("/")) { + let remainder = inputVal.replace(selectedVal, ""); + if (remainder != "" && remainder[0] != "/") + return selectedVal; + } + + let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup); + + let uri; + if (this.getAttribute("pageproxystate") == "valid") { + uri = gBrowser.currentURI; + } else { + // We're dealing with an autocompleted value, create a new URI from that. + try { + uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_NONE); + } catch (e) {} + if (!uri) + return selectedVal; + } + + // Avoid copying 'about:reader?url=', and always provide the original URI: + let readerOriginalURL = ReaderMode.getOriginalUrl(uri.spec); + if (readerOriginalURL) { + uri = uriFixup.createFixupURI(readerOriginalURL, Ci.nsIURIFixup.FIXUP_FLAG_NONE); + } + + // Only copy exposable URIs + try { + uri = uriFixup.createExposableURI(uri); + } catch (ex) {} + + // If the entire URL is selected, just use the actual loaded URI. + if (inputVal == selectedVal) { + // ... but only if isn't a javascript: or data: URI, since those + // are hard to read when encoded + if (!uri.schemeIs("javascript") && !uri.schemeIs("data")) { + selectedVal = uri.spec; + } + + return selectedVal; + } + + // Just the beginning of the URL is selected, check for a trimmed + // value + let spec = uri.spec; + let trimmedSpec = this.trimValue(spec); + if (spec != trimmedSpec) { + // Prepend the portion that trimValue removed from the beginning. + // This assumes trimValue will only truncate the URL at + // the beginning or end (or both). + let trimmedSegments = spec.split(trimmedSpec); + selectedVal = trimmedSegments[0] + selectedVal; + } + + return selectedVal; + ]]></body> + </method> + + <field name="_copyCutController"><![CDATA[ + ({ + urlbar: this, + doCommand: function(aCommand) { + var urlbar = this.urlbar; + var val = urlbar._getSelectedValueForClipboard(); + if (!val) + return; + + if (aCommand == "cmd_cut" && this.isCommandEnabled(aCommand)) { + let start = urlbar.selectionStart; + let end = urlbar.selectionEnd; + urlbar.inputField.value = urlbar.inputField.value.substring(0, start) + + urlbar.inputField.value.substring(end); + urlbar.selectionStart = urlbar.selectionEnd = start; + + let event = document.createEvent("UIEvents"); + event.initUIEvent("input", true, false, window, 0); + urlbar.dispatchEvent(event); + + SetPageProxyState("invalid"); + } + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(val); + }, + supportsCommand: function(aCommand) { + switch (aCommand) { + case "cmd_copy": + case "cmd_cut": + return true; + } + return false; + }, + isCommandEnabled: function(aCommand) { + return this.supportsCommand(aCommand) && + (aCommand != "cmd_cut" || !this.urlbar.readOnly) && + this.urlbar.selectionStart < this.urlbar.selectionEnd; + }, + onEvent: function(aEventName) {} + }) + ]]></field> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + if (aTopic == "nsPref:changed") { + switch (aData) { + case "clickSelectsAll": + case "doubleClickSelectsAll": + this[aData] = this._prefs.getBoolPref(aData); + break; + case "autoFill": + this.completeDefaultIndex = this._prefs.getBoolPref(aData); + break; + case "delay": + this.timeout = this._prefs.getIntPref(aData); + break; + case "formatting.enabled": + this._formattingEnabled = this._prefs.getBoolPref(aData); + break; + case "suggest.searches": + case "userMadeSearchSuggestionsChoice": + // Mirror the value for future use, see the comment in the + // binding's constructor. + this._prefs.setBoolPref("searchSuggestionsChoice", + this._prefs.getBoolPref("suggest.searches")); + + this._cacheUserMadeSearchSuggestionsChoice(); + if (this._userMadeSearchSuggestionsChoice) { + this.popup.searchSuggestionsNotificationWasDismissed( + this._prefs.getBoolPref("suggest.searches") + ); + } + break; + case "trimURLs": + this._mayTrimURLs = this._prefs.getBoolPref(aData); + break; + case "oneOffSearches": + this._enableOrDisableOneOffSearches(); + break; + } + } + ]]></body> + </method> + + <method name="_enableOrDisableOneOffSearches"> + <body><![CDATA[ + let enable = this._prefs.getBoolPref("oneOffSearches"); + this.popup.enableOneOffSearches(enable); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "paste": + let originalPasteData = aEvent.clipboardData.getData("text/plain"); + if (!originalPasteData) { + return; + } + + let oldValue = this.inputField.value; + let oldStart = oldValue.substring(0, this.inputField.selectionStart); + // If there is already non-whitespace content in the URL bar + // preceding the pasted content, it's not necessary to check + // protocols used by the pasted content: + if (oldStart.trim()) { + return; + } + let oldEnd = oldValue.substring(this.inputField.selectionEnd); + + let pasteData = stripUnsafeProtocolOnPaste(originalPasteData); + if (originalPasteData != pasteData) { + // Unfortunately we're not allowed to set the bits being pasted + // so cancel this event: + aEvent.preventDefault(); + aEvent.stopImmediatePropagation(); + + this.inputField.value = oldStart + pasteData + oldEnd; + // Fix up cursor/selection: + let newCursorPos = oldStart.length + pasteData.length; + this.inputField.selectionStart = newCursorPos; + this.inputField.selectionEnd = newCursorPos; + } + break; + case "mousedown": + if (this.doubleClickSelectsAll && + aEvent.button == 0 && aEvent.detail == 2) { + this.editor.selectAll(); + aEvent.preventDefault(); + } + break; + case "mousemove": + this._initURLTooltip(); + break; + case "mouseout": + this._hideURLTooltip(); + break; + case "overflow": + this._contentIsCropped = true; + break; + case "underflow": + this._contentIsCropped = false; + this._hideURLTooltip(); + break; + } + ]]></body> + </method> + + <!-- + onBeforeTextValueSet is called by the base-binding's .textValue getter. + It should return the value that the getter should use. + --> + <method name="onBeforeTextValueGet"> + <body><![CDATA[ + return { value: this.inputField.value }; + ]]></body> + </method> + + <!-- + onBeforeTextValueSet is called by the base-binding's .textValue setter. + It should return the value that the setter should use. + --> + <method name="onBeforeTextValueSet"> + <parameter name="aValue"/> + <body><![CDATA[ + let val = aValue; + let uri; + try { + uri = makeURI(val); + } catch (ex) {} + + if (uri) { + // Do not touch moz-action URIs at all. They depend on being + // properly encoded and decoded and will break if decoded + // unexpectedly. + if (!this._parseActionUrl(val)) { + val = losslessDecodeURI(uri); + } + } + + return val; + ]]></body> + </method> + + <method name="_parseActionUrl"> + <parameter name="aUrl"/> + <body><![CDATA[ + const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/; + if (!MOZ_ACTION_REGEX.test(aUrl)) + return null; + + // URL is in the format moz-action:ACTION,PARAMS + // Where PARAMS is a JSON encoded object. + let [, type, params] = aUrl.match(MOZ_ACTION_REGEX); + + let action = { + type: type, + }; + + action.params = JSON.parse(params); + for (let key in action.params) { + action.params[key] = decodeURIComponent(action.params[key]); + } + + if ("url" in action.params) { + let uri; + try { + uri = makeURI(action.params.url); + action.params.displayUrl = losslessDecodeURI(uri); + } catch (e) { + action.params.displayUrl = action.params.url; + } + } + + return action; + ]]></body> + </method> + + <property name="_noActionKeys" readonly="true"> + <getter><![CDATA[ + if (!this.__noActionKeys) { + this.__noActionKeys = new Set([ + KeyEvent.DOM_VK_ALT, + KeyEvent.DOM_VK_SHIFT, + ]); + let modifier = this.AppConstants.platform == "macosx" ? + KeyEvent.DOM_VK_META : + KeyEvent.DOM_VK_CONTROL; + this.__noActionKeys.add(modifier); + } + return this.__noActionKeys; + ]]></getter> + </property> + + <field name="_pressedNoActionKeys"><![CDATA[ + new Set() + ]]></field> + + <method name="_clearNoActions"> + <parameter name="aURL"/> + <body><![CDATA[ + this._pressedNoActionKeys.clear(); + this.popup.removeAttribute("noactions"); + let action = this._parseActionUrl(this._value); + if (action) + this.setAttribute("actiontype", action.type); + ]]></body> + </method> + + <method name="onInput"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!this.mIgnoreInput && this.mController.input == this) { + this._value = this.inputField.value; + gBrowser.userTypedValue = this.value; + this.valueIsTyped = true; + // Only wait for a result when we are sure to get one. In some + // cases, like when pasting the same exact text, we may not fire + // a new search and we won't get a result. + if (this.mController.handleText()) { + this.gotResultForCurrentQuery = false; + } + } + this.resetActionType(); + ]]></body> + </method> + + <method name="handleEnter"> + <parameter name="event"/> + <body><![CDATA[ + // We need to ensure we're using a selected autocomplete result. + // A result should automatically be selected by default, + // however autocomplete is async and therefore we may not + // have a result set relating to the current input yet. If that + // happens, we need to mark that when the first result does get added, + // it needs to be handled as if enter was pressed with that first + // result selected. + // If anything other than the default (first) result is selected, then + // it must have been manually selected by the human. We let this + // explicit choice be used, even if it may be related to a previous + // input. + // However, if the default result is automatically selected, we + // ensure that it corresponds to the current input. + + // Store the current search string so it can be used in + // handleCommand, which will be called as a result of + // mController.handleEnter(). + // Note this is also used to detect if we should perform a delayed + // handleEnter, in such a case it won't have been cleared. + this.handleEnterInstance = { + searchString: this.mController.searchString, + event: event + }; + + if (this.popup.selectedIndex != 0 || this.gotResultForCurrentQuery) { + this.maybeCanonizeURL(event, this.value); + let rv = this.mController.handleEnter(false, event); + this.handleEnterInstance = null; + this.popup.overrideValue = null; + return rv; + } + + return true; + ]]></body> + </method> + + <method name="handleDelete"> + <body><![CDATA[ + // If the heuristic result is selected, then the autocomplete + // controller's handleDelete implementation will remove it, which is + // not what we want. So in that case, call handleText so it acts as + // a backspace on the text value instead of removing the result. + if (this.popup.selectedIndex == 0 && + this.popup._isFirstResultHeuristic) { + this.mController.handleText(); + return false; + } + return this.mController.handleDelete(); + ]]></body> + </method> + + <field name="_userMadeSearchSuggestionsChoice"><![CDATA[ + false + ]]></field> + + <method name="_cacheUserMadeSearchSuggestionsChoice"> + <body><![CDATA[ + this._userMadeSearchSuggestionsChoice = + this._prefs.getBoolPref("userMadeSearchSuggestionsChoice") || + this._prefs.getBoolPref("suggest.searches"); + ]]></body> + </method> + + <property name="shouldShowSearchSuggestionsNotification" readonly="true"> + <getter><![CDATA[ + return !this._userMadeSearchSuggestionsChoice && + !this.inPrivateContext && + // When _urlbarFocused is true, tabbrowser would close the + // popup if it's opened here, so don't show the notification. + !gBrowser.selectedBrowser._urlbarFocused && + Services.prefs.getBoolPref("browser.search.suggest.enabled") && + this._prefs.getIntPref("daysBeforeHidingSuggestionsPrompt"); + ]]></getter> + </property> + + </implementation> + + <handlers> + <handler event="keydown"><![CDATA[ + if (this._noActionKeys.has(event.keyCode) && + this.popup.selectedIndex >= 0 && + !this._pressedNoActionKeys.has(event.keyCode)) { + if (this._pressedNoActionKeys.size == 0) { + this.popup.setAttribute("noactions", "true"); + this.removeAttribute("actiontype"); + } + this._pressedNoActionKeys.add(event.keyCode); + } + ]]></handler> + + <handler event="keyup"><![CDATA[ + if (this._noActionKeys.has(event.keyCode) && + this._pressedNoActionKeys.has(event.keyCode)) { + this._pressedNoActionKeys.delete(event.keyCode); + if (this._pressedNoActionKeys.size == 0) + this._clearNoActions(); + } + ]]></handler> + + <handler event="focus"><![CDATA[ + if (event.originalTarget == this.inputField) { + this._hideURLTooltip(); + this.formatValue(); + } + ]]></handler> + + <handler event="blur"><![CDATA[ + if (event.originalTarget == this.inputField) { + this._clearNoActions(); + this.formatValue(); + } +#ifdef MOZ_WEBEXTENSIONS + if (ExtensionSearchHandler.hasActiveInputSession()) { + ExtensionSearchHandler.handleInputCancelled(); + } +#endif + ]]></handler> + + <handler event="dragstart" phase="capturing"><![CDATA[ + // Drag only if the gesture starts from the input field. + if (this.inputField != event.originalTarget && + !(this.inputField.compareDocumentPosition(event.originalTarget) & + Node.DOCUMENT_POSITION_CONTAINED_BY)) + return; + + // Drag only if the entire value is selected and it's a valid URI. + var isFullSelection = this.selectionStart == 0 && + this.selectionEnd == this.textLength; + if (!isFullSelection || + this.getAttribute("pageproxystate") != "valid") + return; + + var urlString = gBrowser.selectedBrowser.currentURI.spec; + var title = gBrowser.selectedBrowser.contentTitle || urlString; + var htmlString = "<a href=\"" + urlString + "\">" + urlString + "</a>"; + + var dt = event.dataTransfer; + dt.setData("text/x-moz-url", urlString + "\n" + title); + dt.setData("text/unicode", urlString); + dt.setData("text/html", htmlString); + + dt.effectAllowed = "copyLink"; + event.stopPropagation(); + ]]></handler> + + <handler event="dragover" phase="capturing" action="this.onDragOver(event, this);"/> + <handler event="drop" phase="capturing" action="this.onDrop(event, this);"/> + <handler event="select"><![CDATA[ + if (!Cc["@mozilla.org/widget/clipboard;1"] + .getService(Ci.nsIClipboard) + .supportsSelectionClipboard()) + return; + + if (!window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .isHandlingUserInput) + return; + + var val = this._getSelectedValueForClipboard(); + if (!val) + return; + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyStringToClipboard(val, Ci.nsIClipboard.kSelectionClipboard); + ]]></handler> + </handlers> + + </binding> + + <!-- Note: this binding is applied to the autocomplete popup used in web page content and extended in search.xml for the searchbar. --> + <binding id="browser-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-result-popup"> + <implementation> + <field name="AppConstants" readonly="true"> + (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants; + </field> + + <method name="openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body> + <![CDATA[ + // initially the panel is hidden + // to avoid impacting startup / new window performance + aInput.popup.hidden = false; + + // this method is defined on the base binding + this._openAutocompletePopup(aInput, aElement); + ]]></body> + </method> + + <method name="onPopupClick"> + <parameter name="aEvent"/> + <body><![CDATA[ + // Ignore all right-clicks + if (aEvent.button == 2) + return; + + var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); + + var searchBar = BrowserSearch.searchBar; + var popupForSearchBar = searchBar && searchBar.textbox == this.mInput; + if (popupForSearchBar) { + searchBar.telemetrySearchDetails = { + index: controller.selection.currentIndex, + kind: "mouse" + }; + } + + // Check for unmodified left-click, and use default behavior + if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey && + !aEvent.altKey && !aEvent.metaKey) { + controller.handleEnter(true, aEvent); + return; + } + + // Check for middle-click or modified clicks on the search bar + if (popupForSearchBar) { + // Handle search bar popup clicks + var search = controller.getValueAt(this.selectedIndex); + + // open the search results according to the clicking subtlety + var where = whereToOpenLink(aEvent, false, true); + let params = {}; + + // But open ctrl/cmd clicks on autocomplete items in a new background tab. + let modifier = this.AppConstants.platform == "macosx" ? + aEvent.metaKey : + aEvent.ctrlKey; + if (where == "tab" && (aEvent instanceof MouseEvent) && + (aEvent.button == 1 || modifier)) + params.inBackground = true; + + // leave the popup open for background tab loads + if (!(where == "tab" && params.inBackground)) { + // close the autocomplete popup and revert the entered search term + this.closePopup(); + controller.handleEscape(); + } + + searchBar.doSearch(search, where, null, params); + if (where == "tab" && params.inBackground) + searchBar.focus(); + else + searchBar.value = search; + } + ]]></body> + </method> + </implementation> + </binding> + + <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup"> + + <resources> + <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/> + <stylesheet src="chrome://browser/skin/searchbar.css"/> + </resources> + + <content ignorekeys="true" level="top" consumeoutsideclicks="never" + aria-owns="richlistbox"> + <xul:hbox anonid="search-suggestions-notification" + align="center" + role="alert" + aria-describedby="search-suggestions-notification-text"> + <xul:description flex="1"> + &urlbar.searchSuggestionsNotification.question; + <!-- Several things here are to make the label accessibile via an + accesskey so that a11y doesn't suck: the accesskey, using an + onclick handler instead of an href attribute, the control + attribute, and having the control attribute refer to a valid ID + that is the label itself. --> + <xul:label id="search-suggestions-notification-learn-more" + class="text-link" + role="link" + value="&urlbar.searchSuggestionsNotification.learnMore;" + accesskey="&urlbar.searchSuggestionsNotification.learnMore.accesskey;" + onclick="document.getBindingParent(this).openSearchSuggestionsNotificationLearnMoreURL();" + control="search-suggestions-notification-learn-more"/> + </xul:description> + <xul:button anonid="search-suggestions-notification-disable" + label="&urlbar.searchSuggestionsNotification.disable;" + accesskey="&urlbar.searchSuggestionsNotification.disable.accesskey;" + onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/> + <xul:button anonid="search-suggestions-notification-enable" + label="&urlbar.searchSuggestionsNotification.enable;" + accesskey="&urlbar.searchSuggestionsNotification.enable.accesskey;" + onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/> + </xul:hbox> + <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox" + flex="1"/> + <xul:hbox anonid="footer"> + <children/> + <xul:vbox anonid="one-off-search-buttons" + class="search-one-offs" + compact="true" + includecurrentengine="true" + disabletab="true" + flex="1"/> + </xul:hbox> + </content> + + <implementation> + <field name="_maxResults">0</field> + + <field name="_bundle" readonly="true"> + Cc["@mozilla.org/intl/stringbundle;1"]. + getService(Ci.nsIStringBundleService). + createBundle("chrome://browser/locale/places/places.properties"); + </field> + + <field name="searchSuggestionsNotification" readonly="true"> + document.getAnonymousElementByAttribute( + this, "anonid", "search-suggestions-notification" + ); + </field> + + <field name="footer" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "footer"); + </field> + + <field name="oneOffSearchButtons" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", + "one-off-search-buttons"); + </field> + + <field name="_oneOffSearchesEnabled">false</field> + + <field name="_overrideValue">null</field> + <property name="overrideValue" + onget="return this._overrideValue;" + onset="this._overrideValue = val; return val;"/> + + <method name="onPopupClick"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.button == 2) { + // Ignore right-clicks. + return; + } + // Otherwise "call super" -- do what autocomplete-base-popup does. + let controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); + controller.handleEnter(true, aEvent); + ]]></body> + </method> + + <method name="enableOneOffSearches"> + <parameter name="enable"/> + <body><![CDATA[ + this._oneOffSearchesEnabled = enable; + if (enable) { + this.oneOffSearchButtons.telemetryOrigin = "urlbar"; + this.oneOffSearchButtons.style.display = "-moz-box"; + this.oneOffSearchButtons.popup = this; + this.oneOffSearchButtons.textbox = this.input; + } else { + this.oneOffSearchButtons.telemetryOrigin = null; + this.oneOffSearchButtons.style.display = "none"; + this.oneOffSearchButtons.popup = null; + this.oneOffSearchButtons.textbox = null; + } + ]]></body> + </method> + + <method name="openSearchSuggestionsNotificationLearnMoreURL"> + <body><![CDATA[ + let url = Services.urlFormatter.formatURL( + Services.prefs.getCharPref("app.support.baseURL") + "suggestions" + ); + openUILinkIn(url, "tab"); + ]]></body> + </method> + + <method name="dismissSearchSuggestionsNotification"> + <parameter name="enableSuggestions"/> + <body><![CDATA[ + // Make sure the urlbar is focused. It won't be, for example, if the + // user used an accesskey to make an opt-in choice. mIgnoreFocus + // prevents the text from being selected. + this.input.mIgnoreFocus = true; + this.input.focus(); + this.input.mIgnoreFocus = false; + + Services.prefs.setBoolPref( + "browser.urlbar.suggest.searches", enableSuggestions + ); + Services.prefs.setBoolPref( + "browser.urlbar.userMadeSearchSuggestionsChoice", true + ); + // The input's pref observer will now hide the notification. + ]]></body> + </method> + + <!-- Override this so that navigating between items results in an item + always being selected. --> + <method name="getNextIndex"> + <parameter name="reverse"/> + <parameter name="amount"/> + <parameter name="index"/> + <parameter name="maxRow"/> + <body><![CDATA[ + if (maxRow < 0) + return -1; + + let newIndex = index + (reverse ? -1 : 1) * amount; + + // We only want to wrap if navigation is in any direction by one item, + // otherwise we clamp to one end of the list. + // ie, hitting page-down will only cause is to wrap if we're already + // at one end of the list. + + // Allow the selection to be removed if the first result is not a + // heuristic result. + if (!this._isFirstResultHeuristic) { + if (reverse && index == -1 || newIndex > maxRow && index != maxRow) + newIndex = maxRow; + else if (!reverse && index == -1 || newIndex < 0 && index != 0) + newIndex = 0; + + if (newIndex < 0 && index == 0 || newIndex > maxRow && index == maxRow) + newIndex = -1; + + return newIndex; + } + + // Otherwise do not allow the selection to be removed. + if (newIndex < 0) { + newIndex = index > 0 ? 0 : maxRow; + } else if (newIndex > maxRow) { + newIndex = index < maxRow ? maxRow : 0; + } + return newIndex; + ]]></body> + </method> + + <property name="_isFirstResultHeuristic" readonly="true"> + <getter> + <![CDATA[ + // The popup usually has a special "heuristic" first result (added + // by UnifiedComplete.js) that is automatically selected when the + // popup opens. + return this.input.mController.matchCount > 0 && + this.input.mController + .getStyleAt(0) + .split(/\s+/).indexOf("heuristic") > 0; + ]]> + </getter> + </property> + + <property name="maxResults" readonly="true"> + <getter> + <![CDATA[ + if (!this._maxResults) { + var prefService = + Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + this._maxResults = prefService.getIntPref("browser.urlbar.maxRichResults"); + } + return this._maxResults; + ]]> + </getter> + </property> + + <method name="openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body> + <![CDATA[ + // initially the panel is hidden + // to avoid impacting startup / new window performance + aInput.popup.hidden = false; + + let showNotification = aInput.shouldShowSearchSuggestionsNotification; + if (showNotification) { + let prefs = aInput._prefs; + let now = new Date(); + let date = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate(); + let previousDate = prefs.getIntPref("lastSuggestionsPromptDate"); + if (previousDate < date) { + let remainingDays = + prefs.getIntPref("daysBeforeHidingSuggestionsPrompt") - 1; + prefs.setIntPref("daysBeforeHidingSuggestionsPrompt", + remainingDays); + prefs.setIntPref("lastSuggestionsPromptDate", date); + if (!remainingDays) + showNotification = false; + } + } + + if (showNotification) { + this._showSearchSuggestionsNotification(); + } else if (this.classList.contains("showSearchSuggestionsNotification")) { + this._hideSearchSuggestionsNotification(); + } + + this._openAutocompletePopup(aInput, aElement); + ]]> + </body> + </method> + + <method name="_openAutocompletePopup"> + <parameter name="aInput"/> + <parameter name="aElement"/> + <body><![CDATA[ + if (this.mPopupOpen) { + return; + } + + this.mInput = aInput; + aInput.controller.setInitiallySelectedIndex(this._isFirstResultHeuristic ? 0 : -1); + this.view = aInput.controller.QueryInterface(Components.interfaces.nsITreeView); + this._invalidate(); + + var rect = window.document.documentElement.getBoundingClientRect(); + var width = rect.right - rect.left; + this.setAttribute("width", width); + + // Adjust the direction of the autocomplete popup list based on the textbox direction, bug 649840 + var popupDirection = aElement.ownerDocument.defaultView.getComputedStyle(aElement).direction; + this.style.direction = popupDirection; + + // Make the popup's starting margin negative so that the leading edge + // of the popup aligns with the window border. + let elementRect = aElement.getBoundingClientRect(); + if (popupDirection == "rtl") { + let offset = elementRect.right - rect.right + this.style.marginRight = offset + "px"; + } else { + let offset = rect.left - elementRect.left; + this.style.marginLeft = offset + "px"; + } + + // Keep the popup items' site icons aligned with the urlbar's identity + // icon if it's not too far from the edge of the window. If there are + // at most two toolbar buttons between the window edge and the urlbar, + // then consider that as "not too far." The forward button's + // visibility may have changed since the last time the popup was + // opened, so this needs to happen now. Do it *before* the popup + // opens because otherwise the items will visibly shift. + let nodes = [...document.getElementById("nav-bar-customization-target").childNodes]; + let urlbarPosition = nodes.findIndex(n => n.id == "urlbar-container"); + let alignSiteIcons = urlbarPosition <= 2 && + nodes.slice(0, urlbarPosition) + .every(n => n.localName == "toolbarbutton"); + if (alignSiteIcons) { + let identityRect = + document.getElementById("identity-icon").getBoundingClientRect(); + this.siteIconStart = popupDirection == "rtl" ? identityRect.right + : identityRect.left; + } + else { + // Reset the alignment so that the site icons are positioned + // according to whatever's in the CSS. + this.siteIconStart = undefined; + } + + // Position the popup below the navbar. To get the y-coordinate, + // which is an offset from the bottom of the input, subtract the + // bottom of the navbar from the buttom of the input. + let yOffset = + document.getElementById("nav-bar").getBoundingClientRect().bottom - + aInput.getBoundingClientRect().bottom; + this.openPopup(aElement, "after_start", 0, yOffset, false, false); + ]]></body> + </method> + + <method name="_updateFooterVisibility"> + <body> + <![CDATA[ + this.footer.collapsed = this._matchCount == 0; + ]]> + </body> + </method> + + <method name="_showSearchSuggestionsNotification"> + <body> + <![CDATA[ + // With the notification shown, the listbox's height can sometimes be + // too small when it's flexed, as it normally is. Also, it can start + // out slightly scrolled down. Both problems appear together, most + // often when the popup is very narrow and the notification's text + // must wrap. Work around them by removing the flex. + // + // But without flexing the listbox, the listbox's height animation + // sometimes fails to complete, leaving the popup too tall. Work + // around that problem by disabling the listbox animation. + this.richlistbox.flex = 0; + this.setAttribute("dontanimate", "true"); + + this.classList.add("showSearchSuggestionsNotification"); + this._updateFooterVisibility(); + + // This event allows accessibility APIs to see the notification. + if (!this.popupOpen) { + let event = document.createEvent("Events"); + event.initEvent("AlertActive", true, true); + this.searchSuggestionsNotification.dispatchEvent(event); + } + ]]> + </body> + </method> + + <method name="searchSuggestionsNotificationWasDismissed"> + <parameter name="enableSuggestions"/> + <body> + <![CDATA[ + if (!this.popupOpen) { + this._hideSearchSuggestionsNotification(); + return; + } + this._hideSearchSuggestionsNotificationWithAnimation().then(() => { + if (enableSuggestions && this.input.textValue) { + // Start a new search so that suggestions appear immediately. + this.input.controller.startSearch(this.input.textValue); + } + }); + ]]> + </body> + </method> + + <method name="_hideSearchSuggestionsNotification"> + <body> + <![CDATA[ + this.classList.remove("showSearchSuggestionsNotification"); + this.richlistbox.flex = 1; + this.removeAttribute("dontanimate"); + if (this._matchCount) { + // Update popup height. + this._invalidate(); + } else { + this.closePopup(); + } + ]]> + </body> + </method> + + <method name="_hideSearchSuggestionsNotificationWithAnimation"> + <body> + <![CDATA[ + return new Promise(resolve => { + let notificationHeight = this.searchSuggestionsNotification + .getBoundingClientRect() + .height; + this.searchSuggestionsNotification.style.marginTop = + "-" + notificationHeight + "px"; + + let popupHeightPx = + (this.getBoundingClientRect().height - notificationHeight) + "px"; + this.style.height = popupHeightPx; + + let onTransitionEnd = () => { + this.removeEventListener("transitionend", onTransitionEnd, true); + this.searchSuggestionsNotification.style.marginTop = "0px"; + this.style.removeProperty("height"); + this._hideSearchSuggestionsNotification(); + resolve(); + }; + this.addEventListener("transitionend", onTransitionEnd, true); + }); + ]]> + </body> + </method> + + <method name="_selectedOneOffChanged"> + <body><![CDATA[ + // Update all searchengine result items to use the newly selected + // engine. + for (let item of this.richlistbox.childNodes) { + if (item.collapsed) { + break; + } + let url = item.getAttribute("url"); + if (url) { + let action = item._parseActionUrl(url); + if (action && action.type == "searchengine") { + item._adjustAcItem(); + } + } + } + ]]></body> + </method> + + <!-- This handles keypress changes to the selection among the one-off + search buttons and between the one-offs and the listbox. It returns + true if the keypress was consumed and false if not. --> + <method name="handleKeyPress"> + <parameter name="aEvent"/> + <body><![CDATA[ + this.oneOffSearchButtons.handleKeyPress(aEvent, this._matchCount, + !this._isFirstResultHeuristic, + gBrowser.userTypedValue); + return aEvent.defaultPrevented; + ]]></body> + </method> + + <!-- This is called when a one-off is clicked and when "search in new tab" + is selected from a one-off context menu. --> + <method name="handleOneOffSearch"> + <parameter name="event"/> + <parameter name="engine"/> + <parameter name="where"/> + <parameter name="params"/> + <body><![CDATA[ + this.input.handleCommand(event, where, params); + ]]></body> + </method> + + <!-- Result listitems call this to determine which search engine they + should show in their labels and include in their url attributes. --> + <property name="overrideSearchEngineName" readonly="true"> + <getter><![CDATA[ + let button = this.oneOffSearchButtons.selectedButton; + return button && button.engine && button.engine.name; + ]]></getter> + </property> + + <method name="createResultLabel"> + <parameter name="item"/> + <parameter name="proposedLabel"/> + <body> + <![CDATA[ + let parts = [proposedLabel]; + + let action = this.mInput._parseActionUrl(item.getAttribute("url")); + if (action) { + switch (action.type) { + case "searchengine": + parts = [ + action.params.searchSuggestion || action.params.searchQuery, + action.params.engineName, + ]; + break; + case "switchtab": + case "remotetab": + parts = [ + item.getAttribute("title"), + item.getAttribute("displayurl"), + ]; + break; + } + } + + let types = item.getAttribute("type").split(/\s+/); + let type = types.find(type => type != "action" && type != "heuristic"); + try { + // Some types intentionally do not map to strings, which is not + // an error. + parts.push(this._bundle.GetStringFromName(type + "ResultLabel")); + } catch (e) {} + + return parts.filter(str => str).join(" "); + ]]> + </body> + </method> + + <method name="onResultsAdded"> + <body> + <![CDATA[ + // If nothing is selected yet, select the first result if it is a + // pre-selected "heuristic" result. (See UnifiedComplete.js.) + if (this.selectedIndex == -1 && this._isFirstResultHeuristic) { + // Don't fire DOMMenuItemActive so that screen readers still see + // the input as being focused. + this.richlistbox.suppressMenuItemEvent = true; + this.input.controller.setInitiallySelectedIndex(0); + this.richlistbox.suppressMenuItemEvent = false; + } + + this.input.gotResultForCurrentQuery = true; + + // Check if we should perform a delayed handleEnter. + if (this.input.handleEnterInstance) { + let instance = this.input.handleEnterInstance; + this.input.handleEnterInstance = null; + // Don't handle this immediately or we could cause a recursive + // loop where the controller sets popupOpen and re-enters here. + setTimeout(() => { + // Safety check: handle only if the search string didn't change. + let { event, searchString } = instance; + if (this.input.mController.searchString == searchString) { + this.input.maybeCanonizeURL(event, searchString); + this.input.mController.handleEnter(false, event); + this.overrideValue = null; + } + }, 0); + } + ]]> + </body> + </method> + + <method name="_onSearchBegin"> + <body><![CDATA[ + // Set the selected index to 0 (heuristic) until a result comes back + // and we can evaluate it better. + // + // This is required to properly manage delayed handleEnter: + // 1. if a search starts we set selectedIndex to 0 here, and it will + // be updated by onResultsAdded. Since selectedIndex is 0, + // handleEnter will delay the action if a result didn't arrive yet. + // 2. if a search doesn't start (for example if autocomplete is + // disabled), this won't be called, and the selectedIndex will be + // the default -1 value. Then handleEnter will know it should not + // delay the action, cause a result wont't ever arrive. + this.input.controller.setInitiallySelectedIndex(0); + ]]></body> + </method> + + </implementation> + <handlers> + + <handler event="SelectedOneOffButtonChanged"><![CDATA[ + this._selectedOneOffChanged(); + ]]></handler> + + <handler event="mousedown"><![CDATA[ + // Required to make the xul:label.text-link elements in the search + // suggestions notification work correctly when clicked on Linux. + // This is copied from the mousedown handler in + // browser-search-autocomplete-result-popup, which apparently had a + // similar problem. + event.preventDefault(); + ]]></handler> + + </handlers> + </binding> + + <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification"> + <implementation> + <constructor><![CDATA[ + if (!this.notification) + return; + + this.notification.options.installs.forEach(function(aInstall) { + aInstall.addListener(this); + }, this); + + // Calling updateProgress can sometimes cause this notification to be + // removed in the middle of refreshing the notification panel which + // makes the panel get refreshed again. Just initialise to the + // undetermined state and then schedule a proper check at the next + // opportunity + this.setProgress(0, -1); + this._updateProgressTimeout = setTimeout(this.updateProgress.bind(this), 0); + ]]></constructor> + + <destructor><![CDATA[ + this.destroy(); + ]]></destructor> + + <field name="progressmeter" readonly="true"> + document.getElementById("addon-progress-notification-progressmeter"); + </field> + <field name="progresstext" readonly="true"> + document.getElementById("addon-progress-notification-progresstext"); + </field> + <property name="DownloadUtils" readonly="true"> + <getter><![CDATA[ + let module = {}; + Components.utils.import("resource://gre/modules/DownloadUtils.jsm", module); + Object.defineProperty(this, "DownloadUtils", { + configurable: true, + enumerable: true, + writable: true, + value: module.DownloadUtils + }); + return module.DownloadUtils; + ]]></getter> + </property> + + <method name="destroy"> + <body><![CDATA[ + if (!this.notification) + return; + + this.notification.options.installs.forEach(function(aInstall) { + aInstall.removeListener(this); + }, this); + clearTimeout(this._updateProgressTimeout); + ]]></body> + </method> + + <method name="setProgress"> + <parameter name="aProgress"/> + <parameter name="aMaxProgress"/> + <body><![CDATA[ + if (aMaxProgress == -1) { + this.progressmeter.setAttribute("mode", "undetermined"); + } + else { + this.progressmeter.setAttribute("mode", "determined"); + this.progressmeter.setAttribute("value", (aProgress * 100) / aMaxProgress); + } + + let now = Date.now(); + + if (!this.notification.lastUpdate) { + this.notification.lastUpdate = now; + this.notification.lastProgress = aProgress; + return; + } + + let delta = now - this.notification.lastUpdate; + if ((delta < 400) && (aProgress < aMaxProgress)) + return; + + delta /= 1000; + + // This code is taken from nsDownloadManager.cpp + let speed = (aProgress - this.notification.lastProgress) / delta; + if (this.notification.speed) + speed = speed * 0.9 + this.notification.speed * 0.1; + + this.notification.lastUpdate = now; + this.notification.lastProgress = aProgress; + this.notification.speed = speed; + + let status = null; + [status, this.notification.last] = this.DownloadUtils.getDownloadStatus(aProgress, aMaxProgress, speed, this.notification.last); + this.progresstext.setAttribute("value", status); + this.progresstext.setAttribute("tooltiptext", status); + ]]></body> + </method> + + <method name="cancel"> + <body><![CDATA[ + let installs = this.notification.options.installs; + installs.forEach(function(aInstall) { + try { + aInstall.cancel(); + } + catch (e) { + // Cancel will throw if the download has already failed + } + }, this); + + PopupNotifications.remove(this.notification); + ]]></body> + </method> + + <method name="updateProgress"> + <body><![CDATA[ + if (!this.notification) + return; + + let downloadingCount = 0; + let progress = 0; + let maxProgress = 0; + + this.notification.options.installs.forEach(function(aInstall) { + if (aInstall.maxProgress == -1) + maxProgress = -1; + progress += aInstall.progress; + if (maxProgress >= 0) + maxProgress += aInstall.maxProgress; + if (aInstall.state < AddonManager.STATE_DOWNLOADED) + downloadingCount++; + }); + + if (downloadingCount == 0) { + this.destroy(); + if (Preferences.get("xpinstall.customConfirmationUI", false)) { + this.progressmeter.setAttribute("mode", "undetermined"); + let status = gNavigatorBundle.getString("addonDownloadVerifying"); + this.progresstext.setAttribute("value", status); + this.progresstext.setAttribute("tooltiptext", status); + } else { + PopupNotifications.remove(this.notification); + } + } + else { + this.setProgress(progress, maxProgress); + } + ]]></body> + </method> + + <method name="onDownloadProgress"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + + <method name="onDownloadFailed"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + + <method name="onDownloadCancelled"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + + <method name="onDownloadEnded"> + <body><![CDATA[ + this.updateProgress(); + ]]></body> + </method> + </implementation> + </binding> + + <binding id="plugin-popupnotification-center-item"> + <content align="center"> + <xul:vbox pack="center" anonid="itemBox" class="itemBox"> + <xul:description anonid="center-item-label" class="center-item-label" /> + <xul:hbox flex="1" pack="start" align="center" anonid="center-item-warning"> + <xul:image anonid="center-item-warning-icon" class="center-item-warning-icon"/> + <xul:label anonid="center-item-warning-label"/> + <xul:label anonid="center-item-link" value="&checkForUpdates;" class="text-link"/> + </xul:hbox> + </xul:vbox> + <xul:vbox pack="center"> + <xul:menulist class="center-item-menulist" + anonid="center-item-menulist"> + <xul:menupopup> + <xul:menuitem anonid="allownow" value="allownow" + label="&pluginActivateNow.label;" /> + <xul:menuitem anonid="allowalways" value="allowalways" + label="&pluginActivateAlways.label;" /> + <xul:menuitem anonid="block" value="block" + label="&pluginBlockNow.label;" /> + </xul:menupopup> + </xul:menulist> + </xul:vbox> + </content> + <resources> + <stylesheet src="chrome://global/skin/notification.css"/> + </resources> + <implementation> + <constructor><![CDATA[ + document.getAnonymousElementByAttribute(this, "anonid", "center-item-label").value = this.action.pluginName; + + let curState = "block"; + if (this.action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) { + if (this.action.pluginPermissionType == Ci.nsIPermissionManager.EXPIRE_SESSION) { + curState = "allownow"; + } + else { + curState = "allowalways"; + } + } + document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").value = curState; + + let warningString = ""; + let linkString = ""; + + let link = document.getAnonymousElementByAttribute(this, "anonid", "center-item-link"); + + let url; + let linkHandler; + + if (this.action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) { + document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true; + warningString = gNavigatorBundle.getString("pluginActivateDisabled.label"); + linkString = gNavigatorBundle.getString("pluginActivateDisabled.manage"); + linkHandler = function(event) { + event.preventDefault(); + gPluginHandler.managePlugins(); + }; + document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-icon").hidden = true; + } + else { + url = this.action.detailsLink; + + switch (this.action.blocklistState) { + case Ci.nsIBlocklistService.STATE_NOT_BLOCKED: + document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning").hidden = true; + break; + case Ci.nsIBlocklistService.STATE_BLOCKED: + document.getAnonymousElementByAttribute(this, "anonid", "center-item-menulist").hidden = true; + warningString = gNavigatorBundle.getString("pluginActivateBlocked.label"); + linkString = gNavigatorBundle.getString("pluginActivate.learnMore"); + break; + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + warningString = gNavigatorBundle.getString("pluginActivateOutdated.label"); + linkString = gNavigatorBundle.getString("pluginActivate.updateLabel"); + break; + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + warningString = gNavigatorBundle.getString("pluginActivateVulnerable.label"); + linkString = gNavigatorBundle.getString("pluginActivate.riskLabel"); + break; + } + } + document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-label").value = warningString; + + let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow); + let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin); + + if (isWindowPrivate) { + // TODO: temporary compromise of hiding some privacy leaks, remove once bug 892487 is fixed + let allowalways = document.getAnonymousElementByAttribute(this, "anonid", "allowalways"); + let block = document.getAnonymousElementByAttribute(this, "anonid", "block"); + let allownow = document.getAnonymousElementByAttribute(this, "anonid", "allownow"); + + allowalways.hidden = curState !== "allowalways"; + block.hidden = curState !== "block"; + allownow.hidden = curState === "allowalways"; + } + + if (url || linkHandler) { + link.value = linkString; + if (url) { + link.href = url; + } + if (linkHandler) { + link.addEventListener("click", linkHandler, false); + } + } + else { + link.hidden = true; + } + ]]></constructor> + <property name="value"> + <getter> + return document.getAnonymousElementByAttribute(this, "anonid", + "center-item-menulist").value; + </getter> + <setter><!-- This should be used only in automated tests --> + document.getAnonymousElementByAttribute(this, "anonid", + "center-item-menulist").value = val; + </setter> + </property> + </implementation> + </binding> + + <binding id="click-to-play-plugins-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification"> + <content align="start" style="width: &pluginNotification.width;;"> + <xul:vbox flex="1" align="stretch" class="popup-notification-main-box" + xbl:inherits="popupid"> + <xul:hbox class="click-to-play-plugins-notification-description-box" flex="1" align="start"> + <xul:description class="click-to-play-plugins-outer-description" flex="1"> + <html:span anonid="click-to-play-plugins-notification-description" /> + <xul:label class="text-link click-to-play-plugins-notification-link" anonid="click-to-play-plugins-notification-link" /> + </xul:description> + <xul:toolbarbutton anonid="closebutton" + class="messageCloseButton popup-notification-closebutton tabbable close-icon" + xbl:inherits="oncommand=closebuttoncommand" + tooltiptext="&closeNotification.tooltip;"/> + </xul:hbox> + <xul:grid anonid="click-to-play-plugins-notification-center-box" + class="click-to-play-plugins-notification-center-box"> + <xul:columns> + <xul:column flex="1"/> + <xul:column/> + </xul:columns> + <xul:rows> + <children includes="row"/> + <xul:hbox pack="start" anonid="plugin-notification-showbox"> + <xul:button label="&pluginNotification.showAll.label;" + accesskey="&pluginNotification.showAll.accesskey;" + class="plugin-notification-showbutton" + oncommand="document.getBindingParent(this)._setState(2)"/> + </xul:hbox> + </xul:rows> + </xul:grid> + <xul:hbox anonid="button-container" + class="click-to-play-plugins-notification-button-container" + pack="center" align="center"> + <xul:button anonid="primarybutton" + class="click-to-play-popup-button" + oncommand="document.getBindingParent(this)._onButton(this)" + flex="1"/> + <xul:button anonid="secondarybutton" + class="click-to-play-popup-button" + oncommand="document.getBindingParent(this)._onButton(this);" + flex="1"/> + </xul:hbox> + <xul:box hidden="true"> + <children/> + </xul:box> + </xul:vbox> + </content> + <resources> + <stylesheet src="chrome://global/skin/notification.css"/> + </resources> + <implementation> + <field name="_states"> + ({SINGLE: 0, MULTI_COLLAPSED: 1, MULTI_EXPANDED: 2}) + </field> + <field name="_primaryButton"> + document.getAnonymousElementByAttribute(this, "anonid", "primarybutton"); + </field> + <field name="_secondaryButton"> + document.getAnonymousElementByAttribute(this, "anonid", "secondarybutton") + </field> + <field name="_buttonContainer"> + document.getAnonymousElementByAttribute(this, "anonid", "button-container") + </field> + <field name="_brandShortName"> + document.getElementById("bundle_brand").getString("brandShortName") + </field> + <field name="_items">[]</field> + <constructor><![CDATA[ + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let sortedActions = []; + for (let action of this.notification.options.pluginData.values()) { + sortedActions.push(action); + } + sortedActions.sort((a, b) => a.pluginName.localeCompare(b.pluginName)); + + for (let action of sortedActions) { + let item = document.createElementNS(XUL_NS, "row"); + item.setAttribute("class", "plugin-popupnotification-centeritem"); + item.action = action; + this.appendChild(item); + this._items.push(item); + } + switch (this._items.length) { + case 0: + PopupNotifications._dismiss(); + break; + case 1: + this._setState(this._states.SINGLE); + break; + default: + if (this.notification.options.primaryPlugin) { + this._setState(this._states.MULTI_COLLAPSED); + } else { + this._setState(this._states.MULTI_EXPANDED); + } + } + ]]></constructor> + <method name="_setState"> + <parameter name="state" /> + <body><![CDATA[ + var grid = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-center-box"); + + if (this._states.SINGLE == state) { + grid.hidden = true; + this._setupSingleState(); + return; + } + + let prePath = this.notification.options.principal.URI.prePath; + this._setupDescription("pluginActivateMultiple.message", null, prePath); + + var showBox = document.getAnonymousElementByAttribute(this, "anonid", "plugin-notification-showbox"); + + var dialogStrings = Services.strings.createBundle("chrome://global/locale/dialog.properties"); + this._primaryButton.label = dialogStrings.GetStringFromName("button-accept"); + this._primaryButton.setAttribute("default", "true"); + + this._secondaryButton.label = dialogStrings.GetStringFromName("button-cancel"); + this._primaryButton.setAttribute("action", "_multiAccept"); + this._secondaryButton.setAttribute("action", "_cancel"); + + grid.hidden = false; + + if (this._states.MULTI_COLLAPSED == state) { + for (let child of this.childNodes) { + if (child.tagName != "row") { + continue; + } + child.hidden = this.notification.options.primaryPlugin != + child.action.permissionString; + } + showBox.hidden = false; + } + else { + for (let child of this.childNodes) { + if (child.tagName != "row") { + continue; + } + child.hidden = false; + } + showBox.hidden = true; + } + this._setupLink(null); + ]]></body> + </method> + <method name="_setupSingleState"> + <body><![CDATA[ + var action = this._items[0].action; + var prePath = action.pluginPermissionPrePath; + let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow); + let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin); + + let label, linkLabel, button1, button2; + + if (action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) { + button1 = { + label: "pluginBlockNow.label", + accesskey: "pluginBlockNow.accesskey", + action: "_singleBlock" + }; + button2 = { + label: "pluginContinue.label", + accesskey: "pluginContinue.accesskey", + action: "_singleContinue", + default: true + }; + switch (action.blocklistState) { + case Ci.nsIBlocklistService.STATE_NOT_BLOCKED: + label = "pluginEnabled.message"; + linkLabel = "pluginActivate.learnMore"; + break; + + case Ci.nsIBlocklistService.STATE_BLOCKED: + Cu.reportError(Error("Cannot happen!")); + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + label = "pluginEnabledOutdated.message"; + linkLabel = "pluginActivate.updateLabel"; + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + label = "pluginEnabledVulnerable.message"; + linkLabel = "pluginActivate.riskLabel" + break; + + default: + Cu.reportError(Error("Unexpected blocklist state")); + } + + // TODO: temporary compromise, remove this once bug 892487 is fixed + if (isWindowPrivate) { + this._buttonContainer.hidden = true; + } + } + else if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) { + let linkElement = + document.getAnonymousElementByAttribute( + this, "anonid", "click-to-play-plugins-notification-link"); + linkElement.textContent = gNavigatorBundle.getString("pluginActivateDisabled.manage"); + linkElement.setAttribute("onclick", "gPluginHandler.managePlugins()"); + + let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + descElement.textContent = gNavigatorBundle.getFormattedString( + "pluginActivateDisabled.message", [action.pluginName, this._brandShortName]) + " "; + this._buttonContainer.hidden = true; + return; + } + else if (action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { + let descElement = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + descElement.textContent = gNavigatorBundle.getFormattedString( + "pluginActivateBlocked.message", [action.pluginName, this._brandShortName]) + " "; + this._setupLink("pluginActivate.learnMore", action.detailsLink); + this._buttonContainer.hidden = true; + return; + } + else { + button1 = { + label: "pluginActivateNow.label", + accesskey: "pluginActivateNow.accesskey", + action: "_singleActivateNow" + }; + button2 = { + label: "pluginActivateAlways.label", + accesskey: "pluginActivateAlways.accesskey", + action: "_singleActivateAlways" + }; + switch (action.blocklistState) { + case Ci.nsIBlocklistService.STATE_NOT_BLOCKED: + label = "pluginActivateNew.message"; + linkLabel = "pluginActivate.learnMore"; + button2.default = true; + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE: + label = "pluginActivateOutdated.message"; + linkLabel = "pluginActivate.updateLabel"; + button1.default = true; + break; + + case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE: + label = "pluginActivateVulnerable.message"; + linkLabel = "pluginActivate.riskLabel" + button1.default = true; + break; + + default: + Cu.reportError(Error("Unexpected blocklist state")); + } + + // TODO: temporary compromise, remove this once bug 892487 is fixed + if (isWindowPrivate) { + button1.default = true; + this._secondaryButton.hidden = true; + } + } + this._setupDescription(label, action.pluginName, prePath); + this._setupLink(linkLabel, action.detailsLink); + + this._primaryButton.label = gNavigatorBundle.getString(button1.label); + this._primaryButton.accessKey = gNavigatorBundle.getString(button1.accesskey); + this._primaryButton.setAttribute("action", button1.action); + + this._secondaryButton.label = gNavigatorBundle.getString(button2.label); + this._secondaryButton.accessKey = gNavigatorBundle.getString(button2.accesskey); + this._secondaryButton.setAttribute("action", button2.action); + if (button1.default) { + this._primaryButton.setAttribute("default", "true"); + } + else if (button2.default) { + this._secondaryButton.setAttribute("default", "true"); + } + ]]></body> + </method> + <method name="_setupDescription"> + <parameter name="baseString" /> + <parameter name="pluginName" /> <!-- null for the multiple-plugin case --> + <parameter name="prePath" /> + <body><![CDATA[ + var span = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + while (span.lastChild) { + span.removeChild(span.lastChild); + } + + var args = ["__prepath__", this._brandShortName]; + if (pluginName) { + args.unshift(pluginName); + } + var bases = gNavigatorBundle.getFormattedString(baseString, args). + split("__prepath__", 2); + + span.appendChild(document.createTextNode(bases[0])); + var prePathSpan = document.createElementNS("http://www.w3.org/1999/xhtml", "em"); + prePathSpan.appendChild(document.createTextNode(prePath)); + span.appendChild(prePathSpan); + span.appendChild(document.createTextNode(bases[1] + " ")); + ]]></body> + </method> + <method name="_setupLink"> + <parameter name="linkString"/> + <parameter name="linkUrl" /> + <body><![CDATA[ + var link = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-link"); + if (!linkString || !linkUrl) { + link.hidden = true; + return; + } + + link.hidden = false; + link.textContent = gNavigatorBundle.getString(linkString); + link.href = linkUrl; + ]]></body> + </method> + <method name="_onButton"> + <parameter name="aButton" /> + <body><![CDATA[ + let methodName = aButton.getAttribute("action"); + this[methodName](); + ]]></body> + </method> + <method name="_singleActivateNow"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this._items[0].action, + "allownow"); + this._cancel(); + ]]></body> + </method> + <method name="_singleBlock"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this._items[0].action, + "block"); + this._cancel(); + ]]></body> + </method> + <method name="_singleActivateAlways"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this._items[0].action, + "allowalways"); + this._cancel(); + ]]></body> + </method> + <method name="_singleContinue"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this._items[0].action, + "continue"); + this._cancel(); + ]]></body> + </method> + <method name="_multiAccept"> + <body><![CDATA[ + for (let item of this._items) { + let action = item.action; + if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED || + action.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) { + continue; + } + gPluginHandler._updatePluginPermission(this.notification, + item.action, item.value); + } + this._cancel(); + ]]></body> + </method> + <method name="_cancel"> + <body><![CDATA[ + PopupNotifications._dismiss(); + ]]></body> + </method> + <method name="_accept"> + <parameter name="aEvent" /> + <body><![CDATA[ + if (aEvent.defaultPrevented) + return; + aEvent.preventDefault(); + if (this._primaryButton.getAttribute("default") == "true") { + this._primaryButton.click(); + } + else if (this._secondaryButton.getAttribute("default") == "true") { + this._secondaryButton.click(); + } + ]]></body> + </method> + </implementation> + <handlers> + <!-- The _accept method checks for .defaultPrevented so that if focus is in a button, + enter activates the button and not this default action --> + <handler event="keypress" keycode="VK_RETURN" group="system" action="this._accept(event);"/> + </handlers> + </binding> + + <binding id="splitmenu"> + <content> + <xul:hbox anonid="menuitem" flex="1" + class="splitmenu-menuitem" + xbl:inherits="iconic,label,disabled,onclick=oncommand,_moz-menuactive=active"/> + <xul:menu anonid="menu" class="splitmenu-menu" + xbl:inherits="disabled,_moz-menuactive=active" + oncommand="event.stopPropagation();"> + <children includes="menupopup"/> + </xul:menu> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor><![CDATA[ + this._parentMenupopup.addEventListener("DOMMenuItemActive", this, false); + this._parentMenupopup.addEventListener("popuphidden", this, false); + ]]></constructor> + + <destructor><![CDATA[ + this._parentMenupopup.removeEventListener("DOMMenuItemActive", this, false); + this._parentMenupopup.removeEventListener("popuphidden", this, false); + ]]></destructor> + + <field name="menuitem" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "menuitem"); + </field> + <field name="menu" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "menu"); + </field> + + <field name="_menuDelay">600</field> + + <field name="_parentMenupopup"><![CDATA[ + this._getParentMenupopup(this); + ]]></field> + + <method name="_getParentMenupopup"> + <parameter name="aNode"/> + <body><![CDATA[ + let node = aNode.parentNode; + while (node) { + if (node.localName == "menupopup") + break; + node = node.parentNode; + } + return node; + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="event"/> + <body><![CDATA[ + switch (event.type) { + case "DOMMenuItemActive": + if (this.getAttribute("active") == "true" && + event.target != this && + this._getParentMenupopup(event.target) == this._parentMenupopup) + this.removeAttribute("active"); + break; + case "popuphidden": + if (event.target == this._parentMenupopup) + this.removeAttribute("active"); + break; + } + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="mouseover"><![CDATA[ + if (this.getAttribute("active") != "true") { + this.setAttribute("active", "true"); + + let event = document.createEvent("Events"); + event.initEvent("DOMMenuItemActive", true, false); + this.dispatchEvent(event); + + if (this.getAttribute("disabled") != "true") { + let self = this; + setTimeout(function () { + if (self.getAttribute("active") == "true") + self.menu.open = true; + }, this._menuDelay); + } + } + ]]></handler> + + <handler event="popupshowing"><![CDATA[ + if (event.target == this.firstChild && + this._parentMenupopup._currentPopup) + this._parentMenupopup._currentPopup.hidePopup(); + ]]></handler> + + <handler event="click" phase="capturing"><![CDATA[ + if (this.getAttribute("disabled") == "true") { + // Prevent the command from being carried out + event.stopPropagation(); + return; + } + + let node = event.originalTarget; + while (true) { + if (node == this.menuitem) + break; + if (node == this) + return; + node = node.parentNode; + } + + this._parentMenupopup.hidePopup(); + ]]></handler> + </handlers> + </binding> + + <binding id="menuitem-tooltip" extends="chrome://global/content/bindings/menu.xml#menuitem"> + <implementation> + <constructor><![CDATA[ + this.setAttribute("tooltiptext", this.getAttribute("acceltext")); + // TODO: Simplify this to this.setAttribute("acceltext", "") once bug + // 592424 is fixed + document.getAnonymousElementByAttribute(this, "anonid", "accel").firstChild.setAttribute("value", ""); + ]]></constructor> + </implementation> + </binding> + + <binding id="menuitem-iconic-tooltip" extends="chrome://global/content/bindings/menu.xml#menuitem-iconic"> + <implementation> + <constructor><![CDATA[ + this.setAttribute("tooltiptext", this.getAttribute("acceltext")); + // TODO: Simplify this to this.setAttribute("acceltext", "") once bug + // 592424 is fixed + document.getAnonymousElementByAttribute(this, "anonid", "accel").firstChild.setAttribute("value", ""); + ]]></constructor> + </implementation> + </binding> +</bindings> diff --git a/application/basilisk/base/content/usercontext.svg b/application/basilisk/base/content/usercontext.svg new file mode 100644 index 000000000..705f80bfd --- /dev/null +++ b/application/basilisk/base/content/usercontext.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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/. --> +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + width="32" height="32" viewBox="0 0 32 32"> + <style> + path, circle { + fill: menutext; + } + path:not(:target), + circle:not(:target) { + display: none; + } + </style> + <path id="dollar" d="M17.3857868,14.0527919 C14.2304569,13.0862944 13.4913706,12.4609137 13.4913706,11.0964467 C13.4913706,9.61827411 14.7137056,8.85076142 16.4192893,8.85076142 C17.9827411,8.85076142 19.3187817,9.33401015 20.5979695,10.4994924 L22.4456853,8.42436548 C21.1664975,7.20203046 19.3187819,6.26535905 17,6.00952148 L17,2 L15,2 L15,6.00952148 C12.3827412,6.43591742 9.76751269,8.53807107 9.76751269,11.3238579 C9.76751269,14.1664975 11.4730964,15.786802 15.4812183,17.0091371 C18.4375635,17.9187817 19.2335025,18.6294416 19.2335025,20.2213198 C19.2335025,22.0690355 17.7553299,23.035533 15.7370558,23.035533 C13.7756345,23.035533 12.2406091,22.3248731 10.9329949,21.1025381 L9,23.2345178 C10.4213198,24.6274112 12.8659899,25.8324934 15,26.0030518 L15,30 L17,30 L17,26.0030518 C20.7116753,25.4060974 22.9857868,22.893401 22.9857868,20.022335 C22.9857868,16.4690355 20.7116751,15.1045685 17.3857868,14.0527919 Z"/> + <path id="briefcase" fill-rule="evenodd" d="M22,9.99887085 L21.635468,10 L29.0034652,10 C29.5538362,10 30,10.4449463 30,10.9933977 L30,27.0066023 C30,27.5552407 29.5601869,28 29.0034652,28 L2.99653482,28 C2.44616384,28 2,27.5550537 2,27.0066023 L2,10.9933977 C2,10.4447593 2.43981314,10 2.99653482,10 L8,10 L8,7.99922997 C8,5.79051625 10.0426627,4 12.5635454,4 L19.4364546,4 C21.9568311,4 24,5.79246765 24,7.99922997 L24,9.99267578 L22,9.99887085 L22,10 L10,10 L10,7.99922997 C10,6.89421235 11.0713286,6 12.3917227,6 L19.6082773,6 C20.9273761,6 22,6.89552665 22,7.99922997 L22,9.99887085 Z"/> + <path id="fingerprint" d="M7.17741905,12 C7.10965537,12 7.041327,11.9953181 6.97243393,11.985018 C6.33263187,11.8918489 5.90515601,11.3862071 6.01809547,10.8552833 C7.41798011,4.26321358 12.2613889,2.57493207 15.0238882,2.15590491 C19.6448063,1.45690206 24.3408291,3.21541158 25.8344535,5.29743816 C26.1664955,5.76047488 25.9835336,6.35881757 25.4244832,6.63364321 C24.8654329,6.9098734 24.1437497,6.75583996 23.8122724,6.29327142 C22.8923805,5.01043967 19.1749781,3.51130562 15.4479759,4.07406612 C12.8080159,4.474834 9.43056132,6.03623689 8.33561323,11.1942506 C8.23453242,11.666651 7.73816348,12 7.17741905,12 Z M16.63127,26 C16.1452186,26 15.6509104,25.9658335 15.147795,25.8938767 C10.637921,25.257137 6.71207921,21.8114952 6.01575422,17.8807924 C5.91171832,17.2932317 6.33391695,16.7382846 6.95813239,16.6404441 C7.58454965,16.5343208 8.17298555,16.9406954 8.27757192,17.5272206 C8.80876054,20.5255916 11.9766264,23.26409 15.4885263,23.7610576 C17.3975027,24.02766 20.959494,23.8221432 23.3220449,19.3789425 C24.4625867,17.2331815 23.0049831,11.881462 19.9521622,9.34692739 C18.2380468,7.92384005 16.4573263,7.76905536 14.6628445,8.89499751 C13.26469,9.77142052 11.8070864,12.2857658 11.8665355,14.6287608 C11.9127737,16.4835887 12.8386382,17.9325598 14.6171568,18.9363308 C15.2210054,19.2764429 16.9411759,19.4933486 17.9424527,18.8296898 C18.7257495,18.3104622 18.9591422,17.2761485 18.6365758,15.7583267 C18.3822659,14.5650869 17.2219077,12.4452096 16.6664991,12.3711821 C16.6692513,12.3722175 16.4666841,12.4312324 16.1276041,12.9095636 C15.8545786,13.2936782 15.58981,14.7297074 15.9476054,15.3581643 C16.0142104,15.4761941 16.0725586,15.5465978 16.3202632,15.5465978 C16.9532859,15.5465978 17.46686,16.0290705 17.46686,16.6249139 C17.46686,17.2207573 16.9543868,17.7042653 16.3213641,17.7042653 C15.2644914,17.7042653 14.4140391,17.2336992 13.9268868,16.3774655 C13.1083609,14.9388479 13.5536787,12.6548678 14.2202791,11.7137354 C15.2540327,10.2564816 16.3631986,10.1151564 17.1123672,10.2564816 C19.7066595,10.7389543 20.8763754,15.2908666 20.8857331,15.3359043 C21.5303153,18.3648181 20.3594985,19.8665919 19.264094,20.593407 C17.4151172,21.8192603 14.6920186,21.493643 13.4380832,20.7859819 C10.3280151,19.0310652 9.62013053,16.497566 9.5744428,14.6805283 C9.49022326,11.3643051 11.4779146,8.30018945 13.391845,7.10021984 C16.0417332,5.43848454 18.9877658,5.66781436 21.4714167,7.72919442 C25.1176276,10.7565552 27.0871539,17.1229168 25.3746898,20.3433702 C23.4326862,23.9950465 20.2983981,26 16.63127,26 Z M16.0845157,30 C14.9348455,30 13.9050564,29.8557557 13.0394288,29.6610017 C10.2114238,29.0257442 7.58700058,27.4599412 6.18892823,25.5735955 C5.84440518,25.1078371 5.98426642,24.4803503 6.50105099,24.1700066 C7.01675554,23.8596629 7.71552172,23.986423 8.06112477,24.4507244 C9.89498097,26.9252176 15.9397944,29.9781448 22.2508301,26.1937972 C22.7676147,25.8844249 23.4658409,26.0087566 23.8109039,26.474515 C24.155427,26.9397877 24.0161057,27.5672745 23.4993212,27.8776182 C20.7987573,29.4963593 18.2315746,30 16.0845157,30 Z"/> + <path id="cart" fill-rule="evenodd" d="M20.8195396,14 L15.1804604,14 L15.1804604,14 L15.8471271,18 L20.1528729,18 L20.8195396,14 Z M22.8471271,14 L27.6125741,14 L27.6125741,14 L26.2792408,18 L22.1804604,18 L22.8471271,14 Z M21.1528729,12 L14.8471271,12 L14.8471271,12 L14.1804604,8 L21.8195396,8 L21.1528729,12 Z M23.1804604,12 L28.2792408,12 L28.2792408,12 L29.6125741,8 L23.8471271,8 L23.1804604,12 Z M13.1528729,14 L8.47703296,14 L10.077033,18 L10.077033,18 L13.8195396,18 L13.1528729,14 Z M12.8195396,12 L7.67703296,12 L6.07703296,8 L12.1528729,8 L12.8195396,12 L12.8195396,12 Z M31.7207592,8 L32,8 L32,6 L31,6 L5.27703296,6 L5.27703296,6 L4,2.8074176 L4,2 L3,2 L1,2 L0,2 L0,4 L1,4 L2.32296704,4 L9.78931928,22.6658806 L9.78931928,22.6658806 C8.71085924,23.3823847 8,24.6081773 8,26 C8,28.209139 9.790861,30 12,30 C14.209139,30 16,28.209139 16,26 C16,25.2714257 15.8052114,24.5883467 15.4648712,24 L22.5351288,24 C22.1947886,24.5883467 22,25.2714257 22,26 C22,28.209139 23.790861,30 26,30 C28.209139,30 30,28.209139 30,26 C30,23.790861 28.209139,22 26,22 L11.677033,22 L10.877033,20 L27,20 L28,20 L28,19.1622777 L31.7207592,8 L31.7207592,8 Z M26,28 C27.1045695,28 28,27.1045695 28,26 C28,24.8954305 27.1045695,24 26,24 C24.8954305,24 24,24.8954305 24,26 C24,27.1045695 24.8954305,28 26,28 Z M12,28 C13.1045695,28 14,27.1045695 14,26 C14,24.8954305 13.1045695,24 12,24 C10.8954305,24 10,24.8954305 10,26 C10,27.1045695 10.8954305,28 12,28 Z"/> + <circle id="circle" r="16" cx="16" cy="16" fill-rule="evenodd" /> +</svg> + diff --git a/application/basilisk/base/content/utilityOverlay.js b/application/basilisk/base/content/utilityOverlay.js new file mode 100644 index 000000000..38ca82f55 --- /dev/null +++ b/application/basilisk/base/content/utilityOverlay.js @@ -0,0 +1,949 @@ +/* -*- 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/. */ + +// Services = object with smart getters for common XPCOM services +Components.utils.import("resource://gre/modules/AppConstants.jsm"); +Components.utils.import("resource://gre/modules/ContextualIdentityService.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +Components.utils.import("resource:///modules/RecentWindow.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ShellService", + "resource:///modules/ShellService.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService", + "@mozilla.org/browser/aboutnewtab-service;1", + "nsIAboutNewTabService"); + +this.__defineGetter__("BROWSER_NEW_TAB_URL", () => { + if (PrivateBrowsingUtils.isWindowPrivate(window) && + !PrivateBrowsingUtils.permanentPrivateBrowsing && + !aboutNewTabService.overridden) { + return "about:privatebrowsing"; + } + return aboutNewTabService.newTabURL; +}); + +var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab"; + +var gBidiUI = false; + +/** + * Determines whether the given url is considered a special URL for new tabs. + */ +function isBlankPageURL(aURL) { + return aURL == "about:blank" || aURL == "about:newtab" || aURL == "about:logopage"; +} + +function getBrowserURL() +{ + return "chrome://browser/content/browser.xul"; +} + +function getTopWin(skipPopups) { + // If this is called in a browser window, use that window regardless of + // whether it's the frontmost window, since commands can be executed in + // background windows (bug 626148). + if (top.document.documentElement.getAttribute("windowtype") == "navigator:browser" && + (!skipPopups || top.toolbar.visible)) + return top; + + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + return RecentWindow.getMostRecentBrowserWindow({private: isPrivate, + allowPopups: !skipPopups}); +} + +function openTopWin(url) { + /* deprecated */ + openUILinkIn(url, "current"); +} + +function getBoolPref(prefname, def) +{ + try { + return Services.prefs.getBoolPref(prefname); + } + catch (er) { + return def; + } +} + +/* openUILink handles clicks on UI elements that cause URLs to load. + * + * As the third argument, you may pass an object with the same properties as + * accepted by openUILinkIn, plus "ignoreButton" and "ignoreAlt". + */ +function openUILink(url, event, aIgnoreButton, aIgnoreAlt, aAllowThirdPartyFixup, + aPostData, aReferrerURI) { + let params; + + if (aIgnoreButton && typeof aIgnoreButton == "object") { + params = aIgnoreButton; + + // don't forward "ignoreButton" and "ignoreAlt" to openUILinkIn + aIgnoreButton = params.ignoreButton; + aIgnoreAlt = params.ignoreAlt; + delete params.ignoreButton; + delete params.ignoreAlt; + } else { + params = { + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostData, + referrerURI: aReferrerURI, + referrerPolicy: Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT, + initiatingDoc: event ? event.target.ownerDocument : null, + }; + } + + let where = whereToOpenLink(event, aIgnoreButton, aIgnoreAlt); + openUILinkIn(url, where, params); +} + + +/* whereToOpenLink() looks at an event to decide where to open a link. + * + * The event may be a mouse event (click, double-click, middle-click) or keypress event (enter). + * + * On Windows, the modifiers are: + * Ctrl new tab, selected + * Shift new window + * Ctrl+Shift new tab, in background + * Alt save + * + * Middle-clicking is the same as Ctrl+clicking (it opens a new tab). + * + * Exceptions: + * - Alt is ignored for menu items selected using the keyboard so you don't accidentally save stuff. + * (Currently, the Alt isn't sent here at all for menu items, but that will change in bug 126189.) + * - Alt is hard to use in context menus, because pressing Alt closes the menu. + * - Alt can't be used on the bookmarks toolbar because Alt is used for "treat this as something draggable". + * - The button is ignored for the middle-click-paste-URL feature, since it's always a middle-click. + */ +function whereToOpenLink( e, ignoreButton, ignoreAlt ) +{ + // This method must treat a null event like a left click without modifier keys (i.e. + // e = { shiftKey:false, ctrlKey:false, metaKey:false, altKey:false, button:0 }) + // for compatibility purposes. + if (!e) + return "current"; + + var shift = e.shiftKey; + var ctrl = e.ctrlKey; + var meta = e.metaKey; + var alt = e.altKey && !ignoreAlt; + + // ignoreButton allows "middle-click paste" to use function without always opening in a new window. + var middle = !ignoreButton && e.button == 1; + var middleUsesTabs = getBoolPref("browser.tabs.opentabfor.middleclick", true); + + // Don't do anything special with right-mouse clicks. They're probably clicks on context menu items. + + var metaKey = AppConstants.platform == "macosx" ? meta : ctrl; + if (metaKey || (middle && middleUsesTabs)) + return shift ? "tabshifted" : "tab"; + + if (alt && getBoolPref("browser.altClickSave", false)) + return "save"; + + if (shift || (middle && !middleUsesTabs)) + return "window"; + + return "current"; +} + +/* openUILinkIn opens a URL in a place specified by the parameter |where|. + * + * |where| can be: + * "current" current tab (if there aren't any browser windows, then in a new window instead) + * "tab" new tab (if there aren't any browser windows, then in a new window instead) + * "tabshifted" same as "tab" but in background if default is to select new tabs, and vice versa + * "window" new window + * "save" save to disk (with no filename hint!) + * + * aAllowThirdPartyFixup controls whether third party services such as Google's + * I Feel Lucky are allowed to interpret this URL. This parameter may be + * undefined, which is treated as false. + * + * Instead of aAllowThirdPartyFixup, you may also pass an object with any of + * these properties: + * allowThirdPartyFixup (boolean) + * postData (nsIInputStream) + * referrerURI (nsIURI) + * relatedToCurrent (boolean) + * skipTabAnimation (boolean) + * allowPinnedTabHostChange (boolean) + * allowPopups (boolean) + * userContextId (unsigned int) + */ +function openUILinkIn(url, where, aAllowThirdPartyFixup, aPostData, aReferrerURI) { + var params; + + if (arguments.length == 3 && typeof arguments[2] == "object") { + params = aAllowThirdPartyFixup; + } else { + params = { + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostData, + referrerURI: aReferrerURI, + referrerPolicy: Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT, + }; + } + + params.fromChrome = true; + + openLinkIn(url, where, params); +} + +/* eslint-disable complexity */ +function openLinkIn(url, where, params) { + if (!where || !url) + return; + const Cc = Components.classes; + const Ci = Components.interfaces; + + var aFromChrome = params.fromChrome; + var aAllowThirdPartyFixup = params.allowThirdPartyFixup; + var aPostData = params.postData; + var aCharset = params.charset; + var aReferrerURI = params.referrerURI; + var aReferrerPolicy = ('referrerPolicy' in params ? + params.referrerPolicy : Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT); + var aRelatedToCurrent = params.relatedToCurrent; + var aAllowMixedContent = params.allowMixedContent; + var aForceAllowDataURI = params.forceAllowDataURI; + var aInBackground = params.inBackground; + var aDisallowInheritPrincipal = params.disallowInheritPrincipal; + var aInitiatingDoc = params.initiatingDoc; + var aIsPrivate = params.private; + var aSkipTabAnimation = params.skipTabAnimation; + var aAllowPinnedTabHostChange = !!params.allowPinnedTabHostChange; + var aNoReferrer = params.noReferrer; + var aAllowPopups = !!params.allowPopups; + var aUserContextId = params.userContextId; + var aIndicateErrorPageLoad = params.indicateErrorPageLoad; + var aPrincipal = params.originPrincipal; + var aTriggeringPrincipal = params.triggeringPrincipal; + var aForceAboutBlankViewerInCurrent = + params.forceAboutBlankViewerInCurrent; + + // Establish a window in which we're running this code. + var w = getTopWin(); + + if ((where == "tab" || where == "tabshifted") && + w && !w.toolbar.visible) { + w = getTopWin(true); + aRelatedToCurrent = false; + } + + // Can only do this after we're sure of what |w| will be the rest of this function. + // Note that if |w| is null we might have no current browser (we'll open a new window). + var aCurrentBrowser = params.currentBrowser || (w && w.gBrowser.selectedBrowser); + + if (where == "save") { + // TODO(1073187): propagate referrerPolicy. + + // ContentClick.jsm passes isContentWindowPrivate for saveURL instead of passing a CPOW initiatingDoc + if ("isContentWindowPrivate" in params) { + saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, null, params.isContentWindowPrivate); + } + else { + if (!aInitiatingDoc) { + Components.utils.reportError("openUILink/openLinkIn was called with " + + "where == 'save' but without initiatingDoc. See bug 814264."); + return; + } + saveURL(url, null, null, true, true, aNoReferrer ? null : aReferrerURI, aInitiatingDoc); + } + return; + } + + // Teach the principal about the right OA to use, e.g. in case when + // opening a link in a new private window, or in a new container tab. + // Please note we do not have to do that for SystemPrincipals and we + // can not do it for NullPrincipals since NullPrincipals are only + // identical if they actually are the same object (See Bug: 1346759) + function useOAForPrincipal(principal) { + if (principal && principal.isCodebasePrincipal) { + let attrs = { + userContextId: aUserContextId, + privateBrowsingId: aIsPrivate || (w && PrivateBrowsingUtils.isWindowPrivate(w)), + }; + return Services.scriptSecurityManager.createCodebasePrincipal(principal.URI, attrs); + } + return principal; + } + aPrincipal = useOAForPrincipal(aPrincipal); + aTriggeringPrincipal = useOAForPrincipal(aTriggeringPrincipal); + + if (!w || where == "window") { + // Strip referrer data when opening a new private window, to prevent + // regular browsing data from leaking into it. + if (aIsPrivate) { + aNoReferrer = true; + } + + // This propagates to window.arguments. + var sa = Cc["@mozilla.org/array;1"]. + createInstance(Ci.nsIMutableArray); + + var wuri = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + wuri.data = url; + + let charset = null; + if (aCharset) { + charset = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + charset.data = "charset=" + aCharset; + } + + var allowThirdPartyFixupSupports = Cc["@mozilla.org/supports-PRBool;1"]. + createInstance(Ci.nsISupportsPRBool); + allowThirdPartyFixupSupports.data = aAllowThirdPartyFixup; + + var referrerURISupports = null; + if (aReferrerURI && !aNoReferrer) { + referrerURISupports = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + referrerURISupports.data = aReferrerURI.spec; + } + + var referrerPolicySupports = Cc["@mozilla.org/supports-PRUint32;1"]. + createInstance(Ci.nsISupportsPRUint32); + referrerPolicySupports.data = aReferrerPolicy; + + var userContextIdSupports = Cc["@mozilla.org/supports-PRUint32;1"]. + createInstance(Ci.nsISupportsPRUint32); + userContextIdSupports.data = aUserContextId; + + sa.appendElement(wuri, /* weak =*/ false); + sa.appendElement(charset, /* weak =*/ false); + sa.appendElement(referrerURISupports, /* weak =*/ false); + sa.appendElement(aPostData, /* weak =*/ false); + sa.appendElement(allowThirdPartyFixupSupports, /* weak =*/ false); + sa.appendElement(referrerPolicySupports, /* weak =*/ false); + sa.appendElement(userContextIdSupports, /* weak =*/ false); + sa.appendElement(aPrincipal, /* weak =*/ false); + sa.appendElement(aTriggeringPrincipal, /* weak =*/ false); + + let features = "chrome,dialog=no,all"; + if (aIsPrivate) { + features += ",private"; + } + + Services.ww.openWindow(w || window, getBrowserURL(), null, features, sa); + return; + } + + let loadInBackground = where == "current" ? false : aInBackground; + if (loadInBackground == null) { + loadInBackground = aFromChrome ? + false : + getBoolPref("browser.tabs.loadInBackground"); + } + + let uriObj; + if (where == "current") { + try { + uriObj = Services.io.newURI(url, null, null); + } catch (e) {} + } + + // We avoid using |w| here because in the 'popup window' case, + // if we pass a currentBrowser param |w.gBrowser| might not be the + // tabbrowser that contains |aCurrentBrowser|. We really only care + // about the tab linked to |aCurrentBrowser|. + let tab = aCurrentBrowser.getTabBrowser().getTabForBrowser(aCurrentBrowser); + if (where == "current" && tab.pinned && + !aAllowPinnedTabHostChange) { + try { + // nsIURI.host can throw for non-nsStandardURL nsIURIs. + if (!uriObj || (!uriObj.schemeIs("javascript") && + aCurrentBrowser.currentURI.host != uriObj.host)) { + where = "tab"; + loadInBackground = false; + } + } catch (err) { + where = "tab"; + loadInBackground = false; + } + } + + // Raise the target window before loading the URI, since loading it may + // result in a new frontmost window (e.g. "javascript:window.open('');"). + w.focus(); + + let browserUsedForLoad = null; + switch (where) { + case "current": + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + + if (aAllowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + + // LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL isn't supported for javascript URIs, + // i.e. it causes them not to load at all. Callers should strip + // "javascript:" from pasted strings to protect users from malicious URIs + // (see stripUnsafeProtocolOnPaste). + if (aDisallowInheritPrincipal && !(uriObj && uriObj.schemeIs("javascript"))) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; + } + + if (aAllowPopups) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_POPUPS; + } + if (aIndicateErrorPageLoad) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ERROR_LOAD_CHANGES_RV; + } + if (aForceAllowDataURI) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI; + } + + let {URI_INHERITS_SECURITY_CONTEXT} = Ci.nsIProtocolHandler; + if (aForceAboutBlankViewerInCurrent && + (!uriObj || + (Services.io.getProtocolFlags(uriObj.scheme) & URI_INHERITS_SECURITY_CONTEXT))) { + // Unless we know for sure we're not inheriting principals, + // force the about:blank viewer to have the right principal: + aCurrentBrowser.createAboutBlankContentViewer(aPrincipal); + } + + aCurrentBrowser.loadURIWithFlags(url, { + triggeringPrincipal: aTriggeringPrincipal, + flags: flags, + referrerURI: aNoReferrer ? null : aReferrerURI, + referrerPolicy: aReferrerPolicy, + postData: aPostData, + userContextId: aUserContextId + }); + browserUsedForLoad = aCurrentBrowser; + break; + case "tabshifted": + loadInBackground = !loadInBackground; + // fall through + case "tab": + let tabUsedForLoad = w.gBrowser.loadOneTab(url, { + referrerURI: aReferrerURI, + referrerPolicy: aReferrerPolicy, + charset: aCharset, + postData: aPostData, + inBackground: loadInBackground, + allowThirdPartyFixup: aAllowThirdPartyFixup, + relatedToCurrent: aRelatedToCurrent, + skipAnimation: aSkipTabAnimation, + allowMixedContent: aAllowMixedContent, + noReferrer: aNoReferrer, + userContextId: aUserContextId, + originPrincipal: aPrincipal, + triggeringPrincipal: aTriggeringPrincipal, + }); + browserUsedForLoad = tabUsedForLoad.linkedBrowser; + break; + } + + // Focus the content, but only if the browser used for the load is selected. + if (browserUsedForLoad && + browserUsedForLoad == browserUsedForLoad.getTabBrowser().selectedBrowser) { + browserUsedForLoad.focus(); + } + + if (!loadInBackground && w.isBlankPageURL(url)) { + w.focusAndSelectUrlBar(); + } +} + +// Used as an onclick handler for UI elements with link-like behavior. +// e.g. onclick="checkForMiddleClick(this, event);" +function checkForMiddleClick(node, event) { + // We should be using the disabled property here instead of the attribute, + // but some elements that this function is used with don't support it (e.g. + // menuitem). + if (node.getAttribute("disabled") == "true") + return; // Do nothing + + if (event.button == 1) { + /* Execute the node's oncommand or command. + * + * XXX: we should use node.oncommand(event) once bug 246720 is fixed. + */ + var target = node.hasAttribute("oncommand") ? node : + node.ownerDocument.getElementById(node.getAttribute("command")); + var fn = new Function("event", target.getAttribute("oncommand")); + fn.call(target, event); + + // If the middle-click was on part of a menu, close the menu. + // (Menus close automatically with left-click but not with middle-click.) + closeMenus(event.target); + } +} + +// Populate a menu with user-context menu items. This method should be called +// by onpopupshowing passing the event as first argument. +function createUserContextMenu(event, isContextMenu = false, excludeUserContextId = 0) { + while (event.target.hasChildNodes()) { + event.target.removeChild(event.target.firstChild); + } + + let bundle = document.getElementById("bundle_browser"); + let docfrag = document.createDocumentFragment(); + + // If we are excluding a userContextId, we want to add a 'no-container' item. + if (excludeUserContextId) { + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("data-usercontextid", "0"); + menuitem.setAttribute("label", bundle.getString("userContextNone.label")); + menuitem.setAttribute("accesskey", bundle.getString("userContextNone.accesskey")); + + // We don't set an oncommand/command attribute because if we have + // to exclude a userContextId we are generating the contextMenu and + // isContextMenu will be true. + + docfrag.appendChild(menuitem); + + let menuseparator = document.createElement("menuseparator"); + docfrag.appendChild(menuseparator); + } + + ContextualIdentityService.getIdentities().forEach(identity => { + if (identity.userContextId == excludeUserContextId) { + return; + } + + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("data-usercontextid", identity.userContextId); + menuitem.setAttribute("label", ContextualIdentityService.getUserContextLabel(identity.userContextId)); + + if (identity.accessKey) { + menuitem.setAttribute("accesskey", bundle.getString(identity.accessKey)); + } + + menuitem.classList.add("menuitem-iconic"); + menuitem.setAttribute("data-identity-color", identity.color); + + if (!isContextMenu) { + menuitem.setAttribute("command", "Browser:NewUserContextTab"); + } + + menuitem.setAttribute("data-identity-icon", identity.icon); + + docfrag.appendChild(menuitem); + }); + + if (!isContextMenu) { + docfrag.appendChild(document.createElement("menuseparator")); + + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("label", + bundle.getString("userContext.aboutPage.label")); + menuitem.setAttribute("accesskey", + bundle.getString("userContext.aboutPage.accesskey")); + menuitem.setAttribute("command", "Browser:OpenAboutContainers"); + docfrag.appendChild(menuitem); + } + + event.target.appendChild(docfrag); + return true; +} + +// Closes all popups that are ancestors of the node. +function closeMenus(node) +{ + if ("tagName" in node) { + if (node.namespaceURI == "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + && (node.tagName == "menupopup" || node.tagName == "popup")) + node.hidePopup(); + + closeMenus(node.parentNode); + } +} + +/** This function takes in a key element and compares it to the keys pressed during an event. + * + * @param aEvent + * The KeyboardEvent event you want to compare against your key. + * + * @param aKey + * The <key> element checked to see if it was called in aEvent. + * For example, aKey can be a variable set to document.getElementById("key_close") + * to check if the close command key was pressed in aEvent. +*/ +function eventMatchesKey(aEvent, aKey) +{ + let keyPressed = aKey.getAttribute("key").toLowerCase(); + let keyModifiers = aKey.getAttribute("modifiers"); + let modifiers = ["Alt", "Control", "Meta", "Shift"]; + + if (aEvent.key != keyPressed) { + return false; + } + let eventModifiers = modifiers.filter(modifier => aEvent.getModifierState(modifier)); + // Check if aEvent has a modifier and aKey doesn't + if (eventModifiers.length > 0 && keyModifiers.length == 0) { + return false; + } + // Check whether aKey's modifiers match aEvent's modifiers + if (keyModifiers) { + keyModifiers = keyModifiers.split(/[\s,]+/); + // Capitalize first letter of aKey's modifers to compare to aEvent's modifier + keyModifiers.forEach(function(modifier, index) { + if (modifier == "accel") { + keyModifiers[index] = AppConstants.platform == "macosx" ? "Meta" : "Control"; + } else { + keyModifiers[index] = modifier[0].toUpperCase() + modifier.slice(1); + } + }); + return modifiers.every(modifier => keyModifiers.includes(modifier) == aEvent.getModifierState(modifier)); + } + return true; +} + +// Gather all descendent text under given document node. +function gatherTextUnder ( root ) +{ + var text = ""; + var node = root.firstChild; + var depth = 1; + while ( node && depth > 0 ) { + // See if this node is text. + if ( node.nodeType == Node.TEXT_NODE ) { + // Add this text to our collection. + text += " " + node.data; + } else if ( node instanceof HTMLImageElement) { + // If it has an "alt" attribute, add that. + var altText = node.getAttribute( "alt" ); + if ( altText && altText != "" ) { + text += " " + altText; + } + } + // Find next node to test. + // First, see if this node has children. + if ( node.hasChildNodes() ) { + // Go to first child. + node = node.firstChild; + depth++; + } else { + // No children, try next sibling (or parent next sibling). + while ( depth > 0 && !node.nextSibling ) { + node = node.parentNode; + depth--; + } + if ( node.nextSibling ) { + node = node.nextSibling; + } + } + } + // Strip leading and tailing whitespace. + text = text.trim(); + // Compress remaining whitespace. + text = text.replace( /\s+/g, " " ); + return text; +} + +// This function exists for legacy reasons. +function getShellService() +{ + return ShellService; +} + +function isBidiEnabled() { + // first check the pref. + if (getBoolPref("bidi.browser.ui", false)) + return true; + + // then check intl.uidirection.<locale> + var chromeReg = Components.classes["@mozilla.org/chrome/chrome-registry;1"]. + getService(Components.interfaces.nsIXULChromeRegistry); + if (chromeReg.isLocaleRTL("global")) + return true; + + // now see if the system locale is an RTL one. + var rv = false; + + try { + var localeService = Components.classes["@mozilla.org/intl/nslocaleservice;1"] + .getService(Components.interfaces.nsILocaleService); + var systemLocale = localeService.getSystemLocale().getCategory("NSILOCALE_CTYPE").substr(0, 3); + + switch (systemLocale) { + case "ar-": + case "he-": + case "fa-": + case "ug-": + case "ur-": + case "syr": + rv = true; + Services.prefs.setBoolPref("bidi.browser.ui", true); + } + } catch (e) {} + + return rv; +} + +function openAboutDialog() { + var enumerator = Services.wm.getEnumerator("Browser:About"); + while (enumerator.hasMoreElements()) { + // Only open one about window (Bug 599573) + let win = enumerator.getNext(); + if (win.closed) { + continue; + } + win.focus(); + return; + } + + var features = "chrome,"; + if (AppConstants.platform == "win") { + features += "centerscreen,dependent"; + } else if (AppConstants.platform == "macosx") { + features += "resizable=no,minimizable=no"; + } else { + features += "centerscreen,dependent,dialog=no"; + } + + window.openDialog("chrome://browser/content/aboutDialog.xul", "", features); +} + +function openPreferences(paneID, extraArgs) +{ + function switchToAdvancedSubPane(doc) { + if (extraArgs && extraArgs["advancedTab"]) { + let advancedPaneTabs = doc.getElementById("advancedPrefs"); + advancedPaneTabs.selectedTab = doc.getElementById(extraArgs["advancedTab"]); + } + } + + // This function is duplicated from preferences.js. + function internalPrefCategoryNameToFriendlyName(aName) { + return (aName || "").replace(/^pane./, function(toReplace) { return toReplace[4].toLowerCase(); }); + } + + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let friendlyCategoryName = internalPrefCategoryNameToFriendlyName(paneID); + let params; + if (extraArgs && extraArgs["urlParams"]) { + params = new URLSearchParams(); + let urlParams = extraArgs["urlParams"]; + for (let name in urlParams) { + if (urlParams[name] !== undefined) { + params.set(name, urlParams[name]); + } + } + } + let preferencesURL = "about:preferences" + (params ? "?" + params : "") + + (friendlyCategoryName ? "#" + friendlyCategoryName : ""); + let newLoad = true; + let browser = null; + if (!win) { + const Cc = Components.classes; + const Ci = Components.interfaces; + let windowArguments = Cc["@mozilla.org/array;1"] + .createInstance(Ci.nsIMutableArray); + let supportsStringPrefURL = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + supportsStringPrefURL.data = preferencesURL; + windowArguments.appendElement(supportsStringPrefURL, /* weak =*/ false); + + win = Services.ww.openWindow(null, Services.prefs.getCharPref("browser.chromeURL"), + "_blank", "chrome,dialog=no,all", windowArguments); + } else { + let shouldReplaceFragment = friendlyCategoryName ? "whenComparingAndReplace" : "whenComparing"; + newLoad = !win.switchToTabHavingURI(preferencesURL, true, { ignoreFragment: shouldReplaceFragment, replaceQueryString: true }); + browser = win.gBrowser.selectedBrowser; + } + + if (newLoad) { + Services.obs.addObserver(function advancedPaneLoadedObs(prefWin, topic, data) { + if (!browser) { + browser = win.gBrowser.selectedBrowser; + } + if (prefWin != browser.contentWindow) { + return; + } + Services.obs.removeObserver(advancedPaneLoadedObs, "advanced-pane-loaded"); + switchToAdvancedSubPane(browser.contentDocument); + }, "advanced-pane-loaded", false); + } else { + if (paneID) { + browser.contentWindow.gotoPref(paneID); + } + switchToAdvancedSubPane(browser.contentDocument); + } +} + +function openAdvancedPreferences(tabID) +{ + openPreferences("paneAdvanced", { "advancedTab" : tabID }); +} + +/** + * Opens the troubleshooting information (about:support) page for this version + * of the application. + */ +function openTroubleshootingPage() +{ + openUILinkIn("about:support", "tab"); +} + +/** + * Opens the troubleshooting information (about:support) page for this version + * of the application. + */ +function openHealthReport() +{ + openUILinkIn("about:healthreport", "tab"); +} + +/** + * Opens the feedback page for this version of the application. + */ +function openFeedbackPage() +{ + var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Components.interfaces.nsIURLFormatter) + .formatURLPref("app.feedback.baseURL"); + openUILinkIn(url, "tab"); +} + +function buildHelpMenu() +{ + // Enable/disable the "Report Web Forgery" menu item. + if (typeof gSafeBrowsing != "undefined") + gSafeBrowsing.setReportPhishingMenu(); +} + +function isElementVisible(aElement) +{ + if (!aElement) + return false; + + // If aElement or a direct or indirect parent is hidden or collapsed, + // height, width or both will be 0. + var bo = aElement.boxObject; + return (bo.height > 0 && bo.width > 0); +} + +function makeURLAbsolute(aBase, aUrl) +{ + // Note: makeURI() will throw if aUri is not a valid URI + return makeURI(aUrl, null, makeURI(aBase)).spec; +} + +/** + * openNewTabWith: opens a new tab with the given URL. + * + * @param aURL + * The URL to open (as a string). + * @param aDocument + * Note this parameter is now ignored. There is no security check & no + * referrer header derived from aDocument (null case). + * @param aPostData + * Form POST data, or null. + * @param aEvent + * The triggering event (for the purpose of determining whether to open + * in the background), or null. + * @param aAllowThirdPartyFixup + * If true, then we allow the URL text to be sent to third party services + * (e.g., Google's I Feel Lucky) for interpretation. This parameter may + * be undefined in which case it is treated as false. + * @param [optional] aReferrer + * This will be used as the referrer. There will be no security check. + * @param [optional] aReferrerPolicy + * Referrer policy - Ci.nsIHttpChannel.REFERRER_POLICY_*. + */ +function openNewTabWith(aURL, aDocument, aPostData, aEvent, + aAllowThirdPartyFixup, aReferrer, aReferrerPolicy) { + + // As in openNewWindowWith(), we want to pass the charset of the + // current document over to a new tab. + let originCharset = null; + if (document.documentElement.getAttribute("windowtype") == "navigator:browser") + originCharset = gBrowser.selectedBrowser.characterSet; + + openLinkIn(aURL, aEvent && aEvent.shiftKey ? "tabshifted" : "tab", + { charset: originCharset, + postData: aPostData, + allowThirdPartyFixup: aAllowThirdPartyFixup, + referrerURI: aReferrer, + referrerPolicy: aReferrerPolicy, + }); +} + +/** + * @param aDocument + * Note this parameter is ignored. See openNewTabWith() + */ +function openNewWindowWith(aURL, aDocument, aPostData, aAllowThirdPartyFixup, + aReferrer, aReferrerPolicy) { + // Extract the current charset menu setting from the current document and + // use it to initialize the new browser window... + let originCharset = null; + if (document.documentElement.getAttribute("windowtype") == "navigator:browser") + originCharset = gBrowser.selectedBrowser.characterSet; + + openLinkIn(aURL, "window", + { charset: originCharset, + postData: aPostData, + allowThirdPartyFixup: aAllowThirdPartyFixup, + referrerURI: aReferrer, + referrerPolicy: aReferrerPolicy, + }); +} + +function getHelpLinkURL(aHelpTopic) { + var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Components.interfaces.nsIURLFormatter) + .formatURLPref("app.support.baseURL"); + return url + aHelpTopic; +} + +// aCalledFromModal is optional +function openHelpLink(aHelpTopic, aCalledFromModal, aWhere) { + var url = getHelpLinkURL(aHelpTopic); + var where = aWhere; + if (!aWhere) + where = aCalledFromModal ? "window" : "tab"; + + openUILinkIn(url, where); +} + +function openPrefsHelp() { + // non-instant apply prefwindows are usually modal, so we can't open in the topmost window, + // since its probably behind the window. + var instantApply = getBoolPref("browser.preferences.instantApply"); + + var helpTopic = document.getElementsByTagName("prefwindow")[0].currentPane.helpTopic; + openHelpLink(helpTopic, !instantApply); +} + +function trimURL(aURL) { + // This function must not modify the given URL such that calling + // nsIURIFixup::createFixupURI with the result will produce a different URI. + + // remove single trailing slash for http/https/ftp URLs + let url = aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1"); + + // remove http:// + if (!url.startsWith("http://")) { + return url; + } + let urlWithoutProtocol = url.substring(7); + + let flags = Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP | + Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS; + let fixedUpURL, expectedURLSpec; + try { + fixedUpURL = Services.uriFixup.createFixupURI(urlWithoutProtocol, flags); + expectedURLSpec = makeURI(aURL).spec; + } catch (ex) { + return url; + } + if (fixedUpURL.spec == expectedURLSpec) { + return urlWithoutProtocol; + } + return url; +} diff --git a/application/basilisk/base/content/viewSourceOverlay.xul b/application/basilisk/base/content/viewSourceOverlay.xul new file mode 100644 index 000000000..8b40ddfd2 --- /dev/null +++ b/application/basilisk/base/content/viewSourceOverlay.xul @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?> + +<overlay id="viewSourceOverlay" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<window id="viewSource"> + <commandset id="baseMenuCommandSet"/> + <keyset id="baseMenuKeyset"/> + <stringbundleset id="stringbundleset"/> +</window> + +<menubar id="viewSource-main-menubar"> +#ifdef XP_MACOSX + <menu id="windowMenu"/> + <menupopup id="menu_ToolsPopup"/> +#endif + <menu id="helpMenu"/> +</menubar> + +</overlay> diff --git a/application/basilisk/base/content/web-panels.js b/application/basilisk/base/content/web-panels.js new file mode 100644 index 000000000..3a64b92a0 --- /dev/null +++ b/application/basilisk/base/content/web-panels.js @@ -0,0 +1,104 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const NS_ERROR_MODULE_NETWORK = 2152398848; +const NS_NET_STATUS_READ_FROM = NS_ERROR_MODULE_NETWORK + 8; +const NS_NET_STATUS_WROTE_TO = NS_ERROR_MODULE_NETWORK + 9; + +function getPanelBrowser() +{ + return document.getElementById("web-panels-browser"); +} + +var panelProgressListener = { + onProgressChange : function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + }, + + onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) + { + if (!aRequest) + return; + + // ignore local/resource:/chrome: files + if (aStatus == NS_NET_STATUS_READ_FROM || aStatus == NS_NET_STATUS_WROTE_TO) + return; + + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + window.parent.document.getElementById('sidebar-throbber').setAttribute("loading", "true"); + } + else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + window.parent.document.getElementById('sidebar-throbber').removeAttribute("loading"); + } + } + , + + onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags) { + UpdateBackForwardCommands(getPanelBrowser().webNavigation); + }, + + onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) { + }, + + onSecurityChange : function(aWebProgress, aRequest, aState) { + }, + + QueryInterface : function(aIID) + { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + } +}; + +var gLoadFired = false; +function loadWebPanel(aURI) { + var panelBrowser = getPanelBrowser(); + if (gLoadFired) { + panelBrowser.webNavigation + .loadURI(aURI, nsIWebNavigation.LOAD_FLAGS_NONE, + null, null, null); + } + panelBrowser.setAttribute("cachedurl", aURI); +} + +function load() +{ + var panelBrowser = getPanelBrowser(); + panelBrowser.webProgress.addProgressListener(panelProgressListener, + Ci.nsIWebProgress.NOTIFY_ALL); + panelBrowser.messageManager.loadFrameScript("chrome://browser/content/content.js", true); + var cachedurl = panelBrowser.getAttribute("cachedurl") + if (cachedurl) { + panelBrowser.webNavigation + .loadURI(cachedurl, nsIWebNavigation.LOAD_FLAGS_NONE, null, + null, null); + } + + gLoadFired = true; +} + +function unload() +{ + getPanelBrowser().webProgress.removeProgressListener(panelProgressListener); +} + +function PanelBrowserStop() +{ + getPanelBrowser().webNavigation.stop(nsIWebNavigation.STOP_ALL) +} + +function PanelBrowserReload() +{ + getPanelBrowser().webNavigation + .sessionHistory + .QueryInterface(nsIWebNavigation) + .reload(nsIWebNavigation.LOAD_FLAGS_NONE); +} diff --git a/application/basilisk/base/content/web-panels.xul b/application/basilisk/base/content/web-panels.xul new file mode 100644 index 000000000..223b20ed7 --- /dev/null +++ b/application/basilisk/base/content/web-panels.xul @@ -0,0 +1,70 @@ +<?xml version="1.0"?> + +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +<!DOCTYPE page [ +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd"> +%browserDTD; +<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd"> +%textcontextDTD; +]> + +<page id="webpanels-window" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="load()" onunload="unload()"> + <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + <script type="application/javascript" src="chrome://browser/content/browser.js"/> + <script type="application/javascript" src="chrome://browser/content/browser-places.js"/> + <script type="application/javascript" src="chrome://browser/content/browser-fxaccounts.js"/> + <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/> + <script type="application/javascript" src="chrome://browser/content/web-panels.js"/> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/> + </stringbundleset> + + <broadcasterset id="mainBroadcasterSet"> + <broadcaster id="isFrameImage"/> + </broadcasterset> + + <commandset id="mainCommandset"> + <command id="Browser:Back" + oncommand="getPanelBrowser().webNavigation.goBack();" + disabled="true"/> + <command id="Browser:Forward" + oncommand="getPanelBrowser().webNavigation.goForward();" + disabled="true"/> + <command id="Browser:Stop" oncommand="PanelBrowserStop();"/> + <command id="Browser:Reload" oncommand="PanelBrowserReload();"/> + </commandset> + + <popupset id="mainPopupSet"> + <tooltip id="aHTMLTooltip" page="true"/> + <menupopup id="contentAreaContextMenu" pagemenu="start" + onpopupshowing="if (event.target != this) + return true; + gContextMenu = new nsContextMenu(this, event.shiftKey); + if (gContextMenu.shouldDisplay) + document.popupNode = this.triggerNode; + return gContextMenu.shouldDisplay;" + onpopuphiding="if (event.target != this) + return; + gContextMenu.hiding(); + gContextMenu = null;"> +#include browser-context.inc + </menupopup> + </popupset> + + <commandset id="editMenuCommands"/> + <browser id="web-panels-browser" persist="cachedurl" type="content" flex="1" + context="contentAreaContextMenu" tooltip="aHTMLTooltip" + onclick="window.parent.contentAreaClick(event, true);"/> +</page> diff --git a/application/basilisk/base/content/webrtcIndicator.js b/application/basilisk/base/content/webrtcIndicator.js new file mode 100644 index 000000000..301607031 --- /dev/null +++ b/application/basilisk/base/content/webrtcIndicator.js @@ -0,0 +1,194 @@ +/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/webrtcUI.jsm"); + +const BUNDLE_URL = "chrome://browser/locale/webrtcIndicator.properties"; +var gStringBundle; + +function init(event) { + gStringBundle = Services.strings.createBundle(BUNDLE_URL); + + let brand = Services.strings.createBundle("chrome://branding/locale/brand.properties"); + let brandShortName = brand.GetStringFromName("brandShortName"); + document.title = + gStringBundle.formatStringFromName("webrtcIndicator.windowtitle", + [brandShortName], 1); + + for (let id of ["audioVideoButton", "screenSharePopup"]) { + let popup = document.getElementById(id); + popup.addEventListener("popupshowing", onPopupMenuShowing); + popup.addEventListener("popuphiding", onPopupMenuHiding); + popup.addEventListener("command", onPopupMenuCommand); + } + + let fxButton = document.getElementById("firefoxButton"); + fxButton.addEventListener("click", onFirefoxButtonClick); + fxButton.addEventListener("mousedown", PositionHandler); + + updateIndicatorState(); + + // Alert accessibility implementations stuff just changed. We only need to do + // this initially, because changes after this will automatically fire alert + // events if things change materially. + let ev = new CustomEvent("AlertActive", {bubbles: true, cancelable: true}); + document.documentElement.dispatchEvent(ev); +} + +function updateIndicatorState() { + updateWindowAttr("sharingvideo", webrtcUI.showCameraIndicator); + updateWindowAttr("sharingaudio", webrtcUI.showMicrophoneIndicator); + updateWindowAttr("sharingscreen", webrtcUI.showScreenSharingIndicator); + + // Camera and microphone button tooltip. + let shareTypes = []; + if (webrtcUI.showCameraIndicator) + shareTypes.push("Camera"); + if (webrtcUI.showMicrophoneIndicator) + shareTypes.push("Microphone"); + + let audioVideoButton = document.getElementById("audioVideoButton"); + if (shareTypes.length) { + let stringId = "webrtcIndicator.sharing" + shareTypes.join("And") + ".tooltip"; + audioVideoButton.setAttribute("tooltiptext", + gStringBundle.GetStringFromName(stringId)); + } + else { + audioVideoButton.removeAttribute("tooltiptext"); + } + + // Screen sharing button tooltip. + let screenShareButton = document.getElementById("screenShareButton"); + if (webrtcUI.showScreenSharingIndicator) { + let stringId = "webrtcIndicator.sharing" + + webrtcUI.showScreenSharingIndicator + ".tooltip"; + screenShareButton.setAttribute("tooltiptext", + gStringBundle.GetStringFromName(stringId)); + } + else { + screenShareButton.removeAttribute("tooltiptext"); + } + + // Resize and ensure the window position is correct + // (sizeToContent messes with our position). + window.sizeToContent(); + PositionHandler.adjustPosition(); +} + +function updateWindowAttr(attr, value) { + let docEl = document.documentElement; + if (value) + docEl.setAttribute(attr, "true"); + else + docEl.removeAttribute(attr); +} + +function onPopupMenuShowing(event) { + let popup = event.target; + let type = popup.getAttribute("type"); + + let activeStreams; + if (type == "Devices") + activeStreams = webrtcUI.getActiveStreams(true, true, false); + else + activeStreams = webrtcUI.getActiveStreams(false, false, true); + + if (activeStreams.length == 1) { + webrtcUI.showSharingDoorhanger(activeStreams[0], type); + event.preventDefault(); + return; + } + + for (let stream of activeStreams) { + let item = document.createElement("menuitem"); + item.setAttribute("label", stream.browser.contentTitle || stream.uri); + item.setAttribute("tooltiptext", stream.uri); + item.stream = stream; + popup.appendChild(item); + } +} + +function onPopupMenuHiding(event) { + let popup = event.target; + while (popup.firstChild) + popup.firstChild.remove(); +} + +function onPopupMenuCommand(event) { + let item = event.target; + webrtcUI.showSharingDoorhanger(item.stream, + item.parentNode.getAttribute("type")); +} + +function onFirefoxButtonClick(event) { + event.target.blur(); + let activeStreams = webrtcUI.getActiveStreams(true, true, true); + activeStreams[0].browser.ownerGlobal.focus(); +} + +var PositionHandler = { + positionCustomized: false, + threshold: 10, + adjustPosition: function() { + if (!this.positionCustomized) { + // Center the window horizontally on the screen (not the available area). + // Until we have moved the window to y=0, 'screen.width' may give a value + // for a secondary screen, so use values from the screen manager instead. + let primaryScreen = Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager) + .primaryScreen; + let widthDevPix = {}; + primaryScreen.GetRect({}, {}, widthDevPix, {}); + let availTopDevPix = {}; + primaryScreen.GetAvailRect({}, availTopDevPix, {}, {}); + let scaleFactor = primaryScreen.defaultCSSScaleFactor; + let widthCss = widthDevPix.value / scaleFactor; + window.moveTo((widthCss - document.documentElement.clientWidth) / 2, + availTopDevPix.value / scaleFactor); + } else { + // This will ensure we're at y=0. + this.setXPosition(window.screenX); + } + }, + setXPosition: function(desiredX) { + // Ensure the indicator isn't moved outside the available area of the screen. + desiredX = Math.max(desiredX, screen.availLeft); + let maxX = + screen.availLeft + screen.availWidth - document.documentElement.clientWidth; + window.moveTo(Math.min(desiredX, maxX), screen.availTop); + }, + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "mousedown": + if (aEvent.button != 0 || aEvent.defaultPrevented) + return; + + this._startMouseX = aEvent.screenX; + this._startWindowX = window.screenX; + this._deltaX = this._startMouseX - this._startWindowX; + + window.addEventListener("mousemove", this); + window.addEventListener("mouseup", this); + break; + + case "mousemove": + let moveOffset = Math.abs(aEvent.screenX - this._startMouseX); + if (this._dragFullyStarted || moveOffset > this.threshold) { + this.setXPosition(aEvent.screenX - this._deltaX); + this._dragFullyStarted = true; + } + break; + + case "mouseup": + this._dragFullyStarted = false; + window.removeEventListener("mousemove", this); + window.removeEventListener("mouseup", this); + this.positionCustomized = + Math.abs(this._startWindowX - window.screenX) >= this.threshold; + break; + } + } +}; diff --git a/application/basilisk/base/content/webrtcIndicator.xul b/application/basilisk/base/content/webrtcIndicator.xul new file mode 100644 index 000000000..9208dc814 --- /dev/null +++ b/application/basilisk/base/content/webrtcIndicator.xul @@ -0,0 +1,35 @@ +<?xml version="1.0"?> + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/webRTC-indicator.css" type="text/css"?> + +<!DOCTYPE window> + +<window xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="webrtcIndicator" + role="alert" + windowtype="Browser:WebRTCGlobalIndicator" + onload="init(event);" +#ifdef XP_MACOSX + inwindowmenu="false" +#endif + sizemode="normal" + hidechrome="true" + orient="horizontal" + > + <script type="application/javascript" src="chrome://browser/content/webrtcIndicator.js"/> + + <button id="firefoxButton"/> + <button id="audioVideoButton" type="menu"> + <menupopup id="audioVideoPopup" type="Devices"/> + </button> + <separator id="shareSeparator"/> + <button id="screenShareButton" type="menu"> + <menupopup id="screenSharePopup" type="Screen"/> + </button> +</window> |