diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-02 03:35:06 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-02 03:35:06 -0500 |
commit | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (patch) | |
tree | 6efefe6a09feb09d965932b24e10436b9ac8189c /application/palemoon/base/content | |
parent | e72ef92b5bdc43cd2584198e2e54e951b70299e8 (diff) | |
download | uxp-49ee0794b5d912db1f95dce6eb52d781dc210db5.tar.gz |
Add Pale Moon
Diffstat (limited to 'application/palemoon/base/content')
140 files changed, 41331 insertions, 0 deletions
diff --git a/application/palemoon/base/content/aboutDialog.css b/application/palemoon/base/content/aboutDialog.css new file mode 100644 index 0000000000..aa79b0795c --- /dev/null +++ b/application/palemoon/base/content/aboutDialog.css @@ -0,0 +1,70 @@ +/* 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/. */ + +#PMaboutDialog { + width: 620px; +} + +#PMrightBox { + 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; +} + +#PMrightBox:-moz-locale-dir(rtl) { + background-position: 100% 0; +} + +#PMbottomBox { + padding: 15px 10px 0; +} + +#PMversion { + margin-top: 10px; + -moz-margin-start: 0; + -moz-user-select: text; + -moz-user-focus: normal; + cursor: text; +} + +#distribution, +#distributionId { + font-weight: bold; + display: none; + margin-top: 0; + margin-bottom: 0; +} + +.text-blurb { + margin-bottom: 10px; + -moz-margin-start: 0; + -moz-padding-start: 0; +} + +#updateButton, +#updateDeck > hbox > label { + -moz-margin-start: 0; + -moz-padding-start: 0; +} + +.update-throbber { + width: 16px; + min-height: 16px; + -moz-margin-end: 3px; + list-style-image: url("chrome://global/skin/icons/loading_16.png"); +} + +.text-link, +.text-link:focus { + margin: 0px; + padding: 0px; +} + +.bottom-link, +.bottom-link:focus { + text-align: center; + margin: 0 40px; +} diff --git a/application/palemoon/base/content/aboutDialog.js b/application/palemoon/base/content/aboutDialog.js new file mode 100644 index 0000000000..f4c2a990ca --- /dev/null +++ b/application/palemoon/base/content/aboutDialog.js @@ -0,0 +1,590 @@ +# 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/Services.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 if this is an "a#" or "b#" build + let version = Services.appinfo.version; + if (/[ab]\d+$/.test(version)) { + let buildID = Services.appinfo.appBuildID; + let buildDate = buildID.slice(0,4) + "-" + buildID.slice(4,6) + "-" + buildID.slice(6,8); + document.getElementById("PMversion").textContent += " (" + buildDate + ")"; + } + +#ifdef MOZ_UPDATER + gAppUpdater = new appUpdater(); +#endif + +#ifdef XP_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); +#endif + +// get release notes URL from prefs + var formatter = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Components.interfaces.nsIURLFormatter); + var releaseNotesURL = formatter.formatURLPref("app.releaseNotesURL"); + if (releaseNotesURL != "about:blank") { + var relnotes = document.getElementById("releaseNotesURL"); + relnotes.setAttribute("href", releaseNotesURL); + } +} + +#ifdef MOZ_UPDATER +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/DownloadUtils.jsm"); +Components.utils.import("resource://gre/modules/AddonManager.jsm"); + +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() +{ + this.updateDeck = document.getElementById("updateDeck"); + + // Hide the update deck when there is already an update window open to avoid + // syncing issues between them. + if (Services.wm.getMostRecentWindow("Update:Wizard")) { + this.updateDeck.hidden = true; + return; + } + + 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.bundle = Services.strings. + createBundle("chrome://browser/locale/browser.properties"); + + this.updateBtn = document.getElementById("updateButton"); + + // The button label value must be set so its height is correct. + this.setupUpdateButton("update.checkInsideButton"); + + 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.setupUpdateButton("update.restart." + + (this.isMajor ? "upgradeButton" : "updateButton")); + return; + } + + if (this.aus.isOtherInstanceHandlingUpdates) { + this.selectPanel("otherInstanceHandlingUpdates"); + return; + } + + if (this.isDownloading) { + this.startDownload(); + return; + } + + if (this.updateEnabled && this.updateAuto) { + this.selectPanel("checkingForUpdates"); + this.isChecking = true; + this.checker.checkForUpdates(this.updateCheckListener, true); + return; + } +} + +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"; + } + return this.um.activeUpdate && + (this.um.activeUpdate.state == "pending" || + this.um.activeUpdate.state == "pending-service"); + }, + + // 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 the update type is major. + get isMajor() { + if (this.update) + return this.update.type == "major"; + return this.um.activeUpdate.type == "major"; + }, + + // 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 deck's selected panel. + * + * @param aChildID + * The id of the deck's child to select. + */ + selectPanel: function(aChildID) { + this.updateDeck.selectedPanel = document.getElementById(aChildID); + this.updateBtn.disabled = (aChildID != "updateButtonBox"); + }, + + /** + * Sets the update button's label and accesskey. + * + * @param aKeyPrefix + * The prefix for the properties file entry to use for setting the + * label and accesskey. + */ + setupUpdateButton: function(aKeyPrefix) { + this.updateBtn.label = this.bundle.GetStringFromName(aKeyPrefix + ".label"); + this.updateBtn.accessKey = this.bundle.GetStringFromName(aKeyPrefix + ".accesskey"); + if (!document.commandDispatcher.focusedElement || + document.commandDispatcher.focusedElement == this.updateBtn) + this.updateBtn.focus(); + }, + + /** + * Handles oncommand for the update button. + */ + buttonOnCommand: function() { + if (this.isPending || this.isApplied) { + // 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) + 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); + return; + } + + const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul"; + // Firefox no longer displays a license for updates and the licenseURL check + // is just in case a distibution does. + if (this.update) { + var ary = null; + ary = Components.classes["@mozilla.org/supports-array;1"]. + createInstance(Components.interfaces.nsISupportsArray); + ary.AppendElement(this.update); + var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no"; + Services.ww.openWindow(null, URI_UPDATE_PROMPT_DIALOG, "", openFeatures, ary); + window.close(); + return; + } + + this.selectPanel("checkingForUpdates"); + this.isChecking = true; + this.checker.checkForUpdates(this.updateCheckListener, true); + }, + + /** + * 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; + } + + gAppUpdater.selectPanel("updateButtonBox"); + gAppUpdater.setupUpdateButton("update.openUpdateUI." + + (this.isMajor ? "upgradeButton" + : "applyButton")); + }, + + /** + * 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; + } + }, + + /** + * Checks the compatibility of add-ons for the application update. + */ + checkAddonCompatibility: function() { + var self = this; + AddonManager.getAllAddons(function(aAddons) { + self.addons = []; + self.addonsCheckedCount = 0; + aAddons.forEach(function(aAddon) { + // Protect against code that overrides the add-ons manager and doesn't + // implement the isCompatibleWith or the findUpdates method. + if (!("isCompatibleWith" in aAddon) || !("findUpdates" in aAddon)) { + let errMsg = "Add-on doesn't implement either the isCompatibleWith " + + "or the findUpdates method!"; + if (aAddon.id) + errMsg += " Add-on ID: " + aAddon.id; + Components.utils.reportError(errMsg); + return; + } + + // If an add-on isn't appDisabled and isn't userDisabled then it is + // either active now or the user expects it to be active after the + // restart. If that is the case and the add-on is not installed by the + // application and is not compatible with the new application version + // then the user should be warned that the add-on will become + // incompatible. If an addon's type equals plugin it is skipped since + // checking plugins compatibility information isn't supported and + // getting the scope property of a plugin breaks in some environments + // (see bug 566787). + try { + if (aAddon.type != "plugin" && aAddon.isCompatible && + !aAddon.appDisabled && !aAddon.userDisabled && + aAddon.scope != AddonManager.SCOPE_APPLICATION && + !aAddon.isCompatibleWith(self.update.appVersion, + self.update.platformVersion)) + self.addons.push(aAddon); + } + catch (e) { + Components.utils.reportError(e); + } + }); + self.addonsTotalCount = self.addons.length; + if (self.addonsTotalCount == 0) { + self.startDownload(); + return; + } + + self.checkAddonsForUpdates(); + }); + }, + + /** + * Checks if there are updates for add-ons that are incompatible with the + * application update. + */ + checkAddonsForUpdates: function() { + this.addons.forEach(function(aAddon) { + aAddon.findUpdates(this, AddonManager.UPDATE_WHEN_NEW_APP_DETECTED, + this.update.appVersion, + this.update.platformVersion); + }, this); + }, + + /** + * See XPIProvider.jsm + */ + onCompatibilityUpdateAvailable: function(aAddon) { + for (var i = 0; i < this.addons.length; ++i) { + if (this.addons[i].id == aAddon.id) { + this.addons.splice(i, 1); + break; + } + } + }, + + /** + * See XPIProvider.jsm + */ + onUpdateAvailable: function(aAddon, aInstall) { + if (!Services.blocklist.isAddonBlocklisted(aAddon.id, aInstall.version, + this.update.appVersion, + this.update.platformVersion)) { + // Compatibility or new version updates mean the same thing here. + this.onCompatibilityUpdateAvailable(aAddon); + } + }, + + /** + * See XPIProvider.jsm + */ + onUpdateFinished: function(aAddon) { + ++this.addonsCheckedCount; + + if (this.addonsCheckedCount < this.addonsTotalCount) + return; + + if (this.addons.length == 0) { + // Compatibility updates or new version updates were found for all add-ons + this.startDownload(); + return; + } + + this.selectPanel("updateButtonBox"); + this.setupUpdateButton("update.openUpdateUI." + + (this.isMajor ? "upgradeButton" : "applyButton")); + }, + + /** + * 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 update = this.um.activeUpdate; + 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") { + // 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("updateButtonBox"); + self.setupUpdateButton("update.restart." + + (self.isMajor ? "upgradeButton" : "updateButton")); + } 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("updateButtonBox"); + this.setupUpdateButton("update.restart." + + (this.isMajor ? "upgradeButton" : "updateButton")); + } + 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; + } +}; +#endif diff --git a/application/palemoon/base/content/aboutDialog.xul b/application/palemoon/base/content/aboutDialog.xul new file mode 100644 index 0000000000..1ba8f06a1a --- /dev/null +++ b/application/palemoon/base/content/aboutDialog.xul @@ -0,0 +1,120 @@ +<?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="PMaboutDialog" + 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"/> + + <vbox id="aboutPMDialogContainer"> + <hbox id="PMclientBox"> + <vbox id="PMleftBox" flex="1"/> + <vbox id="PMrightBox" flex="1"> +#ifdef HAVE_64BIT_BUILD +#expand <label id="PMversion">Version: __MOZ_APP_VERSION__ (64-bit)</label> +#else +#expand <label id="PMversion">Version: __MOZ_APP_VERSION__ (32-bit)</label> +#endif + <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="updateButtonBox" align="center"> + <button id="updateButton" align="start" + oncommand="gAppUpdater.buttonOnCommand();"/> + <spacer flex="1"/> + </hbox> + <hbox id="checkingForUpdates" align="center"> + <image class="update-throbber"/><label>&update.checkingForUpdates;</label> + </hbox> + <hbox id="checkingAddonCompat" align="center"> + <image class="update-throbber"/><label>&update.checkingAddonCompat;</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="manualUpdate" align="center"> + <label>&update.manual.start;</label><label id="manualLink" class="text-link"/><label>&update.manual.end;</label> + </hbox> + </deck> +#endif + </vbox> + + <description class="text-pmcreds"> + Pale Moon is released by <label class="text-link" href="http://www.moonchildproductions.info">Moonchild Productions</label>. + </description> + <description class="text-pmcreds"> + Special thanks to all our supporters and donors for making this browser possible! + </description> + <description class="text-blurb"> + If you wish to contribute, please consider helping out by providing support to other users on the <label class="text-link" href="https://forum.palemoon.org/">Pale Moon forum</label> + or getting involved in our development by tackling some of the issues found in our GitHub issue tracker. + </description> + </vbox> + </vbox> + </hbox> + <vbox id="PMbottomBox"> + <hbox pack="center"> + <label class="text-link bottom-link" href="about:license">Licensing information</label> + <label class="text-link bottom-link" href="about:rights">End-user rights</label> + <label class="text-link bottom-link" id="releaseNotesURL">Release notes</label> + </hbox> + <description id="PMtrademark">&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/palemoon/base/content/aboutRobots-icon.png b/application/palemoon/base/content/aboutRobots-icon.png Binary files differnew file mode 100644 index 0000000000..1c4899aaf7 --- /dev/null +++ b/application/palemoon/base/content/aboutRobots-icon.png diff --git a/application/palemoon/base/content/aboutRobots-widget-left.png b/application/palemoon/base/content/aboutRobots-widget-left.png Binary files differnew file mode 100644 index 0000000000..3a1e48d5fa --- /dev/null +++ b/application/palemoon/base/content/aboutRobots-widget-left.png diff --git a/application/palemoon/base/content/aboutRobots.xhtml b/application/palemoon/base/content/aboutRobots.xhtml new file mode 100644 index 0000000000..23fe3ba17f --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/abouthome/aboutHome.css b/application/palemoon/base/content/abouthome/aboutHome.css new file mode 100644 index 0000000000..73c6862021 --- /dev/null +++ b/application/palemoon/base/content/abouthome/aboutHome.css @@ -0,0 +1,339 @@ +%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%,90%); + color: #000; + height: 100%; +} + +body { + margin: 0; + display: -moz-box; + -moz-box-orient: vertical; + width: 100%; + height: 100%; + background-image: url(chrome://browser/content/abouthome/noise.png), + linear-gradient(hsla(0,0%,100%,.7), hsla(0,0%,100%,.4)); +} + +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; +} + +#searchForm { + width: 470px; +} + +#searchForm { + display: -moz-box; +} + +#searchLogoContainer { + display: -moz-box; + -moz-box-align: center; + padding-top: 2px; + -moz-padding-end: 8px; +} + +#searchLogoContainer[hidden] { + display: none; +} + +#searchEngineLogo { + display: inline-block; + height: 28px; + width: 70px; + min-width: 70px; +} + +#searchText { + -moz-box-flex: 1; + padding: 6px 8px; + background: hsla(0,0%,100%,.9) padding-box; + border: 1px solid; + 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); + border-radius: 2.5px 0 0 2.5px; +} + +#searchText:-moz-dir(rtl) { + border-radius: 0 2.5px 2.5px 0; +} + +#searchText:focus, +#searchText[autofocus] { + border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6); +} + +#searchSubmit { + -moz-margin-start: -1px; + background: linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box; + padding: 0 9px; + border: 1px solid; + border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2); + -moz-border-start: 1px solid transparent; + border-radius: 0 2.5px 2.5px 0; + 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; +} + +#searchSubmit:-moz-dir(rtl) { + border-radius: 2.5px 0 0 2.5px; +} + +#searchText:focus + #searchSubmit, +#searchText + #searchSubmit:hover, +#searchText[autofocus] + #searchSubmit { + border-color: #59b5fc #45a3e7 #3294d5; + color: white; +} + +#searchText:focus + #searchSubmit, +#searchText[autofocus] + #searchSubmit { + background-image: 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 + #searchSubmit:hover { + background-image: 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: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: 2.5px; + 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; + -moz-margin-end: 8px; +} + +#restorePreviousSession:-moz-dir(rtl)::before { + transform: scaleX(-1); +} + +body[narrow] #restorePreviousSession::before { + content: url("chrome://browser/content/abouthome/restore.png"); + height: 32px; + width: 32px; +} + +/* [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 { + transform: scale(.5); + transform-origin: 0 0; + } + + #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"); + } +} + diff --git a/application/palemoon/base/content/abouthome/aboutHome.js b/application/palemoon/base/content/abouthome/aboutHome.js new file mode 100644 index 0000000000..9e826b69e9 --- /dev/null +++ b/application/palemoon/base/content/abouthome/aboutHome.js @@ -0,0 +1,354 @@ +/* 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 SEARCH_ENGINES = { + "Google": { + // This is the "2x" image designed for OS X retina resolution, Windows at 192dpi, etc.; + // it will be scaled down as necessary on lower-dpi displays. + image: "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAIwAAAA4CAYAAAAvmxBdAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ" + + "bWFnZVJlYWR5ccllPAAAGrFJREFUeNrtfHt4VdW172+utZOASLJ5+BaIFrUeXkFsa0Fl++gDnznV" + + "VlvFxt7aqvUUarXtse3Bau35ak/rZ9XT26NtfOvV6wFET+FYCQEKWqsQIT5RCAgSXnlnrzXneNw/" + + "1lphJSSQ8BB7bub3zW+LO3uN+fiNMcf4jTEX0N/6W3/rb/2tv30smtnXB3zmRi2FQakxQNKX3WkW" + + "9S/tgW3HLpmQM543A0BWVSHMYGIwOTDxzxrOf3/RQQfMZ2/SLAvKhTFVBGUqKFONH2QAzwOMF38a" + + "wHhYZAxWAqhe/iszp3+b970d/sInc57vz/J8L2eMB2MAEYkBQ6DQ3dRw4dq7AUjcP3rAfPZmLWXC" + + "LHKoIAcQAUxaB5EaEfc6AEBhjDEwmcx43/fO9HxT4vkReBIAAZgjgodW3NcPnn1sHgD/iHknn+0d" + + "6s8XEUhsXXac/34WAAGw8afuT8GZ3X055YeSJcIsG+pMZwFn0UihezRofPt3G54f/0E8cNMN+Myo" + + "8jVTCgYd823PLzrPeIBnABiUQ1F+UoWsVOYb33mkoKp/7/dKyT0AGc47X4s0sjBEoLxbBqAQAMfW" + + "Rfe38B4BM+VHUkYOs8mi1FrABbK4dcvK73zwp1M3xYPOxANKBqbpCdXNGb0UwPKRF74xpfDQ0t+K" + + "54+IvlKoahmAhaO/mv/ZmicG3tqPgT61ZM2dZMQJOYhIdByRM/F3dCCOox4Bc3oEliqyyNoQCPPu" + + "sXceKZqRsigu7pwaWBowiRb46+f9Q1V2wl1nDx09/R7jF30x9adNlN8yPx4DHwht+B/cBIBoRqeI" + + "E4hE/oshTcB0wNbT6/o/zrhFyohR5ZxmrVWE+fDxdx4puhGAH4OkPe5B6pykeJAc/7cDEMZ/095Y" + + "870P339m+BXs2v4kbCFsm9u2vnpJ3bzR7wAo2B/R2v+PjSnyXcRxtOLUSXFxwAFz5i2SZUIVO82S" + + "BWye/vLOIwNvjL8OYqCEfXCmJAZPHkC7sK1REbj2+lmbq86qTVmmfuuyN2cTiREWKCvACgml9kDL" + + "7HQksehsZmSdA6yVpsa6P38v3swg7m4vN1dGXrThKGP8yS5fP33j/LEvxKDbl2f2A0YFCtkZQDOa" + + "PjLAnP4jrmBGjh1AVhG2ttxfX33++vjY2eeNXf/siLUAzgEwMJZrY2vF/Vu/t4BRqCqgCmj07wMV" + + "HXUCzJQfUlZE72ICnANcqNj21h8eiK1AX46gXh29KT9H+rd9XxBjYGCgig7QHOgjPgMAKigXQZYp" + + "si4uCOc3v35zY2wF9ufGSgxA7fdd9g8ho9ol4P4ojiQWnSUMMANECrJNy1NWYH8eGfsEvJbLv1IK" + + "1XIAUwEtA0xplJMwjcaYlTDeShg8dOgjj6/cJxNYfWIWkHJoh5yyjkSZ8RbB89YBZq4/pXafGeuz" + + "b9WciXJxo2B2houqgAjABJCLOwFMqFv57+bBxMIAJm1det3avnl1OYCLAeSgWhofaY1QXQSRuYc+" + + "/OiD3QLmUzNdqTBKhRVMADsF5beuToXJB90KtFz+lVIVniXOVUAUqjpXVB4WwPjGTPB8/0zjeTnj" + + "ezl43szmKy6vNkDF4MeeXNc3oJyUhfAMkJsJkSxUVrLos6o6z/O8Ucb3phrPzyHKeVTwkpPXseg3" + + "Cqe+1SfG+swfaw6KGTAoJ5eyGF3IBeEIJB2AcXxb0FI/L45uFQBMGiu6Z3ai9eqrclBUClFWVatV" + + "5GERNT5wEVQnQLUcIuVNX75kFjn60rA5c1d0AoywlkcxfdwZ2LSgbOmBZAv70povu7RcyFUqcZYd" + + "Pbxix44fnLv8pbYUOWh+P3ZM9uJRo34xoLDgq8b3YTxvqhqsaPzyJTdmn36msjdyqPqkMhWqBFGZ" + + "MtV8uDX4zMjp2zemyEoPgGn4zyOvGzy48A54GcD3Sz1jFrqqE+4uOOvdmb0ASlYEs5mQE9afUdhy" + + "0yv3lHzwya/8ZcjgI0+5yssU3QKYkgQ4Ivp60LL1n8kBQfOWuvdnj6uLldgHQKoKxU7HV/eg2y1X" + + "XXmXEs1U0ZVb29o//4k5c5P5eQB+s+68aVeUFBTcCxUoS6kRWfjhueecc9SfX3ytA9QTr7eVACqY" + + "FDYEwnbB2qcHHg6gLY6ODhpomi77coUyVaojhKH9+ZHzF/wqXiztEg34APxNX/jCvQOLCi83fpy8" + + "UsCJXHLYnGdn785S0uKTyyBUBXJZcW5x4bSN56ciyLQcD4Bf/+ThVwwbUvRb+JkoswqAWX5b9Lm1" + + "M3uSM/UnUiaCKiZk2blvvnxX0ePxuBNAmpMur51wyLBPzjVeBBoVwIXBk6vuP+SG+LkcuwkWAA96" + + "/JjZKnKxkACkkFb5Nztz220xX9bJlWi+6opKFalQlpqlmzZNu6B6SaJ0knKJ/DW5qd8p8TO3x6AB" + + "qza1EE06cdmy9wDAY5LjmBTMkQnUnZ42H0ywNF52aU6FK4UY5NySI+cv+E3MCnMM5HyqtwFoO3rB" + + "gmuDMFjGjiCOIEQwzH9c+7lzju+JTaYlJ2ehUqXMWWFqeurFxqsAFMVf25Ss9kTOEZdvebClJbxT" + + "yUGZoEzwlL/b9tzRX+pOztSfSBZApSqyIrL45buKnkaUJEzLCN5+csxr+ab6fyILkI2OIZYBlx9/" + + "2bYvpLgw2+EqKLKdwoceVKJp+tfuEpYKZcaW1tZbLqheEsbj3GV+oxdV3x0GwQZrHUIiWKIST3Vm" + + "DG54zFrKrBBWiGgSyx9Uv6Xh0n/MKlGlOII4h80trQ+kuJt8HGklZHg6FZF/Y/uOb7O1YOvAzkGt" + + "Kxmoehe6SYNEpkErwZIFC4I2fuLKf2tLtDOPzumPhA6wAPJDLt1yuzjaAEcAMUCMApXfvPP7IcO6" + + "gkYFs4RRpgy49qanUsAPu/T8W48e/YwL6S/kYtBYwM8U/yu6KVlQUShr9CkKyK7b1vDVy0qVeaYy" + + "gaxbdeK85/8a/z7sYR3zgXM1gXUInEPoCEw8PR6z8YQxaidQPh6RrgrPEOZS4chKjFuydEEKFD1x" + + "QgrAnfO3V98Jw/B5dhFgmByU+MK/nnrq6K6gcQtPyqlIubJAibCxPv/fsVVNgCI9yGEAQdBq71NH" + + "UEdQIoBo5PBBeklazuQfSpYFM0UAFsDmd2yMf9+1XkUT3otc8AiRwpFChCBCI0detGbSLtYr5uw6" + + "tk26XctZwgxhRt65ZSmr1t389M1Jk85wzKcHRAiJkCfasDnI/0sMGN+jlLMrAigMhp0+f+TBBIw4" + + "milEYOcQBHZZAoZeEIgKgIIgeJbD2MqEFhxaDAFmdAWMisxQFigzlAUnX9e4rA9yeHuTna3koBQB" + + "RogxwOPvxNbQAAA7VHQEFKSQKEFIu4lA5d3HiiuFNB4XQZlhUHBK11QO0oRdD7ouROVCkeJZG7ak" + + "/KBOYHlz4sTy1WVlVY5oYego2+bs82+3tFw6YcVrp01dteqpxNfyhKQuGlxCMSsKBh570ABT/8XP" + + "5dhRVpyDWAd2Ns0O9yrhWdfcMpvCEByEoNCCwhBgvgBdM+PM5TH5FPW+1ZLo8de2viehe12dhVoH" + + "OAtDPO61O4o+kYCTnE5wVuGsxlzKHul7BUDKdomKgwpB2QHAyNiP2Dl+0Z2WRXZ9YP0F55WJczvX" + + "0jp09U3fLiurWD1+/NqQaHZIVNbu3O1vt7aM+fSqVRWXvPvu0pRldwAkQ5brjO+NMh0kgMIvGjYZ" + + "wIKETPxIrYt1U5M8iThKJil9yZGc++ab298dP36Jb8wZohqhQHRErKEeAA6fG5FT5yIlYYI6tzfO" + + "vtiQni3MYDw0ChqEgUMyejyAdwGwDeW4ZI9FAGQOmwzgv/cERmZbDXhnKBNUGMJkUhGVduSSJJ1P" + + "6rw8HIalJo7ilBkchgCgL48fVzLceDc4kZnWUdap1AQi10x+660n4jXyk1M7ZXEZgHhMUkMO4Njp" + + "hQGMf8h56Fx++ZE1a+1xZC2Szjs3sk9uUEhUbSMvP3LeyOGZ0tKJiearo1J1DHVRPYmS7JUcG2g1" + + "pxxUsooBnpmQWAOb10YbKGygcKFCZOC0XqxrRKokCBQG5euX77In2k1P+2hhWEZBAAoCuCCEcW7E" + + "2xMn/m6oYo0jyjnmuc3Off6UN96YMvmtt5LILSmQ61r3xAA0I+xqPBiIejAd1f7e2MPPfvm4LQs/" + + "89a+bP6nZuSzfsaU+T7g+UBixYQVRFGS01kFO22srRy0EgA4CEvFRHS3MANMY/fGbybmlQqAFSBV" + + "sCp8kWwCGA5dqefFShnnRV77ecHYU37iXuqLoB0tsuIo34v3NfJR1GlJsrnOuiXGy1y8k+rwxh57" + + "3srSD/6rbLdra7yMqgjUCGAULR8uWr0LJPYAGApCeCbKNygLPKIxJ65YOSU+YpLUUCYGiqBzQVy3" + + "Ft1zbevnJl60UARqACgcVDo9ZZr63Mqua68QxlpmrWJC1FmrmLSKCFVktcpZrbKhzg4D26E5Lgjg" + + "8vnoMwwh1hU/dvTRo/qcDyJqcESw5Dp6o3XNHVrqLDSubAdFjuXwwWZcX+Wc9APboKxQUoiLurXa" + + "IYfCpjlCDsoxZ6OCouLRt+xpbY3nA8aDMR6E2+9vffOWxl02cQ+Bbdjevt7l83D5ABRaKNHYO484" + + "YmgMkoJ4jElCOL8Lz9NN87YumrRDxc2DElQZKgIVhZcZcO1hZ74wtK/H0thvtuXGXdM2S0S/ziQ1" + + "FPJiG7pHwvbgDhtKnQ0VNhCEeUHQLmiuf2fymieGvJGY8DCfX+yCEC5xWIlwtO+P6+s4VESJGS4+" + + "liwxKjZ/2FGRZvPhYgktxEZdHWOAr2P34ihWIQWTgJ2CnWJbo9Ymz1g/5+h1QsF9wgKJ19Z4hV87" + + "4fKNE3cnx8v4V8H4UOjqhvce+zW6qdWVlOvSjQsDlw/WUT4A5QNQGIJDizMPHXR+CiRBb4GSzlYr" + + "26Z7vYKSC42nUOPBqA9VU1I0ZOJPEYWj1NvVW/3AoEUAFgO4IzZ1hYk2jf9WUw7IjCIXHUVhXrFp" + + "/sQtKZPIoXXr/PjoSkZeoHo6gP/bFyeciECqcHG3IrXp37a2SF3xQNPxRAXgq5nS1bHsDWCYALYA" + + "u+h0W/impI8Pad9ec/vAoWVTjV84Nsn5FAwcvmDMN5rOqf1jyatdHzjuGjvThloKYH3b5qVXt775" + + "44ZuN1QEKknF3a6ImfDee4tWjBrV6R5Qoeq1AP6Avaxx8gDolhdPXAh2qzQmZFQ4ZhALrj/mvLpT" + + "+qhxya0BP5VVZQBkA6jNR0AJ2xUUcjKGjsx4k3PVYUwaJU6rJ3reLiHlHppjBjF3fLYSzU/noEZ8" + + "3611VusoVJBVsFWAdezim/3jemSFe+SNIsvCpAhCXf7TBZI+PnTr4nO2t2xcME3ZroYKIouEEqDo" + + "xfHfav/GxOttFgBOucGWll0XVqrqXYDWNLz3aG7bsovWp4i2TvkhScLqNBezq/M/zxLBxV2Yx/75" + + "yCPP6usc04CJ+B3bcLMwQTiK+0UIwgz1ip8+4pyaYX0x0SnWMkjnYGygkm9nBO0MGzoI2TTDyQBw" + + "7ubNawPmeZYZNt5wZhrxX8OHX9yXSTJzGcVgIWasbs8/hc7XRzXM670cg0Vs5H+MHm6u74ucrb/K" + + "lAlFPoySoqFFn+rm+OCGV762df2cYWe4fP0M5qDWhoowRIm1/h+s1YZx3wrVOV1LDhXMaGzfXntF" + + "46vXtMQRS/clsqRRT9SNd0GMBo6edRStZbKeg4D//ciQIcP2CTDbqsdVKQePq1JMFkXxv4qO9AaM" + + "fPGoaeuG9kXp0LkU0wGgMFC1gYAdAeyg0m3IrE3W3mtTvodjRpHq9X3xL4h5Qsq63P/z9ra6LqSc" + + "vvmBPkwOTex2lnf4wNee/47fa99NGGVJ8Zl1qP3UPfwkdr15mDDV+Y3Pf+Kh9c9kz9pee89J7dve" + + "vaRt+7qLbVv47y5UUKggp3BB/okNz0/aHI8332OaIgELxWDpptQtt6X+Qcu03nVYGQYxjxzl+7/e" + + "GyvjdYrCtv31JiW7QTjy6qWj83jF4AeP/MLaodiHRtZBXAihEEIWkq4eSgGmvKGhqpX5d1YEVhiW" + + "BaI6Zf6QITN7s5ELhw4tZZavkwhIZMOC1rZfo5s64nPv4+1NzXot2/hYiqKckglH4/7eRojCOosp" + + "St6u2ijfS1Hv3I0SdVy5aam9ecumBeOqN8w7aRkxSlMVdRDmRHa4m5xWPKPEusUA6maIrcy/cCKw" + + "InASKaCoXrlo2LAH+xpMpAEjLauu2ObaNnxVmZqUHaI8SaR+KnIhTPHCo6ZtOn6vk4qUPNNGnV2P" + + "J0ptENweMq92zHBMcMwwIrfMLS6etKdJEnMlCYOZm9YE4dUPkWvsIUckJ/+SZwd5PCEOEBc5rh7j" + + "grqf+VfvSc7mO/xZSihVAra3YMY/PqqrUhZVe7C8yRHTBqAVQJuQN5idgJ2ASQAz4PJjptWevKc0" + + "RZQ0TQATRWDd/dmFDQ2VeaLH0z4dRVTK9EXZ7IqFJSXH7W6eLw0blntp2NAydGOSqPGVs/5mW9Zc" + + "JGKbRSxELIRDCFuIuAmiBa8eMW37rcdc1JDtM+3PYdSp43k9/ulPgmDrsnz+vFBktRWBZYEVKSlU" + + "feH5wYPP7u5Hfy4uzi4oLq50IjkSaXrf2vIfBPnV6PlKiwKg0XfyNe2BPkmJ8+oUGeh/bLjNu7En" + + "0Gy+w5sppLcyKRra9IZJ98hTvciop9MPSSFUwGTnEjHICsgpyKHYHzjquWMvrJ+wewUENPFjCIAx" + + "k3uStyIMbw5FVieWJvJpBE5kgqq+X1VcPGdRcfHMxSUluSUlJbmlUZ+1tKRkLRGVnrZ9Rw12rSLt" + + "sDpFg8vmfbpw0HH3wcuMMSaiao2XAbwMjPFhPL/ReN6DfsY8tHHekN0WXR929vqsCpWruFshPEqF" + + "o3IyADuWTxgea1rYTbRVeEMmc+SnCwp+OcB4l3kmLq0D4BnzkA/MMUBjvDMXC1DBqlkCFr9N9E//" + + "HIZpPyDsQVuTFwsMfP273k8GFeLbvo9izwe8DGA8VMPgIc/D2piALlPFDGWUMqNuazOun/RbeQU7" + + "L/zl0cfC+SPOXjG84NBRawCvJNoSE7PiBgr5Xx/MKf7jLnzIbUPKlHVF5C11KgJfD9+shY8Vxjd3" + + "0780rEvP8bFDDvnVQGO+lU5MeTDwzM5aTbOzNyrw/XNbWx9JFLknk+sjqjobUHJq9XS/cNj3jZcZ" + + "Ac9PwBIDyAeMD2O8RhhvpTFYqYpGqMQOM2UhlFOhsvjfgNJ6ofxyoZaXbHPt8mDNjDU9ACYBbyGA" + + "AT/KZEZ/MpO5qciYyRlgROeJGSh0nQCL21Ufmx4EL8dMpqScRt4DFVAAYMCtORx+0Rhz7aFF+GJB" + + "BmNM/JKklGo1KlBtHZ474U79P9hZOZcQYb0unD/mwu05qADCZwE4C8Y7I3kTk4kFx+mUuzfMKf5e" + + "+rn+rUMq4PR4hFII0gw0xpdvGAWGoDqHf9m8IuV8m2Qtf1pQMPok37+50JhpHlC8EzwRcAzwOqs+" + + "Vkv06I+da04nInd3RvuxgCIAhcUTF5zvFQ79oucP+Cy8zIjE6qQnt5Pviu5IqAogVKNCNSrBUte6" + + "blnrqi/Vo3O9rI3Pc7cbP6sgGQcAf7rvl3zK908uBKjAGK5jrrmNKKHj/RS3E6L3V2USLUzkZAB4" + + "i75pTivwwQMyoKYQ685+QOtScvzUHPbIlJ54ZVsuDPTrZDmnQqUQggo1qkoNRDyFeJ6XGQfjF0fW" + + "3O9YWxW6adNzw36Dzm/JKEJ0k7QgtfiSygd1vSrkdZ3jlb6fneT7Y+MN1xrmVX9gbkw9q1MdsemF" + + "U5wkpwqSRSw49gfZAcPPHOsVlIww/sBjjPEVnqfGZEQlWKVCjWK31TW/dv56pCruU126TGxPl+US" + + "IrAgNQ7TQ+pNukQqfalLNimApvMt6CZMTvsiu3VOJ17XnrNWZ9m85oK8Qmz4sFB+CeXrF29dfOqG" + + "1PwKs6fOKyvKjrnb8wrHGD8TWfCOEoX85zb96dgXY9leN2NM+y3SJZG4u7XsSldIykFPz09NHxbR" + + "T2U3M11AsKf8aRqtnBqQoG91oWkGOS0/XaQo2Pf3u5mUDK9LukD7Mv5Tv9teSQ4VzipsINUtW9Zc" + + "t/mFiRu7WbcOuQNP+MXQ4hGX3mEKBl1mjB9bbwAqSz6cf+TZ8Qaabta/u6hM92ItpZs5dvyor5R/" + + "dwvp9QAa6eFzfxRlpVMk2mXh93czeyPn1Bn5ShWtYAJsyEve+OPgC7Hzmgx3USDtejQedlbtDX7h" + + "0Ns6HChV5LcvP7rpb1+qx/690dHrtewL05c2c7ZLtrM91fOpDGjXyvT9+WYBPQAg3NPcey1n4vVt" + + "FUJSIfGNjJZNy2ekkqzpazIJOefSoTaA9q1VY+5Wbvs9NAoYVBkFh5Sesi9lJ/u6lt5+WETpoi2M" + + "PpZU/k9szmKGtVGRWBjQ6g3zP78pxfSGKb+tJ4LPAsi31S/+uXCUlVZmCIc+DlI15L4Cpr/1FA1d" + + "0VLqAilzgcCGChdQc5eoTXqpkNS66hv1YLsUElURiG1sOZj7lunf3v3fwlBKjRfX9EjEHKcscV98" + + "D40zRKIqgEpz4yvTVnfjU/VbmL/r4yhwTTbPCNsZNi8g50/OnvbCsXu5wQqVURCBuOb7seu98n7A" + + "/L23Tc8NX8mW6pL73UoOhYPH/GJv/I7Dzlqbg5pRUG1q++A//+Ng+4f9gDlATVzLHfErZiHioKrn" + + "H37uhgeG597sdYnIYeeszypQqQawre9dHNbd0Yj9/5KnfsB8DJpuXXj8Q+ryj3dUZglD1Uz3MsWv" + + "HX7uh1fv6QGHn7upAmrWQpEV2zSt+bVptamw+6C9VaP/hcoHrvkABgydUjPLywy6Oboh6HW6PgLj" + + "LYqStqYRQHKDMQflMhXOQrnata27tvGvufrEn8ZBfmdPP2AO7NpmAAw85B8qTyjKlt1svAHTjPGL" + + "k4w0jAcTAyllnBoh9Kxw/tEdS8cuT0WyH4vX1PYD5qMBzQDE2eFDxz09zsscWuwVHX6a8YwaFAiM" + + "NAkHr4vdUdf82rQN6JwnSl4N4vAxeKdxP2A+mjXuKTvcXcY9TdOnyxPk4zKZ/vbRAqe75C3QfZZY" + + "0P/y6/7299z+H4QrdGsoib8JAAAAAElFTkSuQmCC" + }, + "DuckDuckGo": { + image: "data:image/png;base64," + + "iVBORw0KGgoAAAANSUhEUgAAAIwAAAA4CAYAAAAvmxBdAAAVhUlEQVR4Xu3dd5SU1d3A8e/vPs/0" + + "2crussBSdkHEAgoomEQSUTAW3hRbfMUeUwgSj9FoorGXqDGxBHvMazRGE0KsBQuiEVRUEEEM0pfO" + + "1tndmZ32PPf3knDCUZAlIYsxOfM553f2v91/vnufOzP33BFV5TOnQFQ1snFN/YCVb88Z6S2dd1B8" + + "3Qf7lTSv6R9PNle4uXQEVNRxvUy4qL29pPeGRNXA5d6g4fOLhoyYN2C/oe8Vl5QmAoFAnm72GQqm" + + "oKO9vXj5e/NHtr48/fjq92eOq2xYOsixvuMpKFuhfJywjQMYI5oKF7evrR09t/LE7z3Ze9TYZyPx" + + "+FpjjPdfEkxBY0ND9ftP//7EkpceOLNm/cJh+J6rylYWcIwSiCHhuEo4ggRdMCLq+UomK5pJq2Y7" + + "BD8HqoIAAmKhPdKjuX7EMc9WnfCde/YZOfot13Xz/6HBFKi1pdmlCya23Dz5PPeDN/eygCqAqIn3" + + "ULduiAb2Ha3BfUYgJeUgBhxHRAwgoupbfF/wPcXL461bRX7xm5Jb8q7Yhno0lzUYMIANx9Lh0y99" + + "svjEc292YkXzAfufE0yBse0tX+qY+uNrOp/+9SGo5yggTlADQw72I4efQGDf4Wg6RW7xO5Jf8g7+" + + "ulVi21rRXAr8HKpWRBzFCSGRIpyKSnX6701wv0PU7Vunms2RmfO0ZGc/Z/zWjSKiAqJOdV1LyUVT" + + "7wkdcuQvENP8mQ+mQGPZt2ZelLj2nCl+Q30ZAqijoVFH+rGTJiHROJnXniE75znxN64yms8AKghd" + + "062DEZVIqQbq9tHwYcdpcL+DNDvvFUlNv1dsywYHA0jAjx512lslF956vkSL5n5Wgymwfq+O/7vx" + + "jvZfX/0/+FkXC27N3n7xlOvVlFdp8pFfSnbuC0bTbYKqIOw+BcSoKeut0WNPtZEjjtPOx++X1FMP" + + "GPysAXD777epxy1PXuj2qXsEsJ+hYArUy9e2Xn7GtPTLj44AFVVHY1/7tld0+g8l+cht2vnE/Y7N" + + "p0S2htJ9FEDUlPWxxZOusE5VjSRunIK3YbkrAhIpzlRMfeGy4P6jbwH8z0AwBZrPDWqacvQzmfkv" + + "D0ZETbxCS3/wC9/t1ZeWq78t3oZlDqiwp6nRyJiveMXnXEL7fdeTef1JV9UKKlp118wrQgeNvX5X" + + "0Rj2uMJjqOmik/6UmbclFkSdylrb4/qHfU0naTzvK463fqkLKijo1oGt0/3ESudrT7jNPznTxL8x" + + "iehXvuUhroJKw6RxV+aWzJ8MyL9vhSmIJm778fT2h244CiPqVg+0Pa64TzPzZtv2X18XUD8jAIiB" + + "3nWEK6rBDaHZTmyiCb+lGe1MoGpB6FZOWR+/7KJbbXb+n0lOv8tV64mJlnX2mr74ZKei11PshMue" + + "UmA6X3nyqrbf/uxIAKe4l5ZdcqdNz5vNllhc9TKCAIAaQ6puNLEzzqN86EhQRTs78BvWkX3/bTpf" + + "mkZm3p/RbAoM3cJrWe+03PB9yn881drOlJd85gHXT7VGG77/1TvK7n1pRThe/MGnuMIU+M2bj91w" + + "wrBHbUdDnEDUVlx2n29TbbT8/AIXLy18hAQiFJ8wmdD44wnvPwoxZvs9ENlFb9D2qxvIzH0BxNId" + + "VMGtGuBXXPNrm7j7OskueNkBKDnjkudKp1x7ItD5KQRToNavaLzgGy91vjr9ABAtPuUCL/LFo2m8" + + "8ETHJlsMwsek9zqEztMvRbw8TjBMqLSU4spKiquqicVjiAgANtVBx8O3kbjvOtTPgPCvUwjufZBX" + + "ftEt2njBScZv2+gYN5KvfvCN84N7H3DHpxBMQerNmZc3nHvU5ajnBGqHedW3Psam848jv+I9F2FH" + + "4qA4gIJvkHgZgeGHEvzSUZSMP4FQccnHVpvk0w+Seu73ZN57Hc11guFfo6JFX/+uFzpgNE1XnOUi" + + "KpEDvriy4p4XxrrB0Jo9GExB0+bNtanvjX/VX7mor6jR6rtmeOk3ZpJ46CZXRKWrx4MTK6fkrB8S" + + "n3AqTnkVuAFEgO0qU1Xw8ngbVpO462o6ZjyCGMu/RB3tOfUZr+03t5B5+/kAIhq7/g8/rTrqhEv3" + + "YDAFCx+889qiWyZfahVihx2fL598haw7ebRRmzbshCgEBgyj+rY/Eui/F/8UVVp+eTmt918HRvlX" + + "hOqGexWX3q4bvn2kg582nZW1awc9vuhL4Whs1R4IpqC1ubnXhm8d/mp45cK9cEK29/0v+22P3Elq" + + "xsMBhJ3Ssj7U/OYVwv0GsTvU99h03nGkXnsKEXabqqNVV96b75z9vCRf+kPAEWi5+P4fjvzfs2/e" + + "Ay+rC96f9fzYPqsX11mF2EGH+yYal9TMJ4wCKJ9ILAQmXbXbsWSyeVLpPGUX3ULm3Tfxk43sNrG0" + + "/eE+Uz7pMk29/Li1Nmeyj917QsexJ9xbVFzcDmDoFgWe5wWysx7/mvq+o1Y0NuEUOp6bpjaXEgV2" + + "Nuke/Sg6+n8B8H3LklWNzJq7gtXrW7BW6UpzopN7fj+X+6bNZdqCNuKnnof6oOzmqEr2w/cc9fMa" + + "2OsAtQoVq947YPVfFu/XzStMQWtTU1WPJXNHWwWnR28bHjZKWu+9AUVFlE+mkDxoPEXxCNYq055f" + + "yKamJGNHD0REUFVA2JlgwOGbJxxMLBKkrSNDONWTjkfvxG/dwO6yXobO2TMl+sVjNPPBO+pmM+FV" + + "s18cP3T0597oxmAKNqxYtm9R07oaayG0/0HqNW4mt26Vg4LyycSD7N6jcIFM3iMWDTH5lKEEXId/" + + "RFEsxN+VFkfQWDXxcceReHQqGHaPqnS+NctUXnyzlUBIfS8jzvzXxnieF3ZdN+PSLQo6PlhwcMxa" + + "Y30IH/h5Mu+/o9bLsCu58l4AhIMuR4/ZG9cx/LNS6RwbGzuorSkjfuTxtP7hLsBntwjkNq0T9TxM" + + "RV/1Ni2jdPUH+3q5XNFfgzF0hwLHXfmXA3wFcRwN7zuC9HvviKqC0uXkjYsCIrItlpa2TmbM/pCV" + + "a5tR1a5DTWWZ+MNHuPTWGbwwZxnBQfvi9hwAym6PptvFb20kWDsQtRBNbO6ZSyX7dNcjqUA1HG9a" + + "308VJF6qblVvydUvQa2KCjtlFGwqScazRAMOAIn2NOdc9kfqN7Ry8jEHcvyRQ6mrKWdn1m5KsHJd" + + "C9Fw4G97oKMO+SrBQUPIbVgBwu5RJbP8Qwn03UvVn4FR39H21kFUVi0wdIeCYDjRWKkKpqiHqlr1" + + "WpsEdvGfDLgNa2nPeADbVpctEeD7lufnLGXpqka6MnhAJRMnDKdf7zLO/NpIxA0QqKlF7XZ/a+uA" + + "bB0UdGcrjKrkN9QT6N0fFVEVcFJt3bXCFKiq6zdtKlYFJxoDL49NZ1GlawLRVYtozfhUFwFA76pi" + + "vvyFvXnpjWVUlcU4aP8auuI6hovPOQxVRUQAMOE4WFC2MmEI9YaiUUJ0X0F9yKyGxIuW3AZA+DgF" + + "v61ZnPJKRQEFL9FS3k3BFAjq4uWCqkAoiFormvdF6ZoKRFcupjnt8XfhUIDLJx3BN48/mMqyGPFY" + + "iF1jWyyqis21E6iGyF5CdD8hMkQI9gYJCFgAiB6oaN7Q8LAFYQeay6iJRFQFVMHx8+HuC6ZAsCoA" + + "iICqKICyS6H1S9mcaEf7Fm1bIYJBl9qacrqm4DWguTWgafDbIL8O0u9R/qWn6HGEgxMTAFC2soAB" + + "P6G0zrS0PKEggPIxqqBWQURQUO3mE3cF4uG6nirYnAeOYzGOURB2wSTb8NavJrNPLyIBh11jayTN" + + "v0TbHgevETQHeKAWALcYQEDZSkBEyayDtlmWttlKvpGthE8WDInN5nRbLMZ43RdMgS/hWEqh3E+m" + + "RNygEgqqtrNrCsFlC2g79OBdB6OKpl5G10+C7CpAQYRtRPgYB/x2JTlfScxSUksUzW4XirIDtWDi" + + "ZeolWrEWACQUaeuuYApEck5JeTNKX789gRhHnJJS8pvXIkKX1ED0w3m0ZM+muoguaXYxWj8R/CYQ" + + "AQSskmsCJw5OVEDA71BSi5S217b+9FOg2/ekXUcc6NmX/MZ1YFUQcGJFm7ormAIh41b1Wm+VAzXZ" + + "gteR0GDNYNJL39cthF0IL1tIUzIPFXStcy74jSAGAFWl/lpLxzuKBMCJAgb8JKgHOHyMKv8QMUZD" + + "g4aQnPMiKoCIOqU9VnZbMAWSD9UN+QDlWJvJSeYv7xMeOpzEzD8h7Fpw43Kam5rw+xXjGGGnIsPB" + + "REHTgGDTkF6tqANY8JJsgwEUAJSPPL0EULoWjGmgujfp5R8KgImVtG0JZhWAoVsUlIz/2jtqRUGl" + + "8903NDb8EMSEUNjlmM40/pplpHIeXZHwUKTHZMAFwIkJ1acZghWAgNqPjAIGnDhE66DHl4Wacw0D" + + "LjGE+8FOP7VQcCur1cSKNbe+XhSIjfjCMhONd+cepiBYO/hdU1TW6idbyjvemWuqzv2JBqr62OzG" + + "FQ67oh7BD9+l/YjDKA4H2CkJID0vJ1OfQJvvI1QjlI8zFB0sZJYr2U3gd4I44JZAsEoI9gS3FCQo" + + "CEpmDXgZ2PnLftkS+xc0/eH7+Ml2wUB05Ji54jipbgymwEQi6yNDhi1Mvv3KYdk1SyW3ZqUWjz3G" + + "Njw81QgqdEFVCS9ZQFPGUlNC10yUxBt9aLjXEttHKB4txIcKsf3lb+GgoApYthLAQm6j0vqK0vSs" + + "Jd8CIjuPsnjcMdoy7TeiqBjj+LERh7wIaDcGUyCO27klkGc7tgSDlzctT/7eVpx8Ng2/uwfVHLsS" + + "Wv0+ifYUWhVBROiKWh8vBe3v6t/GhCHYE6IDhUidEKoGEwIvCZl6SP1F6Vyh+B2AbB1lRyiEB+zl" + + "B/v0p+PtOQaBQJ8BqyN77/c2QDcHU1AybsLTm35184Vec0NVYsbjUn3uj6Ro9OFe++szAghdcho3" + + "0LlpI7naHoRcoStueSXKNvgZSK+GzlWKiO74ASMg0vV7LwCqRstPPlsTzz2Gl2wTMVB82DHPumXl" + + "mwvXfewB6vvO6h+c/mDLE787Ra1or8mXeMWHHcmHJx3uiPiGLqgE2XTlg3z+xK9THg3SlbZZM1h+" + + "1gTApzsFq+u8QQ8+ydKTxomX2OSYaFHH4N++OD42YvTcPbDCFIjj+JWnn3tX2ysvTMgnmoo3P3CH" + + "6XHyWfT46kS/6YmHBFTYCdEcgSXvksh+lfIoXQrVDsKUVOIlNrGdrhaRrlmjvS66yjb+7n7JNW9y" + + "cUR7njFlRmz4qPl78H6YgtiBo96s/t4lz6iKesmEs/6Gy2yvC66QQGU/q12djbEQWrqI5lSOXa8E" + + "fQgP2ptP+n1N8SCpoPPPnbBT0dIj/icfrhssmx+611GBQGXftupvnX8bIvk9G0xhlfGqTv/2jZEB" + + "+zQAND89zU0teFv7Xn6TlUDUdtEMwbVLaG9N4FslmW+gKbOGjN+5wzFNE45QPGY8WFAAC4niEHdM" + + "GMjJU0bw4Ji+GPsP9qIQqq6zfS6+Rtb85HzRXMqAY/v+6PpH3PKKN9mOc+WVV9K9CiQQ3Bzdd1iw" + + "afrDX1LNO8m359LzrO+pW1yh7W+/blAr7AjJWzoOPZaaAX2Yu/lWHls1ldc2z2VjOklJsILiQBwR" + + "wVefXDRAy1N/gnyWv4yu4s4zhzCztox2DAIctaABlF1y4mW29md32y2bdJqfneYCUnzI4cv6XnrD" + + "d8SYxKd1e0OBaqz+yose23j/z8cBFA3/gjfw9l/Lxjt+rg2P/soFX9iBQ+OP7mTUWWeyoOkaXtv0" + + "KqtTsDxpSfoVfLn34YzoU8bsxnksb23EeWMxxwRyvDGigqVJWJ5U2vLQvznNA3cuIJLz6YqEiuyA" + + "a27x1fOov+J8x+bTxo2Xdw6btfDUYK8+j32aN1AViKT6/eS6ye1zXn45tWR+Tce7r7v1V/zQ73/N" + + "L0R9z2+Y9oCzQzTWx/1wEa1pH8SwlWDE0JBp5oHVv2eB+jQnhdaUoWNQnIE1LmQUUP4uHzDkHEOY" + + "nQSjYCJFtt9lN/kmFmflxZMdm0sbxbGDpj50+5ZYngT49IMpPJqW7TP9pVPf/fy+T3qJTcUtM59y" + + "FPEGXHuLOOUV3oZ7fuGieeEjgsvfo7WjE9cN8FECOI5gEEQEgJyFVF7ZnhXBIqiyA1UIlFb5tdff" + + "ZlFY+aMpjt/ebFSh/yU/nV467pgrAf/fdItmgVtS9uqwF98620TK0mCl5aUn3OWTT6dq4tky8Of3" + + "eSZSZlXZJrC+nmRTC0aibE/4OFVFAWv4GMcqxirbUysaG3yAN+S3T2i+sYHlF37H8doajSr0Ovv7" + + "s/qce+E5QPbffO1qQah33+kH/nnhaYHKfq2qKm3vvOYu/to43LIKhr0415aOOTpvNaBWwSSayNav" + + "QrR0hzhcP86g6H4MjNUyuuJArjrwO9w06hGOesWl3+oOgr5iBEpSecJZH2vZOiqKG7N9Jl3k7f2b" + + "P7Hp/+7RlZed7/rpdqM4ts+5lz5be+2txyHS/hm62Lkg39x05AenfOWejoVv9hdUkIBWTzzHqznv" + + "YumYN1fX//JnJvXBItNy7k8lftpgZm28iRVJZXM2yoiKcXx3yERqi3qxvaY/Pcqyb09kc0WQRf3i" + + "lKY8Rq5IYBF1wnFKDxtva6ZcaHONTdRffZF0Ll/iYsAEI/m6a29/qPq0b56/LZbPVjAFNpMeuvrK" + + "i2/f+ODdY9TmHXwI1dT6vSedpz3GHyvJhfN1VUMSjhljFrb/UuLBfeRzPY+hX7w/O2PzORYePYbk" + + "orcQFRXXJVBdo+Vjj7QVx5+MuAHZcPdt2vTsYw54gkKopq55yN2/vano4M/dBmQBPqvBFKiWtc56" + + "4YJlF3x3Unb96nIEUKOR2sG28usnafmErxOoHUwwGkLEiCDCNgg70paXnmPNjVdr0fCRWjJmLOEB" + + "daRXraDxj7+j9dUXjc2kBFTEuH7VSWfOrbvqpkvc0rI/Awrw2Q+mwPgdHaPX3X3rj9dNvfEom0kF" + + "VAEVdYvLtGjoAVo85ggtGf05CfcbqMGqKjGhMB9pRwEBUN/Ha23R9OrlZFatlMRrL2v73NclXb/C" + + "qJ8XMQCyJaZD1g687hdTi0aMvh+Rlv/AL9gq0Hw+3PbWnMPX3n7jlLY5s8baXDYEgIIiagIh3NIe" + + "Gqqq1EBVb9zyCtxoXDFGbT5n/PaE5ho2mtzmjeSbW/A720R9X8SwTbimf33Pb5zxUO9vTv5VoKKq" + + "/r/gK/wKbDYTTi1eNHTzH393SvPzT0/IrF5Zp2KNCFtpF8cqBba/ndVEYqmKCcfP6Xn8xEeLRx78" + + "rFtS2oCIAvx3BVMgms/H8q3N+zc9/cTYphlPf/6vIWU3ru+jnufySUTULSpujwzca9mWPcy8skMP" + + "e6Xkc4fODlb32iyOk6cb/T/N+faHj8AX2gAAAABJRU5ErkJggg==" + } +}; + +// This global tracks if the page has been set up before, to prevent double inits +let gInitialized = false; +let gObserver = new MutationObserver(function (mutations) { + for (let mutation of mutations) { + if (mutation.attributeName == "searchEngineURL") { + setupSearchEngine(); + if (!gInitialized) { + gInitialized = true; + } + return; + } + } +}); + +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 }); + fitToWidth(); + window.addEventListener("resize", fitToWidth); +}); + +window.addEventListener("pagehide", function() { + window.gObserver.disconnect(); + window.removeEventListener("resize", fitToWidth); +}); + +function onSearchSubmit(aEvent) +{ + let searchTerms = document.getElementById("searchText").value; + let searchURL = document.documentElement.getAttribute("searchEngineURL"); + + if (searchURL && searchTerms.length > 0) { + // Send an event that a search was performed. This was originally + // added so Firefox Health Report could record that a search from + // about:home had occurred. + let engineName = document.documentElement.getAttribute("searchEngineName"); + let event = new CustomEvent("AboutHomeSearchEvent", {detail: engineName}); + document.dispatchEvent(event); + + const SEARCH_TOKEN = "_searchTerms_"; + let searchPostData = document.documentElement.getAttribute("searchEnginePostData"); + if (searchPostData) { + // Check if a post form already exists. If so, remove it. + const POST_FORM_NAME = "searchFormPost"; + let form = document.forms[POST_FORM_NAME]; + if (form) { + form.parentNode.removeChild(form); + } + + // Create a new post form. + form = document.body.appendChild(document.createElement("form")); + form.setAttribute("name", POST_FORM_NAME); + // Set the URL to submit the form to. + form.setAttribute("action", searchURL.replace(SEARCH_TOKEN, searchTerms)); + form.setAttribute("method", "post"); + + // Create new <input type=hidden> elements for search param. + searchPostData = searchPostData.split("&"); + for (let postVar of searchPostData) { + let [name, value] = postVar.split("="); + if (value == SEARCH_TOKEN) { + value = searchTerms; + } + let input = document.createElement("input"); + input.setAttribute("type", "hidden"); + input.setAttribute("name", name); + input.setAttribute("value", value); + form.appendChild(input); + } + // Submit the form. + form.submit(); + } else { + searchURL = searchURL.replace(SEARCH_TOKEN, encodeURIComponent(searchTerms)); + window.location.href = searchURL; + } + } + + aEvent.preventDefault(); +} + + +function setupSearchEngine() +{ + // 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. + let searchText = document.getElementById("searchText"); + searchText.addEventListener("blur", function searchText_onBlur() { + searchText.removeEventListener("blur", searchText_onBlur); + searchText.removeAttribute("autofocus"); + }); + + let searchEngineName = document.documentElement.getAttribute("searchEngineName"); + let searchEngineInfo = SEARCH_ENGINES[searchEngineName]; + let logoElt = document.getElementById("searchEngineLogo"); + + // Add search engine logo. + if (searchEngineInfo && searchEngineInfo.image) { + logoElt.parentNode.hidden = false; + logoElt.src = searchEngineInfo.image; + logoElt.alt = searchEngineName; + searchText.placeholder = ""; + } + else { + logoElt.parentNode.hidden = true; + searchText.placeholder = searchEngineName; + } + +} + +function fitToWidth() { + if (window.scrollMaxX) { + document.body.setAttribute("narrow", "true"); + } else if (document.body.hasAttribute("narrow")) { + document.body.removeAttribute("narrow"); + fitToWidth(); + } +} diff --git a/application/palemoon/base/content/abouthome/aboutHome.xhtml b/application/palemoon/base/content/abouthome/aboutHome.xhtml new file mode 100644 index 0000000000..cb3fa634a6 --- /dev/null +++ b/application/palemoon/base/content/abouthome/aboutHome.xhtml @@ -0,0 +1,60 @@ +<?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/abouthome/aboutHome.css"/> + + <script type="text/javascript;version=1.8" + src="chrome://browser/content/abouthome/aboutHome.js"/> + </head> + + <body dir="&locale.dir;"> + <div class="spacer"/> + <div id="topSection"> + <div id="brandLogo"></div> + + <div id="searchContainer"> + <form name="searchForm" id="searchForm" onsubmit="onSearchSubmit(event)"> + <div id="searchLogoContainer"><img id="searchEngineLogo"/></div> + <input type="text" name="q" value="" id="searchText" maxlength="256" + autofocus="autofocus"/> + <input id="searchSubmit" type="submit" value="&abouthome.searchEngineButton.label;"/> + </form> + </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> + <button class="launchButton" id="settings">&abouthome.settingsButton.label;</button> + <div id="restorePreviousSessionSeparator"/> + <button class="launchButton" id="restorePreviousSession">&historyRestoreLastSession.label;</button> + </div> + </body> +</html> diff --git a/application/palemoon/base/content/abouthome/addons.png b/application/palemoon/base/content/abouthome/addons.png Binary files differnew file mode 100644 index 0000000000..41519ce498 --- /dev/null +++ b/application/palemoon/base/content/abouthome/addons.png diff --git a/application/palemoon/base/content/abouthome/addons@2x.png b/application/palemoon/base/content/abouthome/addons@2x.png Binary files differnew file mode 100644 index 0000000000..d4d04ee8ca --- /dev/null +++ b/application/palemoon/base/content/abouthome/addons@2x.png diff --git a/application/palemoon/base/content/abouthome/bookmarks.png b/application/palemoon/base/content/abouthome/bookmarks.png Binary files differnew file mode 100644 index 0000000000..5c7e194a61 --- /dev/null +++ b/application/palemoon/base/content/abouthome/bookmarks.png diff --git a/application/palemoon/base/content/abouthome/bookmarks@2x.png b/application/palemoon/base/content/abouthome/bookmarks@2x.png Binary files differnew file mode 100644 index 0000000000..7ede00744c --- /dev/null +++ b/application/palemoon/base/content/abouthome/bookmarks@2x.png diff --git a/application/palemoon/base/content/abouthome/downloads.png b/application/palemoon/base/content/abouthome/downloads.png Binary files differnew file mode 100644 index 0000000000..3d4d10e7ab --- /dev/null +++ b/application/palemoon/base/content/abouthome/downloads.png diff --git a/application/palemoon/base/content/abouthome/downloads@2x.png b/application/palemoon/base/content/abouthome/downloads@2x.png Binary files differnew file mode 100644 index 0000000000..d384a22c6d --- /dev/null +++ b/application/palemoon/base/content/abouthome/downloads@2x.png diff --git a/application/palemoon/base/content/abouthome/history.png b/application/palemoon/base/content/abouthome/history.png Binary files differnew file mode 100644 index 0000000000..ae742b1aa8 --- /dev/null +++ b/application/palemoon/base/content/abouthome/history.png diff --git a/application/palemoon/base/content/abouthome/history@2x.png b/application/palemoon/base/content/abouthome/history@2x.png Binary files differnew file mode 100644 index 0000000000..696902e7ca --- /dev/null +++ b/application/palemoon/base/content/abouthome/history@2x.png diff --git a/application/palemoon/base/content/abouthome/noise.png b/application/palemoon/base/content/abouthome/noise.png Binary files differnew file mode 100644 index 0000000000..3467cf4d47 --- /dev/null +++ b/application/palemoon/base/content/abouthome/noise.png diff --git a/application/palemoon/base/content/abouthome/restore-large.png b/application/palemoon/base/content/abouthome/restore-large.png Binary files differnew file mode 100644 index 0000000000..ef593e6e14 --- /dev/null +++ b/application/palemoon/base/content/abouthome/restore-large.png diff --git a/application/palemoon/base/content/abouthome/restore-large@2x.png b/application/palemoon/base/content/abouthome/restore-large@2x.png Binary files differnew file mode 100644 index 0000000000..d5c71d0b04 --- /dev/null +++ b/application/palemoon/base/content/abouthome/restore-large@2x.png diff --git a/application/palemoon/base/content/abouthome/restore.png b/application/palemoon/base/content/abouthome/restore.png Binary files differnew file mode 100644 index 0000000000..5c3d6f4376 --- /dev/null +++ b/application/palemoon/base/content/abouthome/restore.png diff --git a/application/palemoon/base/content/abouthome/restore@2x.png b/application/palemoon/base/content/abouthome/restore@2x.png Binary files differnew file mode 100644 index 0000000000..5acb630522 --- /dev/null +++ b/application/palemoon/base/content/abouthome/restore@2x.png diff --git a/application/palemoon/base/content/abouthome/settings.png b/application/palemoon/base/content/abouthome/settings.png Binary files differnew file mode 100644 index 0000000000..4b0c309909 --- /dev/null +++ b/application/palemoon/base/content/abouthome/settings.png diff --git a/application/palemoon/base/content/abouthome/settings@2x.png b/application/palemoon/base/content/abouthome/settings@2x.png Binary files differnew file mode 100644 index 0000000000..c77cb9a92c --- /dev/null +++ b/application/palemoon/base/content/abouthome/settings@2x.png diff --git a/application/palemoon/base/content/abouthome/snippet1.png b/application/palemoon/base/content/abouthome/snippet1.png Binary files differnew file mode 100644 index 0000000000..ce2ec55c23 --- /dev/null +++ b/application/palemoon/base/content/abouthome/snippet1.png diff --git a/application/palemoon/base/content/abouthome/snippet1@2x.png b/application/palemoon/base/content/abouthome/snippet1@2x.png Binary files differnew file mode 100644 index 0000000000..f57cd0a827 --- /dev/null +++ b/application/palemoon/base/content/abouthome/snippet1@2x.png diff --git a/application/palemoon/base/content/abouthome/snippet2.png b/application/palemoon/base/content/abouthome/snippet2.png Binary files differnew file mode 100644 index 0000000000..e0724fb6df --- /dev/null +++ b/application/palemoon/base/content/abouthome/snippet2.png diff --git a/application/palemoon/base/content/abouthome/snippet2@2x.png b/application/palemoon/base/content/abouthome/snippet2@2x.png Binary files differnew file mode 100644 index 0000000000..40577f52f6 --- /dev/null +++ b/application/palemoon/base/content/abouthome/snippet2@2x.png diff --git a/application/palemoon/base/content/abouthome/sync.png b/application/palemoon/base/content/abouthome/sync.png Binary files differnew file mode 100644 index 0000000000..11e40cc937 --- /dev/null +++ b/application/palemoon/base/content/abouthome/sync.png diff --git a/application/palemoon/base/content/abouthome/sync@2x.png b/application/palemoon/base/content/abouthome/sync@2x.png Binary files differnew file mode 100644 index 0000000000..6354f5bf90 --- /dev/null +++ b/application/palemoon/base/content/abouthome/sync@2x.png diff --git a/application/palemoon/base/content/autorecovery.js b/application/palemoon/base/content/autorecovery.js new file mode 100644 index 0000000000..29ccaed3fe --- /dev/null +++ b/application/palemoon/base/content/autorecovery.js @@ -0,0 +1,60 @@ +/* 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/. */ + +/* Auto-recovery module. + * This module aims to catch fatal browser initialization errors and either + * automatically correct likely causes from them, or automatically restarting + * the browser in safe mode. This is hooked into the browser's "onload" + * event because it can be assumed that at that point, everything must + * have been properly initialized already. + */ + +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; + +// Services = object with smart getters for common XPCOM services +Cu.import("resource://gre/modules/Services.jsm"); + +var browser_autoRecovery = +{ + onLoad: function() { + + var nsIAS = Ci.nsIAppStartup; // Application startup interface + + if (typeof gBrowser === "undefined") { + // gBrowser should always be defined at this point, but if it is not, then most likely + // it is due to an incompatible or outdated language pack being installed and selected. + // In this case, we reset "general.useragent.locale" to try to recover browser startup. + if (Services.prefs.prefHasUserValue("general.useragent.locale")) { + // Restart automatically in en-US. + Services.prefs.clearUserPref("general.useragent.locale"); + Cc["@mozilla.org/toolkit/app-startup;1"].getService(nsIAS).quit(nsIAS.eRestart | nsIAS.eAttemptQuit); + } else if (!Services.appinfo.inSafeMode) { + // gBrowser isn't defined, and we're not using a custom locale. Most likely + // a user-installed add-on causes issues here, so we restart in Safe Mode. + let RISM = Services.prompt.confirm(null, "Error", + "The Browser didn't start properly!\n"+ + "This is usually caused by an add-on or misconfiguration.\n\n"+ + "Restart in Safe Mode?"); + if (RISM) { + Cc["@mozilla.org/toolkit/app-startup;1"].getService(nsIAS).restartInSafeMode(nsIAS.eRestart | nsIAS.eAttemptQuit); + } else { + // Force quit application + Cc["@mozilla.org/toolkit/app-startup;1"].getService(nsIAS).quit(nsIAS.eForceQuit); + } + } + // Something else caused this issue and we're already in Safe Mode, so we return + // without doing anything else, and let normal error handling take place. + return; + } // gBrowser undefined + + // Other checks than gBrowser undefined can go here! + + // Remove our listener, since we don't want this to fire on every load. + window.removeEventListener("load", browser_autoRecovery.onLoad, false); + } +}; + +window.addEventListener("load", browser_autoRecovery.onLoad, false); diff --git a/application/palemoon/base/content/autorecovery.xul b/application/palemoon/base/content/autorecovery.xul new file mode 100644 index 0000000000..866bdf288b --- /dev/null +++ b/application/palemoon/base/content/autorecovery.xul @@ -0,0 +1,12 @@ +<?xml version="1.0"?> + +<overlay + id="autorecovery" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/x-javascript" src="chrome://browser/content/autorecovery.js"/> + +<!-- This is an empty overlay to allow separation of the script into its + own context (needed for locale issues preventing browser start) --> + +</overlay> diff --git a/application/palemoon/base/content/baseMenuOverlay.xul b/application/palemoon/base/content/baseMenuOverlay.xul new file mode 100644 index 0000000000..c6c1b16dc7 --- /dev/null +++ b/application/palemoon/base/content/baseMenuOverlay.xul @@ -0,0 +1,100 @@ +<?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="&hideThisAppCmdMac.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="&productHelp.label;" + accesskey="&productHelp.accesskey;" +#ifdef XP_MACOSX + key="key_openHelpMac"/> +#else + /> +#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;" + oncommand="restart(true);"/> + <menuseparator id="aboutSeparator"/> + <menuitem id="aboutName" + accesskey="&aboutProduct.accesskey;" + label="&aboutProduct.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="&hideThisAppCmdMac.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/palemoon/base/content/blockedSite.xhtml b/application/palemoon/base/content/blockedSite.xhtml new file mode 100644 index 0000000000..b56875eb6c --- /dev/null +++ b/application/palemoon/base/content/blockedSite.xhtml @@ -0,0 +1,193 @@ +<?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://global/skin/netError.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 + + // 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; + } + + /** + * 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() + { + // Handoff to the appropriate initializer, based on error code + switch (getErrorCode()) { + case "malwareBlocked" : + initPage_malware(); + break; + case "phishingBlocked" : + initPage_phishing(); + break; + } + } + + /** + * Initialize custom strings and functionality for blocked malware case + */ + function initPage_malware() + { + // Remove phishing strings + var 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); + + // Set sitename + document.getElementById("malware_sitename").textContent = getHostString(); + document.title = document.getElementById("errorTitleText_malware") + .innerHTML; + } + + /** + * Initialize custom strings and functionality for blocked phishing case + */ + function initPage_phishing() + { + // Remove malware strings + var 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); + + // Set sitename + document.getElementById("phishing_sitename").textContent = getHostString(); + document.title = document.getElementById("errorTitleText_phishing") + .innerHTML; + } + ]]></script> + <style type="text/css"> + /* Style warning button to look like a small text link in the + bottom right. This is preferable to just using a text link + since there is already a mechanism in browser.js for trapping + oncommand events from unprivileged chrome pages (BrowserOnCommand).*/ + #ignoreWarningButton { + -moz-appearance: none; + background: transparent; + border: none; + color: white; /* Hard coded because netError.css forces this page's background to dark red */ + text-decoration: underline; + margin: 0; + padding: 0; + position: relative; + top: 23px; + left: 20px; + font-size: smaller; + } + + #ignoreWarning { + text-align: right; + } + </style> + </head> + + <body dir="&locale.dir;"> + <div id="errorPageContainer"> + + <!-- Error Title --> + <div id="errorTitle"> + <h1 id="errorTitleText_phishing">&safeb.blocked.phishingPage.title;</h1> + <h1 id="errorTitleText_malware">&safeb.blocked.malwarePage.title;</h1> + </div> + + <div id="errorLongContent"> + + <!-- Short Description --> + <div id="errorShortDesc"> + <p id="errorShortDescText_phishing">&safeb.blocked.phishingPage.shortDesc;</p> + <p id="errorShortDescText_malware">&safeb.blocked.malwarePage.shortDesc;</p> + </div> + + <!-- Long Description --> + <div id="errorLongDesc"> + <p id="errorLongDescText_phishing">&safeb.blocked.phishingPage.longDesc;</p> + <p id="errorLongDescText_malware">&safeb.blocked.malwarePage.longDesc;</p> + </div> + + <!-- Action buttons --> + <div id="buttons"> + <!-- Commands handled in browser.js --> + <button id="getMeOutButton">&safeb.palm.accept.label;</button> + <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/palemoon/base/content/browser-addons.js b/application/palemoon/base/content/browser-addons.js new file mode 100644 index 0000000000..7993a0c9c9 --- /dev/null +++ b/application/palemoon/base/content/browser-addons.js @@ -0,0 +1,536 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// 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; + }, + + 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 = { + 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("xpinstallPromptWarningOrigin", + [brandShortName]); + + let popup = PopupNotifications.show(browser, notificationID, + messageString, anchorID, + null, null, options); + removeNotificationOnEnd(popup, installInfo.installs); + break; } + case "addon-install-blocked": + let originatingHost; + try { + originatingHost = installInfo.originatingURI.host; + } catch (ex) { + // Need to deal with missing originatingURI and with about:/data: URIs more gracefully, + // see bug 1063418 - but for now, bail: + return; + } + messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarning", + [brandShortName, originatingHost]); + + action = { + label: gNavigatorBundle.getString("xpinstallPromptAllowButton"), + accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"), + callback: function() { + installInfo.install(); + } + }; + + let popup = PopupNotifications.show(browser, notificationID, messageString, + anchorID, action, null, options); + removeNotificationOnEnd(popup, installInfo.installs); + break; + case "addon-install-started": + var 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("addonDownloading"); + messageString = PluralForm.get(installInfo.installs.length, messageString); + options.installs = installInfo.installs; + options.contentWindow = browser.contentWindow; + options.sourceURI = browser.currentURI; + options.eventCallback = function(aEvent) { + if (aEvent != "removed") + return; + options.contentWindow = null; + options.sourceURI = null; + }; + PopupNotifications.show(browser, notificationID, messageString, anchorID, + null, null, options); + break; + case "addon-install-failed": + // TODO This isn't terribly ideal for the multiple failure case + for (let install of installInfo.installs) { + let host = (installInfo.originatingURI instanceof Ci.nsIStandardURL) && + installInfo.originatingURI.host; + if (!host) + host = (install.sourceURI instanceof Ci.nsIStandardURL) && + install.sourceURI.host; + + let error = (host || install.error == 0) ? "addonError" : "addonLocalError"; + if (install.error != 0) + error += install.error; + else if (install.addon.jetsdk) + error += "JetSDK"; + else if (install.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) + error += "Blocklisted"; + else + error += "Incompatible"; + + messageString = gNavigatorBundle.getString(error); + messageString = messageString.replace("#1", install.name); + if (host) + messageString = messageString.replace("#2", host); + messageString = messageString.replace("#3", brandShortName); + messageString = messageString.replace("#4", Services.appinfo.version); + + PopupNotifications.show(browser, notificationID, messageString, anchorID, + action, null, options); + } + break; + case "addon-install-complete": + var needsRestart = installInfo.installs.some(function(i) { + return i.addon.pendingOperations != AddonManager.PENDING_NONE; + }); + + if (needsRestart) { + messageString = gNavigatorBundle.getString("addonsInstalledNeedsRestart"); + action = { + label: gNavigatorBundle.getString("addonInstallRestartButton"), + accessKey: gNavigatorBundle.getString("addonInstallRestartButton.accesskey"), + callback: function() { + Application.restart(); + } + }; + } + 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; + } + } +}; + +/* + * When addons are installed/uninstalled, check and see if the number of items + * on the add-on bar changed: + * - If an add-on was installed, incrementing the count, show the bar. + * - If an add-on was uninstalled, and no more items are left, hide the bar. + */ +let AddonsMgrListener = { + get addonBar() document.getElementById("addon-bar"), + get statusBar() document.getElementById("status-bar"), + getAddonBarItemCount: function() { + // Take into account the contents of the status bar shim for the count. + var itemCount = this.statusBar.childNodes.length; + + var defaultOrNoninteractive = this.addonBar.getAttribute("defaultset") + .split(",") + .concat(["separator", "spacer", "spring"]); + for (let item of this.addonBar.currentSet.split(",")) { + if (defaultOrNoninteractive.indexOf(item) == -1) + itemCount++; + } + + return itemCount; + }, + onInstalling: function(aAddon) { + this.lastAddonBarCount = this.getAddonBarItemCount(); + }, + onInstalled: function(aAddon) { + if (this.getAddonBarItemCount() > this.lastAddonBarCount) + setToolbarVisibility(this.addonBar, true); + }, + onUninstalling: function(aAddon) { + this.lastAddonBarCount = this.getAddonBarItemCount(); + }, + onUninstalled: function(aAddon) { + if (this.getAddonBarItemCount() == 0) + setToolbarVisibility(this.addonBar, false); + }, + onEnabling: function(aAddon) this.onInstalling(), + onEnabled: function(aAddon) this.onInstalled(), + onDisabling: function(aAddon) this.onUninstalling(), + onDisabled: function(aAddon) this.onUninstalled(), +}; + + +var LightWeightThemeWebInstaller = { + handleEvent: function (event) { + switch (event.type) { + case "InstallBrowserTheme": + case "PreviewBrowserTheme": + case "ResetBrowserThemePreview": + // ignore requests from background tabs + if (event.target.ownerDocument.defaultView.top != content) + return; + } + switch (event.type) { + case "InstallBrowserTheme": + this._installRequest(event); + break; + case "PreviewBrowserTheme": + this._preview(event); + break; + case "ResetBrowserThemePreview": + this._resetPreview(event); + break; + case "pagehide": + case "TabSelect": + this._resetPreview(); + break; + } + }, + + get _manager () { + var temp = {}; + Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); + delete this._manager; + return this._manager = temp.LightweightThemeManager; + }, + + _installRequest: function (event) { + var node = event.target; + var data = this._getThemeFromNode(node); + if (!data) + return; + + if (this._isAllowed(node)) { + this._install(data); + return; + } + + var allowButtonText = + gNavigatorBundle.getString("lwthemeInstallRequest.allowButton"); + var allowButtonAccesskey = + gNavigatorBundle.getString("lwthemeInstallRequest.allowButton.accesskey"); + var message = + gNavigatorBundle.getFormattedString("lwthemeInstallRequest.message", + [node.ownerDocument.location.host]); + var buttons = [{ + label: allowButtonText, + accessKey: allowButtonAccesskey, + callback: function () { + LightWeightThemeWebInstaller._install(data); + } + }]; + + this._removePreviousNotifications(); + + var notificationBox = gBrowser.getNotificationBox(); + var notificationBar = + notificationBox.appendNotification(message, "lwtheme-install-request", "", + notificationBox.PRIORITY_INFO_MEDIUM, + buttons); + notificationBar.persistence = 1; + }, + + _install: function (newLWTheme) { + var previousLWTheme = this._manager.currentTheme; + + var 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 () { + Application.restart(); + } + }; + + let options = { + timeout: Date.now() + 30000 + }; + + PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change", + messageString, "addons-notification-icon", + action, null, options); + }, + + onEnabled: function(aAddon) { + 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); + } + + var 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(); + + var notificationBox = gBrowser.getNotificationBox(); + var 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 () { + var box = gBrowser.getNotificationBox(); + + ["lwtheme-install-request", + "lwtheme-install-notification"].forEach(function (value) { + var notification = box.getNotificationWithValue(value); + if (notification) + box.removeNotification(notification); + }); + }, + + _previewWindow: null, + _preview: function (event) { + if (!this._isAllowed(event.target)) + return; + + var data = this._getThemeFromNode(event.target); + if (!data) + return; + + this._resetPreview(); + + this._previewWindow = event.target.ownerDocument.defaultView; + this._previewWindow.addEventListener("pagehide", this, true); + gBrowser.tabContainer.addEventListener("TabSelect", this, false); + + this._manager.previewTheme(data); + }, + + _resetPreview: function (event) { + if (!this._previewWindow || + event && !this._isAllowed(event.target)) + return; + + this._previewWindow.removeEventListener("pagehide", this, true); + this._previewWindow = null; + gBrowser.tabContainer.removeEventListener("TabSelect", this, false); + + this._manager.resetPreview(); + }, + + _isAllowed: function (node) { + var pm = Services.perms; + + var uri = node.ownerDocument.documentURIObject; + return pm.testPermission(uri, "install") == pm.ALLOW_ACTION; + }, + + _getThemeFromNode: function (node) { + return this._manager.parseTheme(node.getAttribute("data-browsertheme"), + node.baseURI); + } +} + +/* + * Listen for Lightweight Theme styling changes and update the browser's theme accordingly. + */ +let 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; + } + }); + + 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/palemoon/base/content/browser-appmenu.inc b/application/palemoon/base/content/browser-appmenu.inc new file mode 100644 index 0000000000..835bf22bcf --- /dev/null +++ b/application/palemoon/base/content/browser-appmenu.inc @@ -0,0 +1,394 @@ +# -*- 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/. + +<menupopup id="appmenu-popup" + onpopupshowing="if (event.target == this) { + updateEditUIVisibility(); +#ifdef MOZ_SERVICES_SYNC + gSyncUI.updateUI(); +#endif + return; + } + updateCharacterEncodingMenuState(); + if (event.target.parentNode.parentNode.parentNode.parentNode == this) + this._currentPopup = event.target;"> + <hbox> + <vbox id="appmenuPrimaryPane"> + <menuitem id="appmenu_newTab_popup" + label="&tabCmd.label;" + command="cmd_newNavigatorTab" + key="key_newNavigatorTab"/> + <menuitem id="appmenu_newNavigator" + label="&newNavigatorCmd.label;" + command="cmd_newNavigator" + key="key_newNavigator"/> + <menuitem id="appmenu_newPrivateWindow" + class="menuitem-iconic menuitem-iconic-tooltip" + label="&newPrivateWindow.label;" + command="Tools:PrivateBrowsing" + key="key_privatebrowsing"/> + <menuitem label="&goOfflineCmd.label;" + id="appmenu_offlineModeRecovery" + type="checkbox" + observes="workOfflineMenuitemState" + oncommand="BrowserOffline.toggleOfflineStatus();"/> + <menuseparator class="appmenu-menuseparator"/> + <hbox> + <menuitem id="appmenu-edit-label" + label="&appMenuEdit.label;" + disabled="true"/> + <toolbarbutton id="appmenu-cut" + class="appmenu-edit-button" + command="cmd_cut" + onclick="if (!this.disabled) hidePopup();" + tooltiptext="&cutButton.tooltip;"/> + <toolbarbutton id="appmenu-copy" + class="appmenu-edit-button" + command="cmd_copy" + onclick="if (!this.disabled) hidePopup();" + tooltiptext="©Button.tooltip;"/> + <toolbarbutton id="appmenu-paste" + class="appmenu-edit-button" + command="cmd_paste" + onclick="if (!this.disabled) hidePopup();" + tooltiptext="&pasteButton.tooltip;"/> + <spacer flex="1"/> + <menu id="appmenu-editmenu"> + <menupopup id="appmenu-editmenu-menupopup"> + <menuitem id="appmenu-editmenu-cut" + class="menuitem-iconic" + label="&cutCmd.label;" + key="key_cut" + command="cmd_cut"/> + <menuitem id="appmenu-editmenu-copy" + class="menuitem-iconic" + label="©Cmd.label;" + key="key_copy" + command="cmd_copy"/> + <menuitem id="appmenu-editmenu-paste" + class="menuitem-iconic" + label="&pasteCmd.label;" + key="key_paste" + command="cmd_paste"/> + <menuseparator/> + <menuitem id="appmenu-editmenu-undo" + label="&undoCmd.label;" + key="key_undo" + command="cmd_undo"/> + <menuitem id="appmenu-editmenu-redo" + label="&redoCmd.label;" + key="key_redo" + command="cmd_redo"/> + <menuseparator/> + <menuitem id="appmenu-editmenu-selectAll" + label="&selectAllCmd.label;" + key="key_selectAll" + command="cmd_selectAll"/> + <menuseparator/> + <menuitem id="appmenu-editmenu-delete" + label="&deleteCmd.label;" + key="key_delete" + command="cmd_delete"/> + </menupopup> + </menu> + </hbox> + <menuitem id="appmenu_find" + class="menuitem-tooltip" + label="&appMenuFind.label;" + command="cmd_find" + key="key_find"/> + <menuseparator class="appmenu-menuseparator"/> + <menuitem id="appmenu_openFile" + label="&openFileCmd.label;" + command="Browser:OpenFile" + key="openFileKb"/> + <menuitem id="appmenu_savePage" + class="menuitem-tooltip" + label="&savePageCmd.label;" + command="Browser:SavePage" + key="key_savePage"/> + <menuitem id="appmenu_sendLink" + label="&emailPageCmd.label;" + command="Browser:SendLink"/> + <splitmenu id="appmenu_print" + iconic="true" + label="&printCmd.label;" + command="cmd_print"> + <menupopup> + <menuitem id="appmenu_print_popup" + class="menuitem-iconic" + label="&printCmd.label;" + command="cmd_print" + key="printKb"/> + <menuitem id="appmenu_printPreview" + label="&printPreviewCmd.label;" + command="cmd_printPreview"/> + <menuitem id="appmenu_printSetup" + label="&printSetupCmd.label;" + command="cmd_pageSetup"/> + </menupopup> + </splitmenu> + <menuseparator class="appmenu-menuseparator"/> + <splitmenu id="appmenu_webDeveloper" + command="Tools:DevToolbox" + label="&appMenuWebDeveloper.label;"> + <menupopup id="appmenu_webDeveloper_popup"> +#ifdef MOZ_DEVTOOLS + <menuitem id="appmenu_devToolbox" + observes="devtoolsMenuBroadcaster_DevToolbox"/> + <menuseparator id="appmenu_devtools_separator"/> + <menuitem id="appmenu_devToolbar" + observes="devtoolsMenuBroadcaster_DevToolbar"/> + <menuitem id="appmenu_chromeDebugger" + observes="devtoolsMenuBroadcaster_ChromeDebugger"/> + <menuitem id="appmenu_browserConsole" + observes="devtoolsMenuBroadcaster_BrowserConsole"/> + <menuitem id="appmenu_responsiveUI" + observes="devtoolsMenuBroadcaster_ResponsiveUI"/> + <menuitem id="appmenu_eyedropper" + observes="devtoolsMenuBroadcaster_Eyedropper"/> + <menuitem id="appmenu_scratchpad" + observes="devtoolsMenuBroadcaster_Scratchpad"/> +#endif + <menuitem id="appmenu_pageSource" + observes="devtoolsMenuBroadcaster_PageSource"/> + <menuitem id="appmenu_errorConsole" + observes="devtoolsMenuBroadcaster_ErrorConsole"/> +#ifdef MOZ_DEVTOOLS + <menuitem id="appmenu_devtools_connect" + observes="devtoolsMenuBroadcaster_connect"/> +#endif + <menuseparator id="appmenu_devToolsEndSeparator"/> + <menuitem id="appmenu_getMoreDevtools" + observes="devtoolsMenuBroadcaster_GetMoreTools"/> + <menuseparator/> +#define ID_PREFIX appmenu_developer_ +#define OMIT_ACCESSKEYS +#include browser-charsetmenu.inc +#undef ID_PREFIX +#undef OMIT_ACCESSKEYS + <menuitem label="&goOfflineCmd.label;" + type="checkbox" + observes="workOfflineMenuitemState" + oncommand="BrowserOffline.toggleOfflineStatus();"/> + </menupopup> + </splitmenu> + <menuseparator class="appmenu-menuseparator"/> +#define ID_PREFIX appmenu_ +#define OMIT_ACCESSKEYS +#include browser-charsetmenu.inc +#undef ID_PREFIX +#undef OMIT_ACCESSKEYS + <menuitem id="appmenu_fullScreen" + class="menuitem-tooltip" + label="&fullScreenCmd.label;" + type="checkbox" + observes="View:FullScreen" + key="key_fullScreen"/> + <menuitem id="appmenu_restart" + class="menuitem-iconic" + label="&appMenuRestart.label;" + command="cmd_restartApplication"/> + <menuitem id="appmenu-quit" + class="menuitem-iconic" +#ifdef XP_WIN + label="&quitApplicationCmdWin.label;" +#else + label="&quitApplicationCmd.label;" +#endif + command="cmd_quitApplication"/> + </vbox> + <vbox id="appmenuSecondaryPane"> + <splitmenu id="appmenu_bookmarks" + iconic="true" + label="&bookmarksMenu.label;" + command="Browser:ShowAllBookmarks"> + <menupopup id="appmenu_bookmarksPopup" + placespopup="true" + context="placesContext" + openInTabs="children" + oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);" + onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);" + onpopupshowing="BookmarkingUI.onPopupShowing(event); + if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=BOOKMARKS_MENU');" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <menuitem id="appmenu_showAllBookmarks" + class="menuitem-iconic" + label="&organizeBookmarks.label;" + command="Browser:ShowAllBookmarks" + context="" + key="manBookmarkKb"/> + <menuseparator/> + <menuitem id="appmenu_bookmarkThisPage" + class="menuitem-iconic" + label="&bookmarkThisPageCmd.label;" + command="Browser:AddBookmarkAs" + key="addBookmarkAsKb"/> + <menuitem id="appmenu_subscribeToPage" + class="menuitem-iconic" + label="&subscribeToPageMenuitem.label;" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);" + observes="singleFeedMenuitemState"/> + <menu id="appmenu_subscribeToPageMenu" + class="menu-iconic" + label="&subscribeToPageMenupopup.label;" + observes="multipleFeedsMenuState"> + <menupopup id="appmenu_subscribeToPageMenupopup" + onpopupshowing="return FeedHandler.buildFeedList(event.target);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </menu> + <menuseparator/> + <menu id="appmenu_bookmarksToolbar" + placesanonid="toolbar-autohide" + class="menu-iconic bookmark-item" + label="&personalbarCmd.label;" + container="true"> + <menupopup id="appmenu_bookmarksToolbarPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=TOOLBAR');"/> + </menu> + <menuseparator/> + <!-- Bookmarks menu items --> + <menuseparator builder="end" + class="hide-if-empty-places-result"/> + <menuitem id="appmenu_unsortedBookmarks" + class="menuitem-iconic" + label="&appMenuUnsorted.label;" + oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/> + </menupopup> + </splitmenu> + <splitmenu id="appmenu_history" + iconic="true" + label="&historyMenu.label;" + command="Browser:ShowAllHistory"> + <menupopup id="appmenu_historyMenupopup" + placespopup="true" + context="placesContext" + oncommand="this.parentNode._placesView._onCommand(event);" + onclick="checkForMiddleClick(this, event);" + onpopupshowing="if (!this.parentNode._placesView) + new HistoryMenu(event);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <menuitem id="appmenu_showAllHistory" + class="menuitem-iconic" + label="&showAllHistoryCmd2.label;" + command="Browser:ShowAllHistory" + key="showAllHistoryKb"/> + <menuseparator/> + <menuitem id="appmenu_sanitizeHistory" + class="menuitem-iconic" + label="&clearRecentHistory.label;" + key="key_sanitize" + command="Tools:Sanitize"/> + <menuseparator class="hide-if-empty-places-result"/> +#ifdef MOZ_SERVICES_SYNC + <menuitem id="appmenu_sync-tabs" + class="syncTabsMenuItem" + label="&syncTabsMenu2.label;" + oncommand="BrowserOpenSyncTabs();" + disabled="true"/> +#endif + <menuitem id="appmenu_restoreLastSession" + label="&historyRestoreLastSession.label;" + command="Browser:RestoreLastSession"/> + <menu id="appmenu_recentlyClosedTabsMenu" + class="recentlyClosedTabsMenu" + label="&historyUndoMenu.label;" + disabled="true"> + <menupopup id="appmenu_recentlyClosedTabsMenupopup" + onpopupshowing="document.getElementById('appmenu_history')._placesView.populateUndoSubmenu();"/> + </menu> + <menu id="appmenu_recentlyClosedWindowsMenu" + class="recentlyClosedWindowsMenu" + label="&historyUndoWindowMenu.label;" + disabled="true"> + <menupopup id="appmenu_recentlyClosedWindowsMenupopup" + onpopupshowing="document.getElementById('appmenu_history')._placesView.populateUndoWindowSubmenu();"/> + </menu> + <menuseparator/> + </menupopup> + </splitmenu> + <menuitem id="appmenu_downloads" + class="menuitem-tooltip" + label="&downloads.label;" + command="Tools:Downloads" + key="key_openDownloads"/> + <spacer id="appmenuSecondaryPane-spacer"/> + <menuitem id="appmenu_addons" + class="menuitem-iconic menuitem-iconic-tooltip" + label="&addons.label;" + command="Tools:Addons" + key="key_openAddons"/> + <menuitem id="appmenu_permissions" + class="menuitem-iconic" + label="&permissions.label;" + command="Tools:Permissions" + key="key_openPermissions"/> +#ifdef MOZ_SERVICES_SYNC + <!-- only one of sync-setup or sync-syncnow will be showing at once --> + <menuitem id="sync-setup-appmenu" + label="&syncSetup.label;" + observes="sync-setup-state" + oncommand="gSyncUI.openSetup()"/> + <menuitem id="sync-syncnowitem-appmenu" + label="&syncSyncNowItem.label;" + observes="sync-syncnow-state" + oncommand="gSyncUI.doSync(event);"/> +#endif + <splitmenu id="appmenu_customize" + label="&preferencesCmd2.label;" + oncommand="openPreferences();"> + <menupopup id="appmenu_customizeMenu" + onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('appmenu_toggleToolbarsSeparator'));"> + <menuitem id="appmenu_preferences" + label="&preferencesCmd2.label;" + oncommand="openPreferences();"/> + <menuseparator/> + <menuseparator id="appmenu_toggleToolbarsSeparator"/> + <menuitem id="appmenu_toggleTabsOnTop" + label="&viewTabsOnTop.label;" + type="checkbox" + command="cmd_ToggleTabsOnTop"/> + <menuitem id="appmenu_toolbarLayout" + label="&appMenuToolbarLayout.label;" + command="cmd_CustomizeToolbars"/> + </menupopup> + </splitmenu> + <splitmenu id="appmenu_help" + label="&helpMenu.label;" + oncommand="openHelpLink('firefox-help')"> + <menupopup id="appmenu_helpMenupopup"> + <menuitem id="appmenu_openHelp" + label="&helpMenu.label;" + oncommand="openHelpLink('firefox-help')" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="appmenu_troubleshootingInfo" + label="&helpTroubleshootingInfo.label;" + oncommand="openTroubleshootingPage()" + onclick="checkForMiddleClick(this,event);"/> + <menuitem id="appmenu_feedbackPage" + label="&helpFeedbackPage.label;" + oncommand="openFeedbackPage()" + onclick="checkForMiddleClick(this, event);"/> + <menuseparator/> + <menuitem id="appmenu_safeMode" + label="&appMenuSafeMode.label;" + oncommand="restart(true);"/> + <menuseparator/> + <menuitem id="appmenu_about" + label="&aboutProduct.label;" + oncommand="openAboutDialog();"/> + </menupopup> + </splitmenu> + </vbox> + </hbox> +</menupopup> diff --git a/application/palemoon/base/content/browser-charsetmenu.inc b/application/palemoon/base/content/browser-charsetmenu.inc new file mode 100644 index 0000000000..628de1341f --- /dev/null +++ b/application/palemoon/base/content/browser-charsetmenu.inc @@ -0,0 +1,62 @@ +# 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/. + +#filter substitution + +#expand <menu id="__ID_PREFIX__charsetMenu" + label="&charsetMenu.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenu.accesskey;" +#endif + oncommand="MultiplexHandler(event)" +#ifdef OMIT_ACCESSKEYS +#expand onpopupshowing="CharsetMenu.build(event, '__ID_PREFIX__');" +#else +#expand onpopupshowing="CharsetMenu.build(event, '__ID_PREFIX__', true);" +#endif + onpopupshown="UpdateMenus(event);"> + <menupopup> + <menu label="&charsetMenuAutodet.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.accesskey;" +#endif + > + <menupopup> + <menuitem type="radio" + name="detectorGroup" +#expand id="__ID_PREFIX__chardet.off" + label="&charsetMenuAutodet.off.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.off.accesskey;" +#endif + /> + <menuitem type="radio" + name="detectorGroup" +#expand id="__ID_PREFIX__chardet.ja_parallel_state_machine" + label="&charsetMenuAutodet.ja.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.ja.accesskey;" +#endif + /> + <menuitem type="radio" + name="detectorGroup" +#expand id="__ID_PREFIX__chardet.ruprob" + label="&charsetMenuAutodet.ru.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.ru.accesskey;" +#endif + /> + <menuitem type="radio" + name="detectorGroup" +#expand id="__ID_PREFIX__chardet.ukprob" + label="&charsetMenuAutodet.uk.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.uk.accesskey;" +#endif + /> + </menupopup> + </menu> + <menuseparator/> + </menupopup> +</menu> diff --git a/application/palemoon/base/content/browser-context.inc b/application/palemoon/base/content/browser-context.inc new file mode 100644 index 0000000000..f672ede615 --- /dev/null +++ b/application/palemoon/base/content/browser-context.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/. + + <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-openlinkintab" + label="&openLinkCmdInTab.label;" + accesskey="&openLinkCmdInTab.accesskey;" + oncommand="gContextMenu.openLinkInTab();"/> + <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();"/> + <menuitem id="context-openlinkincurrent" + label="&openLinkCmdInCurrent.label;" + accesskey="&openLinkCmdInCurrent.accesskey;" + oncommand="gContextMenu.openLinkInCurrent();"/> + <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-sendlink" + label="&sendLinkCmd.label;" + accesskey="&sendLinkCmd.accesskey;" + oncommand="gContextMenu.sendLink();"/> + <menuitem id="context-copyemail" + label="©EmailCmd.label;" + accesskey="©EmailCmd.accesskey;" + oncommand="gContextMenu.copyEmail();"/> + <menuitem id="context-copylink" + label="©LinkCmd.label;" + accesskey="©LinkCmd.accesskey;" + oncommand="goDoCommand('cmd_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="&mediaPlaybackRate.label;" accesskey="&mediaPlaybackRate.accesskey;"> + <menupopup> + <menuitem id="context-media-playbackrate-050x" + label="&mediaPlaybackRate050x.label;" + accesskey="&mediaPlaybackRate050x.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 0.5);"/> + <menuitem id="context-media-playbackrate-100x" + label="&mediaPlaybackRate100x.label;" + accesskey="&mediaPlaybackRate100x.accesskey;" + type="radio" + name="playbackrate" + checked="true" + oncommand="gContextMenu.mediaCommand('playbackRate', 1.0);"/> + <menuitem id="context-media-playbackrate-150x" + label="&mediaPlaybackRate150x.label;" + accesskey="&mediaPlaybackRate150x.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 1.5);"/> + <menuitem id="context-media-playbackrate-200x" + label="&mediaPlaybackRate200x.label;" + accesskey="&mediaPlaybackRate200x.accesskey;" + type="radio" + name="playbackrate" + oncommand="gContextMenu.mediaCommand('playbackRate', 2.0);"/> + </menupopup> + </menu> + <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-showstats" + accesskey="&videoShowStats.accesskey;" + label="&videoShowStats.label;" + oncommand="gContextMenu.mediaCommand('showstats');"/> + <menuitem id="context-video-hidestats" + accesskey="&videoHideStats.accesskey;" + label="&videoHideStats.label;" + oncommand="gContextMenu.mediaCommand('hidestats');"/> + <menuitem id="context-video-fullscreen" + accesskey="&videoFullScreen.accesskey;" + label="&videoFullScreen.label;" + oncommand="gContextMenu.fullScreenVideo();"/> + <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-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();"/> + <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-back" + label="&backCmd.label;" + accesskey="&backCmd.accesskey;" + command="Browser:BackOrBackDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-forward" + label="&forwardCmd.label;" + accesskey="&forwardCmd.accesskey;" + command="Browser:ForwardOrForwardDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-reload" + label="&reloadCmd.label;" + accesskey="&reloadCmd.accesskey;" + oncommand="gContextMenu.reload(event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="context-stop" + label="&stopCmd.label;" + accesskey="&stopCmd.accesskey;" + command="Browser:Stop"/> + <menuseparator id="context-sep-stop"/> + <menuitem id="context-bookmarkpage" + label="&bookmarkPageCmd2.label;" + accesskey="&bookmarkPageCmd2.accesskey;" + oncommand="gContextMenu.bookmarkThisPage();"/> + <menuitem id="context-savepage" + label="&savePageCmd.label;" + accesskey="&savePageCmd.accesskey2;" + oncommand="gContextMenu.savePageAs();"/> + <menuitem id="context-sendpage" + label="&sendPageCmd.label;" + accesskey="&sendPageCmd.accesskey;" + oncommand="gContextMenu.sendPage();"/> + <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="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="BrowserViewSourceOfDocument(gContextMenu.browser.contentDocument);" + observes="isImage"/> + <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();"/> + <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();"/> +#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 diff --git a/application/palemoon/base/content/browser-doctype.inc b/application/palemoon/base/content/browser-doctype.inc new file mode 100644 index 0000000000..6ee6384b64 --- /dev/null +++ b/application/palemoon/base/content/browser-doctype.inc @@ -0,0 +1,19 @@ +<!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://browser/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; +<!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd"> +%aboutHomeDTD; +]> + diff --git a/application/palemoon/base/content/browser-feeds.js b/application/palemoon/base/content/browser-feeds.js new file mode 100644 index 0000000000..c678772694 --- /dev/null +++ b/application/palemoon/base/content/browser-feeds.js @@ -0,0 +1,224 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/** + * The Feed Handler object manages discovery of RSS/ATOM feeds in web pages + * and shows UI when they are discovered. + */ +var FeedHandler = { + + /* Pale Moon: Address Bar: Feeds + * The click handler for the Feed icon in the location bar. Opens the + * subscription page if user is not given a choice of feeds. + * (Otherwise the list of available feeds will be presented to the + * user in a popup menu.) + */ + onFeedButtonPMClick: function(event) { + event.stopPropagation(); + + if (event.target.hasAttribute("feed") && + event.eventPhase == Event.AT_TARGET && + (event.button == 0 || event.button == 1)) { + this.subscribeToFeed(null, event); + } + }, + + /** + * The click handler for the Feed icon in the toolbar. Opens the + * subscription page if user is not given a choice of feeds. + * (Otherwise the list of available feeds will be presented to the + * user in a popup menu.) + */ + onFeedButtonClick: function(event) { + event.stopPropagation(); + + let feeds = gBrowser.selectedBrowser.feeds || []; + // If there are multiple feeds, the menu will open, so no need to do + // anything. If there are no feeds, nothing to do either. + if (feeds.length != 1) + return; + + if (event.eventPhase == Event.AT_TARGET && + (event.button == 0 || event.button == 1)) { + this.subscribeToFeed(feeds[0].href, event); + } + }, + + /** Called when the user clicks on the Subscribe to This Page... menu item. + * 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 menuPopup + * The feed list menupopup to be populated. + * @returns true if the menu 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). + */ + buildFeedList: function(menuPopup) { + var feeds = gBrowser.selectedBrowser.feeds; + if (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. + menuPopup.parentNode.removeAttribute("open"); + return false; + } + + while (menuPopup.firstChild) + menuPopup.removeChild(menuPopup.firstChild); + + if (feeds.length == 1) { + var feedButtonPM = document.getElementById("ub-feed-button"); + if (feedButtonPM) + feedButtonPM.setAttribute("feed", feeds[0].href); + return false; + } + + if (feeds.length <= 1) + return false; + + // Build the menu showing the available feed choices for viewing. + for (let feedInfo of feeds) { + var menuItem = document.createElement("menuitem"); + var baseTitle = feedInfo.title || feedInfo.href; + var labelStr = gNavigatorBundle.getFormattedString("feedShowFeedNew", [baseTitle]); + menuItem.setAttribute("class", "feed-menuitem"); + menuItem.setAttribute("label", labelStr); + menuItem.setAttribute("feed", feedInfo.href); + menuItem.setAttribute("tooltiptext", feedInfo.href); + menuItem.setAttribute("crop", "center"); + menuPopup.appendChild(menuItem); + } + 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 + * 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: function(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); + var 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: function(href, event) { + var 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: function() { + if (this._updateFeedTimeout) + clearTimeout(this._updateFeedTimeout); + + var feeds = gBrowser.selectedBrowser.feeds; + var haveFeeds = feeds && feeds.length > 0; + + var feedButtonPM = document.getElementById("ub-feed-button"); + + var feedButton = document.getElementById("feed-button"); + + if (feedButton) + feedButton.disabled = !haveFeeds; + + if (feedButtonPM) { + if (!haveFeeds) { + feedButtonPM.collapsed = true; + feedButtonPM.removeAttribute("feed"); + } else { + feedButtonPM.collapsed = !gPrefService.getBoolPref("browser.urlbar.rss"); + } + } + + if (!haveFeeds) { + this._feedMenuitem.setAttribute("disabled", "true"); + this._feedMenuitem.removeAttribute("hidden"); + this._feedMenupopup.setAttribute("hidden", "true"); + return; + } + + if (feeds.length > 1) { + if (feedButtonPM) + feedButtonPM.removeAttribute("feed"); + this._feedMenuitem.setAttribute("hidden", "true"); + this._feedMenupopup.removeAttribute("hidden"); + } else { + if (feedButtonPM) + feedButtonPM.setAttribute("feed", feeds[0].href); + this._feedMenuitem.setAttribute("feed", feeds[0].href); + this._feedMenuitem.removeAttribute("disabled"); + this._feedMenuitem.removeAttribute("hidden"); + this._feedMenupopup.setAttribute("hidden", "true"); + } + }, + + addFeed: function(link, targetDoc) { + // find which tab this is for, and set the attribute on the browser + var browserForLink = gBrowser.getBrowserForDocument(targetDoc); + if (!browserForLink) { + // ignore feeds loaded in subframes (see bug 305472) + return; + } + + 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) { + var feedButtonPM = document.getElementById("ub-feed-button"); + if (feedButtonPM) + feedButtonPM.collapsed = !gPrefService.getBoolPref("browser.urlbar.rss"); + // 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); + } + } +}; diff --git a/application/palemoon/base/content/browser-fullScreen.js b/application/palemoon/base/content/browser-fullScreen.js new file mode 100644 index 0000000000..b8a29199ee --- /dev/null +++ b/application/palemoon/base/content/browser-fullScreen.js @@ -0,0 +1,607 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +var FullScreen = { + _XULNS: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + get _fullScrToggler() { + delete this._fullScrToggler; + return this._fullScrToggler = document.getElementById("fullscr-toggler"); + }, + toggle: function (event) { + var enterFS = window.fullScreen; + + // We get the fullscreen event _before_ the window transitions into or out of FS mode. + if (event && event.type == "fullscreen") + enterFS = !enterFS; + + // Toggle the View:FullScreen command, which controls elements like the + // fullscreen menuitem, menubars, and the appmenu. + let fullscreenCommand = document.getElementById("View:FullScreen"); + if (enterFS) { + fullscreenCommand.setAttribute("checked", enterFS); + } else { + fullscreenCommand.removeAttribute("checked"); + } + +#ifdef XP_MACOSX + // Make sure the menu items are adjusted. + document.getElementById("enterFullScreenItem").hidden = enterFS; + document.getElementById("exitFullScreenItem").hidden = !enterFS; +#endif + + // On OS X Lion we don't want to hide toolbars when entering fullscreen, unless + // we're entering DOM fullscreen, in which case we should hide the toolbars. + // If we're leaving fullscreen, then we'll go through the exit code below to + // make sure toolbars are made visible in the case of DOM fullscreen. + if (enterFS && this.useLionFullScreen) { + if (document.mozFullScreen) { + this.showXULChrome("toolbar", false); + } + else { + gNavToolbox.setAttribute("inFullscreen", true); + document.documentElement.setAttribute("inFullscreen", true); + } + return; + } + + // show/hide menubars, toolbars (except the full screen toolbar) + this.showXULChrome("toolbar", !enterFS); + + if (enterFS) { + // Add a tiny toolbar to receive mouseover and dragenter events, and provide affordance. + // This will help simulate the "collapse" metaphor while also requiring less code and + // events than raw listening of mouse coords. We don't add the toolbar in DOM full-screen + // mode, only browser full-screen mode. + if (!document.mozFullScreen) { + this._fullScrToggler.addEventListener("mouseover", this._expandCallback, false); + this._fullScrToggler.addEventListener("dragenter", this._expandCallback, false); + } + if (gPrefService.getBoolPref("browser.fullscreen.autohide")) + gBrowser.mPanelContainer.addEventListener("mousemove", + this._collapseCallback, false); + + document.addEventListener("keypress", this._keyToggleCallback, false); + document.addEventListener("popupshown", this._setPopupOpen, false); + document.addEventListener("popuphidden", this._setPopupOpen, false); + // We don't animate the toolbar collapse if in DOM full-screen mode, + // as the size of the content area would still be changing after the + // mozfullscreenchange event fired, which could confuse content script. + this._shouldAnimate = !document.mozFullScreen; + this.mouseoverToggle(false); + + // Autohide prefs + gPrefService.addObserver("browser.fullscreen", this, false); + } + else { + // The user may quit fullscreen during an animation + this._cancelAnimation(); + gNavToolbox.style.marginTop = ""; + if (this._isChromeCollapsed) + this.mouseoverToggle(true); + // This is needed if they use the context menu to quit fullscreen + this._isPopupOpen = false; + + document.documentElement.removeAttribute("inDOMFullscreen"); + + this.cleanup(); + } + }, + + exitDomFullScreen : function() { + document.mozCancelFullScreen(); + }, + + handleEvent: function (event) { + switch (event.type) { + case "activate": + if (document.mozFullScreen) { + this.showWarning(this.fullscreenDoc); + } + break; + case "transitionend": + if (event.propertyName == "opacity") + this.cancelWarning(); + break; + } + }, + + enterDomFullscreen : function(event) { + if (!document.mozFullScreen) + return; + + // However, if we receive a "MozEnteredDomFullScreen" event for a document + // which is not a subdocument of a currently active (ie. visible) browser + // or iframe, we know that we've switched to a different frame since the + // request to enter full-screen was made, so we should exit full-screen + // since the "full-screen document" isn't acutally visible. + if (!event.target.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell).isActive) { + document.mozCancelFullScreen(); + return; + } + + let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + if (focusManager.activeWindow != window) { + // The top-level window has lost focus since the request to enter + // full-screen was made. Cancel full-screen. + document.mozCancelFullScreen(); + return; + } + + document.documentElement.setAttribute("inDOMFullscreen", true); + + if (gFindBarInitialized) + gFindBar.close(); + + this.showWarning(event.target); + + // 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. + if (!this.useLionFullScreen) { + window.addEventListener("activate", this); + } + + // Cancel any "hide the toolbar" animation which is in progress, and make + // the toolbar hide immediately. + this._cancelAnimation(); + this.mouseoverToggle(false); + + // Remove listeners on the full-screen toggler, so that mouseover + // the top of the screen will not cause the toolbar to re-appear. + this._fullScrToggler.removeEventListener("mouseover", this._expandCallback, false); + this._fullScrToggler.removeEventListener("dragenter", this._expandCallback, false); + }, + + cleanup: function () { + if (window.fullScreen) { + gBrowser.mPanelContainer.removeEventListener("mousemove", + this._collapseCallback, false); + document.removeEventListener("keypress", this._keyToggleCallback, false); + document.removeEventListener("popupshown", this._setPopupOpen, false); + document.removeEventListener("popuphidden", this._setPopupOpen, false); + gPrefService.removeObserver("browser.fullscreen", this); + + this._fullScrToggler.removeEventListener("mouseover", this._expandCallback, false); + this._fullScrToggler.removeEventListener("dragenter", this._expandCallback, false); + this.cancelWarning(); + gBrowser.tabContainer.removeEventListener("TabOpen", this.exitDomFullScreen); + gBrowser.tabContainer.removeEventListener("TabClose", this.exitDomFullScreen); + gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen); + if (!this.useLionFullScreen) + window.removeEventListener("activate", this); + this.fullscreenDoc = null; + } + }, + + observe: function(aSubject, aTopic, aData) + { + if (aData == "browser.fullscreen.autohide") { + if (gPrefService.getBoolPref("browser.fullscreen.autohide")) { + gBrowser.mPanelContainer.addEventListener("mousemove", + this._collapseCallback, false); + } + else { + gBrowser.mPanelContainer.removeEventListener("mousemove", + this._collapseCallback, false); + } + } + }, + + // Event callbacks + _expandCallback: function() + { + FullScreen.mouseoverToggle(true); + }, + _collapseCallback: function() + { + FullScreen.mouseoverToggle(false); + }, + _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._shouldAnimate = false; + FullScreen.mouseoverToggle(false, true); + } + // F6 is another shortcut to the address bar, but its not covered in OpenLocation() + else if (aEvent.keyCode == aEvent.DOM_VK_F6) + FullScreen.mouseoverToggle(true); + }, + + // Checks whether we are allowed to collapse the chrome + _isPopupOpen: false, + _isChromeCollapsed: false, + _safeToCollapse: function(forceHide) + { + if (!gPrefService.getBoolPref("browser.fullscreen.autohide")) + return false; + + // a popup menu is open in chrome: don't collapse chrome + if (!forceHide && this._isPopupOpen) + 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") { + if (forceHide) + // hidden textboxes that still have focus are bad bad bad + document.commandDispatcher.focusedElement.blur(); + else + 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; + }, + + // 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")); + }, + + // Animate the toolbars disappearing + _shouldAnimate: true, + _isAnimating: false, + _animationTimeout: 0, + _animationHandle: 0, + _animateUp: function() { + // check again, the user may have done something before the animation was due to start + if (!window.fullScreen || !this._safeToCollapse(false)) { + this._isAnimating = false; + this._shouldAnimate = true; + return; + } + + this._animateStartTime = window.mozAnimationStartTime; + if (!this._animationHandle) + this._animationHandle = window.mozRequestAnimationFrame(this); + }, + + sample: function (timeStamp) { + const duration = 1500; + const timePassed = timeStamp - this._animateStartTime; + const pos = timePassed >= duration ? 1 : + 1 - Math.pow(1 - timePassed / duration, 4); + + if (pos >= 1) { + // We've animated enough + this._cancelAnimation(); + gNavToolbox.style.marginTop = ""; + this.mouseoverToggle(false); + return; + } + + gNavToolbox.style.marginTop = (gNavToolbox.boxObject.height * pos * -1) + "px"; + this._animationHandle = window.mozRequestAnimationFrame(this); + }, + + _cancelAnimation: function() { + window.mozCancelAnimationFrame(this._animationHandle); + this._animationHandle = 0; + clearTimeout(this._animationTimeout); + this._isAnimating = false; + this._shouldAnimate = false; + }, + + cancelWarning: function(event) { + if (!this.warningBox) + return; + this.warningBox.removeEventListener("transitionend", this); + if (this.warningFadeOutTimeout) { + clearTimeout(this.warningFadeOutTimeout); + this.warningFadeOutTimeout = null; + } + + // Ensure focus switches away from the (now hidden) warning box. If the user + // clicked buttons in the fullscreen key authorization UI, 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(); + + this.warningBox.setAttribute("hidden", true); + this.warningBox.removeAttribute("fade-warning-out"); + this.warningBox.removeAttribute("obscure-browser"); + this.warningBox = null; + }, + + setFullscreenAllowed: function(isApproved) { + // The "remember decision" checkbox is hidden when showing for documents that + // the permission manager can't handle (documents with URIs without a host). + // We simply require those to be approved every time instead. + let rememberCheckbox = document.getElementById("full-screen-remember-decision"); + let uri = this.fullscreenDoc.nodePrincipal.URI; + if (!rememberCheckbox.hidden) { + if (rememberCheckbox.checked) + Services.perms.add(uri, + "fullscreen", + isApproved ? Services.perms.ALLOW_ACTION : Services.perms.DENY_ACTION, + Services.perms.EXPIRE_NEVER); + else if (isApproved) { + // The user has only temporarily approved fullscren for this fullscreen + // session only. Add the permission (so Goanna knows to approve any further + // fullscreen requests for this host in this fullscreen session) but add + // a listener to revoke the permission when the chrome document exits + // fullscreen. + Services.perms.add(uri, + "fullscreen", + Services.perms.ALLOW_ACTION, + Services.perms.EXPIRE_SESSION); + let host = uri.host; + var onFullscreenchange = function onFullscreenchange(event) { + if (event.target == document && document.mozFullScreenElement == null) { + // The chrome document has left fullscreen. Remove the temporary permission grant. + Services.perms.remove(host, "fullscreen"); + document.removeEventListener("mozfullscreenchange", onFullscreenchange); + } + } + document.addEventListener("mozfullscreenchange", onFullscreenchange); + } + } + if (this.warningBox) + this.warningBox.setAttribute("fade-warning-out", "true"); + // If the document has been granted fullscreen, notify Goanna so it can resume + // any pending pointer lock requests, otherwise exit fullscreen; the user denied + // the fullscreen request. + if (isApproved) + Services.obs.notifyObservers(this.fullscreenDoc, "fullscreen-approved", ""); + else + document.mozCancelFullScreen(); + }, + + warningBox: null, + warningFadeOutTimeout: null, + fullscreenDoc: null, + + // Shows the fullscreen approval UI, or if the domain has already been approved + // for fullscreen, shows a warning that the site has entered fullscreen for a short + // duration. + showWarning: function(targetDoc) { + if (!document.mozFullScreen || + !gPrefService.getBoolPref("full-screen-api.approval-required")) + return; + + // Set the strings on the fullscreen approval UI. + this.fullscreenDoc = targetDoc; + let uri = this.fullscreenDoc.nodePrincipal.URI; + let host = null; + try { + host = uri.host; + } catch (e) { } + let hostLabel = document.getElementById("full-screen-domain-text"); + let rememberCheckbox = document.getElementById("full-screen-remember-decision"); + let isApproved = false; + if (host) { + // Document's principal's URI has a host. Display a warning including the hostname and + // show UI to enable the user to permanently grant this host permission to enter fullscreen. + let utils = {}; + Cu.import("resource://gre/modules/DownloadUtils.jsm", utils); + let displayHost = utils.DownloadUtils.getURIHost(uri.spec)[0]; + let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); + + hostLabel.textContent = bundle.formatStringFromName("fullscreen.entered", [displayHost], 1); + hostLabel.removeAttribute("hidden"); + + rememberCheckbox.label = bundle.formatStringFromName("fullscreen.rememberDecision", [displayHost], 1); + rememberCheckbox.checked = false; + rememberCheckbox.removeAttribute("hidden"); + + // Note we only allow documents whose principal's URI has a host to + // store permission grants. + isApproved = Services.perms.testPermission(uri, "fullscreen") == Services.perms.ALLOW_ACTION; + } else { + hostLabel.setAttribute("hidden", "true"); + rememberCheckbox.setAttribute("hidden", "true"); + } + + // Note: the warning box can be non-null if the warning box from the previous request + // wasn't hidden before another request was made. + if (!this.warningBox) { + this.warningBox = document.getElementById("full-screen-warning-container"); + // Add a listener to clean up state after the warning is hidden. + this.warningBox.addEventListener("transitionend", this); + this.warningBox.removeAttribute("hidden"); + } else { + if (this.warningFadeOutTimeout) { + clearTimeout(this.warningFadeOutTimeout); + this.warningFadeOutTimeout = null; + } + this.warningBox.removeAttribute("fade-warning-out"); + } + + // If fullscreen mode has not yet been approved for the fullscreen + // document's domain, show the approval UI and don't auto fade out the + // fullscreen warning box. Otherwise, we're just notifying of entry into + // fullscreen mode. Note if the resource's host is null, we must be + // showing a local file or a local data URI, and we require explicit + // approval every time. + let authUI = document.getElementById("full-screen-approval-pane"); + if (isApproved) { + authUI.setAttribute("hidden", "true"); + this.warningBox.removeAttribute("obscure-browser"); + } else { + // Partially obscure the <browser> element underneath the approval UI. + this.warningBox.setAttribute("obscure-browser", "true"); + authUI.removeAttribute("hidden"); + } + + // If we're not showing the fullscreen approval UI, we're just notifying the user + // of the transition, so set a timeout to fade the warning out after a few moments. + if (isApproved) + this.warningFadeOutTimeout = + setTimeout( + function() { + if (this.warningBox) + this.warningBox.setAttribute("fade-warning-out", "true"); + }.bind(this), + 3000); + }, + + mouseoverToggle: function(aShow, forceHide) + { + // Don't do anything if: + // a) we're already in the state we want, + // b) we're animating and will become collapsed soon, or + // c) we can't collapse because it would be undesirable right now + if (aShow != this._isChromeCollapsed || (!aShow && this._isAnimating) || + (!aShow && !this._safeToCollapse(forceHide))) + return; + + // browser.fullscreen.animateUp + // 0 - never animate up + // 1 - animate only for first collapse after entering fullscreen (default for perf's sake) + // 2 - animate every time it collapses + if (gPrefService.getIntPref("browser.fullscreen.animateUp") == 0) + this._shouldAnimate = false; + + if (!aShow && this._shouldAnimate) { + this._isAnimating = true; + this._shouldAnimate = false; + this._animationTimeout = setTimeout(this._animateUp.bind(this), 800); + return; + } + + // The chrome is collapsed so don't spam needless mousemove events + if (aShow) { + gBrowser.mPanelContainer.addEventListener("mousemove", + this._collapseCallback, false); + } + else { + gBrowser.mPanelContainer.removeEventListener("mousemove", + this._collapseCallback, false); + } + + // Hiding/collapsing the toolbox interferes with the tab bar's scrollbox, + // so we just move it off-screen instead. See bug 430687. + gNavToolbox.style.marginTop = + aShow ? "" : -gNavToolbox.getBoundingClientRect().height + "px"; + + this._fullScrToggler.collapsed = aShow; + this._isChromeCollapsed = !aShow; + if (gPrefService.getIntPref("browser.fullscreen.animateUp") == 2) + this._shouldAnimate = true; + }, + + showXULChrome: function(aTag, aShow) + { + var els = document.getElementsByTagNameNS(this._XULNS, aTag); + + for (let el of els) { + // XXX don't interfere with previously collapsed toolbars + if (el.getAttribute("fullscreentoolbar") == "true") { + if (!aShow) { + + var toolbarMode = el.getAttribute("mode"); + if (toolbarMode != "text") { + el.setAttribute("saved-mode", toolbarMode); + el.setAttribute("saved-iconsize", el.getAttribute("iconsize")); + el.setAttribute("mode", "icons"); + el.setAttribute("iconsize", "small"); + } + + // 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 { + var restoreAttr = function restoreAttr(attrName) { + var savedAttr = "saved-" + attrName; + if (el.hasAttribute(savedAttr)) { + el.setAttribute(attrName, el.getAttribute(savedAttr)); + el.removeAttribute(savedAttr); + } + } + + restoreAttr("mode"); + restoreAttr("iconsize"); + restoreAttr("context"); + + el.removeAttribute("inFullscreen"); + } + } else { + // use moz-collapsed so it doesn't persist hidden/collapsed, + // so that new windows don't have missing toolbars + if (aShow) + el.removeAttribute("moz-collapsed"); + else + el.setAttribute("moz-collapsed", "true"); + } + } + + if (aShow) { + gNavToolbox.removeAttribute("inFullscreen"); + document.documentElement.removeAttribute("inFullscreen"); + } else { + gNavToolbox.setAttribute("inFullscreen", true); + document.documentElement.setAttribute("inFullscreen", true); + } + + // In tabs-on-top mode, move window controls to the tab bar, + // and in tabs-on-bottom mode, move them back to the navigation toolbar. + // When there is a chance the tab bar may be collapsed, put window + // controls on nav bar. + var fullscreenctls = document.getElementById("window-controls"); + var navbar = document.getElementById("nav-bar"); + var ctlsOnTabbar = window.toolbar.visible && + (navbar.collapsed || (TabsOnTop.enabled && + !gPrefService.getBoolPref("browser.tabs.autoHide"))); + 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 = aShow; + + ToolbarIconColor.inferFromText(); + } +}; +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" +#ifdef XP_MACOSX + return parseFloat(Services.sysinfo.getProperty("version")) >= 11 && + document.documentElement.getAttribute("fullscreenbutton") == "true"; +#else + return false; +#endif +}); diff --git a/application/palemoon/base/content/browser-fullZoom.js b/application/palemoon/base/content/browser-fullZoom.js new file mode 100644 index 0000000000..0837bf7c22 --- /dev/null +++ b/application/palemoon/base/content/browser-fullZoom.js @@ -0,0 +1,550 @@ +/* +#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 + */ + +// One of the possible values for the mousewheel.* preferences. +// From nsEventStateManager.cpp. +const MOUSE_SCROLL_ZOOM = 3; + +/** + * 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() { + // Listen for scrollwheel events so we can save scrollwheel-based changes. + window.addEventListener("DOMMouseScroll", this, false); + + // 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.clear(); + this._initialLocations = null; + }, + + destroy: function FullZoom_destroy() { + gPrefService.removeObserver("browser.zoom.", this); + this._cps2.removeObserverForName(this.name, this); + window.removeEventListener("DOMMouseScroll", this, false); + }, + + + //**************************************************************************// + // Event Handlers + + // nsIDOMEventListener + + handleEvent: function FullZoom_handleEvent(event) { + switch (event.type) { + case "DOMMouseScroll": + this._handleMouseScrolled(event); + break; + } + }, + + _handleMouseScrolled: function FullZoom__handleMouseScrolled(event) { + // Construct the "mousewheel action" pref key corresponding to this event. + // Based on nsEventStateManager::WheelPrefs::GetBasePrefName(). + var pref = "mousewheel."; + + var pressedModifierCount = event.shiftKey + event.ctrlKey + event.altKey + + event.metaKey + event.getModifierState("OS"); + if (pressedModifierCount != 1) { + pref += "default."; + } else if (event.shiftKey) { + pref += "with_shift."; + } else if (event.ctrlKey) { + pref += "with_control."; + } else if (event.altKey) { + pref += "with_alt."; + } else if (event.metaKey) { + pref += "with_meta."; + } else { + pref += "with_win."; + } + + pref += "action"; + + // Don't do anything if this isn't a "zoom" scroll event. + var isZoomEvent = false; + try { + isZoomEvent = (gPrefService.getIntPref(pref) == MOUSE_SCROLL_ZOOM); + } catch (e) {} + if (!isZoomEvent) + return; + + // XXX Lazily cache all the possible action prefs so we don't have to get + // them anew from the pref service for every scroll event? We'd have to + // make sure to observe them so we can update the cache when they change. + + // We have to call _applyZoomToPref in a timeout because we handle the + // event before the event state manager has a chance to apply the zoom + // during nsEventStateManager::PostHandleEvent. + let browser = gBrowser.selectedBrowser; + let token = this._getBrowserToken(browser); + window.setTimeout(function () { + if (token.isCurrent) + this._applyZoomToPref(browser); + }.bind(this), 0); + }, + + // 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) { + this._onContentPrefChanged(aGroup, aValue); + }, + + onContentPrefRemoved: function FullZoom_onContentPrefRemoved(aGroup, aName) { + this._onContentPrefChanged(aGroup, undefined); + }, + + /** + * 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) { + 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 domain = this._cps2.extractDomain(browser.currentURI.spec); + if (aGroup) { + if (aGroup == domain) + 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 ctxt = this._loadContextFromBrowser(browser); + 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(); + return; + } + + // Avoid the cps roundtrip and apply the default/global pref. + if (aURI.spec == "about:blank") { + this._applyPrefToZoom(undefined, browser, + this._notifyOnLocationChange.bind(this)); + 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(); + 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)); + 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(); + return; + } + this._applyPrefToZoom(value, browser, + this._notifyOnLocationChange.bind(this)); + }.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 of the page in the current browser to the global zoom + * level. + */ + reset: function FullZoom_reset() { + let browser = gBrowser.selectedBrowser; + let token = this._getBrowserToken(browser); + this._getGlobalValue(browser, function (value) { + if (token.isCurrent) { + ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value); + this._ignorePendingZoomAccesses(browser); + } + }); + this._removePref(browser); + }, + + /** + * 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, function (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) { + 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) { + 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; + }, + }; + }, + + /** + * 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. + * + * WARNING: callback may be called synchronously or asynchronously. The + * reason is that it's usually desirable to avoid turns of the event loop + * where possible, since they can lead to visible, jarring jumps in zoom + * level. It's not always possible to avoid them, though. As a convenience, + * then, this method takes a callback and returns nothing. + * + * @param browser The content browser pertaining to the zoom. + * @param callback Synchronously or asynchronously called when done. It's + * bound to this object (FullZoom) and called as: + * callback(prefValue) + */ + _getGlobalValue: function FullZoom__getGlobalValue(browser, callback) { + // * !("_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. + if ("_globalValue" in this) { + callback.call(this, this._globalValue, true); + return; + } + let value = undefined; + this._cps2.getGlobal(this.name, this._loadContextFromBrowser(browser), { + handleResult: function (pref) value = pref.value, + handleCompletion: function (reason) { + this._globalValue = this._ensureValid(value); + callback.call(this, this._globalValue); + }.bind(this) + }); + }, + + /** + * Gets the load context from the given content 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 a "browser-fullZoom:locationChange" notification + * so that tests can select tabs, load pages, etc. and 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() { + this._executeSoon(function () { + Services.obs.notifyObservers(null, "browser-fullZoom:locationChange", ""); + }); + }, + + _executeSoon: function FullZoom__executeSoon(callback) { + if (!callback) + return; + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); + }, +}; diff --git a/application/palemoon/base/content/browser-gestureSupport.js b/application/palemoon/base/content/browser-gestureSupport.js new file mode 100644 index 0000000000..d88f47c793 --- /dev/null +++ b/application/palemoon/base/content/browser-gestureSupport.js @@ -0,0 +1,1059 @@ +# 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. + +let 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) { + // Bug 863514 - Make gesture support work in electrolysis + if (gMultiProcessBrowser) + return; + + const gestureEvents = ["SwipeGestureStart", + "SwipeGestureUpdate", "SwipeGestureEnd", "SwipeGesture", + "MagnifyGestureStart", "MagnifyGestureUpdate", "MagnifyGesture", + "RotateGestureStart", "RotateGestureUpdate", "RotateGesture", + "TapGesture", "PressTapGesture"]; + + let addRemove = aAddListener ? window.addEventListener : + window.removeEventListener; + + gestureEvents.forEach(function (event) addRemove("Moz" + event, this, true), + this); + }, + + /** + * 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 = function(aThreshold, aLatched) + ({ threshold: aThreshold, latched: !!aLatched }); + + switch (aEvent.type) { + case "MozSwipeGestureStart": + aEvent.preventDefault(); + this._setupSwipeGesture(aEvent); + 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(); +#ifdef XP_WIN + this._setupGesture(aEvent, "pinch", def(25, 0), "out", "in"); +#else + this._setupGesture(aEvent, "pinch", def(150, 1), "out", "in"); +#endif + 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] in Iterator(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"; + }, + + /** + * Sets up the history swipe animations for a swipe gesture event, if enabled. + * + * @param aEvent + * The swipe gesture start event. + */ + _setupSwipeGesture: function GS__setupSwipeGesture(aEvent) { + if (!this._swipeNavigatesHistory(aEvent)) + return; + + 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; + + gHistorySwipeAnimation.startAnimation(); + + 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 = []; + ["shift", "alt", "ctrl", "meta"].forEach(function (key) { + 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 = "get" + (type == "boolean" ? "Bool" : + type == "number" ? "Int" : "Char") + "Pref"; + return gPrefService[getFunc](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) +let 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.mozMatchesSelector( + ":-moz-locale-dir(ltr)"); + this._trackedSnapshots = []; + this._historyIndex = -1; + this._boxWidth = -1; + this._maxSnapshots = this._getMaxSnapshots(); + this._lastSwipeDir = ""; + + // 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.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.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). + */ + startAnimation: function HSA_startAnimation() { + if (this.isAnimationRunning()) { + gBrowser.stop(); + this._lastSwipeDir = "RELOAD"; // just ensure that != "" + this._canGoBack = this.canGoBack(); + this._canGoForward = this.canGoForward(); + this._handleFastSwiping(); + } + else { + this._historyIndex = gBrowser.webNavigation.sessionHistory.index; + this._canGoBack = this.canGoBack(); + this._canGoForward = this.canGoForward(); + if (this.active) { + this._takeSnapshot(); + this._installPrevAndNextSnapshots(); + this._addBoxes(); + this._lastSwipeDir = ""; + } + } + this.updateAnimation(0); + }, + + /** + * Stops the swipe animation. + */ + stopAnimation: function HSA_stopAnimation() { + gHistorySwipeAnimation._removeBoxes(); + }, + + /** + * 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; + + if ((aVal >= 0 && this.isLTR) || + (aVal <= 0 && !this.isLTR)) { + if (aVal > 1) + aVal = 1; // Cap value to avoid sliding the page further than allowed. + + if (this._canGoBack) + this._prevBox.collapsed = false; + else + 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); + + // The forward page should be pushed offscreen all the way to the right. + this._positionBox(this._nextBox, 1); + } + else { + if (aVal < -1) + aVal = -1; // Cap value to avoid sliding the page further than allowed. + + // 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). + if (this._canGoForward) { + let offset = this.isLTR ? 1 : -1; + this._positionBox(this._curBox, 0); + this._positionBox(this._nextBox, offset + aVal); // aVal is negative + } + else { + this._prevBox.collapsed = true; + this._positionBox(this._curBox, aVal); + } + } + }, + + /** + * Event handler for events relevant to the history swipe animation. + * + * @param aEvent + * An event to process. + */ + handleEvent: function HSA_handleEvent(aEvent) { + switch (aEvent.type) { + case "TabClose": + let browser = gBrowser.getBrowserForTab(aEvent.target); + this._removeTrackedSnapshot(-1, browser); + break; + case "pageshow": + case "popstate": + if (this.isAnimationRunning()) { + if (aEvent.target != gBrowser.selectedBrowser.contentDocument) + break; + this.stopAnimation(); + } + this._historyIndex = gBrowser.webNavigation.sessionHistory.index; + break; + case "pagehide": + if (aEvent.target == gBrowser.selectedBrowser.contentDocument) { + // Take a snapshot of a page whenever it's about to be navigated away + // from. + this._takeSnapshot(); + } + 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() { + if (this._lastSwipeDir != "") + this._navigateToHistoryIndex(); + else + this.stopAnimation(); + }, + + /** + * 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 { + gBrowser.webNavigation.sessionHistory.getEntryAtIndex(aIndex, false); + } + catch(ex) { + return false; + } + return true; + }, + + /** + * 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); + } + }, + + /** + * 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); + + this._boxWidth = this._curBox.getBoundingClientRect().width; // cache width + }, + + /** + * 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; + }, + + /** + * 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) { + aBox.style.transform = "translateX(" + this._boxWidth * aPosition + "px)"; + }, + + /** + * Takes a snapshot of the page the browser is currently on. + */ + _takeSnapshot: function HSA__takeSnapshot() { + if ((this._maxSnapshots < 1) || + (gBrowser.webNavigation.sessionHistory.index < 0)) + return; + + let browser = gBrowser.selectedBrowser; + let r = browser.getBoundingClientRect(); + let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", + "canvas"); + canvas.mozOpaque = true; + canvas.width = r.width; + canvas.height = r.height; + let ctx = canvas.getContext("2d"); + let zoom = browser.markupDocumentViewer.fullZoom; + ctx.scale(zoom, zoom); + ctx.drawWindow(browser.contentWindow, 0, 0, r.width, r.height, "white", + ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW | + ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES | + ctx.DRAWWINDOW_USE_WIDGET_LAYERS); + + this._installCurrentPageSnapshot(canvas); + this._assignSnapshotToCurrentBrowser(canvas); + }, + + /** + * 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 = browser.webNavigation.sessionHistory.index; + + 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] = aCanvas; + + // Kick off snapshot compression. + aCanvas.toBlob(function(aBlob) { + snapshots[currIndex] = aBlob; + }, "image/png" + ); + }, + + /** + * 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]; + 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 = URL.createObjectURL(aBlob); + img.onload = function() { + URL.revokeObjectURL(url); + }; + img.src = url; + return img; + }, + + /** + * 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; + if (!currSnapshot) { + let snapshots = gBrowser.selectedBrowser.snapshots || {}; + let currIndex = this._historyIndex; + if (currIndex in snapshots) + currSnapshot = this._convertToImg(snapshots[currIndex]); + } + 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]); + document.mozSetImageElement("historySwipeAnimationPreviousPageSnapshot", + prevSnapshot); + + let nextIndex = currIndex + 1; + let nextSnapshot = null; + if (nextIndex in snapshots) + nextSnapshot = this._convertToImg(snapshots[nextIndex]); + document.mozSetImageElement("historySwipeAnimationNextPageSnapshot", + nextSnapshot); + }, +}; diff --git a/application/palemoon/base/content/browser-menubar.inc b/application/palemoon/base/content/browser-menubar.inc new file mode 100644 index 0000000000..f818f5149e --- /dev/null +++ b/application/palemoon/base/content/browser-menubar.inc @@ -0,0 +1,595 @@ +# -*- 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"> + <menuitem id="menu_newNavigatorTab" + label="&tabCmd.label;" + command="cmd_newNavigatorTab" + key="key_newNavigatorTab" + accesskey="&tabCmd.accesskey;"/> + <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"/> + <menuitem id="menu_openLocation" + class="show-only-for-keyboard" + label="&openLocationCmd.label;" + command="Browser:OpenLocation" + key="focusURLBar" + accesskey="&openLocationCmd.accesskey;"/> + <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/> + <menuitem id="menu_printSetup" + label="&printSetupCmd.label;" + accesskey="&printSetupCmd.accesskey;" + command="cmd_pageSetup"/> +#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_restart" + label="&appMenuRestart.label;" + accesskey="&appMenuRestart.accesskey;" + command="cmd_restartApplication"/> + <menuitem id="menu_FileQuitItem" +#ifdef XP_WIN + label="&quitApplicationCmdWin.label;" + accesskey="&quitApplicationCmdWin.accesskey;" +#else +#ifdef XP_MACOSX + label="&quitApplicationCmdMac.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_tabsOnTop" + command="cmd_ToggleTabsOnTop" + type="checkbox" + label="&viewTabsOnTop.label;" + accesskey="&viewTabsOnTop.accesskey;"/> + <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" + label="&bookmarksButton.label;"/> + <menuitem id="menu_historySidebar" + key="key_gotoHistory" + observes="viewHistorySidebar" + label="&historyButton.label;"/> + </menupopup> + </menu> + <menuseparator/> + <menuitem id="menu_stop" + class="show-only-for-keyboard" + label="&stopCmd.label;" + accesskey="&stopCmd.accesskey;" + command="Browser:Stop" +#ifdef XP_MACOSX + key="key_stop_mac"/> +#else + key="key_stop"/> +#endif + <menuitem id="menu_reload" + class="show-only-for-keyboard" + label="&reloadCmd.label;" + accesskey="&reloadCmd.accesskey;" + key="key_reload" + command="Browser:ReloadOrDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuseparator class="show-only-for-keyboard"/> + <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_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="SwitchDocumentDirection(window.content)"/> + </menupopup> + </menu> + + <menu id="history-menu" + label="&historyMenu.label;" + accesskey="&historyMenu.accesskey;"> + <menupopup id="goPopup" +#ifndef XP_MACOSX + placespopup="true" +#endif + context="placesContext" + oncommand="this.parentNode._placesView._onCommand(event);" + onclick="checkForMiddleClick(this, event);" + onpopupshowing="if (!this.parentNode._placesView) + new HistoryMenu(event);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <menuitem id="historyMenuBack" + class="show-only-for-keyboard" + label="&backCmd.label;" +#ifdef XP_MACOSX + key="goBackKb2" +#else + key="goBackKb" +#endif + command="Browser:BackOrBackDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="historyMenuForward" + class="show-only-for-keyboard" + label="&forwardCmd.label;" +#ifdef XP_MACOSX + key="goForwardKb2" +#else + key="goForwardKb" +#endif + command="Browser:ForwardOrForwardDuplicate" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="historyMenuHome" + class="show-only-for-keyboard" + label="&historyHomeCmd.label;" + oncommand="BrowserGoHome(event);" + onclick="checkForMiddleClick(this, event);" + key="goHome"/> + <menuseparator id="historyMenuHomeSeparator" + class="show-only-for-keyboard"/> + <menuitem id="menu_showAllHistory" + label="&showAllHistoryCmd2.label;" +#ifndef XP_MACOSX + class="menuitem-iconic" + key="showAllHistoryKb" +#endif + command="Browser:ShowAllHistory"/> + <menuitem id="sanitizeItem" +#ifndef XP_MACOSX + class="menuitem-iconic" +#endif + label="&clearRecentHistory.label;" + key="key_sanitize" + command="Tools:Sanitize"/> + <menuseparator id="sanitizeSeparator"/> +#ifdef MOZ_SERVICES_SYNC + <menuitem id="sync-tabs-menuitem" + class="syncTabsMenuItem" + label="&syncTabsMenu2.label;" + oncommand="BrowserOpenSyncTabs();" + disabled="true"/> +#endif + <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="PlacesCommandHook.updateBookmarkAllTabsCommand(); + if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=BOOKMARKS_MENU');" + tooltip="bhTooltip" popupsinherittooltip="true"> + <menuitem id="bookmarksShowAll" +#ifndef XP_MACOSX + class="menuitem-iconic" +#endif + label="&organizeBookmarks.label;" + command="Browser:ShowAllBookmarks" + key="manBookmarkKb"/> + <menuseparator id="organizeBookmarksSeparator"/> + <menuitem id="menu_bookmarkThisPage" +#ifndef XP_MACOSX + class="menuitem-iconic" +#endif + label="&bookmarkThisPageCmd.label;" + command="Browser:AddBookmarkAs" + 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 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> + <menuseparator id="bookmarksMenuItemsSeparator"/> + <!-- Bookmarks menu items --> + <menuseparator builder="end" + class="hide-if-empty-places-result"/> + <menuitem id="menu_unsortedBookmarks" +#ifndef XP_MACOSX + class="menuitem-iconic" +#endif + label="&unsortedBookmarksCmd.label;" + oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/> + </menupopup> + </menu> + + <menu id="tools-menu" + label="&toolsMenu.label;" + accesskey="&toolsMenu.accesskey;"> + <menupopup id="menu_ToolsPopup" +#ifdef MOZ_SERVICES_SYNC + onpopupshowing="gSyncUI.updateUI();" +#endif + > + <menuitem id="menu_search" + class="show-only-for-keyboard" + label="&search.label;" + accesskey="&search.accesskey;" + key="key_search" + command="Tools:Search"/> + <menuseparator id="browserToolsSeparator" + class="show-only-for-keyboard"/> + <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"/> + <menuitem id="menu_openPermissions" + label="&permissions.label;" + command="Tools:Permissions" + accesskey="&permissions.accesskey;"/> +#ifdef MOZ_SERVICES_SYNC + <!-- only one of sync-setup or sync-menu will be showing at once --> + <menuitem id="sync-setup" + label="&syncSetup.label;" + accesskey="&syncSetup.accesskey;" + observes="sync-setup-state" + oncommand="gSyncUI.openSetup()"/> + <menuitem id="sync-syncnowitem" + label="&syncSyncNowItem.label;" + accesskey="&syncSyncNowItem.accesskey;" + observes="sync-syncnow-state" + oncommand="gSyncUI.doSync(event);"/> +#endif + <menuseparator id="devToolsSeparator"/> + <menu id="webDeveloperMenu" + label="&webDeveloperMenu.label;" + accesskey="&webDeveloperMenu.accesskey;"> + <menupopup id="menuWebDeveloperPopup"> +#ifdef MOZ_DEVTOOLS + <menuitem id="menu_devToolbox" + observes="devtoolsMenuBroadcaster_DevToolbox" + accesskey="&devToolboxMenuItem.accesskey;"/> + <menuseparator id="menu_devtools_separator"/> + <menuitem id="menu_devToolbar" + observes="devtoolsMenuBroadcaster_DevToolbar" + accesskey="&devToolbarMenu.accesskey;"/> + <menuitem id="menu_chromeDebugger" + observes="devtoolsMenuBroadcaster_ChromeDebugger"/> + <menuitem id="menu_browserConsole" + observes="devtoolsMenuBroadcaster_BrowserConsole" + accesskey="&browserConsoleCmd.accesskey;"/> + <menuitem id="menu_responsiveUI" + observes="devtoolsMenuBroadcaster_ResponsiveUI" + accesskey="&responsiveDesignTool.accesskey;"/> + <menuitem id="menu_eyedropper" + observes="devtoolsMenuBroadcaster_Eyedropper" + accesskey="&eyedropper.accesskey;"/> + <menuitem id="menu_scratchpad" + observes="devtoolsMenuBroadcaster_Scratchpad" + accesskey="&scratchpad.accesskey;"/> +#endif + <menuitem id="menu_pageSource" + observes="devtoolsMenuBroadcaster_PageSource" + accesskey="&pageSourceCmd.accesskey;"/> + <menuitem id="javascriptConsole" + observes="devtoolsMenuBroadcaster_ErrorConsole" + accesskey="&errorConsoleCmd.accesskey;"/> +#ifdef MOZ_DEVTOOLS + <menuitem id="menu_devtools_connect" + observes="devtoolsMenuBroadcaster_connect"/> +#endif + <menuseparator id="devToolsEndSeparator"/> + <menuitem id="getMoreDevtools" + observes="devtoolsMenuBroadcaster_GetMoreTools" + accesskey="&getMoreDevtoolsCmd.accesskey;"/> + </menupopup> + </menu> + <menuitem id="menu_pageInfo" + accesskey="&pageInfoCmd.accesskey;" + label="&pageInfoCmd.label;" +#ifndef XP_WIN + key="key_viewInfo" +#endif + command="View:PageInfo"/> + <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/palemoon/base/content/browser-menudragging.js b/application/palemoon/base/content/browser-menudragging.js new file mode 100644 index 0000000000..cf26b2ba42 --- /dev/null +++ b/application/palemoon/base/content/browser-menudragging.js @@ -0,0 +1,362 @@ +// 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/. +// +// Based on original code by alice0775 https://github.com/alice0775 + + +"use strict"; +var browserMenuDragging = { + //-- config -- + STAY_OPEN_ONDRAGEXIT: false, + DEBUG: false, + //-- config -- + + menupopup: ['bookmarksMenuPopup', + 'PlacesToolbar', + 'BMB_bookmarksPopup', + 'appmenu_bookmarksPopup', + 'BookmarksMenuToolButtonPopup', + 'UnsortedBookmarksFolderToolButtonPopup', + 'bookmarksMenuPopup-context'], + timer:[], + count:[], + + + init: function(){ + window.removeEventListener('load', this, false); + window.addEventListener('unload', this, false); + this.addPrefListener(this.PrefListener); + + window.addEventListener('aftercustomization', this, false); + + this.initPref(); + this.delayedStartup(); + }, + + uninit: function(){ + window.removeEventListener('unload', this, false); + this.removePrefListener(this.PrefListener); + + window.removeEventListener('aftercustomization', this, false); + + for (var i = 0; i < this.menupopup.length; i++){ + var menupopup = document.getElementById(this.menupopup[i]); + if (menupopup){ + menupopup.removeEventListener('popupshowing', this, false); + menupopup.removeEventListener('popuphiding', this, false); + } + } + + }, + + initPref: function(){ + this.STAY_OPEN_ONDRAGEXIT = + this.getPref('browser.menu.dragging.stayOpen', + 'bool', false); + this.DEBUG = + this.getPref('browser.menu.dragging.debug', + 'bool', false); + }, + + //delayed startup + delayedStartup: function(){ + //wait until construction of bookmarksBarContent is completed. + for (var i = 0; i < this.menupopup.length; i++){ + this.count[i] = 0; + this.timer[i] = setInterval(function(self, i){ + if(++self.count[i] > 50 || document.getElementById(self.menupopup[i])){ + clearInterval(self.timer[i]); + var menupopup = document.getElementById(self.menupopup[i]); + if (menupopup) { + menupopup.addEventListener('popupshowing', self, false); + menupopup.addEventListener('popuphiding', self, false); + } + } + }, 250, this, i); + } + }, + + handleEvent: function(event){ + switch (event.type) { + case 'popupshowing': + this.popupshowing(event); + break; + case 'popuphiding': + this.popuphiding(event); + break; + case 'aftercustomization': + setTimeout(function(self){self.delayedStartup(self);}, 0, this); + break; + case 'load': + this.init(); + break; + case 'unload': + this.uninit(); + break; + } + }, + + popuphiding: function(event) { + var menupopup = event.originalTarget; + menupopup.parentNode.parentNode.openNode = null; + + if (menupopup.parentNode.localName == 'toolbarbutton') { + // Fix for Bug 225434 - dragging bookmark from personal toolbar and releasing + // (on same bookmark or elsewhere) or clicking on bookmark menu then cancelling + // leaves button depressed/sunken when hovered + menupopup.parentNode.parentNode._openedMenuButton = null; + + if (!PlacesControllerDragHelper.getSession()) + // Clear the dragover attribute if present, if we are dragging into a + // folder in the hierachy of current opened popup we don't clear + // this attribute on clearOverFolder. See Notify for closeTimer. + if (menupopup.parentNode.hasAttribute('dragover')) + menupopup.parentNode.removeAttribute('dragover'); + } + }, + + popupshowing: function(event) { + var menupopup = event.originalTarget; + browserMenuDragging.debug("popupshowing ===============\n" + menupopup.parentNode.getAttribute('label')); + + var parentPopup = menupopup.parentNode.parentNode; + + if (!!parentPopup.openNode){ + try { + parentPopup.openNode.hidePopup(); + } catch(e){} + } + parentPopup.openNode = menupopup; + + menupopup.onDragStart = function (event) { + // Bug 555474 - While bookmark is dragged, the tooltip should not appear + browserMenuDragging.hideTooltip(); + } + + menupopup.onDragOver = function (event) { + // Bug 555474 - While bookmark is dragged, the tooltip should not appear + browserMenuDragging.hideTooltip(); + + var target = event.originalTarget; + while (target) { + if (/menupopup/.test(target.localName)) + break; + target = target.parentNode; + } + if (this != target) + return; + event.stopPropagation(); + browserMenuDragging.debug("onDragOver " + "\n" + this.parentNode.getAttribute('label')); + + PlacesControllerDragHelper.currentDropTarget = event.target; + let dt = event.dataTransfer; + + let dropPoint = this._getDropPoint(event); + + if (!dropPoint || !dropPoint.ip || + !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)) { + this._indicatorBar.hidden = true; + event.stopPropagation(); + return; + } + + // Mark this popup as being dragged over. + this.setAttribute('dragover', 'true'); + + if (dropPoint.folderElt) { + // We are dragging over a folder. + // _overFolder should take the care of opening it on a timer. + if (this._overFolder.elt && + this._overFolder.elt != dropPoint.folderElt) { + } + if (!this._overFolder.elt) { + this._overFolder.elt = dropPoint.folderElt; + // Create the timer to open this folder. + this._overFolder.openTimer = this._overFolder + .setTimer(this._overFolder.hoverTime); + } + } + else { + // We are not dragging over a folder. + } + + // Autoscroll the popup strip if we drag over the scroll buttons. + let anonid = event.originalTarget.getAttribute('anonid'); + let scrollDir = anonid == 'scrollbutton-up' ? -1 : + anonid == 'scrollbutton-down' ? 1 : 0; + if (scrollDir != 0) { + this._scrollBox.scrollByIndex(scrollDir, false); + } + + // Check if we should hide the drop indicator for this target. + if (dropPoint.folderElt || this._hideDropIndicator(event)) { + this._indicatorBar.hidden = true; + event.preventDefault(); + event.stopPropagation(); + return; + } + + // We should display the drop indicator relative to the arrowscrollbox. + let sbo = this._scrollBox.scrollBoxObject; + let newMarginTop = 0; + if (scrollDir == 0) { + let elt = this.firstChild; + while (elt && event.screenY > elt.boxObject.screenY + + elt.boxObject.height / 2) + elt = elt.nextSibling; + newMarginTop = elt ? elt.boxObject.screenY - sbo.screenY : + sbo.height; + } + else if (scrollDir == 1) + newMarginTop = sbo.height; + + // Set the new marginTop based on arrowscrollbox. + newMarginTop += sbo.y - this._scrollBox.boxObject.y; + this._indicatorBar.firstChild.style.marginTop = newMarginTop + 'px'; + this._indicatorBar.hidden = false; + + event.preventDefault(); + event.stopPropagation(); + } + + menupopup.onDragExit = function (event) { + var target = event.originalTarget; + while (target) { + if (/menupopup/.test(target.localName)) + break; + target = target.parentNode; + } + if (this != target) + return; + event.stopPropagation(); + browserMenuDragging.debug("onDragExit " + browserMenuDragging.STAY_OPEN_ONDRAGEXIT); + + PlacesControllerDragHelper.currentDropTarget = null; + this.removeAttribute('dragover'); + + // If we have not moved to a valid new target clear the drop indicator + // this happens when moving out of the popup. + target = event.relatedTarget; + if (!target) + this._indicatorBar.hidden = true; + + // Close any folder being hovered over + if (this._overFolder.elt) { + this._overFolder.closeTimer = this._overFolder + .setTimer(this._overFolder.hoverTime); + } + + // The auto-opened attribute is set when this folder was automatically + // opened after the user dragged over it. If this attribute is set, + // auto-close the folder on drag exit. + // We should also try to close this popup if the drag has started + // from here, the timer will check if we are dragging over a child. + if (this.hasAttribute('autoopened') || + !browserMenuDragging.STAY_OPEN_ONDRAGEXIT && + this.hasAttribute('dragstart')) { + this._overFolder.closeMenuTimer = this._overFolder + .setTimer(this._overFolder.hoverTime); + } + + event.stopPropagation(); + } + + menupopup.addEventListener('dragstart', menupopup.onDragStart, true); + menupopup.addEventListener('dragover', menupopup.onDragOver, true); + menupopup.addEventListener('dragleave', menupopup.onDragExit, true); + }, + + hideTooltip: function() { + ['bhTooltip', 'btTooltip2'].forEach(function(id) { + var tooltip = document.getElementById(id); + if (tooltip) + tooltip.hidePopup(); + }); + }, + + get getVer(){ + const Cc = Components.classes; + const Ci = Components.interfaces; + var info = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo); + var ver = parseInt(info.version.substr(0,3) * 10,10) / 10; + return ver; + }, + + debug: function(aMsg){ + if (!browserMenuDragging.DEBUG) + return; + Components.classes["@mozilla.org/consoleservice;1"] + .getService(Components.interfaces.nsIConsoleService) + .logStringMessage(aMsg); + }, + + getPref: function(aPrefString, aPrefType, aDefault){ + var xpPref = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + try{ + switch (aPrefType){ + case 'complex': + return xpPref.getComplexValue(aPrefString, Components.interfaces.nsILocalFile); break; + case 'str': + return xpPref.getCharPref(aPrefString).toString(); break; + case 'int': + return xpPref.getIntPref(aPrefString); break; + case 'bool': + default: + return xpPref.getBoolPref(aPrefString); break; + } + }catch(e){ + } + return aDefault; + }, + + setPref: function(aPrefString, aPrefType, aValue){ + var xpPref = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + try{ + switch (aPrefType){ + case 'complex': + return xpPref.setComplexValue(aPrefString, Components.interfaces.nsILocalFile, aValue); break; + case 'str': + return xpPref.setCharPref(aPrefString, aValue); break; + case 'int': + aValue = parseInt(aValue); + return xpPref.setIntPref(aPrefString, aValue); break; + case 'bool': + default: + return xpPref.setBoolPref(aPrefString, aValue); break; + } + }catch(e){ + } + return null; + }, + + addPrefListener: function(aObserver) { + try { + var pbi = Components.classes["@mozilla.org/preferences;1"]. + getService(Components.interfaces.nsIPrefBranch2); + pbi.addObserver(aObserver.domain, aObserver, false); + } catch(e) {} + }, + + removePrefListener: function(aObserver) { + try { + var pbi = Components.classes["@mozilla.org/preferences;1"]. + getService(Components.interfaces.nsIPrefBranch2); + pbi.removeObserver(aObserver.domain, aObserver); + } catch(e) {} + }, + + PrefListener:{ + domain : 'browser.menu.dragging.stayOpen', + + observe : function(aSubject, aTopic, aPrefstring) { + if (aTopic == 'nsPref:changed') { + browserMenuDragging.initPref(); + } + } + } +} + +window.addEventListener('load', browserMenuDragging, false);
\ No newline at end of file diff --git a/application/palemoon/base/content/browser-menudragging.xul b/application/palemoon/base/content/browser-menudragging.xul new file mode 100644 index 0000000000..f5cabe5f62 --- /dev/null +++ b/application/palemoon/base/content/browser-menudragging.xul @@ -0,0 +1,13 @@ +<?xml version="1.0"?> + + +<overlay id="menuDraggingOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<window id="main-window"> + + <script type="application/x-javascript" src="chrome://browser/content/browser-menudragging.js" /> + +</window> + +</overlay> diff --git a/application/palemoon/base/content/browser-places.js b/application/palemoon/base/content/browser-places.js new file mode 100644 index 0000000000..cf9c28597f --- /dev/null +++ b/application/palemoon/base/content/browser-places.js @@ -0,0 +1,1303 @@ +# 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/. + +//////////////////////////////////////////////////////////////////////////////// +//// StarUI + +var StarUI = { + _itemId: -1, + uri: null, + _batching: false, + + _element: function(aID) { + return document.getElementById(aID); + }, + + // Edit-bookmark panel + get panel() { + delete this.panel; + var element = this._element("editBookmarkPanel"); + // initially the panel is hidden + // to avoid impacting startup / new window performance + element.hidden = false; + element.addEventListener("popuphidden", this, false); + element.addEventListener("keypress", 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(function (id) this._element(id), this); + }, + + _blockCommands: function SU__blockCommands() { + this._blockedCommands.forEach(function (elt) { + // make sure not to permanently disable this item (see bug 409155) + if (elt.hasAttribute("wasDisabled")) + return; + if (elt.getAttribute("disabled") == "true") { + elt.setAttribute("wasDisabled", "true"); + } else { + elt.setAttribute("wasDisabled", "false"); + elt.setAttribute("disabled", "true"); + } + }); + }, + + _restoreCommandsState: function SU__restoreCommandsState() { + this._blockedCommands.forEach(function (elt) { + if (elt.getAttribute("wasDisabled") != "true") + elt.removeAttribute("disabled"); + elt.removeAttribute("wasDisabled"); + }); + }, + + // nsIDOMEventListener + handleEvent: function SU_handleEvent(aEvent) { + switch (aEvent.type) { + case "popuphidden": + if (aEvent.originalTarget == this.panel) { + if (!this._element("editBookmarkPanelContent").hidden) + this.quitEditMode(); + + this._restoreCommandsState(); + this._itemId = -1; + if (this._batching) { + PlacesUtils.transactionManager.endBatch(false); + this._batching = false; + } + + switch (this._actionOnHide) { + case "cancel": { + PlacesUtils.transactionManager.undoTransaction(); + break; + } + case "remove": { + // Remove all bookmarks for the bookmark's url, this also removes + // the tags for the url. + PlacesUtils.transactionManager.beginBatch(null); + let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval); + for (let i = 0; i < itemIds.length; i++) { + let txn = new PlacesRemoveItemTransaction(itemIds[i]); + PlacesUtils.transactionManager.doTransaction(txn); + } + PlacesUtils.transactionManager.endBatch(false); + break; + } + } + this._actionOnHide = ""; + } + break; + case "keypress": + if (aEvent.defaultPrevented) { + // The event has already been consumed inside of the panel. + break; + } + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_ESCAPE: + if (!this._element("editBookmarkPanelContent").hidden) + this.cancelButtonOnCommand(); + break; + case KeyEvent.DOM_VK_RETURN: + if (aEvent.target.className == "expander-up" || + aEvent.target.className == "expander-down" || + aEvent.target.id == "editBMPanel_newFolderButton") { + //XXX Why is this necessary? The defaultPrevented check should + // be enough. + break; + } + this.panel.hidePopup(); + break; + } + break; + } + }, + + _overlayLoaded: false, + _overlayLoading: false, + showEditBookmarkPopup: + function SU_showEditBookmarkPopup(aItemId, aAnchorElement, aPosition) { + // Performance: load the overlay the first time the panel is opened + // (see bug 392443). + if (this._overlayLoading) + return; + + if (this._overlayLoaded) { + this._doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition); + return; + } + + this._overlayLoading = true; + document.loadOverlay( + "chrome://browser/content/places/editBookmarkOverlay.xul", + (function (aSubject, aTopic, aData) { + //XXX We just caused localstore.rdf to be re-applied (bug 640158) + retrieveToolbarIconsizesFromTheme(); + + // 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(aItemId, aAnchorElement, aPosition); + }).bind(this) + ); + }, + + _doShowEditBookmarkPanel: + function SU__doShowEditBookmarkPanel(aItemId, aAnchorElement, aPosition) { + if (this.panel.state != "closed") + return; + + this._blockCommands(); // un-done in the popuphiding handler + + // Set panel title: + // if we are batching, i.e. the bookmark has been added now, + // then show Page Bookmarked, else if the bookmark did already exist, + // we are about editing it, then use Edit This Bookmark. + this._element("editBookmarkPanelTitle").value = + this._batching ? + gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") : + gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"); + + // No description; show the Done, Cancel; + this._element("editBookmarkPanelDescription").textContent = ""; + this._element("editBookmarkPanelBottomButtons").hidden = false; + this._element("editBookmarkPanelContent").hidden = false; + + // The remove button is shown only if we're not already batching, i.e. + // if the cancel button/ESC does not remove the bookmark. + this._element("editBookmarkPanelRemoveButton").hidden = this._batching; + + // The label of the remove button differs if the URI is bookmarked + // multiple times. + var bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI); + var forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label"); + var 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 = aItemId !== undefined ? aItemId : this._itemId; + this.beginBatch(); + + this.panel.openPopup(aAnchorElement, aPosition); + + gEditItemOverlay.initPanel(this._itemId, + { hiddenRows: ["description", "location", + "loadInSidebar", "keyword"] }); + }, + + panelShown: + function SU_panelShown(aEvent) { + if (aEvent.target == this.panel) { + if (!this._element("editBookmarkPanelContent").hidden) { + let fieldToFocus = "editBMPanel_" + + gPrefService.getCharPref("browser.bookmarks.editDialog.firstEditField"); + var elt = this._element(fieldToFocus); + elt.focus(); + elt.select(); + } + else { + // Note this isn't actually used anymore, we should remove this + // once we decide not to bring back the page bookmarked notification + this.panel.focus(); + } + } + }, + + quitEditMode: function SU_quitEditMode() { + this._element("editBookmarkPanelContent").hidden = true; + this._element("editBookmarkPanelBottomButtons").hidden = true; + gEditItemOverlay.uninitPanel(true); + }, + + cancelButtonOnCommand: function SU_cancelButtonOnCommand() { + this._actionOnHide = "cancel"; + this.panel.hidePopup(); + }, + + removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() { + this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId); + this._actionOnHide = "remove"; + this.panel.hidePopup(); + }, + + beginBatch: function SU_beginBatch() { + if (!this._batching) { + PlacesUtils.transactionManager.beginBatch(null); + this._batching = true; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// PlacesCommandHook + +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: function PCH_bookmarkPage(aBrowser, aParent, aShowEditUI) { + var uri = aBrowser.currentURI; + var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri); + if (itemId == -1) { + // Copied over from addBookmarkForBrowser: + // Bug 52536: We obtain the URL and title from the nsIWebNavigation + // associated with a <browser/> rather than from a DOMWindow. + // This is because when a full page plugin is loaded, there is + // no DOMWindow (?) but information about the loaded document + // may still be obtained from the webNavigation. + var webNav = aBrowser.webNavigation; + var url = webNav.currentURI; + var title; + var description; + var charset; + try { + let isErrorPage = /^about:(neterror|certerror|blocked)/ + .test(webNav.document.documentURI); + title = isErrorPage ? PlacesUtils.history.getPageTitle(url) + : webNav.document.title; + title = title || url.spec; + description = PlacesUIUtils.getDescriptionFromDocument(webNav.document); + charset = webNav.document.characterSet; + } + catch (e) { } + + if (aShowEditUI) { + // If we bookmark the page here (i.e. page was not "starred" already) + // but open right into the "edit" state, start batching here, so + // "Cancel" in that state removes the bookmark. + 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.isWindowPrivate(aBrowser.contentWindow)) + PlacesUtils.setCharsetForURI(uri, charset); + } + + // Revert the contents of the location bar + if (gURLBar) + 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 page-proxy-favicon + // 3. the content area + if (BookmarkingUI.anchor) { + StarUI.showEditBookmarkPopup(itemId, BookmarkingUI.anchor, + "bottomcenter topright"); + return; + } + + let pageProxyFavicon = document.getElementById("page-proxy-favicon"); + if (isElementVisible(pageProxyFavicon)) { + StarUI.showEditBookmarkPopup(itemId, pageProxyFavicon, + "bottomcenter topright"); + } else { + StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap"); + } + }, + + /** + * 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 + */ + bookmarkLink: function PCH_bookmarkLink(aParent, aURL, aTitle) { + var linkURI = makeURI(aURL); + var itemId = PlacesUtils.getMostRecentBookmarkForURI(linkURI); + if (itemId == -1) { + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: linkURI + , title: aTitle + , hiddenRows: [ "description" + , "location" + , "loadInSidebar" + , "keyword" ] + }, window); + } + else { + PlacesUIUtils.showBookmarkDialog({ action: "edit" + , type: "bookmark" + , itemId: itemId + }, window); + } + }, + + /** + * 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(function (tab) { + let spec = tab.linkedBrowser.currentURI.spec; + if (!tab.pinned && !(spec in uniquePages)) { + uniquePages[spec] = null; + URIs.push(tab.linkedBrowser.currentURI); + } + }); + 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: function PCH_addLiveBookmark(url, feedTitle, feedSubtitle) { + let toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId, -1); + + let feedURI = makeURI(url); + let title = feedTitle || gBrowser.contentTitle; + let description = feedSubtitle; + if (!description) { + description = PlacesUIUtils.getDescriptionFromDocument(gBrowser.contentDocument); + } + + 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.selectLeftPaneQuery(aLeftPaneRoot); + organizer.focus(); + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// HistoryMenu + +// 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; + XPCOMUtils.defineLazyServiceGetter(this, "_ss", + "@mozilla.org/browser/sessionstore;1", + "nsISessionStore"); + PlacesMenu.call(this, aPopupShowingEvent, + "place:sort=4&maxResults=15"); +} + +HistoryMenu.prototype = { + toggleRestoreLastSession: function HM_toggleRestoreLastSession() { + let restoreItem = this._rootElt.ownerDocument.getElementById("Browser:RestoreLastSession"); + + if (this._ss.canRestoreLastSession && + !PrivateBrowsingUtils.isWindowPrivate(window)) + restoreItem.removeAttribute("disabled"); + else + restoreItem.setAttribute("disabled", true); + }, + + 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._ss.getClosedTabCount(window) == 0) + undoMenu.setAttribute("disabled", true); + else + undoMenu.removeAttribute("disabled"); + }, + + /** + * Re-open a closed tab and put it to the end of the tab strip. + * Used for a middle click. + * @param aEvent + * The event when the user clicks the menu item + */ + _undoCloseMiddleClick: function PHM__undoCloseMiddleClick(aEvent) { + if (aEvent.button != 1) + return; + + undoCloseTab(aEvent.originalTarget.value); + gBrowser.moveTabToEnd(); + }, + + /** + * 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._ss.getClosedTabCount(window) == 0) { + undoMenu.setAttribute("disabled", true); + return; + } + + // enable menu + undoMenu.removeAttribute("disabled"); + + // populate menu + var undoItems = JSON.parse(this._ss.getClosedTabData(window)); + for (var i = 0; i < undoItems.length; i++) { + var m = document.createElement("menuitem"); + m.setAttribute("label", undoItems[i].title); + if (undoItems[i].image) { + let iconURL = undoItems[i].image; + // don't initiate a connection just to fetch a favicon (see bug 467828) + if (/^https?:/.test(iconURL)) + iconURL = "moz-anno:favicon:" + iconURL; + m.setAttribute("image", iconURL); + } + m.setAttribute("class", "menuitem-iconic bookmark-item menuitem-with-favicon"); + m.setAttribute("value", i); + m.setAttribute("oncommand", "undoCloseTab(" + i + ");"); + + // Set the targetURI attribute so it will be shown in tooltip and trigger + // onLinkHovered. SessionStore uses one-based indexes, so we need to + // normalize them. + let tabData = undoItems[i].state; + let activeIndex = (tabData.index || tabData.entries.length) - 1; + if (activeIndex >= 0 && tabData.entries[activeIndex]) + m.setAttribute("targetURI", tabData.entries[activeIndex].url); + + m.addEventListener("click", this._undoCloseMiddleClick, false); + if (i == 0) + m.setAttribute("key", "key_undoCloseTab"); + undoPopup.appendChild(m); + } + + // "Restore All Tabs" + var strings = gNavigatorBundle; + undoPopup.appendChild(document.createElement("menuseparator")); + m = undoPopup.appendChild(document.createElement("menuitem")); + m.id = "menu_restoreAllTabs"; + m.setAttribute("label", strings.getString("menuRestoreAllTabs.label")); + m.addEventListener("command", function() { + for (var i = 0; i < undoItems.length; i++) + undoCloseTab(); + }, false); + }, + + 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 (this._ss.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; + let menuLabelString = gNavigatorBundle.getString("menuUndoCloseWindowLabel"); + let menuLabelStringSingleTab = + gNavigatorBundle.getString("menuUndoCloseWindowSingleTabLabel"); + + // remove existing menu items + while (undoPopup.hasChildNodes()) + undoPopup.removeChild(undoPopup.firstChild); + + // no restorable windows, so make sure menu is disabled, and return + if (this._ss.getClosedWindowCount() == 0) { + undoMenu.setAttribute("disabled", true); + return; + } + + // enable menu + undoMenu.removeAttribute("disabled"); + + // populate menu + let undoItems = JSON.parse(this._ss.getClosedWindowData()); + for (let i = 0; i < undoItems.length; i++) { + let undoItem = undoItems[i]; + let otherTabsCount = undoItem.tabs.length - 1; + let label = (otherTabsCount == 0) ? menuLabelStringSingleTab + : PluralForm.get(otherTabsCount, menuLabelString); + let menuLabel = label.replace("#1", undoItem.title) + .replace("#2", otherTabsCount); + let m = document.createElement("menuitem"); + m.setAttribute("label", menuLabel); + let selectedTab = undoItem.tabs[undoItem.selected - 1]; + if (selectedTab.image) { + let iconURL = selectedTab.image; + // don't initiate a connection just to fetch a favicon (see bug 467828) + if (/^https?:/.test(iconURL)) + iconURL = "moz-anno:favicon:" + iconURL; + m.setAttribute("image", iconURL); + } + m.setAttribute("class", "menuitem-iconic bookmark-item menuitem-with-favicon"); + m.setAttribute("oncommand", "undoCloseWindow(" + i + ");"); + + // Set the targetURI attribute so it will be shown in tooltip. + // SessionStore uses one-based indexes, so we need to normalize them. + let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1; + if (activeIndex >= 0 && selectedTab.entries[activeIndex]) + m.setAttribute("targetURI", selectedTab.entries[activeIndex].url); + + if (i == 0) + m.setAttribute("key", "key_undoCloseWindow"); + undoPopup.appendChild(m); + } + + // "Open All in Windows" + undoPopup.appendChild(document.createElement("menuseparator")); + let m = undoPopup.appendChild(document.createElement("menuitem")); + m.id = "menu_restoreAllWindows"; + m.setAttribute("label", gNavigatorBundle.getString("menuRestoreAllWindows.label")); + m.setAttribute("oncommand", + "for (var i = 0; i < " + undoItems.length + "; i++) undoCloseWindow();"); + }, + + toggleTabsFromOtherComputers: function PHM_toggleTabsFromOtherComputers() { + // This is a no-op if MOZ_SERVICES_SYNC isn't defined +#ifdef MOZ_SERVICES_SYNC + // 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 Sync isn't configured yet, then don't show the menuitem. + if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || + Weave.Svc.Prefs.get("firstSync", "") == "notReady") { + menuitem.setAttribute("hidden", true); + return; + } + + // The tabs engine might never be inited (if services.sync.registerEngines + // is modified), so make sure we avoid undefined errors. + let enabled = Weave.Service.isLoggedIn && + Weave.Service.engineManager.get("tabs") && + Weave.Service.engineManager.get("tabs").enabled; + menuitem.setAttribute("disabled", !enabled); + menuitem.setAttribute("hidden", false); +#endif + }, + + _onPopupShowing: function HM__onPopupShowing(aEvent) { + PlacesMenu.prototype._onPopupShowing.apply(this, arguments); + + // Don't handle events for submenus. + if (aEvent.target != aEvent.currentTarget) + return; + + this.toggleRestoreLastSession(); + 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 }); + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// BookmarksEventHandler + +/** + * 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. +#ifdef XP_MACOSX + var modifKey = aEvent.metaKey || aEvent.shiftKey; +#else + var modifKey = aEvent.ctrlKey || aEvent.shiftKey; +#endif + 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 (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; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// PlacesMenuDNDHandler + +// Handles special drag and drop functionality for Places menus that are not +// part of a Places view (e.g. the bookmarks menu in the menubar). +var PlacesMenuDNDHandler = { + _springLoadDelay: 350, // milliseconds + _loadTimer: null, + _closerTimer: 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; + + 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._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT); + event.preventDefault(); + event.stopPropagation(); + }, + + /** + * Handles dragleave on the <menu> element. + * @returns true if the element is a container element (menu or + * menu-toolbarbutton), false otherwise. + */ + onDragLeave: function PMDH_onDragLeave(event) { + // Handle menu-button separate targets. + if (event.relatedTarget === event.currentTarget || + event.relatedTarget.parentNode === event.currentTarget) + return; + + // Closing menus in a Places popup is handled by the view itself. + if (!this._isStaticContainer(event.target)) + return; + + 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._closeTimer.initWithCallback(function() { + this._closeTimer = 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._springLoadDelay, Ci.nsITimer.TYPE_ONE_SHOT); + }, + + /** + * Determines if a XUL element represents a static container. + * @returns true if the element is a container element (menu or + *` menu-toolbarbutton), false otherwise. + */ + _isStaticContainer: function PMDH__isContainer(node) { + let isMenu = node.localName == "menu" || + (node.localName == "toolbarbutton" && + (node.getAttribute("type") == "menu" || + 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, + Ci.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, + Ci.nsITreeView.DROP_ON); + PlacesControllerDragHelper.onDrop(ip, event.dataTransfer); + event.stopPropagation(); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// PlacesToolbarHelper + +/** + * This object handles the initialization and uninitialization of the bookmarks + * toolbar. + */ +let PlacesToolbarHelper = { + _place: "place:folder=TOOLBAR", + + get _viewElt() { + return document.getElementById("PlacesToolbar"); + }, + + init: function PTH_init() { + let viewElt = this._viewElt; + if (!viewElt || viewElt._placesView) + return; + + // If the bookmarks toolbar item is hidden because the parent toolbar is + // collapsed or hidden (i.e. in a popup), spare the initialization. Also, + // there is no need to initialize the toolbar if customizing because + // init() will be called when the customization is done. + let toolbar = viewElt.parentNode.parentNode; + if (toolbar.collapsed || + getComputedStyle(toolbar, "").display == "none" || + this._isCustomizing) + return; + + new PlacesToolbar(this._place); + }, + + customizeStart: function PTH_customizeStart() { + let viewElt = this._viewElt; + if (viewElt && viewElt._placesView) + viewElt._placesView.uninit(); + + this._isCustomizing = true; + }, + + customizeDone: function PTH_customizeDone() { + this._isCustomizing = false; + this.init(); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// BookmarkingUI + +/** + * Handles the bookmarks star button in the URL bar, as well as the bookmark + * menu button. + */ + +let BookmarkingUI = { + get button() { + if (!this._button) { + this._button = document.getElementById("bookmarks-menu-button"); + } + return this._button; + }, + + get star() { + if (!this._star) { + this._star = document.getElementById("star-button"); + } + return this._star; + }, + + get anchor() { + if (this.star && isElementVisible(this.star)) { + // Anchor to the icon, so the panel looks more natural. + return this.star; + } + return null; + }, + + STATUS_UPDATING: -1, + STATUS_UNSTARRED: 0, + STATUS_STARRED: 1, + get status() { + if (this._pendingStmt) + return this.STATUS_UPDATING; + return this.star && + this.star.hasAttribute("starred") ? this.STATUS_STARRED + : this.STATUS_UNSTARRED; + }, + + get _starredTooltip() + { + delete this._starredTooltip; + return this._starredTooltip = + gNavigatorBundle.getString("starButtonOn.tooltip"); + }, + + get _unstarredTooltip() + { + delete this._unstarredTooltip; + return this._unstarredTooltip = + gNavigatorBundle.getString("starButtonOff.tooltip"); + }, + + /** + * 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; + + 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. + let personalToolbar = document.getElementById("PersonalToolbar"); + viewToolbarMenuitem.setAttribute("checked", !personalToolbar.collapsed); + } + + let toolbarMenuitem = getPlacesAnonymousElement("toolbar-autohide"); + if (toolbarMenuitem) { + // If bookmarks items are visible, hide Bookmarks Toolbar menu and the + // separator after it. + toolbarMenuitem.collapsed = toolbarMenuitem.nextSibling.collapsed = + isElementVisible(document.getElementById("personal-bookmarks")); + } + }, + + /** + * Handles star styling based on page proxy state changes. + */ + onPageProxyStateChanged: function BUI_onPageProxyStateChanged(aState) { + if (!this.star) { + return; + } + + if (aState == "invalid") { + this.star.setAttribute("disabled", "true"); + this.star.removeAttribute("starred"); + } + else { + this.star.removeAttribute("disabled"); + } + }, + + _updateToolbarStyle: function BUI__updateToolbarStyle() { + if (!this.button) { + return; + } + + let personalToolbar = document.getElementById("PersonalToolbar"); + let onPersonalToolbar = this.button.parentNode == personalToolbar || + this.button.parentNode.parentNode == personalToolbar; + + if (onPersonalToolbar) { + this.button.classList.add("bookmark-item"); + this.button.classList.remove("toolbarbutton-1"); + } + else { + this.button.classList.remove("bookmark-item"); + this.button.classList.add("toolbarbutton-1"); + } + }, + + _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 && this.button._placesView) { + this.button._placesView.uninit(); + } + // Also uninit the main menubar placesView, since it would have the same + // issues. + let menubar = document.getElementById("bookmarksMenu"); + if (menubar && menubar._placesView) { + menubar._placesView.uninit(); + } + }, + + customizeStart: function BUI_customizeStart() { + this._uninitView(); + }, + + customizeChange: function BUI_customizeChange() { + this._updateToolbarStyle(); + }, + + customizeDone: function BUI_customizeDone() { + delete this._button; + this.onToolbarVisibilityChange(); + this._updateToolbarStyle(); + }, + + _hasBookmarksObserver: false, + uninit: function BUI_uninit() { + 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; + } + + // We can load about:blank before the actual page, but there is no point in handling that page. + if (isBlankPageURL(this._uri.spec)) { + return; + } + + 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( + function (id) aItemIds.indexOf(id) == -1 + ).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; + }, this); + }, + + _updateStar: function BUI__updateStar() { + if (!this.star) { + return; + } + + if (this._itemIds.length > 0) { + this.star.setAttribute("starred", "true"); + this.star.setAttribute("tooltiptext", this._starredTooltip); + } + else { + this.star.removeAttribute("starred"); + this.star.setAttribute("tooltiptext", this._unstarredTooltip); + } + }, + + onCommand: function BUI_onCommand(aEvent) { + if (aEvent.target != aEvent.currentTarget) { + return; + } + // Ignore clicks on the star if we are updating its state. + if (!this._pendingStmt) { + PlacesCommandHook.bookmarkCurrentPage(this._itemIds.length > 0); + } + }, + + // 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.indexOf(aItemId) == -1) { + this._itemIds.push(aItemId); + 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); + 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); + 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); + this._updateStar(); + } + } + }, + + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onBeforeItemRemoved: function () {}, + onItemVisited: function () {}, + onItemMoved: function () {}, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver + ]) +}; diff --git a/application/palemoon/base/content/browser-plugins.js b/application/palemoon/base/content/browser-plugins.js new file mode 100644 index 0000000000..769ac6d8af --- /dev/null +++ b/application/palemoon/base/content/browser-plugins.js @@ -0,0 +1,797 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +const kPrefSessionPersistMinutes = "plugin.sessionPermissionNow.intervalInMinutes"; +const kPrefPersistentDays = "plugin.persistentPermissionAlways.intervalInDays"; + +var gPluginHandler = { + PLUGIN_SCRIPTED_STATE_NONE: 0, + PLUGIN_SCRIPTED_STATE_FIRED: 1, + PLUGIN_SCRIPTED_STATE_DONE: 2, + + getPluginUI: function (plugin, anonid) { + return plugin.ownerDocument. + getAnonymousElementByAttribute(plugin, "anonid", anonid); + }, + + _getPluginInfo: function (pluginElement) { + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + pluginElement.QueryInterface(Ci.nsIObjectLoadingContent); + + let tagMimetype; + let pluginName = gNavigatorBundle.getString("pluginInfo.unknownPlugin"); + let pluginTag = null; + let permissionString = null; + let fallbackType = null; + let blocklistState = null; + + if (pluginElement instanceof HTMLAppletElement) { + tagMimetype = "application/x-java-vm"; + } else { + tagMimetype = pluginElement.actualType; + + if (tagMimetype == "") { + tagMimetype = pluginElement.type; + } + } + + if (gPluginHandler.isKnownPlugin(pluginElement)) { + pluginTag = pluginHost.getPluginTagForType(pluginElement.actualType); + pluginName = gPluginHandler.makeNicePluginName(pluginTag.name); + + permissionString = pluginHost.getPermissionStringForType(pluginElement.actualType); + fallbackType = pluginElement.defaultFallbackType; + blocklistState = pluginHost.getBlocklistStateForType(pluginElement.actualType); + // Make state-softblocked == state-notblocked for our purposes, + // they have the same UI. STATE_OUTDATED should not exist for plugin + // items, but let's alias it anyway, just in case. + if (blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED || + blocklistState == Ci.nsIBlocklistService.STATE_OUTDATED) { + blocklistState = Ci.nsIBlocklistService.STATE_NOT_BLOCKED; + } + } + + return { mimetype: tagMimetype, + pluginName: pluginName, + pluginTag: pluginTag, + permissionString: permissionString, + fallbackType: fallbackType, + blocklistState: blocklistState, + }; + }, + + // Map the plugin's name to a filtered version more suitable for user UI. + makeNicePluginName : function (aName) { + if (aName == "Shockwave Flash") + return "Adobe Flash"; + + // Clean up the plugin name by stripping off any trailing version numbers + // or "plugin". EG, "Foo Bar Plugin 1.23_02" --> "Foo Bar" + // Do this by first stripping the numbers, etc. off the end, and then + // removing "Plugin" (and then trimming to get rid of any whitespace). + // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled) + let newName = aName.replace(/[\s\d\.\-\_\(\)]+$/, "").replace(/\bplug-?in\b/i, "").trim(); + return newName; + }, + + isTooSmall : function (plugin, overlay) { + // Is the <object>'s size too small to hold what we want to show? + let pluginRect = plugin.getBoundingClientRect(); + // XXX bug 446693. The text-shadow on the submitted-report text at + // the bottom causes scrollHeight to be larger than it should be. + // Clamp width/height to properly show CTP overlay on different + // zoom levels when embedded in iframes (rounding bug). (Bug 972237) + let overflows = (overlay.scrollWidth > Math.ceil(pluginRect.width)) || + (overlay.scrollHeight - 5 > Math.ceil(pluginRect.height)); + return overflows; + }, + + addLinkClickCallback: function (linkNode, callbackName /*callbackArgs...*/) { + // XXX just doing (callback)(arg) was giving a same-origin error. bug? + let self = this; + let callbackArgs = Array.prototype.slice.call(arguments).slice(2); + linkNode.addEventListener("click", + function(evt) { + if (!evt.isTrusted) + return; + evt.preventDefault(); + if (callbackArgs.length == 0) + callbackArgs = [ evt ]; + (self[callbackName]).apply(self, callbackArgs); + }, + true); + + linkNode.addEventListener("keydown", + function(evt) { + if (!evt.isTrusted) + return; + if (evt.keyCode == evt.DOM_VK_RETURN) { + evt.preventDefault(); + if (callbackArgs.length == 0) + callbackArgs = [ evt ]; + evt.preventDefault(); + (self[callbackName]).apply(self, callbackArgs); + } + }, + true); + }, + + // Helper to get the binding handler type from a plugin object + _getBindingType : function(plugin) { + if (!(plugin instanceof Ci.nsIObjectLoadingContent)) + return null; + + switch (plugin.pluginFallbackType) { + case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED: + return "PluginNotFound"; + case Ci.nsIObjectLoadingContent.PLUGIN_DISABLED: + return "PluginDisabled"; + case Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED: + return "PluginBlocklisted"; + case Ci.nsIObjectLoadingContent.PLUGIN_OUTDATED: + return "PluginOutdated"; + case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY: + return "PluginClickToPlay"; + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE: + return "PluginVulnerableUpdatable"; + case Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE: + return "PluginVulnerableNoUpdate"; + case Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW: + return "PluginPlayPreview"; + default: + // Not all states map to a handler + return null; + } + }, + + handleEvent : function(event) { + let plugin; + let doc; + + let eventType = event.type; + if (eventType === "PluginRemoved") { + doc = event.target; + } + else { + plugin = event.target; + doc = plugin.ownerDocument; + + if (!(plugin instanceof Ci.nsIObjectLoadingContent)) + return; + } + + if (eventType == "PluginBindingAttached") { + // The plugin binding fires this event when it is created. + // As an untrusted event, ensure that this object actually has a binding + // and make sure we don't handle it twice + let overlay = this.getPluginUI(plugin, "main"); + if (!overlay || overlay._bindingHandled) { + return; + } + overlay._bindingHandled = true; + + // Lookup the handler for this binding + eventType = this._getBindingType(plugin); + if (!eventType) { + // Not all bindings have handlers + return; + } + } + + let shouldShowNotification = false; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + if (!browser) + return; + + switch (eventType) { + case "PluginCrashed": + this.pluginInstanceCrashed(plugin, event); + break; + + case "PluginNotFound": + /* No action (plugin finder obsolete) */ + break; + + case "PluginBlocklisted": + case "PluginOutdated": + shouldShowNotification = true; + break; + + case "PluginVulnerableUpdatable": + let updateLink = this.getPluginUI(plugin, "checkForUpdatesLink"); + this.addLinkClickCallback(updateLink, "openPluginUpdatePage"); + /* FALLTHRU */ + + case "PluginVulnerableNoUpdate": + case "PluginClickToPlay": + this._handleClickToPlayEvent(plugin); + let overlay = this.getPluginUI(plugin, "main"); + let pluginName = this._getPluginInfo(plugin).pluginName; + let messageString = gNavigatorBundle.getFormattedString("PluginClickToActivate", [pluginName]); + let overlayText = this.getPluginUI(plugin, "clickToPlay"); + overlayText.textContent = messageString; + if (eventType == "PluginVulnerableUpdatable" || + eventType == "PluginVulnerableNoUpdate") { + let vulnerabilityString = gNavigatorBundle.getString(eventType); + let vulnerabilityText = this.getPluginUI(plugin, "vulnerabilityStatus"); + vulnerabilityText.textContent = vulnerabilityString; + } + shouldShowNotification = true; + break; + + case "PluginPlayPreview": + this._handlePlayPreviewEvent(plugin); + break; + + case "PluginDisabled": + let manageLink = this.getPluginUI(plugin, "managePluginsLink"); + this.addLinkClickCallback(manageLink, "managePlugins"); + shouldShowNotification = true; + break; + + case "PluginInstantiated": + //Pale Moon: don't show the indicator when plugins are enabled/allowed + if (gPrefService.getBoolPref("plugins.always_show_indicator")) { + shouldShowNotification = true; + } + break; + case "PluginRemoved": + shouldShowNotification = true; + break; + } + + // Show the in-content UI if it's not too big. The crashed plugin handler already did this. + if (eventType != "PluginCrashed" && eventType != "PluginRemoved") { + let overlay = this.getPluginUI(plugin, "main"); + if (overlay != null) { + if (!this.isTooSmall(plugin, overlay)) { + overlay.style.visibility = "visible"; + } + plugin.addEventListener("overflow", function(event) { + overlay.style.visibility = "hidden"; + }); + plugin.addEventListener("underflow", function(event) { + // this is triggered if only one dimension underflows, + // the other dimension might still overflow + if (!gPluginHandler.isTooSmall(plugin, overlay)) { + overlay.style.visibility = "visible"; + } + }); + } + } + + // Only show the notification after we've done the isTooSmall check, so + // that the notification can decide whether to show the "alert" icon + if (shouldShowNotification) { + this._showClickToPlayNotification(browser); + } + }, + + isKnownPlugin: function PH_isKnownPlugin(objLoadingContent) { + return (objLoadingContent.getContentTypeForMIMEType(objLoadingContent.actualType) == + Ci.nsIObjectLoadingContent.TYPE_PLUGIN); + }, + + canActivatePlugin: function PH_canActivatePlugin(objLoadingContent) { + // if this isn't a known plugin, we can't activate it + // (this also guards pluginHost.getPermissionStringForType against + // unexpected input) + if (!gPluginHandler.isKnownPlugin(objLoadingContent)) + return false; + + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType); + let principal = objLoadingContent.ownerDocument.defaultView.top.document.nodePrincipal; + let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString); + + let isFallbackTypeValid = + objLoadingContent.pluginFallbackType >= Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY && + objLoadingContent.pluginFallbackType <= Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE; + + if (objLoadingContent.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW) { + // checking if play preview is subject to CTP rules + let playPreviewInfo = pluginHost.getPlayPreviewInfo(objLoadingContent.actualType); + isFallbackTypeValid = !playPreviewInfo.ignoreCTP; + } + + return !objLoadingContent.activated && + pluginPermission != Ci.nsIPermissionManager.DENY_ACTION && + isFallbackTypeValid; + }, + + hideClickToPlayOverlay: function(aPlugin) { + let overlay = this.getPluginUI(aPlugin, "main"); + if (overlay) + overlay.style.visibility = "hidden"; + }, + + stopPlayPreview: function PH_stopPlayPreview(aPlugin, aPlayPlugin) { + let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (objLoadingContent.activated) + return; + + if (aPlayPlugin) + objLoadingContent.playPlugin(); + else + objLoadingContent.cancelPlayPreview(); + }, + + // Callback for user clicking on a disabled plugin + managePlugins: function (aEvent) { + 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 (aEvent) { + openURL(Services.urlFormatter.formatURLPref("plugins.update.url")); + }, + + // 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); + }, + + // Event listener for click-to-play plugins. + _handleClickToPlayEvent: function PH_handleClickToPlayEvent(aPlugin) { + let doc = aPlugin.ownerDocument; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let objLoadingContent = aPlugin.QueryInterface(Ci.nsIObjectLoadingContent); + // guard against giving pluginHost.getPermissionStringForType a type + // not associated with any known plugin + if (!gPluginHandler.isKnownPlugin(objLoadingContent)) + return; + let permissionString = pluginHost.getPermissionStringForType(objLoadingContent.actualType); + let principal = doc.defaultView.top.document.nodePrincipal; + let pluginPermission = Services.perms.testPermissionFromPrincipal(principal, permissionString); + + let overlay = this.getPluginUI(aPlugin, "main"); + + if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) { + if (overlay) + overlay.style.visibility = "hidden"; + return; + } + + if (overlay) { + overlay.addEventListener("click", gPluginHandler._overlayClickListener, true); + let closeIcon = gPluginHandler.getPluginUI(aPlugin, "closeIcon"); + closeIcon.addEventListener("click", function(aEvent) { + if (aEvent.button == 0 && aEvent.isTrusted) + gPluginHandler.hideClickToPlayOverlay(aPlugin); + }, true); + } + }, + + _overlayClickListener: { + handleEvent: function PH_handleOverlayClick(aEvent) { + let plugin = document.getBindingParent(aEvent.target); + let contentWindow = plugin.ownerDocument.defaultView.top; + // gBrowser.getBrowserForDocument does not exist in the case where we + // drag-and-dropped a tab from a window containing only that tab. In + // that case, the window gets destroyed. + let browser = gBrowser.getBrowserForDocument ? + gBrowser.getBrowserForDocument(contentWindow.document) : + null; + // If browser is null here, we've been drag-and-dropped from another + // window, and this is the wrong click handler. + if (!browser) { + aEvent.target.removeEventListener("click", gPluginHandler._overlayClickListener, true); + return; + } + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + // Have to check that the target is not the link to update the plugin + if (!(aEvent.originalTarget instanceof HTMLAnchorElement) && + (aEvent.originalTarget.getAttribute('anonid') != 'closeIcon') && + aEvent.button == 0 && aEvent.isTrusted) { + gPluginHandler._showClickToPlayNotification(browser, plugin); + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + } + }, + + _handlePlayPreviewEvent: function PH_handlePlayPreviewEvent(aPlugin) { + let doc = aPlugin.ownerDocument; + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + let pluginInfo = this._getPluginInfo(aPlugin); + let playPreviewInfo = pluginHost.getPlayPreviewInfo(pluginInfo.mimetype); + + let previewContent = this.getPluginUI(aPlugin, "previewPluginContent"); + let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0]; + if (!iframe) { + // lazy initialization of the iframe + iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe"); + iframe.className = "previewPluginContentFrame"; + previewContent.appendChild(iframe); + + // Force a style flush, so that we ensure our binding is attached. + aPlugin.clientTop; + } + iframe.src = playPreviewInfo.redirectURL; + + // MozPlayPlugin event can be dispatched from the extension chrome + // code to replace the preview content with the native plugin + previewContent.addEventListener("MozPlayPlugin", function playPluginHandler(aEvent) { + if (!aEvent.isTrusted) + return; + + previewContent.removeEventListener("MozPlayPlugin", playPluginHandler, true); + + let playPlugin = !aEvent.detail; + gPluginHandler.stopPlayPreview(aPlugin, playPlugin); + + // cleaning up: removes overlay iframe from the DOM + let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0]; + if (iframe) + previewContent.removeChild(iframe); + }, true); + + if (!playPreviewInfo.ignoreCTP) { + gPluginHandler._showClickToPlayNotification(browser); + } + }, + + reshowClickToPlayNotification: function PH_reshowClickToPlayNotification() { + let browser = gBrowser.selectedBrowser; + let contentWindow = browser.contentWindow; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let doc = contentWindow.document; + let plugins = cwu.plugins; + for (let plugin of plugins) { + let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main"); + if (overlay) + overlay.removeEventListener("click", gPluginHandler._overlayClickListener, true); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (gPluginHandler.canActivatePlugin(objLoadingContent)) + gPluginHandler._handleClickToPlayEvent(plugin); + } + gPluginHandler._showClickToPlayNotification(browser); + }, + + _clickToPlayNotificationEventCallback: function PH_ctpEventCallback(event) { + if (event == "showing") { + gPluginHandler._makeCenterActions(this); + } + else if (event == "dismissed") { + // Once the popup is dismissed, clicking the icon should show the full + // list again + this.options.primaryPlugin = null; + } + }, + + // Match the behaviour of nsPermissionManager + _getHostFromPrincipal: function PH_getHostFromPrincipal(principal) { + if (!principal.URI || principal.URI.schemeIs("moz-nullprincipal")) { + return "(null)"; + } + + try { + if (principal.URI.host) + return principal.URI.host; + } catch (e) {} + + return principal.origin; + }, + + _makeCenterActions: function PH_makeCenterActions(notification) { + let contentWindow = notification.browser.contentWindow; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + + let principal = contentWindow.document.nodePrincipal; + // This matches the behavior of nsPermssionManager, used for display purposes only + let principalHost = this._getHostFromPrincipal(principal); + + let centerActions = []; + let pluginsFound = new Set(); + for (let plugin of cwu.plugins) { + plugin.QueryInterface(Ci.nsIObjectLoadingContent); + if (plugin.getContentTypeForMIMEType(plugin.actualType) != Ci.nsIObjectLoadingContent.TYPE_PLUGIN) { + continue; + } + + let pluginInfo = this._getPluginInfo(plugin); + if (pluginInfo.permissionString === null) { + Components.utils.reportError("No permission string for active plugin."); + continue; + } + if (pluginsFound.has(pluginInfo.permissionString)) { + continue; + } + pluginsFound.add(pluginInfo.permissionString); + + // Add the per-site permissions and details URLs to pluginInfo here + // because they are more expensive to compute and so we avoid it in + // the tighter loop above. + let permissionObj = Services.perms. + getPermissionObject(principal, pluginInfo.permissionString, false); + if (permissionObj) { + pluginInfo.pluginPermissionHost = permissionObj.host; + pluginInfo.pluginPermissionType = permissionObj.expireType; + } + else { + pluginInfo.pluginPermissionHost = principalHost; + pluginInfo.pluginPermissionType = undefined; + } + + let url; + // TODO: allow the blocklist to specify a better link, bug 873093 + if (pluginInfo.blocklistState == Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE) { + url = Services.urlFormatter.formatURLPref("plugins.update.url"); + } + else if (pluginInfo.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) { + url = Services.blocklist.getPluginBlocklistURL(pluginInfo.pluginTag); + } + pluginInfo.detailsLink = url; + + centerActions.push(pluginInfo); + } + centerActions.sort(function(a, b) { + return a.pluginName.localeCompare(b.pluginName); + }); + + notification.options.centerActions = centerActions; + }, + + /** + * 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 PH_setPermissionForPlugins(aNotification, aPluginInfo, aNewState) { + let permission; + let expireType; + let expireTime; + + switch (aNewState) { + case "allownow": + permission = Ci.nsIPermissionManager.ALLOW_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_SESSION; + expireTime = Date.now() + Services.prefs.getIntPref(kPrefSessionPersistMinutes) * 60 * 1000; + break; + + case "allowalways": + permission = Ci.nsIPermissionManager.ALLOW_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_TIME; + expireTime = Date.now() + + Services.prefs.getIntPref(kPrefPersistentDays) * 24 * 60 * 60 * 1000; + break; + + case "block": + permission = Ci.nsIPermissionManager.PROMPT_ACTION; + expireType = Ci.nsIPermissionManager.EXPIRE_NEVER; + expireTime = 0; + 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": + break; + + default: + Cu.reportError(Error("Unexpected plugin state: " + aNewState)); + return; + } + + let browser = aNotification.browser; + let contentWindow = browser.contentWindow; + if (aNewState != "continue") { + let principal = contentWindow.document.nodePrincipal; + Services.perms.addFromPrincipal(principal, aPluginInfo.permissionString, + permission, expireType, expireTime); + + if (aNewState == "block") { + return; + } + } + + // Manually activate the plugins that would have been automatically + // activated. + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + let plugins = cwu.plugins; + let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + + for (let plugin of plugins) { + plugin.QueryInterface(Ci.nsIObjectLoadingContent); + // canActivatePlugin will return false if this isn't a known plugin type, + // so the pluginHost.getPermissionStringForType call is protected + if (gPluginHandler.canActivatePlugin(plugin) && + aPluginInfo.permissionString == pluginHost.getPermissionStringForType(plugin.actualType)) { + plugin.playPlugin(); + } + } + }, + + _showClickToPlayNotification: function PH_showClickToPlayNotification(aBrowser, aPrimaryPlugin) { + let notification = PopupNotifications.getNotification("click-to-play-plugins", aBrowser); + + let contentWindow = aBrowser.contentWindow; + let contentDoc = aBrowser.contentDocument; + let cwu = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + // Pale Moon: cwu.plugins may contain non-plugin <object>s, filter them out + let plugins = cwu.plugins.filter(function(plugin) { + return (plugin.getContentTypeForMIMEType(plugin.actualType) == + Ci.nsIObjectLoadingContent.TYPE_PLUGIN); + }); + if (plugins.length == 0) { + if (notification) { + PopupNotifications.remove(notification); + } + return; + } + + let icon = 'plugins-notification-icon'; + for (let plugin of plugins) { + let fallbackType = plugin.pluginFallbackType; + if (fallbackType == plugin.PLUGIN_VULNERABLE_UPDATABLE || + fallbackType == plugin.PLUGIN_VULNERABLE_NO_UPDATE || + fallbackType == plugin.PLUGIN_BLOCKLISTED) { + icon = 'blocked-plugins-notification-icon'; + break; + } + if (fallbackType == plugin.PLUGIN_CLICK_TO_PLAY) { + let overlay = contentDoc.getAnonymousElementByAttribute(plugin, "anonid", "main"); + if (!overlay || overlay.style.visibility == 'hidden') { + icon = 'alert-plugins-notification-icon'; + } + } + } + + let dismissed = notification ? notification.dismissed : true; + if (aPrimaryPlugin) + dismissed = false; + + let primaryPluginPermission = null; + if (aPrimaryPlugin) { + primaryPluginPermission = this._getPluginInfo(aPrimaryPlugin).permissionString; + } + + let options = { + dismissed: dismissed, + eventCallback: this._clickToPlayNotificationEventCallback, + primaryPlugin: primaryPluginPermission + }; + PopupNotifications.show(aBrowser, "click-to-play-plugins", + "", icon, + null, null, options); + }, + + // Crashed-plugin observer. Notified once per plugin crash, before events + // are dispatched to individual plugin instances. + pluginCrashed : function(subject, topic, data) { + let propertyBag = subject; + if (!(propertyBag instanceof Ci.nsIPropertyBag2) || + !(propertyBag instanceof Ci.nsIWritablePropertyBag2)) + return; + }, + + // Crashed-plugin event listener. Called for every instance of a + // plugin in content. + pluginInstanceCrashed: function (plugin, aEvent) { + // Ensure the plugin and event are of the right type. + if (!(aEvent instanceof Ci.nsIDOMDataContainerEvent)) + return; + + let submittedReport = aEvent.getData("submittedCrashReport"); + let doPrompt = true; // XXX followup for .getData("doPrompt"); + let submitReports = true; // XXX followup for .getData("submitReports"); + let pluginName = aEvent.getData("pluginName"); + let pluginDumpID = aEvent.getData("pluginDumpID"); + let browserDumpID = aEvent.getData("browserDumpID"); + + // Remap the plugin name to a more user-presentable form. + pluginName = this.makeNicePluginName(pluginName); + + let messageString = gNavigatorBundle.getFormattedString("crashedpluginsMessage.title", [pluginName]); + + // + // Configure the crashed-plugin placeholder. + // + + // Force a layout flush so the binding is attached. + plugin.clientTop; + let doc = plugin.ownerDocument; + let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + let statusDiv = doc.getAnonymousElementByAttribute(plugin, "class", "submitStatus"); + + let crashText = doc.getAnonymousElementByAttribute(plugin, "class", "msgCrashedText"); + crashText.textContent = messageString; + + let browser = gBrowser.getBrowserForDocument(doc.defaultView.top.document); + + let link = doc.getAnonymousElementByAttribute(plugin, "class", "reloadLink"); + this.addLinkClickCallback(link, "reloadPage", browser); + + let notificationBox = gBrowser.getNotificationBox(browser); + + let isShowing = true; + + // Is the <object>'s size too small to hold what we want to show? + if (this.isTooSmall(plugin, overlay)) { + // First try hiding the crash report submission UI. + statusDiv.removeAttribute("status"); + + if (this.isTooSmall(plugin, overlay)) { + // Hide the overlay's contents. Use visibility style, so that it doesn't + // collapse down to 0x0. + overlay.style.visibility = "hidden"; + isShowing = false; + } + } + + if (isShowing) { + // If a previous plugin on the page was too small and resulted in adding a + // notification bar, then remove it because this plugin instance it big + // enough to serve as in-content notification. + hideNotificationBar(); + doc.mozNoPluginCrashedNotification = true; + } else { + // If another plugin on the page was large enough to show our UI, we don't + // want to show a notification bar. + if (!doc.mozNoPluginCrashedNotification) + showNotificationBar(pluginDumpID, browserDumpID); + } + + function hideNotificationBar() { + let notification = notificationBox.getNotificationWithValue("plugin-crashed"); + if (notification) + notificationBox.removeNotification(notification, true); + } + + function showNotificationBar(pluginDumpID, browserDumpID) { + // If there's already an existing notification bar, don't do anything. + 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 submitLabel = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.label"); + let submitKey = gNavigatorBundle.getString("crashedpluginsMessage.submitButton.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); + + // Remove the notfication when the page is reloaded. + doc.defaultView.top.addEventListener("unload", function() { + notificationBox.removeNotification(notification); + }, false); + } + + } +}; diff --git a/application/palemoon/base/content/browser-sets.inc b/application/palemoon/base/content/browser-sets.inc new file mode 100644 index 0000000000..78cfb7faad --- /dev/null +++ b/application/palemoon/base/content/browser-sets.inc @@ -0,0 +1,436 @@ +# -*- 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()"/> + <command id="cmd_handleBackspace" oncommand="BrowserHandleBackspace();" /> + <command id="cmd_handleShiftBackspace" oncommand="BrowserHandleShiftBackspace();" /> + + <command id="cmd_newNavigatorTab" oncommand="BrowserOpenTab();"/> + <command id="Browser:OpenFile" oncommand="BrowserOpenFileWindow();"/> + <command id="Browser:SavePage" oncommand="saveDocument(window.content.document);"/> + + <command id="Browser:SendLink" + oncommand="MailIntegration.sendLinkForWindow(window.content);"/> + + <command id="cmd_pageSetup" oncommand="PrintUtils.showPageSetup();"/> + <command id="cmd_print" oncommand="PrintUtils.print();"/> + <command id="cmd_printPreview" oncommand="PrintUtils.printPreview(PrintPreviewListener);"/> + <command id="cmd_close" oncommand="BrowserCloseTabOrWindow()"/> + <command id="cmd_closeWindow" oncommand="BrowserTryToCloseWindow()"/> + <command id="cmd_ToggleTabsOnTop" oncommand="TabsOnTop.toggle()"/> + <command id="cmd_CustomizeToolbars" oncommand="BrowserCustomizeToolbar()"/> + <command id="cmd_restartApplication" oncommand="restart(false);"/> + <command id="cmd_quitApplication" oncommand="goQuitApplication()"/> + + + <commandset id="editMenuCommands"/> + + <command id="View:PageSource" oncommand="BrowserViewSourceOfDocument(content.document);" observes="isImage"/> + <command id="View:PageInfo" oncommand="BrowserPageInfo();"/> + <command id="View:FullScreen" oncommand="BrowserFullScreen();"/> + <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"/> + <!-- 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);"/> + <command id="Browser:PrevTab" oncommand="gBrowser.tabContainer.advanceSelectedTab(-1, true);"/> + <command id="Browser:ShowAllTabs" oncommand="allTabs.open();"/> + <command id="Browser:FocusNextFrame" oncommand="focusNextFrame(event);"/> + <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="Tools:Search" oncommand="BrowserSearch.webSearch();"/> + <command id="Tools:Downloads" oncommand="BrowserDownloadsUI();"/> +#ifdef MOZ_DEVTOOLS + <command id="Tools:DevToolbox" oncommand="gDevToolsBrowser.toggleToolboxCommand(gBrowser);"/> + <command id="Tools:DevToolbar" oncommand="DeveloperToolbar.toggle();" disabled="true" hidden="true"/> + <command id="Tools:DevToolbarFocus" oncommand="DeveloperToolbar.focusToggle();" disabled="true"/> + <command id="Tools:DevAppMgr" oncommand="gDevToolsBrowser.openAppManager(gBrowser);" disabled="true" hidden="true"/> + <command id="Tools:WebIDE" oncommand="gDevToolsBrowser.openWebIDE();" disabled="true" hidden="true"/> + <command id="Tools:ChromeDebugger" oncommand="BrowserToolboxProcess.init();" disabled="true" hidden="true"/> + <command id="Tools:BrowserConsole" oncommand="HUDService.openBrowserConsoleOrFocus();"/> + <command id="Tools:Scratchpad" oncommand="Scratchpad.openScratchpad();" disabled="true" hidden="true"/> + <command id="Tools:ResponsiveUI" oncommand="ResponsiveUI.toggle();" disabled="true" hidden="true"/> + <command id="Tools:Eyedropper" oncommand="openEyedropper();"/> +#endif + <command id="Tools:Addons" oncommand="BrowserOpenAddonsMgr();"/> + <command id="Tools:Permissions" oncommand="BrowserOpenPermissionsMgr();"/> + <command id="Tools:ErrorConsole" oncommand="toJavaScriptConsole()" disabled="true" hidden="true"/> +#ifdef MOZ_DEVTOOLS + <command id="Tools:DevToolsConnect" oncommand="gDevToolsBrowser.openConnectScreen(gBrowser)" disabled="true" hidden="true"/> +#endif + <command id="Tools:Sanitize" + oncommand="Cc['@mozilla.org/browser/browserglue;1'].getService(Ci.nsIBrowserGlue).sanitize(window);"/> + <command id="Tools:PrivateBrowsing" + oncommand="OpenBrowserWindow({private: true});"/> + <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/> + <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/> + <command id="Browser:ToggleAddonBar" oncommand="toggleAddonBar();"/> + </commandset> + + <commandset id="placesCommands"> + <command id="Browser:ShowAllBookmarks" + oncommand="PlacesCommandHook.showPlacesOrganizer('AllBookmarks');"/> + <command id="Browser:ShowAllHistory" + oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/> + </commandset> + + <broadcasterset id="mainBroadcasterSet"> + <broadcaster id="viewBookmarksSidebar" autoCheck="false" sidebartitle="&bookmarksButton.label;" + type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul" + oncommand="toggleSidebar('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="toggleSidebar('viewHistorySidebar');"/> + + <broadcaster id="viewWebPanelsSidebar" autoCheck="false" + type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul" + oncommand="toggleSidebar('viewWebPanelsSidebar');"/> + + <!-- 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="isFrameImage"/> + <broadcaster id="singleFeedMenuitemState" disabled="true"/> + <broadcaster id="multipleFeedsMenuState" hidden="true"/> +#ifdef MOZ_SERVICES_SYNC + <broadcaster id="sync-setup-state"/> + <broadcaster id="sync-syncnow-state"/> +#endif + <broadcaster id="workOfflineMenuitemState"/> + +#ifdef MOZ_DEVTOOLS + <!-- DevTools broadcasters --> + <broadcaster id="devtoolsMenuBroadcaster_DevToolbox" + label="&devToolboxMenuItem.label;" + type="checkbox" autocheck="false" + command="Tools:DevToolbox" + key="key_devToolbox"/> + <broadcaster id="devtoolsMenuBroadcaster_DevToolbar" + label="&devToolbarMenu.label;" + type="checkbox" autocheck="false" + command="Tools:DevToolbar" + key="key_devToolbar"/> + <broadcaster id="devtoolsMenuBroadcaster_DevAppMgr" + label="&devAppMgrMenu.label;" + command="Tools:DevAppMgr"/> + <broadcaster id="devtoolsMenuBroadcaster_webide" + label="&webide.label;" + command="Tools:WebIDE" + key="key_webide"/> + <broadcaster id="devtoolsMenuBroadcaster_ChromeDebugger" + label="&chromeDebuggerMenu.label;" + command="Tools:ChromeDebugger"/> + <broadcaster id="devtoolsMenuBroadcaster_BrowserConsole" + label="&browserConsoleCmd.label;" + key="key_browserConsole" + command="Tools:BrowserConsole"/> + <broadcaster id="devtoolsMenuBroadcaster_Scratchpad" + label="&scratchpad.label;" + command="Tools:Scratchpad" + key="key_scratchpad"/> + <broadcaster id="devtoolsMenuBroadcaster_ResponsiveUI" + label="&responsiveDesignTool.label;" + type="checkbox" autocheck="false" + command="Tools:ResponsiveUI" + key="key_responsiveUI"/> + <broadcaster id="devtoolsMenuBroadcaster_Eyedropper" + label="&eyedropper.label;" + type="checkbox" autocheck="false" + command="Tools:Eyedropper"/> +#endif + <broadcaster id="devtoolsMenuBroadcaster_PageSource" + label="&pageSourceCmd.label;" + key="key_viewSource" + command="View:PageSource"/> + <broadcaster id="devtoolsMenuBroadcaster_ErrorConsole" + label="&errorConsoleCmd.label;" + command="Tools:ErrorConsole"/> + <broadcaster id="devtoolsMenuBroadcaster_GetMoreTools" + label="&getMoreDevtoolsCmd.label;" + oncommand="openUILinkIn(gPrefService.getCharPref('browser.getdevtools.url'), 'tab');"/> +#ifdef MOZ_DEVTOOLS + <broadcaster id="devtoolsMenuBroadcaster_connect" + label="&devtoolsConnect.label;" + command="Tools:DevToolsConnect"/> +#endif + </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_newNavigatorTab"/> + <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"/> +#ifdef MOZ_DEVTOOLS + <key id="key_browserConsole" key="&browserConsoleCmd.commandkey;" command="Tools:BrowserConsole" modifiers="accel,shift"/> + <key id="key_devToolbox" keycode="VK_F12" keytext="F12" command="Tools:DevToolbox"/> + <key id="key_devToolbar" keycode="&devToolbar.keycode;" modifiers="shift" + keytext="&devToolbar.keytext;" command="Tools:DevToolbarFocus"/> + <key id="key_responsiveUI" key="&responsiveDesignTool.commandkey;" command="Tools:ResponsiveUI" +#ifdef XP_MACOSX + modifiers="accel,alt" +#else + modifiers="accel,shift" +#endif + /> + <key id="key_scratchpad" keycode="&scratchpad.keycode;" modifiers="shift" + keytext="&scratchpad.keytext;" command="Tools:Scratchpad"/> +#endif + <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_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 keycode="VK_F6" command="Browser:FocusNextFrame"/> + <key keycode="VK_F6" command="Browser:FocusNextFrame" modifiers="shift"/> + <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 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"/> + <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 + +# Navigation cancel keys: Esc performs a cancel on loading (i.e.: stop button equivalent) +# Shift-Esc (and similar Shift-modified stop on Mac) performs a "superstop": this halts all +# networking requests, XHR, animated gifs, etc. + <key id="key_stop" keycode="VK_ESCAPE" command="Browser:Stop"/> + <key id="key_stop_all" keycode="VK_ESCAPE" modifiers="shift" oncommand="BrowserStop();"/> +#ifdef XP_MACOSX + <key id="key_stop_mac" modifiers="accel" key="&stopCmd.macCommandKey;" command="Browser:Stop"/> + <key id="key_stop_all_mac" modifiers="accel,shift" key="&stopCmd.macCommandKey;" oncommand="BrowserStop();"/> +#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"/> +#endif +#ifdef 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__"/> + + <key id="key_toggleAddonBar" command="Browser:ToggleAddonBar" key="&toggleAddonBarCmd.key;" modifiers="accel"/> + + </keyset> + +# Used by baseMenuOverlay +#ifdef XP_MACOSX + <commandset id="baseMenuCommandSet" /> +#endif + <keyset id="baseMenuKeyset" /> diff --git a/application/palemoon/base/content/browser-syncui.js b/application/palemoon/base/content/browser-syncui.js new file mode 100644 index 0000000000..fc8c7f0162 --- /dev/null +++ b/application/palemoon/base/content/browser-syncui.js @@ -0,0 +1,470 @@ +# 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/. + +// gSyncUI handles updating the tools menu +let gSyncUI = { + _obs: ["weave:service:sync:start", + "weave:service:sync:delayed", + "weave:service:quota:remaining", + "weave:service:setup-complete", + "weave:service:login:start", + "weave:service:login:finish", + "weave:service:logout:finish", + "weave:service:start-over", + "weave:ui:login:error", + "weave:ui:sync:error", + "weave:ui:sync:finish", + "weave:ui:clear-error", + ], + + _unloaded: false, + + init: function SUI_init() { + // Proceed to set up the UI if Sync has already started up. + // Otherwise we'll do it when Sync is firing up. + let xps = Components.classes["@mozilla.org/weave/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + if (xps.ready) { + this.initUI(); + return; + } + + Services.obs.addObserver(this, "weave:service:ready", 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"); + + 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); + + if (gBrowser && Weave.Notifications.notifications.length) { + this.initNotifications(); + } + this.updateUI(); + }, + + initNotifications: function SUI_initNotifications() { + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let notificationbox = document.createElementNS(XULNS, "notificationbox"); + notificationbox.id = "sync-notifications"; + notificationbox.setAttribute("flex", "1"); + + let bottombox = document.getElementById("browser-bottombox"); + bottombox.insertBefore(notificationbox, bottombox.firstChild); + + // Force a style flush to ensure that our binding is attached. + notificationbox.clientTop; + + // notificationbox will listen to observers from now on. + Services.obs.removeObserver(this, "weave:notification:added"); + }, + + _wasDelayed: false, + + _needsSetup: function SUI__needsSetup() { + let firstSync = ""; + try { + firstSync = Services.prefs.getCharPref("services.sync.firstSync"); + } catch (e) { } + return Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED || + firstSync == "notReady"; + }, + + updateUI: function SUI_updateUI() { + let needsSetup = this._needsSetup(); + document.getElementById("sync-setup-state").hidden = !needsSetup; + document.getElementById("sync-syncnow-state").hidden = needsSetup; + + if (!gBrowser) + return; + + let button = document.getElementById("sync-button"); + if (!button) + return; + + button.removeAttribute("status"); + this._updateLastSyncTime(); + if (needsSetup) + button.removeAttribute("tooltiptext"); + }, + + + // Functions called by observers + onActivityStart: function SUI_onActivityStart() { + if (!gBrowser) + return; + + let button = document.getElementById("sync-button"); + if (!button) + return; + + button.setAttribute("status", "active"); + }, + + onSyncDelay: function SUI_onSyncDelay() { + // basically, we want to just inform users that stuff is going to take a while + let title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + let description = this._stringBundle.GetStringFromName("error.sync.no_node_found"); + let buttons = [new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"), + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"), + function() { gSyncUI.openServerStatus(); return true; } + )]; + let notification = new Weave.Notification( + title, description, null, Weave.Notifications.PRIORITY_INFO, buttons); + Weave.Notifications.replaceTitle(notification); + this._wasDelayed = true; + }, + + onLoginFinish: function SUI_onLoginFinish() { + // Clear out any login failure notifications + let title = this._stringBundle.GetStringFromName("error.login.title"); + this.clearError(title); + }, + + onSetupComplete: function SUI_onSetupComplete() { + this.onLoginFinish(); + }, + + onLoginError: function SUI_onLoginError() { + // if login fails, any other notifications are essentially moot + Weave.Notifications.removeAll(); + + // if we haven't set up the client, don't show errors + if (this._needsSetup()) { + this.updateUI(); + return; + } + + let title = this._stringBundle.GetStringFromName("error.login.title"); + + let description; + if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) { + // Convert to days + let lastSync = + Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; + description = + this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1); + } else { + let reason = Weave.Utils.getErrorString(Weave.Status.login); + description = + this._stringBundle.formatStringFromName("error.sync.description", [reason], 1); + } + + let buttons = []; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.login.prefs.label"), + this._stringBundle.GetStringFromName("error.login.prefs.accesskey"), + function() { gSyncUI.openPrefs(); return true; } + )); + + let notification = new Weave.Notification(title, description, null, + Weave.Notifications.PRIORITY_WARNING, buttons); + Weave.Notifications.replaceTitle(notification); + this.updateUI(); + }, + + onLogout: function SUI_onLogout() { + this.updateUI(); + }, + + onStartOver: function SUI_onStartOver() { + this.clearError(); + }, + + onQuotaNotice: function onQuotaNotice(subject, data) { + let title = this._stringBundle.GetStringFromName("warning.sync.quota.label"); + let description = this._stringBundle.GetStringFromName("warning.sync.quota.description"); + let buttons = []; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.label"), + this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.accesskey"), + function() { gSyncUI.openQuotaDialog(); return true; } + )); + + let notification = new Weave.Notification( + title, description, null, Weave.Notifications.PRIORITY_WARNING, buttons); + Weave.Notifications.replaceTitle(notification); + }, + + openServerStatus: function () { + let statusURL = Services.prefs.getCharPref("services.sync.statusURL"); + window.openUILinkIn(statusURL, "tab"); + }, + + // Commands + doSync: function SUI_doSync() { + setTimeout(function() Weave.Service.errorHandler.syncAndReportErrors(), 0); + }, + + handleToolbarButton: function SUI_handleStatusbarButton() { + if (this._needsSetup()) + this.openSetup(); + else + this.doSync(); + }, + + //XXXzpao should be part of syncCommon.js - which we might want to make a module... + // To be fixed in a followup (bug 583366) + + /** + * 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 + */ + + openSetup: function SUI_openSetup(wizardType) { + 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); + } + }, + + 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"); + }, + + openQuotaDialog: function SUI_openQuotaDialog() { + let win = Services.wm.getMostRecentWindow("Sync:ViewQuota"); + if (win) + win.focus(); + else + Services.ww.activeWindow.openDialog( + "chrome://browser/content/sync/quota.xul", "", + "centerscreen,chrome,dialog,modal"); + }, + + openPrefs: function SUI_openPrefs() { + openPreferences("paneSync"); + }, + + + // Helpers + _updateLastSyncTime: function SUI__updateLastSyncTime() { + if (!gBrowser) + return; + + let syncButton = document.getElementById("sync-button"); + if (!syncButton) + return; + + let lastSync; + try { + lastSync = Services.prefs.getCharPref("services.sync.lastSync"); + } + catch (e) { }; + if (!lastSync || this._needsSetup()) { + syncButton.removeAttribute("tooltiptext"); + return; + } + + // Show the day-of-week and time (HH:MM) of last sync + let lastSyncDate = new Date(lastSync).toLocaleFormat("%a %H:%M"); + let lastSyncLabel = + this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDate], 1); + + syncButton.setAttribute("tooltiptext", lastSyncLabel); + }, + + clearError: function SUI_clearError(errorString) { + Weave.Notifications.removeAll(errorString); + this.updateUI(); + }, + + onSyncFinish: function SUI_onSyncFinish() { + let title = this._stringBundle.GetStringFromName("error.sync.title"); + + // Clear out sync failures on a successful sync + this.clearError(title); + + if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) { + title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + this.clearError(title); + this._wasDelayed = false; + } + }, + + onSyncError: function SUI_onSyncError() { + let title = this._stringBundle.GetStringFromName("error.sync.title"); + + if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) { + this.onLoginError(); + return; + } + + let description; + if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) { + // Convert to days + let lastSync = + Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400; + description = + this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1); + } else { + let error = Weave.Utils.getErrorString(Weave.Status.sync); + description = + this._stringBundle.formatStringFromName("error.sync.description", [error], 1); + } + let priority = Weave.Notifications.PRIORITY_WARNING; + let buttons = []; + + // Check if the client is outdated in some way + let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE; + for (let [engine, reason] in Iterator(Weave.Status.engines)) + outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE; + + if (outdated) { + description = this._stringBundle.GetStringFromName( + "error.sync.needUpdate.description"); + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.needUpdate.label"), + this._stringBundle.GetStringFromName("error.sync.needUpdate.accesskey"), + function() { + window.openUILinkIn(Services.prefs.getCharPref("services.sync.outdated.url"), "tab"); + return true; + } + )); + } + else if (Weave.Status.sync == Weave.OVER_QUOTA) { + description = this._stringBundle.GetStringFromName( + "error.sync.quota.description"); + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName( + "error.sync.viewQuotaButton.label"), + this._stringBundle.GetStringFromName( + "error.sync.viewQuotaButton.accesskey"), + function() { gSyncUI.openQuotaDialog(); return true; } ) + ); + } + else if (Weave.Status.enforceBackoff) { + priority = Weave.Notifications.PRIORITY_INFO; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"), + this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"), + function() { gSyncUI.openServerStatus(); return true; } + )); + } + else { + priority = Weave.Notifications.PRIORITY_INFO; + buttons.push(new Weave.NotificationButton( + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"), + this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"), + function() { gSyncUI.doSync(); return true; } + )); + } + + let notification = + new Weave.Notification(title, description, null, priority, buttons); + Weave.Notifications.replaceTitle(notification); + + if (this._wasDelayed && Weave.Status.sync != Weave.NO_SYNC_NODE_FOUND) { + title = this._stringBundle.GetStringFromName("error.sync.no_node_found.title"); + Weave.Notifications.removeAll(title); + this._wasDelayed = false; + } + + this.updateUI(); + }, + + observe: function SUI_observe(subject, topic, data) { + if (this._unloaded) { + Cu.reportError("SyncUI observer called after unload: " + topic); + return; + } + + switch (topic) { + case "weave:service:sync:start": + this.onActivityStart(); + break; + case "weave:ui:sync:finish": + this.onSyncFinish(); + break; + case "weave:ui:sync:error": + this.onSyncError(); + break; + case "weave:service:sync:delayed": + this.onSyncDelay(); + break; + case "weave:service:quota:remaining": + this.onQuotaNotice(); + break; + case "weave:service:setup-complete": + this.onSetupComplete(); + break; + case "weave:service:login:start": + this.onActivityStart(); + break; + case "weave:service:login:finish": + this.onLoginFinish(); + break; + case "weave:ui:login:error": + this.onLoginError(); + break; + case "weave:service:logout:finish": + this.onLogout(); + break; + case "weave:service:start-over": + this.onStartOver(); + break; + case "weave:service:ready": + this.initUI(); + break; + case "weave:notification:added": + this.initNotifications(); + break; + case "weave:ui:clear-error": + this.clearError(); + 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"); +}); + diff --git a/application/palemoon/base/content/browser-tabPreviews.js b/application/palemoon/base/content/browser-tabPreviews.js new file mode 100644 index 0000000000..eaae78ba84 --- /dev/null +++ b/application/palemoon/base/content/browser-tabPreviews.js @@ -0,0 +1,1051 @@ +/* +#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 + */ + +/** + * Tab previews utility, produces thumbnails + */ +var tabPreviews = { + aspectRatio: 0.5625, // 16:9 + + get width() { + delete this.width; + return this.width = Math.ceil(screen.availWidth / 5.75); + }, + + get height() { + delete this.height; + return this.height = Math.round(this.width * this.aspectRatio); + }, + + init: function tabPreviews_init() { + if (this._selectedTab) + return; + this._selectedTab = gBrowser.selectedTab; + + gBrowser.tabContainer.addEventListener("TabSelect", this, false); + gBrowser.tabContainer.addEventListener("SSTabRestored", this, false); + }, + + 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, aStore) { + var thumbnail = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + thumbnail.mozOpaque = true; + thumbnail.height = this.height; + thumbnail.width = this.width; + + var ctx = thumbnail.getContext("2d"); + var win = aTab.linkedBrowser.contentWindow; + var snippetWidth = win.innerWidth * .6; + var scale = this.width / snippetWidth; + ctx.scale(scale, scale); + ctx.drawWindow(win, win.scrollX, win.scrollY, + snippetWidth, snippetWidth * this.aspectRatio, "rgb(255,255,255)"); + + if (aStore && + aTab.linkedBrowser /* bug 795608: the tab may got removed while drawing the thumbnail */) { + aTab.__thumbnail = thumbnail; + aTab.__thumbnail_lastURI = aTab.linkedBrowser.currentURI.spec; + } + + return thumbnail; + }, + + 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) { + Cc["@mozilla.org/focus-manager;1"] + .getService(Ci.nsIFocusManager) + .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 recentlyUsedLimit () { + delete this.recentlyUsedLimit; + return this.recentlyUsedLimit = gPrefService.getIntPref("browser.ctrlTab.recentlyUsedLimit"); + }, + 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 () this._selectedIndex < 0 ? + document.activeElement : + this.previews.item(this._selectedIndex), + get isOpen () this.panel.state == "open" || this.panel.state == "showing" || this._timer, + get tabCount () this.tabList.length, + get tabPreviewCount () Math.min(this.previews.length - 1, this.tabCount), + get canvasWidth () Math.min(tabPreviews.width, + Math.ceil(screen.availWidth * .85 / this.tabPreviewCount)), + get canvasHeight () Math.round(this.canvasWidth * tabPreviews.aspectRatio), + + get tabList () { + if (this._tabList) + return this._tabList; + + // Using gBrowser.tabs instead of gBrowser.visibleTabs, as the latter + // exlcudes closing tabs, breaking the following loop in case the the + // selected tab is closing. + let list = Array.filter(gBrowser.tabs, function (tab) !tab.hidden); + + // Rotate the list until the selected tab is first + while (!list[0].selected) + list.push(list.shift()); + + list = list.filter(function (tab) !tab.closing); + + if (this.recentlyUsedLimit != 0) { + let recentlyUsedTabs = []; + for (let tab of this._recentlyUsedTabs) { + if (!tab.hidden && !tab.closing) { + recentlyUsedTabs.push(tab); + if (this.recentlyUsedLimit > 0 && recentlyUsedTabs.length >= this.recentlyUsedLimit) + break; + } + } + for (let i = recentlyUsedTabs.length - 1; i >= 0; i--) { + list.splice(list.indexOf(recentlyUsedTabs[i]), 1); + list.unshift(recentlyUsedTabs[i]); + } + } + + return this._tabList = list; + }, + + init: function ctrlTab_init() { + if (!this._recentlyUsedTabs) { + tabPreviews.init(); + + this._recentlyUsedTabs = [gBrowser.selectedTab]; + 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.showAll.label"); + this.showAllButton.label = + PluralForm.get(this.tabCount, showAllLabel).replace("#1", this.tabCount); + }, + + 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 (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; + + allTabs.close(); + + 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); + + Array.forEach(this.previews, function (preview) { + this.updatePreview(preview, null); + }, this); + + this._tabList = 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._tabList = null; + 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 "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; + } + }, + + _init: function ctrlTab__init(enable) { + var toggleEventListener = enable ? "addEventListener" : "removeEventListener"; + + 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 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; + + // 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 panel + */ +var allTabs = { + get panel () { + delete this.panel; + return this.panel = document.getElementById("allTabs-panel"); + }, + get filterField () { + delete this.filterField; + return this.filterField = document.getElementById("allTabs-filter"); + }, + get container () { + delete this.container; + return this.container = document.getElementById("allTabs-container"); + }, + get tabCloseButton () { + delete this.tabCloseButton; + return this.tabCloseButton = document.getElementById("allTabs-tab-close-button"); + }, + get toolbarButton() document.getElementById("alltabs-button"), + get previews () this.container.getElementsByClassName("allTabs-preview"), + get isOpen () this.panel.state == "open" || this.panel.state == "showing", + + init: function allTabs_init() { + if (this._initiated) + return; + this._initiated = true; + + tabPreviews.init(); + + Array.forEach(gBrowser.tabs, function (tab) { + this._addPreview(tab); + }, this); + + gBrowser.tabContainer.addEventListener("TabOpen", this, false); + gBrowser.tabContainer.addEventListener("TabAttrModified", this, false); + gBrowser.tabContainer.addEventListener("TabMove", this, false); + gBrowser.tabContainer.addEventListener("TabClose", this, false); + }, + + uninit: function allTabs_uninit() { + if (!this._initiated) + return; + + gBrowser.tabContainer.removeEventListener("TabOpen", this, false); + gBrowser.tabContainer.removeEventListener("TabAttrModified", this, false); + gBrowser.tabContainer.removeEventListener("TabMove", this, false); + gBrowser.tabContainer.removeEventListener("TabClose", this, false); + + while (this.container.hasChildNodes()) + this.container.removeChild(this.container.firstChild); + + this._initiated = false; + }, + + prefName: "browser.allTabs.previews", + readPref: function allTabs_readPref() { + var allTabsButton = this.toolbarButton; + if (!allTabsButton) + return; + + if (gPrefService.getBoolPref(this.prefName)) { + allTabsButton.removeAttribute("type"); + allTabsButton.setAttribute("command", "Browser:ShowAllTabs"); + } else { + allTabsButton.setAttribute("type", "menu"); + allTabsButton.removeAttribute("command"); + allTabsButton.removeAttribute("oncommand"); + } + }, + observe: function (aSubject, aTopic, aPrefName) { + this.readPref(); + }, + + pick: function allTabs_pick(aPreview) { + if (!aPreview) + aPreview = this._firstVisiblePreview; + if (aPreview) + this.tabToSelect = aPreview._tab; + + this.close(); + }, + + closeTab: function allTabs_closeTab(event) { + this.filterField.focus(); + gBrowser.removeTab(event.currentTarget._targetPreview._tab); + }, + + filter: function allTabs_filter() { + if (this._currentFilter == this.filterField.value) + return; + + this._currentFilter = this.filterField.value; + + var filter = this._currentFilter.split(/\s+/g); + this._visible = 0; + Array.forEach(this.previews, function (preview) { + var tab = preview._tab; + var matches = 0; + if (filter.length && !tab.hidden) { + let tabstring = tab.linkedBrowser.currentURI.spec; + try { + tabstring = decodeURI(tabstring); + } catch (e) {} + tabstring = tab.label + " " + tab.label.toLocaleLowerCase() + " " + tabstring; + for (let i = 0; i < filter.length; i++) + matches += tabstring.includes(filter[i]); + } + if (matches < filter.length || tab.hidden) { + preview.hidden = true; + } + else { + this._visible++; + this._updatePreview(preview); + preview.hidden = false; + } + }, this); + + this._reflow(); + }, + + open: function allTabs_open() { + var allTabsButton = this.toolbarButton; + if (allTabsButton && + allTabsButton.getAttribute("type") == "menu") { + // Without setTimeout, the menupopup won't stay open when invoking + // "View > Show All Tabs" and the menu bar auto-hides. + setTimeout(function () { + allTabsButton.open = true; + }, 0); + return; + } + + this.init(); + + if (this.isOpen) + return; + + this._maxPanelHeight = Math.max(gBrowser.clientHeight, screen.availHeight / 2); + this._maxPanelWidth = Math.max(gBrowser.clientWidth, screen.availWidth / 2); + + this.filter(); + + tabPreviewPanelHelper.opening(this); + + this.panel.popupBoxObject.setConsumeRollupEvent(PopupBoxObject.ROLLUP_NO_CONSUME); + this.panel.openPopup(gBrowser, "overlap", 0, 0, false, true); + }, + + close: function allTabs_close() { + this.panel.hidePopup(); + }, + + setupGUI: function allTabs_setupGUI() { + this.filterField.focus(); + this.filterField.placeholder = this.filterField.tooltipText; + + this.panel.addEventListener("keypress", this, false); + this.panel.addEventListener("keypress", this, true); + this._browserCommandSet.addEventListener("command", this, false); + + // When the panel is open, a second click on the all tabs button should + // close the panel but not re-open it. + document.getElementById("Browser:ShowAllTabs").setAttribute("disabled", "true"); + }, + + suspendGUI: function allTabs_suspendGUI() { + this.filterField.placeholder = ""; + this.filterField.value = ""; + this._currentFilter = null; + + this._updateTabCloseButton(); + + this.panel.removeEventListener("keypress", this, false); + this.panel.removeEventListener("keypress", this, true); + this._browserCommandSet.removeEventListener("command", this, false); + + setTimeout(function () { + document.getElementById("Browser:ShowAllTabs").removeAttribute("disabled"); + }, 300); + }, + + handleEvent: function allTabs_handleEvent(event) { + if (event.type.startsWith("Tab")) { + var tab = event.target; + if (event.type != "TabOpen") + var preview = this._getPreview(tab); + } + switch (event.type) { + case "TabAttrModified": + // tab attribute modified (e.g. label, crop, busy, image) + if (!preview.hidden) + this._updatePreview(preview); + break; + case "TabOpen": + if (this.isOpen) + this.close(); + this._addPreview(tab); + break; + case "TabMove": + let siblingPreview = tab.nextSibling && + this._getPreview(tab.nextSibling); + if (siblingPreview) + siblingPreview.parentNode.insertBefore(preview, siblingPreview); + else + this.container.lastChild.appendChild(preview); + if (this.isOpen && !preview.hidden) { + this._reflow(); + preview.focus(); + } + break; + case "TabClose": + this._removePreview(preview); + break; + case "keypress": + this._onKeyPress(event); + break; + case "command": + if (event.target.id != "Browser:ShowAllTabs") { + // Close the panel when there's a browser command executing in the background. + this.close(); + } + break; + } + }, + + _visible: 0, + _currentFilter: null, + get _stack () { + delete this._stack; + return this._stack = document.getElementById("allTabs-stack"); + }, + get _browserCommandSet () { + delete this._browserCommandSet; + return this._browserCommandSet = document.getElementById("mainCommandSet"); + }, + get _previewLabelHeight () { + delete this._previewLabelHeight; + return this._previewLabelHeight = parseInt(getComputedStyle(this.previews[0], "").lineHeight); + }, + + get _visiblePreviews () + Array.filter(this.previews, function (preview) !preview.hidden), + + get _firstVisiblePreview () { + if (this._visible == 0) + return null; + var previews = this.previews; + for (let i = 0; i < previews.length; i++) { + if (!previews[i].hidden) + return previews[i]; + } + return null; + }, + + _reflow: function allTabs_reflow() { + this._updateTabCloseButton(); + + const CONTAINER_MAX_WIDTH = this._maxPanelWidth * .95; + const CONTAINER_MAX_HEIGHT = this._maxPanelHeight - 35; + // the size of the whole preview relative to the thumbnail + const REL_PREVIEW_THUMBNAIL = 1.2; + const REL_PREVIEW_HEIGHT_WIDTH = tabPreviews.height / tabPreviews.width; + const PREVIEW_MAX_WIDTH = tabPreviews.width * REL_PREVIEW_THUMBNAIL; + + var rows, previewHeight, previewWidth, outerHeight; + this._columns = Math.floor(CONTAINER_MAX_WIDTH / PREVIEW_MAX_WIDTH); + do { + rows = Math.ceil(this._visible / this._columns); + previewWidth = Math.min(PREVIEW_MAX_WIDTH, + Math.round(CONTAINER_MAX_WIDTH / this._columns)); + previewHeight = Math.round(previewWidth * REL_PREVIEW_HEIGHT_WIDTH); + outerHeight = previewHeight + this._previewLabelHeight; + } while (rows * outerHeight > CONTAINER_MAX_HEIGHT && ++this._columns); + + var outerWidth = previewWidth; + { + let innerWidth = Math.ceil(previewWidth / REL_PREVIEW_THUMBNAIL); + let innerHeight = Math.ceil(previewHeight / REL_PREVIEW_THUMBNAIL); + var canvasStyle = "max-width:" + innerWidth + "px;" + + "min-width:" + innerWidth + "px;" + + "max-height:" + innerHeight + "px;" + + "min-height:" + innerHeight + "px;"; + } + + var previews = Array.slice(this.previews); + + while (this.container.hasChildNodes()) + this.container.removeChild(this.container.firstChild); + for (let i = rows || 1; i > 0; i--) + this.container.appendChild(document.createElement("hbox")); + + var row = this.container.firstChild; + var colCount = 0; + previews.forEach(function (preview) { + if (!preview.hidden && + ++colCount > this._columns) { + row = row.nextSibling; + colCount = 1; + } + preview.setAttribute("minwidth", outerWidth); + preview.setAttribute("height", outerHeight); + preview.setAttribute("canvasstyle", canvasStyle); + preview.removeAttribute("closebuttonhover"); + row.appendChild(preview); + }, this); + + this._stack.width = this._maxPanelWidth; + this.container.width = Math.ceil(outerWidth * Math.min(this._columns, this._visible)); + this.container.left = Math.round((this._maxPanelWidth - this.container.width) / 2); + this.container.maxWidth = this._maxPanelWidth - this.container.left; + this.container.maxHeight = rows * outerHeight; + }, + + _addPreview: function allTabs_addPreview(aTab) { + var preview = document.createElement("button"); + preview.className = "allTabs-preview"; + preview._tab = aTab; + this.container.lastChild.appendChild(preview); + }, + + _removePreview: function allTabs_removePreview(aPreview) { + var updateUI = (this.isOpen && !aPreview.hidden); + aPreview._tab = null; + aPreview.parentNode.removeChild(aPreview); + if (updateUI) { + this._visible--; + this._reflow(); + this.filterField.focus(); + } + }, + + _getPreview: function allTabs_getPreview(aTab) { + var previews = this.previews; + for (let i = 0; i < previews.length; i++) + if (previews[i]._tab == aTab) + return previews[i]; + return null; + }, + + _updateTabCloseButton: function allTabs_updateTabCloseButton(event) { + if (event && event.target == this.tabCloseButton) + return; + + if (this.tabCloseButton._targetPreview) { + if (event && event.target == this.tabCloseButton._targetPreview) + return; + + this.tabCloseButton._targetPreview.removeAttribute("closebuttonhover"); + } + + if (event && + event.target.parentNode.parentNode == this.container && + (event.target._tab.previousSibling || event.target._tab.nextSibling)) { + let canvas = event.target.firstChild.getBoundingClientRect(); + let container = this.container.getBoundingClientRect(); + let tabCloseButton = this.tabCloseButton.getBoundingClientRect(); + let alignLeft = getComputedStyle(this.panel, "").direction == "rtl"; +#ifdef XP_MACOSX + alignLeft = !alignLeft; +#endif + this.tabCloseButton.left = canvas.left - + container.left + + parseInt(this.container.left) + + (alignLeft ? 0 : + canvas.width - tabCloseButton.width); + this.tabCloseButton.top = canvas.top - container.top; + this.tabCloseButton._targetPreview = event.target; + this.tabCloseButton.style.visibility = "visible"; + event.target.setAttribute("closebuttonhover", "true"); + } else { + this.tabCloseButton.style.visibility = "hidden"; + this.tabCloseButton.left = this.tabCloseButton.top = 0; + this.tabCloseButton._targetPreview = null; + } + }, + + _updatePreview: function allTabs_updatePreview(aPreview) { + aPreview.setAttribute("label", aPreview._tab.label); + aPreview.setAttribute("tooltiptext", aPreview._tab.label); + aPreview.setAttribute("crop", aPreview._tab.crop); + if (aPreview._tab.image) + aPreview.setAttribute("image", aPreview._tab.image); + else + aPreview.removeAttribute("image"); + + var thumbnail = tabPreviews.get(aPreview._tab); + if (aPreview.firstChild) { + if (aPreview.firstChild == thumbnail) + return; + aPreview.removeChild(aPreview.firstChild); + } + aPreview.appendChild(thumbnail); + }, + + _onKeyPress: function allTabs_onKeyPress(event) { + if (event.eventPhase == event.CAPTURING_PHASE) { + this._onCapturingKeyPress(event); + return; + } + + if (event.keyCode == event.DOM_VK_ESCAPE) { + this.close(); + event.preventDefault(); + event.stopPropagation(); + return; + } + + if (event.target == this.filterField) { + switch (event.keyCode) { + case event.DOM_VK_UP: + if (this._visible) { + let previews = this._visiblePreviews; + let columns = Math.min(previews.length, this._columns); + previews[Math.floor(previews.length / columns) * columns - 1].focus(); + event.preventDefault(); + event.stopPropagation(); + } + break; + case event.DOM_VK_DOWN: + if (this._visible) { + this._firstVisiblePreview.focus(); + event.preventDefault(); + event.stopPropagation(); + } + break; + } + } + }, + + _onCapturingKeyPress: function allTabs_onCapturingKeyPress(event) { + switch (event.keyCode) { + case event.DOM_VK_UP: + case event.DOM_VK_DOWN: + if (event.target != this.filterField) + this._advanceFocusVertically(event); + break; + case event.DOM_VK_RETURN: + if (event.target == this.filterField) { + this.filter(); + this.pick(); + event.preventDefault(); + event.stopPropagation(); + } + break; + } + }, + + _advanceFocusVertically: function allTabs_advanceFocusVertically(event) { + var preview = document.activeElement; + if (!preview || preview.parentNode.parentNode != this.container) + return; + + event.stopPropagation(); + + var up = (event.keyCode == event.DOM_VK_UP); + var previews = this._visiblePreviews; + + if (up && preview == previews[0]) { + this.filterField.focus(); + return; + } + + var i = previews.indexOf(preview); + var columns = Math.min(previews.length, this._columns); + var column = i % columns; + var row = Math.floor(i / columns); + + function newIndex() row * columns + column; + function outOfBounds() newIndex() >= previews.length; + + if (up) { + row--; + if (row < 0) { + let rows = Math.ceil(previews.length / columns); + row = rows - 1; + column--; + if (outOfBounds()) + row--; + } + } else { + row++; + if (outOfBounds()) { + if (column == columns - 1) { + this.filterField.focus(); + return; + } + row = 0; + column++; + } + } + previews[newIndex()].focus(); + } +}; diff --git a/application/palemoon/base/content/browser-tabPreviews.xml b/application/palemoon/base/content/browser-tabPreviews.xml new file mode 100644 index 0000000000..e957649e75 --- /dev/null +++ b/application/palemoon/base/content/browser-tabPreviews.xml @@ -0,0 +1,75 @@ +<?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> + + <binding id="allTabs-preview" extends="chrome://global/content/bindings/button.xml#button-base"> + <content pack="center" align="center"> + <xul:stack> + <xul:vbox class="allTabs-preview-inner" align="center" pack="center"> + <xul:hbox class="tabPreview-canvas" xbl:inherits="style=canvasstyle"> + <children/> + </xul:hbox> + <xul:label flex="1" xbl:inherits="value=label,crop" class="allTabs-preview-label plain"/> + </xul:vbox> + <xul:hbox class="allTabs-favicon-container"> + <xul:image class="allTabs-favicon" xbl:inherits="src=image"/> + </xul:hbox> + </xul:stack> + </content> + <handlers> + <handler event="command" action="allTabs.pick(this);"/> + <handler event="click" button="1" action="gBrowser.removeTab(this._tab);"/> + + <handler event="dragstart"><![CDATA[ + event.dataTransfer.mozSetDataAt("application/x-moz-node", this._tab, 0); + ]]></handler> + + <handler event="dragover"><![CDATA[ + let tab = event.dataTransfer.mozGetDataAt("application/x-moz-node", 0); + if (tab && tab.parentNode == gBrowser.tabContainer) + event.preventDefault(); + ]]></handler> + + <handler event="drop"><![CDATA[ + let tab = event.dataTransfer.mozGetDataAt("application/x-moz-node", 0); + if (tab && tab.parentNode == gBrowser.tabContainer) { + let newIndex = Array.indexOf(gBrowser.tabs, this._tab); + gBrowser.moveTabTo(tab, newIndex); + } + ]]></handler> + </handlers> + </binding> +</bindings> diff --git a/application/palemoon/base/content/browser-thumbnails.js b/application/palemoon/base/content/browser-thumbnails.js new file mode 100644 index 0000000000..dbe33e3ed2 --- /dev/null +++ b/application/palemoon/base/content/browser-thumbnails.js @@ -0,0 +1,198 @@ +#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 + +/** + * Keeps thumbnails of open web pages up-to-date. + */ +let 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() { + // Bug 863512 - Make page thumbnails work in electrolysis + if (gMultiProcessBrowser) + return; + + try { + if (Services.prefs.getBoolPref("browser.pagethumbnails.capturing_disabled")) + return; + } catch (e) {} + + 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() { + // Bug 863512 - Make page thumbnails work in electrolysis + if (gMultiProcessBrowser) + return; + + 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([browser.currentURI.spec for (browser of gBrowser.browsers)]); + }, + + /** + * 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) { + if (this._shouldCapture(aBrowser)) + PageThumbs.captureAndStore(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) { + // Capture only if it's the currently selected tab. + if (aBrowser != gBrowser.selectedBrowser) + return false; + + // Don't capture in per-window private browsing mode. + if (PrivateBrowsingUtils.isWindowPrivate(window)) + return false; + + let doc = aBrowser.contentDocument; + + // FIXME Bug 720575 - Don't capture thumbnails for SVG or XML documents as + // that currently regresses Talos SVG tests. + if (doc instanceof SVGDocument || doc instanceof XMLDocument) + return false; + + // There's no point in taking screenshot of loading pages. + if (aBrowser.docShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE) + return false; + + // Don't take screenshots of about: pages. + if (aBrowser.currentURI.schemeIs("about")) + return false; + + let channel = aBrowser.docShell.currentDocumentChannel; + + // No valid document channel. We shouldn't take a screenshot. + if (!channel) + return false; + + // Don't take screenshots of internally redirecting about: pages. + // This includes error pages. + let uri = channel.originalURI; + if (uri.schemeIs("about")) + return false; + + let httpChannel; + try { + httpChannel = channel.QueryInterface(Ci.nsIHttpChannel); + } catch (e) { /* Not an HTTP channel. */ } + + if (httpChannel) { + // Continue only if we have a 2xx status code. + try { + if (Math.floor(httpChannel.responseStatus / 100) != 2) + return false; + } catch (e) { + // Can't get response information from the httpChannel + // because mResponseHead is not available. + return false; + } + + // Cache-Control: no-store. + if (httpChannel.isNoStoreResponse()) + return false; + + // Don't capture HTTPS pages unless the user explicitly enabled it. + if (uri.schemeIs("https") && !this._sslDiskCacheEnabled) + return false; + } + + return true; + }, + + _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/palemoon/base/content/browser-title.css b/application/palemoon/base/content/browser-title.css new file mode 100644 index 0000000000..66b5e6731b --- /dev/null +++ b/application/palemoon/base/content/browser-title.css @@ -0,0 +1,204 @@ +/* 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/. */ + +#main-window::after { + content: attr(title); + line-height: 50px; + max-height: 50px; + overflow: -moz-hidden-unscrollable; + pointer-events: none; + position: fixed; + word-wrap: break-word; + -moz-hyphens: auto; + color: CaptionText; + font-weight: bold; + text-align: left; +} + +#main-window:-moz-window-inactive::after { + color: InactiveCaptionText; +} + +/* Win10 doesn't respond to inactive caption, so dim it instead */ +@media (-moz-os-version: windows-win10) { + #main-window:-moz-window-inactive::after { + opacity: 0.5; + } +} + +/* Hide in fullscreen/TiT mode */ +#main-window[inFullscreen="true"]::after, +#main-window[sizemode="maximized"][tabsintitlebar="true"]::after, +#main-window:not([chromemargin])::after { + opacity: 0 !important; +} + + +#main-window::after { + padding: 0 132px; /* AppMenu button/wincontrols width offset */ + left: 0; + right: 0; +} + +#main-window[privatebrowsingmode=temporary]::after { + padding: 0px 132px 0px 152px; /* AppMenu button width offset for PB mode */ +} + +#main-window[sizemode="normal"]::after { + left: -12px; + right: -12px; +} + +/* Lightweight Themes */ + +#main-window:-moz-lwtheme::after { + color: inherit; + text-shadow: inherit; +} + +/* Windows Classic theme */ + +@media all and (-moz-windows-classic) { + + #main-window::after { + top: -13px; + text-shadow: none !important; + background-position: 2px 18px; + } + +} + + +/* Windows Aero (Vista, non-glass 7/8) */ + +@media all and (-moz-windows-theme: aero) { + + #main-window::after { + top: -11px; + font-weight: normal; + text-shadow: none; + background-position: 2px 17px; + } + + #main-window[sizemode="maximized"]::after { + top: -7px; + } + +} + + +/* Windows Aero Glass */ + +@media (-moz-windows-glass) { + + #main-window::after { + top: -13px; + color: black; + text-shadow: rgba(255,255,255,.6) 7px -1px 12px, + rgba(255,255,255,.6) 6px -1px 13px, + rgba(255,255,255,.9) 5px -1px 14px, + rgba(255,255,255,.6) -7px -1px 12px, + rgba(255,255,255,.6) -6px -1px 13px, + rgba(255,255,255,.9) -5px -1px 14px; + z-index: -99999; + background-position: 2px 18px; + font-weight: bold; + } + + #main-window[sizemode="maximized"]::after { + top: -7px; + } + + #main-window:-moz-window-inactive::after { + opacity: .9; + color: black; + text-shadow: rgba(255,255,255,.7) 7px -1px 12px, + rgba(255,255,255,.5) 6px -1px 13px, + rgba(255,255,255,.5) 5px -1px 14px, + rgba(255,255,255,.7) -7px -1px 12px, + rgba(255,255,255,.5) -6px -1px 13px, + rgba(255,255,255,.5) -5px -1px 14px; + } + +} + + +/* Generic other themes */ + +@media all and (-moz-windows-theme: generic) { + + #main-window::after { + font-family: trebuchet MS; + font-size: 13px; + text-shadow: 1px 1px rgba(0, 0, 0, .2); + top: -9px; + background-position: 2px 16px; + } + + #main-window:-moz-window-inactive::after { + text-shadow: none; + } + +} + + +/* Compositor style for Win 8/10 */ + +@media all and (-moz-windows-compositor) { + @media not all and (-moz-windows-glass) { + + #main-window::after { + background-position: 4px 17px; + top: -13px; + } + + @media (-moz-os-version: windows-win8) { + #main-window::after { + font-size: 15px; + text-align: center; + } + + #main-window[darkwindowframe="true"]:not(:-moz-window-inactive):not(:-moz-lwtheme)::after { + /* Dark window frame/accent color on Win 8 */ + color: white; + } + } + + @media (-moz-os-version: windows-win10) { + #main-window::after { + text-align: left; + } + + @media (-moz-windows-accent-color-applies: 0) { + #main-window:not(:-moz-window-inactive):not(:-moz-lwtheme)::after { + /* Default Windows 10 styling is white - apply black text styling */ + color: black; + } + } + + @media (-moz-windows-accent-color-applies) { + #main-window:not(:-moz-window-inactive):not(:-moz-lwtheme)::after { + /* Accent color is applied - use the associated text styling */ + color: -moz-win-accentcolortext; + } + } + } + + #main-window[sizemode="maximized"]::after { + top: -5px; + } + } + +} + + +/* Hide for small windows */ + +@media not all and (min-width: 320px) { + + #main-window::after { + opacity: 0 !important; + } + +}
\ No newline at end of file diff --git a/application/palemoon/base/content/browser-webrtcUI.js b/application/palemoon/base/content/browser-webrtcUI.js new file mode 100644 index 0000000000..a6c9008ca9 --- /dev/null +++ b/application/palemoon/base/content/browser-webrtcUI.js @@ -0,0 +1,55 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +let WebrtcIndicator = { + init: function () { + let temp = {}; + Cu.import("resource:///modules/webrtcUI.jsm", temp); + this.UIModule = temp.webrtcUI; + + this.updateButton(); + }, + + get button() { + delete this.button; + return this.button = document.getElementById("webrtc-status-button"); + }, + + updateButton: function () { + this.button.hidden = !this.UIModule.showGlobalIndicator; + }, + + fillPopup: function (aPopup) { + this._menuitemData = new WeakMap; + for (let streamData of this.UIModule.activeStreams) { + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("label", streamData.uri); + menuitem.setAttribute("tooltiptext", streamData.uri); + + this._menuitemData.set(menuitem, streamData); + + aPopup.appendChild(menuitem); + } + }, + + clearPopup: function (aPopup) { + while (aPopup.lastChild) + aPopup.removeChild(aPopup.lastChild); + }, + + menuCommand: function (aMenuitem) { + let streamData = this._menuitemData.get(aMenuitem); + if (!streamData) + return; + + let browserWindow = streamData.browser.ownerDocument.defaultView; + if (streamData.tab) { + browserWindow.gBrowser.selectedTab = streamData.tab; + } else { + streamData.browser.focus(); + } + browserWindow.focus(); + } +} diff --git a/application/palemoon/base/content/browser.css b/application/palemoon/base/content/browser.css new file mode 100644 index 0000000000..2c8ba3e694 --- /dev/null +++ b/application/palemoon/base/content/browser.css @@ -0,0 +1,693 @@ +/* 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"); + +searchbar { + -moz-binding: url("chrome://browser/content/search/search.xml#searchbar"); +} + +browser[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; +} + +#alltabs-button { /* Pale Moon: Always show this button! (less jumpy UI) */ + visibility: visible !important; +} + +#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 */ +} + +.tabbrowser-tab { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tab"); +} + +.tabbrowser-tab:not([pinned]) { + -moz-box-flex: 100; + max-width: 250px; + min-width: 100px; + width: 0; + transition: min-width 175ms ease-out, + max-width 200ms ease-out, + opacity 80ms ease-out 20ms /* hide the tab for the first 20ms of the max-width transition */; +} + +.tabbrowser-tab:not([pinned]):not([fadein]) { + max-width: 0.1px; + min-width: 0.1px; + opacity: 0 !important; + transition: min-width 175ms ease-out, + max-width 200ms ease-out, + opacity 80ms ease-out 180ms /* hide the tab for the last 20ms of the max-width transition */; +} + +.tab-throbber:not([fadein]):not([pinned]), +.tab-label:not([fadein]):not([pinned]), +.tab-icon-image:not([fadein]):not([pinned]), +.tab-close-button:not([fadein]):not([pinned]) { + 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; +} + +#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-menubar { + -moz-box-ordinal-group: 5; +} + +#navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar) { + -moz-box-ordinal-group: 50; +} + +#TabsToolbar { + -moz-box-ordinal-group: 100; +} + +#TabsToolbar[tabsontop="true"] { + -moz-box-ordinal-group: 10; +} + +%ifdef CAN_DRAW_IN_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"); +} + +#titlebar-spacer { + pointer-events: none; +} + +#main-window[tabsintitlebar] #appmenu-button-container, +#main-window[tabsintitlebar] #titlebar-buttonbox { + position: relative; +} +%endif + +#main-window[inDOMFullscreen] #sidebar-box, +#main-window[inDOMFullscreen] #sidebar-splitter { + visibility: collapse; +} + +.bookmarks-toolbar-customize, +#wrapper-personal-bookmarks > #personal-bookmarks > #PlacesToolbar > hbox > #PlacesToolbarItems { + display: none; +} + +#wrapper-personal-bookmarks[place="toolbar"] > #personal-bookmarks > #PlacesToolbar > .bookmarks-toolbar-customize { + display: -moz-box; +} + +#main-window[disablechrome] #navigator-toolbox[tabsontop="true"] > toolbar:not(#toolbar-menubar):not(#TabsToolbar) { + visibility: collapse; +} + +#wrapper-urlbar-container #urlbar-container > #urlbar > toolbarbutton, +#urlbar-container:not([combined]) > #urlbar > toolbarbutton, +#urlbar-container[combined] + #reload-button + #stop-button, +#urlbar-container[combined] + #reload-button, +toolbar:not([mode="icons"]) > #urlbar-container > #urlbar > toolbarbutton, +toolbar[mode="icons"] > #urlbar-container > #urlbar > #urlbar-reload-button:not([displaystop]) + #urlbar-stop-button, +toolbar[mode="icons"] > #urlbar-container > #urlbar > #urlbar-reload-button[displaystop], +toolbar[mode="icons"] > #reload-button:not([displaystop]) + #stop-button, +toolbar[mode="icons"] > #reload-button[displaystop] { + visibility: collapse; +} + +#feed-button > .toolbarbutton-menu-dropmarker { + display: none; +} + +#feed-menu > .feed-menuitem:-moz-locale-dir(rtl) { + direction: rtl; +} + +#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; +} + +splitmenu { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#splitmenu"); +} + +.splitmenu-menuitem { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem"); + list-style-image: inherit; + -moz-image-region: inherit; +} + +.splitmenu-menuitem[iconic="true"] { + -moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic"); +} + +.splitmenu-menu > .menu-text, +:-moz-any(.splitmenu-menu, .splitmenu-menuitem) > .menu-accel-container, +#appmenu-editmenu > .menu-text, +#appmenu-editmenu > .menu-accel-container { + display: none; +} + +.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"); +} + +%ifdef MENUBAR_CAN_AUTOHIDE +%ifndef CAN_DRAW_IN_TITLEBAR +#appmenu-toolbar-button > .toolbarbutton-text { + display: -moz-box; +} +%endif + +#appmenu_offlineModeRecovery:not([checked=true]) { + display: none; +} +%endif + +/* 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); +} + +.ac-url-text:-moz-locale-dir(rtl), +.ac-title:-moz-locale-dir(rtl) > description { + direction: ltr !important; +} + +/* For results that are actions, their description text is shown instead of + the URL - this needs to follow the locale's direction, unlike URLs. */ +panel:not([noactions]) > richlistbox > richlistitem[type~="action"]:-moz-locale-dir(rtl) > .ac-url-box { + direction: rtl; +} + +panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-url > .ac-action-text, +panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-action-icon { + visibility: collapse; +} + +panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-url > .ac-url-text { + visibility: visible; +} + +#urlbar:not([actiontype]) > #urlbar-display-box { + display: none; +} + +#wrapper-urlbar-container > #urlbar-container > #urlbar { + -moz-user-input: disabled; + cursor: -moz-grab; +} + +#PopupAutoComplete { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup"); +} + +#PopupAutoCompleteRichResult { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup"); +} + +/* Pale Moon: Address bar: Feeds */ +#ub-feed-button > .button-box > .box-inherit > .button-text, +#ub-feed-button > .button-box > .button-menu-dropmarker { + display: none; +} + +#ub-feed-menu > .feed-menuitem:-moz-locale-dir(rtl) { + direction: rtl; +} + + +#urlbar-container[combined] > #urlbar > #urlbar-icons > #go-button, +#urlbar[pageproxystate="invalid"] > #urlbar-icons > .urlbar-icon:not(#go-button), +#urlbar[pageproxystate="valid"] > #urlbar-icons > #go-button, +#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; +} + +#urlbar[pageproxystate="invalid"] > #identity-box { + pointer-events: none; +} + +#identity-icon-labels { + max-width: 18em; +} + +#identity-icon-country-label { + direction: ltr; +} + +#identity-box.verifiedIdentity > #identity-icon-labels > #identity-icon-label { + -moz-margin-end: 0.25em !important; +} + +#wrapper-search-container > #search-container > #searchbar > .searchbar-textbox > .autocomplete-textbox-container > .textbox-input-box > html|*.textbox-input { + 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; +} + +%ifdef MENUBAR_CAN_AUTOHIDE +#toolbar-menubar:not([autohide="true"]) ~ toolbar > #bookmarks-menu-button, +#toolbar-menubar:not([autohide="true"]) > #bookmarks-menu-button, +#toolbar-menubar:not([autohide="true"]) ~ toolbar > #history-menu-button, +#toolbar-menubar:not([autohide="true"]) > #history-menu-button { + display: none; +} +%endif + +#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; +} + +#sidebar-header > .tabs-closebutton { + -moz-user-focus: normal; +} + +/* apply Fitts' law to the notification bar's close button */ +window[sizemode="maximized"] #content .notification-inner { + border-right: 0px !important; +} + +/* Hide extension toolbars that neglected to set the proper class */ +window[chromehidden~="location"][chromehidden~="toolbar"] toolbar:not(.chromeclass-menubar), +window[chromehidden~="toolbar"] toolbar:not(.toolbar-primary):not(#nav-bar):not(#TabsToolbar):not(#print-preview-toolbar):not(.chromeclass-menubar) { + display: none; +} + +#navigator-toolbox , +#status-bar , +#mainPopupSet { + min-width: 1px; +} + +%ifdef MOZ_SERVICES_SYNC +/* Sync notification UI */ +#sync-notifications { + -moz-binding: url("chrome://browser/content/sync/notification.xml#notificationbox"); + overflow-y: visible !important; +} + +#sync-notifications notification { + -moz-binding: url("chrome://browser/content/sync/notification.xml#notification"); +} +%endif + +/* 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); +} + +/* Identity UI */ +#identity-popup-content-box.unknownIdentity > #identity-popup-connectedToLabel , +#identity-popup-content-box.unknownIdentity > #identity-popup-runByLabel , +#identity-popup-content-box.unknownIdentity > #identity-popup-content-host , +#identity-popup-content-box.unknownIdentity > #identity-popup-content-owner , +#identity-popup-content-box.verifiedIdentity > #identity-popup-connectedToLabel2 , +#identity-popup-content-box.verifiedDomain > #identity-popup-connectedToLabel2 { + display: none; +} + +/* Full Screen UI */ + +#fullscr-toggler { + height: 1px; + background: black; +} + +#full-screen-warning-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2147483647 !important; +} + +#full-screen-warning-container[fade-warning-out] { + transition-property: opacity !important; + transition-duration: 500ms !important; + opacity: 0.0; +} + +/* When the modal fullscreen approval UI is showing, don't allow interaction + with the page, but when we're just showing the warning upon entering + fullscreen on an already approved page, do allow interaction with the page. + */ +#full-screen-warning-container:not([obscure-browser]) { + pointer-events: none; +} + +#full-screen-warning-message { + /* We must specify a max-width, otherwise word-wrap:break-word doesn't + work in descendant <description> and <label> elements. Bug 630864. */ + max-width: 800px; +} + +#full-screen-domain-text, +#full-screen-remember-decision > .checkbox-label-box > .checkbox-label { + word-wrap: break-word; + /* We must specify a min-width, otherwise word-wrap:break-word doesn't work. Bug 630864. */ + min-width: 1px; +} + +#nav-bar[mode="text"] > #window-controls > toolbarbutton > .toolbarbutton-icon { + display: -moz-box; +} + +#nav-bar[mode="text"] > #window-controls > toolbarbutton > .toolbarbutton-text { + display: none; +} + +/* ::::: Keyboard UI Panel ::::: */ +.KUI-panel-closebutton { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-image"); +} + +:-moz-any(.ctrlTab-preview, .allTabs-preview) > html|img, +:-moz-any(.ctrlTab-preview, .allTabs-preview) > html|canvas { + min-width: inherit; + max-width: inherit; + min-height: inherit; + max-height: inherit; +} + +.ctrlTab-favicon-container, +.allTabs-favicon-container { + -moz-box-align: start; +%ifdef XP_MACOSX + -moz-box-pack: end; +%else + -moz-box-pack: start; +%endif +} + +.ctrlTab-favicon, +.allTabs-favicon { + width: 16px; + height: 16px; +} + +/* ::::: Ctrl-Tab Panel ::::: */ +.ctrlTab-preview { + -moz-binding: url("chrome://browser/content/browser-tabPreviews.xml#ctrlTab-preview"); +} + +/* ::::: All Tabs Panel ::::: */ +.allTabs-preview { + -moz-binding: url("chrome://browser/content/browser-tabPreviews.xml#allTabs-preview"); +} + +#allTabs-tab-close-button { + -moz-binding: url("chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-image"); + margin: 0; +} + + +/* notification anchors should only be visible when their associated + notifications are */ +.notification-anchor-icon { + -moz-user-focus: normal; +} + +.notification-anchor-icon:not([showing]) { + display: none; +} + +/* This was added with the identity toolkit, does it have any other purpose? */ +#notification-popup .text-link.custom-link { + -moz-binding: url("chrome://global/content/bindings/text.xml#text-label"); + text-decoration: none; +} + +#invalid-form-popup > description { + max-width: 280px; +} + +.form-validation-anchor { + /* should occupy space but not be visible */ + opacity: 0; + visibility: hidden; + pointer-events: none; +} + +#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"); +} + +/* override hidden="true" for the status bar compatibility shim + in case it was persisted for the real status bar */ +#status-bar { + display: -moz-box; +} + +/* Remove the resizer from the statusbar compatibility shim */ +#status-bar[hideresizer] > .statusbar-resizerpanel { + display: none; +} + +browser[tabmodalPromptShowing] { + -moz-user-focus: none !important; +} + +/* Status panel */ + +statuspanel { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#statuspanel"); + position: fixed; + margin-top: -3em; + left: 0; + 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; +} + +.panel-inner-arrowcontentfooter[footertype="promobox"] { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#promobox"); +} + +/* highlighter */ +%include highlighter.css + +/* 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; +} + +.toolbarbutton-badge[badge]:not([badge=""])::after { + content: attr(badge); +} + +toolbarbutton[type="badged"] { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#toolbarbutton-badged"); +} + +/* Strict icon size for PMkit 'ui/button' */ +toolbarbutton[pmkit-button="true"] > .toolbarbutton-badge-container > .toolbarbutton-icon { + width: 16px; + height: 16px; +} + +/* Remove white bar at the bottom of the screen when watching HTML5 video in fullscreen */ +#main-window[inFullscreen] #global-notificationbox, +#main-window[inFullscreen] #high-priority-global-notificationbox { + visibility: collapse; +} diff --git a/application/palemoon/base/content/browser.js b/application/palemoon/base/content/browser.js new file mode 100644 index 0000000000..34b91b6cb1 --- /dev/null +++ b/application/palemoon/base/content/browser.js @@ -0,0 +1,7163 @@ +# -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +let Ci = Components.interfaces; +let Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource:///modules/RecentWindow.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", + "resource:///modules/CharsetMenu.jsm"); + +const nsIWebNavigation = Ci.nsIWebNavigation; +const gToolbarInfoSeparators = ["|", "-"]; + +var gLastBrowserCharset = null; +var gPrevCharset = null; +var gProxyFavIcon = null; +var gLastValidURLStr = ""; +var gInPrintPreviewMode = false; +var gContextMenu = null; // nsContextMenu instance +var gMultiProcessBrowser = false; + +#ifndef XP_MACOSX +var gEditUIVisible = true; +#endif + +[ + ["gBrowser", "content"], + ["gNavToolbox", "navigator-toolbox"], + ["gURLBar", "urlbar"], + ["gNavigatorBundle", "bundle_browser"] +].forEach(function (elementGlobal) { + var [name, id] = elementGlobal; + window.__defineGetter__(name, function () { + var element = document.getElementById(id); + if (!element) + return null; + delete window[name]; + return window[name] = element; + }); + window.__defineSetter__(name, function (val) { + delete window[name]; + return window[name] = val; + }); +}); + +// Smart getter for the findbar. If you don't wish to force the creation of +// the findbar, check gFindBarInitialized first. +var gFindBarInitialized = false; +XPCOMUtils.defineLazyGetter(window, "gFindBar", function() { + let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let findbar = document.createElementNS(XULNS, "findbar"); + findbar.id = "FindToolbar"; + + let browserBottomBox = document.getElementById("browser-bottombox"); + browserBottomBox.insertBefore(findbar, browserBottomBox.firstChild); + + // Force a style flush to ensure that our binding is attached. + findbar.clientTop; + findbar.browser = gBrowser; + window.gFindBarInitialized = true; + return findbar; +}); + +XPCOMUtils.defineLazyGetter(this, "gPrefService", function() { + return Services.prefs; +}); + +this.__defineGetter__("AddonManager", function() { + let tmp = {}; + Cu.import("resource://gre/modules/AddonManager.jsm", tmp); + return this.AddonManager = tmp.AddonManager; +}); +this.__defineSetter__("AddonManager", function (val) { + delete this.AddonManager; + return this.AddonManager = val; +}); + +this.__defineGetter__("PluralForm", function() { + Cu.import("resource://gre/modules/PluralForm.jsm"); + return this.PluralForm; +}); +this.__defineSetter__("PluralForm", function (val) { + delete this.PluralForm; + return this.PluralForm = val; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils", + "resource:///modules/AboutHomeUtils.jsm"); + +#ifdef MOZ_SERVICES_SYNC +XPCOMUtils.defineLazyModuleGetter(this, "Weave", + "resource://services-sync/main.js"); +#endif + +XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () { + let tmp = {}; + Cu.import("resource:///modules/PopupNotifications.jsm", tmp); + try { + return new tmp.PopupNotifications(gBrowser, + document.getElementById("notification-popup"), + document.getElementById("notification-popup-box")); + } catch (ex) { + Cu.reportError(ex); + return null; + } +}); + +#ifdef MOZ_DEVTOOLS +XPCOMUtils.defineLazyGetter(this, "DeveloperToolbar", function() { + let tmp = {}; + Cu.import("resource://gre/modules/devtools/DeveloperToolbar.jsm", tmp); + return new tmp.DeveloperToolbar(window, document.getElementById("developer-toolbar")); +}); + +XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function() { + let tmp = {}; + Cu.import("resource://gre/modules/devtools/ToolboxProcess.jsm", tmp); + return tmp.BrowserToolboxProcess; +}); +#endif + +XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", + "resource://gre/modules/PageThumbs.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "gBrowserNewTabPreloader", + "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FormValidationHandler", + "resource:///modules/FormValidationHandler.jsm"); + +let gInitialPages = [ + "about:blank", + "about:newtab", + "about:home", + "about:privatebrowsing", + "about:sessionrestore", + "about:logopage" +]; + +#include browser-addons.js +#include browser-feeds.js +#include browser-fullScreen.js +#include browser-fullZoom.js +#include browser-places.js +#include browser-plugins.js +#include browser-tabPreviews.js +#include browser-thumbnails.js +#include browser-webrtcUI.js +#include browser-gestureSupport.js + +#ifdef MOZ_SERVICES_SYNC +#include browser-syncui.js +#endif + +XPCOMUtils.defineLazyGetter(this, "Win7Features", function () { +#ifdef XP_WIN + const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; + if (WINTASKBAR_CONTRACTID in Cc && + Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) { + let AeroPeek = Cu.import("resource:///modules/WindowsPreviewPerTab.jsm", {}).AeroPeek; + return { + onOpenWindow: function () { + AeroPeek.onOpenWindow(window); + }, + onCloseWindow: function () { + AeroPeek.onCloseWindow(window); + } + }; + } +#endif + return null; +}); + +XPCOMUtils.defineLazyGetter(this, "PageMenu", function() { + let tmp = {}; + Cu.import("resource:///modules/PageMenu.jsm", tmp); + return new tmp.PageMenu(); +}); + +/** +* We can avoid adding multiple load event listeners and save some time by adding +* one listener that calls all real handlers. +*/ +function pageShowEventHandlers(persisted) { + charsetLoadListener(); + XULBrowserWindow.asyncUpdateUI(); + + // The PluginClickToPlay events are not fired when navigating using the + // BF cache. |persisted| is true when the page is loaded from the + // BF cache, so this code reshows the notification if necessary. + if (persisted) + gPluginHandler.reshowClickToPlayNotification(); +} + +function UpdateBackForwardCommands(aWebNavigation) { + var backBroadcaster = document.getElementById("Browser:Back"); + var forwardBroadcaster = document.getElementById("Browser:Forward"); + + // Avoid setting attributes on broadcasters if the value hasn't changed! + // Remember, guys, setting attributes on elements is expensive! They + // get inherited into anonymous content, broadcast to other widgets, etc.! + // Don't do it if the value hasn't changed! - dwh + + var backDisabled = backBroadcaster.hasAttribute("disabled"); + var forwardDisabled = forwardBroadcaster.hasAttribute("disabled"); + if (backDisabled == aWebNavigation.canGoBack) { + if (backDisabled) + backBroadcaster.removeAttribute("disabled"); + else + backBroadcaster.setAttribute("disabled", true); + } + + if (forwardDisabled == aWebNavigation.canGoForward) { + if (forwardDisabled) + forwardBroadcaster.removeAttribute("disabled"); + else + forwardBroadcaster.setAttribute("disabled", true); + } +} + +/** + * Click-and-Hold implementation for the Back and Forward buttons + * XXXmano: should this live in toolbarbutton.xml? + */ +function SetClickAndHoldHandlers() { + var timer; + + function openMenu(aButton) { + cancelHold(aButton); + aButton.firstChild.hidden = false; + aButton.open = true; + } + + function mousedownHandler(aEvent) { + if (aEvent.button != 0 || + aEvent.currentTarget.open || + aEvent.currentTarget.disabled) + return; + + // Prevent the menupopup from opening immediately + aEvent.currentTarget.firstChild.hidden = true; + + aEvent.currentTarget.addEventListener("mouseout", mouseoutHandler, false); + aEvent.currentTarget.addEventListener("mouseup", mouseupHandler, false); + timer = setTimeout(openMenu, 500, aEvent.currentTarget); + } + + function mouseoutHandler(aEvent) { + let buttonRect = aEvent.currentTarget.getBoundingClientRect(); + if (aEvent.clientX >= buttonRect.left && + aEvent.clientX <= buttonRect.right && + aEvent.clientY >= buttonRect.bottom) + openMenu(aEvent.currentTarget); + else + cancelHold(aEvent.currentTarget); + } + + function mouseupHandler(aEvent) { + cancelHold(aEvent.currentTarget); + } + + function cancelHold(aButton) { + clearTimeout(timer); + aButton.removeEventListener("mouseout", mouseoutHandler, false); + aButton.removeEventListener("mouseup", mouseupHandler, false); + } + + function clickHandler(aEvent) { + if (aEvent.button == 0 && + aEvent.target == aEvent.currentTarget && + !aEvent.currentTarget.open && + !aEvent.currentTarget.disabled) { + let cmdEvent = document.createEvent("xulcommandevent"); + cmdEvent.initCommandEvent("command", true, true, window, 0, + aEvent.ctrlKey, aEvent.altKey, aEvent.shiftKey, + aEvent.metaKey, null); + aEvent.currentTarget.dispatchEvent(cmdEvent); + } + } + + function _addClickAndHoldListenersOnElement(aElm) { + aElm.addEventListener("mousedown", mousedownHandler, true); + aElm.addEventListener("click", clickHandler, true); + } + + // Bug 414797: Clone unified-back-forward-button's context menu into both the + // back and the forward buttons. + var unifiedButton = document.getElementById("unified-back-forward-button"); + if (unifiedButton && !unifiedButton._clickHandlersAttached) { + unifiedButton._clickHandlersAttached = true; + + let popup = document.getElementById("backForwardMenu").cloneNode(true); + popup.removeAttribute("id"); + // Prevent the context attribute on unified-back-forward-button from being + // inherited. + popup.setAttribute("context", ""); + + let backButton = document.getElementById("back-button"); + backButton.setAttribute("type", "menu"); + backButton.appendChild(popup); + _addClickAndHoldListenersOnElement(backButton); + + let forwardButton = document.getElementById("forward-button"); + popup = popup.cloneNode(true); + forwardButton.setAttribute("type", "menu"); + forwardButton.appendChild(popup); + _addClickAndHoldListenersOnElement(forwardButton); + } +} + +const gSessionHistoryObserver = { + observe: function(subject, topic, data) + { + if (topic != "browser:purge-session-history") + return; + + var backCommand = document.getElementById("Browser:Back"); + backCommand.setAttribute("disabled", "true"); + var fwdCommand = document.getElementById("Browser:Forward"); + fwdCommand.setAttribute("disabled", "true"); + + // Hide session restore button on about:home + window.messageManager.broadcastAsyncMessage("Browser:HideSessionRestoreButton"); + + if (gURLBar) { + // Clear undo history of the URL bar + gURLBar.editor.transactionManager.clear() + } + } +}; + +/** + * Given a starting docshell and a URI to look up, find the docshell the URI + * is loaded in. + * @param aDocument + * A document to find instead of using just a URI - this is more specific. + * @param aDocShell + * The doc shell to start at + * @param aSoughtURI + * The URI that we're looking for + * @returns The doc shell that the sought URI is loaded in. Can be in + * subframes. + */ +function findChildShell(aDocument, aDocShell, aSoughtURI) { + aDocShell.QueryInterface(Components.interfaces.nsIWebNavigation); + aDocShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor); + var doc = aDocShell.getInterface(Components.interfaces.nsIDOMDocument); + if ((aDocument && doc == aDocument) || + (aSoughtURI && aSoughtURI.spec == aDocShell.currentURI.spec)) + return aDocShell; + + var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeItem); + for (var i = 0; i < node.childCount; ++i) { + var docShell = node.getChildAt(i); + docShell = findChildShell(aDocument, docShell, aSoughtURI); + if (docShell) + return docShell; + } + return null; +} + +var gPopupBlockerObserver = { + _reportButton: null, + + onReportButtonClick: function (aEvent) + { + if (aEvent.button != 0 || aEvent.target != this._reportButton) + return; + + document.getElementById("blockedPopupOptions") + .openPopup(this._reportButton, "after_end", 0, 2, false, false, aEvent); + }, + + handleEvent: function (aEvent) + { + if (aEvent.originalTarget != gBrowser.selectedBrowser) + return; + + if (!this._reportButton && gURLBar) + this._reportButton = document.getElementById("page-report-button"); + + if (!gBrowser.selectedBrowser.blockedPopups) { + // Hide the icon in the location bar (if the location bar exists) + if (gURLBar) + this._reportButton.hidden = true; + return; + } + + if (gURLBar) + this._reportButton.hidden = false; + + // Only show the notification again if we've not already shown it. Since + // notifications are per-browser, we don't need to worry about re-adding + // it. + if (!gBrowser.selectedBrowser.blockedPopups.reported) { + if (gPrefService.getBoolPref("privacy.popups.showBrowserMessage")) { + var brandBundle = document.getElementById("bundle_brand"); + var brandShortName = brandBundle.getString("brandShortName"); + var popupCount = gBrowser.selectedBrowser.blockedPopups.length; + var popupButtonText = gNavigatorBundle.getString("popupWarningButton"); + var popupButtonAccesskey = gNavigatorBundle.getString("popupWarningButton.accesskey"); + var messageBase = gNavigatorBundle.getString("popupWarning.message"); + var message = PluralForm.get(popupCount, messageBase) + .replace("#1", brandShortName) + .replace("#2", popupCount); + + var notificationBox = gBrowser.getNotificationBox(); + var notification = notificationBox.getNotificationWithValue("popup-blocked"); + if (notification) { + notification.label = message; + } + else { + var buttons = [{ + label: popupButtonText, + accessKey: popupButtonAccesskey, + popup: "blockedPopupOptions", + callback: null + }]; + + const priority = notificationBox.PRIORITY_WARNING_MEDIUM; + notificationBox.appendNotification(message, "popup-blocked", + "chrome://browser/skin/Info.png", + priority, buttons); + } + } + + // Record the fact that we've reported this blocked popup, so we don't + // show it again. + gBrowser.selectedBrowser.blockedPopups.reported = true; + } + }, + + toggleAllowPopupsForSite: function (aEvent) + { + var pm = Services.perms; + var shouldBlock = aEvent.target.getAttribute("block") == "true"; + var perm = shouldBlock ? pm.DENY_ACTION : pm.ALLOW_ACTION; + pm.add(gBrowser.currentURI, "popup", perm); + + gBrowser.getNotificationBox().removeCurrentNotification(); + }, + + fillPopupList: function (aEvent) + { + // XXXben - rather than using |currentURI| here, which breaks down on multi-framed sites + // we should really walk the blockedPopups and create a list of "allow for <host>" + // menuitems for the common subset of hosts present in the report, this will + // make us frame-safe. + // + // XXXjst - Note that when this is fixed to work with multi-framed sites, + // also back out the fix for bug 343772 where + // nsGlobalWindow::CheckOpenAllow() was changed to also + // check if the top window's location is whitelisted. + let browser = gBrowser.selectedBrowser; + var uri = browser.currentURI; + var blockedPopupAllowSite = document.getElementById("blockedPopupAllowSite"); + try { + blockedPopupAllowSite.removeAttribute("hidden"); + + var pm = Services.perms; + if (pm.testPermission(uri, "popup") == pm.ALLOW_ACTION) { + // Offer an item to block popups for this site, if a whitelist entry exists + // already for it. + let blockString = gNavigatorBundle.getFormattedString("popupBlock", [uri.host || uri.spec]); + blockedPopupAllowSite.setAttribute("label", blockString); + blockedPopupAllowSite.setAttribute("block", "true"); + } + else { + // Offer an item to allow popups for this site + let allowString = gNavigatorBundle.getFormattedString("popupAllow", [uri.host || uri.spec]); + blockedPopupAllowSite.setAttribute("label", allowString); + blockedPopupAllowSite.removeAttribute("block"); + } + } + catch (e) { + blockedPopupAllowSite.setAttribute("hidden", "true"); + } + + if (PrivateBrowsingUtils.isWindowPrivate(window)) + blockedPopupAllowSite.setAttribute("disabled", "true"); + else + blockedPopupAllowSite.removeAttribute("disabled"); + + var foundUsablePopupURI = false; + var blockedPopups = browser.blockedPopups; + 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.popupWindowURI) + continue; + var popupURIspec = blockedPopup.popupWindowURI.spec; + + // 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 == 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("popupWindowURI", popupURIspec); + menuitem.setAttribute("popupWindowFeatures", blockedPopup.popupWindowFeatures); + menuitem.setAttribute("popupWindowName", blockedPopup.popupWindowName); + menuitem.setAttribute("oncommand", "gPopupBlockerObserver.showBlockedPopup(event);"); + menuitem.setAttribute("popupReportIndex", i); + menuitem.popupReportBrowser = browser; + aEvent.target.appendChild(menuitem); + } + } + + // Show or hide the separator, depending on whether we added any + // showable popup addresses to the menu. + var blockedPopupsSeparator = + document.getElementById("blockedPopupsSeparator"); + if (foundUsablePopupURI) + blockedPopupsSeparator.removeAttribute("hidden"); + else + blockedPopupsSeparator.setAttribute("hidden", true); + + var blockedPopupDontShowMessage = document.getElementById("blockedPopupDontShowMessage"); + var 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")); + }, + + onPopupHiding: function (aEvent) { + if (aEvent.target.anchorNode.id == "page-report-button") + aEvent.target.anchorNode.removeAttribute("open"); + + let item = aEvent.target.lastChild; + while (item && item.getAttribute("observes") != "blockedPopupsSeparator") { + let next = item.previousSibling; + item.parentNode.removeChild(item); + item = next; + } + }, + + showBlockedPopup: function (aEvent) + { + var target = aEvent.target; + var popupReportIndex = target.getAttribute("popupReportIndex"); + let browser = target.popupReportBrowser; + browser.unblockPopup(popupReportIndex); + }, + + editPopupSettings: function () + { + var host = ""; + try { + host = gBrowser.currentURI.host; + } + catch (e) { } + + var bundlePreferences = document.getElementById("bundle_preferences"); + var params = { blockVisible : false, + sessionVisible : false, + allowVisible : true, + prefilledHost : host, + permissionType : "popup", + windowTitle : bundlePreferences.getString("popuppermissionstitle"), + introText : bundlePreferences.getString("popuppermissionstext") }; + var existingWindow = Services.wm.getMostRecentWindow("Browser:Permissions"); + if (existingWindow) { + existingWindow.initWithParams(params); + existingWindow.focus(); + } + else + window.openDialog("chrome://browser/content/preferences/permissions.xul", + "_blank", "resizable,dialog=no,centerscreen", params); + }, + + dontShowMessage: function () + { + var showMessage = gPrefService.getBoolPref("privacy.popups.showBrowserMessage"); + gPrefService.setBoolPref("privacy.popups.showBrowserMessage", !showMessage); + gBrowser.getNotificationBox().removeCurrentNotification(); + } +}; + +const gXSSObserver = { + + observe: function (aSubject, aTopic, aData) + { + + // Don't do anything if the notification is disabled. + if (!gPrefService.getBoolPref("security.xssfilter.displayWarning")) + return; + + // Parse incoming XSS array + aSubject.QueryInterface(Ci.nsIArray); + var policy = aSubject.queryElementAt(0, Ci.nsISupportsString).data; + var content = aSubject.queryElementAt(1, Ci.nsISupportsString).data; + var domain = aSubject.queryElementAt(2, Ci.nsISupportsString).data; + var url = aSubject.queryElementAt(3, Ci.nsISupportsCString).data; + var blockMode = aSubject.queryElementAt(4, Ci.nsISupportsPRBool).data; + + // If it is a block mode event, do not display the infobar + if (blockMode) + return; + + var nb = gBrowser.getNotificationBox(); + const priority = nb.PRIORITY_WARNING_MEDIUM; + + var buttons = [{ + label: 'View Unsafe Content', + accessKey: 'V', + popup: null, + callback: function () { + alert(content); + } + }]; + + if (domain !== "") + buttons.push({ + label: 'Add Domain Exception', + accessKey: 'A', + popup: null, + callback: function () { + let whitelist = gPrefService.getCharPref("security.xssfilter.whitelist"); + if (whitelist != "") { + whitelist = whitelist + "," + domain; + } else { + whitelist = domain; + } + // Write the updated whitelist. Since this is observed by the XSS filter, + // it will automatically sync to the back-end and update immediately. + gPrefService.setCharPref("security.xssfilter.whitelist", whitelist); + // After setting this, we automatically reload the page. + BrowserReloadSkipCache(); + } + }); + + nb.appendNotification("The XSS Filter has detected a potential XSS attack. Type: " + + policy, 'popup-blocked', 'chrome://browser/skin/Info.png', + priority, buttons); + } +}; + +var gBrowserInit = { + onLoad: function() { + gMultiProcessBrowser = gPrefService.getBoolPref("browser.tabs.remote"); + + var mustLoadSidebar = false; + + Cc["@mozilla.org/eventlistenerservice;1"] + .getService(Ci.nsIEventListenerService) + .addSystemEventListener(gBrowser, "click", contentAreaClick, true); + + gBrowser.addEventListener("DOMUpdatePageReport", gPopupBlockerObserver, false); + + // Note that the XBL binding is untrusted + gBrowser.addEventListener("PluginBindingAttached", gPluginHandler, true, true); + gBrowser.addEventListener("PluginCrashed", gPluginHandler, true); + gBrowser.addEventListener("PluginOutdated", gPluginHandler, true); + gBrowser.addEventListener("PluginInstantiated", gPluginHandler, true); + gBrowser.addEventListener("PluginRemoved", gPluginHandler, true); + + Services.obs.addObserver(gPluginHandler.pluginCrashed, "plugin-crashed", false); + + window.addEventListener("AppCommand", HandleAppCommandEvent, true); + + messageManager.loadFrameScript("chrome://browser/content/content.js", true); + messageManager.loadFrameScript("chrome://browser/content/content-sessionStore.js", true); + + // initialize observers and listeners + // and give C++ access to gBrowser + XULBrowserWindow.init(); + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow) + .XULBrowserWindow = window.XULBrowserWindow; + window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = + new nsBrowserAccess(); + + // set default character set if provided + // window.arguments[1]: character set (string) + if ("arguments" in window && window.arguments.length > 1 && window.arguments[1]) { + if (window.arguments[1].startsWith("charset=")) { + var arrayArgComponents = window.arguments[1].split("="); + if (arrayArgComponents) { + //we should "inherit" the charset menu setting in a new window + //TFE FIXME: this is now a wrappednative and can't be set this way. + //getMarkupDocumentViewer().defaultCharacterSet = arrayArgComponents[1]; + } + } + } + + // Manually hook up session and global history for the first browser + // so that we don't have to load global history before bringing up a + // window. + // Wire up session and global history before any possible + // progress notifications for back/forward button updating + gBrowser.webNavigation.sessionHistory = Cc["@mozilla.org/browser/shistory;1"]. + createInstance(Ci.nsISHistory); + Services.obs.addObserver(gBrowser.browsers[0], "browser:purge-session-history", false); + + // remove the disablehistory attribute so the browser cleans up, as + // though it had done this work itself + gBrowser.browsers[0].removeAttribute("disablehistory"); + + // enable global history + try { + if (!gMultiProcessBrowser) + gBrowser.docShell.useGlobalHistory = true; + } catch(ex) { + Cu.reportError("Places database may be locked: " + ex); + } + + // hook up UI through progress listener + gBrowser.addProgressListener(window.XULBrowserWindow); + gBrowser.addTabsProgressListener(window.TabsProgressListener); + + // setup our common DOMLinkAdded listener + gBrowser.addEventListener("DOMLinkAdded", DOMLinkHandler, false); + + // setup our MozApplicationManifest listener + gBrowser.addEventListener("MozApplicationManifest", + OfflineApps, false); + + // setup simple gestures support + gGestureSupport.init(true); + + // setup history swipe animation + gHistorySwipeAnimation.init(); + + if (window.opener && !window.opener.closed) { + let openerSidebarBox = window.opener.document.getElementById("sidebar-box"); + // If the opener had a sidebar, open the same sidebar in our window. + // The opener can be the hidden window too, if we're coming from the state + // where no windows are open, and the hidden window has no sidebar box. + if (openerSidebarBox && !openerSidebarBox.hidden) { + let sidebarCmd = openerSidebarBox.getAttribute("sidebarcommand"); + let sidebarCmdElem = document.getElementById(sidebarCmd); + + // dynamically generated sidebars will fail this check. + if (sidebarCmdElem) { + let sidebarBox = document.getElementById("sidebar-box"); + let sidebarTitle = document.getElementById("sidebar-title"); + + sidebarTitle.setAttribute( + "value", window.opener.document.getElementById("sidebar-title").getAttribute("value")); + sidebarBox.setAttribute("width", openerSidebarBox.boxObject.width); + + sidebarBox.setAttribute("sidebarcommand", sidebarCmd); + // Note: we're setting 'src' on sidebarBox, which is a <vbox>, not on + // the <browser id="sidebar">. This lets us delay the actual load until + // delayedStartup(). + sidebarBox.setAttribute( + "src", window.opener.document.getElementById("sidebar").getAttribute("src")); + mustLoadSidebar = true; + + sidebarBox.hidden = false; + document.getElementById("sidebar-splitter").hidden = false; + sidebarCmdElem.setAttribute("checked", "true"); + } + } + } + else { + let box = document.getElementById("sidebar-box"); + if (box.hasAttribute("sidebarcommand")) { + let commandID = box.getAttribute("sidebarcommand"); + if (commandID) { + let command = document.getElementById(commandID); + if (command) { + mustLoadSidebar = true; + box.hidden = false; + document.getElementById("sidebar-splitter").hidden = false; + command.setAttribute("checked", "true"); + } + else { + // Remove the |sidebarcommand| attribute, because the element it + // refers to no longer exists, so we should assume this sidebar + // panel has been uninstalled. (249883) + box.removeAttribute("sidebarcommand"); + } + } + } + } + + // Certain kinds of automigration rely on this notification to complete their + // tasks BEFORE the browser window is shown. + Services.obs.notifyObservers(null, "browser-window-before-show", ""); + + // Set a sane starting width/height for all resolutions on new profiles. + if (!document.documentElement.hasAttribute("width")) { + let defaultWidth; + let defaultHeight; + + // Very small: maximize the window + // Portrait : use about full width and 3/4 height, to view entire pages + // at once (without being obnoxiously tall) + // Widescreen: use about half width, to suggest side-by-side page view + // Otherwise : use 3/4 height and width + if (screen.availHeight <= 600) { + document.documentElement.setAttribute("sizemode", "maximized"); + defaultWidth = 610; + defaultHeight = 450; + } + else { + if (screen.availWidth <= screen.availHeight) { + defaultWidth = screen.availWidth * .9; + defaultHeight = screen.availHeight * .75; + } + else if (screen.availWidth >= 2048) { + defaultWidth = (screen.availWidth / 2) - 20; + defaultHeight = screen.availHeight - 10; + } + else { + defaultWidth = screen.availWidth * .75; + defaultHeight = screen.availHeight * .75; + } + +#ifdef MOZ_WIDGET_GTK2 + // On X, we're not currently able to account for the size of the window + // border. Use 28px as a guess (titlebar + bottom window border) + defaultHeight -= 28; +#endif + } + document.documentElement.setAttribute("width", defaultWidth); + document.documentElement.setAttribute("height", defaultHeight); + } + + if (!gShowPageResizers) + document.getElementById("status-bar").setAttribute("hideresizer", "true"); + + if (!window.toolbar.visible) { + // adjust browser UI for popups + if (gURLBar) { + gURLBar.setAttribute("readonly", "true"); + gURLBar.setAttribute("enablehistory", "false"); + } + goSetCommandEnabled("cmd_newNavigatorTab", false); + } + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif + + // Misc. inits. + CombinedStopReload.init(); + allTabs.readPref(); + TabsOnTop.init(); + gPrivateBrowsingUI.init(); + TabsInTitlebar.init(); + retrieveToolbarIconsizesFromTheme(); + ToolbarIconColor.init(); + +#ifdef XP_WIN + if (window.matchMedia("(-moz-os-version: windows-win8)").matches && + window.matchMedia("(-moz-windows-default-theme)").matches) { + let windows8WindowFrameColor = Cu.import("resource:///modules/Windows8WindowFrameColor.jsm", {}).Windows8WindowFrameColor; + + var windowFrameColor; + windowFrameColor = windows8WindowFrameColor.get_win8(); + + // Formula from Microsoft's UWP guideline. + let backgroundLuminance = (windowFrameColor[0] * 2 + + windowFrameColor[1] * 5 + + windowFrameColor[2]) / 8; + if (backgroundLuminance <= 128) { + document.documentElement.setAttribute("darkwindowframe", "true"); + } + } +#endif + + // Wait until chrome is painted before executing code not critical to making the window visible + this._boundDelayedStartup = this._delayedStartup.bind(this, mustLoadSidebar); + window.addEventListener("MozAfterPaint", this._boundDelayedStartup); + + this._loadHandled = true; + }, + + _cancelDelayedStartup: function () { + window.removeEventListener("MozAfterPaint", this._boundDelayedStartup); + this._boundDelayedStartup = null; + }, + + _delayedStartup: function(mustLoadSidebar) { + let tmp = {}; + + this._cancelDelayedStartup(); + + let uriToLoad = this._getUriToLoad(); + var isLoadingBlank = isBlankPageURL(uriToLoad); + + // This pageshow listener needs to be registered before we may call + // swapBrowsersAndCloseOther() to receive pageshow events fired by that. + gBrowser.addEventListener("pageshow", function(event) { + // Filter out events that are not about the document load we are interested in + if (content && event.target == content.document) + setTimeout(pageShowEventHandlers, 0, event.persisted); + }, true); + + if (uriToLoad && uriToLoad != "about:blank") { + if (uriToLoad instanceof Ci.nsISupportsArray) { + let count = uriToLoad.Count(); + let specs = []; + for (let i = 0; i < count; i++) { + let urisstring = uriToLoad.GetElementAt(i).QueryInterface(Ci.nsISupportsString); + specs.push(urisstring.data); + } + + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(specs, false, true); + } catch (e) {} + } + else if (uriToLoad instanceof XULElement) { + // swap the given tab with the default about:blank tab and then close + // the original tab in the other window. + + // Stop the about:blank load + gBrowser.stop(); + // make sure it has a docshell + gBrowser.docShell; + + gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, uriToLoad); + } + // window.arguments[2]: referrer (nsIURI) + // [3]: postData (nsIInputStream) + // [4]: allowThirdPartyFixup (bool) + else if (window.arguments.length >= 3) { + loadURI(uriToLoad, window.arguments[2], window.arguments[3] || null, + window.arguments[4] || false); + window.focus(); + } + // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3. + // Such callers expect that window.arguments[0] is handled as a single URI. + else + loadOneOrMoreURIs(uriToLoad); + } + + Services.obs.addObserver(gSessionHistoryObserver, "browser:purge-session-history", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-started", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-origin-blocked", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-failed", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false); + Services.obs.addObserver(gXSSObserver, "xss-on-violate-policy", false); + + BrowserOffline.init(); + OfflineApps.init(); + IndexedDBPromptHelper.init(); + AddonManager.addAddonListener(AddonsMgrListener); + WebrtcIndicator.init(); + + // Ensure login manager is up and running. + Services.logins; + + if (mustLoadSidebar) { + let sidebar = document.getElementById("sidebar"); + let sidebarBox = document.getElementById("sidebar-box"); + sidebar.setAttribute("src", sidebarBox.getAttribute("src")); + } + + UpdateUrlbarSearchSplitterState(); + + if (!isLoadingBlank || !focusAndSelectUrlBar()) + gBrowser.selectedBrowser.focus(); + + gNavToolbox.customizeDone = BrowserToolboxCustomizeDone; + gNavToolbox.customizeChange = BrowserToolboxCustomizeChange; + + // Set up Sanitize Item + this._initializeSanitizer(); + + // Enable/Disable auto-hide tabbar + gBrowser.tabContainer.updateVisibility(); + + gPrefService.addObserver(gHomeButton.prefDomain, gHomeButton, false); + + var homeButton = document.getElementById("home-button"); + gHomeButton.updateTooltip(homeButton); + gHomeButton.updatePersonalToolbarStyle(homeButton); + + // BiDi UI + gBidiUI = isBidiEnabled(); + if (gBidiUI) { + document.getElementById("documentDirection-separator").hidden = false; + document.getElementById("documentDirection-swap").hidden = false; + document.getElementById("textfieldDirection-separator").hidden = false; + document.getElementById("textfieldDirection-swap").hidden = false; + } + + // Setup click-and-hold gestures access to the session history + // menus if global click-and-hold isn't turned on + if (!getBoolPref("ui.click_hold_context_menus", false)) + SetClickAndHoldHandlers(); + + // Initialize the full zoom setting. + // We do this before the session restore service gets initialized so we can + // apply full zoom settings to tabs restored by the session restore service. + FullZoom.init(); + + // Bug 666804 - NetworkPrioritizer support for e10s + if (!gMultiProcessBrowser) { + let NP = {}; + Cu.import("resource:///modules/NetworkPrioritizer.jsm", NP); + NP.trackBrowserWindow(window); + } + + // initialize the session-restore service (in case it's not already running) + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + let ssPromise = ss.init(window); + + PlacesToolbarHelper.init(); + + ctrlTab.readPref(); + gPrefService.addObserver(ctrlTab.prefName, ctrlTab, false); + gPrefService.addObserver(allTabs.prefName, allTabs, false); + + // Initialize the download manager some time after the app starts so that + // auto-resume downloads begin (such as after crashing or quitting with + // active downloads) and speeds up the first-load of the download manager UI. + // If the user manually opens the download manager before the timeout, the + // downloads will start right away, and getting the service again won't hurt. + setTimeout(function() { + try { + Cu.import("resource:///modules/DownloadsCommon.jsm", {}) + .DownloadsCommon.initializeAllDataLinks(); + Cu.import("resource:///modules/DownloadsTaskbar.jsm", {}) + .DownloadsTaskbar.registerIndicator(window); + } catch(ex) { + Cu.reportError(ex); + } + }, 10000); + + // Load the Login Manager data from disk off the main thread, some time + // after startup. If the data is required before the timeout, for example + // because a restored page contains a password field, it will be loaded on + // the main thread, and this initialization request will be ignored. + setTimeout(function() { + try { + Services.logins; + } catch (ex) { + Cu.reportError(ex); + } + }, 3000); + + // The object handling the downloads indicator is also initialized here in the + // delayed startup function, but the actual indicator element is not loaded + // unless there are downloads to be displayed. + DownloadsButton.initializeIndicator(); + +#ifndef XP_MACOSX + updateEditUIVisibility(); + let placesContext = document.getElementById("placesContext"); + placesContext.addEventListener("popupshowing", updateEditUIVisibility, false); + placesContext.addEventListener("popuphiding", updateEditUIVisibility, false); +#endif + + gBrowser.mPanelContainer.addEventListener("InstallBrowserTheme", LightWeightThemeWebInstaller, false, true); + gBrowser.mPanelContainer.addEventListener("PreviewBrowserTheme", LightWeightThemeWebInstaller, false, true); + gBrowser.mPanelContainer.addEventListener("ResetBrowserThemePreview", LightWeightThemeWebInstaller, false, true); + + // Bug 666808 - AeroPeek support for e10s + if (!gMultiProcessBrowser) { + if (Win7Features) + Win7Features.onOpenWindow(); + } + + // called when we go into full screen, even if initiated by a web page script + window.addEventListener("fullscreen", onFullScreen, true); + + // Called when we enter DOM full-screen mode. Note we can already be in browser + // full-screen mode when we enter DOM full-screen mode. + window.addEventListener("MozEnteredDomFullscreen", onMozEnteredDomFullscreen, true); + + if (window.fullScreen) + onFullScreen(); + if (document.mozFullScreen) + onMozEnteredDomFullscreen(); + +#ifdef MOZ_SERVICES_SYNC + // initialize the sync UI + gSyncUI.init(); +#endif + + gBrowserThumbnails.init(); + + setUrlAndSearchBarWidthForConditionalForwardButton(); + window.addEventListener("resize", function resizeHandler(event) { + if (event.target == window) + setUrlAndSearchBarWidthForConditionalForwardButton(); + }); + +#ifdef MOZ_DEVTOOLS + // Enable Chrome Debugger? + let chromeEnabled = gPrefService.getBoolPref("devtools.chrome.enabled"); + let remoteEnabled = chromeEnabled && + gPrefService.getBoolPref("devtools.debugger.chrome-enabled") && + gPrefService.getBoolPref("devtools.debugger.remote-enabled"); + if (remoteEnabled) { + let cmd = document.getElementById("Tools:ChromeDebugger"); + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + } + + // Enable Scratchpad in the UI, if the preference allows this. + let scratchpadEnabled = gPrefService.getBoolPref(Scratchpad.prefEnabledName); + if (scratchpadEnabled) { + let cmd = document.getElementById("Tools:Scratchpad"); + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + } +#endif + + // Enable Error Console? + let consoleEnabled = gPrefService.getBoolPref("devtools.errorconsole.enabled"); + if (consoleEnabled) { + let cmd = document.getElementById("Tools:ErrorConsole"); + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + } + +#ifdef MENUBAR_CAN_AUTOHIDE + // If the user (or the locale) hasn't enabled the top-level "Character + // Encoding" menu via the "browser.menu.showCharacterEncoding" preference, + // hide it. + if ("true" != gPrefService.getComplexValue("browser.menu.showCharacterEncoding", + Ci.nsIPrefLocalizedString).data) + document.getElementById("appmenu_charsetMenu").hidden = true; +#endif + +#ifdef MOZ_DEVTOOLS + // Enable Responsive UI? + let responsiveUIEnabled = gPrefService.getBoolPref("devtools.responsiveUI.enabled"); + if (responsiveUIEnabled) { + let cmd = document.getElementById("Tools:ResponsiveUI"); + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + } + + // Add Devtools menuitems and listeners + gDevToolsBrowser.registerBrowserWindow(window); +#endif + + let appMenuButton = document.getElementById("appmenu-button"); + let appMenuPopup = document.getElementById("appmenu-popup"); + if (appMenuButton && appMenuPopup) { + let appMenuOpening = null; + appMenuButton.addEventListener("mousedown", function(event) { + if (event.button == 0) + appMenuOpening = new Date(); + }, false); + appMenuPopup.addEventListener("popupshown", function(event) { + if (event.target != appMenuPopup || !appMenuOpening) + return; + let duration = new Date() - appMenuOpening; + appMenuOpening = null; + }, false); + } + + window.addEventListener("mousemove", MousePosTracker, false); + window.addEventListener("dragover", MousePosTracker, false); + + // End startup crash tracking after a delay to catch crashes while restoring + // tabs and to postpone saving the pref to disk. + try { + const startupCrashEndDelay = 30 * 1000; + setTimeout(Services.startup.trackStartupCrashEnd, startupCrashEndDelay); + } catch (ex) { + Cu.reportError("Could not end startup crash tracking: " + ex); + } + + ssPromise.then(() =>{ + // Bail out if the window has been closed in the meantime. + if (window.closed) { + return; + } + if ("TabView" in window) { + TabView.init(); + } + // XXX: do we still need this?... + setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0); + }); + + Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); + }, + + // Returns the URI(s) to load at startup. + _getUriToLoad: function () { + // window.arguments[0]: URI to load (string), or an nsISupportsArray of + // nsISupportsStrings to load, or a xul:tab of + // a tabbrowser, which will be replaced by this + // window (for this case, all other arguments are + // ignored). + if (!window.arguments || !window.arguments[0]) + return null; + + let uri = window.arguments[0]; + let sessionStartup = Cc["@mozilla.org/browser/sessionstartup;1"] + .getService(Ci.nsISessionStartup); + let defaultArgs = Cc["@mozilla.org/browser/clh;1"] + .getService(Ci.nsIBrowserHandler) + .defaultArgs; + + // If the given URI matches defaultArgs (the default homepage) we want + // to block its load if we're going to restore a session anyway. + if (uri == defaultArgs && sessionStartup.willOverrideHomepage) + return null; + + return uri; + }, + + onUnload: function() { + // In certain scenarios it's possible for unload to be fired before onload, + // (e.g. if the window is being closed after browser.js loads but before the + // load completes). In that case, there's nothing to do here. + if (!this._loadHandled) + return; + +#ifdef MOZ_DEVTOOLS + gDevToolsBrowser.forgetBrowserWindow(window); + + let desc = Object.getOwnPropertyDescriptor(window, "DeveloperToolbar"); + if (desc && !desc.get) { + DeveloperToolbar.destroy(); + } +#endif + + // First clean up services initialized in gBrowserInit.onLoad (or those whose + // uninit methods don't depend on the services having been initialized). + + allTabs.uninit(); + + CombinedStopReload.uninit(); + + gGestureSupport.init(false); + + gHistorySwipeAnimation.uninit(); + + FullScreen.cleanup(); + + Services.obs.removeObserver(gPluginHandler.pluginCrashed, "plugin-crashed"); + + try { + gBrowser.removeProgressListener(window.XULBrowserWindow); + gBrowser.removeTabsProgressListener(window.TabsProgressListener); + } catch (ex) { + } + + BookmarkingUI.uninit(); + + TabsOnTop.uninit(); + + TabsInTitlebar.uninit(); + + ToolbarIconColor.uninit(); + + var enumerator = Services.wm.getEnumerator(null); + enumerator.getNext(); + if (!enumerator.hasMoreElements()) { + document.persist("sidebar-box", "sidebarcommand"); + document.persist("sidebar-box", "width"); + document.persist("sidebar-box", "src"); + document.persist("sidebar-title", "value"); + } + + // Now either cancel delayedStartup, or clean up the services initialized from + // it. + if (this._boundDelayedStartup) { + this._cancelDelayedStartup(); + } else { + if (Win7Features) + Win7Features.onCloseWindow(); + + gPrefService.removeObserver(ctrlTab.prefName, ctrlTab); + gPrefService.removeObserver(allTabs.prefName, allTabs); + ctrlTab.uninit(); + if ("TabView" in window) { + TabView.uninit(); + } + gBrowserThumbnails.uninit(); + FullZoom.destroy(); + + Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-started"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-origin-blocked"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete"); + Services.obs.removeObserver(gXSSObserver, "xss-on-violate-policy"); + + try { + gPrefService.removeObserver(gHomeButton.prefDomain, gHomeButton); + } catch (ex) { + Cu.reportError(ex); + } + + BrowserOffline.uninit(); + OfflineApps.uninit(); + IndexedDBPromptHelper.uninit(); + AddonManager.removeAddonListener(AddonsMgrListener); + } + + // Final window teardown, do this last. + window.XULBrowserWindow.destroy(); + window.XULBrowserWindow = null; + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIXULWindow) + .XULBrowserWindow = null; + window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = null; + }, + +#ifdef XP_MACOSX + // nonBrowserWindowStartup(), nonBrowserWindowDelayedStartup(), and + // nonBrowserWindowShutdown() are used for non-browser windows in + // macBrowserOverlay + nonBrowserWindowStartup: function() { + // Disable inappropriate commands / submenus + var disabledItems = ['Browser:SavePage', + 'Browser:SendLink', 'cmd_pageSetup', 'cmd_print', 'cmd_find', 'cmd_findAgain', + 'viewToolbarsMenu', 'viewSidebarMenuMenu', 'Browser:Reload', + 'viewFullZoomMenu', 'pageStyleMenu', 'charsetMenu', 'View:PageSource', 'View:FullScreen', + 'viewHistorySidebar', 'Browser:AddBookmarkAs', 'Browser:BookmarkAllTabs', + 'View:PageInfo', 'Browser:ToggleAddonBar']; + var element; + + for (let disabledItem of disabledItems) { + element = document.getElementById(disabledItem); + if (element) + element.setAttribute("disabled", "true"); + } + + // If no windows are active (i.e. we're the hidden window), disable the close, minimize + // and zoom menu commands as well + if (window.location.href == "chrome://browser/content/hiddenWindow.xul") { + var hiddenWindowDisabledItems = ['cmd_close', 'minimizeWindow', 'zoomWindow']; + for (let hiddenWindowDisabledItem of hiddenWindowDisabledItems) { + element = document.getElementById(hiddenWindowDisabledItem); + if (element) + element.setAttribute("disabled", "true"); + } + + // also hide the window-list separator + element = document.getElementById("sep-window-list"); + element.setAttribute("hidden", "true"); + + // Setup the dock menu. + let dockMenuElement = document.getElementById("menu_mac_dockmenu"); + if (dockMenuElement != null) { + let nativeMenu = Cc["@mozilla.org/widget/standalonenativemenu;1"] + .createInstance(Ci.nsIStandaloneNativeMenu); + + try { + nativeMenu.init(dockMenuElement); + + let dockSupport = Cc["@mozilla.org/widget/macdocksupport;1"] + .getService(Ci.nsIMacDockSupport); + dockSupport.dockMenu = nativeMenu; + } + catch (e) { + } + } + } + + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + document.getElementById("macDockMenuNewWindow").hidden = true; + } + + this._delayedStartupTimeoutId = setTimeout(this.nonBrowserWindowDelayedStartup.bind(this), 0); + }, + + nonBrowserWindowDelayedStartup: function() { + this._delayedStartupTimeoutId = null; + + // initialise the offline listener + BrowserOffline.init(); + + // Set up Sanitize Item + this._initializeSanitizer(); + + // initialize the private browsing UI + gPrivateBrowsingUI.init(); + +#ifdef MOZ_SERVICES_SYNC + // initialize the sync UI + gSyncUI.init(); +#endif + }, + + nonBrowserWindowShutdown: function() { + // If nonBrowserWindowDelayedStartup hasn't run yet, we have no work to do - + // just cancel the pending timeout and return; + if (this._delayedStartupTimeoutId) { + clearTimeout(this._delayedStartupTimeoutId); + return; + } + + BrowserOffline.uninit(); + }, +#endif + + _initializeSanitizer: function() { + const kDidSanitizeDomain = "privacy.sanitize.didShutdownSanitize"; + if (gPrefService.prefHasUserValue(kDidSanitizeDomain)) { + gPrefService.clearUserPref(kDidSanitizeDomain); + // We need to persist this preference change, since we want to + // check it at next app start even if the browser exits abruptly + gPrefService.savePrefFile(null); + } + + /** + * Migrate Firefox 3.0 privacy.item prefs under one of these conditions: + * + * a) User has customized any privacy.item prefs + * b) privacy.sanitize.sanitizeOnShutdown is set + */ + if (!gPrefService.getBoolPref("privacy.sanitize.migrateFx3Prefs")) { + let itemBranch = gPrefService.getBranch("privacy.item."); + let itemArray = itemBranch.getChildList(""); + + // See if any privacy.item prefs are set + let doMigrate = itemArray.some(function (name) itemBranch.prefHasUserValue(name)); + // Or if sanitizeOnShutdown is set + if (!doMigrate) + doMigrate = gPrefService.getBoolPref("privacy.sanitize.sanitizeOnShutdown"); + + if (doMigrate) { + let cpdBranch = gPrefService.getBranch("privacy.cpd."); + let clearOnShutdownBranch = gPrefService.getBranch("privacy.clearOnShutdown."); + for (let name of itemArray) { + try { + // don't migrate password or offlineApps clearing in the CRH dialog since + // there's no UI for those anymore. They default to false. bug 497656 + if (name != "passwords" && name != "offlineApps") + cpdBranch.setBoolPref(name, itemBranch.getBoolPref(name)); + clearOnShutdownBranch.setBoolPref(name, itemBranch.getBoolPref(name)); + } + catch(e) { + Cu.reportError("Exception thrown during privacy pref migration: " + e); + } + } + } + + gPrefService.setBoolPref("privacy.sanitize.migrateFx3Prefs", true); + } + }, +} + + +/* Legacy global init functions */ +var BrowserStartup = gBrowserInit.onLoad.bind(gBrowserInit); +var BrowserShutdown = gBrowserInit.onUnload.bind(gBrowserInit); +#ifdef XP_MACOSX +var nonBrowserWindowStartup = gBrowserInit.nonBrowserWindowStartup.bind(gBrowserInit); +var nonBrowserWindowDelayedStartup = gBrowserInit.nonBrowserWindowDelayedStartup.bind(gBrowserInit); +var nonBrowserWindowShutdown = gBrowserInit.nonBrowserWindowShutdown.bind(gBrowserInit); +#endif + +function HandleAppCommandEvent(evt) { + switch (evt.command) { + case "Back": + BrowserBack(); + break; + case "Forward": + BrowserForward(); + break; + case "Reload": + BrowserReloadSkipCache(); + break; + case "Stop": + if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") + BrowserStop(); + break; + case "Search": + BrowserSearch.webSearch(); + break; + case "Bookmarks": + toggleSidebar('viewBookmarksSidebar'); + break; + case "Home": + BrowserHome(); + break; + case "New": + BrowserOpenTab(); + break; + case "Close": + BrowserCloseTabOrWindow(); + break; + case "Find": + gFindBar.onFindCommand(); + break; + case "Help": + openHelpLink('firefox-help'); + break; + case "Open": + BrowserOpenFileWindow(); + break; + case "Print": + PrintUtils.print(); + break; + case "Save": + saveDocument(window.content.document); + break; + case "SendMail": + MailIntegration.sendLinkForWindow(window.content); + break; + default: + return; + } + evt.stopPropagation(); + evt.preventDefault(); +} + +function gotoHistoryIndex(aEvent) { + let index = aEvent.target.getAttribute("index"); + if (!index) + return false; + + let where = whereToOpenLink(aEvent); + + if (where == "current") { + // Normal click. Go there in the current tab and update session history. + + try { + gBrowser.gotoIndex(index); + } + catch(ex) { + return false; + } + return true; + } + // Modified click. Go there in a new tab/window. + + duplicateTabIn(gBrowser.selectedTab, where, index - gBrowser.sessionHistory.index); + return true; +} + +function BrowserForward(aEvent) { + let where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goForward(); + } + catch(ex) { + } + } + else { + duplicateTabIn(gBrowser.selectedTab, where, 1); + } +} + +function BrowserBack(aEvent) { + let where = whereToOpenLink(aEvent, false, true); + + if (where == "current") { + try { + gBrowser.goBack(); + } + catch(ex) { + } + } + else { + duplicateTabIn(gBrowser.selectedTab, where, -1); + } +} + +function BrowserHandleBackspace() +{ + switch (gPrefService.getIntPref("browser.backspace_action")) { + case 0: + BrowserBack(); + break; + case 1: + goDoCommand("cmd_scrollPageUp"); + break; + } +} + +function BrowserHandleShiftBackspace() +{ + switch (gPrefService.getIntPref("browser.backspace_action")) { + case 0: + BrowserForward(); + break; + case 1: + goDoCommand("cmd_scrollPageDown"); + break; + } +} + +function BrowserStop() { + const stopFlags = nsIWebNavigation.STOP_ALL; + gBrowser.webNavigation.stop(stopFlags); +} + +function BrowserReloadOrDuplicate(aEvent) { + var backgroundTabModifier = aEvent.button == 1 || +#ifdef XP_MACOSX + aEvent.metaKey; +#else + aEvent.ctrlKey; +#endif + if (aEvent.shiftKey && !backgroundTabModifier) { + BrowserReloadSkipCache(); + return; + } + + let where = whereToOpenLink(aEvent, false, true); + if (where == "current") + BrowserReload(); + else + duplicateTabIn(gBrowser.selectedTab, where); +} + +function BrowserReload() { + const reloadFlags = nsIWebNavigation.LOAD_FLAGS_NONE; + BrowserReloadWithFlags(reloadFlags); +} + +function BrowserReloadSkipCache() { + // Bypass proxy and cache. + const reloadFlags = nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + BrowserReloadWithFlags(reloadFlags); +} + +var BrowserHome = BrowserGoHome; +function BrowserGoHome(aEvent) { + if (aEvent && "button" in aEvent && + aEvent.button == 2) // right-click: do nothing + return; + + var homePage = gHomeButton.getHomePage(); + var where = whereToOpenLink(aEvent, false, true); + var urls; + + // Home page should open in a new tab when current tab is an app tab + if (where == "current" && + gBrowser && + gBrowser.selectedTab.pinned) + where = "tab"; + + // openUILinkIn in utilityOverlay.js doesn't handle loading multiple pages + switch (where) { + case "current": + loadOneOrMoreURIs(homePage); + break; + case "tabshifted": + case "tab": + urls = homePage.split("|"); + var loadInBackground = getBoolPref("browser.tabs.loadBookmarksInBackground", false); + gBrowser.loadTabs(urls, loadInBackground); + break; + case "window": + OpenBrowserWindow(); + break; + } +} + +function loadOneOrMoreURIs(aURIString) +{ +#ifdef XP_MACOSX + // we're not a browser window, pass the URI string to a new browser window + if (window.location.href != getBrowserURL()) + { + window.openDialog(getBrowserURL(), "_blank", "all,dialog=no", aURIString); + return; + } +#endif + // This function throws for certain malformed URIs, so use exception handling + // so that we don't disrupt startup + try { + gBrowser.loadTabs(aURIString.split("|"), false, true); + } + catch (e) { + } +} + +function focusAndSelectUrlBar() { + if (gURLBar) { + if (window.fullScreen) + FullScreen.mouseoverToggle(true); + + gURLBar.select(); + if (document.activeElement == gURLBar.inputField) + return true; + } + return false; +} + +function openLocation() { + if (focusAndSelectUrlBar()) + return; + +#ifdef XP_MACOSX + if (window.location.href != getBrowserURL()) { + var win = getTopWin(); + if (win) { + // If there's an open browser window, it should handle this command + win.focus() + win.openLocation(); + } + else { + // If there are no open browser windows, open a new one + win = window.openDialog("chrome://browser/content/", "_blank", + "chrome,all,dialog=no", BROWSER_NEW_TAB_URL); + win.addEventListener("load", openLocationCallback, false); + } + return; + } +#endif + openDialog("chrome://browser/content/openLocation.xul", "_blank", + "chrome,modal,titlebar", window); +} + +function openLocationCallback() +{ + // make sure the DOM is ready + setTimeout(function() { this.openLocation(); }, 0); +} + +function BrowserOpenTab() +{ + openUILinkIn(BROWSER_NEW_TAB_URL, "tab"); +} + +/* Called from the openLocation dialog. This allows that dialog to instruct + its opener to open a new window and then step completely out of the way. + Anything less byzantine is causing horrible crashes, rather believably, + though oddly only on Linux. */ +function delayedOpenWindow(chrome, flags, href, postData) +{ + // The other way to use setTimeout, + // setTimeout(openDialog, 10, chrome, "_blank", flags, url), + // doesn't work here. The extra "magic" extra argument setTimeout adds to + // the callback function would confuse gBrowserInit.onLoad() by making + // window.arguments[1] be an integer instead of null. + setTimeout(function() { openDialog(chrome, "_blank", flags, href, null, null, postData); }, 10); +} + +/* Required because the tab needs time to set up its content viewers and get the load of + the URI kicked off before becoming the active content area. */ +function delayedOpenTab(aUrl, aReferrer, aCharset, aPostData, aAllowThirdPartyFixup) +{ + gBrowser.loadOneTab(aUrl, { + referrerURI: aReferrer, + charset: aCharset, + postData: aPostData, + inBackground: false, + allowThirdPartyFixup: aAllowThirdPartyFixup}); +} + +var gLastOpenDirectory = { + _lastDir: null, + get path() { + if (!this._lastDir || !this._lastDir.exists()) { + try { + this._lastDir = gPrefService.getComplexValue("browser.open.lastDir", + Ci.nsILocalFile); + if (!this._lastDir.exists()) + this._lastDir = null; + } + catch(e) {} + } + return this._lastDir; + }, + set path(val) { + try { + if (!val || !val.isDirectory()) + return; + } catch(e) { + return; + } + this._lastDir = val.clone(); + + // Don't save the last open directory pref inside the Private Browsing mode + if (!PrivateBrowsingUtils.isWindowPrivate(window)) + gPrefService.setComplexValue("browser.open.lastDir", Ci.nsILocalFile, + this._lastDir); + }, + reset: function() { + this._lastDir = null; + } +}; + +function BrowserOpenFileWindow() +{ + // Get filepicker component. + try { + const nsIFilePicker = Ci.nsIFilePicker; + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK) { + try { + if (fp.file) { + gLastOpenDirectory.path = + fp.file.parent.QueryInterface(Ci.nsILocalFile); + } + } catch (ex) { + } + openUILinkIn(fp.fileURL.spec, "current"); + } + }; + + fp.init(window, gNavigatorBundle.getString("openFile"), + nsIFilePicker.modeOpen); + fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText | + nsIFilePicker.filterImages | nsIFilePicker.filterXML | + nsIFilePicker.filterHTML); + fp.displayDirectory = gLastOpenDirectory.path; + fp.open(fpCallback); + } catch (ex) { + } +} + +function BrowserCloseTabOrWindow() { +#ifdef XP_MACOSX + // If we're not a browser window, just close the window + if (window.location.href != getBrowserURL()) { + closeWindow(true); + return; + } +#endif + + // If the current tab is the last one, this will close the window. + gBrowser.removeCurrentTab({animate: true}); +} + +function BrowserTryToCloseWindow() +{ + if (WindowIsClosing()) + window.close(); // WindowIsClosing does all the necessary checks +} + +function loadURI(uri, referrer, postData, allowThirdPartyFixup) { + if (postData === undefined) + postData = null; + + var flags = nsIWebNavigation.LOAD_FLAGS_NONE; + if (allowThirdPartyFixup) { + flags |= nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + flags |= nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + + try { + gBrowser.loadURIWithFlags(uri, flags, referrer, null, postData); + } catch (e) {} +} + +function getShortcutOrURI(aURL, aPostDataRef, aMayInheritPrincipal) { + // Initialize outparam to false + if (aMayInheritPrincipal) + aMayInheritPrincipal.value = false; + + var shortcutURL = null; + var keyword = aURL; + var param = ""; + + var offset = aURL.indexOf(" "); + if (offset > 0) { + keyword = aURL.substr(0, offset); + param = aURL.substr(offset + 1); + } + + if (!aPostDataRef) + aPostDataRef = {}; + + var engine = Services.search.getEngineByAlias(keyword); + if (engine) { + var submission = engine.getSubmission(param); + aPostDataRef.value = submission.postData; + return submission.uri.spec; + } + + [shortcutURL, aPostDataRef.value] = + PlacesUtils.getURLAndPostDataForKeyword(keyword); + + if (!shortcutURL) + return aURL; + + var postData = ""; + if (aPostDataRef.value) + postData = unescape(aPostDataRef.value); + + if (/%s/i.test(shortcutURL) || /%s/i.test(postData)) { + var charset = ""; + const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/; + var matches = shortcutURL.match(re); + if (matches) + [, shortcutURL, charset] = matches; + else { + // Try to get the saved character-set. + try { + // makeURI throws if URI is invalid. + // Will return an empty string if character-set is not found. + charset = PlacesUtils.history.getCharsetForURI(makeURI(shortcutURL)); + } catch (e) {} + } + + // encodeURIComponent produces UTF-8, and cannot be used for other charsets. + // escape() works in those cases, but it doesn't uri-encode +, @, and /. + // Therefore we need to manually replace these ASCII characters by their + // encodeURIComponent result, to match the behavior of nsEscape() with + // url_XPAlphas + var encodedParam = ""; + if (charset && charset != "UTF-8") + encodedParam = escape(convertFromUnicode(charset, param)). + replace(/[+@\/]+/g, encodeURIComponent); + else // Default charset is UTF-8 + encodedParam = encodeURIComponent(param); + + shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param); + + if (/%s/i.test(postData)) // POST keyword + aPostDataRef.value = getPostDataStream(postData, param, encodedParam, + "application/x-www-form-urlencoded"); + } + else if (param) { + // This keyword doesn't take a parameter, but one was provided. Just return + // the original URL. + aPostDataRef.value = null; + + return aURL; + } + + // This URL came from a bookmark, so it's safe to let it inherit the current + // document's principal. + if (aMayInheritPrincipal) + aMayInheritPrincipal.value = true; + + return shortcutURL; +} + +function getPostDataStream(aStringData, aKeyword, aEncKeyword, aType) { + var dataStream = Cc["@mozilla.org/io/string-input-stream;1"]. + createInstance(Ci.nsIStringInputStream); + aStringData = aStringData.replace(/%s/g, aEncKeyword).replace(/%S/g, aKeyword); + dataStream.data = aStringData; + + var mimeStream = Cc["@mozilla.org/network/mime-input-stream;1"]. + createInstance(Ci.nsIMIMEInputStream); + mimeStream.addHeader("Content-Type", aType); + mimeStream.addContentLength = true; + mimeStream.setData(dataStream); + return mimeStream.QueryInterface(Ci.nsIInputStream); +} + +function getLoadContext() { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext); +} + +function readFromClipboard() +{ + var url; + + try { + // Create transferable that will transfer the text. + var trans = Components.classes["@mozilla.org/widget/transferable;1"] + .createInstance(Components.interfaces.nsITransferable); + trans.init(getLoadContext()); + + trans.addDataFlavor("text/unicode"); + + // If available, use selection clipboard, otherwise global one + if (Services.clipboard.supportsSelectionClipboard()) + Services.clipboard.getData(trans, Services.clipboard.kSelectionClipboard); + else + Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); + + var data = {}; + var dataLen = {}; + trans.getTransferData("text/unicode", data, dataLen); + + if (data) { + data = data.value.QueryInterface(Components.interfaces.nsISupportsString); + url = data.data.substring(0, dataLen.value / 2); + } + } catch (ex) { + } + + return url; +} + +function BrowserViewSourceOfDocument(aDocument) +{ + var pageCookie; + var webNav; + + // Get the document charset + var docCharset = "charset=" + aDocument.characterSet; + + // Get the nsIWebNavigation associated with the document + try { + var win; + var ifRequestor; + + // Get the DOMWindow for the requested document. If the DOMWindow + // cannot be found, then just use the content window... + // + // XXX: This is a bit of a hack... + win = aDocument.defaultView; + if (win == window) { + win = content; + } + ifRequestor = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor); + + webNav = ifRequestor.getInterface(nsIWebNavigation); + } catch(err) { + // If nsIWebNavigation cannot be found, just get the one for the whole + // window... + webNav = gBrowser.webNavigation; + } + // + // Get the 'PageDescriptor' for the current document. This allows the + // view-source to access the cached copy of the content rather than + // refetching it from the network... + // + try{ + var PageLoader = webNav.QueryInterface(Components.interfaces.nsIWebPageDescriptor); + + pageCookie = PageLoader.currentDescriptor; + } catch(err) { + // If no page descriptor is available, just use the view-source URL... + } + + top.gViewSourceUtils.viewSource(webNav.currentURI.spec, pageCookie, aDocument); +} + +// doc - document to use for source, or null for this window's document +// initialTab - name of the initial tab to display, or null for the first tab +// imageElement - image to load in the Media Tab of the Page Info window; can be null/omitted +function BrowserPageInfo(doc, initialTab, imageElement) { + var args = {doc: doc, initialTab: initialTab, imageElement: imageElement}; + var windows = Services.wm.getEnumerator("Browser:page-info"); + + var documentURL = doc ? doc.location : window.content.document.location; + + // Check for windows matching the url + while (windows.hasMoreElements()) { + var currentWindow = windows.getNext(); + if (currentWindow.document.documentElement.getAttribute("relatedUrl") == documentURL) { + currentWindow.focus(); + currentWindow.resetPageInfo(args); + return currentWindow; + } + } + + // We didn't find a matching window, so open a new one. + return openDialog("chrome://browser/content/pageinfo/pageInfo.xul", "", + "chrome,toolbar,dialog=no,resizable", args); +} + +function URLBarSetURI(aURI) { + var value = gBrowser.userTypedValue; + var valid = false; + + if (value == null) { + let uri = aURI || gBrowser.currentURI; + // Strip off "wyciwyg://" and passwords for the location bar + try { + uri = Services.uriFixup.createExposableURI(uri); + } catch (e) {} + + // Replace initial page URIs with an empty string + // only if there's no opener (bug 370555). + // Bug 863515 - Make content.opener checks work in electrolysis. + if (gInitialPages.indexOf(uri.spec) != -1) + value = !gMultiProcessBrowser && content.opener ? uri.spec : ""; + else + value = losslessDecodeURI(uri); + + valid = !isBlankPageURL(uri.spec); + } + + let isDifferentValidValue = valid && value != gURLBar.value; + gURLBar.value = value; + gURLBar.valueIsTyped = !valid; + if (isDifferentValidValue) { + gURLBar.selectionStart = gURLBar.selectionEnd = 0; + } + + SetPageProxyState(valid ? "valid" : "invalid"); +} + +function losslessDecodeURI(aURI) { + let scheme = aURI.scheme; + let decodeASCIIOnly = !(/(https|http|file|ftp)/i.test(scheme)); + + var value = aURI.spec; + + // Try to decode as UTF-8 if there's no encoding sequence that we would break. + if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) { + if (decodeASCIIOnly) { + // This only decodes ASCII characters (hex) 20-7e, except 25 (%). + // This avoids both cases stipulated below (%-related issues, and \r, \n + // and \t, which would be %0d, %0a and %09, respectively) as well as any + // non-US-ascii characters. + value = value.replace(/%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, decodeURI); + } else { + try { + value = decodeURI(value) + // 1. decodeURI decodes %25 to %, which creates unintended + // encoding sequences. Re-encode it, unless it's part of + // a sequence that survived decodeURI, i.e. one for: + // ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#' + // (RFC 3987 section 3.2) + // 2. Re-encode whitespace so that it doesn't get eaten away + // by the location bar (bug 410726). + .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|[\r\n\t]/ig, + encodeURIComponent); + } catch (e) {} + } + } + + // Encode invisible characters (C0/C1 control characters, U+007F [DEL], + // U+00A0 [no-break space], line and paragraph separator, + // object replacement character) (bug 452979, bug 909264) + value = value.replace(/[\u0000-\u001f\u007f-\u00a0\u2028\u2029\ufffc]/g, + encodeURIComponent); + + // Encode default ignorable characters (bug 546013) + // except ZWNJ (U+200C) and ZWJ (U+200D) (bug 582186). + // This includes all bidirectional formatting characters. + // (RFC 3987 sections 3.2 and 4.1 paragraph 6) + value = value.replace(/[\u00ad\u034f\u115f-\u1160\u17b4-\u17b5\u180b-\u180d\u200b\u200e-\u200f\u202a-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufff8]|\ud834[\udd73-\udd7a]|[\udb40-\udb43][\udc00-\udfff]/g, + encodeURIComponent); + return value; +} + +function UpdateUrlbarSearchSplitterState() +{ + var splitter = document.getElementById("urlbar-search-splitter"); + var urlbar = document.getElementById("urlbar-container"); + var searchbar = document.getElementById("search-container"); + var stop = document.getElementById("stop-button"); + + var ibefore = null; + if (urlbar && searchbar) { + if (urlbar.nextSibling == searchbar || + urlbar.getAttribute("combined") && + stop && stop.nextSibling == searchbar) + ibefore = searchbar; + else if (searchbar.nextSibling == urlbar) + ibefore = urlbar; + } + + if (ibefore) { + if (!splitter) { + splitter = document.createElement("splitter"); + splitter.id = "urlbar-search-splitter"; + splitter.setAttribute("resizebefore", "flex"); + splitter.setAttribute("resizeafter", "flex"); + splitter.setAttribute("skipintoolbarset", "true"); + splitter.className = "chromeclass-toolbar-additional"; + } + urlbar.parentNode.insertBefore(splitter, ibefore); + } else if (splitter) + splitter.parentNode.removeChild(splitter); +} + +function setUrlAndSearchBarWidthForConditionalForwardButton() { + // Workaround for bug 694084: Showing/hiding the conditional forward button resizes + // the search bar when the url/search bar splitter hasn't been used. + var urlbarContainer = document.getElementById("urlbar-container"); + var searchbarContainer = document.getElementById("search-container"); + if (!urlbarContainer || + !searchbarContainer || + urlbarContainer.hasAttribute("width") || + searchbarContainer.hasAttribute("width") || + urlbarContainer.parentNode != searchbarContainer.parentNode) + return; + urlbarContainer.style.width = searchbarContainer.style.width = ""; + var urlbarWidth = urlbarContainer.clientWidth; + var searchbarWidth = searchbarContainer.clientWidth; + urlbarContainer.style.width = urlbarWidth + "px"; + searchbarContainer.style.width = searchbarWidth + "px"; +} + +function UpdatePageProxyState() +{ + if (gURLBar && gURLBar.value != gLastValidURLStr) + SetPageProxyState("invalid"); +} + +function SetPageProxyState(aState) +{ + BookmarkingUI.onPageProxyStateChanged(aState); + + if (!gURLBar) + return; + + if (!gProxyFavIcon) + gProxyFavIcon = document.getElementById("page-proxy-favicon"); + + gURLBar.setAttribute("pageproxystate", aState); + gProxyFavIcon.setAttribute("pageproxystate", aState); + + // the page proxy state is set to valid via OnLocationChange, which + // gets called when we switch tabs. + if (aState == "valid") { + gLastValidURLStr = gURLBar.value; + gURLBar.addEventListener("input", UpdatePageProxyState, false); + PageProxySetIcon(gBrowser.getIcon()); + } else if (aState == "invalid") { + gURLBar.removeEventListener("input", UpdatePageProxyState, false); + PageProxyClearIcon(); + } +} + +function PageProxySetIcon (aURL) +{ + if (!gProxyFavIcon) + return; + + if (gBrowser.selectedBrowser.contentDocument instanceof ImageDocument) { + // PageProxyClearIcon(); + gProxyFavIcon.setAttribute("src", "chrome://browser/skin/imagedocument.png"); + return; + } + + if (!aURL) + PageProxyClearIcon(); + else if (gProxyFavIcon.getAttribute("src") != aURL) + gProxyFavIcon.setAttribute("src", aURL); +} + +function PageProxyClearIcon () +{ + gProxyFavIcon.removeAttribute("src"); +} + + +function PageProxyClickHandler(aEvent) +{ + if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste")) + middleMousePaste(aEvent); +} + +/** + * Handle load of some pages (about:*) so that we can make modifications + * to the DOM for unprivileged pages. + */ +function BrowserOnAboutPageLoad(doc) { + if (doc.documentURI.toLowerCase() == "about:home") { + let ss = Components.classes["@mozilla.org/browser/sessionstore;1"]. + getService(Components.interfaces.nsISessionStore); + if (ss.canRestoreLastSession && + !PrivateBrowsingUtils.isWindowPrivate(window)) + doc.getElementById("launcher").setAttribute("session", "true"); + + // Inject search engine and snippets URL. + let docElt = doc.documentElement; + // set the following attributes BEFORE searchEngineURL, which triggers to + // show the snippets when it's set. + docElt.setAttribute("snippetsURL", AboutHomeUtils.snippetsURL); + if (AboutHomeUtils.showKnowYourRights) { + docElt.setAttribute("showKnowYourRights", "true"); + // Set pref to indicate we've shown the notification. + let currentVersion = Services.prefs.getIntPref("browser.rights.version"); + Services.prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true); + } + docElt.setAttribute("snippetsVersion", AboutHomeUtils.snippetsVersion); + + function updateSearchEngine() { + let engine = AboutHomeUtils.defaultSearchEngine; + docElt.setAttribute("searchEngineName", engine.name); + docElt.setAttribute("searchEnginePostData", engine.postDataString || ""); + docElt.setAttribute("searchEngineURL", engine.searchURL); + } + updateSearchEngine(); + + // Listen for the event that's triggered when the user changes search engine. + // At this point we simply reload about:home to reflect the change. + Services.obs.addObserver(updateSearchEngine, "browser-search-engine-modified", false); + + // Remove the observer when the page is reloaded or closed. + doc.defaultView.addEventListener("pagehide", function removeObserver() { + doc.defaultView.removeEventListener("pagehide", removeObserver); + Services.obs.removeObserver(updateSearchEngine, "browser-search-engine-modified"); + }, false); + } +} + +/** + * Handle command events bubbling up from error page content + */ +let BrowserOnClick = { + handleEvent: function BrowserOnClick_handleEvent(aEvent) { + if (!aEvent.isTrusted || // Don't trust synthetic events + aEvent.button == 2 || aEvent.target.localName != "button") { + return; + } + + let originalTarget = aEvent.originalTarget; + let ownerDoc = originalTarget.ownerDocument; + + // If the event came from an ssl error page, it is probably either the "Add + // Exception…" or "Get me out of here!" button + if (ownerDoc.documentURI.startsWith("about:certerror")) { + this.onAboutCertError(originalTarget, ownerDoc); + } + else if (ownerDoc.documentURI.startsWith("about:neterror")) { + this.onAboutNetError(originalTarget, ownerDoc); + } + else if (ownerDoc.documentURI.toLowerCase() == "about:home") { + this.onAboutHome(originalTarget, ownerDoc); + } + }, + + onAboutCertError: function BrowserOnClick_onAboutCertError(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + let isTopFrame = (aOwnerDoc.defaultView.parent === aOwnerDoc.defaultView); + + switch (elmId) { + case "exceptionDialogButton": + let params = { exceptionAdded : false }; + + try { + switch (Services.prefs.getIntPref("browser.ssl_override_behavior")) { + case 2 : // Pre-fetch & pre-populate + params.prefetchCert = true; + case 1 : // Pre-populate + params.location = aOwnerDoc.location.href; + } + } catch (e) { + Components.utils.reportError("Couldn't get ssl_override pref: " + e); + } + + window.openDialog('chrome://pippki/content/exceptionDialog.xul', + '','chrome,centerscreen,modal', params); + + // If the user added the exception cert, attempt to reload the page + if (params.exceptionAdded) { + aOwnerDoc.location.reload(); + } + break; + + case "getMeOutOfHereButton": + getMeOutOfHere(); + break; + + case "technicalContent": + break; + + case "expertContent": + break; + + } + }, + + onAboutNetError: function BrowserOnClick_onAboutNetError(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + if (elmId != "errorTryAgain" || !/e=netOffline/.test(aOwnerDoc.documentURI)) + return; + Services.io.offline = false; + }, + + onAboutHome: function BrowserOnClick_onAboutHome(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + + switch (elmId) { + case "restorePreviousSession": + let ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + if (ss.canRestoreLastSession) { + ss.restoreLastSession(); + } + aOwnerDoc.getElementById("launcher").removeAttribute("session"); + break; + + case "downloads": + BrowserDownloadsUI(); + break; + + case "bookmarks": + PlacesCommandHook.showPlacesOrganizer("AllBookmarks"); + break; + + case "history": + PlacesCommandHook.showPlacesOrganizer("History"); + break; + + case "addons": + BrowserOpenAddonsMgr(); + break; + + case "sync": + openPreferences("paneSync"); + break; + + case "settings": + openPreferences(); + break; + } + }, +}; + +/** + * Re-direct the browser to a known-safe page. This function is + * used when, for example, the user browses to a known malware page + * and is presented with about:blocked. The "Get me out of here!" + * button should take the user to the default start page so that even + * when their own homepage is infected, we can get them somewhere safe. + */ +function getMeOutOfHere() { + try { + let toBlank = Services.prefs.getBoolPref("browser.escape_to_blank"); + if (toBlank) { + content.location = "about:logopage"; + return; + } + } catch(e) { + Components.utils.reportError("Couldn't get escape pref: " + e); + } + // Get the start page from the *default* pref branch, not the user's + var prefs = Services.prefs.getDefaultBranch(null); + var url = BROWSER_NEW_TAB_URL; + try { + url = prefs.getComplexValue("browser.startup.homepage", + Ci.nsIPrefLocalizedString).data; + // If url is a pipe-delimited set of pages, just take the first one. + if (url.includes("|")) + url = url.split("|")[0]; + } catch(e) { + Components.utils.reportError("Couldn't get homepage pref: " + e); + } + content.location = url; +} + +function BrowserFullScreen() +{ + window.fullScreen = !window.fullScreen; +} + +function onFullScreen(event) { + FullScreen.toggle(event); +} + +function onMozEnteredDomFullscreen(event) { + FullScreen.enterDomFullscreen(event); +} + +function getWebNavigation() +{ + return gBrowser.webNavigation; +} + +function BrowserReloadWithFlags(reloadFlags) { + /* First, we'll try to use the session history object to reload so + * that framesets are handled properly. If we're in a special + * window (such as view-source) that has no session history, fall + * back on using the web navigation's reload method. + */ + + var webNav = gBrowser.webNavigation; + try { + var sh = webNav.sessionHistory; + if (sh) + webNav = sh.QueryInterface(nsIWebNavigation); + } catch (e) { + } + + try { + webNav.reload(reloadFlags); + } catch (e) { + } +} + +var PrintPreviewListener = { + _printPreviewTab: null, + _tabBeforePrintPreview: null, + + getPrintPreviewBrowser: function () { + if (!this._printPreviewTab) { + this._tabBeforePrintPreview = gBrowser.selectedTab; + this._printPreviewTab = gBrowser.loadOneTab("about:blank", + { inBackground: false }); + gBrowser.selectedTab = this._printPreviewTab; + } + return gBrowser.getBrowserForTab(this._printPreviewTab); + }, + getSourceBrowser: function () { + return this._tabBeforePrintPreview ? + this._tabBeforePrintPreview.linkedBrowser : gBrowser.selectedBrowser; + }, + getNavToolbox: function () { + return gNavToolbox; + }, + onEnter: function () { + // We might have accidentally switched tabs since the user invoked print + // preview + if (gBrowser.selectedTab != this._printPreviewTab) { + gBrowser.selectedTab = this._printPreviewTab; + } + gInPrintPreviewMode = true; + this._toggleAffectedChrome(); + }, + onExit: function () { + gBrowser.selectedTab = this._tabBeforePrintPreview; + this._tabBeforePrintPreview = null; + gInPrintPreviewMode = false; + this._toggleAffectedChrome(); + gBrowser.removeTab(this._printPreviewTab); + this._printPreviewTab = null; + }, + _toggleAffectedChrome: function () { + gNavToolbox.collapsed = gInPrintPreviewMode; + + if (gInPrintPreviewMode) + this._hideChrome(); + else + this._showChrome(); + + if (this._chromeState.sidebarOpen) + toggleSidebar(this._sidebarCommand); + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif + }, + _hideChrome: function () { + this._chromeState = {}; + + var sidebar = document.getElementById("sidebar-box"); + this._chromeState.sidebarOpen = !sidebar.hidden; + this._sidebarCommand = sidebar.getAttribute("sidebarcommand"); + + var notificationBox = gBrowser.getNotificationBox(); + this._chromeState.notificationsOpen = !notificationBox.notificationsHidden; + notificationBox.notificationsHidden = true; + + document.getElementById("sidebar").setAttribute("src", "about:blank"); + var addonBar = document.getElementById("addon-bar"); + this._chromeState.addonBarOpen = !addonBar.collapsed; + addonBar.collapsed = true; + gBrowser.updateWindowResizers(); + + this._chromeState.findOpen = gFindBarInitialized && !gFindBar.hidden; + if (gFindBarInitialized) + gFindBar.close(); + + var globalNotificationBox = document.getElementById("global-notificationbox"); + this._chromeState.globalNotificationsOpen = !globalNotificationBox.notificationsHidden; + globalNotificationBox.notificationsHidden = true; + + this._chromeState.syncNotificationsOpen = false; + var syncNotifications = document.getElementById("sync-notifications"); + if (syncNotifications) { + this._chromeState.syncNotificationsOpen = !syncNotifications.notificationsHidden; + syncNotifications.notificationsHidden = true; + } + }, + _showChrome: function () { + if (this._chromeState.notificationsOpen) + gBrowser.getNotificationBox().notificationsHidden = false; + + if (this._chromeState.addonBarOpen) { + document.getElementById("addon-bar").collapsed = false; + gBrowser.updateWindowResizers(); + } + + if (this._chromeState.findOpen) + gFindBar.open(); + + if (this._chromeState.globalNotificationsOpen) + document.getElementById("global-notificationbox").notificationsHidden = false; + + if (this._chromeState.syncNotificationsOpen) + document.getElementById("sync-notifications").notificationsHidden = false; + } +} + +function getMarkupDocumentViewer() +{ + return gBrowser.markupDocumentViewer; +} + +// This function is obsolete. Newer code should use <tooltip page="true"/> instead. +function FillInHTMLTooltip(tipElement) +{ + document.getElementById("aHTMLTooltip").fillInPageTooltip(tipElement); +} + +var browserDragAndDrop = { + canDropLink: function (aEvent) Services.droppedLinkHandler.canDropLink(aEvent, true), + + dragOver: function (aEvent) + { + if (this.canDropLink(aEvent)) { + aEvent.preventDefault(); + } + }, + + drop: function (aEvent, aName, aDisallowInherit) { + return Services.droppedLinkHandler.dropLink(aEvent, aName, aDisallowInherit); + } +}; + +var homeButtonObserver = { + onDrop: function (aEvent) + { + // disallow setting home pages that inherit the principal + let url = browserDragAndDrop.drop(aEvent, {}, true); + setTimeout(openHomeDialog, 0, url); + }, + + onDragOver: function (aEvent) + { + browserDragAndDrop.dragOver(aEvent); + aEvent.dropEffect = "link"; + }, + onDragExit: function (aEvent) + { + } +} + +function openHomeDialog(aURL) +{ + var promptTitle = gNavigatorBundle.getString("droponhometitle"); + var 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 str = Components.classes["@mozilla.org/supports-string;1"] + .createInstance(Components.interfaces.nsISupportsString); + str.data = aURL; + gPrefService.setComplexValue("browser.startup.homepage", + Components.interfaces.nsISupportsString, str); + } catch (ex) { + dump("Failed to set the home page.\n"+ex+"\n"); + } + } +} + +var bookmarksButtonObserver = { + onDrop: function (aEvent) + { + let name = { }; + let url = browserDragAndDrop.drop(aEvent, name); + try { + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: makeURI(url) + , title: name + , hiddenRows: [ "description" + , "location" + , "loadInSidebar" + , "keyword" ] + }, window); + } catch(ex) { } + }, + + onDragOver: function (aEvent) + { + browserDragAndDrop.dragOver(aEvent); + aEvent.dropEffect = "link"; + }, + + onDragExit: function (aEvent) + { + } +} + +var newTabButtonObserver = { + onDragOver: function (aEvent) + { + browserDragAndDrop.dragOver(aEvent); + }, + + onDragExit: function (aEvent) + { + }, + + onDrop: function (aEvent) + { + let url = browserDragAndDrop.drop(aEvent, { }); + var postData = {}; + url = getShortcutOrURI(url, postData); + if (url) { + // allow third-party services to fixup this URL + openNewTabWith(url, null, postData.value, aEvent, true); + } + } +} + +var newWindowButtonObserver = { + onDragOver: function (aEvent) + { + browserDragAndDrop.dragOver(aEvent); + }, + onDragExit: function (aEvent) + { + }, + onDrop: function (aEvent) + { + let url = browserDragAndDrop.drop(aEvent, { }); + var postData = {}; + url = getShortcutOrURI(url, postData); + if (url) { + // allow third-party services to fixup this URL + openNewWindowWith(url, null, postData.value, true); + } + } +} + +const DOMLinkHandler = { + handleEvent: function (event) { + switch (event.type) { + case "DOMLinkAdded": + this.onLinkAdded(event); + break; + } + }, + getLinkIconURI: function(aLink) { + let targetDoc = aLink.ownerDocument; + var uri = makeURI(aLink.href, targetDoc.characterSet); + + // Verify that the load of this icon is legal. + // Some error or special pages can load their favicon. + // To be on the safe side, only allow chrome:// favicons. + var isAllowedPage = [ + /^about:neterror\?/, + /^about:blocked\?/, + /^about:certerror\?/, + /^about:home$/, + ].some(function (re) re.test(targetDoc.documentURI)); + + if (!isAllowedPage || !uri.schemeIs("chrome")) { + var ssm = Services.scriptSecurityManager; + try { + ssm.checkLoadURIWithPrincipal(targetDoc.nodePrincipal, uri, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + } catch(e) { + return null; + } + } + + try { + var contentPolicy = Cc["@mozilla.org/layout/content-policy;1"]. + getService(Ci.nsIContentPolicy); + } catch(e) { + return null; // Refuse to load if we can't do a security check. + } + + // Security says okay, now ask content policy + if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_IMAGE, + uri, targetDoc.documentURIObject, + aLink, aLink.type, null) + != Ci.nsIContentPolicy.ACCEPT) + return null; + + try { + uri.userPass = ""; + } catch(e) { + // some URIs are immutable + } + return uri; + }, + onLinkAdded: function (event) { + var link = event.originalTarget; + var rel = link.rel && link.rel.toLowerCase(); + if (!link || !link.ownerDocument || !rel || !link.href) + return; + + var feedAdded = false; + var iconAdded = false; + var searchAdded = false; + var rels = {}; + for (let relString of rel.split(/\s+/)) + rels[relString] = true; + + for (let relVal in rels) { + switch (relVal) { + case "feed": + case "alternate": + if (!feedAdded) { + if (!rels.feed && rels.alternate && rels.stylesheet) + break; + + if (isValidFeed(link, link.ownerDocument.nodePrincipal, "feed" in rels)) { + FeedHandler.addFeed(link, link.ownerDocument); + feedAdded = true; + } + } + break; + case "icon": + if (!iconAdded) { + if (!gPrefService.getBoolPref("browser.chrome.site_icons")) + break; + + var uri = this.getLinkIconURI(link); + if (!uri) + break; + + if (gBrowser.isFailedIcon(uri)) + break; + + var browserIndex = gBrowser.getBrowserIndexForDocument(link.ownerDocument); + // no browser? no favicon. + if (browserIndex == -1) + break; + + let tab = gBrowser.tabs[browserIndex]; + gBrowser.setIcon(tab, uri.spec); + iconAdded = true; + } + break; + case "search": + if (!searchAdded) { + var type = link.type && link.type.toLowerCase(); + type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); + + if (type == "application/opensearchdescription+xml" && link.title && + /^(?:https?|ftp):/i.test(link.href) && + !PrivateBrowsingUtils.isWindowPrivate(window)) { + var engine = { title: link.title, href: link.href }; + BrowserSearch.addEngine(engine, link.ownerDocument); + searchAdded = true; + } + } + break; + } + } + } +} + +const BrowserSearch = { + addEngine: function(engine, targetDoc) { + if (!this.searchBar) + return; + + var browser = gBrowser.getBrowserForDocument(targetDoc); + // ignore search engines from subframes (see bug 479408) + if (!browser) + return; + + // Check to see whether we've already added an engine with this title + if (browser.engines) { + if (browser.engines.some(function (e) e.title == engine.title)) + return; + } + + // Append the URI and an appropriate title to the browser data. + // Use documentURIObject in the check for shouldLoadFavIcon so that we + // do the right thing with about:-style error pages. Bug 453442 + var iconURL = null; + if (gBrowser.shouldLoadFavIcon(targetDoc.documentURIObject)) + iconURL = targetDoc.documentURIObject.prePath + "/favicon.ico"; + + var hidden = false; + // If this engine (identified by title) is already in the list, add it + // to the list of hidden engines rather than to the main list. + // XXX This will need to be changed when engines are identified by URL; + // see bug 335102. + if (Services.search.getEngineByName(engine.title)) + hidden = true; + + var engines = (hidden ? browser.hiddenEngines : browser.engines) || []; + + engines.push({ uri: engine.href, + title: engine.title, + icon: iconURL }); + + if (hidden) + browser.hiddenEngines = engines; + else + browser.engines = engines; + }, + + /** + * Gives focus to the search bar, if it is present on the toolbar, or loads + * the default engine's search form otherwise. For Mac, opens a new window + * or focuses an existing window, if necessary. + */ + webSearch: function BrowserSearch_webSearch() { +#ifdef XP_MACOSX + if (window.location.href != getBrowserURL()) { + var win = getTopWin(); + if (win) { + // If there's an open browser window, it should handle this command + win.focus(); + win.BrowserSearch.webSearch(); + } else { + // If there are no open browser windows, open a new one + var observer = function observer(subject, topic, data) { + if (subject == win) { + BrowserSearch.webSearch(); + Services.obs.removeObserver(observer, "browser-delayed-startup-finished"); + } + } + win = window.openDialog(getBrowserURL(), "_blank", + "chrome,all,dialog=no", "about:blank"); + Services.obs.addObserver(observer, "browser-delayed-startup-finished", false); + } + return; + } +#endif + var searchBar = this.searchBar; + if (searchBar && window.fullScreen) + FullScreen.mouseoverToggle(true); + if (searchBar) + searchBar.select(); + if (!searchBar || document.activeElement != searchBar.textbox.inputField) + openUILinkIn(Services.search.defaultEngine.searchForm, "current"); + }, + + /** + * Loads a search results page, given a set of search terms. Uses the current + * engine if the search bar is visible, or the default engine otherwise. + * + * @param searchText + * The search terms to use for the search. + * + * @param useNewTab + * Boolean indicating whether or not the search should load in a new + * tab. + * + * @param purpose [optional] + * A string meant to indicate the context of the search request. This + * allows the search service to provide a different nsISearchSubmission + * depending on e.g. where the search is triggered in the UI. + * + * @return string Name of the search engine used to perform a search or null + * if a search was not performed. + */ + loadSearch: function BrowserSearch_search(searchText, useNewTab, purpose) { + var engine; + + // If the search bar is visible, use the current engine, otherwise, fall + // back to the default engine. + if (isElementVisible(this.searchBar)) + engine = Services.search.currentEngine; + else + engine = Services.search.defaultEngine; + + var submission = engine.getSubmission(searchText, null, purpose); // HTML response + + // getSubmission can return null if the engine doesn't have a URL + // with a text/html response type. This is unlikely (since + // SearchService._addEngineToStore() should fail for such an engine), + // but let's be on the safe side. + if (!submission) { + return null; + } + + let inBackground = Services.prefs.getBoolPref("browser.search.context.loadInBackground"); + openLinkIn(submission.uri.spec, + useNewTab ? "tab" : "current", + { postData: submission.postData, + inBackground: inBackground, + relatedToCurrent: true }); + + return engine.name; + }, + + /** + * Perform a search initiated from the context menu. + * + * This should only be called from the context menu. See + * BrowserSearch.loadSearch for the preferred API. + */ + loadSearchFromContext: function (terms) { + let engine = BrowserSearch.loadSearch(terms, true, "contextmenu"); + }, + + /** + * Returns the search bar element if it is present in the toolbar, null otherwise. + */ + get searchBar() { + return document.getElementById("searchbar"); + }, + + loadAddEngines: function BrowserSearch_loadAddEngines() { + var newWindowPref = gPrefService.getIntPref("browser.link.open_newwindow"); + var where = newWindowPref == 3 ? "tab" : "window"; + var searchEnginesURL = formatURL("browser.search.searchEnginesURL", true); + openUILinkIn(searchEnginesURL, where); + }, +}; + +function FillHistoryMenu(aParent) { + // Lazily add the hover listeners on first showing and never remove them + if (!aParent.hasStatusListener) { + // Show history item's uri in the status bar when hovering, and clear on exit + aParent.addEventListener("DOMMenuItemActive", function(aEvent) { + // Only the current page should have the checked attribute, so skip it + if (!aEvent.target.hasAttribute("checked")) + XULBrowserWindow.setOverLink(aEvent.target.getAttribute("uri")); + }, false); + aParent.addEventListener("DOMMenuItemInactive", function() { + XULBrowserWindow.setOverLink(""); + }, false); + + aParent.hasStatusListener = true; + } + + // Remove old entries if any + var children = aParent.childNodes; + for (var i = children.length - 1; i >= 0; --i) { + if (children[i].hasAttribute("index")) + aParent.removeChild(children[i]); + } + + var webNav = gBrowser.webNavigation; + var sessionHistory = webNav.sessionHistory; + + var count = sessionHistory.count; + if (count <= 1) // don't display the popup for a single item + return false; + + const MAX_HISTORY_MENU_ITEMS = 15; + var index = sessionHistory.index; + var half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2); + var start = Math.max(index - half_length, 0); + var end = Math.min(start == 0 ? MAX_HISTORY_MENU_ITEMS : index + half_length + 1, count); + if (end == count) + start = Math.max(count - MAX_HISTORY_MENU_ITEMS, 0); + + var tooltipBack = gNavigatorBundle.getString("tabHistory.goBack"); + var tooltipCurrent = gNavigatorBundle.getString("tabHistory.current"); + var tooltipForward = gNavigatorBundle.getString("tabHistory.goForward"); + + for (var j = end - 1; j >= start; j--) { + let item = document.createElement("menuitem"); + let entry = sessionHistory.getEntryAtIndex(j, false); + let uri = entry.URI.spec; + + item.setAttribute("uri", uri); + item.setAttribute("label", entry.title || uri); + item.setAttribute("index", j); + + if (j != index) { + PlacesUtils.favicons.getFaviconURLForPage(entry.URI, function (aURI) { + if (aURI) { + let iconURL = PlacesUtils.favicons.getFaviconLinkForIcon(aURI).spec; + item.style.listStyleImage = "url(" + iconURL + ")"; + } + }); + } + + if (j < index) { + item.className = "unified-nav-back menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipBack); + } else if (j == index) { + item.setAttribute("type", "radio"); + item.setAttribute("checked", "true"); + item.className = "unified-nav-current"; + item.setAttribute("tooltiptext", tooltipCurrent); + } else { + item.className = "unified-nav-forward menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipForward); + } + + aParent.appendChild(item); + } + return true; +} + +function addToUrlbarHistory(aUrlToAdd) { + if (!PrivateBrowsingUtils.isWindowPrivate(window) && + aUrlToAdd && + !aUrlToAdd.includes(" ") && + !/[\x00-\x1F]/.test(aUrlToAdd)) + PlacesUIUtils.markPageAsTyped(aUrlToAdd); +} + +function toJavaScriptConsole() +{ + toOpenWindowByType("global:console", "chrome://global/content/console.xul"); +} + +function BrowserDownloadsUI() +{ + if (PrivateBrowsingUtils.isWindowPrivate(window)) { + openUILinkIn("about:downloads", "tab"); + } else { + PlacesCommandHook.showPlacesOrganizer("Downloads"); + } +} + +function toOpenWindowByType(inType, uri, features) +{ + var topWindow = Services.wm.getMostRecentWindow(inType); + + if (topWindow) + topWindow.focus(); + else if (features) + window.open(uri, "_blank", features); + else + window.open(uri, "_blank", "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar"); +} + +function OpenBrowserWindow(options) +{ + function newDocumentShown(doc, topic, data) { + if (topic == "document-shown" && + doc != document && + doc.defaultView == win) { + Services.obs.removeObserver(newDocumentShown, "document-shown"); + } + }; + Services.obs.addObserver(newDocumentShown, "document-shown", false); + + var charsetArg = new String(); + var handler = Components.classes["@mozilla.org/browser/clh;1"] + .getService(Components.interfaces.nsIBrowserHandler); + var defaultArgs = handler.defaultArgs; + var wintype = document.documentElement.getAttribute('windowtype'); + + var extraFeatures = ""; + if (options && options.private) { + extraFeatures = ",private"; + if (!PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Force the new window to load about:privatebrowsing instead of the default home page + defaultArgs = "about:privatebrowsing"; + } + } else { + extraFeatures = ",non-private"; + } + + // if and only if the current window is a browser window and it has a document with a character + // set, then extract the current charset menu setting from the current document and use it to + // initialize the new browser window... + var win; + if (window && (wintype == "navigator:browser") && window.content && window.content.document) + { + var DocCharset = window.content.document.characterSet; + charsetArg = "charset="+DocCharset; + + //we should "inherit" the charset menu setting in a new window + win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs, charsetArg); + } + else // forget about the charset information. + { + win = window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no" + extraFeatures, defaultArgs); + } + + return win; +} + +var gCustomizeSheet = false; +function BrowserCustomizeToolbar() { + // Disable the toolbar context menu items + var menubar = document.getElementById("main-menubar"); + for (let childNode of menubar.childNodes) + childNode.setAttribute("disabled", true); + + var cmd = document.getElementById("cmd_CustomizeToolbars"); + cmd.setAttribute("disabled", "true"); + + var splitter = document.getElementById("urlbar-search-splitter"); + if (splitter) + splitter.parentNode.removeChild(splitter); + + CombinedStopReload.uninit(); + + PlacesToolbarHelper.customizeStart(); + BookmarkingUI.customizeStart(); + DownloadsButton.customizeStart(); + + TabsInTitlebar.allowedBy("customizing-toolbars", false); + + var customizeURL = "chrome://global/content/customizeToolbar.xul"; + gCustomizeSheet = getBoolPref("toolbar.customization.usesheet", false); + + if (gCustomizeSheet) { + let sheetFrame = document.createElement("iframe"); + let panel = document.getElementById("customizeToolbarSheetPopup"); + sheetFrame.id = "customizeToolbarSheetIFrame"; + sheetFrame.toolbox = gNavToolbox; + sheetFrame.panel = panel; + sheetFrame.setAttribute("style", panel.getAttribute("sheetstyle")); + panel.appendChild(sheetFrame); + + // Open the panel, but make it invisible until the iframe has loaded so + // that the user doesn't see a white flash. + panel.style.visibility = "hidden"; + gNavToolbox.addEventListener("beforecustomization", function onBeforeCustomization() { + gNavToolbox.removeEventListener("beforecustomization", onBeforeCustomization, false); + panel.style.removeProperty("visibility"); + }, false); + + sheetFrame.setAttribute("src", customizeURL); + + panel.openPopup(gNavToolbox, "after_start", 0, 0); + } else { + window.openDialog(customizeURL, + "CustomizeToolbar", + "chrome,titlebar,toolbar,location,resizable,dependent", + gNavToolbox); + } +} + +function BrowserToolboxCustomizeDone(aToolboxChanged) { + if (gCustomizeSheet) { + document.getElementById("customizeToolbarSheetPopup").hidePopup(); + let iframe = document.getElementById("customizeToolbarSheetIFrame"); + iframe.parentNode.removeChild(iframe); + } + + // Update global UI elements that may have been added or removed + if (aToolboxChanged) { + gURLBar = document.getElementById("urlbar"); + + gProxyFavIcon = document.getElementById("page-proxy-favicon"); + gHomeButton.updateTooltip(); + gIdentityHandler._cacheElements(); + window.XULBrowserWindow.init(); + +#ifndef XP_MACOSX + updateEditUIVisibility(); +#endif + + // Hacky: update the PopupNotifications' object's reference to the iconBox, + // if it already exists, since it may have changed if the URL bar was + // added/removed. + if (!window.__lookupGetter__("PopupNotifications")) + PopupNotifications.iconBox = document.getElementById("notification-popup-box"); + } + + PlacesToolbarHelper.customizeDone(); + BookmarkingUI.customizeDone(); + DownloadsButton.customizeDone(); + + // The url bar splitter state is dependent on whether stop/reload + // and the location bar are combined, so we need this ordering + CombinedStopReload.init(); + UpdateUrlbarSearchSplitterState(); + setUrlAndSearchBarWidthForConditionalForwardButton(); + + // Update the urlbar + if (gURLBar) { + URLBarSetURI(); + XULBrowserWindow.asyncUpdateUI(); + BookmarkingUI.updateStarState(); + } + + TabsInTitlebar.allowedBy("customizing-toolbars", true); + + // Re-enable parts of the UI we disabled during the dialog + var menubar = document.getElementById("main-menubar"); + for (let childNode of menubar.childNodes) + childNode.setAttribute("disabled", false); + var cmd = document.getElementById("cmd_CustomizeToolbars"); + cmd.removeAttribute("disabled"); + + // make sure to re-enable click-and-hold + if (!getBoolPref("ui.click_hold_context_menus", false)) + SetClickAndHoldHandlers(); + + gBrowser.selectedBrowser.focus(); +} + +function BrowserToolboxCustomizeChange(aType) { + switch (aType) { + case "iconsize": + case "mode": + retrieveToolbarIconsizesFromTheme(); + break; + default: + gHomeButton.updatePersonalToolbarStyle(); + BookmarkingUI.customizeChange(); + allTabs.readPref(); + } +} + +/** + * Allows themes to override the "iconsize" attribute on toolbars. + */ +function retrieveToolbarIconsizesFromTheme() { + function retrieveToolbarIconsize(aToolbar) { + if (aToolbar.localName != "toolbar") + return; + + // The theme indicates that it wants to override the "iconsize" attribute + // by specifying a special value for the "counter-reset" property on the + // toolbar. A custom property cannot be used because getComputedStyle can + // only return the values of standard CSS properties. + let counterReset = getComputedStyle(aToolbar).counterReset; + if (counterReset == "smallicons 0") + aToolbar.setAttribute("iconsize", "small"); + else if (counterReset == "largeicons 0") + aToolbar.setAttribute("iconsize", "large"); + } + + Array.forEach(gNavToolbox.childNodes, retrieveToolbarIconsize); + gNavToolbox.externalToolbars.forEach(retrieveToolbarIconsize); +} + +/** + * Update the global flag that tracks whether or not any edit UI (the Edit menu, + * edit-related items in the context menu, and edit-related toolbar buttons + * is visible, then update the edit commands' enabled state accordingly. We use + * this flag to skip updating the edit commands on focus or selection changes + * when no UI is visible to improve performance (including pageload performance, + * since focus changes when you load a new page). + * + * If UI is visible, we use goUpdateGlobalEditMenuItems to set the commands' + * enabled state so the UI will reflect it appropriately. + * + * If the UI isn't visible, we enable all edit commands so keyboard shortcuts + * still work and just lazily disable them as needed when the user presses a + * shortcut. + * + * This doesn't work on Mac, since Mac menus flash when users press their + * keyboard shortcuts, so edit UI is essentially always visible on the Mac, + * and we need to always update the edit commands. Thus on Mac this function + * is a no op. + */ +function updateEditUIVisibility() +{ +#ifndef XP_MACOSX + let editMenuPopupState = document.getElementById("menu_EditPopup").state; + let contextMenuPopupState = document.getElementById("contentAreaContextMenu").state; + let placesContextMenuPopupState = document.getElementById("placesContext").state; +#ifdef MENUBAR_CAN_AUTOHIDE + let appMenuPopupState = document.getElementById("appmenu-popup").state; +#endif + + // The UI is visible if the Edit menu is opening or open, if the context menu + // is open, or if the toolbar has been customized to include the Cut, Copy, + // or Paste toolbar buttons. + gEditUIVisible = editMenuPopupState == "showing" || + editMenuPopupState == "open" || + contextMenuPopupState == "showing" || + contextMenuPopupState == "open" || + placesContextMenuPopupState == "showing" || + placesContextMenuPopupState == "open" || +#ifdef MENUBAR_CAN_AUTOHIDE + appMenuPopupState == "showing" || + appMenuPopupState == "open" || +#endif + document.getElementById("cut-button") || + document.getElementById("copy-button") || + document.getElementById("paste-button") ? true : false; + + // If UI is visible, update the edit commands' enabled state to reflect + // whether or not they are actually enabled for the current focus/selection. + if (gEditUIVisible) + goUpdateGlobalEditMenuItems(); + + // Otherwise, enable all commands, so that keyboard shortcuts still work, + // then lazily determine their actual enabled state when the user presses + // a keyboard shortcut. + else { + goSetCommandEnabled("cmd_undo", true); + goSetCommandEnabled("cmd_redo", true); + goSetCommandEnabled("cmd_cut", true); + goSetCommandEnabled("cmd_copy", true); + goSetCommandEnabled("cmd_paste", true); + goSetCommandEnabled("cmd_selectAll", true); + goSetCommandEnabled("cmd_delete", true); + goSetCommandEnabled("cmd_switchTextDirection", true); + } +#endif +} + +/** + * Makes the Character Encoding menu enabled or disabled as appropriate. + * To be called when the View menu or the app menu is opened. + */ +function updateCharacterEncodingMenuState() +{ + let charsetMenu = document.getElementById("charsetMenu"); + let appCharsetMenu = document.getElementById("appmenu_charsetMenu"); + let appDevCharsetMenu = + document.getElementById("appmenu_developer_charsetMenu"); + // gBrowser is null on Mac when the menubar shows in the context of + // non-browser windows. The above elements may be null depending on + // what parts of the menubar are present. E.g. no app menu on Mac. + if (gBrowser && + gBrowser.docShell && + gBrowser.docShell.mayEnableCharacterEncodingMenu) { + if (charsetMenu) { + charsetMenu.removeAttribute("disabled"); + } + if (appCharsetMenu) { + appCharsetMenu.removeAttribute("disabled"); + } + if (appDevCharsetMenu) { + appDevCharsetMenu.removeAttribute("disabled"); + } + } else { + if (charsetMenu) { + charsetMenu.setAttribute("disabled", "true"); + } + if (appCharsetMenu) { + appCharsetMenu.setAttribute("disabled", "true"); + } + if (appDevCharsetMenu) { + appDevCharsetMenu.setAttribute("disabled", "true"); + } + } +} + +/** + * Returns true if |aMimeType| is text-based, false otherwise. + * + * @param aMimeType + * The MIME type to check. + * + * If adding types to this function, please also check the similar + * function in findbar.xml + */ +function mimeTypeIsTextBased(aMimeType) +{ + return aMimeType.startsWith("text/") || + aMimeType.endsWith("+xml") || + aMimeType == "application/x-javascript" || + aMimeType == "application/javascript" || + aMimeType == "application/json" || + aMimeType == "application/xml" || + aMimeType == "mozilla.application/cached-xul"; +} + +var XULBrowserWindow = { + // Stored Status, Link and Loading values + status: "", + defaultStatus: "", + overLink: "", + startTime: 0, + statusText: "", + isBusy: false, +/* Pale Moon: Don't hide navigation controls and toolbars for "special" pages. SBaD, M! + inContentWhitelist: ["about:addons", "about:downloads", "about:permissions", + "about:sync-progress"],*/ + inContentWhitelist: [], + + QueryInterface: function (aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsIWebProgressListener2) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsIXULBrowserWindow) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + }, + + get stopCommand () { + delete this.stopCommand; + return this.stopCommand = document.getElementById("Browser:Stop"); + }, + get reloadCommand () { + delete this.reloadCommand; + return this.reloadCommand = document.getElementById("Browser:Reload"); + }, + get statusTextField () { + delete this.statusTextField; + return this.statusTextField = document.getElementById("statusbar-display"); + }, + get isImage () { + delete this.isImage; + return this.isImage = document.getElementById("isImage"); + }, + + init: function () { + this.throbberElement = document.getElementById("navigator-throbber"); + + // Bug 666809 - SecurityUI support for e10s + if (gMultiProcessBrowser) + return; + + // Initialize the security button's state and tooltip text. Remember to reset + // _hostChanged, otherwise onSecurityChange will short circuit. + var securityUI = gBrowser.securityUI; + this._hostChanged = true; + this.onSecurityChange(null, null, securityUI.state); + }, + + destroy: function () { + // XXXjag to avoid leaks :-/, see bug 60729 + delete this.throbberElement; + delete this.stopCommand; + delete this.reloadCommand; + delete this.statusTextField; + delete this.statusText; + }, + + setJSStatus: function () { + // unsupported + }, + + setDefaultStatus: function (status) { + this.defaultStatus = status; + this.updateStatusField(); + }, + + setOverLink: function (url, anchorElt) { + // Encode bidirectional formatting characters. + // (RFC 3987 sections 3.2 and 4.1 paragraph 6) + url = url.replace(/[\u200e\u200f\u202a\u202b\u202c\u202d\u202e]/g, + encodeURIComponent); + + if (gURLBar && gURLBar._mayTrimURLs /* corresponds to browser.urlbar.trimURLs */) + url = trimURL(url); + + this.overLink = url; + LinkTargetDisplay.update(); + }, + + updateStatusField: function () { + var text, type, types = ["overLink"]; + if (this._busyUI) + types.push("status"); + types.push("defaultStatus"); + for (type of types) { + text = this[type]; + if (text) + break; + } + + // check the current value so we don't trigger an attribute change + // and cause needless (slow!) UI updates + if (this.statusText != text) { + let field = this.statusTextField; + field.setAttribute("previoustype", field.getAttribute("type")); + field.setAttribute("type", type); + field.label = text; + field.setAttribute("crop", type == "overLink" ? "center" : "end"); + this.statusText = text; + } + }, + + // Called before links are navigated to to allow us to retarget them if needed. + onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + let target = this._onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab); + return target; + }, + + _onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) { + // Don't modify non-default targets or targets that aren't in top-level app + // tab docshells (isAppTab will be false for app tab subframes). + if (originalTarget != "" || !isAppTab) + return originalTarget; + + // External links from within app tabs should always open in new tabs + // instead of replacing the app tab's page (Bug 575561) + let linkHost; + let docHost; + try { + linkHost = linkURI.host; + docHost = linkNode.ownerDocument.documentURIObject.host; + } catch(e) { + // nsIURI.host can throw for non-nsStandardURL nsIURIs. + // If we fail to get either host, just return originalTarget. + return originalTarget; + } + + if (docHost == linkHost) + return originalTarget; + + // Special case: ignore "www" prefix if it is part of host string + let [longHost, shortHost] = + linkHost.length > docHost.length ? [linkHost, docHost] : [docHost, linkHost]; + if (longHost == "www." + shortHost) + return originalTarget; + + return "_blank"; + }, + + onLinkIconAvailable: function (aIconURL) { + if (gProxyFavIcon && gBrowser.userTypedValue === null) { + PageProxySetIcon(aIconURL); // update the favicon in the URL bar + } + }, + + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + // Do nothing. + }, + + onProgressChange64: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + return this.onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress); + }, + + // This function fires only for the currently selected tab. + onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { + const nsIWebProgressListener = Ci.nsIWebProgressListener; + const nsIChannel = Ci.nsIChannel; + + if (aStateFlags & nsIWebProgressListener.STATE_START && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + + if (aRequest && aWebProgress.isTopLevel) { + // clear out feed data + gBrowser.selectedBrowser.feeds = null; + + // clear out search-engine data + gBrowser.selectedBrowser.engines = null; + } + + this.isBusy = true; + + if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { + this._busyUI = true; + + // Turn the throbber on. + if (this.throbberElement) + this.throbberElement.setAttribute("busy", "true"); + + // XXX: This needs to be based on window activity... + this.stopCommand.removeAttribute("disabled"); + CombinedStopReload.switchToStop(); + } + } + else if (aStateFlags & nsIWebProgressListener.STATE_STOP) { + // This (thanks to the filter) is a network stop or the last + // request stop outside of loading the document, stop throbbers + // and progress bars and such + if (aRequest) { + let msg = ""; + let location; + // Get the URI either from a channel or a pseudo-object + if (aRequest instanceof nsIChannel || "URI" in aRequest) { + location = aRequest.URI; + + // For keyword URIs clear the user typed value since they will be changed into real URIs + if (location.scheme == "keyword" && aWebProgress.isTopLevel) + gBrowser.userTypedValue = null; + + if (location.spec != "about:blank") { + switch (aStatus) { + case Components.results.NS_ERROR_NET_TIMEOUT: + msg = gNavigatorBundle.getString("nv_timeout"); + break; + } + } + } + + this.status = ""; + this.setDefaultStatus(msg); + + // Disable menu entries for images, enable otherwise + if (!gMultiProcessBrowser && content.document && mimeTypeIsTextBased(content.document.contentType)) + this.isImage.removeAttribute('disabled'); + else + this.isImage.setAttribute('disabled', 'true'); + } + + this.isBusy = false; + + if (this._busyUI) { + this._busyUI = false; + + // Turn the throbber off. + if (this.throbberElement) + this.throbberElement.removeAttribute("busy"); + + this.stopCommand.setAttribute("disabled", "true"); + CombinedStopReload.switchToReload(aRequest instanceof Ci.nsIRequest); + } + } + }, + + onLocationChange: function (aWebProgress, aRequest, aLocationURI, aFlags) { + var location = aLocationURI ? aLocationURI.spec : ""; + this._hostChanged = true; + + // If displayed, hide the form validation popup. + FormValidationHandler.hidePopup(); + + let pageTooltip = document.getElementById("aHTMLTooltip"); + let tooltipNode = pageTooltip.triggerNode; + if (tooltipNode) { + // Optimise for the common case + if (aWebProgress.isTopLevel) { + pageTooltip.hidePopup(); + } + else { + for (let tooltipWindow = tooltipNode.ownerDocument.defaultView; + tooltipWindow != tooltipWindow.parent; + tooltipWindow = tooltipWindow.parent) { + if (tooltipWindow == aWebProgress.DOMWindow) { + pageTooltip.hidePopup(); + break; + } + } + } + } + + // Disable menu entries for images, enable otherwise + if (!gMultiProcessBrowser && content.document && mimeTypeIsTextBased(content.document.contentType)) + this.isImage.removeAttribute('disabled'); + else + this.isImage.setAttribute('disabled', 'true'); + + this.hideOverLinkImmediately = true; + this.setOverLink("", null); + this.hideOverLinkImmediately = false; + + // We should probably not do this if the value has changed since the user + // searched + // Update urlbar only if a new page was loaded on the primary content area + // Do not update urlbar if there was a subframe navigation + + var browser = gBrowser.selectedBrowser; + if (aWebProgress.isTopLevel) { + if ((location == "about:blank" && (gMultiProcessBrowser || !content.opener)) || + location == "") { // Second condition is for new tabs, otherwise + // reload function is enabled until tab is refreshed. + this.reloadCommand.setAttribute("disabled", "true"); + } else { + this.reloadCommand.removeAttribute("disabled"); + } + + if (gURLBar) { + URLBarSetURI(aLocationURI); + + // Update starring UI + BookmarkingUI.updateStarState(); + } + + // Show or hide browser chrome based on the whitelist + if (this.hideChromeForLocation(location)) { + document.documentElement.setAttribute("disablechrome", "true"); + } else { + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); + if (ss.getTabValue(gBrowser.selectedTab, "appOrigin")) + document.documentElement.setAttribute("disablechrome", "true"); + else + document.documentElement.removeAttribute("disablechrome"); + } + + // Utility functions for disabling find + var shouldDisableFind = function shouldDisableFind(aDocument) { + let docElt = aDocument.documentElement; + return docElt && docElt.getAttribute("disablefastfind") == "true"; + } + + var disableFindCommands = function disableFindCommands(aDisable) { + let findCommands = [document.getElementById("cmd_find"), + document.getElementById("cmd_findAgain"), + document.getElementById("cmd_findPrevious")]; + for (let elt of findCommands) { + if (aDisable) + elt.setAttribute("disabled", "true"); + else + elt.removeAttribute("disabled"); + } + if (gFindBarInitialized) { + if (!gFindBar.hidden && aDisable) { + gFindBar.hidden = true; + this._findbarTemporarilyHidden = true; + } else if (this._findbarTemporarilyHidden && !aDisable) { + gFindBar.hidden = false; + this._findbarTemporarilyHidden = false; + } + } + }.bind(this); + + var onContentRSChange = function onContentRSChange(e) { + if (e.target.readyState != "interactive" && e.target.readyState != "complete") + return; + + e.target.removeEventListener("readystatechange", onContentRSChange); + disableFindCommands(shouldDisableFind(e.target)); + } + + // Disable find commands in documents that ask for them to be disabled. + if (!gMultiProcessBrowser && aLocationURI && + (aLocationURI.schemeIs("about") || aLocationURI.schemeIs("chrome"))) { + // Don't need to re-enable/disable find commands for same-document location changes + // (e.g. the replaceStates in about:addons) + if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { + if (content.document.readyState == "interactive" || content.document.readyState == "complete") + disableFindCommands(shouldDisableFind(content.document)); + else { + content.document.addEventListener("readystatechange", onContentRSChange); + } + } + } else + disableFindCommands(false); + + if (gFindBarInitialized) { + if (gFindBar.findMode != gFindBar.FIND_NORMAL) { + // Close the Find toolbar if we're in old-style TAF mode + gFindBar.close(); + } + + if (!(gPrefService.getBoolPref("accessibility.typeaheadfind.highlightallremember") || + gPrefService.getBoolPref("accessibility.typeaheadfind.highlightallbydefault"))) { + // fix bug 253793 - turn off highlight when page changes + gFindBar.getElement("highlight").checked = false; + } + } + } + UpdateBackForwardCommands(gBrowser.webNavigation); + + gGestureSupport.restoreRotationState(); + + // See bug 358202, when tabs are switched during a drag operation, + // timers don't fire on windows (bug 203573) + if (aRequest) + setTimeout(function () { XULBrowserWindow.asyncUpdateUI(); }, 0); + else + this.asyncUpdateUI(); + }, + + asyncUpdateUI: function () { + FeedHandler.updateFeeds(); + }, + + hideChromeForLocation: function(aLocation) { + aLocation = aLocation.toLowerCase(); + return this.inContentWhitelist.some(function(aSpec) { + return aSpec == aLocation; + }); + }, + + onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) { + this.status = aMessage; + this.updateStatusField(); + }, + + // Properties used to cache security state used to update the UI + _state: null, + _hostChanged: false, // onLocationChange will flip this bit + + onSecurityChange: function (aWebProgress, aRequest, aState) { + // Don't need to do anything if the data we use to update the UI hasn't + // changed + if (this._state == aState && + !this._hostChanged) { +#ifdef DEBUG + try { + var contentHost = gBrowser.contentWindow.location.host; + if (this._host !== undefined && this._host != contentHost) { + Components.utils.reportError( + "ASSERTION: browser.js host is inconsistent. Content window has " + + "<" + contentHost + "> but cached host is <" + this._host + ">.\n" + ); + } + } catch (ex) {} +#endif + return; + } + this._state = aState; + +#ifdef DEBUG + try { + this._host = gBrowser.contentWindow.location.host; + } catch(ex) { + this._host = null; + } +#endif + + this._hostChanged = false; + + // aState is defined as a bitmask that may be extended in the future. + // We filter out any unknown bits before testing for known values. + const wpl = Components.interfaces.nsIWebProgressListener; + const wpl_security_bits = wpl.STATE_IS_SECURE | + wpl.STATE_IS_BROKEN | + wpl.STATE_IS_INSECURE; + var level; + + switch (this._state & wpl_security_bits) { + case wpl.STATE_IS_SECURE: + level = "high"; + break; + case wpl.STATE_IS_BROKEN: + level = "broken"; + break; + } + + if (level) { + // We don't style the Location Bar based on the the 'level' attribute + // anymore, but still set it for third-party themes. + if (gURLBar) + gURLBar.setAttribute("level", level); + } else { + if (gURLBar) + gURLBar.removeAttribute("level"); + } + + if (gMultiProcessBrowser) + return; + + // Don't pass in the actual location object, since it can cause us to + // hold on to the window object too long. Just pass in the fields we + // care about. (bug 424829) + var location = gBrowser.contentWindow.location; + var locationObj = {}; + try { + // about:blank can be used by webpages so pretend it is http + locationObj.protocol = location == "about:blank" ? "http:" : location.protocol; + locationObj.host = location.host; + locationObj.hostname = location.hostname; + locationObj.port = location.port; + } catch (ex) { + // Can sometimes throw if the URL being visited has no host/hostname, + // e.g. about:blank. The _state for these pages means we won't need these + // properties anyways, though. + } + gIdentityHandler.checkIdentity(this._state, locationObj); + }, + + // simulate all change notifications after switching tabs + onUpdateCurrentBrowser: function XWB_onUpdateCurrentBrowser(aStateFlags, aStatus, aMessage, aTotalProgress) { + if (FullZoom.updateBackgroundTabs) + FullZoom.onLocationChange(gBrowser.currentURI, true); + var nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + var loadingDone = aStateFlags & nsIWebProgressListener.STATE_STOP; + // use a pseudo-object instead of a (potentially nonexistent) channel for getting + // a correct error message - and make sure that the UI is always either in + // loading (STATE_START) or done (STATE_STOP) mode + this.onStateChange( + gBrowser.webProgress, + { URI: gBrowser.currentURI }, + loadingDone ? nsIWebProgressListener.STATE_STOP : nsIWebProgressListener.STATE_START, + aStatus + ); + // status message and progress value are undefined if we're done with loading + if (loadingDone) + return; + this.onStatusChange(gBrowser.webProgress, null, 0, aMessage); + } +}; + +var LinkTargetDisplay = { + get DELAY_SHOW() { + delete this.DELAY_SHOW; + return this.DELAY_SHOW = Services.prefs.getIntPref("browser.overlink-delay"); + }, + + DELAY_HIDE: 250, + _timer: 0, + + get _isVisible () XULBrowserWindow.statusTextField.label != "", + + update: function () { + clearTimeout(this._timer); + window.removeEventListener("mousemove", this, true); + + if (!XULBrowserWindow.overLink) { + if (XULBrowserWindow.hideOverLinkImmediately) + this._hide(); + else + this._timer = setTimeout(this._hide.bind(this), this.DELAY_HIDE); + return; + } + + if (this._isVisible) { + XULBrowserWindow.updateStatusField(); + } else { + // Let the display appear when the mouse doesn't move within the delay + this._showDelayed(); + window.addEventListener("mousemove", this, true); + } + }, + + handleEvent: function (event) { + switch (event.type) { + case "mousemove": + // Restart the delay since the mouse was moved + clearTimeout(this._timer); + this._showDelayed(); + break; + } + }, + + _showDelayed: function () { + this._timer = setTimeout(function (self) { + XULBrowserWindow.updateStatusField(); + window.removeEventListener("mousemove", self, true); + }, this.DELAY_SHOW, this); + }, + + _hide: function () { + clearTimeout(this._timer); + + XULBrowserWindow.updateStatusField(); + } +}; + +var CombinedStopReload = { + init: function () { + if (this._initialized) + return; + + var urlbar = document.getElementById("urlbar-container"); + var reload = document.getElementById("reload-button"); + var stop = document.getElementById("stop-button"); + + if (urlbar) { + if (urlbar.parentNode.getAttribute("mode") != "icons" || + !reload || urlbar.nextSibling != reload || + !stop || reload.nextSibling != stop) + urlbar.removeAttribute("combined"); + else { + urlbar.setAttribute("combined", "true"); + reload = document.getElementById("urlbar-reload-button"); + stop = document.getElementById("urlbar-stop-button"); + } + } + if (!stop || !reload || reload.nextSibling != stop) + return; + + this._initialized = true; + if (XULBrowserWindow.stopCommand.getAttribute("disabled") != "true") + reload.setAttribute("displaystop", "true"); + stop.addEventListener("click", this, false); + this.reload = reload; + this.stop = stop; + }, + + uninit: function () { + if (!this._initialized) + return; + + this._cancelTransition(); + this._initialized = false; + this.stop.removeEventListener("click", this, false); + this.reload = null; + this.stop = null; + }, + + handleEvent: function (event) { + // the only event we listen to is "click" on the stop button + if (event.button == 0 && + !this.stop.disabled) + this._stopClicked = true; + }, + + switchToStop: function () { + if (!this._initialized) + return; + + this._cancelTransition(); + this.reload.setAttribute("displaystop", "true"); + }, + + switchToReload: function (aDelay) { + if (!this._initialized) + return; + + this.reload.removeAttribute("displaystop"); + + if (!aDelay || this._stopClicked) { + this._stopClicked = false; + this._cancelTransition(); + this.reload.disabled = XULBrowserWindow.reloadCommand + .getAttribute("disabled") == "true"; + return; + } + + if (this._timer) + return; + + // Temporarily disable the reload button to prevent the user from + // accidentally reloading the page when intending to click the stop button + this.reload.disabled = true; + this._timer = setTimeout(function (self) { + self._timer = 0; + self.reload.disabled = XULBrowserWindow.reloadCommand + .getAttribute("disabled") == "true"; + }, 650, this); + }, + + _cancelTransition: function () { + if (this._timer) { + clearTimeout(this._timer); + this._timer = 0; + } + } +}; + +var TabsProgressListener = { + onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + + // Attach a listener to watch for "click" events bubbling up from error + // pages and other similar page. This lets us fix bugs like 401575 which + // require error page UI to do privileged things, without letting error + // pages have any privilege themselves. + // We can't look for this during onLocationChange since at that point the + // document URI is not yet the about:-uri of the error page. + + let doc = gMultiProcessBrowser ? null : aWebProgress.DOMWindow.document; + if (!gMultiProcessBrowser && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + Components.isSuccessCode(aStatus) && + doc.documentURI.startsWith("about:") && + !doc.documentURI.toLowerCase().startsWith("about:blank") && + !doc.documentElement.hasAttribute("hasBrowserHandlers")) { + // STATE_STOP may be received twice for documents, thus store an + // attribute to ensure handling it just once. + doc.documentElement.setAttribute("hasBrowserHandlers", "true"); + aBrowser.addEventListener("click", BrowserOnClick, true); + aBrowser.addEventListener("pagehide", function onPageHide(event) { + if (event.target.defaultView.frameElement) + return; + aBrowser.removeEventListener("click", BrowserOnClick, true); + aBrowser.removeEventListener("pagehide", onPageHide, true); + if (event.target.documentElement) + event.target.documentElement.removeAttribute("hasBrowserHandlers"); + }, true); + + // We also want to make changes to page UI for unprivileged about pages. + BrowserOnAboutPageLoad(doc); + } + }, + + onLocationChange: function (aBrowser, aWebProgress, aRequest, aLocationURI, + aFlags) { + // Filter out location changes caused by anchor navigation + // or history.push/pop/replaceState. + if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) + return; + + // Only need to call locationChange if the PopupNotifications object + // for this window has already been initialized (i.e. its getter no + // longer exists) + if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get) + PopupNotifications.locationChange(aBrowser); + + gBrowser.getNotificationBox(aBrowser).removeTransientNotifications(); + + // Filter out location changes in sub documents. + if (aWebProgress.isTopLevel) { + // Initialize the click-to-play state. + aBrowser._clickToPlayPluginsActivated = new Map(); + aBrowser._clickToPlayAllPluginsActivated = false; + aBrowser._pluginScriptedState = gPluginHandler.PLUGIN_SCRIPTED_STATE_NONE; + + FullZoom.onLocationChange(aLocationURI, false, aBrowser); + } + }, + + onRefreshAttempted: function (aBrowser, aWebProgress, aURI, aDelay, aSameURI) { + if (gPrefService.getBoolPref("accessibility.blockautorefresh")) { + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let refreshButtonText = + gNavigatorBundle.getString("refreshBlocked.goButton"); + let refreshButtonAccesskey = + gNavigatorBundle.getString("refreshBlocked.goButton.accesskey"); + let message = + gNavigatorBundle.getFormattedString(aSameURI ? "refreshBlocked.refreshLabel" + : "refreshBlocked.redirectLabel", + [brandShortName]); + let docShell = aWebProgress.DOMWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + let notificationBox = gBrowser.getNotificationBox(aBrowser); + let notification = notificationBox.getNotificationWithValue("refresh-blocked"); + if (notification) { + notification.label = message; + notification.refreshURI = aURI; + notification.delay = aDelay; + notification.docShell = docShell; + } else { + let buttons = [{ + label: refreshButtonText, + accessKey: refreshButtonAccesskey, + callback: function (aNotification, aButton) { + var refreshURI = aNotification.docShell + .QueryInterface(Ci.nsIRefreshURI); + refreshURI.forceRefreshURI(aNotification.refreshURI, + aNotification.delay, true); + } + }]; + notification = + notificationBox.appendNotification(message, "refresh-blocked", + "chrome://browser/skin/Info.png", + notificationBox.PRIORITY_INFO_MEDIUM, + buttons); + notification.refreshURI = aURI; + notification.delay = aDelay; + notification.docShell = docShell; + } + return false; + } + return true; + } +} + +function nsBrowserAccess() { } + +nsBrowserAccess.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]), + + openURI: function (aURI, aOpener, aWhere, aContext) { + var newWindow = null; + var isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + + if (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"); + } + switch (aWhere) { + case Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW : + // FIXME: Bug 408379. So how come this doesn't send the + // referrer like the other loads do? + var url = aURI ? aURI.spec : "about:blank"; + // Pass all params to openDialog to ensure that "url" isn't passed through + // loadOneOrMoreURIs, which splits based on "|" + newWindow = openDialog(getBrowserURL(), "_blank", "all,dialog=no", url, null, null, null); + break; + case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB : + let win, needToFocusWin; + + // try the current window. if we're in a popup, fall back on the most recent browser window + if (window.toolbar.visible) + win = window; + else { + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aOpener || window); + win = RecentWindow.getMostRecentBrowserWindow({private: isPrivate}); + needToFocusWin = true; + } + + if (!win) { + // we couldn't find a suitable window, a new one needs to be opened. + return null; + } + + if (isExternal && (!aURI || aURI.spec == "about:blank")) { + win.BrowserOpenTab(); // this also focuses the location bar + win.focus(); + newWindow = win.content; + break; + } + + let loadInBackground = gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground"); + let referrer = aOpener ? makeURI(aOpener.location.href) : null; + + let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", { + referrerURI: referrer, + fromExternal: isExternal, + inBackground: loadInBackground}); + let browser = win.gBrowser.getBrowserForTab(tab); + + if (gPrefService.getBoolPref("browser.tabs.noWindowActivationOnExternal")) { + isExternal = false; // this is a hack, but it works + } + + newWindow = browser.contentWindow; + if (needToFocusWin || (!loadInBackground && isExternal)) + newWindow.focus(); + break; + default : // OPEN_CURRENTWINDOW or an illegal value + newWindow = content; + if (aURI) { + let referrer = aOpener ? makeURI(aOpener.location.href) : null; + let loadflags = isExternal ? + Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL : + Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + gBrowser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null); + } + if (!gPrefService.getBoolPref("browser.tabs.loadDivertedInBackground")) + window.focus(); + } + return newWindow; + }, + + isTabContentWindow: function (aWindow) { + return gBrowser.browsers.some(function (browser) browser.contentWindow == aWindow); + } +} + +function onViewToolbarsPopupShowing(aEvent, aInsertPoint) { + var popup = aEvent.target; + if (popup != aEvent.currentTarget) + return; + + // Empty the menu + for (var i = popup.childNodes.length-1; i >= 0; --i) { + var deadItem = popup.childNodes[i]; + if (deadItem.hasAttribute("toolbarId")) + popup.removeChild(deadItem); + } + + var firstMenuItem = aInsertPoint || popup.firstChild; + + let toolbarNodes = Array.slice(gNavToolbox.childNodes); + toolbarNodes.push(document.getElementById("addon-bar")); + + for (let toolbar of toolbarNodes) { + let toolbarName = toolbar.getAttribute("toolbarname"); + if (toolbarName) { + let menuItem = document.createElement("menuitem"); + let hidingAttribute = toolbar.getAttribute("type") == "menubar" ? + "autohide" : "collapsed"; + menuItem.setAttribute("id", "toggle_" + toolbar.id); + menuItem.setAttribute("toolbarId", toolbar.id); + menuItem.setAttribute("type", "checkbox"); + menuItem.setAttribute("label", toolbarName); + menuItem.setAttribute("checked", toolbar.getAttribute(hidingAttribute) != "true"); + if (popup.id != "appmenu_customizeMenu") + menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey")); + if (popup.id != "toolbar-context-menu") + menuItem.setAttribute("key", toolbar.getAttribute("key")); + + popup.insertBefore(menuItem, firstMenuItem); + + menuItem.addEventListener("command", onViewToolbarCommand, false); + } + } +} + +function onViewToolbarCommand(aEvent) { + var toolbarId = aEvent.originalTarget.getAttribute("toolbarId"); + var toolbar = document.getElementById(toolbarId); + var isVisible = aEvent.originalTarget.getAttribute("checked") == "true"; + setToolbarVisibility(toolbar, isVisible); +} + +function setToolbarVisibility(toolbar, isVisible) { + var hidingAttribute = toolbar.getAttribute("type") == "menubar" ? + "autohide" : "collapsed"; + + toolbar.setAttribute(hidingAttribute, !isVisible); + document.persist(toolbar.id, hidingAttribute); + + // Customizable toolbars - persist the hiding attribute. + if (toolbar.hasAttribute("customindex")) { + var toolbox = toolbar.parentNode; + var name = toolbar.getAttribute("toolbarname"); + if (toolbox.toolbarset) { + try { + // Checking all attributes starting with "toolbar". + Array.prototype.slice.call(toolbox.toolbarset.attributes, 0) + .find(x => { + if (x.name.startsWith("toolbar")) { + var toolbarInfo = x.value; + var infoSplit = toolbarInfo.split(gToolbarInfoSeparators[0]); + if (infoSplit[0] == name) { + infoSplit[1] = [ + infoSplit[1].split(gToolbarInfoSeparators[1], 1), !isVisible + ].join(gToolbarInfoSeparators[1]); + toolbox.toolbarset.setAttribute( + x.name, infoSplit.join(gToolbarInfoSeparators[0])); + document.persist(toolbox.toolbarset.id, x.name); + } + } + }); + } catch (e) { + Components.utils.reportError( + "Customizable toolbars - persist the hiding attribute: " + e); + } + } + } + + PlacesToolbarHelper.init(); + BookmarkingUI.onToolbarVisibilityChange(); + gBrowser.updateWindowResizers(); + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif + + if (isVisible) + ToolbarIconColor.inferFromText(); +} + +var TabsOnTop = { + init: function TabsOnTop_init() { + Services.prefs.addObserver(this._prefName, this, false); +// Pale Moon: Stop Being a Derp, Mozilla (#3) + // Only show the toggle UI if the user disabled tabs on top. +// if (Services.prefs.getBoolPref(this._prefName)) { +// for (let item of document.querySelectorAll("menuitem[command=cmd_ToggleTabsOnTop]")) +// item.parentNode.removeChild(item); +// } + }, + + uninit: function TabsOnTop_uninit() { + Services.prefs.removeObserver(this._prefName, this); + }, + + toggle: function () { + this.enabled = !Services.prefs.getBoolPref(this._prefName); + }, + + syncUI: function () { + let userEnabled = Services.prefs.getBoolPref(this._prefName); + let enabled = userEnabled && gBrowser.tabContainer.visible; + + document.getElementById("cmd_ToggleTabsOnTop") + .setAttribute("checked", userEnabled); + + document.documentElement.setAttribute("tabsontop", enabled); + document.getElementById("navigator-toolbox").setAttribute("tabsontop", enabled); + document.getElementById("TabsToolbar").setAttribute("tabsontop", enabled); + document.getElementById("nav-bar").setAttribute("tabsontop", enabled); + gBrowser.tabContainer.setAttribute("tabsontop", enabled); + TabsInTitlebar.allowedBy("tabs-on-top", enabled); + }, + + get enabled () { + return gNavToolbox.getAttribute("tabsontop") == "true"; + }, + + set enabled (val) { + Services.prefs.setBoolPref(this._prefName, !!val); + return val; + }, + + observe: function (subject, topic, data) { + if (topic == "nsPref:changed") + this.syncUI(); + }, + + _prefName: "browser.tabs.onTop" +} + +var TabsInTitlebar = { + init: function () { +#ifdef CAN_DRAW_IN_TITLEBAR + this._readPref(); + Services.prefs.addObserver(this._prefName, this, false); + + // Don't trust the initial value of the sizemode attribute; wait for + // the resize event (handled in tabbrowser.xml). + this.allowedBy("sizemode", false); + + this._initialized = true; +#endif + }, + + allowedBy: function (condition, allow) { +#ifdef CAN_DRAW_IN_TITLEBAR + if (allow) { + if (condition in this._disallowed) { + delete this._disallowed[condition]; + this._update(); + } + } else { + if (!(condition in this._disallowed)) { + this._disallowed[condition] = null; + this._update(); + } + } +#endif + }, + + get enabled() { + return document.documentElement.getAttribute("tabsintitlebar") == "true"; + }, + +#ifdef CAN_DRAW_IN_TITLEBAR + observe: function (subject, topic, data) { + if (topic == "nsPref:changed") + this._readPref(); + }, + + _initialized: false, + _disallowed: {}, + _prefName: "browser.tabs.drawInTitlebar", + + _readPref: function () { + this.allowedBy("pref", + Services.prefs.getBoolPref(this._prefName)); + }, + + _update: function () { + function $(id) document.getElementById(id); + function rect(ele) ele.getBoundingClientRect(); + + if (!this._initialized || window.fullScreen) + return; + + let allowed = true; + for (let something in this._disallowed) { + allowed = false; + break; + } + + if (allowed == this.enabled) + return; + + let titlebar = $("titlebar"); + + if (allowed) { + let tabsToolbar = $("TabsToolbar"); + +#ifdef MENUBAR_CAN_AUTOHIDE + let appmenuButtonBox = $("appmenu-button-container"); + this._sizePlaceholder("appmenu-button", rect(appmenuButtonBox).width); +#endif + let captionButtonsBox = $("titlebar-buttonbox"); + this._sizePlaceholder("caption-buttons", rect(captionButtonsBox).width); + + let tabsToolbarRect = rect(tabsToolbar); + let titlebarTop = rect($("titlebar-content")).top; + titlebar.style.marginBottom = - Math.min(tabsToolbarRect.top - titlebarTop, + tabsToolbarRect.height) + "px"; + + document.documentElement.setAttribute("tabsintitlebar", "true"); + + if (!this._draghandle) { + let tmp = {}; + Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp); + this._draghandle = new tmp.WindowDraggingElement(tabsToolbar); + this._draghandle.mouseDownCheck = function () { + return !this._dragBindingAlive && TabsInTitlebar.enabled; + }; + } + } else { + document.documentElement.removeAttribute("tabsintitlebar"); + + titlebar.style.marginBottom = ""; + } + + ToolbarIconColor.inferFromText(); + }, + + _sizePlaceholder: function (type, width) { + Array.forEach(document.querySelectorAll(".titlebar-placeholder[type='"+ type +"']"), + function (node) { node.width = width; }); + }, +#endif + + uninit: function () { +#ifdef CAN_DRAW_IN_TITLEBAR + this._initialized = false; + Services.prefs.removeObserver(this._prefName, this); +#endif + } +}; + +#ifdef MENUBAR_CAN_AUTOHIDE +function updateAppButtonDisplay() { + var displayAppButton = + !gInPrintPreviewMode && + window.menubar.visible && + document.getElementById("toolbar-menubar").getAttribute("autohide") == "true"; + +#ifdef CAN_DRAW_IN_TITLEBAR + document.getElementById("titlebar").hidden = !displayAppButton; + + if (displayAppButton) + document.documentElement.setAttribute("chromemargin", "0,2,2,2"); + else + document.documentElement.removeAttribute("chromemargin"); + + TabsInTitlebar.allowedBy("drawing-in-titlebar", displayAppButton); +#else + document.getElementById("appmenu-toolbar-button").hidden = + !displayAppButton; +#endif +} +#endif + +#ifdef CAN_DRAW_IN_TITLEBAR +function onTitlebarMaxClick() { + if (window.windowState == window.STATE_MAXIMIZED) + window.restore(); + else + window.maximize(); +} +#endif + +function displaySecurityInfo() +{ + BrowserPageInfo(null, "securityTab"); +} + +/** + * Opens or closes the sidebar identified by commandID. + * + * @param commandID a string identifying the sidebar to toggle; see the + * note below. (Optional if a sidebar is already open.) + * @param forceOpen boolean indicating whether the sidebar should be + * opened regardless of its current state (optional). + * @note + * We expect to find a xul:broadcaster element with the specified ID. + * The following attributes on that element may be used and/or modified: + * - id (required) the string to match commandID. The convention + * is to use this naming scheme: 'view<sidebar-name>Sidebar'. + * - sidebarurl (required) specifies the URL to load in this sidebar. + * - sidebartitle or label (in that order) specify the title to + * display on the sidebar. + * - checked indicates whether the sidebar is currently displayed. + * Note that toggleSidebar updates this attribute when + * it changes the sidebar's visibility. + * - group this attribute must be set to "sidebar". + */ +function toggleSidebar(commandID, forceOpen) { + + var sidebarBox = document.getElementById("sidebar-box"); + if (!commandID) + commandID = sidebarBox.getAttribute("sidebarcommand"); + + var sidebarBroadcaster = document.getElementById(commandID); + var sidebar = document.getElementById("sidebar"); // xul:browser + var sidebarTitle = document.getElementById("sidebar-title"); + var sidebarSplitter = document.getElementById("sidebar-splitter"); + + if (sidebarBroadcaster.getAttribute("checked") == "true") { + if (!forceOpen) { + // Replace the document currently displayed in the sidebar with about:blank + // so that we can free memory by unloading the page. We need to explicitly + // create a new content viewer because the old one doesn't get destroyed + // until about:blank has loaded (which does not happen as long as the + // element is hidden). + sidebar.setAttribute("src", "about:blank"); + sidebar.docShell.createAboutBlankContentViewer(null); + + sidebarBroadcaster.removeAttribute("checked"); + sidebarBox.setAttribute("sidebarcommand", ""); + sidebarTitle.value = ""; + sidebarBox.hidden = true; + sidebarSplitter.hidden = true; + gBrowser.selectedBrowser.focus(); + } else { + fireSidebarFocusedEvent(); + } + return; + } + + // now we need to show the specified sidebar + + // ..but first update the 'checked' state of all sidebar broadcasters + var broadcasters = document.getElementsByAttribute("group", "sidebar"); + for (let broadcaster of broadcasters) { + // skip elements that observe sidebar broadcasters and random + // other elements + if (broadcaster.localName != "broadcaster") + continue; + + if (broadcaster != sidebarBroadcaster) + broadcaster.removeAttribute("checked"); + else + sidebarBroadcaster.setAttribute("checked", "true"); + } + + sidebarBox.hidden = false; + sidebarSplitter.hidden = false; + + var url = sidebarBroadcaster.getAttribute("sidebarurl"); + var title = sidebarBroadcaster.getAttribute("sidebartitle"); + if (!title) + title = sidebarBroadcaster.getAttribute("label"); + sidebar.setAttribute("src", url); // kick off async load + sidebarBox.setAttribute("sidebarcommand", sidebarBroadcaster.id); + sidebarTitle.value = title; + + // We set this attribute here in addition to setting it on the <browser> + // element itself, because the code in gBrowserInit.onUnload persists this + // attribute, not the "src" of the <browser id="sidebar">. The reason it + // does that is that we want to delay sidebar load a bit when a browser + // window opens. See delayedStartup(). + sidebarBox.setAttribute("src", url); + + if (sidebar.contentDocument.location.href != url) + sidebar.addEventListener("load", sidebarOnLoad, true); + else // older code handled this case, so we do it too + fireSidebarFocusedEvent(); +} + +function sidebarOnLoad(event) { + var sidebar = document.getElementById("sidebar"); + sidebar.removeEventListener("load", sidebarOnLoad, true); + // We're handling the 'load' event before it bubbles up to the usual + // (non-capturing) event handlers. Let it bubble up before firing the + // SidebarFocused event. + setTimeout(fireSidebarFocusedEvent, 0); +} + +/** + * Fire a "SidebarFocused" event on the sidebar's |window| to give the sidebar + * a chance to adjust focus as needed. An additional event is needed, because + * we don't want to focus the sidebar when it's opened on startup or in a new + * window, only when the user opens the sidebar. + */ +function fireSidebarFocusedEvent() { + var sidebar = document.getElementById("sidebar"); + var event = document.createEvent("Events"); + event.initEvent("SidebarFocused", true, false); + sidebar.contentWindow.dispatchEvent(event); +} + +var gHomeButton = { + prefDomain: "browser.startup.homepage", + observe: function (aSubject, aTopic, aPrefName) + { + if (aTopic != "nsPref:changed" || aPrefName != this.prefDomain) + return; + + this.updateTooltip(); + }, + + updateTooltip: function (homeButton) + { + if (!homeButton) + homeButton = document.getElementById("home-button"); + if (homeButton) { + var homePage = this.getHomePage(); + homePage = homePage.replace(/\|/g,', '); + if (homePage.toLowerCase() == "about:home") + homeButton.setAttribute("tooltiptext", homeButton.getAttribute("aboutHomeOverrideTooltip")); + else + homeButton.setAttribute("tooltiptext", homePage); + } + }, + + getHomePage: function () + { + var url; + try { + url = gPrefService.getComplexValue(this.prefDomain, + Components.interfaces.nsIPrefLocalizedString).data; + } catch (e) { + } + + // use this if we can't find the pref + if (!url) { + var configBundle = Services.strings + .createBundle("chrome://branding/locale/browserconfig.properties"); + url = configBundle.GetStringFromName(this.prefDomain); + } + + return url; + }, + + updatePersonalToolbarStyle: function (homeButton) + { + if (!homeButton) + homeButton = document.getElementById("home-button"); + if (homeButton) + homeButton.className = homeButton.parentNode.id == "PersonalToolbar" + || homeButton.parentNode.parentNode.id == "PersonalToolbar" ? + homeButton.className.replace("toolbarbutton-1", "bookmark-item") : + homeButton.className.replace("bookmark-item", "toolbarbutton-1"); + } +}; + +/** + * Gets the selected text in the active browser. Leading and trailing + * whitespace is removed, and consecutive whitespace is replaced by a single + * space. A maximum of 150 characters will be returned, regardless of the value + * of aCharLen. + * + * @param aCharLen + * The maximum number of characters to return. + */ +function getBrowserSelection(aCharLen) { + // selections of more than 150 characters aren't useful + const kMaxSelectionLen = 150; + const charLen = Math.min(aCharLen || kMaxSelectionLen, kMaxSelectionLen); + let commandDispatcher = document.commandDispatcher; + + var focusedWindow = commandDispatcher.focusedWindow; + var selection = focusedWindow.getSelection().toString(); + // try getting a selected text in text input. + if (!selection) { + let element = commandDispatcher.focusedElement; + var isOnTextInput = function isOnTextInput(elem) { + // we avoid to return a value if a selection is in password field. + // ref. bug 565717 + return elem instanceof HTMLTextAreaElement || + (elem instanceof HTMLInputElement && elem.mozIsTextField(true)); + }; + + if (isOnTextInput(element)) { + selection = element.QueryInterface(Ci.nsIDOMNSEditableElement) + .editor.selection.toString(); + } + } + + if (selection) { + if (selection.length > charLen) { + // only use the first charLen important chars. see bug 221361 + var pattern = new RegExp("^(?:\\s*.){0," + charLen + "}"); + pattern.test(selection); + selection = RegExp.lastMatch; + } + + selection = selection.trim().replace(/\s+/g, " "); + + if (selection.length > charLen) + selection = selection.substr(0, charLen); + } + return selection; +} + +var gWebPanelURI; +function openWebPanel(aTitle, aURI) +{ + // Ensure that the web panels sidebar is open. + toggleSidebar('viewWebPanelsSidebar', true); + + // Set the title of the panel. + document.getElementById("sidebar-title").value = aTitle; + + // Tell the Web Panels sidebar to load the bookmark. + var sidebar = document.getElementById("sidebar"); + if (sidebar.docShell && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser')) { + sidebar.contentWindow.loadWebPanel(aURI); + if (gWebPanelURI) { + gWebPanelURI = ""; + sidebar.removeEventListener("load", asyncOpenWebPanel, true); + } + } + else { + // The panel is still being constructed. Attach an onload handler. + if (!gWebPanelURI) + sidebar.addEventListener("load", asyncOpenWebPanel, true); + gWebPanelURI = aURI; + } +} + +function asyncOpenWebPanel(event) +{ + var sidebar = document.getElementById("sidebar"); + if (gWebPanelURI && sidebar.contentDocument && sidebar.contentDocument.getElementById('web-panels-browser')) + sidebar.contentWindow.loadWebPanel(gWebPanelURI); + gWebPanelURI = ""; + sidebar.removeEventListener("load", asyncOpenWebPanel, true); +} + +/* + * - [ Dependencies ] --------------------------------------------------------- + * utilityOverlay.js: + * - gatherTextUnder + */ + +/** + * Extracts linkNode and href for the current click target. + * + * @param event + * The click event. + * @return [href, linkNode]. + * + * @note linkNode will be null if the click wasn't on an anchor + * element (or XLink). + */ +function hrefAndLinkNodeForClickEvent(event) +{ + function isHTMLLink(aNode) + { + // Be consistent with what nsContextMenu.js does. + return ((aNode instanceof HTMLAnchorElement && aNode.href) || + (aNode instanceof HTMLAreaElement && aNode.href) || + aNode instanceof HTMLLinkElement); + } + + let node = event.target; + while (node && !isHTMLLink(node)) { + node = node.parentNode; + } + + if (node) + return [node.href, node]; + + // If there is no linkNode, try simple XLink. + let href, baseURI; + node = event.target; + while (node && !href) { + if (node.nodeType == Node.ELEMENT_NODE && + (node.localName == "a" || + node.namespaceURI == "http://www.w3.org/1998/Math/MathML")) { + href = node.getAttributeNS("http://www.w3.org/1999/xlink", "href"); + if (href) { + baseURI = node.baseURI; + break; + } + } + node = node.parentNode; + } + + // In case of XLink, we don't return the node we got href from since + // callers expect <a>-like elements. + return [href ? makeURLAbsolute(baseURI, href) : null, null]; +} + +/** + * Called whenever the user clicks in the content area. + * + * @param event + * The click event. + * @param isPanelClick + * Whether the event comes from a web panel. + * @note default event is prevented if the click is handled. + */ +function contentAreaClick(event, isPanelClick) +{ + if (!event.isTrusted || event.defaultPrevented || event.button == 2) + return; + + let [href, linkNode] = hrefAndLinkNodeForClickEvent(event); + if (!href) { + // Not a link, handle middle mouse navigation. + if (event.button == 1 && + gPrefService.getBoolPref("middlemouse.contentLoadURL") && + !gPrefService.getBoolPref("general.autoScroll")) { + middleMousePaste(event); + event.preventDefault(); + } + return; + } + + // This code only applies if we have a linkNode (i.e. clicks on real anchor + // elements, as opposed to XLink). + if (linkNode && event.button == 0 && + !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) { + // A Web panel's links should target the main content area. Do this + // if no modifier keys are down and if there's no target or the target + // equals _main (the IE convention) or _content (the Mozilla convention). + let target = linkNode.target; + let mainTarget = !target || target == "_content" || target == "_main"; + if (isPanelClick && mainTarget) { + // javascript and data links should be executed in the current browser. + if (linkNode.getAttribute("onclick") || + href.startsWith("javascript:") || + href.startsWith("data:")) + return; + + try { + urlSecurityCheck(href, linkNode.ownerDocument.nodePrincipal); + } + catch(ex) { + // Prevent loading unsecure destinations. + event.preventDefault(); + return; + } + + loadURI(href, null, null, false); + event.preventDefault(); + return; + } + + if (linkNode.getAttribute("rel") == "sidebar") { + // This is the Opera convention for a special link that, when clicked, + // allows to add a sidebar panel. The link's title attribute contains + // the title that should be used for the sidebar panel. + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: makeURI(href) + , title: linkNode.getAttribute("title") + , loadBookmarkInSidebar: true + , hiddenRows: [ "description" + , "location" + , "keyword" ] + }, window); + event.preventDefault(); + return; + } + } + + handleLinkClick(event, href, linkNode); + + // Mark the page as a user followed link. This is done so that history can + // distinguish automatic embed visits from user activated ones. For example + // pages loaded in frames are embed visits and lost with the session, while + // visits across frames should be preserved. + try { + if (!PrivateBrowsingUtils.isWindowPrivate(window)) + PlacesUIUtils.markPageAsFollowedLink(href); + } catch (ex) { /* Skip invalid URIs. */ } +} + +/** + * Handles clicks on links. + * + * @return true if the click event was handled, false otherwise. + */ +function handleLinkClick(event, href, linkNode) { + if (event.button == 2) // right click + return false; + + var where = whereToOpenLink(event); + if (where == "current") + return false; + + var doc = event.target.ownerDocument; + + if (where == "save") { + saveURL(href, linkNode ? gatherTextUnder(linkNode) : "", null, true, + true, doc.documentURIObject, doc); + event.preventDefault(); + return true; + } + + urlSecurityCheck(href, doc.nodePrincipal); + openLinkIn(href, where, { referrerURI: doc.documentURIObject, + charset: doc.characterSet }); + 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, ""); + + let mayInheritPrincipal = { value: false }; + let url = getShortcutOrURI(clipboard, mayInheritPrincipal); + try { + makeURI(url); + } catch (ex) { + // Not a valid URI. + return; + } + + 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); + } + + openUILink(url, event, + { ignoreButton: true, + disallowInheritPrincipal: !mayInheritPrincipal.value }); + + event.stopPropagation(); +} + +function handleDroppedLink(event, url, name) +{ + let postData = { }; + let uri = getShortcutOrURI(url, postData); + if (uri) + loadURI(uri, null, postData.value, false); + + // Keep the event from being handled by the dragDrop listeners + // built-in to goanna if they happen to be above us. + event.preventDefault(); +}; + +function MultiplexHandler(event) +{ try { + var node = event.target; + var name = node.getAttribute('name'); + + if (name == 'detectorGroup') { + BrowserCharsetReload(); + SelectDetector(event, false); + } else if (name == 'charsetGroup') { + var charset = node.getAttribute('id'); + charset = charset.substring(charset.indexOf('charset.') + 'charset.'.length); + BrowserSetForcedCharacterSet(charset); + } else if (name == 'charsetCustomize') { + //do nothing - please remove this else statement, once the charset prefs moves to the pref window + } else { + BrowserSetForcedCharacterSet(node.getAttribute('id')); + } + } catch(ex) { alert(ex); } +} + +function SelectDetector(event, doReload) +{ + var uri = event.target.getAttribute("id"); + var prefvalue = uri.substring(uri.indexOf('chardet.') + 'chardet.'.length); + if ("off" == prefvalue) { // "off" is special value to turn off the detectors + prefvalue = ""; + } + + try { + var str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + + str.data = prefvalue; + gPrefService.setComplexValue("intl.charset.detector", Ci.nsISupportsString, str); + if (doReload) + window.content.location.reload(); + } + catch (ex) { + dump("Failed to set the intl.charset.detector preference.\n"); + } +} + +function BrowserSetForcedCharacterSet(aCharset) +{ + gBrowser.docShell.charset = aCharset; + // Save the forced character-set + if (!PrivateBrowsingUtils.isWindowPrivate(window)) + PlacesUtils.setCharsetForURI(getWebNavigation().currentURI, aCharset); + BrowserCharsetReload(); +} + +function BrowserCharsetReload() +{ + BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); +} + +function charsetMenuGetElement(parent, id) { + return parent.getElementsByAttribute("id", id)[0]; +} + +function UpdateCurrentCharset(target) { + // extract the charset from DOM + var wnd = document.commandDispatcher.focusedWindow; + if ((window == wnd) || (wnd == null)) wnd = window.content; + + // Uncheck previous item + if (gPrevCharset) { + var pref_item = charsetMenuGetElement(target, "charset." + gPrevCharset); + if (pref_item) + pref_item.setAttribute('checked', 'false'); + } + + var menuitem = charsetMenuGetElement(target, "charset." + wnd.document.characterSet); + if (menuitem) { + menuitem.setAttribute('checked', 'true'); + } +} + +function UpdateCharsetDetector(target) { + var prefvalue; + + try { + prefvalue = gPrefService.getComplexValue("intl.charset.detector", Ci.nsIPrefLocalizedString).data; + } + catch (ex) {} + + if (!prefvalue) + prefvalue = "off"; + + var menuitem = charsetMenuGetElement(target, "chardet." + prefvalue); + if (menuitem) + menuitem.setAttribute("checked", "true"); +} + +function UpdateMenus(event) { + UpdateCurrentCharset(event.target); + UpdateCharsetDetector(event.target); +} + +function charsetLoadListener() { + var charset = window.content.document.characterSet; + + if (charset.length > 0 && (charset != gLastBrowserCharset)) { + gPrevCharset = gLastBrowserCharset; + gLastBrowserCharset = charset; + } +} + + +var gPageStyleMenu = { + + _getAllStyleSheets: function (frameset) { + var styleSheetsArray = Array.slice(frameset.document.styleSheets); + for (let i = 0; i < frameset.frames.length; i++) { + let frameSheets = this._getAllStyleSheets(frameset.frames[i]); + styleSheetsArray = styleSheetsArray.concat(frameSheets); + } + return styleSheetsArray; + }, + + fillPopup: function (menuPopup) { + var noStyle = menuPopup.firstChild; + var persistentOnly = noStyle.nextSibling; + var sep = persistentOnly.nextSibling; + while (sep.nextSibling) + menuPopup.removeChild(sep.nextSibling); + + var styleSheets = this._getAllStyleSheets(window.content); + var currentStyleSheets = {}; + var styleDisabled = getMarkupDocumentViewer().authorStyleDisabled; + var haveAltSheets = false; + var altStyleSelected = false; + + for (let currentStyleSheet of styleSheets) { + if (!currentStyleSheet.title) + continue; + + // Skip any stylesheets whose media attribute doesn't match. + if (currentStyleSheet.media.length > 0) { + let mediaQueryList = currentStyleSheet.media.mediaText; + if (!window.content.matchMedia(mediaQueryList).matches) + continue; + } + + if (!currentStyleSheet.disabled) + altStyleSelected = true; + + haveAltSheets = true; + + let lastWithSameTitle = null; + if (currentStyleSheet.title in currentStyleSheets) + lastWithSameTitle = currentStyleSheets[currentStyleSheet.title]; + + if (!lastWithSameTitle) { + let menuItem = document.createElement("menuitem"); + menuItem.setAttribute("type", "radio"); + menuItem.setAttribute("label", currentStyleSheet.title); + menuItem.setAttribute("data", currentStyleSheet.title); + menuItem.setAttribute("checked", !currentStyleSheet.disabled && !styleDisabled); + menuItem.setAttribute("oncommand", "gPageStyleMenu.switchStyleSheet(this.getAttribute('data'));"); + menuPopup.appendChild(menuItem); + currentStyleSheets[currentStyleSheet.title] = menuItem; + } else if (currentStyleSheet.disabled) { + lastWithSameTitle.removeAttribute("checked"); + } + } + + noStyle.setAttribute("checked", styleDisabled); + persistentOnly.setAttribute("checked", !altStyleSelected && !styleDisabled); + persistentOnly.hidden = (window.content.document.preferredStyleSheetSet) ? haveAltSheets : false; + sep.hidden = (noStyle.hidden && persistentOnly.hidden) || !haveAltSheets; + }, + + _stylesheetInFrame: function (frame, title) { + return Array.some(frame.document.styleSheets, + function (stylesheet) stylesheet.title == title); + }, + + _stylesheetSwitchFrame: function (frame, title) { + var docStyleSheets = frame.document.styleSheets; + + for (let i = 0; i < docStyleSheets.length; ++i) { + let docStyleSheet = docStyleSheets[i]; + + if (docStyleSheet.title) + docStyleSheet.disabled = (docStyleSheet.title != title); + else if (docStyleSheet.disabled) + docStyleSheet.disabled = false; + } + }, + + _stylesheetSwitchAll: function (frameset, title) { + if (!title || this._stylesheetInFrame(frameset, title)) + this._stylesheetSwitchFrame(frameset, title); + + for (let i = 0; i < frameset.frames.length; i++) + this._stylesheetSwitchAll(frameset.frames[i], title); + }, + + switchStyleSheet: function (title, contentWindow) { + getMarkupDocumentViewer().authorStyleDisabled = false; + this._stylesheetSwitchAll(contentWindow || content, title); + }, + + disableStyle: function () { + getMarkupDocumentViewer().authorStyleDisabled = true; + }, +}; + +/* Legacy global page-style functions */ +var getAllStyleSheets = gPageStyleMenu._getAllStyleSheets.bind(gPageStyleMenu); +var stylesheetFillPopup = gPageStyleMenu.fillPopup.bind(gPageStyleMenu); +function stylesheetSwitchAll(contentWindow, title) { + gPageStyleMenu.switchStyleSheet(title, contentWindow); +} +function setStyleDisabled(disabled) { + if (disabled) + gPageStyleMenu.disableStyle(); +} + + +var BrowserOffline = { + _inited: false, + + ///////////////////////////////////////////////////////////////////////////// + // BrowserOffline Public Methods + init: function () + { + if (!this._uiElement) + this._uiElement = document.getElementById("workOfflineMenuitemState"); + + Services.obs.addObserver(this, "network:offline-status-changed", false); + + this._updateOfflineUI(Services.io.offline); + + this._inited = true; + }, + + uninit: function () + { + if (this._inited) { + Services.obs.removeObserver(this, "network:offline-status-changed"); + } + }, + + toggleOfflineStatus: function () + { + var ioService = Services.io; + + // Stop automatic management of the offline status + try { + ioService.manageOfflineStatus = false; + } catch (ex) { + } + + if (!ioService.offline && !this._canGoOffline()) { + this._updateOfflineUI(false); + return; + } + + ioService.offline = !ioService.offline; + }, + + ///////////////////////////////////////////////////////////////////////////// + // nsIObserver + observe: function (aSubject, aTopic, aState) + { + if (aTopic != "network:offline-status-changed") + return; + + this._updateOfflineUI(aState == "offline"); + }, + + ///////////////////////////////////////////////////////////////////////////// + // BrowserOffline Implementation Methods + _canGoOffline: function () + { + try { + var cancelGoOffline = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelGoOffline, "offline-requested", null); + + // Something aborted the quit process. + if (cancelGoOffline.data) + return false; + } + catch (ex) { + } + + return true; + }, + + _uiElement: null, + _updateOfflineUI: function (aOffline) + { + var offlineLocked = gPrefService.prefIsLocked("network.online"); + if (offlineLocked) + this._uiElement.setAttribute("disabled", "true"); + + this._uiElement.setAttribute("checked", aOffline); + } +}; + +var OfflineApps = { + ///////////////////////////////////////////////////////////////////////////// + // OfflineApps Public Methods + init: function () + { + Services.obs.addObserver(this, "offline-cache-update-completed", false); + }, + + uninit: function () + { + Services.obs.removeObserver(this, "offline-cache-update-completed"); + }, + + handleEvent: function(event) { + if (event.type == "MozApplicationManifest") { + this.offlineAppRequested(event.originalTarget.defaultView); + } + }, + + ///////////////////////////////////////////////////////////////////////////// + // OfflineApps Implementation Methods + + // XXX: _getBrowserWindowForContentWindow and _getBrowserForContentWindow + // were taken from browser/components/feeds/src/WebContentConverter. + _getBrowserWindowForContentWindow: function(aContentWindow) { + return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .wrappedJSObject; + }, + + _getBrowserForContentWindow: function(aBrowserWindow, aContentWindow) { + // This depends on pseudo APIs of browser.js and tabbrowser.xml + aContentWindow = aContentWindow.top; + var browsers = aBrowserWindow.gBrowser.browsers; + for (let browser of browsers) { + if (browser.contentWindow == aContentWindow) + return browser; + } + // handle other browser/iframe elements that may need popupnotifications + let browser = aContentWindow + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + if (browser.getAttribute("popupnotificationanchor")) + return browser; + return null; + }, + + _getManifestURI: function(aWindow) { + if (!aWindow.document.documentElement) + return null; + + var attr = aWindow.document.documentElement.getAttribute("manifest"); + if (!attr) + return null; + + try { + var contentURI = makeURI(aWindow.location.href, null, null); + return makeURI(attr, aWindow.document.characterSet, contentURI); + } catch (e) { + return null; + } + }, + + // A cache update isn't tied to a specific window. Try to find + // the best browser in which to warn the user about space usage + _getBrowserForCacheUpdate: function(aCacheUpdate) { + // Prefer the current browser + var uri = this._getManifestURI(content); + if (uri && uri.equals(aCacheUpdate.manifestURI)) { + return gBrowser.selectedBrowser; + } + + var browsers = gBrowser.browsers; + for (let browser of browsers) { + uri = this._getManifestURI(browser.contentWindow); + if (uri && uri.equals(aCacheUpdate.manifestURI)) { + return browser; + } + } + + // is this from a non-tab browser/iframe? + browsers = document.querySelectorAll("iframe[popupnotificationanchor] | browser[popupnotificationanchor]"); + for (let browser of browsers) { + uri = this._getManifestURI(browser.contentWindow); + if (uri && uri.equals(aCacheUpdate.manifestURI)) { + return browser; + } + } + + return null; + }, + + _warnUsage: function(aBrowser, aURI) { + if (!aBrowser) + return; + + let mainAction = { + label: gNavigatorBundle.getString("offlineApps.manageUsage"), + accessKey: gNavigatorBundle.getString("offlineApps.manageUsageAccessKey"), + callback: OfflineApps.manage + }; + + let warnQuota = gPrefService.getIntPref("offline-apps.quota.warn"); + let message = gNavigatorBundle.getFormattedString("offlineApps.usage", + [ aURI.host, + warnQuota / 1024 ]); + + let anchorID = "indexedDB-notification-icon"; + PopupNotifications.show(aBrowser, "offline-app-usage", message, + anchorID, mainAction); + + // Now that we've warned once, prevent the warning from showing up + // again. + Services.perms.add(aURI, "offline-app", + Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN); + }, + + // XXX: duplicated in preferences/advanced.js + _getOfflineAppUsage: function (host, groups) + { + var cacheService = Cc["@mozilla.org/network/application-cache-service;1"]. + getService(Ci.nsIApplicationCacheService); + if (!groups) + groups = cacheService.getGroups(); + + var usage = 0; + for (let group of groups) { + var uri = Services.io.newURI(group, null, null); + if (uri.asciiHost == host) { + var cache = cacheService.getActiveCache(group); + usage += cache.usage; + } + } + + return usage; + }, + + _checkUsage: function(aURI) { + // if the user has already allowed excessive usage, don't bother checking + if (Services.perms.testExactPermission(aURI, "offline-app") != + Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN) { + var usage = this._getOfflineAppUsage(aURI.asciiHost); + var warnQuota = gPrefService.getIntPref("offline-apps.quota.warn"); + if (usage >= warnQuota * 1024) { + return true; + } + } + + return false; + }, + + offlineAppRequested: function(aContentWindow) { + if (!gPrefService.getBoolPref("browser.offline-apps.notify")) { + return; + } + + let browserWindow = this._getBrowserWindowForContentWindow(aContentWindow); + let browser = this._getBrowserForContentWindow(browserWindow, + aContentWindow); + + let currentURI = aContentWindow.document.documentURIObject; + + // don't bother showing UI if the user has already made a decision + if (Services.perms.testExactPermission(currentURI, "offline-app") != Services.perms.UNKNOWN_ACTION) + return; + + try { + if (gPrefService.getBoolPref("offline-apps.allow_by_default")) { + // all pages can use offline capabilities, no need to ask the user + return; + } + } catch(e) { + // this pref isn't set by default, ignore failures + } + + let host = currentURI.asciiHost; + let notificationID = "offline-app-requested-" + host; + let notification = PopupNotifications.getNotification(notificationID, browser); + + if (notification) { + notification.options.documents.push(aContentWindow.document); + } else { + let mainAction = { + label: gNavigatorBundle.getString("offlineApps.allow"), + accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"), + callback: function() { + for (let document of notification.options.documents) { + OfflineApps.allowSite(document); + } + } + }; + let secondaryActions = [{ + label: gNavigatorBundle.getString("offlineApps.never"), + accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"), + callback: function() { + for (let document of notification.options.documents) { + OfflineApps.disallowSite(document); + } + } + }]; + let message = gNavigatorBundle.getFormattedString("offlineApps.available", + [ host ]); + let anchorID = "indexedDB-notification-icon"; + let options= { + documents : [ aContentWindow.document ] + }; + notification = PopupNotifications.show(browser, notificationID, message, + anchorID, mainAction, + secondaryActions, options); + } + }, + + allowSite: function(aDocument) { + Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.ALLOW_ACTION); + + // When a site is enabled while loading, manifest resources will + // start fetching immediately. This one time we need to do it + // ourselves. + this._startFetching(aDocument); + }, + + disallowSite: function(aDocument) { + Services.perms.add(aDocument.documentURIObject, "offline-app", Services.perms.DENY_ACTION); + }, + + manage: function() { + openAdvancedPreferences("networkTab"); + }, + + _startFetching: function(aDocument) { + if (!aDocument.documentElement) + return; + + var manifest = aDocument.documentElement.getAttribute("manifest"); + if (!manifest) + return; + + var manifestURI = makeURI(manifest, aDocument.characterSet, + aDocument.documentURIObject); + + var updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]. + getService(Ci.nsIOfflineCacheUpdateService); + updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject, window); + }, + + ///////////////////////////////////////////////////////////////////////////// + // nsIObserver + observe: function (aSubject, aTopic, aState) + { + if (aTopic == "offline-cache-update-completed") { + var cacheUpdate = aSubject.QueryInterface(Ci.nsIOfflineCacheUpdate); + + var uri = cacheUpdate.manifestURI; + if (OfflineApps._checkUsage(uri)) { + var browser = this._getBrowserForCacheUpdate(cacheUpdate); + if (browser) { + OfflineApps._warnUsage(browser, cacheUpdate.manifestURI); + } + } + } + } +}; + +var IndexedDBPromptHelper = { + _permissionsPrompt: "indexedDB-permissions-prompt", + _permissionsResponse: "indexedDB-permissions-response", + + _quotaPrompt: "indexedDB-quota-prompt", + _quotaResponse: "indexedDB-quota-response", + _quotaCancel: "indexedDB-quota-cancel", + + _notificationIcon: "indexedDB-notification-icon", + + init: + function IndexedDBPromptHelper_init() { + Services.obs.addObserver(this, this._permissionsPrompt, false); + Services.obs.addObserver(this, this._quotaPrompt, false); + Services.obs.addObserver(this, this._quotaCancel, false); + }, + + uninit: + function IndexedDBPromptHelper_uninit() { + Services.obs.removeObserver(this, this._permissionsPrompt); + Services.obs.removeObserver(this, this._quotaPrompt); + Services.obs.removeObserver(this, this._quotaCancel); + }, + + observe: + function IndexedDBPromptHelper_observe(subject, topic, data) { + if (topic != this._permissionsPrompt && + topic != this._quotaPrompt && + topic != this._quotaCancel) { + throw new Error("Unexpected topic!"); + } + + var requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor); + + var contentWindow = requestor.getInterface(Ci.nsIDOMWindow); + var contentDocument = contentWindow.document; + var browserWindow = + OfflineApps._getBrowserWindowForContentWindow(contentWindow); + + if (browserWindow != window) { + // Must belong to some other window. + return; + } + + var browser = + OfflineApps._getBrowserForContentWindow(browserWindow, contentWindow); + + var host = contentDocument.documentURIObject.asciiHost; + + var message; + var responseTopic; + if (topic == this._permissionsPrompt) { + message = gNavigatorBundle.getFormattedString("offlineApps.available", + [ host ]); + responseTopic = this._permissionsResponse; + } + else if (topic == this._quotaPrompt) { + message = gNavigatorBundle.getFormattedString("indexedDB.usage", + [ host, data ]); + responseTopic = this._quotaResponse; + } + else if (topic == this._quotaCancel) { + responseTopic = this._quotaResponse; + } + + const hiddenTimeoutDuration = 30000; // 30 seconds + const firstTimeoutDuration = 300000; // 5 minutes + + var timeoutId; + + var observer = requestor.getInterface(Ci.nsIObserver); + + var mainAction = { + label: gNavigatorBundle.getString("offlineApps.allow"), + accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"), + callback: function() { + clearTimeout(timeoutId); + observer.observe(null, responseTopic, + Ci.nsIPermissionManager.ALLOW_ACTION); + } + }; + + var secondaryActions = [ + { + label: gNavigatorBundle.getString("offlineApps.never"), + accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"), + callback: function() { + clearTimeout(timeoutId); + observer.observe(null, responseTopic, + Ci.nsIPermissionManager.DENY_ACTION); + } + } + ]; + + // This will be set to the result of PopupNotifications.show() below, or to + // the result of PopupNotifications.getNotification() if this is a + // quotaCancel notification. + var notification; + + function timeoutNotification() { + // Remove the notification. + if (notification) { + notification.remove(); + } + + // Clear all of our timeout stuff. We may be called directly, not just + // when the timeout actually elapses. + clearTimeout(timeoutId); + + // And tell the page that the popup timed out. + observer.observe(null, responseTopic, + Ci.nsIPermissionManager.UNKNOWN_ACTION); + } + + var options = { + eventCallback: function(state) { + // Don't do anything if the timeout has not been set yet. + if (!timeoutId) { + return; + } + + // If the popup is being dismissed start the short timeout. + if (state == "dismissed") { + clearTimeout(timeoutId); + timeoutId = setTimeout(timeoutNotification, hiddenTimeoutDuration); + return; + } + + // If the popup is being re-shown then clear the timeout allowing + // unlimited waiting. + if (state == "shown") { + clearTimeout(timeoutId); + } + } + }; + + if (topic == this._quotaCancel) { + notification = PopupNotifications.getNotification(this._quotaPrompt, + browser); + timeoutNotification(); + return; + } + + notification = PopupNotifications.show(browser, topic, message, + this._notificationIcon, mainAction, + secondaryActions, options); + + // Set the timeoutId after the popup has been created, and use the long + // timeout value. If the user doesn't notice the popup after this amount of + // time then it is most likely not visible and we want to alert the page. + timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration); + } +}; + +function WindowIsClosing() +{ + let event = document.createEvent("Events"); + event.initEvent("WindowIsClosing", true, true); + if (!window.dispatchEvent(event)) + return false; + + if (!closeWindow(false, warnAboutClosingWindow)) + return false; + + for (let browser of gBrowser.browsers) { + let ds = browser.docShell; + if (ds.contentViewer && !ds.contentViewer.permitUnload()) + return false; + } + + return true; +} + +/** + * Checks if this is the last full *browser* window around. If it is, this will + * be communicated like quitting. Otherwise, we warn about closing multiple tabs. + * @returns true if closing can proceed, false if it got cancelled. + */ +function warnAboutClosingWindow() { + // Popups aren't considered full browser windows. + let isPBWindow = PrivateBrowsingUtils.isWindowPrivate(window); + if (!isPBWindow && !toolbar.visible) + return gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); + + // Figure out if there's at least one other browser window around. + let e = Services.wm.getEnumerator("navigator:browser"); + let otherPBWindowExists = false; + let nonPopupPresent = false; + while (e.hasMoreElements()) { + let win = e.getNext(); + if (win != window) { + if (isPBWindow && PrivateBrowsingUtils.isWindowPrivate(win)) + otherPBWindowExists = true; + if (win.toolbar.visible) + nonPopupPresent = true; + // If the current window is not in private browsing mode we don't need to + // look for other pb windows, we can leave the loop when finding the + // first non-popup window. If however the current window is in private + // browsing mode then we need at least one other pb and one non-popup + // window to break out early. + if ((!isPBWindow || otherPBWindowExists) && nonPopupPresent) + break; + } + } + + if (isPBWindow && !otherPBWindowExists) { + let exitingCanceled = Cc["@mozilla.org/supports-PRBool;1"]. + createInstance(Ci.nsISupportsPRBool); + exitingCanceled.data = false; + Services.obs.notifyObservers(exitingCanceled, + "last-pb-context-exiting", + null); + if (exitingCanceled.data) + return false; + } + + if (nonPopupPresent) { + return isPBWindow || gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); + } + + let os = Services.obs; + + let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"]. + createInstance(Ci.nsISupportsPRBool); + os.notifyObservers(closingCanceled, + "browser-lastwindow-close-requested", null); + if (closingCanceled.data) + return false; + + os.notifyObservers(null, "browser-lastwindow-close-granted", null); + +#ifdef XP_MACOSX + // OS X doesn't quit the application when the last window is closed, but keeps + // the session alive. Hence don't prompt users to save tabs, but warn about + // closing multiple tabs. + return isPBWindow || gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); +#else + return true; +#endif +} + +var MailIntegration = { + sendLinkForWindow: function (aWindow) { + this.sendMessage(aWindow.location.href, + aWindow.document.title); + }, + + sendMessage: function (aBody, aSubject) { + // generate a mailto url based on the url and the url's title + var mailtoUrl = "mailto:"; + if (aBody) { + mailtoUrl += "?body=" + encodeURIComponent(aBody); + mailtoUrl += "&subject=" + encodeURIComponent(aSubject); + } + + var uri = makeURI(mailtoUrl); + + // now pass this uri to the operating system + this._launchExternalUrl(uri); + }, + + // a generic method which can be used to pass arbitrary urls to the operating + // system. + // aURL --> a nsIURI which represents the url to launch + _launchExternalUrl: function (aURL) { + var extProtocolSvc = + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService); + if (extProtocolSvc) + extProtocolSvc.loadUrl(aURL); + } +}; + +function BrowserOpenAddonsMgr(aView) { + if (aView) { + let emWindow; + let browserWindow; + + var receivePong = function receivePong(aSubject, aTopic, aData) { + let browserWin = aSubject.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + if (!emWindow || browserWin == window /* favor the current window */) { + emWindow = aSubject; + browserWindow = browserWin; + } + } + Services.obs.addObserver(receivePong, "EM-pong", false); + Services.obs.notifyObservers(null, "EM-ping", ""); + Services.obs.removeObserver(receivePong, "EM-pong"); + + if (emWindow) { + emWindow.loadView(aView); + browserWindow.gBrowser.selectedTab = + browserWindow.gBrowser._getTabForContentWindow(emWindow); + emWindow.focus(); + return; + } + } + + var newLoad = !switchToTabHavingURI("about:addons", true); + + if (aView) { + // This must be a new load, else the ping/pong would have + // found the window above. + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + Services.obs.removeObserver(observer, aTopic); + aSubject.loadView(aView); + }, "EM-loaded", false); + } +} + +function BrowserOpenPermissionsMgr() { + switchToTabHavingURI("about:permissions", true); +} + +function AddKeywordForSearchField() { + var node = document.popupNode; + + var charset = node.ownerDocument.characterSet; + + var docURI = makeURI(node.ownerDocument.URL, + charset); + + var formURI = makeURI(node.form.getAttribute("action"), + charset, + docURI); + + var spec = formURI.spec; + + var isURLEncoded = + (node.form.method.toUpperCase() == "POST" + && (node.form.enctype == "application/x-www-form-urlencoded" || + node.form.enctype == "")); + + var title = gNavigatorBundle.getFormattedString("addKeywordTitleAutoFill", + [node.ownerDocument.title]); + var description = PlacesUIUtils.getDescriptionFromDocument(node.ownerDocument); + + var formData = []; + + function escapeNameValuePair(aName, aValue, aIsFormUrlEncoded) { + if (aIsFormUrlEncoded) + return escape(aName + "=" + aValue); + else + return escape(aName) + "=" + escape(aValue); + } + + for (let el of node.form.elements) { + if (!el.type) // happens with fieldsets + continue; + + if (el == node) { + formData.push((isURLEncoded) ? escapeNameValuePair(el.name, "%s", true) : + // Don't escape "%s", just append + escapeNameValuePair(el.name, "", false) + "%s"); + continue; + } + + let type = el.type.toLowerCase(); + + if (((el instanceof HTMLInputElement && el.mozIsTextField(true)) || + type == "hidden" || type == "textarea") || + ((type == "checkbox" || type == "radio") && el.checked)) { + formData.push(escapeNameValuePair(el.name, el.value, isURLEncoded)); + } else if (el instanceof HTMLSelectElement && el.selectedIndex >= 0) { + for (var j=0; j < el.options.length; j++) { + if (el.options[j].selected) + formData.push(escapeNameValuePair(el.name, el.options[j].value, + isURLEncoded)); + } + } + } + + var postData; + + if (isURLEncoded) + postData = formData.join("&"); + else + spec += "?" + formData.join("&"); + + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: makeURI(spec) + , title: title + , description: description + , keyword: "" + , postData: postData + , charSet: charset + , hiddenRows: [ "location" + , "description" + , "tags" + , "loadInSidebar" ] + }, window); +} + +function SwitchDocumentDirection(aWindow) { + // document.dir can also be "auto", in which case it won't change + if (aWindow.document.dir == "ltr" || aWindow.document.dir == "") { + aWindow.document.dir = "rtl"; + } else if (aWindow.document.dir == "rtl") { + aWindow.document.dir = "ltr"; + } + for (var run = 0; run < aWindow.frames.length; run++) + SwitchDocumentDirection(aWindow.frames[run]); +} + +function convertFromUnicode(charset, str) +{ + try { + var unicodeConverter = Components + .classes["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); + unicodeConverter.charset = charset; + str = unicodeConverter.ConvertFromUnicode(str); + return str + unicodeConverter.Finish(); + } catch(ex) { + return null; + } +} + +/** + * Re-open a closed tab. + * @param aIndex + * The index of the tab (via nsSessionStore.getClosedTabData) + * @returns a reference to the reopened tab. + */ +function undoCloseTab(aIndex) { + // wallpaper patch to prevent an unnecessary blank tab (bug 343895) + var blankTabToRemove = null; + if (gBrowser.tabs.length == 1 && + !gPrefService.getBoolPref("browser.tabs.autoHide") && + isTabEmpty(gBrowser.selectedTab)) + blankTabToRemove = gBrowser.selectedTab; + + var tab = null; + var ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + if (ss.getClosedTabCount(window) > (aIndex || 0)) { + tab = ss.undoCloseTab(window, aIndex || 0); + + if (blankTabToRemove) + gBrowser.removeTab(blankTabToRemove); + } + + return tab; +} + +/** + * Re-open a closed window. + * @param aIndex + * The index of the window (via nsSessionStore.getClosedWindowData) + * @returns a reference to the reopened window. + */ +function undoCloseWindow(aIndex) { + let ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + let window = null; + if (ss.getClosedWindowCount() > (aIndex || 0)) + window = ss.undoCloseWindow(aIndex || 0); + + return window; +} + +/* + * Determines if a tab is "empty", usually used in the context of determining + * if it's ok to close the tab. + */ +function isTabEmpty(aTab) { + if (aTab.hasAttribute("busy")) + return false; + + let browser = aTab.linkedBrowser; + if (!isBlankPageURL(browser.currentURI.spec)) + return false; + + // Bug 863515 - Make content.opener checks work in electrolysis. + if (!gMultiProcessBrowser && browser.contentWindow.opener) + return false; + + if (browser.sessionHistory && browser.sessionHistory.count >= 2) + return false; + + return true; +} + +#ifdef MOZ_SERVICES_SYNC +function BrowserOpenSyncTabs() { + switchToTabHavingURI("about:sync-tabs", true); +} +#endif + +/** + * Format a URL + * eg: + * echo formatURL("https://addons.mozilla.org/%LOCALE%/%APP%/%VERSION%/"); + * > https://addons.mozilla.org/en-US/firefox/3.0a1/ + * + * Currently supported built-ins are LOCALE, APP, and any value from nsIXULAppInfo, uppercased. + */ +function formatURL(aFormat, aIsPref) { + var formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter); + return aIsPref ? formatter.formatURLPref(aFormat) : formatter.formatURL(aFormat); +} + +/** + * Utility object to handle manipulations of the identity indicators in the UI + */ +var gIdentityHandler = { + // Mode strings used to control CSS display + IDENTITY_MODE_IDENTIFIED : "verifiedIdentity", // High-quality identity information + IDENTITY_MODE_DOMAIN_VERIFIED : "verifiedDomain", // Minimal SSL CA-signed domain verification + IDENTITY_MODE_UNKNOWN : "unknownIdentity", // No trusted identity information + IDENTITY_MODE_MIXED_CONTENT : "unknownIdentity mixedContent", // SSL with unauthenticated content + IDENTITY_MODE_MIXED_ACTIVE_CONTENT : "unknownIdentity mixedContent mixedActiveContent", // SSL with unauthenticated content + IDENTITY_MODE_CHROMEUI : "chromeUI", // Part of the product's UI + + // Cache the most recent SSLStatus and Location seen in checkIdentity + _lastStatus : null, + _lastLocation : null, + _mode : "unknownIdentity", + + // smart getters + get _encryptionLabel () { + delete this._encryptionLabel; + this._encryptionLabel = {}; + this._encryptionLabel[this.IDENTITY_MODE_DOMAIN_VERIFIED] = + gNavigatorBundle.getString("identity.encrypted"); + this._encryptionLabel[this.IDENTITY_MODE_IDENTIFIED] = + gNavigatorBundle.getString("identity.encrypted"); + this._encryptionLabel[this.IDENTITY_MODE_UNKNOWN] = + gNavigatorBundle.getString("identity.unencrypted"); + this._encryptionLabel[this.IDENTITY_MODE_MIXED_CONTENT] = + gNavigatorBundle.getString("identity.mixed_content"); + this._encryptionLabel[this.IDENTITY_MODE_MIXED_ACTIVE_CONTENT] = + gNavigatorBundle.getString("identity.mixed_content"); + return this._encryptionLabel; + }, + get _identityPopup () { + delete this._identityPopup; + return this._identityPopup = document.getElementById("identity-popup"); + }, + get _identityBox () { + delete this._identityBox; + return this._identityBox = document.getElementById("identity-box"); + }, + get _identityPopupContentBox () { + delete this._identityPopupContentBox; + return this._identityPopupContentBox = + document.getElementById("identity-popup-content-box"); + }, + get _identityPopupContentHost () { + delete this._identityPopupContentHost; + return this._identityPopupContentHost = + document.getElementById("identity-popup-content-host"); + }, + get _identityPopupContentOwner () { + delete this._identityPopupContentOwner; + return this._identityPopupContentOwner = + document.getElementById("identity-popup-content-owner"); + }, + get _identityPopupContentSupp () { + delete this._identityPopupContentSupp; + return this._identityPopupContentSupp = + document.getElementById("identity-popup-content-supplemental"); + }, + get _identityPopupContentVerif () { + delete this._identityPopupContentVerif; + return this._identityPopupContentVerif = + document.getElementById("identity-popup-content-verifier"); + }, + get _identityPopupEncLabel () { + delete this._identityPopupEncLabel; + return this._identityPopupEncLabel = + document.getElementById("identity-popup-encryption-label"); + }, + get _identityIconLabel () { + delete this._identityIconLabel; + return this._identityIconLabel = document.getElementById("identity-icon-label"); + }, + get _overrideService () { + delete this._overrideService; + return this._overrideService = Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService); + }, + get _identityIconCountryLabel () { + delete this._identityIconCountryLabel; + return this._identityIconCountryLabel = document.getElementById("identity-icon-country-label"); + }, + get _identityIcon () { + delete this._identityIcon; + return this._identityIcon = document.getElementById("page-proxy-favicon"); + }, + + /** + * Rebuild cache of the elements that may or may not exist depending + * on whether there's a location bar. + */ + _cacheElements : function() { + delete this._identityBox; + delete this._identityIconLabel; + delete this._identityIconCountryLabel; + delete this._identityIcon; + this._identityBox = document.getElementById("identity-box"); + this._identityIconLabel = document.getElementById("identity-icon-label"); + this._identityIconCountryLabel = document.getElementById("identity-icon-country-label"); + this._identityIcon = document.getElementById("page-proxy-favicon"); + }, + + /** + * Handler for mouseclicks on the "More Information" button in the + * "identity-popup" panel. + */ + handleMoreInfoClick : function(event) { + displaySecurityInfo(); + event.stopPropagation(); + }, + + /** + * Helper to parse out the important parts of _lastStatus (of the SSL cert in + * particular) for use in constructing identity UI strings + */ + getIdentityData : function() { + var result = {}; + var status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus); + var cert = status.serverCert; + + // Human readable name of Subject + result.subjectOrg = cert.organization; + + // SubjectName fields, broken up for individual access + if (cert.subjectName) { + result.subjectNameFields = {}; + cert.subjectName.split(",").forEach(function(v) { + var field = v.split("="); + this[field[0]] = field[1]; + }, result.subjectNameFields); + + // Call out city, state, and country specifically + result.city = result.subjectNameFields.L; + result.state = result.subjectNameFields.ST; + result.country = result.subjectNameFields.C; + } + + // Human readable name of Certificate Authority + result.caOrg = cert.issuerOrganization || cert.issuerCommonName; + result.cert = cert; + + return result; + }, + + /** + * Determine the identity of the page being displayed by examining its SSL cert + * (if available) and, if necessary, update the UI to reflect this. Intended to + * be called by onSecurityChange + * + * @param PRUint32 state + * @param JS Object location that mirrors an nsLocation (i.e. has .host and + * .hostname and .port) + */ + checkIdentity : function(state, location) { + var currentStatus = gBrowser.securityUI + .QueryInterface(Components.interfaces.nsISSLStatusProvider) + .SSLStatus; + this._lastStatus = currentStatus; + this._lastLocation = location; + + let nsIWebProgressListener = Ci.nsIWebProgressListener; + if (location.protocol == "chrome:" || location.protocol == "about:") { + this.setMode(this.IDENTITY_MODE_CHROMEUI); + } else if (state & nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) { + this.setMode(this.IDENTITY_MODE_IDENTIFIED); + } else if (state & nsIWebProgressListener.STATE_IS_SECURE) { + this.setMode(this.IDENTITY_MODE_DOMAIN_VERIFIED); + } else if (state & nsIWebProgressListener.STATE_IS_BROKEN) { + if ((state & nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) && + gPrefService.getBoolPref("security.mixed_content.block_active_content")) { + this.setMode(this.IDENTITY_MODE_MIXED_ACTIVE_CONTENT); + } else { + this.setMode(this.IDENTITY_MODE_MIXED_CONTENT); + } + } else { + this.setMode(this.IDENTITY_MODE_UNKNOWN); + } + + // Ensure the doorhanger is shown when mixed active content is blocked. + if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) + this.showMixedContentDoorhanger(); + }, + + /** + * Display the Mixed Content Blocker doohanger, providing an option + * to the user to override mixed content blocking + */ + showMixedContentDoorhanger : function() { + // If we've already got an active notification, bail out to avoid showing it repeatedly. + if (PopupNotifications.getNotification("mixed-content-blocked", gBrowser.selectedBrowser)) + return; + + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let messageString = gNavigatorBundle.getFormattedString("mixedContentBlocked.message", [brandShortName]); + let action = { + label: gNavigatorBundle.getString("mixedContentBlocked.keepBlockingButton.label"), + accessKey: gNavigatorBundle.getString("mixedContentBlocked.keepBlockingButton.accesskey"), + callback: function() { /* NOP */ } + }; + let secondaryActions = [ + { + label: gNavigatorBundle.getString("mixedContentBlocked.unblock.label"), + accessKey: gNavigatorBundle.getString("mixedContentBlocked.unblock.accesskey"), + callback: function() { + // Reload the page with the content unblocked + BrowserReloadWithFlags(nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT); + } + } + ]; + let options = { + dismissed: true, + learnMoreURL: Services.urlFormatter.formatURLPref("browser.mixedcontent.warning.infoURL"), + }; + PopupNotifications.show(gBrowser.selectedBrowser, "mixed-content-blocked", + messageString, "mixed-content-blocked-notification-icon", + action, secondaryActions, options); + }, + + /** + * Return the eTLD+1 version of the current hostname + */ + getEffectiveHost : function() { + try { + let baseDomain = + Services.eTLD.getBaseDomainFromHost(this._lastLocation.hostname); + return this._IDNService.convertToDisplayIDN(baseDomain, {}); + } catch (e) { + // If something goes wrong (e.g. hostname is an IP address) just fail back + // to the full domain. + return this._lastLocation.hostname; + } + }, + + /** + * Update the UI to reflect the specified mode, which should be one of the + * IDENTITY_MODE_* constants. + */ + setMode : function(newMode) { + if (!this._identityBox) { + // No identity box means the identity box is not visible, in which + // case there's nothing to do. + return; + } + + this._identityBox.className = newMode; + this.setIdentityMessages(newMode); + + // Update the popup too, if it's open + if (this._identityPopup.state == "open") + this.setPopupMessages(newMode); + + this._mode = newMode; + }, + + /** + * Set up the messages for the primary identity UI based on the specified mode, + * and the details of the SSL cert, where applicable + * + * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants. + */ + setIdentityMessages : function(newMode) { + let icon_label = ""; + let tooltip = ""; + let icon_country_label = ""; + let icon_labels_dir = "ltr"; + + if (!this._IDNService) + this._IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + let punyID = gPrefService.getIntPref("browser.identity.display_punycode", 1); + + switch (newMode) { + case this.IDENTITY_MODE_DOMAIN_VERIFIED: { + let iData = this.getIdentityData(); + + let label_display = ""; + + //Pale Moon: honor browser.identity.ssl_domain_display! + switch (gPrefService.getIntPref("browser.identity.ssl_domain_display")) { + case 2 : // Show full domain + label_display = this._lastLocation.hostname; + break; + case 1 : // Show eTLD. + label_display = this.getEffectiveHost(); + } + + if (punyID >= 1) { + // Display punycode version in identity panel + icon_label = this._IDNService.convertUTF8toACE(label_display); + } else { + icon_label = label_display; + } + + // Verifier is either the CA Org, for a normal cert, or a special string + // for certs that are trusted because of a security exception. + tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier", + [iData.caOrg]); + + // Check whether this site is a security exception. XPConnect does the right + // thing here in terms of converting _lastLocation.port from string to int, but + // the overrideService doesn't like undefined ports, so make sure we have + // something in the default case (bug 432241). + // .hostname can return an empty string in some exceptional cases - + // hasMatchingOverride does not handle that, so avoid calling it. + // Updating the tooltip value in those cases isn't critical. + // FIXME: Fixing bug 646690 would probably makes this check unnecessary + if (this._lastLocation.hostname && + this._overrideService.hasMatchingOverride(this._lastLocation.hostname, + (this._lastLocation.port || 443), + iData.cert, {}, {})) + tooltip = gNavigatorBundle.getString("identity.identified.verified_by_you"); + break; } + case this.IDENTITY_MODE_IDENTIFIED: { + // If it's identified, then we can populate the dialog with credentials + let iData = this.getIdentityData(); + tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier", + [iData.caOrg]); + icon_label = iData.subjectOrg; + if (iData.country) + icon_country_label = "(" + iData.country + ")"; + + // If the organization name starts with an RTL character, then + // swap the positions of the organization and country code labels. + // The Unicode ranges reflect the definition of the UCS2_CHAR_IS_BIDI + // macro in intl/unicharutil/util/nsBidiUtils.h. When bug 218823 gets + // fixed, this test should be replaced by one adhering to the + // Unicode Bidirectional Algorithm proper (at the paragraph level). + icon_labels_dir = /^[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/.test(icon_label) ? + "rtl" : "ltr"; + break; } + case this.IDENTITY_MODE_CHROMEUI: + break; + default: + tooltip = gNavigatorBundle.getString("identity.unknown.tooltip"); + if (punyID == 2) { + // Check for IDN and display if so... + let rawHost = this._IDNService.convertUTF8toACE(this._lastLocation.hostname); + if (this._IDNService.isACE(rawHost)) { + icon_label = rawHost; + } + } + } + + // Push the appropriate strings out to the UI + this._identityBox.tooltipText = tooltip; + this._identityIconLabel.value = icon_label; + this._identityIconCountryLabel.value = icon_country_label; + // Set cropping and direction + this._identityIconLabel.crop = icon_country_label ? "end" : "center"; + this._identityIconLabel.parentNode.style.direction = icon_labels_dir; + // Hide completely if the organization label is empty + this._identityIconLabel.parentNode.collapsed = icon_label ? false : true; + }, + + /** + * Set up the title and content messages for the identity message popup, + * based on the specified mode, and the details of the SSL cert, where + * applicable + * + * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants. + */ + setPopupMessages : function(newMode) { + + this._identityPopup.className = newMode; + this._identityPopupContentBox.className = newMode; + + // Set the static strings up front + this._identityPopupEncLabel.textContent = this._encryptionLabel[newMode]; + + // Initialize the optional strings to empty values + let supplemental = ""; + let verifier = ""; + let host = ""; + let owner = ""; + + switch (newMode) { + case this.IDENTITY_MODE_DOMAIN_VERIFIED: + host = this.getEffectiveHost(); + owner = gNavigatorBundle.getString("identity.ownerUnknown2"); + verifier = this._identityBox.tooltipText; + break; + case this.IDENTITY_MODE_IDENTIFIED: { + // If it's identified, then we can populate the dialog with credentials + let iData = this.getIdentityData(); + host = this.getEffectiveHost(); + owner = iData.subjectOrg; + verifier = this._identityBox.tooltipText; + + // Build an appropriate supplemental block out of whatever location data we have + if (iData.city) + supplemental += iData.city + "\n"; + if (iData.state && iData.country) + supplemental += gNavigatorBundle.getFormattedString("identity.identified.state_and_country", + [iData.state, iData.country]); + else if (iData.state) // State only + supplemental += iData.state; + else if (iData.country) // Country only + supplemental += iData.country; + break; } + } + + // Push the appropriate strings out to the UI + this._identityPopupContentHost.textContent = host; + this._identityPopupContentOwner.textContent = owner; + this._identityPopupContentSupp.textContent = supplemental; + this._identityPopupContentVerif.textContent = verifier; + }, + + hideIdentityPopup : function() { + this._identityPopup.hidePopup(); + }, + + /** + * Click handler for the identity-box element in primary chrome. + */ + handleIdentityButtonEvent : function(event) { + event.stopPropagation(); + + if ((event.type == "click" && event.button != 0) || + (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE && + event.keyCode != KeyEvent.DOM_VK_RETURN)) { + return; // Left click, space or enter only + } + + // Don't allow left click, space or enter if the location + // is chrome UI or the location has been modified. + if (this._mode == this.IDENTITY_MODE_CHROMEUI || + gURLBar.getAttribute("pageproxystate") != "valid") { + return; + } + + // Make sure that the display:none style we set in xul is removed now that + // the popup is actually needed + this._identityPopup.hidden = false; + + // Update the popup strings + this.setPopupMessages(this._identityBox.className); + + // Add the "open" attribute to the identity box for styling + this._identityBox.setAttribute("open", "true"); + var self = this; + this._identityPopup.addEventListener("popuphidden", function onPopupHidden(e) { + e.currentTarget.removeEventListener("popuphidden", onPopupHidden, false); + self._identityBox.removeAttribute("open"); + }, false); + + // Now open the popup, anchored off the primary chrome element + this._identityPopup.openPopup(this._identityIcon, "bottomcenter topleft"); + }, + + onPopupShown : function(event) { + document.getElementById('identity-popup-more-info-button').focus(); + }, + + onDragStart: function (event) { + if (gURLBar.getAttribute("pageproxystate") != "valid") + return; + + var value = content.location.href; + var urlString = value + "\n" + content.document.title; + var htmlString = "<a href=\"" + value + "\">" + value + "</a>"; + + var dt = event.dataTransfer; + dt.setData("text/x-moz-url", urlString); + dt.setData("text/uri-list", value); + dt.setData("text/plain", value); + dt.setData("text/html", htmlString); + dt.setDragImage(gProxyFavIcon, 16, 16); + } +}; + +function getNotificationBox(aWindow) { + var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document); + if (foundBrowser) + return gBrowser.getNotificationBox(foundBrowser) + return null; +}; + +function getTabModalPromptBox(aWindow) { + var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document); + if (foundBrowser) + return gBrowser.getTabModalPromptBox(foundBrowser); + return null; +}; + +/* DEPRECATED */ +function getBrowser() gBrowser; +function getNavToolbox() gNavToolbox; + +let gPrivateBrowsingUI = { + init: function PBUI_init() { + // Do nothing for normal windows + if (!PrivateBrowsingUtils.isWindowPrivate(window)) { + return; + } + + // Disable the Clear Recent History... menu item when in PB mode + // temporary fix until bug 463607 is fixed + document.getElementById("Tools:Sanitize").setAttribute("disabled", "true"); + + if (window.location.href == getBrowserURL()) { +#ifdef XP_MACOSX + if (!PrivateBrowsingUtils.permanentPrivateBrowsing) { + document.documentElement.setAttribute("drawintitlebar", true); + } +#endif + + // Adjust the window's title + let docElement = document.documentElement; + if (!PrivateBrowsingUtils.permanentPrivateBrowsing) { + docElement.setAttribute("title", + docElement.getAttribute("title_privatebrowsing")); + docElement.setAttribute("titlemodifier", + docElement.getAttribute("titlemodifier_privatebrowsing")); + } + docElement.setAttribute("privatebrowsingmode", + PrivateBrowsingUtils.permanentPrivateBrowsing ? "permanent" : "temporary"); + gBrowser.updateTitlebar(); + + if (PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Adjust the New Window menu entries + [ + { normal: "menu_newNavigator", private: "menu_newPrivateWindow" }, + { normal: "appmenu_newNavigator", private: "appmenu_newPrivateWindow" }, + ].forEach(function(menu) { + let newWindow = document.getElementById(menu.normal); + let newPrivateWindow = document.getElementById(menu.private); + if (newWindow && newPrivateWindow) { + newPrivateWindow.hidden = true; + newWindow.label = newPrivateWindow.label; + newWindow.accessKey = newPrivateWindow.accessKey; + newWindow.command = newPrivateWindow.command; + } + }); + } + } + + if (gURLBar && + !PrivateBrowsingUtils.permanentPrivateBrowsing) { + // Disable switch to tab autocompletion for private windows + // (not for "Always use private browsing" mode) + gURLBar.setAttribute("autocompletesearchparam", ""); + } + } +}; + + +/** + * Switch to a tab that has a given URI, and focusses its browser window. + * If a matching tab is in this window, it will be switched to. Otherwise, other + * windows will be searched. + * + * @param aURI + * URI to search for + * @param aOpenNew + * True to open a new tab and switch to it, if no existing tab is found. + * If no suitable window is found, a new one will be opened. + * @return True if an existing tab was found, false otherwise + */ +function switchToTabHavingURI(aURI, aOpenNew) { + // This will switch to the tab in aWindow having aURI, if present. + function switchIfURIInWindow(aWindow) { + // Only switch to the tab if neither the source and desination window are + // private and they are not in permanent private borwsing mode + if ((PrivateBrowsingUtils.isWindowPrivate(window) || + PrivateBrowsingUtils.isWindowPrivate(aWindow)) && + !PrivateBrowsingUtils.permanentPrivateBrowsing) { + return false; + } + + let browsers = aWindow.gBrowser.browsers; + for (let i = 0; i < browsers.length; i++) { + let browser = browsers[i]; + if (browser.currentURI.equals(aURI)) { + // Focus the matching window & tab + aWindow.focus(); + aWindow.gBrowser.tabContainer.selectedIndex = i; + return true; + } + } + return false; + } + + // This can be passed either nsIURI or a string. + if (!(aURI instanceof Ci.nsIURI)) + aURI = Services.io.newURI(aURI, null, null); + + let isBrowserWindow = !!window.gBrowser; + + // Prioritise this window. + if (isBrowserWindow && switchIfURIInWindow(window)) + return true; + + let winEnum = Services.wm.getEnumerator("navigator:browser"); + while (winEnum.hasMoreElements()) { + let browserWin = winEnum.getNext(); + // Skip closed (but not yet destroyed) windows, + // and the current window (which was checked earlier). + if (browserWin.closed || browserWin == window) + continue; + if (switchIfURIInWindow(browserWin)) + return true; + } + + // No opened tab has that url. + if (aOpenNew) { + if (isBrowserWindow && isTabEmpty(gBrowser.selectedTab)) + gBrowser.selectedBrowser.loadURI(aURI.spec); + else + openUILinkIn(aURI.spec, "tab"); + } + + return false; +} + +function restoreLastSession() { + let ss = Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore); + ss.restoreLastSession(); +} + +var TabContextMenu = { + contextTab: null, + updateContextMenu: function updateContextMenu(aPopupMenu) { + this.contextTab = aPopupMenu.triggerNode.localName == "tab" ? + aPopupMenu.triggerNode : gBrowser.selectedTab; + let disabled = gBrowser.tabs.length == 1; + + // Enable the "Close Tab" menuitem when the window doesn't close with the last tab. + document.getElementById("context_closeTab").disabled = + disabled && gBrowser.tabContainer._closeWindowWithLastTab; + + var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple"); + for (let menuItem of menuItems) + menuItem.disabled = disabled; + + disabled = gBrowser.visibleTabs.length == 1; + menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple-visible"); + for (let menuItem of menuItems) + menuItem.disabled = disabled; + + // Session store + document.getElementById("context_undoCloseTab").disabled = + Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore). + getClosedTabCount(window) == 0; + + // Only one of pin/unpin should be visible + document.getElementById("context_pinTab").hidden = this.contextTab.pinned; + document.getElementById("context_unpinTab").hidden = !this.contextTab.pinned; + + // Disable "Close Tabs to the Right" if there are no tabs + // following it and hide it when the user rightclicked on a pinned + // tab. + document.getElementById("context_closeTabsToTheEnd").disabled = + gBrowser.getTabsToTheEndFrom(this.contextTab).length == 0; + document.getElementById("context_closeTabsToTheEnd").hidden = this.contextTab.pinned; + + // Disable "Close other Tabs" if there is only one unpinned tab and + // hide it when the user rightclicked on a pinned tab. + let unpinnedTabs = gBrowser.visibleTabs.length - gBrowser._numPinnedTabs; + document.getElementById("context_closeOtherTabs").disabled = unpinnedTabs <= 1; + document.getElementById("context_closeOtherTabs").hidden = this.contextTab.pinned; + + // Hide "Bookmark All Tabs" for a pinned tab. Update its state if visible. + let bookmarkAllTabs = document.getElementById("context_bookmarkAllTabs"); + bookmarkAllTabs.hidden = this.contextTab.pinned; + if (!bookmarkAllTabs.hidden) + PlacesCommandHook.updateBookmarkAllTabsCommand(); + } +}; + +#ifdef MOZ_DEVTOOLS +XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", + "resource://gre/modules/devtools/gDevTools.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "gDevToolsBrowser", + "resource://gre/modules/devtools/gDevTools.jsm"); + +Object.defineProperty(this, "HUDService", { + get: function HUDService_getter() { + let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; + return devtools.require("devtools/webconsole/hudservice").HUDService; + }, + configurable: true, + enumerable: true +}); +#endif + +// Prompt user to restart the browser in safe mode or normally +function restart(safeMode) +{ + let promptTitleString = null; + let promptMessageString = null; + let restartTextString = null; + if (safeMode) { + promptTitleString = "safeModeRestartPromptTitle"; + promptMessageString = "safeModeRestartPromptMessage"; + restartTextString = "safeModeRestartButton"; + } else { + promptTitleString = "restartPromptTitle"; + promptMessageString = "restartPromptMessage"; + restartTextString = "restartButton"; + } + + let flags = Ci.nsIAppStartup.eAttemptQuit; + + // Prompt the user to confirm + let promptTitle = gNavigatorBundle.getString(promptTitleString); + let brandBundle = document.getElementById("bundle_brand"); + let brandShortName = brandBundle.getString("brandShortName"); + let promptMessage = + gNavigatorBundle.getFormattedString(promptMessageString, [brandShortName]); + let restartText = gNavigatorBundle.getString(restartTextString); + let buttonFlags = (Services.prompt.BUTTON_POS_0 * + Services.prompt.BUTTON_TITLE_IS_STRING) + + (Services.prompt.BUTTON_POS_1 * + Services.prompt.BUTTON_TITLE_CANCEL) + + Services.prompt.BUTTON_POS_0_DEFAULT; + + let rv = Services.prompt.confirmEx(window, promptTitle, promptMessage, + buttonFlags, restartText, null, null, + null, {}); + + if (rv == 0) { + // Notify all windows that an application quit has been requested. + let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"] + .createInstance(Ci.nsISupportsPRBool); + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); + + // Something aborted the quit process. + if (cancelQuit.data) { + return; + } + + if (safeMode) { + Services.startup.restartInSafeMode(flags); + } else { + Services.startup.quit(flags | Ci.nsIAppStartup.eRestart); + } + } +} + +/* duplicateTabIn duplicates tab in a place specified by the parameter |where|. + * + * |where| can be: + * "tab" new tab + * "tabshifted" same as "tab" but in background if default is to select new + * tabs, and vice versa + * "window" new window + * + * delta is the offset to the history entry that you want to load. + */ +function duplicateTabIn(aTab, where, delta) { + let newTab = Cc['@mozilla.org/browser/sessionstore;1'] + .getService(Ci.nsISessionStore) + .duplicateTab(window, aTab, delta); + + switch (where) { + case "window": + gBrowser.hideTab(newTab); + gBrowser.replaceTabWithWindow(newTab); + break; + case "tabshifted": + // A background tab has been opened, nothing else to do here. + break; + case "tab": + gBrowser.selectedTab = newTab; + break; + } +} + +function toggleAddonBar() { + let addonBar = document.getElementById("addon-bar"); + setToolbarVisibility(addonBar, addonBar.collapsed); +} + +#ifdef MOZ_DEVTOOLS +var Scratchpad = { + prefEnabledName: "devtools.scratchpad.enabled", + + openScratchpad: function SP_openScratchpad() { + return this.ScratchpadManager.openScratchpad(); + } +}; + +XPCOMUtils.defineLazyGetter(Scratchpad, "ScratchpadManager", function() { + let tmp = {}; + Cu.import("resource://gre/modules/devtools/scratchpad-manager.jsm", tmp); + return tmp.ScratchpadManager; +}); + +var ResponsiveUI = { + toggle: function RUI_toggle() { + this.ResponsiveUIManager.toggle(window, gBrowser.selectedTab); + } +}; + +XPCOMUtils.defineLazyGetter(ResponsiveUI, "ResponsiveUIManager", function() { + let tmp = {}; + Cu.import("resource://gre/modules/devtools/responsivedesign.jsm", tmp); + return tmp.ResponsiveUIManager; +}); + +function openEyedropper() { + var eyedropper = new this.Eyedropper(this, { context: "menu", + copyOnSelect: true }); + eyedropper.open(); +} + +Object.defineProperty(this, "Eyedropper", { + get: function() { + let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; + return devtools.require("devtools/eyedropper/eyedropper").Eyedropper; + }, + configurable: true, + enumerable: true +}); +#endif + +XPCOMUtils.defineLazyGetter(window, "gShowPageResizers", function () { +#ifdef XP_WIN + // Only show resizers on Windows 2000 and XP + return parseFloat(Services.sysinfo.getProperty("version")) < 6; +#else + return false; +#endif +}); + +var MousePosTracker = { + _listeners: [], + _x: 0, + _y: 0, + get _windowUtils() { + delete this._windowUtils; + return this._windowUtils = window.getInterface(Ci.nsIDOMWindowUtils); + }, + + addListener: function (listener) { + if (this._listeners.indexOf(listener) >= 0) + return; + + listener._hover = false; + this._listeners.push(listener); + + this._callListener(listener); + }, + + removeListener: function (listener) { + var index = this._listeners.indexOf(listener); + if (index < 0) + return; + + this._listeners.splice(index, 1); + }, + + handleEvent: function (event) { + var fullZoom = this._windowUtils.fullZoom; + this._x = event.screenX / fullZoom - window.mozInnerScreenX; + this._y = event.screenY / fullZoom - window.mozInnerScreenY; + + this._listeners.forEach(function (listener) { + try { + this._callListener(listener); + } catch (e) { + Cu.reportError(e); + } + }, this); + }, + + _callListener: function (listener) { + let rect = listener.getMouseTargetRect(); + let hover = this._x >= rect.left && + this._x <= rect.right && + this._y >= rect.top && + this._y <= rect.bottom; + + if (hover == listener._hover) + return; + + listener._hover = hover; + + if (hover) { + if (listener.onMouseEnter) + listener.onMouseEnter(); + } else { + if (listener.onMouseLeave) + listener.onMouseLeave(); + } + } +}; + +function focusNextFrame(event) { + let fm = Services.focus; + let dir = event.shiftKey ? fm.MOVEFOCUS_BACKWARDDOC : fm.MOVEFOCUS_FORWARDDOC; + let element = fm.moveFocus(window, null, dir, fm.FLAG_BYKEY); + if (element.ownerDocument == document) + focusAndSelectUrlBar(); +} +let BrowserChromeTest = { + _cb: null, + _ready: false, + markAsReady: function () { + this._ready = true; + if (this._cb) { + this._cb(); + this._cb = null; + } + }, + runWhenReady: function (cb) { + if (this._ready) + cb(); + else + this._cb = cb; + } +}; + +let ToolbarIconColor = { + init: function () { + this._initialized = true; + + window.addEventListener("activate", this); + window.addEventListener("deactivate", this); + Services.obs.addObserver(this, "lightweight-theme-styling-update", false); + gPrefService.addObserver("ui.colorChanged", this, false); + + // If the window isn't active now, we assume that it has never been active + // before and will soon become active such that inferFromText will be + // called from the initial activate event. + if (Services.focus.activeWindow == window) + this.inferFromText(); + }, + + uninit: function () { + this._initialized = false; + + window.removeEventListener("activate", this); + window.removeEventListener("deactivate", this); + Services.obs.removeObserver(this, "lightweight-theme-styling-update"); + gPrefService.removeObserver("ui.colorChanged", this); + }, + + handleEvent: function (event) { + switch (event.type) { + case "activate": + case "deactivate": + this.inferFromText(); + break; + } + }, + + observe: function (aSubject, aTopic, aData) { + switch (aTopic) { + case "lightweight-theme-styling-update": + // inferFromText needs to run after LightweightThemeConsumer.jsm's + // lightweight-theme-styling-update observer. + setTimeout(() => { this.inferFromText(); }, 0); + break; + case "nsPref:changed": + // system color change + var colorChangedPref = false; + try { + colorChangedPref = gPrefService.getBoolPref("ui.colorChanged"); + } catch(e) { } + // if pref indicates change, call inferFromText() on a small delay + if (colorChangedPref == true) + setTimeout(() => { this.inferFromText(); }, 300); + break; + default: + console.error("ToolbarIconColor: Uncaught topic " + aTopic); + } + }, + + inferFromText: function () { + if (!this._initialized) + return; + + function parseRGB(aColorString) { + let rgb = aColorString.match(/^rgba?\((\d+), (\d+), (\d+)/); + rgb.shift(); + return rgb.map(x => parseInt(x)); + } + + let toolbarSelector = "toolbar:not([collapsed=true])"; +#ifdef XP_MACOSX + toolbarSelector += ":not([type=menubar])"; +#endif + + // The getComputedStyle calls and setting the brighttext are separated in + // two loops to avoid flushing layout and making it dirty repeatedly. + + let luminances = new Map; + for (let toolbar of document.querySelectorAll(toolbarSelector)) { + let [r, g, b] = parseRGB(getComputedStyle(toolbar).color); + let luminance = (2 * r + 5 * g + b) / 8; + luminances.set(toolbar, luminance); + } + + for (let [toolbar, luminance] of luminances) { + if (luminance <= 128) + toolbar.removeAttribute("brighttext"); + else + toolbar.setAttribute("brighttext", "true"); + } + + // Clear pref if set, since we're done applying the color changes. + gPrefService.clearUserPref("ui.colorChanged"); + } +} diff --git a/application/palemoon/base/content/browser.xul b/application/palemoon/base/content/browser.xul new file mode 100644 index 0000000000..f830100232 --- /dev/null +++ b/application/palemoon/base/content/browser.xul @@ -0,0 +1,1058 @@ +#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"?> + +# Restore title to AppMenu windowed use +<?xml-stylesheet href="chrome://browser/content/browser-title.css" type="text/css"?> + +<?xml-stylesheet href="chrome://browser/content/places/places.css" type="text/css"?> +#ifdef MOZ_DEVTOOLS +<?xml-stylesheet href="chrome://global/skin/devtools/common.css" type="text/css"?> +#endif +<?xml-stylesheet href="chrome://browser/skin/" 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"?> + +# Padlock feature +<?xul-overlay href="chrome://browser/content/padlock.xul"?> +# Improve bookmark menu dragging +<?xul-overlay href="chrome://browser/content/browser-menudragging.xul"?> +# Automatic browser recovery +<?xul-overlay href="chrome://browser/content/autorecovery.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="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 + titlemenuseparator="&mainWindow.titlemodifiermenuseparator;" + lightweightthemes="true" + lightweightthemesfooter="browser-bottombox" + windowtype="navigator:browser" + macanimationtype="document" + screenX="4" screenY="4" + fullscreenbutton="true" + 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/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);"/> + <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/> + <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);"/> + <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"/> + + <!-- for search and content formfill/pw manager --> + <panel type="autocomplete" id="PopupAutoComplete" noautofocus="true" hidden="true"/> + + <!-- for url bar autocomplete --> + <panel type="autocomplete-richlistbox" id="PopupAutoCompleteRichResult" noautofocus="true" hidden="true"/> + + <!-- 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" + footertype="promobox" + orient="vertical" + ignorekeys="true" + consumeoutsideclicks="true" + hidden="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"/> + <hbox> + <button id="editBookmarkPanelRemoveButton" + class="editBookmarkPanelHeaderButton" + oncommand="StarUI.removeBookmarkButtonCommand();" + accesskey="&editBookmark.removeBookmark.accessKey;"/> + </hbox> + </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="editBookmarkPanelDeleteButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.cancel.label;" + oncommand="StarUI.cancelButtonOnCommand();"/> +#else + <button id="editBookmarkPanelDeleteButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.cancel.label;" + oncommand="StarUI.cancelButtonOnCommand();"/> + <button id="editBookmarkPanelDoneButton" + class="editBookmarkPanelBottomButton" + label="&editBookmark.done.label;" + default="true" + oncommand="StarUI.panel.hidePopup();"/> +#endif + </hbox> + </panel> + + <menupopup id="toolbar-context-menu" + onpopupshowing="onViewToolbarsPopupShowing(event);"> + <menuseparator/> + <menuitem command="cmd_ToggleTabsOnTop" + type="checkbox" + label="&viewTabsOnTop.label;" + accesskey="&viewTabsOnTop.accesskey;"/> + <menuitem command="cmd_CustomizeToolbars" + 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="start" + 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"/> + + + <panel id="ctrlTab-panel" class="KUI-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> + + <panel id="allTabs-panel" hidden="true" norestorefocus="true" ignorekeys="true" + onmouseover="allTabs._updateTabCloseButton(event);"> + <hbox id="allTabs-meta" align="center"> + <spacer flex="1"/> + <textbox id="allTabs-filter" + tooltiptext="&allTabs.filter.emptyText;" + type="search" + oncommand="allTabs.filter();"/> + <spacer flex="1"/> + <toolbarbutton class="KUI-panel-closebutton" + oncommand="allTabs.close()" + tooltiptext="&closeCmd.label;"/> + </hbox> + <stack id="allTabs-stack"> + <vbox id="allTabs-container"><hbox/></vbox> + <toolbarbutton id="allTabs-tab-close-button" + class="tabs-closebutton close-icon" + oncommand="allTabs.closeTab(event);" + tooltiptext="&closeCmd.label;" + style="visibility:hidden"/> + </stack> + </panel> + + <!-- Bookmarks and history tooltip --> + <tooltip id="bhTooltip"/> + + <panel id="customizeToolbarSheetPopup" + noautohide="true" + sheetstyle="&dialog.dimensions;"/> + + <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 + + </popupset> + +#ifdef CAN_DRAW_IN_TITLEBAR +<vbox id="titlebar"> + <hbox id="titlebar-content"> +#ifdef MENUBAR_CAN_AUTOHIDE + <hbox id="appmenu-button-container"> + <button id="appmenu-button" + type="menu" + label="&brandShortName;" + tooltiptext="&appMenuButton.tooltip;" + style="-moz-user-focus: ignore;"> +#include browser-appmenu.inc + </button> + </hbox> +#endif + <spacer id="titlebar-spacer" flex="1"/> + <hbox id="titlebar-buttonbox-container" align="start"> + <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> + </hbox> +</vbox> +#endif + +<deck flex="1" id="tab-view-deck"> +<vbox flex="1" id="browser-panel"> + + <toolbox id="navigator-toolbox" + defaultmode="icons" mode="icons" + iconsize="large"> + <!-- Menu --> + <toolbar type="menubar" id="toolbar-menubar" class="chromeclass-menubar" customizable="true" + defaultset="menubar-items" + mode="icons" iconsize="small" defaulticonsize="small" + lockiconsize="true" +#ifdef MENUBAR_CAN_AUTOHIDE + toolbarname="&menubarCmd.label;" + accesskey="&menubarCmd.accesskey;" +#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 + <hbox class="titlebar-placeholder" type="appmenu-button" ordinal="0"/> + <hbox class="titlebar-placeholder" type="caption-buttons" ordinal="1000"/> +#endif + </toolbar> + + <toolbar id="nav-bar" class="toolbar-primary chromeclass-toolbar" + toolbarname="&navbarCmd.label;" accesskey="&navbarCmd.accesskey;" + fullscreentoolbar="true" mode="icons" customizable="true" + iconsize="large" + defaultset="unified-back-forward-button,reload-button,stop-button,home-button,urlbar-container,search-container,bookmarks-menu-button,history-menu-button,downloads-button,window-controls" + context="toolbar-context-menu"> + + <toolbaritem id="unified-back-forward-button" class="chromeclass-toolbar-additional" + context="backForwardMenu" removable="true" + forwarddisabled="true" + title="&backForwardItem.title;"> + <toolbarbutton id="back-button" class="toolbarbutton-1" + label="&backCmd.label;" + command="Browser:BackOrBackDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltip="back-button-tooltip"/> + <toolbarbutton id="forward-button" class="toolbarbutton-1" + label="&forwardCmd.label;" + command="Browser:ForwardOrForwardDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltip="forward-button-tooltip"/> + <dummyobservertarget hidden="true" + onbroadcast="if (this.getAttribute('disabled') == 'true') + this.parentNode.setAttribute('forwarddisabled', 'true'); + else + this.parentNode.removeAttribute('forwarddisabled');"> + <observes element="Browser:ForwardOrForwardDuplicate" attribute="disabled"/> + </dummyobservertarget> + </toolbaritem> + + <toolbarbutton id="reload-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&reloadCmd.label;" removable="true" + command="Browser:ReloadOrDuplicate" + onclick="checkForMiddleClick(this, event);" + tooltiptext="&reloadButton.tooltip;"/> + + <toolbarbutton id="stop-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&stopCmd.label;" removable="true" + command="Browser:Stop" + tooltiptext="&stopButton.tooltip;"/> + + <toolbarbutton id="home-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + persist="class" removable="true" + label="&homeButton.label;" + ondragover="homeButtonObserver.onDragOver(event)" + ondragenter="homeButtonObserver.onDragOver(event)" + ondrop="homeButtonObserver.onDrop(event)" + ondragexit="homeButtonObserver.onDragExit(event)" + onclick="BrowserGoHome(event);" + aboutHomeOverrideTooltip="&abouthome.pageTitle;"/> + + <toolbaritem id="urlbar-container" align="center" flex="400" persist="width" combined="true" + title="&locationItem.title;" class="chromeclass-location" removable="true"> + <textbox id="urlbar" flex="1" + placeholder="&urlbar.placeholder2;" + type="autocomplete" + autocompletesearch="urlinline history" + autocompletesearchparam="enable-actions" + autocompletepopup="PopupAutoCompleteRichResult" + completeselectedindex="true" + tabscrolling="true" + showcommentcolumn="true" + showimagecolumn="true" + enablehistory="true" + maxrows="6" + newlines="stripsurroundingwhitespace" + oninput="gBrowser.userTypedValue = this.value;" + ontextentered="this.handleCommand(param);" + ontextreverted="return this.handleRevert();" + pageproxystate="invalid" + onfocus="document.getElementById('identity-box').style.MozUserFocus= 'normal'" + onblur="setTimeout(function() document.getElementById('identity-box').style.MozUserFocus = '', 0);"> + <box id="notification-popup-box" hidden="true" align="center"> + <image id="default-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="geo-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="addons-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="indexedDB-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="password-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="webapps-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="plugins-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="web-notifications-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="alert-plugins-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="blocked-plugins-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="mixed-content-blocked-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="webRTC-shareDevices-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="webRTC-sharingDevices-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="pointerLock-notification-icon" class="notification-anchor-icon" role="button"/> + <image id="servicesInstall-notification-icon" class="notification-anchor-icon" role="button"/> + </box> + <!-- Use onclick instead of normal popup= syntax since the popup + code fires onmousedown, and hence eats our favicon drag events. + We only add the identity-box button to the tab order when the location bar + has focus, otherwise pressing F6 focuses it instead of the location bar --> + <box id="identity-box" role="button" + align="center" + onclick="gIdentityHandler.handleIdentityButtonEvent(event);" + onkeypress="gIdentityHandler.handleIdentityButtonEvent(event);" + ondragstart="gIdentityHandler.onDragStart(event);"> + <image id="page-proxy-favicon" + onclick="PageProxyClickHandler(event);" + pageproxystate="invalid"/> + <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="urlbar-display" value="&urlbar.switchToTab.label;"/> + </box> + <hbox id="urlbar-icons"> + <image id="page-report-button" + class="urlbar-icon" + hidden="true" + tooltiptext="&pageReportIcon.tooltip;" + onclick="gPopupBlockerObserver.onReportButtonClick(event);"/> + <button type="menu" + style="-moz-user-focus: none" + class="plain urlbar-icon" + id="ub-feed-button" + collapsed="true" + tooltiptext="&feedButton.tooltip;" + onclick="return FeedHandler.onFeedButtonPMClick(event);"> + <menupopup position="after_end" + id="ub-feed-menu" + onpopupshowing="return FeedHandler.buildFeedList(this);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </button> + <image id="star-button" + class="urlbar-icon" + onclick="BookmarkingUI.onCommand(event);"/> + <image id="go-button" + class="urlbar-icon" + tooltiptext="&goEndCap.tooltip;" + onclick="gURLBar.handleCommand(event);"/> + </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> + </toolbaritem> + + <toolbaritem id="search-container" title="&searchItem.title;" + align="center" class="chromeclass-toolbar-additional" + flex="100" persist="width" removable="true"> + <searchbar id="searchbar" flex="1"/> + </toolbaritem> + + <toolbarbutton id="webrtc-status-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + type="menu" + hidden="true" + orient="horizontal" + label="&webrtcIndicatorButton.label;" + tooltiptext="&webrtcIndicatorButton.tooltip;"> + <menupopup onpopupshowing="WebrtcIndicator.fillPopup(this);" + onpopuphiding="WebrtcIndicator.clearPopup(this);" + oncommand="WebrtcIndicator.menuCommand(event.target);"/> + </toolbarbutton> + + <toolbarbutton id="bookmarks-menu-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + persist="class" + removable="true" + type="menu" + label="&bookmarksMenuButton.label;" + tooltiptext="&bookmarksMenuButton.tooltip;" + onclick="if (event.button == 1) + toggleSidebar('viewBookmarksSidebar');" + ondragenter="PlacesMenuDNDHandler.onDragEnter(event);" + ondragover="PlacesMenuDNDHandler.onDragOver(event);" + ondragleave="PlacesMenuDNDHandler.onDragLeave(event);" + ondrop="PlacesMenuDNDHandler.onDrop(event);"> + <menupopup id="BMB_bookmarksPopup" + placespopup="true" + context="placesContext" + openInTabs="children" + oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);" + onclick="event.stopPropagation(); + BookmarksEventHandler.onClick(event, this.parentNode._placesView);" + onpopupshowing="BookmarkingUI.onPopupShowing(event); + if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=BOOKMARKS_MENU');" + tooltip="bhTooltip" popupsinherittooltip="true"> + <menuitem id="BMB_viewBookmarksToolbar" + placesanonid="view-toolbar" + toolbarId="PersonalToolbar" + type="checkbox" + oncommand="onViewToolbarCommand(event)" + label="&viewBookmarksToolbar.label;"/> + <menuseparator/> + <menuitem id="BMB_bookmarksShowAll" +#ifndef XP_MACOSX + class="menuitem-iconic" +#endif + label="&organizeBookmarks.label;" + command="Browser:ShowAllBookmarks" + key="manBookmarkKb"/> + <menuseparator/> + <menuitem id="BMB_bookmarkThisPage" +#ifndef XP_MACOSX + class="menuitem-iconic" +#endif + label="&bookmarkThisPageCmd.label;" + command="Browser:AddBookmarkAs" + key="addBookmarkAsKb"/> + <menuitem id="BMB_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="BMB_subscribeToPageMenupopup" +#ifndef XP_MACOSX + class="menu-iconic" +#endif + label="&subscribeToPageMenupopup.label;" + observes="multipleFeedsMenuState"> + <menupopup id="BMB_subscribeToPageSubmenuMenupopup" + onpopupshowing="return FeedHandler.buildFeedList(event.target);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </menu> + <menuseparator/> + <menu id="BMB_bookmarksToolbar" + placesanonid="toolbar-autohide" + class="menu-iconic bookmark-item" + label="&personalbarCmd.label;" + container="true"> + <menupopup id="BMB_bookmarksToolbarPopup" + placespopup="true" + context="placesContext" + onpopupshowing="if (!this.parentNode._placesView) + new PlacesMenu(event, 'place:folder=TOOLBAR');"/> + </menu> + <menuseparator/> + <!-- Bookmarks menu items --> + <menuseparator builder="end" + class="hide-if-empty-places-result"/> + <menuitem id="BMB_unsortedBookmarks" + class="menuitem-iconic" + label="&bookmarksMenuButton.unsorted.label;" + oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/> + </menupopup> + </toolbarbutton> + + <toolbarbutton id="history-menu-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + persist="class" + removable="true" + type="menu" + label="&historyButton.label;" + tooltiptext="&historyButton.tooltip;" + onclick="if (event.button == 1) + toggleSidebar('viewHistorySidebar');"> + <menupopup id="HMB_historyPopup" + placespopup="true" + context="placesContext" + oncommand="this.parentNode._placesView._onCommand(event);" + onclick="event.stopPropagation(); + checkForMiddleClick(this, event);" + onpopupshowing="if (!this.parentNode._placesView) + new HistoryMenu(event);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <menuitem id="HMB_showAllHistory" + label="&showAllHistoryCmd2.label;" +#ifndef XP_MACOSX + class="menuitem-iconic" + key="showAllHistoryKb" +#endif + command="Browser:ShowAllHistory"/> + <menuitem id="HMB_sanitizeItem" +#ifndef XP_MACOSX + class="menuitem-iconic" +#endif + label="&clearRecentHistory.label;" + key="key_sanitize" + command="Tools:Sanitize"/> + <menuseparator id="HMB_sanitizeSeparator"/> +#ifdef MOZ_SERVICES_SYNC + <menuitem id="HMB_sync-tabs-menuitem" + class="syncTabsMenuItem" + label="&syncTabsMenu2.label;" + oncommand="BrowserOpenSyncTabs();" + disabled="true"/> +#endif + <menuitem id="HMB_historyRestoreLastSession" + label="&historyRestoreLastSession.label;" + command="Browser:RestoreLastSession"/> + <menu id="HMB_historyUndoMenu" + class="recentlyClosedTabsMenu" + label="&historyUndoMenu.label;" + disabled="true"> + <menupopup id="HMB_historyUndoPopup" + placespopup="true" + onpopupshowing="document.getElementById('history-menu-button')._placesView.populateUndoSubmenu();"/> + </menu> + <menu id="HMB_historyUndoWindowMenu" + class="recentlyClosedWindowsMenu" + label="&historyUndoWindowMenu.label;" + disabled="true"> + <menupopup id="HMB_historyUndoWindowPopup" + placespopup="true" + onpopupshowing="document.getElementById('history-menu-button')._placesView.populateUndoWindowSubmenu();"/> + </menu> + <menuseparator id="HMB_startHistorySeparator" + class="hide-if-empty-places-result"/> + <!-- History menu items --> + </menupopup> + </toolbarbutton> + + <hbox id="window-controls" hidden="true" pack="end"> + <toolbarbutton id="minimize-button" + tooltiptext="&fullScreenMinimize.tooltip;" + oncommand="window.minimize();"/> + + <toolbarbutton id="restore-button" + 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" defaulticonsize="small" + lockiconsize="true" + class="chromeclass-directories" + context="toolbar-context-menu" + defaultset="personal-bookmarks" + toolbarname="&personalbarCmd.label;" accesskey="&personalbarCmd.accesskey;" + collapsed="false" + customizable="true"> + <toolbaritem flex="1" id="personal-bookmarks" title="&bookmarksItem.title;" + removable="true"> + <hbox flex="1" + id="PlacesToolbar" + context="placesContext" + onclick="BookmarksEventHandler.onClick(event, this._placesView);" + oncommand="BookmarksEventHandler.onCommand(event, this._placesView);" + tooltip="bhTooltip" + popupsinherittooltip="true"> + <toolbarbutton class="bookmark-item bookmarks-toolbar-customize" + mousethrough="never" + label="&bookmarksToolbarItem.label;"/> + <hbox flex="1"> + <hbox align="center"> + <image id="PlacesToolbarDropIndicator" + mousethrough="always" + collapsed="true"/> + </hbox> + <scrollbox orient="horizontal" + id="PlacesToolbarItems" + flex="1"/> + <toolbarbutton type="menu" + id="PlacesChevron" + class="chevron" + mousethrough="never" + collapsed="true" + tooltiptext="&bookmarksToolbarChevron.tooltip;" + onpopupshowing="document.getElementById('PlacesToolbar') + ._placesView._onChevronPopupShowing(event);"> + <menupopup id="PlacesChevronPopup" + placespopup="true" + tooltip="bhTooltip" popupsinherittooltip="true" + context="placesContext"/> + </toolbarbutton> + </hbox> + </hbox> + </toolbaritem> + </toolbar> + +#ifdef MENUBAR_CAN_AUTOHIDE +#ifndef CAN_DRAW_IN_TITLEBAR +#define APPMENU_ON_TABBAR +#endif +#endif + + + <toolbar id="TabsToolbar" + class="toolbar-primary" + fullscreentoolbar="true" + customizable="true" + mode="icons" lockmode="true" + iconsize="small" defaulticonsize="small" lockiconsize="true" + aria-label="&tabsToolbar.label;" + context="toolbar-context-menu" +#ifdef APPMENU_ON_TABBAR + defaultset="appmenu-toolbar-button,tabbrowser-tabs,new-tab-button,alltabs-button,tabs-closebutton" +#else + defaultset="tabbrowser-tabs,new-tab-button,alltabs-button,tabs-closebutton" +#endif + collapsed="true"> + +#ifdef APPMENU_ON_TABBAR + <toolbarbutton id="appmenu-toolbar-button" + class="chromeclass-toolbar-additional" + type="menu" + label="&brandShortName;" + tooltiptext="&appMenuButton.tooltip;"> +#include browser-appmenu.inc + </toolbarbutton> +#endif + + <tabs id="tabbrowser-tabs" + class="tabbrowser-tabs" + tabbrowser="content" + flex="1" + setfocus="false" + tooltip="tabbrowser-tab-tooltip"> + <tab class="tabbrowser-tab" selected="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);" + tooltiptext="&newTabButton.tooltip;" + ondrop="newTabButtonObserver.onDrop(event)" + ondragover="newTabButtonObserver.onDragOver(event)" + ondragenter="newTabButtonObserver.onDragOver(event)" + ondragexit="newTabButtonObserver.onDragExit(event)" + removable="true"/> + + <toolbarbutton id="alltabs-button" + class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button" + type="menu" + label="&listAllTabs.label;" + tooltiptext="&listAllTabs.label;" + removable="true"> + <menupopup id="alltabs-popup" position="after_end"/> + </toolbarbutton> + + <toolbarbutton id="tabs-closebutton" + class="close-button tabs-closebutton close-icon" + command="cmd_close" + label="&closeTab.label;" + tooltiptext="&closeTab.label;"/> + +#ifdef CAN_DRAW_IN_TITLEBAR + <hbox class="titlebar-placeholder" type="appmenu-button" ordinal="0"/> + <hbox class="titlebar-placeholder" type="caption-buttons" ordinal="1000"/> +#endif + </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" + label="&printButton.label;" command="cmd_print" + tooltiptext="&printButton.tooltip;"/> + + <!-- 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" + oncommand="DownloadsIndicatorView.onCommand(event);" + ondrop="DownloadsIndicatorView.onDrop(event);" + ondragover="DownloadsIndicatorView.onDragOver(event);" + ondragenter="DownloadsIndicatorView.onDragOver(event);" + label="&downloads.label;" + tooltiptext="&downloads.tooltip;"/> + + <toolbarbutton id="history-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + observes="viewHistorySidebar" label="&historyButton.label;" + tooltiptext="&historyButton.tooltip;"/> + + <toolbarbutton id="bookmarks-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + observes="viewBookmarksSidebar" label="&bookmarksButton.label;" + tooltiptext="&bookmarksButton.tooltip;" + ondrop="bookmarksButtonObserver.onDrop(event)" + ondragover="bookmarksButtonObserver.onDragOver(event)" + ondragenter="bookmarksButtonObserver.onDragOver(event)" + ondragexit="bookmarksButtonObserver.onDragExit(event)"/> + + <toolbarbutton id="new-window-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&newNavigatorCmd.label;" + command="key_newNavigator" + tooltiptext="&newWindowButton.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;" + tooltiptext="&fullScreenButton.tooltip;"/> + + <toolbaritem id="zoom-controls" class="chromeclass-toolbar-additional" + title="&zoomControls.label;"> + <toolbarbutton id="zoom-out-button" class="toolbarbutton-1" + label="&fullZoomReduceCmd.label;" + command="cmd_fullZoomReduce" + tooltiptext="&zoomOutButton.tooltip;"/> + <toolbarbutton id="zoom-in-button" class="toolbarbutton-1" + label="&fullZoomEnlargeCmd.label;" + command="cmd_fullZoomEnlarge" + tooltiptext="&zoomInButton.tooltip;"/> + </toolbaritem> + + <toolbarbutton id="feed-button" + type="menu" + class="toolbarbutton-1 chromeclass-toolbar-additional" + disabled="true" + label="&feedButton.label;" + tooltiptext="&feedButton.tooltip;" + onclick="return FeedHandler.onFeedButtonClick(event);"> + <menupopup position="after_end" + id="feed-menu" + onpopupshowing="return FeedHandler.buildFeedList(this);" + oncommand="return FeedHandler.subscribeToFeed(null, event);" + onclick="checkForMiddleClick(this, event);"/> + </toolbarbutton> + + <toolbarbutton id="cut-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&cutCmd.label;" + command="cmd_cut" + tooltiptext="&cutButton.tooltip;"/> + + <toolbarbutton id="copy-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="©Cmd.label;" + command="cmd_copy" + tooltiptext="©Button.tooltip;"/> + + <toolbarbutton id="paste-button" class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&pasteCmd.label;" + command="cmd_paste" + tooltiptext="&pasteButton.tooltip;"/> + +#ifdef MOZ_SERVICES_SYNC + <toolbarbutton id="sync-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + label="&syncToolbarButton.label;" + oncommand="gSyncUI.handleToolbarButton()"/> +#endif + + <toolbaritem id="navigator-throbber" title="&throbberItem.title;" align="center" pack="center" + mousethrough="always"> + <image/> + </toolbaritem> + </toolbarpalette> + </toolbox> + + <hbox id="fullscr-toggler" collapsed="true"/> + + <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="tabs-closebutton close-icon" tooltiptext="&sidebarCloseButton.tooltip;" oncommand="toggleSidebar();"/> + </sidebarheader> + <browser id="sidebar" flex="1" autoscroll="false" disablehistory="true" + style="min-width: 14em; width: 18em; max-width: 36em;"/> + </vbox> + + <splitter id="sidebar-splitter" class="chromeclass-extrachrome sidebar-splitter" hidden="true"/> + <vbox id="appcontent" flex="1"> + <tabbrowser id="content" disablehistory="true" + flex="1" contenttooltip="aHTMLTooltip" + tabcontainer="tabbrowser-tabs" + contentcontextmenu="contentAreaContextMenu" + autocompletepopup="PopupAutoComplete"/> + <chatbar id="pinnedchats" layer="true" mousethrough="always" hidden="true"/> + <statuspanel id="statusbar-display" inactive="true"/> + </vbox> + <vbox id="browser-border-end" hidden="true" layer="true"/> + </hbox> + + <hbox id="full-screen-warning-container" hidden="true" fadeout="true"> + <hbox style="width: 100%;" pack="center"> <!-- Inner hbox needed due to bug 579776. --> + <vbox id="full-screen-warning-message" align="center"> + <description id="full-screen-domain-text"/> + <description class="full-screen-description" value="&fullscreenExitHint.value;"/> + <vbox id="full-screen-approval-pane" align="center"> + <description class="full-screen-description" value="&fullscreenApproval.value;"/> + <hbox> + <button label="&fullscreenAllowButton.label;" + oncommand="FullScreen.setFullscreenAllowed(true);" + class="full-screen-approval-button"/> + <button label="&fullscreenExitButton.label;" + oncommand="FullScreen.setFullscreenAllowed(false);" + class="full-screen-approval-button"/> + </hbox> + <checkbox id="full-screen-remember-decision"/> + </vbox> + </vbox> + </hbox> + </hbox> + + <vbox id="browser-bottombox" layer="true"> + <notificationbox id="global-notificationbox"/> +#ifdef MOZ_DEVTOOLS + <toolbar id="developer-toolbar" + class="devtools-toolbar" + hidden="true"> +#ifdef XP_MACOSX + <toolbarbutton id="developer-toolbar-closebutton" + class="devtools-closebutton" + oncommand="DeveloperToolbar.hide();" + tooltiptext="&devToolbarCloseButton.tooltiptext;"/> +#endif + <stack class="gclitoolbar-stack-node" flex="1"> + <textbox class="gclitoolbar-input-node" rows="1"/> + <hbox class="gclitoolbar-complete-node"/> + </stack> + <toolbarbutton id="developer-toolbar-toolbox-button" + class="developer-toolbar-button" + observes="devtoolsMenuBroadcaster_DevToolbox" + tooltiptext="&devToolbarToolsButton.tooltip;"/> +#ifndef XP_MACOSX + <toolbarbutton id="developer-toolbar-closebutton" + class="devtools-closebutton" + oncommand="DeveloperToolbar.hide();" + tooltiptext="&devToolbarCloseButton.tooltiptext;"/> +#endif + </toolbar> +#endif + + <toolbar id="addon-bar" + toolbarname="&statusBar.label;" accesskey="&statusBar.accesskey;" + collapsed="true" + class="toolbar-primary chromeclass-toolbar" + context="toolbar-context-menu" toolboxid="navigator-toolbox" + mode="icons" iconsize="small" defaulticonsize="small" + lockiconsize="true" + defaultset="addonbar-closebutton,spring,status-bar" + customizable="true" + key="key_toggleAddonBar"> + <toolbarbutton id="addonbar-closebutton" + class="close-icon" + tooltiptext="&addonBarCloseButton.tooltip;" + oncommand="setToolbarVisibility(this.parentNode, false);"/> + <statusbar id="status-bar" ordinal="1000"/> + </toolbar> + </vbox> + +#ifndef XP_UNIX + <svg:svg height="0"> + <svg:clipPath id="windows-keyhole-forward-clip-path" clipPathUnits="objectBoundingBox"> + <svg:path d="M 0,0 C 0.16,0.11 0.28,0.29 0.28,0.5 0.28,0.71 0.16,0.89 0,1 L 1,1 1,0 0,0 z"/> + </svg:clipPath> + <svg:clipPath id="windows-urlbar-back-button-clip-path" clipPathUnits="userSpaceOnUse"> + <svg:path d="M 0,0 0,7.8 C 2.5,11 4,14 4,18 4,22 2.5,25 0,28 l 0,22 10000,0 0,-50 L 0,0 z"/> + </svg:clipPath> + </svg:svg> +#endif +#ifdef XP_MACOSX + <svg:svg height="0"> + <svg:clipPath id="osx-keyhole-forward-clip-path" clipPathUnits="objectBoundingBox"> + <svg:path d="M 0,0 C 0.15,0.12 0.25,0.3 0.25,0.5 0.25,0.7 0.15,0.88 0,1 L 1,1 1,0 0,0 z"/> + </svg:clipPath> + <svg:clipPath id="osx-urlbar-back-button-clip-path" clipPathUnits="userSpaceOnUse"> + <svg:path d="m 0,-5 0,4.03 C 3.6,1.8 6,6.1 6,11 6,16 3.6,20 0,23 l 0,27 10000,0 0,-55 L 0,-5 z"/> + </svg:clipPath> + <svg:clipPath id="osx-tab-ontop-left-curve-clip-path" clipPathUnits="userSpaceOnUse"> + <svg:path d="M 9,0 C 7.3,0 6,1.3 6,3 l 0,14 c 0,3 -2.2,5 -5,5 l -1,0 0,1 12,0 0,-1 0,-19 0,-3 -3,0 z"/> + </svg:clipPath> + <svg:clipPath id="osx-tab-ontop-right-curve-clip-path" clipPathUnits="userSpaceOnUse"> + <svg:path d="m 0,0 0,3 0,19 0,1 12,0 0,-1 -1,0 C 8.2,22 6,20 6,17 L 6,3 C 6,1.3 4.7,0 3,0 L 0,0 z"/> + </svg:clipPath> + <svg:clipPath id="osx-tab-onbottom-left-curve-clip-path" clipPathUnits="userSpaceOnUse"> + <svg:path d="m 0,0 0,1 1,0 c 2.8,0 5,2.2 5,5 l 0,14 c 0,2 1.3,3 3,3 l 3,0 0,-3 L 12,1 12,0 0,0 z"/> + </svg:clipPath> + <svg:clipPath id="osx-tab-onbottom-right-curve-clip-path" clipPathUnits="userSpaceOnUse"> + <svg:path d="m 0,0 0,1 0,19 0,3 3,0 c 1.7,0 3,-1 3,-3 L 6,6 C 6,3.2 8.2,1 11,1 L 12,1 12,0 0,0 z"/> + </svg:clipPath> + </svg:svg> +#endif + +</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/palemoon/base/content/browserMountPoints.inc b/application/palemoon/base/content/browserMountPoints.inc new file mode 100644 index 0000000000..e4315b04a8 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/content.js b/application/palemoon/base/content/content.js new file mode 100644 index 0000000000..19032eb84f --- /dev/null +++ b/application/palemoon/base/content/content.js @@ -0,0 +1,64 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", + "resource://gre/modules/LoginManagerContent.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils", + "resource://gre/modules/InsecurePasswordUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver", + "resource:///modules/FormSubmitObserver.jsm"); + +// Bug 671101 - directly using webNavigation in this context +// causes docshells to leak +this.__defineGetter__("webNavigation", function () { + return docShell.QueryInterface(Ci.nsIWebNavigation); +}); + +addMessageListener("WebNavigation:LoadURI", function (message) { + let flags = message.json.flags || webNavigation.LOAD_FLAGS_NONE; + + webNavigation.loadURI(message.json.uri, flags, null, null, null); +}); + +// TabChildGlobal +var global = this; + +// Load the form validation popup handler +var formSubmitObserver = new FormSubmitObserver(content, this); + +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; + } +}); + +addEventListener("DOMFormHasPassword", function(event) { + InsecurePasswordUtils.checkForInsecurePasswords(event.target); + LoginManagerContent.onFormPassword(event); +}); +addEventListener("DOMAutoComplete", function(event) { + LoginManagerContent.onUsernameInput(event); +}); +addEventListener("blur", function(event) { + LoginManagerContent.onUsernameInput(event); +}); + +// Lazily load the finder code +addMessageListener("Finder:Initialize", function () { + let {RemoteFinderListener} = Cu.import("resource://gre/modules/RemoteFinder.jsm", {}); + new RemoteFinderListener(global); +});
\ No newline at end of file diff --git a/application/palemoon/base/content/downloadManagerOverlay.xul b/application/palemoon/base/content/downloadManagerOverlay.xul new file mode 100644 index 0000000000..9987820cb9 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/global-scripts.inc b/application/palemoon/base/content/global-scripts.inc new file mode 100644 index 0000000000..b4de574ae9 --- /dev/null +++ b/application/palemoon/base/content/global-scripts.inc @@ -0,0 +1,13 @@ +# -*- 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/downloads/downloads.js"/> +<script type="application/javascript" src="chrome://browser/content/downloads/indicator.js"/> +<script type="application/javascript" src="chrome://global/content/inlineSpellCheckUI.js"/> +<script type="application/javascript" src="chrome://global/content/viewSourceUtils.js"/> diff --git a/application/palemoon/base/content/hiddenWindow.xul b/application/palemoon/base/content/hiddenWindow.xul new file mode 100644 index 0000000000..bf201fd604 --- /dev/null +++ b/application/palemoon/base/content/hiddenWindow.xul @@ -0,0 +1,19 @@ +<?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"?> + +<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/palemoon/base/content/highlighter.css b/application/palemoon/base/content/highlighter.css new file mode 100644 index 0000000000..8fb9d8085d --- /dev/null +++ b/application/palemoon/base/content/highlighter.css @@ -0,0 +1,105 @@ +/* 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/. */ + +.highlighter-container { + pointer-events: none; +} + +.highlighter-controls { + position: absolute; + top: 0; + left: 0; +} + +.highlighter-outline-container { + overflow: hidden; + position: relative; +} + +.highlighter-outline { + position: absolute; +} + +.highlighter-outline[hidden] { + opacity: 0; + pointer-events: none; + display: -moz-box; +} + +.highlighter-outline:not([disable-transitions]) { + transition-property: opacity, top, left, width, height; + transition-duration: 0.1s; + transition-timing-function: linear; +} + +/* + * Node Infobar + */ + +.highlighter-nodeinfobar-container { + position: absolute; + max-width: 95%; +} + +.highlighter-nodeinfobar-container[hidden] { + opacity: 0; + pointer-events: none; + display: -moz-box; +} + +.highlighter-nodeinfobar-container:not([disable-transitions]), +.highlighter-nodeinfobar-container[disable-transitions][force-transitions] { + transition-property: transform, opacity, top, left; + transition-duration: 0.1s; + transition-timing-function: linear; +} + +.highlighter-nodeinfobar-text { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + direction: ltr; +} + +.highlighter-nodeinfobar-button > .toolbarbutton-text { + display: none; +} + +.highlighter-nodeinfobar-container:not([locked]):not(:hover) > .highlighter-nodeinfobar > .highlighter-nodeinfobar-button { + visibility: hidden; +} + +.highlighter-nodeinfobar-container[locked] > .highlighter-nodeinfobar, +.highlighter-nodeinfobar-container:not([locked]):hover > .highlighter-nodeinfobar { + pointer-events: auto; +} + +html|*.highlighter-nodeinfobar-id, +html|*.highlighter-nodeinfobar-classes, +html|*.highlighter-nodeinfobar-pseudo-classes, +html|*.highlighter-nodeinfobar-tagname { + -moz-user-select: text; + -moz-user-focus: normal; + cursor: text; +} + +.highlighter-nodeinfobar-arrow { + display: none; +} + +.highlighter-nodeinfobar-container[position="top"]:not([hide-arrow]) > .highlighter-nodeinfobar-arrow-bottom { + display: block; +} + +.highlighter-nodeinfobar-container[position="bottom"]:not([hide-arrow]) > .highlighter-nodeinfobar-arrow-top { + display: block; +} + +.highlighter-nodeinfobar-container[disabled] { + visibility: hidden; +} + +html|*.highlighter-nodeinfobar-tagname { + text-transform: lowercase; +} diff --git a/application/palemoon/base/content/jsConsoleOverlay.xul b/application/palemoon/base/content/jsConsoleOverlay.xul new file mode 100644 index 0000000000..1bc518d4f8 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/macBrowserOverlay.xul b/application/palemoon/base/content/macBrowserOverlay.xul new file mode 100644 index 0000000000..a4d583e16f --- /dev/null +++ b/application/palemoon/base/content/macBrowserOverlay.xul @@ -0,0 +1,64 @@ +<?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/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/palemoon/base/content/newtab/cells.js b/application/palemoon/base/content/newtab/cells.js new file mode 100644 index 0000000000..47d4ef52d7 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/newtab/drag.js b/application/palemoon/base/content/newtab/drag.js new file mode 100644 index 0000000000..8f0bf674ee --- /dev/null +++ b/application/palemoon/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. + */ +let 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-scrollbox"); + 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/palemoon/base/content/newtab/dragDataHelper.js b/application/palemoon/base/content/newtab/dragDataHelper.js new file mode 100644 index 0000000000..a66e4e87e9 --- /dev/null +++ b/application/palemoon/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 + +let gDragDataHelper = { + get mimeType() { + return "text/x-moz-url"; + }, + + getLinkFromDragEvent: function DragDataHelper_getLinkFromDragEvent(aEvent) { + let dt = aEvent.dataTransfer; + if (!dt || !dt.types.contains(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/palemoon/base/content/newtab/drop.js b/application/palemoon/base/content/newtab/drop.js new file mode 100644 index 0000000000..d7bf305068 --- /dev/null +++ b/application/palemoon/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. + */ +let 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/palemoon/base/content/newtab/dropPreview.js b/application/palemoon/base/content/newtab/dropPreview.js new file mode 100644 index 0000000000..9037623457 --- /dev/null +++ b/application/palemoon/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. + */ +let 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/palemoon/base/content/newtab/dropTargetShim.js b/application/palemoon/base/content/newtab/dropTargetShim.js new file mode 100644 index 0000000000..a85a6ccd65 --- /dev/null +++ b/application/palemoon/base/content/newtab/dropTargetShim.js @@ -0,0 +1,188 @@ +#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. + */ +let 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 DropTargetShim_init() { + let node = gGrid.node; + + // Add drag event handlers. + node.addEventListener("dragstart", this, true); + node.addEventListener("dragend", this, true); + }, + + /** + * Handles all shim events. + */ + handleEvent: function DropTargetShim_handleEvent(aEvent) { + switch (aEvent.type) { + case "dragstart": + this._start(aEvent); + break; + case "dragover": + this._dragover(aEvent); + break; + case "dragend": + this._end(aEvent); + break; + } + }, + + /** + * Handles the 'dragstart' event. + * @param aEvent The 'dragstart' event. + */ + _start: function DropTargetShim_start(aEvent) { + if (aEvent.target.classList.contains("newtab-link")) { + gGrid.lock(); + + // XXX bug 505521 - Listen for dragover on the document. + document.documentElement.addEventListener("dragover", this, false); + } + }, + + /** + * Handles the 'drag' event and determines the current drop target. + * @param aEvent The 'drag' event. + */ + _drag: function DropTargetShim_drag(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; + } + }, + + /** + * Handles the 'dragover' event as long as bug 505521 isn't fixed to get + * current mouse cursor coordinates while dragging. + * @param aEvent The 'dragover' event. + */ + _dragover: function DropTargetShim_dragover(aEvent) { + let sourceNode = aEvent.dataTransfer.mozSourceNode.parentNode; + gDrag.drag(sourceNode._newtabSite, aEvent); + + this._drag(aEvent); + }, + + /** + * Handles the 'dragend' event. + * @param aEvent The 'dragend' event. + */ + _end: function DropTargetShim_end(aEvent) { + // Make sure to determine the current drop target in case the dragenter + // event hasn't been fired. + this._drag(aEvent); + + if (this._lastDropTarget) { + if (aEvent.dataTransfer.mozUserCancelled) { + // The drag operation was cancelled. + this._dispatchEvent(aEvent, "dragexit", this._lastDropTarget); + this._dispatchEvent(aEvent, "dragleave", this._lastDropTarget); + } else { + // A site was successfully dropped. + this._dispatchEvent(aEvent, "drop", this._lastDropTarget); + } + + // Clean up. + this._lastDropTarget = null; + this._cellPositions = null; + } + + gGrid.unlock(); + + // XXX bug 505521 - Remove the document's dragover listener. + document.documentElement.removeEventListener("dragover", this, false); + }, + + /** + * 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 DropTargetShim_findDropTarget() { + // 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 DropTargetShim_dispatchEvent(aEvent, aType, aTarget) { + + let node = aTarget.node; + let event = document.createEvent("DragEvents"); + + event.initDragEvent(aType, true, true, window, 0, 0, 0, 0, 0, false, false, + false, false, 0, node, aEvent.dataTransfer); + + node.dispatchEvent(event); + } +}; diff --git a/application/palemoon/base/content/newtab/grid.js b/application/palemoon/base/content/newtab/grid.js new file mode 100644 index 0000000000..37559a0631 --- /dev/null +++ b/application/palemoon/base/content/newtab/grid.js @@ -0,0 +1,171 @@ +#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 represents the grid that contains all sites. + */ +let gGrid = { + /** + * The DOM node of the grid. + */ + _node: null, + get node() { return this._node; }, + + /** + * The cached DOM fragment for sites. + */ + _siteFragment: null, + + /** + * All cells contained in the grid. + */ + _cells: null, + 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._node; }, + + /** + * Initializes the grid. + * @param aSelector The query selector of the grid. + */ + init: function Grid_init() { + this._node = document.getElementById("newtab-grid"); + this._createSiteFragment(); + this._render(); + }, + + /** + * 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); + }, + + /** + * Refreshes the grid and re-creates all sites. + */ + refresh: function Grid_refresh() { + // Remove all sites. + this.cells.forEach(function (cell) { + let node = cell.node; + let child = node.firstElementChild; + + if (child) + node.removeChild(child); + }, this); + + // Render the grid again. + this._render(); + }, + + /** + * 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"); + }, + + /** + * Creates the newtab grid. + */ + _renderGrid: function Grid_renderGrid() { + let row = document.createElementNS(HTML_NAMESPACE, "div"); + let cell = document.createElementNS(HTML_NAMESPACE, "div"); + row.classList.add("newtab-row"); + cell.classList.add("newtab-cell"); + + // Clear the grid + this._node.innerHTML = ""; + + // Creates the structure of one row + for (let i = 0; i < gGridPrefs.gridColumns; i++) { + row.appendChild(cell.cloneNode(true)); + } + // Creates the grid + for (let j = 0; j < gGridPrefs.gridRows; j++) { + this._node.appendChild(row.cloneNode(true)); + } + + // (Re-)initialize all cells. + let cellElements = this.node.querySelectorAll(".newtab-cell"); + this._cells = [new Cell(this, cell) for (cell of cellElements)]; + }, + + /** + * 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 = + '<a class="newtab-link">' + + ' <span class="newtab-thumbnail"/>' + + ' <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"/>'; + + this._siteFragment = document.createDocumentFragment(); + this._siteFragment.appendChild(site); + }, + + /** + * Renders the sites, creates all sites and puts them into their cells. + */ + _renderSites: function Grid_renderSites() { + let cells = this.cells; + // Put sites into the cells. + let links = gLinks.getLinks(); + let length = Math.min(links.length, cells.length); + + for (let i = 0; i < length; i++) { + if (links[i]) + this.createSite(links[i], cells[i]); + } + }, + + /** + * Renders the grid. + */ + _render: function Grid_render() { + if (this._shouldRenderGrid()) { + this._renderGrid(); + } + + this._renderSites(); + }, + + _shouldRenderGrid : function Grid_shouldRenderGrid() { + let rowsLength = this._node.querySelectorAll(".newtab-row").length; + let cellsLength = this._node.querySelectorAll(".newtab-cell").length; + + return (rowsLength != gGridPrefs.gridRows || + cellsLength != (gGridPrefs.gridRows * gGridPrefs.gridColumns)); + } +}; diff --git a/application/palemoon/base/content/newtab/newTab.css b/application/palemoon/base/content/newtab/newTab.css new file mode 100644 index 0000000000..830e4a8c1a --- /dev/null +++ b/application/palemoon/base/content/newtab/newTab.css @@ -0,0 +1,209 @@ +/* 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/. */ + +input[type=button] { + cursor: pointer; +} + +/* SCROLLBOX */ +#newtab-scrollbox { + display: -moz-box; + position: relative; + -moz-box-flex: 1; + -moz-user-focus: normal; +} + +#newtab-scrollbox:not([page-disabled]) { + overflow: auto; +} + +/* UNDO */ +#newtab-undo-container { + transition: opacity 100ms ease-out; + display: -moz-box; + -moz-box-align: center; + -moz-box-pack: center; +} + +#newtab-undo-container[undo-disabled] { + opacity: 0; + pointer-events: none; +} + +/* TOGGLE */ +#newtab-toggle { + position: absolute; + top: 12px; + right: 12px; +} + +#newtab-toggle:-moz-locale-dir(rtl) { + left: 12px; + right: auto; +} + +/* MARGINS */ +#newtab-vertical-margin { + display: -moz-box; + position: relative; + -moz-box-flex: 1; + -moz-box-orient: vertical; +} + +#newtab-margin-top { + min-height: 50px; + max-height: 80px; + display: -moz-box; + -moz-box-flex: 1; + -moz-box-align: center; + -moz-box-pack: center; +} + +#newtab-margin-bottom { + min-height: 40px; + max-height: 100px; + -moz-box-flex: 1; +} + +#newtab-horizontal-margin { + display: -moz-box; + -moz-box-flex: 5; +} + +.newtab-side-margin { + min-width: 40px; + max-width: 300px; + -moz-box-flex: 1; +} + +/* GRID */ +#newtab-grid { + display: -moz-box; + -moz-box-flex: 5; + -moz-box-orient: vertical; + min-width: 600px; + min-height: 400px; + transition: 100ms ease-out; + transition-property: opacity; +} + +#newtab-grid[page-disabled] { + opacity: 0; +} + +#newtab-grid[locked], +#newtab-grid[page-disabled] { + pointer-events: none; +} + +/* ROWS */ +.newtab-row { + display: -moz-box; + -moz-box-orient: horizontal; + -moz-box-direction: normal; + -moz-box-flex: 1; +} + +/* CELLS */ +.newtab-cell { + display: -moz-box; + -moz-box-flex: 1; +} + +/* 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; +} + +.newtab-thumbnail { + opacity: .8; + transition: opacity 100ms ease-out; +} + +.newtab-thumbnail[dragged], +.newtab-link:-moz-focusring > .newtab-thumbnail, +.newtab-site:hover > .newtab-link > .newtab-thumbnail { + opacity: 1; +} + +/* TITLES */ +.newtab-title { + position: absolute; + left: 0; + right: 0; + bottom: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* CONTROLS */ +.newtab-control { + position: absolute; + top: 4px; + opacity: 0; + transition: opacity 100ms ease-out; +} + +.newtab-control:-moz-focusring, +.newtab-site:hover > .newtab-control { + opacity: 1; +} + +.newtab-control[dragged] { + opacity: 0 !important; +} + +@media (-moz-touch-enabled) { + .newtab-control { + opacity: 1; + } +} + +.newtab-control-pin:-moz-locale-dir(ltr), +.newtab-control-block:-moz-locale-dir(rtl) { + left: 4px; +} + +.newtab-control-block:-moz-locale-dir(ltr), +.newtab-control-pin:-moz-locale-dir(rtl) { + right: 4px; +} + +/* 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; +} diff --git a/application/palemoon/base/content/newtab/newTab.js b/application/palemoon/base/content/newtab/newTab.js new file mode 100644 index 0000000000..77c929125c --- /dev/null +++ b/application/palemoon/base/content/newtab/newTab.js @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let Cu = Components.utils; +let 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/NewTabUtils.jsm"); +Cu.import("resource:///modules/promise.js"); + +XPCOMUtils.defineLazyModuleGetter(this, "Rect", + "resource://gre/modules/Geometry.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +let { + 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) gStringBundle.GetStringFromName('newtab.' + name); + +function inPrivateBrowsingMode() { + return PrivateBrowsingUtils.isWindowPrivate(window); +} + +const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; + +#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 + +// Everything is loaded. Initialize the New Tab Page. +gPage.init(); diff --git a/application/palemoon/base/content/newtab/newTab.xul b/application/palemoon/base/content/newtab/newTab.xul new file mode 100644 index 0000000000..6fc202f296 --- /dev/null +++ b/application/palemoon/base/content/newtab/newTab.xul @@ -0,0 +1,55 @@ +<?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://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/content/newtab/newTab.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/newtab/newTab.css" type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd"> + %newTabDTD; +]> + +<xul:window id="newtab-window" xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&newtab.pageTitle;"> + + <div id="newtab-scrollbox"> + + <div id="newtab-vertical-margin"> + <div id="newtab-margin-top"> + <div id="newtab-undo-container" undo-disabled="true"> + <xul:label id="newtab-undo-label" + value="&newtab.undo.removedLabel;" /> + <xul:button id="newtab-undo-button" tabindex="-1" + label="&newtab.undo.undoButton;" + class="newtab-undo-button" /> + <xul:button id="newtab-undo-restore-button" tabindex="-1" + label="&newtab.undo.restoreButton;" + class="newtab-undo-button" /> + <xul:toolbarbutton id="newtab-undo-close-button" tabindex="-1" + class="close-icon" + tooltiptext="&newtab.undo.closeTooltip;" /> + </div> + </div> + + <div id="newtab-horizontal-margin"> + <div class="newtab-side-margin"/> + + <div id="newtab-grid"> + </div> + + <div class="newtab-side-margin"/> + </div> + + <div id="newtab-margin-bottom"/> + </div> + <input id="newtab-toggle" type="button"/> + </div> + + <xul:script type="text/javascript;version=1.8" + src="chrome://browser/content/newtab/newTab.js"/> +</xul:window> diff --git a/application/palemoon/base/content/newtab/page.js b/application/palemoon/base/content/newtab/page.js new file mode 100644 index 0000000000..afe5bfba8a --- /dev/null +++ b/application/palemoon/base/content/newtab/page.js @@ -0,0 +1,135 @@ +#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 represents the whole 'New Tab Page' and takes care of + * initializing all its components. + */ +let 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); + + // Listen for toggle button clicks. + let button = document.getElementById("newtab-toggle"); + button.addEventListener("click", this, false); + + // Check if the new tab feature is enabled. + let enabled = gAllPages.enabled; + if (enabled) + this._init(); + + this._updateAttributes(enabled); + }, + + /** + * Listens for notifications specific to this page. + */ + observe: function Page_observe() { + let enabled = gAllPages.enabled; + this._updateAttributes(enabled); + + // Initialize the whole page if we haven't done that, yet. + if (enabled) { + this._init(); + } else { + gUndoDialog.hide(); + } + }, + + /** + * Updates the whole page and the grid when the storage has changed. + */ + update: function Page_update() { + // The grid might not be ready yet as we initialize it asynchronously. + if (gGrid.ready) { + gGrid.refresh(); + } + }, + + /** + * 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; + + gLinks.populateCache(function () { + // 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 + }.bind(this)); + }, + + /** + * 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-scrollbox, #newtab-toggle, #newtab-grid"; + 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"); + } + + // Update the toggle button's title. + let toggle = document.getElementById("newtab-toggle"); + toggle.setAttribute("title", newTabString(aValue ? "hide" : "show")); + }, + + /** + * Handles all page events. + */ + handleEvent: function Page_handleEvent(aEvent) { + switch (aEvent.type) { + case "unload": + gAllPages.unregister(this); + break; + case "click": + gAllPages.enabled = !gAllPages.enabled; + 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; + } + } +}; diff --git a/application/palemoon/base/content/newtab/sites.js b/application/palemoon/base/content/newtab/sites.js new file mode 100644 index 0000000000..873ef201c5 --- /dev/null +++ b/application/palemoon/base/content/newtab/sites.js @@ -0,0 +1,192 @@ +#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 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; }, + + /** + * 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). + */ + pin: function Site_pin(aIndex) { + if (typeof aIndex == "undefined") + aIndex = this.cell.index; + + this._updateAttributes(true); + gPinnedLinks.pin(this._link, aIndex); + }, + + /** + * 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) { + control.setAttribute("pinned", true); + control.setAttribute("title", newTabString("unpin")); + } else { + control.removeAttribute("pinned"); + control.setAttribute("title", newTabString("pin")); + } + }, + + /** + * Renders the site's data (fills the HTML fragment). + */ + _render: function Site_render() { + let url = this.url; + let title = this.title || url; + let tooltip = (title == url ? title : title + "\n" + url); + + let link = this._querySelector(".newtab-link"); + link.setAttribute("title", tooltip); + link.setAttribute("href", url); + this._querySelector(".newtab-title").textContent = title; + + if (this.isPinned()) + this._updateAttributes(true); + + let thumbnailURL = PageThumbs.getThumbnailURL(this.url); + let thumbnail = this._querySelector(".newtab-thumbnail"); + thumbnail.style.backgroundImage = "url(" + thumbnailURL + ")"; + }, + + /** + * 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); + + let controls = this.node.querySelectorAll(".newtab-control"); + for (let i = 0; i < controls.length; i++) + controls[i].addEventListener("click", this, false); + }, + + /** + * 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); + sc.speculativeConnect(uri, null); + }, + + /** + * Handles all site events. + */ + handleEvent: function Site_handleEvent(aEvent) { + switch (aEvent.type) { + case "click": + aEvent.preventDefault(); + if (aEvent.target.classList.contains("newtab-control-block")) + this.block(); + else if (this.isPinned()) + this.unpin(); + else + this.pin(); + break; + case "mouseover": + this._node.removeEventListener("mouseover", this, false); + this._speculativeConnect(); + break; + case "dragstart": + gDrag.start(this, aEvent); + break; + case "drag": + gDrag.drag(this, aEvent); + break; + case "dragend": + gDrag.end(this, aEvent); + break; + } + } +}; diff --git a/application/palemoon/base/content/newtab/transformations.js b/application/palemoon/base/content/newtab/transformations.js new file mode 100644 index 0000000000..6d1554f5ff --- /dev/null +++ b/application/palemoon/base/content/newtab/transformations.js @@ -0,0 +1,265 @@ +#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. + */ +let 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, 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; + + let deferred = Promise.defer(); + batch.push(deferred.promise); + let cb = function () deferred.resolve(); + + if (!cells[aIndex]) + // The site disappeared from the grid, hide it. + this.hideSite(aSite, cb); + else if (this._getNodeOpacity(aSite.node) != 1) + // The site disappeared before but is now back, show it. + this.showSite(aSite, cb); + else + // The site's position has changed, move it around. + this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: cb}); + }, this); + + let wait = Promise.promised(function () callback && callback()); + wait.apply(null, batch); + }, + + /** + * Listens for the 'transitionend' event on a given node and calls the given + * callback. + * @param aNode The node that is transitioned. + * @param aCallback The callback to call when finished. + */ + _whenTransitionEnded: + function Transformation_whenTransitionEnded(aNode, aCallback) { + + aNode.addEventListener("transitionend", function onEnd() { + aNode.removeEventListener("transitionend", onEnd, false); + aCallback(); + }, false); + }, + + /** + * 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, 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/palemoon/base/content/newtab/undo.js b/application/palemoon/base/content/newtab/undo.js new file mode 100644 index 0000000000..5f619e9806 --- /dev/null +++ b/application/palemoon/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. + */ +let 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/palemoon/base/content/newtab/updater.js b/application/palemoon/base/content/newtab/updater.js new file mode 100644 index 0000000000..7b483e037f --- /dev/null +++ b/application/palemoon/base/content/newtab/updater.js @@ -0,0 +1,186 @@ +#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. + */ +let 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); + + let self = this; + + // Remove sites that are no longer in the grid. + this._removeLegacySites(sites, function () { + // Freeze all site positions so that we can move their DOM nodes around + // without any visual impact. + self._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. + self._moveSiteNodes(sites); + + // Now it's time to animate the sites actually moving to their new + // positions. + self._rearrangeSites(sites, function () { + // Try to fill empty cells and finish. + self._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; + + let deferred = Promise.defer(); + batch.push(deferred.promise); + + // 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); + deferred.resolve(); + }); + }); + + let wait = Promise.promised(aCallback); + wait.apply(null, batch); + }, + + /** + * 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; + let batch = []; + + // Find empty cells and fill them. + sites.forEach(function (aSite, aIndex) { + if (aSite || !aLinks[aIndex]) + return; + + let deferred = Promise.defer(); + batch.push(deferred.promise); + + // 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, function () deferred.resolve()); + }); + + let wait = Promise.promised(aCallback); + wait.apply(null, batch); + } +}; diff --git a/application/palemoon/base/content/nsContextMenu.js b/application/palemoon/base/content/nsContextMenu.js new file mode 100644 index 0000000000..8e6bc96c23 --- /dev/null +++ b/application/palemoon/base/content/nsContextMenu.js @@ -0,0 +1,1588 @@ +# 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/PrivateBrowsingUtils.jsm"); + +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; + if (!aIsShift) { + this.hasPageMenu = PageMenu.maybeBuildAndAttachMenu(this.target, + aXulMenu); + } + + this.isFrameImage = document.getElementById("isFrameImage"); + this.ellipsis = "\u2026"; + try { + this.ellipsis = gPrefService.getComplexValue("intl.ellipsis", + Ci.nsIPrefLocalizedString).data; + } catch (e) { } + + this.isContentSelected = this.isContentSelection(); + this.onPlainTextLink = false; + + // Initialize (disable/remove) menu items. + this.initItems(); + }, + + hiding: function CM_hiding() { + InlineSpellCheckerUI.clearSuggestionsFromMenu(); + InlineSpellCheckerUI.clearDictionaryListFromMenu(); + InlineSpellCheckerUI.uninit(); + }, + + 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(); + }, + + 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)); + } + + // Time to do some bad things and see if we've highlighted a URL that + // isn't actually linked. + if (this.isTextSelected && !this.onLink) { + // Ok, we have some text, let's figure out if it looks like a URL. + let selection = document.commandDispatcher.focusedWindow + .getSelection(); + let linkText = selection.toString().trim(); + let uri; + if (/^(?:https?|ftp):/i.test(linkText)) { + try { + uri = makeURI(linkText); + } catch (ex) {} + } + // Check if this could be a valid url, just missing the protocol. + else if (/^[-a-z\d\.]+\.[-a-z\d]{2,}[-_=~:#%&\?\w\/\.]*$/i.test(linkText)) { + let uriFixup = Cc["@mozilla.org/docshell/urifixup;1"] + .getService(Ci.nsIURIFixup); + try { + uri = uriFixup.createFixupURI(linkText, uriFixup.FIXUP_FLAG_NONE); + } catch (ex) {} + } + + if (uri && uri.host) { + this.linkURI = uri; + this.linkURL = this.linkURI.spec; + this.onPlainTextLink = true; + } + } + + var shouldShow = this.onSaveableLink || isMailtoInternal || this.onPlainTextLink; + var isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + this.showItem("context-openlink", shouldShow && !isWindowPrivate); + this.showItem("context-openlinkprivate", shouldShow); + this.showItem("context-openlinkintab", shouldShow); + this.showItem("context-openlinkincurrent", shouldShow); + 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-back", shouldShow); + this.showItem("context-forward", 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"); + this.showItem("context-sep-stop", !!stopReloadItem); + + // 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.mozFullScreenElement != 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); + this.showItem("context-sendpage", shouldShow); + + // Save+Send link depends on whether we're in a link, or selected text matches valid URL pattern. + this.showItem("context-savelink", this.onSaveableLink || this.onPlainTextLink); + this.showItem("context-sendlink", 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-sendaudio", this.onAudio); + this.setItemAttr("context-sendvideo", "disabled", !this.mediaURL); + this.setItemAttr("context-sendaudio", "disabled", !this.mediaURL); + }, + + 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); + this.showItem("context-viewsource", shouldShow); + this.showItem("context-viewinfo", shouldShow); +#ifdef MOZ_DEVTOOLS + var showInspect = gPrefService.getBoolPref("devtools.inspector.enabled"); + this.showItem("inspect-separator", showInspect); + this.showItem("context-inspect", showInspect); +#endif + + 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 = this.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); + }, + + initMiscItems: function CM_initMiscItems() { + // Use "Bookmark This Link" if on a link. + this.showItem("context-bookmarkpage", + !(this.isContentSelected || this.onTextInput || this.onLink || + this.onImage || this.onVideo || this.onAudio)); + 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 (mimeTypeIsTextBased(this.target.ownerDocument.contentType)) + this.isFrameImage.removeAttribute('hidden'); + else + this.isFrameImage.setAttribute('hidden', 'true'); + } + + // BiDi UI + this.showItem("context-sep-bidi", top.gBidiUI); + this.showItem("context-bidi-text-direction-toggle", + this.onTextInput && top.gBidiUI); + this.showItem("context-bidi-page-direction-toggle", + !this.onTextInput && top.gBidiUI); + }, + + initSpellingItems: function() { + var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck; + var onMisspelling = InlineSpellCheckerUI.overMisspelling; + var showUndo = canSpell && InlineSpellCheckerUI.canUndo(); + this.showItem("spell-check-enabled", canSpell); + this.showItem("spell-separator", canSpell || this.onEditableArea); + 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", canSpell && InlineSpellCheckerUI.enabled); + if (canSpell) { + var dictMenu = document.getElementById("spell-dictionaries-menu"); + var dictSep = document.getElementById("spell-language-separator"); + InlineSpellCheckerUI.addDictionaryListToMenu(dictMenu, dictSep); + 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-add-dictionaries-main", true); + } + 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.showItem("context-media-showcontrols", onMedia && !this.target.controls); + this.showItem("context-media-hidecontrols", onMedia && this.target.controls); + this.showItem("context-video-fullscreen", this.onVideo && this.target.ownerDocument.mozFullScreenElement == null); + var statsShowing = this.onVideo && XPCNativeWrapper.unwrap(this.target).mozMediaStatisticsShowing; + this.showItem("context-video-showstats", this.onVideo && this.target.controls && !statsShowing); + this.showItem("context-video-hidestats", this.onVideo && this.target.controls && statsShowing); + + // 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-150x", "checked", this.target.playbackRate == 1.5); + this.setItemAttr("context-media-playbackrate-200x", "checked", this.target.playbackRate == 2.0); + 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-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.target.readyState >= this.target.HAVE_CURRENT_DATA; + this.setItemAttr("context-video-saveimage", "disabled", !canSaveSnapshot); + this.setItemAttr("context-video-fullscreen", "disabled", hasError); + this.setItemAttr("context-video-showstats", "disabled", hasError); + this.setItemAttr("context-video-hidestats", "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); + }, + + inspectNode: function CM_inspectNode() { + let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); + let gBrowser = this.browser.ownerDocument.defaultView.gBrowser; + let tt = devtools.TargetFactory.forTab(gBrowser.selectedTab); + return gDevTools.showToolbox(tt, "inspector").then(function(toolbox) { + let inspector = toolbox.getCurrentPanel(); + inspector.selection.setNode(this.target, "browser-context-menu"); + }.bind(this)); + }, + + // Set various context menu attributes based on the state of the world. + setTarget: function (aNode, aRangeParent, aRangeOffset) { + const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + if (aNode.namespaceURI == xulNS || + aNode.nodeType == Node.DOCUMENT_NODE || + this.isDisabledForEvents(aNode)) { + this.shouldDisplay = false; + return; + } + + // Initialize contextual info. + this.onImage = false; + this.onLoadedImage = false; + this.onCompletedImage = false; + this.onCanvas = false; + this.onVideo = false; + this.onAudio = false; + this.onTextInput = false; + this.onKeywordField = false; + this.mediaURL = ""; + this.onLink = false; + this.onMailtoLink = false; + this.onSaveableLink = false; + this.link = null; + this.linkURL = ""; + this.linkURI = null; + this.linkProtocol = ""; + this.linkDownload = ""; + 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.textSelected = getBrowserSelection(); + this.isTextSelected = this.textSelected.length != 0; + + // Remember the node that was clicked. + this.target = aNode; + + this.browser = this.target.ownerDocument.defaultView + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + + // Check if we are in a synthetic document (stand alone image, video, etc.). + this.inSyntheticDoc = this.target.ownerDocument.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. + 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)) + this.onCompletedImage = true; + + this.mediaURL = this.target.currentURI.spec; + } + else if (this.target instanceof HTMLCanvasElement) { + this.onCanvas = true; + } + else if (this.target instanceof HTMLVideoElement) { + this.mediaURL = this.target.currentSrc || this.target.src; + // 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; + this.mediaURL = this.target.currentSrc || this.target.src; + } + else if (this.target instanceof HTMLInputElement ) { + this.onTextInput = this.isTargetATextBox(this.target); + // Allow spellchecking UI on all text and search inputs. + if (this.onTextInput && ! this.target.readOnly && + (this.target.type == "text" || this.target.type == "search")) { + this.onEditableArea = true; + InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor); + InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset); + } + this.onKeywordField = this.isTargetAKeywordField(this.target); + } + else if (this.target instanceof HTMLTextAreaElement) { + this.onTextInput = true; + if (!this.target.readOnly) { + this.onEditableArea = true; + InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor); + InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset); + } + } + else if (this.target instanceof HTMLHtmlElement) { + var bodyElt = this.target.ownerDocument.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 + ((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.linkProtocol = this.getLinkProtocol(); + this.onMailtoLink = (this.linkProtocol == "mailto"); + this.onSaveableLink = this.isLinkSaveable( this.link ); + try { + if (elem.download) { + // Ignore download attribute on cross-origin links? + // This shoudn't be an issue because the download link presents + // the originating URL domain and protocol to help user understand + // from where file is downloaded and make right decision. + // If we decide we want this restriction: + // 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 = this.target.ownerDocument.defaultView; + if (docDefaultView != docDefaultView.top) { + this.inFrame = true; + + if (this.target.ownerDocument.isSrcdocDocument) { + this.inSrcdocFrame = true; + } + } + + // if the document is editable, show context menu like in text inputs + if (!this.onEditableArea) { + var win = this.target.ownerDocument.defaultView; + if (win) { + var isEditable = false; + try { + var editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEditingSession); + if (editingSession.windowIsEditable(win) && + this.getComputedStyle(this.target, "-moz-user-modify") == "read-write") { + isEditable = true; + } + } + catch(ex) { + // If someone built with composer disabled, we can't get an editing session. + } + + if (isEditable) { + 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; + InlineSpellCheckerUI.init(editingSession.getEditorForWindow(win)); + var canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck; + InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset); + this.showItem("spell-check-enabled", canSpell); + this.showItem("spell-separator", canSpell); + } + } + } + }, + + // 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; + }, + + // Open linked-to URL in a new window. + openLink : function () { + var doc = this.target.ownerDocument; + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + openLinkIn(this.linkURL, "window", + { charset: doc.characterSet, + referrerURI: doc.documentURIObject }); + }, + + // Open linked-to URL in a new private window. + openLinkInPrivateWindow : function () { + var doc = this.target.ownerDocument; + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + openLinkIn(this.linkURL, "window", + { charset: doc.characterSet, + referrerURI: doc.documentURIObject, + private: true }); + }, + + // Open linked-to URL in a new tab. + openLinkInTab: function() { + var doc = this.target.ownerDocument; + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + openLinkIn(this.linkURL, "tab", + { charset: doc.characterSet, + referrerURI: doc.documentURIObject }); + }, + + // open URL in current tab + openLinkInCurrent: function() { + var doc = this.target.ownerDocument; + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + openLinkIn(this.linkURL, "current", + { charset: doc.characterSet, + referrerURI: doc.documentURIObject }); + }, + + // Open frame in a new tab. + openFrameInTab: function() { + var doc = this.target.ownerDocument; + var frameURL = doc.location.href; + var referrer = doc.referrer; + openLinkIn(frameURL, "tab", + { charset: doc.characterSet, + referrerURI: referrer ? makeURI(referrer) : null }); + }, + + // Reload clicked-in frame. + reloadFrame: function() { + this.target.ownerDocument.location.reload(); + }, + + // Open clicked-in frame in its own window. + openFrame: function() { + var doc = this.target.ownerDocument; + var frameURL = doc.location.href; + var referrer = doc.referrer; + openLinkIn(frameURL, "window", + { charset: doc.characterSet, + referrerURI: referrer ? makeURI(referrer) : null }); + }, + + // Open clicked-in frame in the same window. + showOnlyThisFrame: function() { + var doc = this.target.ownerDocument; + var frameURL = doc.location.href; + + urlSecurityCheck(frameURL, this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + var referrer = doc.referrer; + openUILinkIn(frameURL, "current", { disallowInheritPrincipal: true, + referrerURI: referrer ? makeURI(referrer) : null }); + }, + + reload: function(event) { + BrowserReloadOrDuplicate(event); + }, + + // View Partial Source + viewPartialSource: function(aContext) { + var focusedWindow = document.commandDispatcher.focusedWindow; + if (focusedWindow == window) + focusedWindow = content; + + var docCharset = null; + if (focusedWindow) + docCharset = "charset=" + focusedWindow.document.characterSet; + + // "View Selection Source" and others such as "View MathML Source" + // are mutually exclusive, with the precedence given to the selection + // when there is one + var reference = null; + if (aContext == "selection") + reference = focusedWindow.getSelection(); + else if (aContext == "mathml") + reference = this.target; + else + throw "not reached"; + + // unused (and play nice for fragments generated via XSLT too) + var docUrl = null; + window.openDialog("chrome://global/content/viewPartialSource.xul", + "_blank", "scrollbars,resizable,chrome,dialog=no", + docUrl, docCharset, reference, aContext); + }, + + // Open new "view source" window with the frame's URL. + viewFrameSource: function() { + BrowserViewSourceOfDocument(this.target.ownerDocument); + }, + + viewInfo: function() { + BrowserPageInfo(this.target.ownerDocument.defaultView.top.document); + }, + + viewImageInfo: function() { + BrowserPageInfo(this.target.ownerDocument.defaultView.top.document, + "mediaTab", this.target); + }, + + viewFrameInfo: function() { + BrowserPageInfo(this.target.ownerDocument); + }, + + reloadImage: function(e) { + urlSecurityCheck(this.mediaURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + + if (this.target instanceof Ci.nsIImageLoadingContent) + this.target.forceReload(); + }, + + // Change current window to the URL of the image, video, or audio. + viewMedia: function(e) { + var viewURL; + var doc = this.target.ownerDocument; + if (this.onCanvas) { + var target = this.target; + var win = doc.defaultView; + if (!win) { + Components.utils.reportError( + "View Image (on the <canvas> element):\n" + + "This feature cannot be used, because it hasn't found " + + "an appropriate window."); + } else { + new Promise.resolve({then: function (resolve) { + target.toBlob((blob) => { + resolve(win.URL.createObjectURL(blob)); + }) + }}).then(function (blobURL) { + openUILink(blobURL, e, { disallowInheritPrincipal: true, + referrerURI: doc.documentURIObject }); + }, Components.utils.reportError); + } + } else { + viewURL = this.mediaURL; + urlSecurityCheck(viewURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + let doc = this.target.ownerDocument; + openUILink(viewURL, e, { disallowInheritPrincipal: true, + referrerURI: doc.documentURIObject }); + } + }, + + saveVideoFrameAsImage: function () { + urlSecurityCheck(this.mediaURL, this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + let name = ""; + 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"; + var video = this.target; + var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + var ctxDraw = canvas.getContext("2d"); + ctxDraw.drawImage(video, 0, 0); + saveImageURL(canvas.toDataURL("image/jpeg", ""), name, "SaveImageTitle", true, false, document.documentURIObject, this.target.ownerDocument); + }, + + fullScreenVideo: function () { + let video = this.target; + if (document.mozFullScreenEnabled) + video.mozRequestFullScreen(); + }, + + leaveDOMFullScreen: function() { + document.mozCancelFullScreen(); + }, + + // Change current window to the URL of the background image. + viewBGImage: function(e) { + urlSecurityCheck(this.bgImageURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + var doc = this.target.ownerDocument; + openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true, + referrerURI: doc.documentURIObject }); + }, + + disableSetDesktopBackground: function() { + // Disable the Set as Desktop Background menu item if we're still trying + // to load the image or the load failed. + if (!(this.target instanceof Ci.nsIImageLoadingContent)) + return true; + + if (("complete" in this.target) && !this.target.complete) + return true; + + if (this.target.currentURI.schemeIs("javascript")) + return true; + + var request = this.target + .QueryInterface(Ci.nsIImageLoadingContent) + .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); + if (!request) + return true; + + return false; + }, + + setDesktopBackground: function() { + // Paranoia: check disableSetDesktopBackground again, in case the + // image changed since the context menu was initiated. + if (this.disableSetDesktopBackground()) + return; + + urlSecurityCheck(this.target.currentURI.spec, + this.target.ownerDocument.nodePrincipal); + + // 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. + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var dbWin = wm.getMostRecentWindow("Shell:SetDesktopBackground"); + if (dbWin) { + dbWin.gSetBackground.init(this.target); + dbWin.focus(); + } + else { + openDialog(kDesktopBackgroundURL, "", + "centerscreen,chrome,dialog=no,dependent,resizable=no", + this.target); + } +#else + // On non-Mac platforms, the Set Wallpaper dialog is modal. + openDialog(kDesktopBackgroundURL, "", + "centerscreen,chrome,dialog,modal,dependent", + this.target); +#endif + }, + + // Save URL of clicked-on frame. + saveFrame: function () { + saveDocument(this.target.ownerDocument); + }, + + // 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, + 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); + promptSvc.alert(doc.defaultView, title, msg); + } catch (ex) {} + return; + } + + var extHelperAppSvc = + Cc["@mozilla.org/uriloader/external-helper-app-service;1"]. + getService(Ci.nsIExternalHelperAppService); + var channel = aRequest.QueryInterface(Ci.nsIChannel); + this.extListener = + extHelperAppSvc.doContent(channel.contentType, aRequest, + doc.defaultView, true); + 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, doc.documentURIObject, 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; + } + } + + // set up a channel to do the saving + var ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var channel = ioService.newChannelFromURI(makeURI(linkURL)); + if (linkDownload) + channel.contentDispositionFilename = linkDownload; + if (channel instanceof Ci.nsIPrivateBrowsingChannel) { + let docIsPrivate = PrivateBrowsingUtils.isWindowPrivate(doc.defaultView); + 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 = doc.documentURIObject; + 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.asyncOpen(new saveAsListener(), null); + }, + + // Save URL of clicked-on link. + saveLink: function() { + var doc = this.target.ownerDocument; + var linkText; + // If selected text is found to match valid URL pattern. + if (this.onPlainTextLink) + linkText = document.commandDispatcher.focusedWindow.getSelection().toString().trim(); + else + linkText = this.linkText(); + urlSecurityCheck(this.linkURL, doc.nodePrincipal); + + this.saveHelper(this.linkURL, linkText, null, true, doc, + this.linkDownload); + }, + + sendLink: function() { + // we don't know the title of the link so pass in an empty string + MailIntegration.sendMessage( this.linkURL, "" ); + }, + + // Backwards-compatibility wrapper + saveImage : function() { + if (this.onCanvas || this.onImage) + this.saveMedia(); + }, + + // Save URL of the clicked upon image, video, or audio. + saveMedia: function() { + var doc = this.target.ownerDocument; + if (this.onCanvas) { + // Bypass cache, since it's a blob: URL. + var target = this.target; + var win = doc.defaultView; + if (!win) { + Components.utils.reportError( + "Save Image As (on the <canvas> element):\n" + + "This feature cannot be used, because it hasn't found " + + "an appropriate window."); + } else { + new Promise.resolve({then: function (resolve) { + target.toBlob((blob) => { + resolve(win.URL.createObjectURL(blob)); + }) + }}).then(function (blobURL) { + saveImageURL(blobURL, "canvas.png", "SaveImageTitle", true, + false, doc.documentURIObject, doc); + }, Components.utils.reportError); + } + } else if (this.onImage) { + urlSecurityCheck(this.mediaURL, doc.nodePrincipal); + saveImageURL(this.mediaURL, null, "SaveImageTitle", false, + false, doc.documentURIObject, doc); + } else if (this.onVideo || this.onAudio) { + urlSecurityCheck(this.mediaURL, doc.nodePrincipal); + var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle"; + this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, ""); + } + }, + + // Backwards-compatibility wrapper + sendImage : function() { + if (this.onCanvas || this.onImage) + this.sendMedia(); + }, + + sendMedia: function() { + MailIntegration.sendMessage(this.mediaURL, ""); + }, + + playPlugin: function() { + gPluginHandler._showClickToPlayNotification(this.browser, this.target); + }, + + hidePlugin: function() { + gPluginHandler.hideClickToPlayOverlay(this.target); + }, + + // 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 { + var characterSet = this.target.ownerDocument.characterSet; + const textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"]. + getService(Ci.nsITextToSubURI); + addresses = textToSubURI.unEscapeURIForUI(characterSet, addresses); + } + catch(ex) { + // Do nothing. + } + + var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + clipboard.copyString(addresses, document); + }, + + /////////////// + // 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.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. + linkText: 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; + }, + + // Returns true if anything is selected. + isContentSelection: function() { + return !document.commandDispatcher.focusedWindow.getSelection().isCollapsed; + }, + + 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"; + }, + + isDisabledForEvents: function(aNode) { + let ownerDoc = aNode.ownerDocument; + return + ownerDoc.defaultView && + ownerDoc.defaultView + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils) + .isNodeDisabledForEvents(aNode); + }, + + isTargetATextBox: function(node) { + if (node instanceof HTMLInputElement) + return node.mozIsTextField(false); + + return (node instanceof HTMLTextAreaElement); + }, + + isTargetAKeywordField: function(aNode) { + if (!(aNode instanceof HTMLInputElement)) + return false; + + var form = aNode.form; + if (!form || aNode.type == "password") + return false; + + var method = form.method.toUpperCase(); + + // These are the following types of forms we can create keywords for: + // + // method encoding type can create keyword + // GET * YES + // * YES + // POST YES + // POST application/x-www-form-urlencoded YES + // POST text/plain NO (a little tricky to do) + // POST multipart/form-data NO + // POST everything else YES + return (method == "GET" || method == "") || + (form.enctype != "text/plain") && (form.enctype != "multipart/form-data"); + }, + + // 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() { + var linkText; + // If selected text is found to match valid URL pattern. + if (this.onPlainTextLink) + linkText = document.commandDispatcher.focusedWindow.getSelection().toString().trim(); + else + linkText = this.linkText(); + window.top.PlacesCommandHook.bookmarkLink(PlacesUtils.bookmarksMenuFolderId, this.linkURL, + linkText); + }, + + addBookmarkForFrame: function CM_addBookmarkForFrame() { + var doc = this.target.ownerDocument; + var uri = doc.documentURIObject; + + var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri); + if (itemId == -1) { + var title = doc.title; + var description = PlacesUIUtils.getDescriptionFromDocument(doc); + PlacesUIUtils.showBookmarkDialog({ action: "add" + , type: "bookmark" + , uri: uri + , title: title + , description: description + , hiddenRows: [ "description" + , "location" + , "loadInSidebar" + , "keyword" ] + }, window.top); + } + else { + PlacesUIUtils.showBookmarkDialog({ action: "edit" + , type: "bookmark" + , itemId: itemId + }, window.top); + } + }, + + savePageAs: function CM_savePageAs() { + saveDocument(this.browser.contentDocument); + }, + + sendPage: function CM_sendPage() { + MailIntegration.sendLinkForWindow(this.browser.contentWindow); + }, + + printFrame: function CM_printFrame() { + PrintUtils.print(this.target.ownerDocument.defaultView); + }, + + switchPageDirection: function CM_switchPageDirection() { + SwitchDocumentDirection(this.browser.contentWindow); + }, + + mediaCommand : function CM_mediaCommand(command, data) { + var media = this.target; + + switch (command) { + case "play": + media.play(); + break; + case "pause": + media.pause(); + break; + case "mute": + media.muted = true; + break; + case "unmute": + media.muted = false; + break; + case "playbackRate": + media.playbackRate = data; + break; + case "hidecontrols": + media.removeAttribute("controls"); + break; + case "showcontrols": + media.setAttribute("controls", "true"); + break; + case "hidestats": + case "showstats": + var event = media.ownerDocument.createEvent("CustomEvent"); + event.initCustomEvent("media-showStatistics", false, true, command == "showstats"); + media.dispatchEvent(event); + break; + } + }, + + copyMediaLocation : function () { + var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + clipboard.copyString(this.mediaURL, document); + }, + + 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"); + var selectedText = this.isTextSelected ? this.textSelected : this.linkText(); + + // Store searchTerms in context menu item so we know what to search onclick + menuItem.searchTerms = selectedText; + + if (selectedText.length > 15) + selectedText = selectedText.substr(0,15) + this.ellipsis; + + // Use the current engine if the search bar is visible, the default + // engine otherwise. + var engineName = ""; + var ss = Cc["@mozilla.org/browser/search-service;1"]. + getService(Ci.nsIBrowserSearchService); + if (isElementVisible(BrowserSearch.searchBar)) + engineName = ss.currentEngine.name; + else + engineName = ss.defaultEngine.name; + + // format "Search <engine> for <selection>" string to show in menu + var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch", + [engineName, + selectedText]); + menuItem.label = menuLabel; + menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey"); + } +}; diff --git a/application/palemoon/base/content/openLocation.js b/application/palemoon/base/content/openLocation.js new file mode 100644 index 0000000000..5b731c7e86 --- /dev/null +++ b/application/palemoon/base/content/openLocation.js @@ -0,0 +1,134 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var browser; +var dialog = {}; +var pref = null; +let openLocationModule = {}; +try { + pref = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); +} catch (ex) { + // not critical, remain silent +} + +Components.utils.import("resource:///modules/openLocationLastURL.jsm", openLocationModule); +let gOpenLocationLastURL = new openLocationModule.OpenLocationLastURL(window.opener); + +function onLoad() +{ + dialog.input = document.getElementById("dialog.input"); + dialog.open = document.documentElement.getButton("accept"); + dialog.openWhereList = document.getElementById("openWhereList"); + dialog.openTopWindow = document.getElementById("currentWindow"); + dialog.bundle = document.getElementById("openLocationBundle"); + + if ("arguments" in window && window.arguments.length >= 1) + browser = window.arguments[0]; + + dialog.openWhereList.selectedItem = dialog.openTopWindow; + + if (pref) { + try { + var useAutoFill = pref.getBoolPref("browser.urlbar.autoFill"); + if (useAutoFill) + dialog.input.setAttribute("completedefaultindex", "true"); + } catch (ex) {} + + try { + var value = pref.getIntPref("general.open_location.last_window_choice"); + var element = dialog.openWhereList.getElementsByAttribute("value", value)[0]; + if (element) + dialog.openWhereList.selectedItem = element; + dialog.input.value = gOpenLocationLastURL.value; + } + catch(ex) { + } + if (dialog.input.value) + dialog.input.select(); // XXX should probably be done automatically + } + + doEnabling(); +} + +function doEnabling() +{ + dialog.open.disabled = !dialog.input.value; +} + +function open() +{ + var url; + var postData = {}; + var mayInheritPrincipal = {value: false}; + if (browser) + url = browser.getShortcutOrURI(dialog.input.value, postData, mayInheritPrincipal); + else + url = dialog.input.value; + + try { + // Whichever target we use for the load, we allow third-party services to + // fixup the URI + switch (dialog.openWhereList.value) { + case "0": + var webNav = Components.interfaces.nsIWebNavigation; + var flags = webNav.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | + webNav.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + if (!mayInheritPrincipal.value) + flags |= webNav.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; + browser.gBrowser.loadURIWithFlags(url, flags, null, null, postData.value); + break; + case "1": + window.opener.delayedOpenWindow(getBrowserURL(), "all,dialog=no", + url, postData.value, null, null, true); + break; + case "3": + browser.delayedOpenTab(url, null, null, postData.value, true); + break; + } + } + catch(exception) { + } + + if (pref) { + gOpenLocationLastURL.value = dialog.input.value; + pref.setIntPref("general.open_location.last_window_choice", dialog.openWhereList.value); + } + + // Delay closing slightly to avoid timing bug on Linux. + window.close(); + return false; +} + +function createInstance(contractid, iidName) +{ + var iid = Components.interfaces[iidName]; + return Components.classes[contractid].createInstance(iid); +} + +const nsIFilePicker = Components.interfaces.nsIFilePicker; +function onChooseFile() +{ + try { + let fp = Components.classes["@mozilla.org/filepicker;1"]. + createInstance(nsIFilePicker); + let fpCallback = function fpCallback_done(aResult) { + if (aResult == nsIFilePicker.returnOK && fp.fileURL.spec && + fp.fileURL.spec.length > 0) { + dialog.input.value = fp.fileURL.spec; + } + doEnabling(); + }; + + fp.init(window, dialog.bundle.getString("chooseFileDialogTitle"), + nsIFilePicker.modeOpen); + fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText | + nsIFilePicker.filterImages | nsIFilePicker.filterXML | + nsIFilePicker.filterHTML); + fp.open(fpCallback); + } catch (ex) { + } +} diff --git a/application/palemoon/base/content/openLocation.xul b/application/palemoon/base/content/openLocation.xul new file mode 100644 index 0000000000..7bafed0fe7 --- /dev/null +++ b/application/palemoon/base/content/openLocation.xul @@ -0,0 +1,57 @@ +<?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"?> + +<!DOCTYPE dialog SYSTEM "chrome://browser/locale/openLocation.dtd"> + +<dialog id="openLocation" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&caption.label;" + onload="onLoad()" + buttonlabelaccept="&openBtn.label;" + buttoniconaccept="open" + ondialogaccept="open()" + style="width: 40em;" + persist="screenX screenY" + screenX="24" screenY="24"> + + <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" src="chrome://browser/content/openLocation.js"/> + <script type="application/javascript" src="chrome://browser/content/utilityOverlay.js"/> + + <stringbundle id="openLocationBundle" src="chrome://browser/locale/openLocation.properties"/> + + <hbox> + <separator orient="vertical" class="thin"/> + <vbox flex="1"> + <description>&enter.label;</description> + <separator class="thin"/> + + <hbox align="center"> + <textbox id="dialog.input" flex="1" type="autocomplete" + completeselectedindex="true" + autocompletesearch="urlinline history" + enablehistory="true" + class="uri-element" + oninput="doEnabling();"/> + <button label="&chooseFile.label;" oncommand="onChooseFile();"/> + </hbox> + <hbox align="center"> + <label value="&openWhere.label;"/> + <menulist id="openWhereList"> + <menupopup> + <menuitem value="0" id="currentWindow" label="&topTab.label;"/> + <menuitem value="3" label="&newTab.label;"/> + <menuitem value="1" label="&newWindow.label;"/> + </menupopup> + </menulist> + <spacer flex="1"/> + </hbox> + </vbox> + </hbox> + +</dialog> diff --git a/application/palemoon/base/content/overrides/app-license.html b/application/palemoon/base/content/overrides/app-license.html new file mode 100644 index 0000000000..2d2e3d55ef --- /dev/null +++ b/application/palemoon/base/content/overrides/app-license.html @@ -0,0 +1,6 @@ +<!-- 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</a> under the Pale Moon + Binary <a href="http://www.palemoon.org/redist.shtml">redistribution license</a>.</p>
\ No newline at end of file diff --git a/application/palemoon/base/content/padlock.css b/application/palemoon/base/content/padlock.css new file mode 100644 index 0000000000..649cb2777f --- /dev/null +++ b/application/palemoon/base/content/padlock.css @@ -0,0 +1,203 @@ +#padlock-ib { + -moz-appearance: none; + min-width: 0px; + margin-right: 1px !important; + background-repeat: no-repeat; + background-position: center; + z-index: 1000 !important; + padding: 0px; + margin: 0px; + border: 0px; +} + +#padlock-ib[padshow="ib-trans-bg"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + background-color: transparent; +} + +#padlock-ib[padshow="ib-trans-bg"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + background-color: transparent; +} + +#padlock-ib[padshow="ib-trans-bg"][level="low"], +#padlock-ib[padshow="ib-trans-bg"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + background-color: transparent; +} + +#padlock-ib[padshow="ib-trans-bg"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + background-color: transparent; +} + +#padlock-ib-left { + -moz-appearance: none; + min-width: 0px; + margin-right: 1px !important; + background-repeat: no-repeat; + background-position: center; + z-index: 1000 !important; + padding: 0px; + margin: 0px; + border: 0px; +} + +#padlock-ib-left[padshow="ib-left"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + padding: 2px; + background-color: transparent; +} + +#padlock-ib-left[padshow="ib-left"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + padding: 2px; + background-color: transparent; +} + +#padlock-ib-left[padshow="ib-left"][level="low"], +#padlock-ib-left[padshow="ib-left"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + padding: 2px; + background-color: transparent; +} + +#padlock-ib-left[padshow="ib-left"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + padding: 2px; + background-color: transparent; +} + +#padlock-ub-right { + -moz-appearance: none; + min-width: 0px; + margin-right: 1px !important; + background-repeat: no-repeat; + background-position: center; + z-index: 1000 !important; + padding: 0px; + margin: 0px; + border: 0px; +} + +#padlock-ub-right[padshow="ub-right"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + background-color: transparent; +} + +#padlock-ub-right[padshow="ub-right"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + background-color: transparent; +} + +#padlock-ub-right[padshow="ub-right"][level="low"], +#padlock-ub-right[padshow="ub-right"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + background-color: transparent; +} + +#padlock-ub-right[padshow="ub-right"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + background-color: transparent; +} + +#padlock-sb { + -moz-appearance: none; + background-repeat: no-repeat; + background-position: center; +} + +#padlock-sb[padshow="statbar"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + background-color: transparent; +} + +#padlock-sb[padshow="statbar"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + background-color: transparent; +} + +#padlock-sb[padshow="statbar"][level="low"], +#padlock-sb[padshow="statbar"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + background-color: transparent; +} + +#padlock-sb[padshow="statbar"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + background-color: transparent; +} + +#padlock-tab { + -moz-appearance: none; + background-repeat: no-repeat; + background-position: center; +} + +#padlock-tab[padshow="tabs-bar"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_mod_ev.png"); + background-color: transparent; +} + +#padlock-tab[padshow="tabs-bar"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_mod_https.png"); + background-color: transparent; +} + +#padlock-tab[padshow="tabs-bar"][level="low"], +#padlock-tab[padshow="tabs-bar"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_mod_low.png"); + background-color: transparent; +} + +#padlock-tab[padshow="tabs-bar"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_mod_broken.png"); + background-color: transparent; +} + +/* Classic style */ +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="ev"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="ev"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="ev"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="ev"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="ev"] { + list-style-image: url("chrome://browser/content/padlock_classic_ev.png"); +} + +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="high"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="high"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="high"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="high"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="high"] { + list-style-image: url("chrome://browser/content/padlock_classic_https.png"); +} + +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="low"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="low"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="low"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="low"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="low"], +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="mixed"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="mixed"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="mixed"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="mixed"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="mixed"] { + list-style-image: url("chrome://browser/content/padlock_classic_low.png"); +} + +#padlock-ib[padshow="ib-trans-bg"][padstyle="classic"][level="broken"], +#padlock-ib-left[padshow="ib-left"][padstyle="classic"][level="broken"], +#padlock-ub-right[padshow="ub-right"][padstyle="classic"][level="broken"], +#padlock-sb[padshow="statbar"][padstyle="classic"][level="broken"], +#padlock-tab[padshow="tabs-bar"][padstyle="classic"][level="broken"] { + list-style-image: url("chrome://browser/content/padlock_classic_broken.png"); +} + +/* Remove a few px of dead space for disabled locations */ +#padlock-ib:not([padshow="ib-trans-bg"]), +#padlock-ib-left:not([padshow="ib-left"]), +#padlock-ub-right:not([padshow="ub-right"]), +#padlock-sb:not([padshow="statbar"]), +#padlock-tab:not([padshow="tabs-bar"]) { + visibility: collapse; +}
\ No newline at end of file diff --git a/application/palemoon/base/content/padlock.js b/application/palemoon/base/content/padlock.js new file mode 100644 index 0000000000..53477fd171 --- /dev/null +++ b/application/palemoon/base/content/padlock.js @@ -0,0 +1,234 @@ +let Cc = Components.classes; +let Ci = Components.interfaces; +let Cu = Components.utils; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var padlock_PadLock = +{ + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]), + onButtonClick: function(event) { + event.stopPropagation(); + gIdentityHandler.handleMoreInfoClick(event); + }, + onStateChange: function() {}, + onProgressChange: function() {}, + onLocationChange: function() {}, + onStatusChange: function() {}, + onSecurityChange: function(aCallerWebProgress, aRequestWithState, aState) { + // aState is defined as a bitmask that may be extended in the future. + // We filter out any unknown bits before testing for known values. + const wpl = Ci.nsIWebProgressListener; + const wpl_security_bits = wpl.STATE_IS_SECURE | + wpl.STATE_IS_BROKEN | + wpl.STATE_IS_INSECURE | + wpl.STATE_IDENTITY_EV_TOPLEVEL | + wpl.STATE_SECURE_HIGH | + wpl.STATE_SECURE_MED | + wpl.STATE_SECURE_LOW; + var level; + var is_insecure; + var highlight_urlbar = false; + + switch (aState & wpl_security_bits) { + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_HIGH | wpl.STATE_IDENTITY_EV_TOPLEVEL: + level = "ev"; + is_insecure = ""; + highlight_urlbar = true; + break; + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_HIGH: + level = "high"; + is_insecure = ""; + highlight_urlbar = true; + break; + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_MED: + case wpl.STATE_IS_SECURE | wpl.STATE_SECURE_LOW: + level = "low"; + is_insecure = "insecure"; + break; + case wpl.STATE_IS_BROKEN | wpl.STATE_SECURE_LOW: + level = "mixed"; + is_insecure = "insecure"; + highlight_urlbar = true; + break; + case wpl.STATE_IS_BROKEN: + level = "broken"; + is_insecure = "insecure"; + highlight_urlbar = true; + break; + default: // should not be reached + level = null; + is_insecure = "insecure"; + } + + try { + var proto = gBrowser.contentWindow.location.protocol; + if (proto == "about:" || proto == "chrome:" || proto == "file:" ) { + // do not warn when using local protocols + is_insecure = false; + } + } + catch (ex) {} + + let ub = document.getElementById("urlbar"); + if (ub) { // Only call if URL bar is present. + if (highlight_urlbar) { + ub.setAttribute("security_level", level); + } else { + ub.removeAttribute("security_level"); + } + } + + try { // URL bar may be hidden + padlock_PadLock.setPadlockLevel("padlock-ib", level); + padlock_PadLock.setPadlockLevel("padlock-ib-left", level); + padlock_PadLock.setPadlockLevel("padlock-ub-right", level); + } catch(e) {} + padlock_PadLock.setPadlockLevel("padlock-sb", level); + padlock_PadLock.setPadlockLevel("padlock-tab", level); + }, + setPadlockLevel: function(item, level) { + let secbut = document.getElementById(item); + var sectooltip = ""; + + if (level) { + secbut.setAttribute("level", level); + secbut.hidden = false; + } else { + secbut.hidden = true; + secbut.removeAttribute("level"); + } + + switch (level) { + case "ev": + sectooltip = "Extended Validated"; + break; + case "high": + sectooltip = "Secure"; + break; + case "low": + sectooltip = "Weak security"; + break; + case "mixed": + sectooltip = "Mixed mode (partially encrypted)"; + break; + case "broken": + sectooltip = "Not secure"; + break; + default: + sectooltip = ""; + } + secbut.setAttribute("tooltiptext", sectooltip); + }, + prefbranch : null, + onLoad: function() { + gBrowser.addProgressListener(padlock_PadLock); + + var prefService = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService); + padlock_PadLock.prefbranch = prefService.getBranch("browser.padlock."); + padlock_PadLock.prefbranch.QueryInterface(Components.interfaces.nsIPrefBranch2); + padlock_PadLock.usePrefs(); + padlock_PadLock.prefbranch.addObserver("", padlock_PadLock, false); + }, + onUnLoad: function() { + padlock_PadLock.prefbranch.removeObserver("", padlock_PadLock); + }, + observe: function(subject, topic, data) + { + if (topic != "nsPref:changed") + return; + if (data != "style" && data != "urlbar_background" && data != "shown") + return; + padlock_PadLock.usePrefs(); + }, + usePrefs: function() { + var prefval = padlock_PadLock.prefbranch.getIntPref("style"); + var position; + var padstyle; + if (prefval == 2) { + position = "ib-left"; + padstyle = "modern"; + } + else if (prefval == 3) { + position = "ub-right"; + padstyle = "modern"; + } + else if (prefval == 4) { + position = "statbar"; + padstyle = "modern"; + } + else if (prefval == 5) { + position = "tabs-bar"; + padstyle = "modern"; + } + else if (prefval == 6) { + position = "ib-trans-bg"; + padstyle = "classic"; + } + else if (prefval == 7) { + position = "ib-left"; + padstyle = "classic"; + } + else if (prefval == 8) { + position = "ub-right"; + padstyle = "classic"; + } + else if (prefval == 9) { + position = "statbar"; + padstyle = "classic"; + } + else if (prefval == 10) { + position = "tabs-bar"; + padstyle = "classic"; + } + else { // 1 or anything else_ default + position = "ib-trans-bg"; + padstyle = "modern"; + } + + var colshow; + var colprefval = padlock_PadLock.prefbranch.getIntPref("urlbar_background"); + switch (colprefval) { + case 3: + colshow = "all"; + break; + case 2: + colshow = "secure-mixed"; + break; + case 1: + colshow = "secure-only"; + break; + default: + colshow = ""; // 0 or anything else: no shading + } + try { // URL bar may be hidden + document.getElementById("urlbar").setAttribute("https_color", colshow); + } catch(e) {} + + var lockenabled = padlock_PadLock.prefbranch.getBoolPref("shown"); + var padshow = ""; + if (lockenabled) { + padshow = position; + } + + try { // URL bar may be hidden + document.getElementById("padlock-ib").setAttribute("padshow", padshow); + document.getElementById("padlock-ib-left").setAttribute("padshow", padshow); + document.getElementById("padlock-ub-right").setAttribute("padshow", padshow); + } catch(e) {} + document.getElementById("padlock-sb").setAttribute("padshow", padshow); + document.getElementById("padlock-tab").setAttribute("padshow", padshow); + + try { // URL bar may be hidden + document.getElementById("padlock-ib").setAttribute("padstyle", padstyle); + document.getElementById("padlock-ib-left").setAttribute("padstyle", padstyle); + document.getElementById("padlock-ub-right").setAttribute("padstyle", padstyle); + } catch(e) {} + document.getElementById("padlock-sb").setAttribute("padstyle", padstyle); + document.getElementById("padlock-tab").setAttribute("padstyle", padstyle); + + } +}; + +window.addEventListener("load", padlock_PadLock.onLoad, false ); +window.addEventListener("unload", padlock_PadLock.onUnLoad, false ); diff --git a/application/palemoon/base/content/padlock.xul b/application/palemoon/base/content/padlock.xul new file mode 100644 index 0000000000..e820c19c7b --- /dev/null +++ b/application/palemoon/base/content/padlock.xul @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://browser/content/padlock.css" type="text/css"?> + +<overlay + id="padlock" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script type="application/x-javascript" src="chrome://browser/content/padlock.js"/> + + <hbox id="identity-box"> + <image id="padlock-ib" insertafter="identity-icon-labels" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </hbox> + + <hbox id="identity-box"> + <image id="padlock-ib-left" insertbefore="identity-icon-labels" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </hbox> + + <hbox id="urlbar-icons"> + <image id="padlock-ub-right" insertbefore="star-button" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </hbox> + + <statusbar id="status-bar"> + <statusbarpanel insertafter="security-button" + id="padlock-sb-panel" + class="statusbar-iconic-text"> + <image id="padlock-sb" insertbefore="star-button" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </statusbarpanel> + </statusbar> + + <toolbar id="TabsToolbar"> + <toolbaritem insertafter="tabs-closebutton" id="tabs-padlock-tbitem" + align="center" pack="center"> + <image id="padlock-tab" + class="urlbar-icon" + style="-moz-user-focus: none;" + hidden="false" + tooltiptext="" + onclick="return padlock_PadLock.onButtonClick(event);"/> + </toolbaritem> + </toolbar> + + +</overlay> diff --git a/application/palemoon/base/content/padlock_classic_broken.png b/application/palemoon/base/content/padlock_classic_broken.png Binary files differnew file mode 100644 index 0000000000..437036fe88 --- /dev/null +++ b/application/palemoon/base/content/padlock_classic_broken.png diff --git a/application/palemoon/base/content/padlock_classic_ev.png b/application/palemoon/base/content/padlock_classic_ev.png Binary files differnew file mode 100644 index 0000000000..b3f80c0dad --- /dev/null +++ b/application/palemoon/base/content/padlock_classic_ev.png diff --git a/application/palemoon/base/content/padlock_classic_https.png b/application/palemoon/base/content/padlock_classic_https.png Binary files differnew file mode 100644 index 0000000000..86026c04f4 --- /dev/null +++ b/application/palemoon/base/content/padlock_classic_https.png diff --git a/application/palemoon/base/content/padlock_classic_low.png b/application/palemoon/base/content/padlock_classic_low.png Binary files differnew file mode 100644 index 0000000000..652ad091ff --- /dev/null +++ b/application/palemoon/base/content/padlock_classic_low.png diff --git a/application/palemoon/base/content/padlock_mod_broken.png b/application/palemoon/base/content/padlock_mod_broken.png Binary files differnew file mode 100644 index 0000000000..33a6c06454 --- /dev/null +++ b/application/palemoon/base/content/padlock_mod_broken.png diff --git a/application/palemoon/base/content/padlock_mod_ev.png b/application/palemoon/base/content/padlock_mod_ev.png Binary files differnew file mode 100644 index 0000000000..3dfdcbde50 --- /dev/null +++ b/application/palemoon/base/content/padlock_mod_ev.png diff --git a/application/palemoon/base/content/padlock_mod_https.png b/application/palemoon/base/content/padlock_mod_https.png Binary files differnew file mode 100644 index 0000000000..d494b42b05 --- /dev/null +++ b/application/palemoon/base/content/padlock_mod_https.png diff --git a/application/palemoon/base/content/padlock_mod_low.png b/application/palemoon/base/content/padlock_mod_low.png Binary files differnew file mode 100644 index 0000000000..29179efaf5 --- /dev/null +++ b/application/palemoon/base/content/padlock_mod_low.png diff --git a/application/palemoon/base/content/pageinfo/feeds.js b/application/palemoon/base/content/pageinfo/feeds.js new file mode 100644 index 0000000000..468d8c19db --- /dev/null +++ b/application/palemoon/base/content/pageinfo/feeds.js @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function initFeedTab() +{ + const feedTypes = { + "application/rss+xml": gBundle.getString("feedRss"), + "application/atom+xml": gBundle.getString("feedAtom"), + "text/xml": gBundle.getString("feedXML"), + "application/xml": gBundle.getString("feedXML"), + "application/rdf+xml": gBundle.getString("feedXML") + }; + + // get the feeds + var linkNodes = gDocument.getElementsByTagName("link"); + var length = linkNodes.length; + for (var i = 0; i < length; i++) { + var link = linkNodes[i]; + if (!link.href) + continue; + + var rel = link.rel && link.rel.toLowerCase(); + var rels = {}; + if (rel) { + for each (let relVal in rel.split(/\s+/)) + rels[relVal] = true; + } + + if (rels.feed || (link.type && rels.alternate && !rels.stylesheet)) { + var type = isValidFeed(link, gDocument.nodePrincipal, "feed" in rels); + if (type) { + type = feedTypes[type] || feedTypes["application/rss+xml"]; + addRow(link.title, type, link.href); + } + } + } + + 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/palemoon/base/content/pageinfo/feeds.xml b/application/palemoon/base/content/pageinfo/feeds.xml new file mode 100644 index 0000000000..782c05a73d --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/pageinfo/pageInfo.css b/application/palemoon/base/content/pageinfo/pageInfo.css new file mode 100644 index 0000000000..622b56bb57 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/pageinfo/pageInfo.js b/application/palemoon/base/content/pageinfo/pageInfo.js new file mode 100644 index 0000000000..ba93ee8179 --- /dev/null +++ b/application/palemoon/base/content/pageinfo/pageInfo.js @@ -0,0 +1,1285 @@ +/* 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 Cu = Components.utils; +Cu.import("resource://gre/modules/LoadContextInfo.jsm"); +Cu.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); + }, + + 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 + ); + + 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() { }, + 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 gWindow = null; +var gDocument = 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; + } else { + 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; + if (treecol.index == COL_IMAGE_SIZE || treecol.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, + treecol.index, + comparator, + this.sortcol, + this.sortdir + ); + + this.sortcol = treecol.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 + } +} +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. The global variables gDocument and gWindow are set. +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 are called once for each subframe of the target document and +// the target document itself. The frame is passed as an argument. +var onProcessFrame = [ ]; + +// These functions are called once for each element (in all subframes, if any) +// in the target document. The element is passed as an argument. +var onProcessElement = [ ]; + +// 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 = [ ]; + +// These functions are called once when an image preview is shown. +var onImagePreviewShown = [ ]; + +/* 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]; + + if (!args || !args.doc) { + gWindow = window.opener.content; + gDocument = gWindow.document; + } + + // 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); + + // Make sure the page info window gets focus even if a doorhanger might + // otherwise (async) steal it. + window.focus(); +} + +function loadPageInfo() +{ + var titleFormat = gWindow != gWindow.top ? "pageInfo.frame.title" + : "pageInfo.page.title"; + document.title = gBundle.getFormattedString(titleFormat, [gDocument.location]); + + document.getElementById("main-window").setAttribute("relatedUrl", gDocument.location); + + // do the easy stuff first + makeGeneralTab(); + + // and then the hard stuff + makeTabs(gDocument, gWindow); + + initFeedTab(); + onLoadPermission(); + + /* 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 (args && args.doc) { + gDocument = args.doc; + gWindow = gDocument.defaultView; + } + + gImageElement = args && args.imageElement; + + /* Load the page info */ + loadPageInfo(); + + 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 onClickMore() +{ + var radioGrp = document.getElementById("viewGroup"); + var radioElt = document.getElementById("securityTab"); + radioGrp.selectedItem = radioElt; + showTab('security'); +} + +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); + }, + get mainThreadOnly() { return true; } + }; + diskStorage.asyncOpenURI(Services.io.newURI(key, null, null), "", nsICacheStorage.OPEN_READONLY, checkCacheListener); +} + +function makeGeneralTab() +{ + var title = (gDocument.title) ? gBundle.getFormattedString("pageTitle", [gDocument.title]) : gBundle.getString("noPageTitle"); + document.getElementById("titletext").value = title; + + var url = gDocument.location.toString(); + setItemValue("urltext", url); + + var referrer = ("referrer" in gDocument && gDocument.referrer); + setItemValue("refertext", referrer); + + var mode = ("compatMode" in gDocument && gDocument.compatMode == "BackCompat") ? "generalQuirksMode" : "generalStrictMode"; + document.getElementById("modetext").value = gBundle.getString(mode); + + // find out the mime type + var mimeType = gDocument.contentType; + setItemValue("typetext", mimeType); + + // get the document characterset + var encoding = gDocument.characterSet; + document.getElementById("encodingtext").value = encoding; + + // get the meta tags + var metaNodes = gDocument.getElementsByTagName("meta"); + var length = metaNodes.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; + + for (var i = 0; i < length; i++) + gMetaView.addRow([metaNodes[i].name || metaNodes[i].httpEquiv, metaNodes[i].content]); + + metaGroup.collapsed = false; + } + + // get the date of last modification + var modifiedText = formatDate(gDocument.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); + }); + + securityOnLoad(); +} + +//******** Generic Build-a-tab +// Assumes the views are empty. Only called once to build the tabs, and +// does so by farming the task off to another thread via setTimeout(). +// The actual work is done with a TreeWalker that calls doGrab() once for +// each element node in the document. + +var gFrameList = [ ]; + +function makeTabs(aDocument, aWindow) +{ + goThroughFrames(aDocument, aWindow); + processFrames(); +} + +function goThroughFrames(aDocument, aWindow) +{ + gFrameList.push(aDocument); + if (aWindow && aWindow.frames.length > 0) { + var num = aWindow.frames.length; + for (var i = 0; i < num; i++) + goThroughFrames(aWindow.frames[i].document, aWindow.frames[i]); // recurse through the frames + } +} + +function processFrames() +{ + if (gFrameList.length) { + var doc = gFrameList[0]; + onProcessFrame.forEach(function(func) { func(doc); }); + var iterator = doc.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT, grabAll); + gFrameList.shift(); + setTimeout(doGrab, 10, iterator); + onFinished.push(selectImage); + } + else + onFinished.forEach(function(func) { func(); }); +} + +function doGrab(iterator) +{ + for (var i = 0; i < 500; ++i) + if (!iterator.nextNode()) { + processFrames(); + return; + } + + setTimeout(doGrab, 10, iterator); +} + +function addImage(url, type, alt, elem, isBg) +{ + 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]++; + if (elem == gImageElement) + gImageView.data[i][COL_IMAGE_NODE] = elem; + } +} + +function grabAll(elem) +{ + // check for images defined in CSS (e.g. background, borders), any node may have multiple + var computedStyle = elem.ownerDocument.defaultView.getComputedStyle(elem, ""); + + if (computedStyle) { + var addImgFunc = function (label, val) { + if (val.primitiveType == CSSPrimitiveValue.CSS_URI) { + addImage(val.getStringValue(), label, gStrings.notSet, elem, true); + } + else if (val.primitiveType == CSSPrimitiveValue.CSS_STRING) { + // This is for -moz-image-rect. + // TODO: Reimplement once bug 714757 is fixed + var strVal = val.getStringValue(); + if (strVal.search(/^.*url\(\"?/) > -1) { + url = strVal.replace(/^.*url\(\"?/,"").replace(/\"?\).*$/,""); + addImage(url, label, gStrings.notSet, elem, true); + } + } + else if (val.cssValueType == CSSValue.CSS_VALUE_LIST) { + // recursively resolve multiple nested CSS value lists + for (var i = 0; i < val.length; i++) + addImgFunc(label, val.item(i)); + } + }; + + addImgFunc(gStrings.mediaBGImg, computedStyle.getPropertyCSSValue("background-image")); + addImgFunc(gStrings.mediaBorderImg, computedStyle.getPropertyCSSValue("border-image-source")); + addImgFunc(gStrings.mediaListImg, computedStyle.getPropertyCSSValue("list-style-image")); + addImgFunc(gStrings.mediaCursor, computedStyle.getPropertyCSSValue("cursor")); + } + + // one swi^H^H^Hif-else to rule them all + if (elem instanceof HTMLImageElement) + addImage(elem.src, gStrings.mediaImg, + (elem.hasAttribute("alt")) ? elem.alt : gStrings.notSet, elem, false); + else if (elem instanceof 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 + var href = makeURLAbsolute(elem.baseURI, elem.href.baseVal); + addImage(href, gStrings.mediaImg, "", elem, false); + } catch (e) { } + } + else if (elem instanceof HTMLVideoElement) { + addImage(elem.currentSrc, gStrings.mediaVideo, "", elem, false); + } + else if (elem instanceof HTMLAudioElement) { + addImage(elem.currentSrc, gStrings.mediaAudio, "", elem, false); + } + else if (elem instanceof HTMLLinkElement) { + if (elem.rel && /\bicon\b/i.test(elem.rel)) + addImage(elem.href, gStrings.mediaLink, "", elem, false); + } + else if (elem instanceof HTMLInputElement || elem instanceof HTMLButtonElement) { + if (elem.type.toLowerCase() == "image") + addImage(elem.src, gStrings.mediaInput, + (elem.hasAttribute("alt")) ? elem.alt : gStrings.notSet, elem, false); + } + else if (elem instanceof HTMLObjectElement) + addImage(elem.data, gStrings.mediaObject, getValueText(elem), elem, false); + else if (elem instanceof HTMLEmbedElement) + addImage(elem.src, gStrings.mediaEmbed, "", elem, false); + + onProcessElement.forEach(function(func) { func(elem); }); + + return NodeFilter.FILTER_ACCEPT; +} + +//******** 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), gDocument); + } + } else { + selectSaveFolder(function(aDirectory) { + if (aDirectory) { + var saveAnImage = function(aURIString, aChosenData, aBaseURI) { + internalSave(aURIString, null, null, null, null, false, "SaveImageTitle", + aChosenData, aBaseURI, gDocument); + }; + + 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: uris */ + } + + 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.host, "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]); + } +} + +function makePreview(row) +{ + var imageTree = document.getElementById("imagetree"); + 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); + + var imageText; + if (!isBG && + !(item instanceof SVGImageElement) && + !(gDocument instanceof ImageDocument)) { + imageText = item.title || item.alt; + + if (!imageText && !(item instanceof HTMLImageElement)) + imageText = getValueText(item); + } + setItemValue("imagetext", 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) { + var 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; + var numFrames = 1; + if (item instanceof HTMLObjectElement || + item instanceof HTMLEmbedElement || + item instanceof HTMLLinkElement) + mimeType = item.type; + + if (!mimeType && !isBG && item instanceof nsIImageLoadingContent) { + var imageRequest = item.getRequest(nsIImageLoadingContent.CURRENT_REQUEST); + if (imageRequest) { + mimeType = imageRequest.mimeType; + var image = imageRequest.image; + if (image) + numFrames = image.numFrames; + } + } + + if (!mimeType) + mimeType = getContentTypeFromHeaders(cacheEntry); + + // if we have a data url, get the MIME type from the url + if (!mimeType && url.startsWith("data:")) { + let dataMimeType = /^data:(image\/[^;,]+)/i.exec(url); + if (dataMimeType) + mimeType = dataMimeType[1].toLowerCase(); + } + + 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 instanceof HTMLLinkElement || item instanceof HTMLInputElement || + item instanceof HTMLImageElement || + item instanceof SVGImageElement || + (item instanceof 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. + 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 = newImage.naturalWidth; + newImage.height = newImage.naturalHeight; + } + + if (item instanceof SVGImageElement) { + newImage.width = item.width.baseVal.value; + newImage.height = item.height.baseVal.value; + } + + width = newImage.width; + height = newImage.height; + + document.getElementById("theimagecontainer").collapsed = false + document.getElementById("brokenimagecontainer").collapsed = true; + } + else if (item instanceof 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 instanceof 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; + } + + var 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); + + onImagePreviewShown.forEach(function(func) { func(); }); + }); +} + +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 item = gImageView.data[row][COL_IMAGE_NODE]; + var url = gImageView.data[row][COL_IMAGE_ADDRESS]; + if (makeURI(url).host == permission.host) + makeBlockImage(url); + } + } + } +} + +function getContentTypeFromHeaders(cacheEntryDescriptor) +{ + if (!cacheEntryDescriptor) + return null; + + return (/^Content-Type:\s*(.*?)\s*(?:\;|$)/mi + .exec(cacheEntryDescriptor.getMetaDataElement("response-head")))[1]; +} + +//******** 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 +function getValueText(node) +{ + var valueText = ""; + + // form input elements don't generally contain information that is useful to our callers, so return nothing + if (node instanceof HTMLInputElement || + node instanceof HTMLSelectElement || + node instanceof HTMLTextAreaElement) + return valueText; + + // otherwise recurse for each child + var length = node.childNodes.length; + for (var i = 0; i < length; i++) { + var childNode = node.childNodes[i]; + var nodeType = childNode.nodeType; + + // text nodes are where the goods are + if (nodeType == Node.TEXT_NODE) + valueText += " " + childNode.nodeValue; + // and elements can have more text inside them + else if (nodeType == Node.ELEMENT_NODE) { + // images are special, we want to capture the alt text as if the image weren't there + if (childNode instanceof HTMLImageElement) + valueText += " " + getAltText(childNode); + else + valueText += " " + getValueText(childNode); + } + } + + return 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 +function getAltText(node) +{ + var altText = ""; + + if (node.alt) + return node.alt; + var length = node.childNodes.length; + for (var i = 0; i < length; i++) + if ((altText = 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 +function stripWS(text) +{ + var middleRE = /\s+/g; + var endRE = /(^\s+)|(\s+$)/g; + + text = text.replace(middleRE, " "); + return text.replace(endRE, ""); +} + +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) +{ + // scriptable date formatter, for pretty printing dates + var dateService = Components.classes["@mozilla.org/intl/scriptabledateformat;1"] + .getService(Components.interfaces.nsIScriptableDateFormat); + + var date = new Date(datestr); + if (!date.valueOf()) + return unknown; + + return dateService.FormatDateTime("", dateService.dateFormatLong, + dateService.timeFormatSeconds, + date.getFullYear(), date.getMonth()+1, date.getDate(), + date.getHours(), date.getMinutes(), date.getSeconds()); +} + +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"), document); + } +} + +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 (gImageElement == gImageView.data[i][COL_IMAGE_NODE] && + !gImageView.data[i][COL_IMAGE_BG]) { + 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/palemoon/base/content/pageinfo/pageInfo.xml b/application/palemoon/base/content/pageinfo/pageInfo.xml new file mode 100644 index 0000000000..20d3300467 --- /dev/null +++ b/application/palemoon/base/content/pageinfo/pageInfo.xml @@ -0,0 +1,29 @@ +<?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"> + <content> + <xul:image class="viewButtonIcon" xbl:inherits="src"/> + <xul:label class="viewButtonLabel" xbl:inherits="value=label"/> + </content> + <implementation implements="nsIAccessibleProvider"> + <property name="accessibleType" readonly="true"> + <getter> + <![CDATA[ + return Components.interfaces.nsIAccessibleProvider.XULListitem; + ]]> + </getter> + </property> + </implementation> + </binding> + +</bindings> diff --git a/application/palemoon/base/content/pageinfo/pageInfo.xul b/application/palemoon/base/content/pageinfo/pageInfo.xul new file mode 100644 index 0000000000..5bca1b495b --- /dev/null +++ b/application/palemoon/base/content/pageinfo/pageInfo.xul @@ -0,0 +1,559 @@ +<?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_imageDef" oncommand="onCheckboxClick('image');"/> + <command id="cmd_popupDef" oncommand="onCheckboxClick('popup');"/> + <command id="cmd_cookieDef" oncommand="onCheckboxClick('cookie');"/> + <command id="cmd_desktop-notificationDef" oncommand="onCheckboxClick('desktop-notification');"/> + <command id="cmd_installDef" oncommand="onCheckboxClick('install');"/> + <command id="cmd_fullscreenDef" oncommand="onCheckboxClick('fullscreen');"/> + <command id="cmd_geoDef" oncommand="onCheckboxClick('geo');"/> + <command id="cmd_indexedDBDef" oncommand="onCheckboxClick('indexedDB');"/> + <command id="cmd_pluginsDef" oncommand="onCheckboxClick('plugins');"/> + <command id="cmd_imageToggle" oncommand="onRadioClick('image');"/> + <command id="cmd_popupToggle" oncommand="onRadioClick('popup');"/> + <command id="cmd_cookieToggle" oncommand="onRadioClick('cookie');"/> + <command id="cmd_desktop-notificationToggle" oncommand="onRadioClick('desktop-notification');"/> + <command id="cmd_installToggle" oncommand="onRadioClick('install');"/> + <command id="cmd_fullscreenToggle" oncommand="onRadioClick('fullscreen');"/> + <command id="cmd_geoToggle" oncommand="onRadioClick('geo');"/> + <command id="cmd_indexedDBToggle" oncommand="onRadioClick('indexedDB');"/> + <command id="cmd_pluginsToggle" oncommand="onPluginRadioClick(event);"/> + <command id="cmd_pointerLockDef" oncommand="onCheckboxClick('pointerLock');"/> + <command id="cmd_pointerLockToggle" oncommand="onRadioClick('pointerLock');"/> + </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"> + <textbox class="header" readonly="true" id="titletext"/> + <grid id="generalGrid"> + <columns> + <column/> + <column class="gridSeparator"/> + <column flex="1"/> + </columns> + <rows id="generalRows"> + <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="&generalEncoding;"/> + <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> + <groupbox id="securityBox"> + <caption id="securityBoxCaption" label="&securityHeader;"/> + <description id="general-security-identity" class="header"/> + <description id="general-security-privacy" class="header"/> + <hbox id="securityDetailsButtonBox" align="right"> + <button id="security-view-details" label="&generalSecurityDetails;" + accesskey="&generalSecurityDetails.accesskey;" + oncommand="onClickMore();"/> + </hbox> + </groupbox> + </vbox> + + <!-- Media information --> + <vbox id="mediaPanel"> + <tree id="imagetree" onselect="onImageSelect();" contextmenu="picontext" + ondragstart="onBeginLinkDrag(event,'image-address','image-alt')"> + <treecols> + <treecol sortSeparators="true" primary="true" persist="width" flex="10" + width="10" id="image-address" label="&mediaAddress;" + 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="&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> + </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"> + <vbox class="permission" id="permImageRow"> + <label class="permissionLabel" id="permImageLabel" + value="&permImage;" control="imageRadioGroup"/> + <hbox id="permImageBox" role="group" aria-labelledby="permImageLabel"> + <checkbox id="imageDef" command="cmd_imageDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="imageRadioGroup" orient="horizontal"> + <radio id="image#1" command="cmd_imageToggle" label="&permAllow;"/> + <radio id="image#2" command="cmd_imageToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permPopupRow"> + <label class="permissionLabel" id="permPopupLabel" + value="&permPopup;" control="popupRadioGroup"/> + <hbox id="permPopupBox" role="group" aria-labelledby="permPopupLabel"> + <checkbox id="popupDef" command="cmd_popupDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="popupRadioGroup" orient="horizontal"> + <radio id="popup#1" command="cmd_popupToggle" label="&permAllow;"/> + <radio id="popup#2" command="cmd_popupToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permCookieRow"> + <label class="permissionLabel" id="permCookieLabel" + value="&permCookie;" control="cookieRadioGroup"/> + <hbox id="permCookieBox" role="group" aria-labelledby="permCookieLabel"> + <checkbox id="cookieDef" command="cmd_cookieDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="cookieRadioGroup" orient="horizontal"> + <radio id="cookie#1" command="cmd_cookieToggle" label="&permAllow;"/> + <radio id="cookie#8" command="cmd_cookieToggle" label="&permAllowSession;"/> + <radio id="cookie#9" command="cmd_cookieToggle" label="&permAllowFirstPartyOnly;"/> + <radio id="cookie#2" command="cmd_cookieToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permNotificationRow"> + <label class="permissionLabel" id="permNotificationLabel" + value="&permNotifications;" control="desktop-notificationRadioGroup"/> + <hbox role="group" aria-labelledby="permNotificationLabel"> + <checkbox id="desktop-notificationDef" command="cmd_desktop-notificationDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="desktop-notificationRadioGroup" orient="horizontal"> + <radio id="desktop-notification#0" command="cmd_desktop-notificationToggle" label="&permAskAlways;"/> + <radio id="desktop-notification#1" command="cmd_desktop-notificationToggle" label="&permAllow;"/> + <radio id="desktop-notification#2" command="cmd_desktop-notificationToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permInstallRow"> + <label class="permissionLabel" id="permInstallLabel" + value="&permInstall;" control="installRadioGroup"/> + <hbox id="permInstallBox" role="group" aria-labelledby="permInstallLabel"> + <checkbox id="installDef" command="cmd_installDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="installRadioGroup" orient="horizontal"> + <radio id="install#1" command="cmd_installToggle" label="&permAllow;"/> + <radio id="install#2" command="cmd_installToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permGeoRow" > + <label class="permissionLabel" id="permGeoLabel" + value="&permGeo;" control="geoRadioGroup"/> + <hbox id="permGeoBox" role="group" aria-labelledby="permGeoLabel"> + <checkbox id="geoDef" command="cmd_geoDef" label="&permAskAlways;"/> + <spacer flex="1"/> + <radiogroup id="geoRadioGroup" orient="horizontal"> + <radio id="geo#1" command="cmd_geoToggle" label="&permAllow;"/> + <radio id="geo#2" command="cmd_geoToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permIndexedDBRow"> + <label class="permissionLabel" id="permIndexedDBLabel" + value="&permIndexedDB;" control="indexedDBRadioGroup"/> + <hbox id="permIndexedDBBox" role="group" aria-labelledby="permIndexedDBLabel"> + <checkbox id="indexedDBDef" command="cmd_indexedDBDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="indexedDBRadioGroup" orient="horizontal"> + <radio id="indexedDB#0" command="cmd_indexedDBToggle" label="&permAskAlways;"/> + <radio id="indexedDB#1" command="cmd_indexedDBToggle" label="&permAllow;"/> + <radio id="indexedDB#2" command="cmd_indexedDBToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + <hbox id="permIndexedDBExtras"> + <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> + <vbox class="permission" id="permPluginsRow"> + <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 class="permission" id="permFullscreenRow"> + <label class="permissionLabel" id="permFullscreenLabel" + value="&permFullscreen;" control="fullscreenRadioGroup"/> + <hbox id="permFullscreenBox" role="group" aria-labelledby="permFullscreenLabel"> + <checkbox id="fullscreenDef" command="cmd_fullscreenDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="fullscreenRadioGroup" orient="horizontal"> + <radio id="fullscreen#0" command="cmd_fullscreenToggle" label="&permAskAlways;"/> + <radio id="fullscreen#1" command="cmd_fullscreenToggle" label="&permAllow;"/> + <radio id="fullscreen#2" command="cmd_fullscreenToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + <vbox class="permission" id="permPointerLockRow" > + <label class="permissionLabel" id="permPointerLockLabel" + value="&permPointerLock2;" control="pointerLockRadioGroup"/> + <hbox id="permPointerLockBox" role="group" aria-labelledby="permPointerLockLabel"> + <checkbox id="pointerLockDef" command="cmd_pointerLockDef" label="&permAskAlways;"/> + <spacer flex="1"/> + <radiogroup id="pointerLockRadioGroup" orient="horizontal"> + <radio id="pointerLock#1" command="cmd_pointerLockToggle" label="&permAllow;"/> + <radio id="pointerLock#2" command="cmd_pointerLockToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + </vbox> + </vbox> + </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"/> + </vbox> + </groupbox> + </vbox> + <!-- Others added by overlay --> + </deck> + +#ifdef XP_MACOSX +#include ../browserMountPoints.inc +#endif + +</window> diff --git a/application/palemoon/base/content/pageinfo/permissions.js b/application/palemoon/base/content/pageinfo/permissions.js new file mode 100644 index 0000000000..7a0006b61f --- /dev/null +++ b/application/palemoon/base/content/pageinfo/permissions.js @@ -0,0 +1,398 @@ +/* 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 UNKNOWN = nsIPermissionManager.UNKNOWN_ACTION; // 0 +const ALLOW = nsIPermissionManager.ALLOW_ACTION; // 1 +const DENY = nsIPermissionManager.DENY_ACTION; // 2 +const SESSION = nsICookiePermission.ACCESS_SESSION; // 8 + +const IMAGE_DENY = 2; + +const COOKIE_DENY = 2; +const COOKIE_SESSION = 2; + +const nsIQuotaManager = Components.interfaces.nsIQuotaManager; + +var gPermURI; +var gPrefs; +var gUsageRequest; + +var gPermObj = { + image: function getImageDefaultPermission() + { + if (gPrefs.getIntPref("permissions.default.image") == IMAGE_DENY) { + return DENY; + } + return ALLOW; + }, + popup: function getPopupDefaultPermission() + { + if (gPrefs.getBoolPref("dom.disable_open_during_load")) { + return DENY; + } + return ALLOW; + }, + cookie: function getCookieDefaultPermission() + { + if (gPrefs.getIntPref("network.cookie.cookieBehavior") == COOKIE_DENY) { + return DENY; + } + if (gPrefs.getIntPref("network.cookie.lifetimePolicy") == COOKIE_SESSION) { + return SESSION; + } + return ALLOW; + }, + "desktop-notification": function getNotificationDefaultPermission() + { + if (!gPrefs.getBoolPref("dom.webnotifications.enabled")) { + return DENY; + } + return UNKNOWN; + }, + install: function getInstallDefaultPermission() + { + if (Services.prefs.getBoolPref("xpinstall.whitelist.required")) { + return DENY; + } + return ALLOW; + }, + geo: function getGeoDefaultPermissions() + { + if (!gPrefs.getBoolPref("geo.enabled")) { + return DENY; + } + return ALLOW; + }, + indexedDB: function getIndexedDBDefaultPermissions() + { + if (!gPrefs.getBoolPref("dom.indexedDB.enabled")) { + return DENY; + } + return UNKNOWN; + }, + plugins: function getPluginsDefaultPermissions() + { + return UNKNOWN; + }, + fullscreen: function getFullscreenDefaultPermissions() + { + if (!gPrefs.getBoolPref("full-screen-api.enabled")) { + return DENY; + } + return UNKNOWN; + }, + pointerLock: function getPointerLockPermissions() + { + if (!gPrefs.getBoolPref("full-screen-api.pointer-lock.enabled")) { + return DENY; + } + return ALLOW; + }, +}; + +var permissionObserver = { + observe: function (aSubject, aTopic, aData) + { + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface( + Components.interfaces.nsIPermission); + if (permission.host == gPermURI.host) { + if (permission.type in gPermObj) + initRow(permission.type); + else if (permission.type.startsWith("plugin")) + setPluginsRadioState(); + } + } + } +}; + +function onLoadPermission() +{ + gPrefs = Components.classes[PREFERENCES_CONTRACTID] + .getService(Components.interfaces.nsIPrefBranch); + + var uri = gDocument.documentURIObject; + var permTab = document.getElementById("permTab"); + if (/^https?$/.test(uri.scheme)) { + gPermURI = uri; + var hostText = document.getElementById("hostText"); + hostText.value = gPermURI.host; + + for (var i in gPermObj) + 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; + } + + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + + var checkbox = document.getElementById(aPartId + "Def"); + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + // Desktop Notification, Geolocation and PointerLock permission consumers + // use testExactPermission, not testPermission. + var perm; + if (aPartId == "desktop-notification" || aPartId == "geo" || aPartId == "pointerLock") + perm = permissionManager.testExactPermission(gPermURI, aPartId); + else + perm = permissionManager.testPermission(gPermURI, aPartId); + + if (perm) { + checkbox.checked = false; + command.removeAttribute("disabled"); + } + else { + checkbox.checked = true; + command.setAttribute("disabled", "true"); + perm = gPermObj[aPartId](); + } + setRadioState(aPartId, perm); + + if (aPartId == "indexedDB") { + initIndexedDBRow(); + } +} + +function onCheckboxClick(aPartId) +{ + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + + var command = document.getElementById("cmd_" + aPartId + "Toggle"); + var checkbox = document.getElementById(aPartId + "Def"); + if (checkbox.checked) { + permissionManager.remove(gPermURI.host, aPartId); + command.setAttribute("disabled", "true"); + var perm = gPermObj[aPartId](); + setRadioState(aPartId, perm); + } + else { + onRadioClick(aPartId); + command.removeAttribute("disabled"); + } +} + +function onPluginRadioClick(aEvent) { + onRadioClick(aEvent.originalTarget.getAttribute("id").split('#')[0]); +} + +function onRadioClick(aPartId) +{ + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + + var radioGroup = document.getElementById(aPartId + "RadioGroup"); + var id = radioGroup.selectedItem.id; + var permission = id.split('#')[1]; + if (permission == UNKNOWN) { + permissionManager.remove(gPermURI.host, aPartId); + } else { + permissionManager.add(gPermURI, aPartId, permission); + } +} + +function setRadioState(aPartId, aValue) +{ + var radio = document.getElementById(aPartId + "#" + aValue); + radio.radioGroup.selectedItem = radio; +} + +function initIndexedDBRow() +{ + let row = document.getElementById("permIndexedDBRow"); + let extras = document.getElementById("permIndexedDBExtras"); + + row.appendChild(extras); + + var quotaManager = Components.classes["@mozilla.org/dom/quota/manager;1"] + .getService(nsIQuotaManager); + gUsageRequest = + quotaManager.getUsageForURI(gPermURI, 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;1"] + .getService(nsIQuotaManager) + .clearStoragesForURI(gPermURI); + + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + permissionManager.remove(gPermURI.host, "indexedDB"); + initIndexedDBRow(); +} + +function onIndexedDBUsageCallback(uri, usage, fileUsage) +{ + if (!uri.equals(gPermURI)) { + throw new Error("Callback received for bad URI: " + uri); + } + + 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"); + } +} + +// XXX copied this from browser-plugins.js - is there a way to share? +function makeNicePluginName(aName) { + if (aName == "Shockwave Flash") + return "Adobe Flash"; + + // Clean up the plugin name by stripping off any trailing version numbers + // or "plugin". EG, "Foo Bar Plugin 1.23_02" --> "Foo Bar" + // Do this by first stripping the numbers, etc. off the end, and then + // removing "Plugin" (and then trimming to get rid of any whitespace). + // (Otherwise, something like "Java(TM) Plug-in 1.7.0_07" gets mangled) + let newName = aName.replace(/[\s\d\.\-\_\(\)]+$/, "").replace(/\bplug-?in\b/i, "").trim(); + return newName; +} + +function fillInPluginPermissionTemplate(aPermissionString, aPluginObject) { + let permPluginTemplate = document.getElementById("permPluginTemplate") + .cloneNode(true); + permPluginTemplate.setAttribute("permString", aPermissionString); + permPluginTemplate.setAttribute("tooltiptext", aPluginObject.description); + let attrs = [ + [ ".permPluginTemplateLabel", "value", aPluginObject.name ], + [ ".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"); + permPluginTemplate.removeAttribute("tooltiptext"); + 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 tags = pluginHost.getPluginTags(); + + let permissionMap = new Map(); + + for (let plugin of tags) { + if (plugin.disabled) { + continue; + } + for (let mimeType of plugin.getMimeTypes()) { + if (mimeType == "application/x-shockwave-flash" && plugin.name != "Shockwave Flash") { + continue; + } + let permString = pluginHost.getPermissionStringForType(mimeType); + if (!permissionMap.has(permString)) { + var name = makeNicePluginName(plugin.name) + " " + plugin.version; + if (permString.startsWith("plugin-vulnerable:")) { + name += " \u2014 " + vulnerableLabel; + } + permissionMap.set(permString, { + "name": name, + "description": plugin.description, + }); + } + } + } + + let entries = [ + { + "permission": item[0], + "obj": item[1], + } + for (item of permissionMap) + ]; + entries.sort(function(a, b) { + return ((a.obj.name < b.obj.name) ? -1 : (a.obj.name == b.obj.name ? 0 : 1)); + }); + + let permissionEntries = [ + fillInPluginPermissionTemplate(p.permission, p.obj) for (p of entries) + ]; + + let permPluginsRow = document.getElementById("permPluginsRow"); + clearPluginPermissionTemplate(); + if (permissionEntries.length < 1) { + permPluginsRow.hidden = true; + return; + } + + for (let permissionEntry of permissionEntries) { + permPluginsRow.appendChild(permissionEntry); + } + + setPluginsRadioState(); +} + +function setPluginsRadioState() { + var permissionManager = Components.classes[PERMISSION_CONTRACTID] + .getService(nsIPermissionManager); + let box = document.getElementById("permPluginsRow"); + for (let permissionEntry of box.childNodes) { + if (permissionEntry.hasAttribute("permString")) { + let permString = permissionEntry.getAttribute("permString"); + let permission = permissionManager.testPermission(gPermURI, permString); + setRadioState(permString, permission); + } + } +} diff --git a/application/palemoon/base/content/pageinfo/security.js b/application/palemoon/base/content/pageinfo/security.js new file mode 100644 index 0000000000..e791ab92a8 --- /dev/null +++ b/application/palemoon/base/content/pageinfo/security.js @@ -0,0 +1,378 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var security = { + // Display the server certificate (static) + viewCert : function () { + var cert = security._cert; + viewCertHelper(window, cert); + }, + + _getSecurityInfo : function() { + const nsIX509Cert = Components.interfaces.nsIX509Cert; + const nsIX509CertDB = Components.interfaces.nsIX509CertDB; + const nsX509CertDB = "@mozilla.org/security/x509certdb;1"; + 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 (gWindow != gWindow.top) + return null; + + var hName = null; + try { + hName = gWindow.location.host; + } + catch (exception) { } + + 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 : hName, + cAName : issuerName, + encryptionAlgorithm : undefined, + encryptionStrength : undefined, + encryptionSuite : undefined, + version: undefined, + isBroken : isBroken, + isMixed : isMixed, + isEV : isEV, + cert : cert, + fullLocation : gWindow.location + }; + + var version; + try { + retval.encryptionAlgorithm = status.cipherName; + retval.encryptionStrength = status.secretKeyLength; + retval.encryptionSuite = status.cipherSuite; + 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; + } + + return retval; + } else { + return { + hostName : hName, + cAName : "", + encryptionAlgorithm : "", + encryptionStrength : 0, + encryptionSuite : "", + version: "", + isBroken : isBroken, + isMixed : isMixed, + isEV : isEV, + cert : null, + fullLocation : gWindow.location + }; + } + }, + + // 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; + var uri = gDocument.documentURIObject; + try { + eTLD = eTLDService.getBaseDomain(uri); + } + catch (e) { + // getBaseDomain will fail if the host is an IP address or is empty + eTLD = 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() + { + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var win = wm.getMostRecentWindow("Toolkit:PasswordManager"); + if (win) { + win.setFilter(this._getSecurityInfo().hostName); + win.focus(); + } + else + window.openDialog("chrome://passwordmgr/content/passwordManager.xul", + "Toolkit:PasswordManager", "", + {filterString : this._getSecurityInfo().hostName}); + }, + + _cert : null +}; + +function securityOnLoad() { + var info = security._getSecurityInfo(); + if (!info) { + document.getElementById("securityTab").hidden = true; + document.getElementById("securityBox").collapsed = true; + return; + } + else { + document.getElementById("securityTab").hidden = false; + document.getElementById("securityBox").collapsed = false; + } + + const pageInfoBundle = document.getElementById("pageinfobundle"); + + /* Set Identity section text */ + setText("security-identity-domain-value", info.hostName); + + var owner, verifier, generalPageIdentityString; + 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); + generalPageIdentityString = pageInfoBundle.getFormattedString("generalSiteIdentity", + [owner, verifier]); + } + 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); + generalPageIdentityString = owner; + } + } + else { + // We don't have valid identity credentials. + owner = pageInfoBundle.getString("securityNoOwner"); + verifier = pageInfoBundle.getString("notset"); + generalPageIdentityString = owner; + } + + setText("security-identity-owner-value", owner); + setText("security-identity-verifier-value", verifier); + setText("general-security-identity", generalPageIdentityString); + + /* 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"); + + var uri = gDocument.documentURIObject; + 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"); + } else { + hdr = pkiBundle.getFormattedString("pageInfo_BrokenEncryption", + [info.encryptionAlgorithm, + info.encryptionStrength + "", + info.version]); + } + msg1 = pkiBundle.getString("pageInfo_Privacy_Broken1"); + 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_None3"); + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } + setText("security-technical-shortform", hdr); + setText("security-technical-longform1", msg1); + setText("security-technical-longform2", msg2); + setText("general-security-privacy", hdr); +} + +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/palemoon/base/content/popup-notifications.inc b/application/palemoon/base/content/popup-notifications.inc new file mode 100644 index 0000000000..04f4cb5b78 --- /dev/null +++ b/application/palemoon/base/content/popup-notifications.inc @@ -0,0 +1,97 @@ +# to be included inside a popupset element + + <panel id="notification-popup" + type="arrow" + footertype="promobox" + position="after_start" + hidden="true" + orient="vertical" + role="alert"/> + + <!-- Popup for site identity information --> + <panel id="identity-popup" + type="arrow" + hidden="true" + noautofocus="true" + consumeoutsideclicks="true" + onpopupshown="gIdentityHandler.onPopupShown(event);" + level="top"> + <hbox id="identity-popup-container" align="top"> + <image id="identity-popup-icon"/> + <vbox id="identity-popup-content-box"> + <label id="identity-popup-connectedToLabel" + class="identity-popup-label" + value="&identity.connectedTo;"/> + <label id="identity-popup-connectedToLabel2" + class="identity-popup-label" + value="&identity.unverifiedsite2;"/> + <description id="identity-popup-content-host" + class="identity-popup-description"/> + <label id="identity-popup-runByLabel" + class="identity-popup-label" + value="&identity.runBy;"/> + <description id="identity-popup-content-owner" + class="identity-popup-description"/> + <description id="identity-popup-content-supplemental" + class="identity-popup-description"/> + <description id="identity-popup-content-verifier" + class="identity-popup-description"/> + <hbox id="identity-popup-encryption" flex="1"> + <vbox> + <image id="identity-popup-encryption-icon"/> + </vbox> + <description id="identity-popup-encryption-label" flex="1" + class="identity-popup-description"/> + </hbox> + <!-- Footer button to open security page info --> + <hbox id="identity-popup-button-container" pack="end"> + <button id="identity-popup-more-info-button" + label="&identity.moreInfoLinkText;" + onblur="gIdentityHandler.hideIdentityPopup();" + oncommand="gIdentityHandler.handleMoreInfoClick(event);"/> + </hbox> + </vbox> + </hbox> + </panel> + + + <popupnotification id="webRTC-shareDevices-notification" hidden="true"> + <popupnotificationcontent id="webRTC-selectCamera" orient="vertical"> + <separator class="thin"/> + <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-selectMicrophone" orient="vertical"> + <separator class="thin"/> + <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="pointerLock-notification" hidden="true"> + <popupnotificationcontent orient="vertical" align="start"> + <separator class="thin"/> + <label id="pointerLock-cancel" value="&pointerLock.notification.message;"/> + </popupnotificationcontent> + </popupnotification> + + <popupnotification id="mixed-content-blocked-notification" hidden="true"> + <popupnotificationcontent orient="vertical" align="start"> + <separator/> + <description id="mixed-content-blocked-moreinfo">&mixedContentBlocked.moreinfo;</description> + </popupnotificationcontent> + </popupnotification> diff --git a/application/palemoon/base/content/report-phishing-overlay.xul b/application/palemoon/base/content/report-phishing-overlay.xul new file mode 100644 index 0000000000..76baf01da3 --- /dev/null +++ b/application/palemoon/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="&reportPhishSiteMenu.title2;" + accesskey="&reportPhishSiteMenu.accesskey;" + insertbefore="aboutSeparator" + observes="reportPhishingBroadcaster" + oncommand="openUILink(gSafeBrowsing.getReportURL('Phish'), event);" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="menu_HelpPopup_reportPhishingErrortoolmenu" + label="&safeb.palm.notforgery.label2;" + accesskey="&reportPhishSiteMenu.accesskey;" + insertbefore="aboutSeparator" + observes="reportPhishingErrorBroadcaster" + oncommand="openUILinkIn(gSafeBrowsing.getReportURL('Error'), 'tab');" + onclick="checkForMiddleClick(this, event);"/> + </menupopup> +</overlay> diff --git a/application/palemoon/base/content/safeMode.css b/application/palemoon/base/content/safeMode.css new file mode 100644 index 0000000000..4f093a452f --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/safeMode.js b/application/palemoon/base/content/safeMode.js new file mode 100644 index 0000000000..e1e5c72859 --- /dev/null +++ b/application/palemoon/base/content/safeMode.js @@ -0,0 +1,128 @@ +/* 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 Cc = Components.classes, + Ci = Components.interfaces, + Cu = Components.utils; + +Cu.import("resource://gre/modules/AddonManager.jsm"); + +function restartApp() { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit | Ci.nsIAppStartup.eRestart); +} + +function clearAllPrefs() { + var prefService = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefService); + prefService.resetUserPrefs(); + + // Remove the pref-overrides dir, if it exists + try { + var fileLocator = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties); + const NS_APP_PREFS_OVERRIDE_DIR = "PrefDOverride"; + var prefOverridesDir = fileLocator.get(NS_APP_PREFS_OVERRIDE_DIR, + Ci.nsIFile); + prefOverridesDir.remove(true); + } catch (ex) { + Components.utils.reportError(ex); + } +} + +function restoreDefaultBookmarks() { + var prefBranch = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); + prefBranch.setBoolPref("browser.bookmarks.restore_default_bookmarks", true); +} + +function deleteLocalstore() { + const nsIDirectoryServiceContractID = "@mozilla.org/file/directory_service;1"; + const nsIProperties = Ci.nsIProperties; + var directoryService = Cc[nsIDirectoryServiceContractID] + .getService(nsIProperties); + // Local store file + var localstoreFile = directoryService.get("LStoreS", Components.interfaces.nsIFile); + // XUL store file + var xulstoreFile = directoryService.get("ProfD", Components.interfaces.nsIFile); + xulstoreFile.append("xulstore.json"); + try { + xulstoreFile.remove(false); + if (localstoreFile.exists()) { + localstoreFile.remove(false); + } + } catch(e) { + Components.utils.reportError(e); + } +} + +function disableAddons() { + AddonManager.getAllAddons(function(aAddons) { + aAddons.forEach(function(aAddon) { + if (aAddon.type == "theme") { + // Setting userDisabled to false on the default theme activates it, + // disables all other themes and deactivates the applied persona, if + // any. + const DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}"; + if (aAddon.id == DEFAULT_THEME_ID) + aAddon.userDisabled = false; + } + else { + aAddon.userDisabled = true; + } + }); + + restartApp(); + }); +} + +function restoreDefaultSearchEngines() { + var searchService = Cc["@mozilla.org/browser/search-service;1"] + .getService(Ci.nsIBrowserSearchService); + + searchService.restoreDefaultEngines(); +} + +function onOK() { + try { + if (document.getElementById("resetUserPrefs").checked) + clearAllPrefs(); + if (document.getElementById("deleteBookmarks").checked) + restoreDefaultBookmarks(); + if (document.getElementById("resetToolbars").checked) + deleteLocalstore(); + if (document.getElementById("restoreSearch").checked) + restoreDefaultSearchEngines(); + if (document.getElementById("disableAddons").checked) { + disableAddons(); + // disableAddons will asynchronously restart the application + return false; + } + } catch(e) { + } + + restartApp(); + return false; +} + +function onCancel() { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(Ci.nsIAppStartup.eForceQuit); +} + +function onLoad() { + document.getElementById("tasks") + .addEventListener("CheckboxStateChange", UpdateOKButtonState, false); +} + +function UpdateOKButtonState() { + document.documentElement.getButton("accept").disabled = + !document.getElementById("resetUserPrefs").checked && + !document.getElementById("deleteBookmarks").checked && + !document.getElementById("resetToolbars").checked && + !document.getElementById("disableAddons").checked && + !document.getElementById("restoreSearch").checked; +} diff --git a/application/palemoon/base/content/safeMode.xul b/application/palemoon/base/content/safeMode.xul new file mode 100644 index 0000000000..656df6eaf8 --- /dev/null +++ b/application/palemoon/base/content/safeMode.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> + +<!-- +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE prefwindow [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % safeModeDTD SYSTEM "chrome://browser/locale/safeMode.dtd" > +%safeModeDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" > +%browserDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/"?> + +<dialog id="safeModeDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&safeModeDialog.title;" + buttons="accept,cancel,extra1" + buttonlabelaccept="&changeAndRestartButton.label;" +#ifdef XP_WIN + buttonlabelcancel="&quitApplicationCmdWin.label;" +#else + buttonlabelcancel="&quitApplicationCmd.label;" +#endif + buttonlabelextra1="&continueButton.label;" + width="&window.width;" + ondialogaccept="return onOK()" + ondialogcancel="onCancel()" + ondialogextra1="window.close()" + onload="onLoad();" + buttondisabledaccept="true"> + + <script type="application/javascript" src="chrome://browser/content/safeMode.js"/> + + <stringbundle id="preferencesBundle" src="chrome://browser/locale/preferences/preferences.properties"/> + + <description>&safeModeDescription.label;</description> + + <separator class="thin"/> + + <label value="&safeModeDescription2.label;"/> + <vbox id="tasks"> + <checkbox id="disableAddons" label="&disableAddons.label;" accesskey="&disableAddons.accesskey;"/> + <checkbox id="resetToolbars" label="&resetToolbars.label;" accesskey="&resetToolbars.accesskey;"/> + <checkbox id="deleteBookmarks" label="&deleteBookmarks.label;" accesskey="&deleteBookmarks.accesskey;"/> + <checkbox id="resetUserPrefs" label="&resetUserPrefs.label;" accesskey="&resetUserPrefs.accesskey;"/> + <checkbox id="restoreSearch" label="&restoreSearch.label;" accesskey="&restoreSearch.accesskey;"/> + </vbox> + + <separator class="thin"/> +</dialog> diff --git a/application/palemoon/base/content/sanitize.js b/application/palemoon/base/content/sanitize.js new file mode 100644 index 0000000000..89843c86d3 --- /dev/null +++ b/application/palemoon/base/content/sanitize.js @@ -0,0 +1,527 @@ +# -*- 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/. + +Components.utils.import("resource://gre/modules/XPCOMUtils.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:///modules/promise.js"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "console", + "resource://gre/modules/devtools/Console.jsm"); + +function Sanitizer() {} +Sanitizer.prototype = { + // warning to the caller: this one may raise an exception (e.g. bug #265028) + clearItem: function (aItemName) + { + if (this.items[aItemName].canClear) + this.items[aItemName].clear(); + }, + + canClearItem: function (aItemName, aCallback, aArg) + { + let canClear = this.items[aItemName].canClear; + if (typeof canClear == "function") { + canClear(aCallback, aArg); + return false; + } + + aCallback(aItemName, canClear, aArg); + return canClear; + }, + + prefDomain: "", + isShutDown: false, + + 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. + */ + sanitize: function () + { + var deferred = Promise.defer(); + var psvc = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + var branch = psvc.getBranch(this.prefDomain); + var seenError = false; + + // Cache the range of times to clear + if (this.ignoreTimespan) + var range = null; // If we ignore timespan, clear everything + else + range = this.range || Sanitizer.getClearRange(); + + let itemCount = Object.keys(this.items).length; + let onItemComplete = function() { + if (!--itemCount) { + seenError ? deferred.reject() : deferred.resolve(); + } + }; + for (var itemName in this.items) { + let item = this.items[itemName]; + item.range = range; + item.isShutDown = this.isShutDown; + if ("clear" in item && branch.getBoolPref(itemName)) { + let clearCallback = (itemName, aCanClear) => { + // Some of these clear() may raise exceptions (see bug #265028) + // to sanitize as much as possible, we catch and store them, + // rather than fail fast. + // Callers should check returned errors and give user feedback + // about items that could not be sanitized + let item = this.items[itemName]; + try { + if (aCanClear) + item.clear(); + } catch(er) { + seenError = true; + console.error("Error sanitizing " + itemName + ": " + er + "\n"); + } + onItemComplete(); + }; + this.canClearItem(itemName, clearCallback); + } else { + onItemComplete(); + } + } + + return deferred.promise; + }, + + // 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: function () + { + var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]. + getService(Ci.nsICacheStorageService); + try { + // Cache doesn't consult timespan, nor does it have the + // facility for timespan-based eviction. Wipe it. + cache.clear(); + } catch(er) {} + + var imageCache = Cc["@mozilla.org/image/tools;1"]. + getService(Ci.imgITools).getImgCacheForDocument(null); + try { + imageCache.clearCache(false); // true=chrome, false=content + } catch(er) {} + }, + + get canClear() + { + return true; + } + }, + + cookies: { + clear: function () + { + var cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"] + .getService(Ci.nsICookieManager); + if (this.range) { + // Iterate through the cookies and delete any created after our cutoff. + var cookiesEnum = cookieMgr.enumerator; + while (cookiesEnum.hasMoreElements()) { + var cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2); + + if (cookie.creationTime > this.range[0]) + // This cookie was created after our cutoff, clear it + cookieMgr.remove(cookie.host, cookie.name, cookie.path, false); + } + } + else { + // Remove everything + cookieMgr.removeAll(); + } + + // Clear plugin data. + const phInterface = Ci.nsIPluginHost; + const FLAG_CLEAR_ALL = phInterface.FLAG_CLEAR_ALL; + let ph = Cc["@mozilla.org/plugin/host;1"].getService(phInterface); + + // Determine age range in seconds. (-1 means clear all.) We don't know + // that this.range[1] is actually now, so we compute age range based + // on the lower bound. If this.range results in a negative age, do + // nothing. + let age = this.range ? (Date.now() / 1000 - this.range[0] / 1000000) + : -1; + if (!this.range || age >= 0) { + let tags = ph.getPluginTags(); + for (let i = 0; i < tags.length; i++) { + try { + ph.clearSiteData(tags[i], null, FLAG_CLEAR_ALL, age); + } catch (e) { + // If the plugin doesn't support clearing by age, clear everything. + if (e.result == Components.results. + NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED) { + try { + ph.clearSiteData(tags[i], null, FLAG_CLEAR_ALL, -1); + } catch (e) { + // Ignore errors from the plugin + } + } + } + } + } + }, + + get canClear() + { + return true; + } + }, + + offlineApps: { + clear: function () + { + Components.utils.import("resource:///modules/offlineAppCache.jsm"); + OfflineAppCacheHelper.clear(); + if (!this.range || this.isShutDown) { + Components.utils.import("resource:///modules/QuotaManager.jsm"); + QuotaManagerHelper.clear(this.isShutDown); + } + }, + + get canClear() + { + return true; + } + }, + + history: { + clear: function () + { + if (this.range) + PlacesUtils.history.removeVisitsByTimeframe(this.range[0], this.range[1]); + else + PlacesUtils.history.removeAllPages(); + + try { + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.notifyObservers(null, "browser:purge-session-history", ""); + } + catch (e) { } + + // Clear last URL of the Open Web Location dialog + var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + try { + prefs.clearUserPref("general.open_location.last_url"); + } + catch (e) { } + }, + + get canClear() + { + // bug 347231: Always allow clearing history due to dependencies on + // the browser:purge-session-history notification. (like error console) + return true; + } + }, + + formdata: { + clear: function () + { + // Clear undo history of all searchBars + var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1'] + .getService(Components.interfaces.nsIWindowMediator); + var windows = windowManager.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + let currentDocument = windows.getNext().document; + let searchBar = currentDocument.getElementById("searchbar"); + if (searchBar) + searchBar.textbox.reset(); + let findBar = currentDocument.getElementById("FindToolbar"); + if (findBar) + findBar.clear(); + } + + let change = { op: "remove" }; + if (this.range) { + [ change.firstUsedStart, change.firstUsedEnd ] = this.range; + } + FormHistory.update(change); + }, + + canClear : function(aCallback, aArg) + { + var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1'] + .getService(Components.interfaces.nsIWindowMediator); + var windows = windowManager.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) { + let currentDocument = windows.getNext().document; + let searchBar = currentDocument.getElementById("searchbar"); + if (searchBar) { + let transactionMgr = searchBar.textbox.editor.transactionManager; + if (searchBar.value || + transactionMgr.numberOfUndoItems || + transactionMgr.numberOfRedoItems) { + aCallback("formdata", true, aArg); + return false; + } + } + let findBar = currentDocument.getElementById("FindToolbar"); + if (findBar && findBar.canClear) { + aCallback("formdata", true, aArg); + return false; + } + } + + let count = 0; + let countDone = { + handleResult : function(aResult) count = aResult, + handleError : function(aError) Components.utils.reportError(aError), + handleCompletion : + function(aReason) { aCallback("formdata", aReason == 0 && count > 0, aArg); } + }; + FormHistory.count({}, countDone); + return false; + } + }, + + downloads: { + clear: Task.async(function* (range) { + let 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 {} + }), + + get canClear() + { + //Clearing is always possible with JSTransfers + return true; + } + }, + + passwords: { + clear: function () + { + var pwmgr = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + // Passwords are timeless, and don't respect the timeSpan setting + pwmgr.removeAllLogins(); + }, + + get canClear() + { + var pwmgr = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + var count = pwmgr.countLogins("", "", ""); // count all logins + return (count > 0); + } + }, + + sessions: { + clear: function () + { + // clear all auth tokens + var sdr = Components.classes["@mozilla.org/security/sdr;1"] + .getService(Components.interfaces.nsISecretDecoderRing); + sdr.logoutAndTeardown(); + + // clear FTP and plain HTTP auth sessions + var os = Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService); + os.notifyObservers(null, "net:clear-active-logins", null); + }, + + get canClear() + { + return true; + } + }, + + siteSettings: { + clear: function () + { + // Clear site-specific permissions like "Allow this site to open popups" + var pm = Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager); + pm.removeAll(); + + // Clear site-specific settings like page-zoom level + var cps = Components.classes["@mozilla.org/content-pref/service;1"] + .getService(Components.interfaces.nsIContentPrefService2); + cps.removeAllDomains(null); + + // Clear "Never remember passwords for this site", which is not handled by + // the permission manager + var pwmgr = Components.classes["@mozilla.org/login-manager;1"] + .getService(Components.interfaces.nsILoginManager); + var hosts = pwmgr.getAllDisabledHosts(); + for each (var host in hosts) { + pwmgr.setLoginSavingEnabled(host, true); + } + }, + + get canClear() + { + return true; + } + }, + + connectivityData: { + clear: function () + { + // Clear site security settings + var sss = Components.classes["@mozilla.org/ssservice;1"] + .getService(Components.interfaces.nsISiteSecurityService); + sss.clearAll(); + }, + + get canClear() + { + return true; + } + } + } +}; + + + +// "Static" members +Sanitizer.prefDomain = "privacy.sanitize."; +Sanitizer.prefShutdown = "sanitizeOnShutdown"; +Sanitizer.prefDidShutdown = "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.IS_SHUTDOWN = true; + +// 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_HOUR : + var 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; + 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.prefDomain); +}); + +// Shows sanitization UI +Sanitizer.showUI = function(aParentWindow) +{ + var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIWindowWatcher); +#ifdef XP_MACOSX + ww.openWindow(null, // make this an app-modal window on Mac +#else + ww.openWindow(aParentWindow, +#endif + "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 = function() +{ + // we check for unclean exit with pending sanitization + Sanitizer._checkAndSanitize(); +}; + +Sanitizer.onShutdown = function() +{ + // we check if sanitization is needed and perform it + Sanitizer._checkAndSanitize(Sanitizer.IS_SHUTDOWN); +}; + +// this is called on startup and shutdown, to perform pending sanitizations +Sanitizer._checkAndSanitize = function(isShutDown) +{ + const prefs = Sanitizer.prefs; + if (prefs.getBoolPref(Sanitizer.prefShutdown) && + !prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) { + // this is a shutdown or a startup after an unclean exit + var s = new Sanitizer(); + s.prefDomain = "privacy.clearOnShutdown."; + s.isShutDown = isShutDown; + s.sanitize().then(function() { + prefs.setBoolPref(Sanitizer.prefDidShutdown, true); + }); + } +}; diff --git a/application/palemoon/base/content/sanitize.xul b/application/palemoon/base/content/sanitize.xul new file mode 100644 index 0000000000..691be926e8 --- /dev/null +++ b/application/palemoon/base/content/sanitize.xul @@ -0,0 +1,190 @@ +<?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: &dialog.width2;;" + 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"/> + <preference id="privacy.cpd.connectivityData" name="privacy.cpd.connectivityData" 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="8" 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();"/> + <listitem label="&itemConnectivityData.label;" + type="checkbox" + accesskey="&itemConnectivityData.accesskey;" + preference="privacy.cpd.connectivityData" + noduration="true" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + </listbox> + + </prefpane> +</prefwindow> diff --git a/application/palemoon/base/content/sanitizeDialog.css b/application/palemoon/base/content/sanitizeDialog.css new file mode 100644 index 0000000000..27c3c08666 --- /dev/null +++ b/application/palemoon/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: -moz-grab; +} + + +/* Sanitize everything warnings */ + +#sanitizeEverythingWarning, +#sanitizeEverythingUndoWarning { + white-space: pre-wrap; +} diff --git a/application/palemoon/base/content/sanitizeDialog.js b/application/palemoon/base/content/sanitizeDialog.js new file mode 100644 index 0000000000..18df5e4a42 --- /dev/null +++ b/application/palemoon/base/content/sanitizeDialog.js @@ -0,0 +1,910 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Cc = Components.classes; +const Ci = Components.interfaces; + +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."; + + let sanitizeItemList = document.querySelectorAll("#itemList > [preference]"); + for (let i = 0; i < sanitizeItemList.length; i++) { + let prefItem = sanitizeItemList[i]; + let name = s.getNameFromPreference(prefItem.getAttribute("preference")); + s.canClearItem(name, function canClearCallback(aItem, aCanClear, aPrefItem) { + if (!aCanClear) { + aPrefItem.preference = null; + aPrefItem.checked = false; + aPrefItem.disabled = true; + } + }, prefItem); + } + + 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(window.close, window.close); + } catch (er) { + Components.utils.reportError("Exception during sanitize: " + er); + return true; // We *do* want to close immediately on error. + } + return false; + }, + + /** + * 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."; + + let sanitizeItemList = document.querySelectorAll("#itemList > [preference]"); + for (let i = 0; i < sanitizeItemList.length; i++) { + let prefItem = sanitizeItemList[i]; + let name = s.getNameFromPreference(prefItem.getAttribute("preference")); + s.canClearItem(name, function canClearCallback(aCanClear) { + if (!aCanClear) { + prefItem.preference = null; + prefItem.checked = false; + prefItem.disabled = true; + } + }); + } + + 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(); + } 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/supports-array;1"]. + createInstance(Ci.nsISupportsArray); + var trans = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + trans.init(null); + trans.setTransferData('dummy-flavor', null, 0); + arr.AppendElement(trans); + 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/palemoon/base/content/softwareUpdateOverlay.xul b/application/palemoon/base/content/softwareUpdateOverlay.xul new file mode 100644 index 0000000000..01170e46c3 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/sync/aboutSyncTabs-bindings.xml b/application/palemoon/base/content/sync/aboutSyncTabs-bindings.xml new file mode 100644 index 0000000000..e6108209a4 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/sync/aboutSyncTabs.css b/application/palemoon/base/content/sync/aboutSyncTabs.css new file mode 100644 index 0000000000..5a353175b1 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/sync/aboutSyncTabs.js b/application/palemoon/base/content/sync/aboutSyncTabs.js new file mode 100644 index 0000000000..535c43e561 --- /dev/null +++ b/application/palemoon/base/content/sync/aboutSyncTabs.js @@ -0,0 +1,313 @@ +/* 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 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"); + +let RemoteTabViewer = { + _tabsList: null, + + init: function () { + Services.obs.addObserver(this, "weave:service:login:finish", false); + Services.obs.addObserver(this, "weave:engine:sync:finish", 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"); + }, + + 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 (force) { + if (this._waitingForBuildList) { + this._buildListRequested = true; + return; + } + + this._waitingForBuildList = true; + this._buildListRequested = false; + + this._clearTabList(); + + if (Weave.Service.isLoggedIn && this._refetchTabs(force)) { + this._generateWeaveTabList(); + } else { + //XXXzpao We should say something about not being logged in & not having data + // or tell the appropriate condition. (bug 583344) + } + + function complete() { + this._waitingForBuildList = false; + if (this._buildListRequested) { + CommonUtils.nextTick(this.buildList, this); + } + } + + complete(); + }, + + _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 [guid, client] in Iterator(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); + } + }, + + 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; + } + } + + // if Clients hasn't synced yet this session, we need to sync it as well. + if (Weave.Service.clientsEngine.lastSync == 0) { + Weave.Service.clientsEngine.sync(); + } + + // Force a sync only for the tabs engine + let engine = Weave.Service.engineManager.get("tabs"); + engine.lastModified = null; + engine.sync(); + 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": + this.buildList(true); + break; + case "weave:engine:sync:finish": + if (subject == "tabs") { + 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/palemoon/base/content/sync/aboutSyncTabs.xul b/application/palemoon/base/content/sync/aboutSyncTabs.xul new file mode 100644 index 0000000000..a4aa0032f1 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/sync/addDevice.js b/application/palemoon/base/content/sync/addDevice.js new file mode 100644 index 0000000000..556e757687 --- /dev/null +++ b/application/palemoon/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/. */ + +const Ci = Components.interfaces; +const Cc = Components.classes; +const 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; + +let 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/palemoon/base/content/sync/addDevice.xul b/application/palemoon/base/content/sync/addDevice.xul new file mode 100644 index 0000000000..f2371aad0d --- /dev/null +++ b/application/palemoon/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="http://www.palemoon.org/sync/help/easy-setup.shtml"/> + </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/palemoon/base/content/sync/genericChange.js b/application/palemoon/base/content/sync/genericChange.js new file mode 100644 index 0000000000..6d1ce94856 --- /dev/null +++ b/application/palemoon/base/content/sync/genericChange.js @@ -0,0 +1,234 @@ +/* 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 Ci = Components.interfaces; +const Cc = Components.classes; + +Components.utils.import("resource://services-sync/main.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +let 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 introText2 = document.getElementById("introText2"); + 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; + 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(); + break; + case "ChangePassword": + return this.doChangePassword(); + break; + } + }, + + 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 { + //Pale Moon: Enforce minimum length of 8 for allowed custom passphrase + //and don't restrict it to "out of sync" situations only. People who + //go to this page generally know what they are doing ;) + valid = this._passphraseBox.value.length >= 8; + } + + 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/palemoon/base/content/sync/genericChange.xul b/application/palemoon/base/content/sync/genericChange.xul new file mode 100644 index 0000000000..3c0b2cd6c2 --- /dev/null +++ b/application/palemoon/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 inline-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="http://www.palemoon.org/sync/help/recoverykey.shtml"> + &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/palemoon/base/content/sync/key.xhtml b/application/palemoon/base/content/sync/key.xhtml new file mode 100644 index 0000000000..92abf0ee64 --- /dev/null +++ b/application/palemoon/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="http://www.palemoon.org/sync/">http://www.palemoon.org/sync/</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/palemoon/base/content/sync/notification.xml b/application/palemoon/base/content/sync/notification.xml new file mode 100644 index 0000000000..7a2b773821 --- /dev/null +++ b/application/palemoon/base/content/sync/notification.xml @@ -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/. --> + +<!DOCTYPE bindings [ +<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd"> +%notificationDTD; +]> + +<bindings id="notificationBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="notificationbox" extends="chrome://global/content/bindings/notification.xml#notificationbox"> + <content> + <xul:vbox xbl:inherits="hidden=notificationshidden"> + <xul:spacer/> + <children includes="notification"/> + </xul:vbox> + <children/> + </content> + + <implementation> + <constructor><![CDATA[ + let temp = {}; + Cu.import("resource://services-common/observers.js", temp); + temp.Observers.add("weave:notification:added", this.onNotificationAdded, this); + temp.Observers.add("weave:notification:removed", this.onNotificationRemoved, this); + + for each (var notification in Weave.Notifications.notifications) + this._appendNotification(notification); + ]]></constructor> + + <destructor><![CDATA[ + let temp = {}; + Cu.import("resource://services-common/observers.js", temp); + temp.Observers.remove("weave:notification:added", this.onNotificationAdded, this); + temp.Observers.remove("weave:notification:removed", this.onNotificationRemoved, this); + ]]></destructor> + + <method name="onNotificationAdded"> + <parameter name="subject"/> + <parameter name="data"/> + <body><![CDATA[ + this._appendNotification(subject); + ]]></body> + </method> + + <method name="onNotificationRemoved"> + <parameter name="subject"/> + <parameter name="data"/> + <body><![CDATA[ + // If the view of the notification hasn't been removed yet, remove it. + var notifications = this.allNotifications; + for each (var notification in notifications) { + if (notification.notification == subject) { + notification.close(); + break; + } + } + ]]></body> + </method> + + <method name="_appendNotification"> + <parameter name="notification"/> + <body><![CDATA[ + var node = this.appendNotification(notification.description, + notification.title, + notification.iconURL, + notification.priority, + notification.buttons); + node.notification = notification; + ]]></body> + </method> + + </implementation> + </binding> + + <binding id="notification" extends="chrome://global/content/bindings/notification.xml#notification"> + <content> + <xul:hbox class="notification-inner outset" flex="1" xbl:inherits="type"> + <xul:toolbarbutton ondblclick="event.stopPropagation();" + class="messageCloseButton close-icon tabbable" + xbl:inherits="hidden=hideclose" + tooltiptext="&closeNotification.tooltip;" + oncommand="document.getBindingParent(this).close()"/> + <xul:hbox anonid="details" align="center" flex="1"> + <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image"/> + <xul:description anonid="messageText" class="messageText" xbl:inherits="xbl:text=label"/> + + <!-- The children are the buttons defined by the notification. --> + <xul:hbox oncommand="document.getBindingParent(this)._doButtonCommand(event);"> + <children/> + </xul:hbox> + </xul:hbox> + </xul:hbox> + </content> + <implementation> + <!-- Note: this used to be a field, but for some reason it kept getting + - reset to its default value for TabNotification elements. + - As a property, that doesn't happen, even though the property stores + - its value in a JS property |_notification| that is not defined + - in XBL as a field or property. Maybe this is wrong, but it works. + --> + <property name="notification" + onget="return this._notification" + onset="this._notification = val; return val;"/> + <method name="close"> + <body><![CDATA[ + Weave.Notifications.remove(this.notification); + + // We should be able to call the base class's close method here + // to remove the notification element from the notification box, + // but we can't because of bug 373652, so instead we copied its code + // and execute it below. + var control = this.control; + if (control) + control.removeNotification(this); + else + this.hidden = true; + ]]></body> + </method> + </implementation> + </binding> + +</bindings> diff --git a/application/palemoon/base/content/sync/progress.js b/application/palemoon/base/content/sync/progress.js new file mode 100644 index 0000000000..2063f612a8 --- /dev/null +++ b/application/palemoon/base/content/sync/progress.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/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://services-sync/main.js"); + +let gProgressBar; +let gCounter = 0; + +function onLoad(event) { + Services.obs.addObserver(onEngineSync, "weave:engine:sync:finish", false); + Services.obs.addObserver(onEngineSync, "weave:engine:sync:error", false); + Services.obs.addObserver(onServiceSync, "weave:service:sync:finish", false); + Services.obs.addObserver(onServiceSync, "weave:service:sync:error", false); + + gProgressBar = document.getElementById('uploadProgressBar'); + + if (Services.prefs.getPrefType("services.sync.firstSync") != Ci.nsIPrefBranch.PREF_INVALID) { + gProgressBar.hidden = false; + } + else { + gProgressBar.hidden = true; + } +} + +function onUnload(event) { + cleanUpObservers(); +} + +function cleanUpObservers() { + try { + Services.obs.removeObserver(onEngineSync, "weave:engine:sync:finish"); + Services.obs.removeObserver(onEngineSync, "weave:engine:sync:error"); + Services.obs.removeObserver(onServiceSync, "weave:service:sync:finish"); + Services.obs.removeObserver(onServiceSync, "weave:service:sync:error"); + } + catch (e) { + // may be double called by unload & exit. Ignore. + } +} + +function onEngineSync(subject, topic, data) { + // The Clients engine syncs first. At this point we don't necessarily know + // yet how many engines will be enabled, so we'll ignore the Clients engine + // and evaluate how many engines are enabled when the first "real" engine + // syncs. + if (data == "clients") { + return; + } + + if (!gCounter && + Services.prefs.getPrefType("services.sync.firstSync") != Ci.nsIPrefBranch.PREF_INVALID) { + gProgressBar.max = Weave.Service.engineManager.getEnabled().length; + } + + gCounter += 1; + gProgressBar.setAttribute("value", gCounter); +} + +function onServiceSync(subject, topic, data) { + // To address the case where 0 engines are synced, we will fill the + // progress bar so the user knows that the sync has finished. + gProgressBar.setAttribute("value", gProgressBar.max); + cleanUpObservers(); +} + +function closeTab() { + window.close(); +} diff --git a/application/palemoon/base/content/sync/progress.xhtml b/application/palemoon/base/content/sync/progress.xhtml new file mode 100644 index 0000000000..d403cb20d8 --- /dev/null +++ b/application/palemoon/base/content/sync/progress.xhtml @@ -0,0 +1,55 @@ +<?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 % syncProgressDTD + SYSTEM "chrome://browser/locale/syncProgress.dtd"> + %syncProgressDTD; + <!ENTITY % syncSetupDTD + SYSTEM "chrome://browser/locale/syncSetup.dtd"> + %syncSetupDTD; + <!ENTITY % globalDTD + SYSTEM "chrome://global/locale/global.dtd"> + %globalDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&syncProgress.pageTitle;</title> + + <link rel="stylesheet" type="text/css" media="all" + href="chrome://browser/skin/syncProgress.css"/> + + <link rel="icon" type="image/png" id="favicon" + href="chrome://browser/skin/sync-16.png"/> + + <script type="text/javascript;version=1.8" + src="chrome://browser/content/sync/progress.js"/> + </head> + <body onload="onLoad(event)" onunload="onUnload(event)" dir="&locale.dir;"> + <title>&setup.successPage.title;</title> + <div id="floatingBox" class="main-content"> + <div id="title"> + <h1>&setup.successPage.title;</h1> + </div> + <div id="successLogo"> + <img id="brandSyncLogo" src="chrome://browser/skin/sync-128.png" alt="&syncProgress.logoAltText;" /> + </div> + <div id="loadingText"> + <p id="blurb">&syncProgress.textBlurb; </p> + </div> + <div id="progressBar"> + <progress id="uploadProgressBar" value="0"/> + </div> + <div id="bottomRow"> + <button id="closeButton" onclick="closeTab()">&syncProgress.closeButton; </button> + </div> + </div> + </body> +</html> diff --git a/application/palemoon/base/content/sync/quota.js b/application/palemoon/base/content/sync/quota.js new file mode 100644 index 0000000000..7117a2ddfa --- /dev/null +++ b/application/palemoon/base/content/sync/quota.js @@ -0,0 +1,267 @@ +/* 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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://services-sync/main.js"); +Cu.import("resource://gre/modules/DownloadUtils.jsm"); + +let gSyncQuota = { + + init: function init() { + this.bundle = document.getElementById("quotaStrings"); + let caption = document.getElementById("treeCaption"); + caption.firstChild.nodeValue = this.bundle.getString("quota.treeCaption.label"); + + gUsageTreeView.init(); + this.tree = document.getElementById("usageTree"); + this.tree.view = gUsageTreeView; + + this.loadData(); + }, + + loadData: function loadData() { + this._usage_req = Weave.Service.getStorageInfo(Weave.INFO_COLLECTION_USAGE, + function (error, usage) { + delete gSyncQuota._usage_req; + // displayUsageData handles null values, so no need to check 'error'. + gUsageTreeView.displayUsageData(usage); + }); + + let usageLabel = document.getElementById("usageLabel"); + let bundle = this.bundle; + + this._quota_req = Weave.Service.getStorageInfo(Weave.INFO_QUOTA, + function (error, quota) { + delete gSyncQuota._quota_req; + + if (error) { + usageLabel.value = bundle.getString("quota.usageError.label"); + return; + } + let used = gSyncQuota.convertKB(quota[0]); + if (!quota[1]) { + // No quota on the server. + usageLabel.value = bundle.getFormattedString( + "quota.usageNoQuota.label", used); + return; + } + let percent = Math.round(100 * quota[0] / quota[1]); + let total = gSyncQuota.convertKB(quota[1]); + usageLabel.value = bundle.getFormattedString( + "quota.usagePercentage.label", [percent].concat(used).concat(total)); + }); + }, + + onCancel: function onCancel() { + if (this._usage_req) { + this._usage_req.abort(); + } + if (this._quota_req) { + this._quota_req.abort(); + } + return true; + }, + + onAccept: function onAccept() { + let engines = gUsageTreeView.getEnginesToDisable(); + for each (let engine in engines) { + Weave.Service.engineManager.get(engine).enabled = false; + } + if (engines.length) { + // The 'Weave' object will disappear once the window closes. + let Service = Weave.Service; + Weave.Utils.nextTick(function() { Service.sync(); }); + } + return this.onCancel(); + }, + + convertKB: function convertKB(value) { + return DownloadUtils.convertByteUnits(value * 1024); + } + +}; + +let gUsageTreeView = { + + _ignored: {keys: true, + meta: true, + clients: true}, + + /* + * Internal data structures underlaying the tree. + */ + _collections: [], + _byname: {}, + + init: function init() { + let retrievingLabel = gSyncQuota.bundle.getString("quota.retrieving.label"); + for each (let engine in Weave.Service.engineManager.getEnabled()) { + if (this._ignored[engine.name]) + continue; + + // Some engines use the same pref, which means they can only be turned on + // and off together. We need to combine them here as well. + let existing = this._byname[engine.prefName]; + if (existing) { + existing.engines.push(engine.name); + continue; + } + + let obj = {name: engine.prefName, + title: this._collectionTitle(engine), + engines: [engine.name], + enabled: true, + sizeLabel: retrievingLabel}; + this._collections.push(obj); + this._byname[engine.prefName] = obj; + } + }, + + _collectionTitle: function _collectionTitle(engine) { + try { + return gSyncQuota.bundle.getString( + "collection." + engine.prefName + ".label"); + } catch (ex) { + return engine.Name; + } + }, + + /* + * Process the quota information as returned by info/collection_usage. + */ + displayUsageData: function displayUsageData(data) { + for each (let coll in this._collections) { + coll.size = 0; + // If we couldn't retrieve any data, just blank out the label. + if (!data) { + coll.sizeLabel = ""; + continue; + } + + for each (let engineName in coll.engines) + coll.size += data[engineName] || 0; + let sizeLabel = ""; + sizeLabel = gSyncQuota.bundle.getFormattedString( + "quota.sizeValueUnit.label", gSyncQuota.convertKB(coll.size)); + coll.sizeLabel = sizeLabel; + } + let sizeColumn = this.treeBox.columns.getNamedColumn("size"); + this.treeBox.invalidateColumn(sizeColumn); + }, + + /* + * Handle click events on the tree. + */ + onTreeClick: function onTreeClick(event) { + if (event.button == 2) + return; + + let cell = this.treeBox.getCellAt(event.clientX, event.clientY); + if (cell.col && cell.col.id == "enabled") + this.toggle(cell.row); + }, + + /* + * Toggle enabled state of an engine. + */ + toggle: function toggle(row) { + // Update the tree + let collection = this._collections[row]; + collection.enabled = !collection.enabled; + this.treeBox.invalidateRow(row); + + // Display which ones will be removed + let freeup = 0; + let toremove = []; + for each (collection in this._collections) { + if (collection.enabled) + continue; + toremove.push(collection.name); + freeup += collection.size; + } + + let caption = document.getElementById("treeCaption"); + if (!toremove.length) { + caption.className = ""; + caption.firstChild.nodeValue = gSyncQuota.bundle.getString( + "quota.treeCaption.label"); + return; + } + + toremove = [this._byname[coll].title for each (coll in toremove)]; + toremove = toremove.join(gSyncQuota.bundle.getString("quota.list.separator")); + caption.firstChild.nodeValue = gSyncQuota.bundle.getFormattedString( + "quota.removal.label", [toremove]); + if (freeup) + caption.firstChild.nodeValue += gSyncQuota.bundle.getFormattedString( + "quota.freeup.label", gSyncQuota.convertKB(freeup)); + caption.className = "captionWarning"; + }, + + /* + * Return a list of engines (or rather their pref names) that should be + * disabled. + */ + getEnginesToDisable: function getEnginesToDisable() { + return [coll.name for each (coll in this._collections) if (!coll.enabled)]; + }, + + // nsITreeView + + get rowCount() { + return this._collections.length; + }, + + getRowProperties: function(index) { return ""; }, + getCellProperties: function(row, col) { return ""; }, + getColumnProperties: function(col) { return ""; }, + isContainer: function(index) { return false; }, + isContainerOpen: function(index) { return false; }, + isContainerEmpty: function(index) { return false; }, + isSeparator: function(index) { return false; }, + isSorted: function() { return false; }, + canDrop: function(index, orientation, dataTransfer) { return false; }, + drop: function(row, orientation, dataTransfer) {}, + getParentIndex: function(rowIndex) {}, + hasNextSibling: function(rowIndex, afterIndex) { return false; }, + getLevel: function(index) { return 0; }, + getImageSrc: function(row, col) {}, + + getCellValue: function(row, col) { + return this._collections[row].enabled; + }, + + getCellText: function getCellText(row, col) { + let collection = this._collections[row]; + switch (col.id) { + case "collection": + return collection.title; + case "size": + return collection.sizeLabel; + default: + return ""; + } + }, + + setTree: function setTree(tree) { + this.treeBox = tree; + }, + + toggleOpenState: function(index) {}, + cycleHeader: function(col) {}, + selectionChanged: function() {}, + cycleCell: function(row, col) {}, + isEditable: function(row, col) { return false; }, + isSelectable: function (row, col) { return false; }, + setCellValue: function(row, col, value) {}, + setCellText: function(row, col, value) {}, + performAction: function(action) {}, + performActionOnRow: function(action, row) {}, + performActionOnCell: function(action, row, col) {} + +}; diff --git a/application/palemoon/base/content/sync/quota.xul b/application/palemoon/base/content/sync/quota.xul new file mode 100644 index 0000000000..99e6ed78b7 --- /dev/null +++ b/application/palemoon/base/content/sync/quota.xul @@ -0,0 +1,65 @@ +<?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/syncQuota.css"?> + +<!DOCTYPE dialog [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncQuotaDTD SYSTEM "chrome://browser/locale/syncQuota.dtd"> +%brandDTD; +%syncBrandDTD; +%syncQuotaDTD; +]> +<dialog id="quotaDialog" + windowtype="Sync:ViewQuota" + persist="screenX screenY width height" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gSyncQuota.init()" + buttons="accept,cancel" + title=""a.dialogTitle.label;" + ondialogcancel="return gSyncQuota.onCancel();" + ondialogaccept="return gSyncQuota.onAccept();"> + + <script type="application/javascript" + src="chrome://browser/content/sync/quota.js"/> + + <stringbundleset id="stringbundleset"> + <stringbundle id="quotaStrings" + src="chrome://browser/locale/syncQuota.properties"/> + </stringbundleset> + + <vbox flex="1"> + <label id="usageLabel" + value=""a.retrievingInfo.label;"/> + <separator/> + <tree id="usageTree" + seltype="single" + hidecolumnpicker="true" + onclick="gUsageTreeView.onTreeClick(event);" + flex="1"> + <treecols> + <treecol id="enabled" + type="checkbox" + fixed="true"/> + <splitter class="tree-splitter"/> + <treecol id="collection" + label=""a.typeColumn.label;" + flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="size" + label=""a.sizeColumn.label;" + flex="1"/> + </treecols> + <treechildren flex="1"/> + </tree> + <separator/> + <description id="treeCaption"> </description> + </vbox> + +</dialog> diff --git a/application/palemoon/base/content/sync/setup.js b/application/palemoon/base/content/sync/setup.js new file mode 100644 index 0000000000..99faa038e4 --- /dev/null +++ b/application/palemoon/base/content/sync/setup.js @@ -0,0 +1,1071 @@ +/* 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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const 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() [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", function() 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 false; + this._settingUpNew = true; + this.wizard.pageIndex = NEW_ACCOUNT_START_PAGE; + }, + + useExistingAccount: function () { + if (!Weave.Utils.ensureMPUnlocked()) + return false; + 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: + case EXISTING_ACCOUNT_LOGIN_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])); + } + + // XXX: Addons syncing is currently not operational; + // Make doubly-sure to always disable addons syncing pref + Weave.Svc.Prefs.set("engine.addons", false); + + 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"); + + gSyncUtils.openFirstSyncProgressPage(); + } + 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"); + let str = ""; + 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 = 0; + for each (let i in ids) { + if (i) { + blessedcount++; + } + } + // 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 each (let name in 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/palemoon/base/content/sync/setup.xul b/application/palemoon/base/content/sync/setup.xul new file mode 100644 index 0000000000..cf2cc77e40 --- /dev/null +++ b/application/palemoon/base/content/sync/setup.xul @@ -0,0 +1,491 @@ +<?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="http://www.palemoon.org/sync/help/easy-setup.shtml"/> + </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 inline-link" + onclick="event.stopPropagation();gSyncUtils.openToS();"> + &setup.tosLink.label; + </label> + &setup.tosAgree2.label; + <label class="text-link inline-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="http://www.palemoon.org/sync/help/easy-setup.shtml"/> + </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="http://www.palemoon.org/sync/help/recoverykey.shtml"> + &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="-moz-margin-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="false" + hidden="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/palemoon/base/content/sync/utils.js b/application/palemoon/base/content/sync/utils.js new file mode 100644 index 0000000000..0c02b5bc02 --- /dev/null +++ b/application/palemoon/base/content/sync/utils.js @@ -0,0 +1,218 @@ +/* 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. +let gSyncUtils = { + get bundle() { + delete this.bundle; + return this.bundle = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties"); + }, + + // 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); + }, + + openToS: function () { + this._openLink(Weave.Svc.Prefs.get("termsURL")); + }, + + openPrivacyPolicy: function () { + this._openLink(Weave.Svc.Prefs.get("privacyURL")); + }, + + openFirstSyncProgressPage: function () { + this._openLink("about:sync-progress"); + }, + + /** + * 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/palemoon/base/content/tabbrowser.css b/application/palemoon/base/content/tabbrowser.css new file mode 100644 index 0000000000..94d6dbb2ef --- /dev/null +++ b/application/palemoon/base/content/tabbrowser.css @@ -0,0 +1,68 @@ +/* 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"); + /* Make the content area follow the system colors before load */ + background: Menu; + color: MenuText; +} + +.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"); + display: none; +} + +.tabbrowser-tabs[closebuttons="activetab"] > * > * > * > .tab-close-button:not([pinned])[selected="true"], +.tabbrowser-tabs[closebuttons="alltabs"] > * > * > * > .tab-close-button:not([pinned]) { + 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; +} + +.tab-throbber:not([busy]), +.tab-throbber[busy] + .tab-icon-image { + display: none; +} + +.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; +} diff --git a/application/palemoon/base/content/tabbrowser.xml b/application/palemoon/base/content/tabbrowser.xml new file mode 100644 index 0000000000..b8d5f3e41c --- /dev/null +++ b/application/palemoon/base/content/tabbrowser.xml @@ -0,0 +1,4960 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE bindings [ +<!ENTITY % tabBrowserDTD SYSTEM "chrome://browser/locale/tabbrowser.dtd" > +%tabBrowserDTD; +]> + +<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"> + <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" disablehistory="true" + xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup"/> + </xul:stack> + </xul:vbox> + </xul:hbox> + </xul:notificationbox> + </xul:tabpanels> + </xul:tabbox> + <children/> + </content> + <implementation implements="nsIDOMEventListener, nsIMessageListener"> + + <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, + function (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="mFaviconService" readonly="true"> + Components.classes["@mozilla.org/browser/favicon-service;1"] + .getService(Components.interfaces.nsIFaviconService); + </field> + <field name="_placesAutocomplete" readonly="true"> + Components.classes["@mozilla.org/autocomplete/search;1?name=history"] + .getService(Components.interfaces.mozIPlacesAutoComplete); + </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="mTabsProgressListeners"> + [] + </field> + <field name="mTabListeners"> + [] + </field> + <field name="mTabFilters"> + [] + </field> + <field name="mIsBusy"> + false + </field> + <field name="_outerWindowIDBrowserMap"> + new Map(); + </field> + <field name="arrowKeysShouldWrap" readonly="true"> +#ifdef XP_MACOSX + true +#else + false +#endif + </field> + + <field name="_autoScrollPopup"> + null + </field> + + <field name="_previewMode"> + false + </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="formValidationAnchor" readonly="true"> + <getter><![CDATA[ + if (this.mCurrentTab._formValidationAnchor) { + return this.mCurrentTab._formValidationAnchor; + } + let stack = this.mCurrentBrowser.parentNode; + // Create an anchor for the form validation popup + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let formValidationAnchor = document.createElementNS(NS_XUL, "hbox"); + formValidationAnchor.className = "form-validation-anchor"; + formValidationAnchor.hidden = true; + stack.appendChild(formValidationAnchor); + return this.mCurrentTab._formValidationAnchor = formValidationAnchor; + ]]></getter> + </property> + + <method name="updateWindowResizers"> + <body><![CDATA[ + if (!window.gShowPageResizers) + return; + + var show = document.getElementById("addon-bar").collapsed && + window.windowState == window.STATE_NORMAL; + for (let i = 0; i < this.browsers.length; i++) { + this.browsers[i].showWindowResizer = show; + } + ]]></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).docShell.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.setAttribute("fadein", "true"); + aTab.removeAttribute("pinned"); + aTab.style.MozMarginStart = ""; + this.tabContainer._unlockTabSizing(); + this.tabContainer._positionPinnedTabs(); + this.tabContainer.adjustTabstrip(); + + this.getBrowserForTab(aTab).docShell.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="getBrowserForOuterWindowID"> + <parameter name="aID"/> + <body> + <![CDATA[ + return this._outerWindowIDBrowserMap.get(aID); + ]]> + </body> + </method> + + <method name="_getTabForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + for (let i = 0; i < this.browsers.length; i++) { + if (this.browsers[i].contentWindow == 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[ + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let browser = (aBrowser || this.mCurrentBrowser); + let stack = browser.parentNode; + let self = this; + + let promptBox = { + appendPrompt : function(args, onCloseCallback) { + let newPrompt = document.createElementNS(XUL_NS, "tabmodalprompt"); + stack.appendChild(newPrompt); + browser.setAttribute("tabmodalPromptShowing", true); + + newPrompt.clientTop; // style flush to assure binding is attached + + let tab = self._getTabForContentWindow(browser.contentWindow); + newPrompt.init(args, tab, onCloseCallback); + return newPrompt; + }, + + removePrompt : function(aPrompt) { + stack.removeChild(aPrompt); + + let prompts = this.listPrompts(); + if (prompts.length) { + let prompt = prompts[prompts.length - 1]; + prompt.Dialog.setDefaultFocus(); + } else { + browser.removeAttribute("tabmodalPromptShowing"); + browser.focus(); + } + }, + + listPrompts : function(aPrompt) { + let els = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"); + // NodeList --> real JS array + let prompts = Array.slice(els); + return prompts; + }, + }; + + return promptBox; + ]]> + </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; + + if (!aBrowser) + aBrowser = this.mCurrentBrowser; + + if (aCallGlobalListeners != false && + aBrowser == this.mCurrentBrowser) { + this.mProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + if (!p[aMethod].apply(p, aArguments)) + rv = false; + } catch (e) { + // don't inhibit other listeners + Components.utils.reportError(e); + } + } + }); + } + + if (aCallTabsListeners != false) { + aArguments.unshift(aBrowser); + + this.mTabsProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + if (!p[aMethod].apply(p, aArguments)) + rv = false; + } catch (e) { + // don't inhibit other listeners + Components.utils.reportError(e); + } + } + }); + } + + 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"/> + <body> + <![CDATA[ + return ({ + mTabBrowser: this, + mTab: aTab, + mBrowser: aBrowser, + mBlank: aStartsBlank, + + // cache flags for correct status UI update after tab switching + mStateFlags: 0, + 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; + + if (gMultiProcessBrowser) + return true; + + // Don't show progress indicators in tabs for about: URIs + // pointing to local resources. + try { + let channel = aRequest.QueryInterface(Ci.nsIChannel); + if (channel.originalURI.schemeIs("about") && + (channel.URI.schemeIs("jar") || channel.URI.schemeIs("file"))) + return false; + } catch (e) {} + + return true; + }, + + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0; + + if (!this._shouldShowProgress(aRequest)) + return; + + if (this.mTotalProgress) + this.mTab.setAttribute("progress", "true"); + + this._callProgressListeners("onProgressChange", + [aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress]); + }, + + onProgressChange64: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + return this.onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress); + }, + + onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { + if (!aRequest) + return; + + var oldBlank = this.mBlank; + + const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener; + const nsIChannel = Components.interfaces.nsIChannel; + + 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) { + // It's okay to clear what the user typed when we start + // loading a document. If the user types, this counter gets + // set to zero, if the document load ends without an + // onLocationChange, this counter gets decremented + // (so we keep it while switching tabs after failed loads) + // We need to add 2 because loadURIWithFlags may have + // cancelled a pending load which would have cleared + // its anchor scroll detection temporary increment. + if (aWebProgress.isTopLevel) + this.mBrowser.userTypedClear += 2; + + if (this._shouldShowProgress(aRequest)) { + if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { + this.mTab.setAttribute("busy", "true"); + if (!gMultiProcessBrowser) { + if (aWebProgress.isTopLevel && + !(this.mBrowser.docShell.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); + if (!this.mTab.selected) + this.mTab.setAttribute("unread", "true"); + } + this.mTab.removeAttribute("progress"); + + if (aWebProgress.isTopLevel) { + if (!Components.isSuccessCode(aStatus) && + !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; + + if (this.mTab.selected && gURLBar) + URLBarSetURI(); + } else { + // The document is done loading, we no longer want the + // value cleared. + + if (this.mBrowser.userTypedClear > 1) + this.mBrowser.userTypedClear -= 2; + else if (this.mBrowser.userTypedClear > 0) + this.mBrowser.userTypedClear--; + } + + if (!this.mBrowser.mIconURL) + this.mTabBrowser.useDefaultIcon(this.mTab); + } + + if (this.mBlank) + this.mBlank = false; + + var location = aRequest.QueryInterface(nsIChannel).URI; + + // 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 (oldBlank) { + this._callProgressListeners("onUpdateCurrentBrowser", + [aStateFlags, aStatus, "", 0], + true, false); + } else { + this._callProgressListeners("onStateChange", + [aWebProgress, aRequest, aStateFlags, aStatus], + true, false); + } + + this._callProgressListeners("onStateChange", + [aWebProgress, aRequest, aStateFlags, aStatus], + false); + + if (aStateFlags & (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) { + // If userTypedClear > 0, the document loaded correctly and we should be + // clearing the user typed value. We also 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). + if (this.mBrowser.userTypedClear > 0 || + ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) && + aLocation.spec != "about:blank")) + this.mBrowser.userTypedValue = null; + + // Don't clear the favicon if this onLocationChange was + // triggered by a pushState or a replaceState. See bug 550565. + if (!gMultiProcessBrowser) { + if (aWebProgress.isLoadingDocument && + !(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) + this.mBrowser.mIconURL = null; + } + + let autocomplete = this.mTabBrowser._placesAutocomplete; + if (this.mBrowser.registeredOpenURI) { + autocomplete.unregisterOpenPage(this.mBrowser.registeredOpenURI); + 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)) { + autocomplete.registerOpenPage(aLocation); + this.mBrowser.registeredOpenURI = aLocation; + } + } + + if (!this.mBlank) { + this._callProgressListeners("onLocationChange", + [aWebProgress, aRequest, aLocation, + aFlags]); + } + + if (topLevel) + this.mBrowser.lastURI = aLocation; + }, + + 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> + + <method name="setIcon"> + <parameter name="aTab"/> + <parameter name="aURI"/> + <body> + <![CDATA[ + var browser = this.getBrowserForTab(aTab); + browser.mIconURL = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + + if (aURI && this.mFaviconService) { + if (!(aURI instanceof Ci.nsIURI)) + aURI = makeURI(aURI); + this.mFaviconService.setAndFetchFaviconForPage(browser.currentURI, + aURI, false, + PrivateBrowsingUtils.isWindowPrivate(window) ? + this.mFaviconService.FAVICON_LOAD_PRIVATE : + this.mFaviconService.FAVICON_LOAD_NON_PRIVATE); + } + + let sizedIconUrl = browser.mIconURL || ""; + if (sizedIconUrl) { + let size = Math.round(16 * window.devicePixelRatio); + sizedIconUrl += (sizedIconUrl.includes("#") ? "&" : "#") + + "-moz-resolution=" + size + "," + size; + } + if (sizedIconUrl != aTab.getAttribute("image")) { + if (browser.mIconURL) //PMed + aTab.setAttribute("image", sizedIconUrl); + else + aTab.removeAttribute("image"); + this._tabAttrModified(aTab); + } + + 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[ + // Bug 691610 - e10s support for useDefaultIcon + if (gMultiProcessBrowser) + return; + + var browser = this.getBrowserForTab(aTab); + var docURIObject = browser.contentDocument.documentURIObject; + var icon = null; + <!-- Pale Moon: new image icon method, see bug #305986 --> + let req = browser.contentDocument.imageRequest; + let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size"); + if (browser.contentDocument instanceof ImageDocument && + req && req.image) { + if (Services.prefs.getBoolPref("browser.chrome.site_icons") && sz) { + try { + <!-- Main method: draw on a hidden canvas --> + var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + var tabImg = document.getAnonymousElementByAttribute(aTab, "anonid", "tab-icon"); + var w = tabImg.boxObject.width; + var h = tabImg.boxObject.height; + canvas.width = w; + canvas.height = h; + var ctx = canvas.getContext('2d'); + ctx.drawImage(browser.contentDocument.body.firstChild, 0, 0, w, h); + icon = canvas.toDataURL(); + } + catch (e) { + <!-- Fallback method in case canvas method fails, restricted by sz --> + try { + + if (req && + req.image && + req.image.width <= sz && + req.image.height <= sz) + icon = browser.currentURI; + } + catch (e) { + <!-- Both methods fail (very large or corrupt image): icon remains null --> + } + } + } + } + // Use documentURIObject in the check for shouldLoadFavIcon so that we + // do the right thing with about:-style error pages. Bug 453442 + else if (this.shouldLoadFavIcon(docURIObject)) { + let url = docURIObject.prePath + "/favicon.ico"; + if (!this.isFailedIcon(url)) + icon = url; + } + this.setIcon(aTab, icon); + ]]> + </body> + </method> + + <method name="isFailedIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + if (this.mFaviconService) { + if (!(aURI instanceof Ci.nsIURI)) + aURI = makeURI(aURI); + return this.mFaviconService.isFailedFavicon(aURI); + } + return null; + ]]> + </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="freezeTitlebar"> + <parameter name="aTitle"/> + <body> + <![CDATA[ + this._frozenTitle = aTitle || ""; + this.updateTitlebar(); + ]]> + </body> + </method> + + <method name="unfreezeTitlebar"> + <body> + <![CDATA[ + this._frozenTitle = ""; + this.updateTitlebar(); + ]]> + </body> + </method> + + <method name="updateTitlebar"> + <body> + <![CDATA[ + this.ownerDocument.title = this._frozenTitle || + this.getWindowTitleForBrowser(this.mCurrentBrowser); + ]]> + </body> + </method> + + <method name="updateCurrentBrowser"> + <parameter name="aForceUpdate"/> + <body> + <![CDATA[ + var newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex); + if (this.mCurrentBrowser == newBrowser && !aForceUpdate) + return; + + if (!aForceUpdate) { + window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils) + .beginTabSwitch(); + } + + 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 (oldBrowser) { + oldBrowser.setAttribute("type", "content-targetable"); + oldBrowser.docShellIsActive = false; + this.finder.mListeners.forEach(l => oldBrowser.finder.removeResultListener(l)); + } + + var updateBlockedPopups = false; + if (!oldBrowser || + (oldBrowser.blockedPopups && !newBrowser.blockedPopups) || + (!oldBrowser.blockedPopups && newBrowser.blockedPopups)) + updateBlockedPopups = true; + + newBrowser.setAttribute("type", "content-primary"); + newBrowser.docShellIsActive = + (window.windowState != window.STATE_MINIMIZED); + this.mCurrentBrowser = newBrowser; + this.mCurrentTab = this.tabContainer.selectedItem; + this.finder.mListeners.forEach(l => this.mCurrentBrowser.finder.addResultListener(l)); + this.showTab(this.mCurrentTab); + + var backForwardContainer = document.getElementById("unified-back-forward-button"); + if (backForwardContainer) { + backForwardContainer.setAttribute("switchingtabs", "true"); + window.addEventListener("MozAfterPaint", function removeSwitchingtabsAttr() { + window.removeEventListener("MozAfterPaint", removeSwitchingtabsAttr); + backForwardContainer.removeAttribute("switchingtabs"); + }); + } + + if (updateBlockedPopups) + this.mCurrentBrowser.updateBlockedPopups(); + + // Update the URL bar. + var loc = this.mCurrentBrowser.currentURI; + + // Bug 666809 - SecurityUI support for e10s + var webProgress = this.mCurrentBrowser.webProgress; + var securityUI = this.mCurrentBrowser.securityUI; + + this._callProgressListeners(null, "onLocationChange", + [webProgress, null, loc, 0], true, + false); + + if (securityUI) { + this._callProgressListeners(null, "onSecurityChange", + [webProgress, null, securityUI.state], true, false); + } + + var listener = this.mTabListeners[this.tabContainer.selectedIndex] || null; + if (listener && listener.mStateFlags) { + this._callProgressListeners(null, "onUpdateCurrentBrowser", + [listener.mStateFlags, listener.mStatus, + listener.mMessage, listener.mTotalProgress], + true, false); + } + + if (!this._previewMode) { + this.mCurrentTab.removeAttribute("unread"); + this.selectedTab.lastAccessed = Date.now(); + + // Bug 666816 - TypeAheadFind support for e10s + if (!gMultiProcessBrowser) + this._fastFind.setDocShell(this.mCurrentBrowser.docShell); + + this.updateTitlebar(); + + this.mCurrentTab.removeAttribute("titlechanged"); + } + + // 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); + this._tabAttrModified(this.mCurrentTab); + + // Adjust focus + oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused); + do { + // When 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(); + break; + } + + // 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(); + break; + } + + // 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(); + break; + } else if (isTabEmpty(this.mCurrentTab)) { + focusAndSelectUrlBar(); + break; + } + } + + // If the find bar is focused, keep it focused. + if (gFindBarInitialized && + !gFindBar.hidden && + gFindBar.getElement("findbar-textbox").getAttribute("focused") == "true") + break; + + // Otherwise, focus the content area. + let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + 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); + } while (false); + } + + this.tabContainer._setPositionalAttributes(); + ]]> + </body> + </method> + + <method name="_tabAttrModified"> + <parameter name="aTab"/> + <body><![CDATA[ + if (aTab.closing) + return; + + // This event should be dispatched when any of these attributes change: + // label, crop, busy, image, selected + var event = document.createEvent("Events"); + event.initEvent("TabAttrModified", true, false); + aTab.dispatchEvent(event); + ]]></body> + </method> + + <method name="setTabTitleLoading"> + <parameter name="aTab"/> + <body> + <![CDATA[ + aTab.label = this.mStringBundle.getString("tabs.connecting"); + aTab.crop = "end"; + this._tabAttrModified(aTab); + ]]> + </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 // 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); + + 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 aFromExternal; + var aRelatedToCurrent; + if (arguments.length == 2 && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aReferrerURI = params.referrerURI; + aCharset = params.charset; + aPostData = params.postData; + aLoadInBackground = params.inBackground; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + } + + var bgLoad = (aLoadInBackground != null) ? aLoadInBackground : + Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + var owner = bgLoad ? null : this.selectedTab; + var tab = this.addTab(aURI, { + referrerURI: aReferrerURI, + charset: aCharset, + postData: aPostData, + ownerTab: owner, + allowThirdPartyFixup: aAllowThirdPartyFixup, + fromExternal: aFromExternal, + relatedToCurrent: aRelatedToCurrent}); + if (!bgLoad) + this.selectedTab = tab; + + return tab; + ]]> + </body> + </method> + + <method name="loadTabs"> + <parameter name="aURIs"/> + <parameter name="aLoadInBackground"/> + <parameter name="aReplace"/> + <body><![CDATA[ + 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; + + if (aReplace) { + try { + this.loadURI(aURIs[0], null, null); + } 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}); + + var tabNum = this.tabContainer.selectedIndex; + for (let i = 1; i < aURIs.length; ++i) { + let tab = this.addTab(aURIs[i], {skipAnimation: true}); + if (aReplace) + this.moveTabTo(tab, ++tabNum); + } + + if (!aLoadInBackground) { + if (firstTabAdded) { + // .selectedTab setter focuses the content area + this.selectedTab = firstTabAdded; + } + else + this.selectedBrowser.focus(); + } + ]]></body> + </method> + + <method name="addTab"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <parameter name="aOwner"/> + <parameter name="aAllowThirdPartyFixup"/> + <body> + <![CDATA[ + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var aFromExternal; + var aRelatedToCurrent; + var aSkipAnimation; + if (arguments.length == 2 && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aReferrerURI = params.referrerURI; + aCharset = params.charset; + aPostData = params.postData; + aOwner = params.ownerTab; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + aSkipAnimation = params.skipAnimation; + } + + // 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"; + + if (!aURI || isBlankPageURL(aURI)) + t.setAttribute("label", this.mStringBundle.getString("tabs.emptyTabTitle")); + else + t.setAttribute("label", aURI); + + t.setAttribute("crop", "end"); + t.setAttribute("validate", "never"); //PMed + 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 caches + this._browsers = null; + this._visibleTabs = null; + + this.tabContainer.appendChild(t); + + // If this new tab is owned by another, assert that relationship + if (aOwner) + t.owner = aOwner; + + var b = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "browser"); + b.setAttribute("type", "content-targetable"); + b.setAttribute("message", "true"); + b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu")); + b.setAttribute("tooltip", this.getAttribute("contenttooltip")); + + if (Services.prefs.getPrefType("browser.tabs.remote") == Services.prefs.PREF_BOOL && + Services.prefs.getBoolPref("browser.tabs.remote")) { + b.setAttribute("remote", "true"); + } + + if (window.gShowPageResizers && document.getElementById("addon-bar").collapsed && + window.windowState == window.STATE_NORMAL) { + b.setAttribute("showresizer", "true"); + } + + if (this.hasAttribute("autocompletepopup")) + b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup")); + b.setAttribute("autoscrollpopup", this._autoScrollPopup.id); + + // 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.appendChild(browserSidebarContainer); + + var position = this.tabs.length - 1; + var uniqueId = "panel" + Date.now() + position; + notificationbox.id = uniqueId; + t.linkedPanel = uniqueId; + t.linkedBrowser = b; + this._tabForBrowser.set(b, t); + t._tPos = position; + this.tabContainer._setPositionalAttributes(); + + // Prevent the superfluous initial load of a blank document + // if we're going to load something other than about:blank. + if (!uriIsAboutBlank) { + b.setAttribute("nodefaultsrc", "true"); + } + + // NB: this appendChild call causes us to run constructors for the + // browser element, which fires off a bunch of notifications. Some + // of those notifications can cause code to run that inspects our + // state, so it is important that the tab element is fully + // initialized by this point. + this.mPanelContainer.appendChild(notificationbox); + + this.tabContainer.updateVisibility(); + + // wire up a progress listener for the new browser object. + var tabListener = this.mTabProgressListener(t, b, uriIsAboutBlank); + const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Components.interfaces.nsIWebProgress); + filter.addProgressListener(tabListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL); + b.webProgress.addProgressListener(filter, Components.interfaces.nsIWebProgress.NOTIFY_ALL); + this.mTabListeners[position] = tabListener; + this.mTabFilters[position] = filter; + + b._fastFind = this.fastFind; + b.droppedLinkHandler = handleDroppedLink; + + // If we just created a new tab that loads the default + // newtab url, swap in a preloaded page if possible. + // Do nothing if we're a private window. + let docShellsSwapped = false; + if (aURI == BROWSER_NEW_TAB_URL && + !PrivateBrowsingUtils.isWindowPrivate(window)) { + docShellsSwapped = gBrowserNewTabPreloader.newTab(t); + } + + // 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 evt = document.createEvent("Events"); + evt.initEvent("TabOpen", true, false); + t.dispatchEvent(evt); + + // If we didn't swap docShells with a preloaded browser + // then let's just continue loading the page normally. + if (!docShellsSwapped && !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; + try { + b.loadURIWithFlags(aURI, flags, aReferrerURI, aCharset, aPostData); + } catch (ex) { + Cu.reportError(ex); + } + } + + // We start our browsers out as inactive, and then maintain + // activeness in the tab switcher. + b.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(). + // + // Note: Only do this of we still have a docShell. The TabOpen + // event was dispatched above and a gBrowser.removeTab() call from + // one of its listeners could cause us to fail here. + if (Services.prefs.getPrefType("browser.tabs.remote") == Services.prefs.PREF_BOOL && + !Services.prefs.getBoolPref("browser.tabs.remote") + && b.docShell) { + this._outerWindowIDBrowserMap.set(b.outerWindowID, b); + } + + // 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) { + mozRequestAnimationFrame(function () { + this.tabContainer._handleTabTelemetryStart(t, aURI); + + // kick the animation off + t.setAttribute("fadein", "true"); + + // This call to adjustTabstrip is redundant but needed so that + // when opening a second tab, the first tab's close buttons + // appears immediately rather than when the transition ends. + if (this.tabs.length - this._removingTabs.length == 2) + this.tabContainer.adjustTabstrip(); + }.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 buttonPressed = + ps.confirmEx(window, + bundle.getString("tabs.closeWarningTitle"), + bundle.getFormattedString("tabs.closeWarningMultipleTabs", + [tabsToClose]), + (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 prefs unless they press OK and have unchecked the box + if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value) { + Services.prefs.setBoolPref("browser.tabs.warnOnClose", false); + Services.prefs.setBoolPref("browser.tabs.warnOnCloseOtherTabs", 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"/> + <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], {animate: true}); + } + } + ]]> + </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]); + } + } + ]]> + </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; + } + + // 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, false, null, true)) + 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"); + + if (this.tabs.length - this._removingTabs.length == 1) { + // The second tab just got closed and we will end up with a single + // one. Remove the first tab's close button immediately (if needed) + // rather than after the tabclose animation ends. + this.tabContainer.adjustTabstrip(); + } + + 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="aTabWillBeMoved"/> + <parameter name="aCloseWindowWithLastTab"/> + <parameter name="aCloseWindowFastpath"/> + <body> + <![CDATA[ + if (aTab.closing || + aTab._pendingPermitUnload || + this._windowIsClosing) + return false; + + var browser = this.getBrowserForTab(aTab); + + if (!aTabWillBeMoved) { + let ds = browser.docShell; + if (ds && ds.contentViewer) { + // We need to block while calling permitUnload() because it + // processes the event queue and may lead to another removeTab() + // call before permitUnload() even returned. + aTab._pendingPermitUnload = true; + let permitUnload = ds.contentViewer.permitUnload(); + delete aTab._pendingPermitUnload; + + if (!permitUnload) + return false; + } + } + + var closeWindow = false; + var newTab = false; + if (this.tabs.length - this._removingTabs.length == 1) { + closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab : + !window.toolbar.visible || + this.tabContainer._closeWindowWithLastTab; + + // 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._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow))) + return null; + + newTab = true; + } + + aTab.closing = true; + this._removingTabs.push(aTab); + this._visibleTabs = null; // invalidate cache + 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 = document.createEvent("UIEvent"); + evt.initUIEvent("TabClose", true, false, window, aTabWillBeMoved ? 1 : 0); + aTab.dispatchEvent(evt); + + if (!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.mTabFilters[aTab._tPos]; + + browser.webProgress.removeProgressListener(filter); + + filter.removeProgressListener(this.mTabListeners[aTab._tPos]); + this.mTabListeners[aTab._tPos].destroy(); + + if (browser.registeredOpenURI && !aTabWillBeMoved) { + this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI); + 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. + Array.forEach(this.tabs, function (tab) { + 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. + // Clean up mTabFilters and mTabListeners now rather than in + // _beginRemoveTab, so that their size is always in sync with the + // number of tabs and browsers (the xbl destructor depends on this). + this.mTabFilters.splice(aTab._tPos, 1); + this.mTabListeners.splice(aTab._tPos, 1); + + 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(); + + if (browser == this.mCurrentBrowser) + this.mCurrentBrowser = null; + + var wasPinned = aTab.pinned; + + // Invalidate browsers cache, as the tab is removed from the + // tab container. + this._browsers = null; + + // 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); + } + + // Pale Moon: if resizing immediately, select the tab immediately to the left + // instead of the right (if not leftmost) to prevent focus swap and + // "selected tab not under cursor" + // FIXME: Tabs must be sliding in from the left for this, or it'd unfocus + // in the other direction! Disabled for now. Is there an easier way? :hover? + // Is this even needed when resizing immediately?... + + //if (Services.prefs.getBoolPref("browser.tabs.resize_immediately")) { + // if (this.selectedTab._tPos > 1) { + // let newPos = this.selectedTab._tPos - 1; + // this.selectedTab = this.tabs[newPos]; + // } + //} + + // 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); + + // 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) { + let selectedPanel = this.mTabBox.selectedPanel; + + this.mPanelContainer.removeChild(panel); + + // Under the hood, a selectedIndex attribute controls which panel + // is displayed. Removing a panel A which precedes the selected + // panel B makes selectedIndex point to the panel next to B. We + // need to explicitly preserve B as the selected panel. + this.mTabBox.selectedPanel = selectedPanel; + } + + 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="swapNewTabWithBrowser"> + <parameter name="aNewTab"/> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + // The browser must be standalone. + if (aBrowser.getTabBrowser()) + throw Cr.NS_ERROR_INVALID_ARG; + + // The tab is definitely not loading. + aNewTab.removeAttribute("busy"); + if (aNewTab.selected) { + this.mIsBusy = false; + } + + this._swapBrowserDocShells(aNewTab, aBrowser); + + // Update the new tab's title. + this.setTabTitle(aNewTab); + + if (aNewTab.selected) { + this.updateCurrentBrowser(true); + } + ]]> + </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; + + // That's gBrowser for the other window, not the tab's browser! + var remoteBrowser = aOtherTab.ownerDocument.defaultView.gBrowser; + var isPending = aOtherTab.hasAttribute("pending"); + + // 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, true, true)) + return; + + let ourBrowser = this.getBrowserForTab(aOurTab); + let otherBrowser = aOtherTab.linkedBrowser; + + // 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) { + let ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore) + ss.setTabState(aOurTab, ss.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); + var isBusy = aOtherTab.hasAttribute("busy"); + if (isBusy) { + aOurTab.setAttribute("busy", "true"); + this._tabAttrModified(aOurTab); + if (aOurTab.selected) + this.mIsBusy = true; + } + + this._swapBrowserDocShells(aOurTab, otherBrowser); + } + + // 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); + ]]> + </body> + </method> + + <method name="_swapBrowserDocShells"> + <parameter name="aOurTab"/> + <parameter name="aOtherBrowser"/> + <body> + <![CDATA[ + // Unhook our progress listener + let index = aOurTab._tPos; + const filter = this.mTabFilters[index]; + let tabListener = this.mTabListeners[index]; + 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); + } + // Swap the docshells + ourBrowser.swapDocShells(aOtherBrowser); + + // Register new outerWindowIDs. + this._outerWindowIDBrowserMap.set(ourBrowser.outerWindowID, ourBrowser); + if (remoteBrowser) { + remoteBrowser._outerWindowIDBrowserMap.set(aOtherBrowser.outerWindowID, aOtherBrowser); + } + // Restore the progress listener + this.mTabListeners[index] = tabListener = + this.mTabProgressListener(aOurTab, ourBrowser, false); + + 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._placesAutocomplete.unregisterOpenPage(aOurBrowser.registeredOpenURI); + 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."); + } + + this.mProgressListeners.push(aListener); + ]]> + </body> + </method> + + <method name="removeProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + this.mProgressListeners = + this.mProgressListeners.filter(function (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(function (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[ + Array.forEach(this.tabs, function(tab) { + if (aTabs.indexOf(tab) == -1) + this.hideTab(tab); + else + this.showTab(tab); + }, this); + + 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; + + if (aIndex >= 0 && aIndex < tabs.length) + 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"/> + + <property name="browsers" readonly="true"> + <getter> + <![CDATA[ + return this._browsers || + (this._browsers = Array.map(this.tabs, function (tab) tab.linkedBrowser)); + ]]> + </getter> + </property> + <field name="_browsers">null</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; + + this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(aTab._tPos, 1)[0]); + this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(aTab._tPos, 1)[0]); + + let wasFocused = (document.activeElement == this.mCurrentTab); + + aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1; + this.mCurrentTab._selected = false; + + // invalidate caches + this._browsers = null; + 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; + } + 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> + + <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 --> + <body> + <![CDATA[ + return Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore) + .duplicateTab(window, aTab); + ]]> + </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[ + 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> + + <method name="attachFormFill"> + <body><![CDATA[ + for (var i = 0; i < this.mPanelContainer.childNodes.length; ++i) { + var cb = this.getBrowserAtIndex(i); + cb.attachFormFill(); + } + ]]></body> + </method> + + <method name="detachFormFill"> + <body><![CDATA[ + for (var i = 0; i < this.mPanelContainer.childNodes.length; ++i) { + var cb = this.getBrowserAtIndex(i); + cb.detachFormFill(); + } + ]]></body> + </method> + + <property name="currentURI" + onget="return this.mCurrentBrowser.currentURI;" + readonly="true"/> + + <field name="_fastFind">null</field> + <property name="fastFind" + readonly="true"> + <getter> + <![CDATA[ + if (!this._fastFind) { + this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"] + .createInstance(Components.interfaces.nsITypeAheadFind); + this._fastFind.init(this.docShell); + } + return this._fastFind; + ]]> + </getter> + </property> + + <field name="_lastSearchString">null</field> + <field name="_lastSearchHighlight">false</field> + + <field name="finder"><![CDATA[ + ({ + mTabBrowser: this, + mListeners: new Set(), + get finder() { + return this.mTabBrowser.mCurrentBrowser.finder; + }, + addResultListener: function(aListener) { + this.mListeners.add(aListener); + this.finder.addResultListener(aListener); + }, + removeResultListener: function(aListener) { + this.mListeners.delete(aListener); + this.finder.removeResultListener(aListener); + }, + get searchString() { + return this.finder.searchString; + }, + get clipboardSearchString() { + return this.finder.clipboardSearchString; + }, + set clipboardSearchString(val) { + return this.finder.clipboardSearchString = val; + }, + set caseSensitive(val) { + return this.finder.caseSensitive = val; + }, + fastFind: function(aSearchString, aLinksOnly, aDrawOutline) { + this.finder.fastFind(aSearchString, aLinksOnly, aDrawOutline); + }, + findAgain: function(aFindBackwards, aLinksOnly, aDrawOutline) { + this.finder.findAgain(aFindBackwards, aLinksOnly, aDrawOutline); + }, + setSearchStringToSelection: function() { + return this.finder.setSearchStringToSelection(); + }, + highlight: function(aHighlight, aWord) { + this.finder.highlight(aHighlight, aWord); + }, + getInitialSelection: function() { + this.finder.getInitialSelection(); + }, + enableSelection: function() { + this.finder.enableSelection(); + }, + removeSelection: function() { + this.finder.removeSelection(); + }, + focusContent: function() { + this.finder.focusContent(); + }, + keyPress: function(aEvent) { + this.finder.keyPress(aEvent); + }, + requestMatchesCount: function(aWord, aMatchLimit, aLinksOnly) { + this.finder.requestMatchesCount(aWord, aMatchLimit, aLinksOnly); + } + }) + ]]></field> + + <property name="docShell" + onget="return this.mCurrentBrowser.docShell" + readonly="true"/> + + <property name="webNavigation" + onget="return this.mCurrentBrowser.webNavigation" + readonly="true"/> + + <property name="webBrowserFind" + readonly="true" + onget="return this.mCurrentBrowser.webBrowserFind"/> + + <property name="webProgress" + readonly="true" + onget="return this.mCurrentBrowser.webProgress"/> + + <property name="contentWindow" + readonly="true" + onget="return this.mCurrentBrowser.contentWindow"/> + + <property name="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="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="_handleKeyEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!aEvent.isTrusted) { + // Don't let untrusted events mess with tabs. + return; + } + + if (aEvent.altKey) + return; + + if (aEvent.ctrlKey && aEvent.shiftKey && !aEvent.metaKey) { + switch (aEvent.keyCode) { + case aEvent.DOM_VK_PAGE_UP: + this.moveTabBackward(); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + case aEvent.DOM_VK_PAGE_DOWN: + this.moveTabForward(); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + } + } + + // We need to take care of FAYT-watching as long as the findbar + // isn't initialized. The checks on aEvent are copied from + // _shouldFastFind (see findbar.xml). + if (!gFindBarInitialized && + !(aEvent.ctrlKey || aEvent.metaKey) && + !aEvent.defaultPrevented) { + let charCode = aEvent.charCode; + if (charCode) { + let char = String.fromCharCode(charCode); + if (char == "'" || char == "/" || + Services.prefs.getBoolPref("accessibility.typeaheadfind")) { + gFindBar._onBrowserKeypress(aEvent); + return; + } + } + } + +#ifdef XP_MACOSX + if (!aEvent.metaKey) + return; + + var offset = 1; + switch (aEvent.charCode) { + case '}'.charCodeAt(0): + offset = -1; + case '{'.charCodeAt(0): + if (window.getComputedStyle(this, null).direction == "ltr") + offset *= -1; + this.tabContainer.advanceSelectedTab(offset, true); + aEvent.stopPropagation(); + aEvent.preventDefault(); + } +#else + if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey && + aEvent.keyCode == KeyEvent.DOM_VK_F4 && + !this.mCurrentTab.pinned) { + this.removeCurrentTab({animate: true}); + aEvent.stopPropagation(); + aEvent.preventDefault(); + } +#endif + ]]></body> + </method> + + <property name="userTypedClear" + onget="return this.mCurrentBrowser.userTypedClear;" + onset="return this.mCurrentBrowser.userTypedClear = val;"/> + + <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; + } + event.target.setAttribute("label", tab.mOverCloseButton ? + tab.getAttribute("closetabtext") : + tab.getAttribute("label")); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "keypress": + this._handleKeyEvent(aEvent); + break; + case "sizemodechange": + if (aEvent.target == window) { + this.mCurrentBrowser.docShellIsActive = + (window.windowState != window.STATE_MINIMIZED); + } + break; + } + ]]></body> + </method> + + <method name="receiveMessage"> + <parameter name="aMessage"/> + <body><![CDATA[ + let json = aMessage.json; + let browser = aMessage.target; + + switch (aMessage.name) { + case "DOMTitleChanged": + let tab = this.getTabForBrowser(browser); + if (!tab) + return; + let titleChanged = this.setTabTitle(tab); + if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) + tab.setAttribute("titlechanged", "true"); + } + ]]></body> + </method> + + <constructor> + <![CDATA[ + let browserStack = document.getAnonymousElementByAttribute(this, "anonid", "browserStack"); + this.mCurrentBrowser = document.getAnonymousElementByAttribute(this, "anonid", "initialBrowser"); + if (Services.prefs.getBoolPref("browser.tabs.remote")) { + browserStack.removeChild(this.mCurrentBrowser); + this.mCurrentBrowser.setAttribute("remote", true); + browserStack.appendChild(this.mCurrentBrowser); + } + + this.mCurrentTab = this.tabContainer.firstChild; + document.addEventListener("keypress", this, false); + window.addEventListener("sizemodechange", this, false); + + var uniqueId = "panel" + Date.now(); + this.mPanelContainer.childNodes[0].id = uniqueId; + this.mCurrentTab.linkedPanel = uniqueId; + this.mCurrentTab._tPos = 0; + this.mCurrentTab._fullyOpen = true; + this.mCurrentTab.linkedBrowser = this.mCurrentBrowser; + 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; + this.updateWindowResizers(); + + // Hook up the event listeners to the first browser + var tabListener = this.mTabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true); + 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.mTabListeners[0] = tabListener; + this.mTabFilters[0] = filter; + + try { + // We assume this can only fail because mCurrentBrowser's docShell + // hasn't been created, yet. This may be caused by code accessing + // gBrowser before the window has finished loading. + this._addProgressListenerForInitialTab(); + } catch (e) { + // The binding was constructed too early, wait until the initial + // tab's document is ready, then add the progress listener. + this._waitForInitialContentDocument(); + } + + this.style.backgroundColor = + Services.prefs.getBoolPref("browser.display.use_system_colors") ? + "-moz-default-background-color" : + Services.prefs.getCharPref("browser.display.background_color"); + + if (Services.prefs.getBoolPref("browser.tabs.remote")) { + messageManager.addMessageListener("DOMTitleChanged", this); + } else { + this._outerWindowIDBrowserMap.set(this.mCurrentBrowser.outerWindowID, + this.mCurrentBrowser); + } + ]]> + </constructor> + + <method name="_addProgressListenerForInitialTab"> + <body><![CDATA[ + this.webProgress.addProgressListener(this.mTabFilters[0], Ci.nsIWebProgress.NOTIFY_ALL); + ]]></body> + </method> + + <method name="_waitForInitialContentDocument"> + <body><![CDATA[ + let obs = (subject, topic) => { + if (this.browsers[0].contentWindow == subject) { + Services.obs.removeObserver(obs, topic); + this._addProgressListenerForInitialTab(); + } + }; + + // We use content-document-global-created as an approximation for + // "docShell is initialized". We can do this because in the + // mTabProgressListener we care most about the STATE_STOP notification + // that will reset mBlank. That means it's important to at least add + // the progress listener before the initial about:blank load stops + // if we can't do it before the load starts. + Services.obs.addObserver(obs, "content-document-global-created", false); + ]]></body> + </method> + + <destructor> + <![CDATA[ + for (var i = 0; i < this.mTabListeners.length; ++i) { + let browser = this.getBrowserAtIndex(i); + if (browser.registeredOpenURI) { + this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI); + delete browser.registeredOpenURI; + } + browser.webProgress.removeProgressListener(this.mTabFilters[i]); + this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]); + this.mTabFilters[i] = null; + this.mTabListeners[i].destroy(); + this.mTabListeners[i] = null; + } + document.removeEventListener("keypress", this, false); + window.removeEventListener("sizemodechange", this, false); + + if (Services.prefs.getBoolPref("browser.tabs.remote")) + messageManager.removeMessageListener("DOMTitleChanged", this); + ]]> + </destructor> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <method name="enterTabbedMode"> + <body> + Application.console.log("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> + </implementation> + + <handlers> + <handler event="DOMWindowClose" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + if (this.tabs.length == 1) + return; + + var tab = this._getTabForContentWindow(event.target); + if (tab) { + this.removeTab(tab); + event.preventDefault(); + } + ]]> + </handler> + <handler event="DOMWillOpenModalDialog" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + // We're about to open a modal dialog, make sure the opening + // tab is brought to the front. + this.selectedTab = this._getTabForContentWindow(event.target.top); + ]]> + </handler> + <handler event="DOMTitleChanged"> + <![CDATA[ + if (!event.isTrusted) + return; + + var contentWin = event.target.defaultView; + if (contentWin != contentWin.top) + return; + + var tab = this._getTabForContentWindow(contentWin); + if (!tab) { + return; + } + + var titleChanged = this.setTabTitle(tab); + if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) + tab.setAttribute("titlechanged", "true"); + ]]> + </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> + </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); + + tabs.tabbrowser._removingTabs.forEach(tabs.tabbrowser.removeTab, + tabs.tabbrowser); + + 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;" +#ifndef XP_MACOSX + clicktoscroll="true" +#endif + 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" + command="cmd_newNavigatorTab" + onclick="checkForMiddleClick(this, event);" + onmouseover="document.getBindingParent(this)._enterNewTab();" + onmouseout="document.getBindingParent(this)._leaveNewTab();" + tooltiptext="&newTabButton.tooltip;"/> + <xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer" + style="width: 0;"/> + </xul:arrowscrollbox> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor> + <![CDATA[ + this.mTabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth"); + this.mCloseButtons = Services.prefs.getIntPref("browser.tabs.closeButtons"); + this._closeWindowWithLastTab = Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab"); + + var tab = this.firstChild; + tab.setAttribute("label", + this.tabbrowser.mStringBundle.getString("tabs.emptyTabTitle")); + tab.setAttribute("crop", "end"); + tab.setAttribute("onerror", "this.removeAttribute('image');"); + this.adjustTabstrip(); + + Services.prefs.addObserver("browser.tabs.", this._prefObserver, false); + 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"); + ]]> + </constructor> + + <destructor> + <![CDATA[ + Services.prefs.removeObserver("browser.tabs.", this._prefObserver); + ]]> + </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> + + <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"); + ]]></body> + </method> + + <field name="_prefObserver"><![CDATA[({ + tabContainer: this, + + observe: function (subject, topic, data) { + switch (data) { + case "browser.tabs.closeButtons": + this.tabContainer.mCloseButtons = Services.prefs.getIntPref(data); + this.tabContainer.adjustTabstrip(); + break; + case "browser.tabs.autoHide": + this.tabContainer.updateVisibility(); + break; + case "browser.tabs.closeWindowWithLastTab": + this.tabContainer._closeWindowWithLastTab = Services.prefs.getBoolPref(data); + this.tabContainer.adjustTabstrip(); + break; + } + } + });]]></field> + <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")); + + goSetCommandEnabled("cmd_ToggleTabsOnTop", visible); + + TabsOnTop.syncUI(); + + 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 && + !Services.prefs.getBoolPref("browser.tabs.autoHide"); + else + this.visible = true; + ]]></body> + </method> + + <method name="adjustTabstrip"> + <body><![CDATA[ + let numTabs = this.childNodes.length - + this.tabbrowser._removingTabs.length; + // modes for tabstrip + // 0 - button on active tab only + // 1 - close buttons on all tabs, if available space allows + // 2 - no close buttons at all + // 3 - close button at the end of the tabstrip + switch (this.mCloseButtons) { + case 0: + // If we decide we want to hide the close tab button on the last tab + // when closing the window with the last tab, then we should check + // if (numTabs == 1 && this._closeWindowWithLastTab) here and set + // this.setAttribute("closebuttons", "hidden") appropriately + this.setAttribute("closebuttons", "activetab"); + break; + case 1: + if (numTabs == 1) { + // See remark about potentially hiding the close tab button, above. + this.setAttribute("closebuttons", "alltabs"); + } else 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. + this.setAttribute("closebuttons", "alltabs"); + } else { + let tab = this.tabbrowser.visibleTabs[this.tabbrowser._numPinnedTabs]; + if (tab && tab.getBoundingClientRect().width > this.mTabClipWidth) + this.setAttribute("closebuttons", "alltabs"); + else + this.setAttribute("closebuttons", "activetab"); + } + break; + case 2: + case 3: + this.setAttribute("closebuttons", "never"); + break; + } + var tabstripClosebutton = document.getElementById("tabs-closebutton"); + if (tabstripClosebutton && tabstripClosebutton.parentNode == this._container) + tabstripClosebutton.collapsed = this.mCloseButtons != 3; + ]]></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") { +#ifdef XP_WIN + // Don't need to do anything if we're in overflow mode and we're closing + // the last tab. + if (isEndTab) +#else + // 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) +#endif + 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; + + // Resize immediately if preffed + // XXX: we may want to make this a three-state pref to disable this early + // exit if people prefer a mix of behavior (don't resize in overflow, + // but resize if not overflowing) + if (Services.prefs.getBoolPref("browser.tabs.resize_immediately")) + 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; + + // Resize immediately if preffed + // XXX: we may want to make this a three-state pref to disable this early + // exit if people prefer a mix of behavior (don't resize in overflow, + // but resize if not overflowing) + if (Services.prefs.getBoolPref("browser.tabs.resize_immediately")) + 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.MozMarginStart = - (width + scrollButtonWidth + paddingStart) + "px"; + } + + this.style.MozPaddingStart = width + paddingStart + "px"; + + } else { + this.removeAttribute("positionpinnedtabs"); + + for (let i = 0; i < numPinned; i++) { + let tab = this.childNodes[i]; + tab.style.MozMarginStart = ""; + } + + this.style.MozPaddingStart = ""; + } + + 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; + + let draggingRight = screenX > draggedTab._dragData.animLastScreenX; + 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(); + break; + case "resize": + if (aEvent.target != window) + break; + + let sizemode = document.documentElement.getAttribute("sizemode"); + TabsInTitlebar.allowedBy("sizemode", + sizemode == "maximized" || sizemode == "fullscreen"); + + var width = this.mTabstrip.boxObject.width; + if (width != this.mTabstripWidth) { + this.adjustTabstrip(); + this._fillTrailingGap(); + this._handleTabSelect(); + this.mTabstripWidth = width; + } + + this.tabbrowser.updateWindowResizers(); + 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) + return; + + var scrollRect = this.mTabstrip.scrollClientRect; + var tab = aTab.getBoundingClientRect(); + + // 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(); + + // 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"/> + <body><![CDATA[ + let tab = event.target.localName == "tab" ? event.target : null; + if (tab && + (event.type == "drop" || event.type == "dragover") && + event.dataTransfer.dropEffect == "link") { + 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"/> + <body><![CDATA[ + var tabs = this.childNodes; + var tab = this._getDragTargetTab(event); + 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="_setEffectAllowedForDataTransfer"> + <parameter name="event"/> + <body><![CDATA[ + var dt = event.dataTransfer; + // Disallow dropping multiple items + if (dt.mozItemCount > 1) + return dt.effectAllowed = "none"; + + var types = dt.mozTypesAt(0); + var sourceNode = null; + // tabs are always added as the first type + if (types[0] == TAB_DROP_TYPE) { + var 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 dt.effectAllowed = "none"; + +#ifdef XP_MACOSX + return dt.effectAllowed = event.altKey ? "copy" : "move"; +#else + return dt.effectAllowed = event.ctrlKey ? "copy" : "move"; +#endif + } + } + + if (browserDragAndDrop.canDropLink(event)) { + // Here we need to do this manually + return dt.effectAllowed = dt.dropEffect = "link"; + } + return dt.effectAllowed = "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(); + ]]></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 (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 paints = {}; + let intervals = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .stopFrameTimeRecording(aTab._recordingHandle, paints); + delete aTab._recordingHandle; + paints = paints.value; // The result array itself. + let frameCount = intervals.length; + + if (this._tabAnimationLoggingEnabled) { + let msg = "Tab " + (aTab.closing ? "close" : "open") + " (Frame-interval / paint-processing):\n"; + for (let i = 0; i < frameCount; i++) { + msg += Math.round(intervals[i]) + " / " + Math.round(paints[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. + // But if we recorded only 1 frame (very rare), then the first paint duration is a good + // representative of the first frame interval for our cause (indicates very bad animation). + // First paint duration is always useful for us. + if (frameCount > 0) { + let averageInterval = 0; + let averagePaint = paints[0]; + for (let i = 1; i < frameCount; i++) { + averageInterval += intervals[i]; + averagePaint += paints[i]; + }; + averagePaint /= frameCount; + averageInterval = (frameCount == 1) + ? averagePaint + : averageInterval / (frameCount - 1); + + if (aTab._recordingTabOpenPlain) { + delete aTab._recordingTabOpenPlain; + } + } + ]]> + </body> + </method> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <property name="mTabstripClosebutton" readonly="true" + onget="return document.getElementById('tabs-closebutton');"/> + <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[ +#ifndef XP_MACOSX + // 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 || + (TabsOnTop.enabled && this.parentNode._dragBindingAlive)) + return; +#endif + + 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"><![CDATA[ + if (event.button != 1) + return; + + if (event.target.localName == "tab") { + if (this.childNodes.length > 1 || !this._closeWindowWithLastTab) + this.tabbrowser.removeTab(event.target, {animate: true, byMouse: true}); + } else if (event.originalTarget.localName == "box") { + BrowserOpenTab(); + } else { + return; + } + + event.stopPropagation(); + ]]></handler> + + <handler event="keypress"><![CDATA[ + if (event.altKey || event.shiftKey || +#ifdef XP_MACOSX + !event.metaKey) +#else + !event.ctrlKey || event.metaKey) +#endif + return; + + 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: + // Stop the keypress event for the above keyboard + // shortcuts only. + return; + } + event.stopPropagation(); + event.preventDefault(); + ]]></handler> + + <handler event="dragstart"><![CDATA[ + var tab = this._getDragTargetTab(event); + if (!tab) + 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 = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.mozOpaque = true; + canvas.width = 160 * scale; + canvas.height = 90 * scale; + PageThumbs.captureToCanvas(browser, canvas); + dt.setDragImage(canvas, -16 * scale, -16 * scale); + + // _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) 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._setEffectAllowedForDataTransfer(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); + 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); + 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.MozMarginStart = (-ind.clientWidth) + "px"; + ]]></handler> + + <handler event="drop"><![CDATA[ + var dt = event.dataTransfer; + var dropEffect = dt.dropEffect; + var draggedTab; + if (dropEffect != "link") { // 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); + 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) { + // 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 newIndex = this._getDropIndex(event); + let newTab = this.tabbrowser.addTab("about:blank"); + let newBrowser = this.tabbrowser.getBrowserForTab(newTab); + // Stop the about:blank load + newBrowser.stop(); + // make sure it has a docshell + newBrowser.docShell; + + let numPinned = this.tabbrowser._numPinnedTabs; + if (newIndex < numPinned || draggedTab.pinned && newIndex == numPinned) + this.tabbrowser.pinTab(newTab); + this.tabbrowser.moveTabTo(newTab, newIndex); + + // 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.tabbrowser.selectedTab = newTab; + + draggedTab.parentNode._finishAnimateTabMove(); + this.tabbrowser.swapBrowsersAndCloseOther(newTab, draggedTab); + + // Call updateCurrentBrowser to make sure the URL bar is up to date + // for our new tab after we've done swapBrowsersAndCloseOther. + this.tabbrowser.updateCurrentBrowser(true); + } else { + // Pass true to disallow dropping javascript: or data: urls + let url; + try { + url = browserDragAndDrop.drop(event, { }, true); + } catch (ex) {} + +// // valid urls don't contain spaces ' '; if we have a space it isn't a valid url. +// if (!url || url.includes(" ")) //PMed + if (!url) //FF + return; + + let bgLoad = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + + if (event.shiftKey) + bgLoad = !bgLoad; + + let tab = this._getDragTargetTab(event); + if (!tab || dropEffect == "copy") { + // We're adding a new tab. + let newIndex = this._getDropIndex(event); + let newTab = this.tabbrowser.loadOneTab(url, {inBackground: bgLoad, allowThirdPartyFixup: true}); + this.tabbrowser.moveTabTo(newTab, newIndex); + } else { + // Load in an existing tab. + try { + let webNav = Ci.nsIWebNavigation; + let flags = webNav.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | + webNav.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + this.tabbrowser.getBrowserForTab(tab).loadURIWithFlags(url, flags); + if (!bgLoad) + this.selectedItem = tab; + } catch(ex) { + // Just ignore invalid urls + } + } + } + + 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") { + 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 sX = {}, sY = {}, sWidth = {}, sHeight = {}; + Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager) + .screenForRect(eX, eY, 1, 1) + .GetAvailRect(sX, sY, sWidth, sHeight); + // ensure new window entirely within screen + var winWidth = Math.min(window.outerWidth, sWidth.value); + var winHeight = Math.min(window.outerHeight, sHeight.value); + var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, sX.value), + sX.value + sWidth.value - winWidth); + var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, sY.value), + sY.value + sHeight.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 { + this.tabbrowser.replaceTabWithWindow(draggedTab, { screenX: left, + screenY: top, +#ifndef XP_WIN + outerWidth: winWidth, + outerHeight: winHeight +#endif + }); + } + 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; + /* 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. + */ + if (event.detail > 1 && !this._ignoredClick) { + this._ignoredClick = true; + return; + } + + // Reset the "ignored click" flag + this._ignoredClick = false; + + tabContainer.tabbrowser.removeTab(bindingParent, {animate: true, byMouse: true}); + tabContainer._blockDblClick = true; + + /* XXXmano hack (see bug 343628): + * Since we're removing the event target, if the user + * double-clicks this 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 dblclick handler). + */ + var clickedOnce = false; + function enableDblClick(event) { + var target = event.originalTarget; + if (target.className == 'tab-close-button') + target._ignoredClick = true; + if (!clickedOnce) { + clickedOnce = true; + return; + } + tabContainer._blockDblClick = false; + tabContainer.removeEventListener("click", enableDblClick, true); + } + tabContainer.addEventListener("click", enableDblClick, 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" closetabtext="&closeTab.label;"> + <xul:stack class="tab-stack" flex="1"> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background"> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background-start"/> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background-middle"/> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background-end"/> + </xul:hbox> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-content" align="center"> + <xul:image xbl:inherits="fadein,pinned,busy,progress,selected" + class="tab-throbber" + role="presentation" + layer="true" /> + <xul:image xbl:inherits="validate,src=image,fadein,pinned,selected" + class="tab-icon-image" + role="presentation" + anonid="tab-icon"/> + <xul:label flex="1" + xbl:inherits="value=label,crop,accesskey,fadein,pinned,selected" + class="tab-text tab-label" + role="presentation"/> + <xul:toolbarbutton anonid="close-button" + xbl:inherits="fadein,pinned,selected" + class="tab-close-button close-icon"/> + </xul:hbox> + </xul:stack> + </content> + + <implementation> + <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> + + <field name="mOverCloseButton">false</field> + <field name="mCorrespondingMenuitem">null</field> + <field name="closing">false</field> + <field name="lastAccessed">0</field> + </implementation> + + <handlers> + <handler event="mouseover"><![CDATA[ + let anonid = event.originalTarget.getAttribute("anonid"); + if (anonid == "close-button") + this.mOverCloseButton = true; + + let tab = event.target; + if (tab.closing) + return; + + let tabContainer = this.parentNode; + let visibleTabs = tabContainer.tabbrowser.visibleTabs; + let tabIndex = visibleTabs.indexOf(tab); + 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"); + } + } + ]]></handler> + <handler event="mouseout"><![CDATA[ + let anonid = event.originalTarget.getAttribute("anonid"); + if (anonid == "close-button") + this.mOverCloseButton = false; + + let tabContainer = this.parentNode; + if (tabContainer._beforeHoveredTab) { + tabContainer._beforeHoveredTab.removeAttribute("beforehovered"); + tabContainer._beforeHoveredTab = null; + } + if (tabContainer._afterHoveredTab) { + tabContainer._afterHoveredTab.removeAttribute("afterhovered"); + tabContainer._afterHoveredTab = null; + } + ]]></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) { + // Prevent tabbox.xml from selecting the tab. + event.stopPropagation(); + } + ]]> + </handler> + <handler event="mouseup"> + this.style.MozUserFocus = ''; + </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; + 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); + + if (!aTab.mCorrespondingMenuitem) { + 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"); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="popupshowing"> + <![CDATA[ + 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[ + // 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); + } + } + 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 alignRight = false; + + if (getComputedStyle(document.documentElement).direction == "rtl") + alignRight = !alignRight; + + let rect = this.getBoundingClientRect(); + this._mouseTargetRect = { + top: rect.top, + bottom: rect.bottom, + left: alignRight ? window.innerWidth - rect.width : 0, + right: alignRight ? window.innerWidth : rect.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> + +</bindings> diff --git a/application/palemoon/base/content/test/general/audio.ogg b/application/palemoon/base/content/test/general/audio.ogg Binary files differnew file mode 100644 index 0000000000..7e6ef77ec4 --- /dev/null +++ b/application/palemoon/base/content/test/general/audio.ogg diff --git a/application/palemoon/base/content/urlbarBindings.xml b/application/palemoon/base/content/urlbarBindings.xml new file mode 100644 index 0000000000..c99819f0d3 --- /dev/null +++ b/application/palemoon/base/content/urlbarBindings.xml @@ -0,0 +1,1969 @@ +<?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; +]> + +<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" + xbl:inherits="tooltiptext=inputtooltiptext,value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey"/> + </xul:hbox> + <children includes="hbox"/> + </xul:hbox> + <xul:dropmarker anonid="historydropmarker" + class="autocomplete-history-dropmarker urlbar-history-dropmarker" + allowevents="true" + xbl:inherits="open,enablehistory,parentfocused=focused"/> + <xul:popupset anonid="popupset" + class="autocomplete-result-popupset"/> + <children includes="toolbarbutton"/> + </content> + + <implementation implements="nsIObserver, nsIDOMEventListener"> + <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.inputField.controllers.insertControllerAt(0, this._copyCutController); + 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); + + const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + 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); + } + ]]></constructor> + + <destructor><![CDATA[ + this._prefs.removeObserver("", this); + this._prefs = null; + this.inputField.controllers.removeController(this._copyCutController); + 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> + + <!-- + 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[ + if (this.hasAttribute("actiontype")) + return {value: this._value}; + return null; + ]]></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) { + returnValue = action.param; + } + + // Set the actiontype only if the user is not overriding actions. + if (action && this._noActionsKeys.size == 0) { + this.setAttribute("actiontype", action.type); + } else { + this.removeAttribute("actiontype"); + } + return returnValue; + ]]></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.focused) + return; + + let controller = this.editor.selectionController; + let selection = controller.getSelection(controller.SELECTION_URLSECONDARY); + selection.removeAllRanges(); + + let textNode = this.editor.rootElement.firstChild; + let value = textNode.textContent; + + let protocol = value.match(/^[a-z\d.+\-]+:(?=[^\d])/); + if (protocol && + ["http:", "https:", "ftp:"].indexOf(protocol[0]) == -1) + return; + let matchedURL = value.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/); + if (!matchedURL) + return; + + let [, preDomain, domain] = matchedURL; + let baseDomain = domain; + let subDomain = ""; + // getBaseDomainFromHost doesn't recognize IPv6 literals in brackets as IPs (bug 667159) + if (domain[0] != "[") { + try { + baseDomain = Services.eTLD.getBaseDomainFromHost(domain); + if (!domain.endsWith(baseDomain)) { + // getBaseDomainFromHost converts its resultant to ACE. + let IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + baseDomain = IDNService.convertACEtoUTF8(baseDomain); + } + } catch (e) {} + } + if (baseDomain != domain) { + subDomain = domain.slice(0, -baseDomain.length); + } + + let rangeLength = preDomain.length + subDomain.length; + if (rangeLength) { + let range = document.createRange(); + range.setStart(textNode, 0); + range.setEnd(textNode, rangeLength); + selection.addRange(range); + } + + let startRest = preDomain.length + domain.length; + if (startRest < value.length) { + let range = document.createRange(); + range.setStart(textNode, startRest); + range.setEnd(textNode, value.length); + selection.addRange(range); + } + ]]></body> + </method> + + <method name="_clearFormatting"> + <body><![CDATA[ + if (!this._formattingEnabled) + return; + + let controller = this.editor.selectionController; + let selection = controller.getSelection(controller.SELECTION_URLSECONDARY); + selection.removeAllRanges(); + ]]></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> + + <method name="handleCommand"> + <parameter name="aTriggeringEvent"/> + <body><![CDATA[ + if (aTriggeringEvent instanceof MouseEvent && aTriggeringEvent.button == 2) + return; // Do nothing for right clicks + + var url = this.value; + var mayInheritPrincipal = false; + var postData = null; + + var action = this._parseActionUrl(url); + if (action) { + url = action.param; + if (this.hasAttribute("actiontype")) { + if (action.type == "switchtab") { + this.handleRevert(); + let prevTab = gBrowser.selectedTab; + if (switchToTabHavingURI(url) && + isTabEmpty(prevTab)) + gBrowser.removeTab(prevTab); + } + return; + } + } + else { + [url, postData, mayInheritPrincipal] = this._canonizeURL(aTriggeringEvent); + if (!url) + return; + } + + this.value = url; + gBrowser.userTypedValue = 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); + } + + function loadCurrent() { + let webnav = Ci.nsIWebNavigation; + let flags = webnav.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | + webnav.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from + // inheriting the currently loaded document's principal, unless this + // URL is marked as safe to inherit (e.g. came from a bookmark + // keyword). + if (!mayInheritPrincipal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; + gBrowser.loadURIWithFlags(url, flags, null, null, postData); + } + + // 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. + gBrowser.selectedBrowser.focus(); + + let isMouseEvent = aTriggeringEvent instanceof MouseEvent; + let altEnter = !isMouseEvent && aTriggeringEvent && aTriggeringEvent.altKey; + + if (altEnter) { + // XXX This was added a long time ago, and I'm not sure why it is + // necessary. Alt+Enter's default action might cause a system beep, + // or something like that? + aTriggeringEvent.preventDefault(); + aTriggeringEvent.stopPropagation(); + } + + // If the current tab is empty, ignore Alt+Enter (just reuse this tab) + altEnter = altEnter && !isTabEmpty(gBrowser.selectedTab); + + if (isMouseEvent || altEnter) { + // Use the standard UI link behaviors for clicks or Alt+Enter + let where = "tab"; + if (isMouseEvent) + where = whereToOpenLink(aTriggeringEvent, false, false); + + if (where == "current") { + loadCurrent(); + } else { + this.handleRevert(); + let params = { allowThirdPartyFixup: true, + postData: postData, + initiatingDoc: document }; + openUILinkIn(url, where, params); + } + } else { + loadCurrent(); + } + ]]></body> + </method> + + <method name="_canonizeURL"> + <parameter name="aTriggeringEvent"/> + <body><![CDATA[ + var url = this.value; + if (!url) + return ["", null, false]; + + // 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(url) && + (aTriggeringEvent instanceof KeyEvent)) { +#ifdef XP_MACOSX + let accel = aTriggeringEvent.metaKey; +#else + let accel = aTriggeringEvent.ctrlKey; +#endif + 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) { + // 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; + } + + url = "http://www." + url; + } + } + + var postData = {}; + var mayInheritPrincipal = { value: false }; + url = getShortcutOrURI(url, postData, mayInheritPrincipal); + + return [url, postData.value, mayInheritPrincipal.value]; + ]]></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="onDragOver"> + <parameter name="aEvent"/> + <body> + var types = aEvent.dataTransfer.types; + if (types.contains("application/x-moz-file") || + types.contains("text/x-moz-url") || + types.contains("text/uri-list") || + types.contains("text/unicode")) + aEvent.preventDefault(); + </body> + </method> + + <method name="onDrop"> + <parameter name="aEvent"/> + <body><![CDATA[ + let url = browserDragAndDrop.drop(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 (url) { + aEvent.preventDefault(); + this.value = url; + SetPageProxyState("invalid"); + this.focus(); + try { + urlSecurityCheck(url, + gBrowser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + } catch (ex) { + return; + } + 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, nothing else to do here. + if (this.selectionStart > 0 || this.valueIsTyped) + 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; + try { + uri = uriFixup.createFixupURI(inputVal, Ci.nsIURIFixup.FIXUP_FLAG_NONE); + } catch (e) {} + if (!uri) + return selectedVal; + + // 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")) { + // Parentheses are known to confuse third-party applications (bug 458565). + selectedVal = uri.spec.replace(/[()]/g, function (c) escape(c)); + } + + 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; + urlbar.removeAttribute("actiontype"); + SetPageProxyState("invalid"); + } + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(val, document); + }, + 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 "trimURLs": + this._mayTrimURLs = this._prefs.getBoolPref(aData); + break; + } + } + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + 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> + + <property name="textValue" + onget="return this.value;"> + <setter> + <![CDATA[ + try { + val = losslessDecodeURI(makeURI(val)); + } catch (ex) { } + + // Trim popup selected values, but never trim results coming from + // autofill. + if (this.popup.selectedIndex == -1) + this._disableTrim = true; + this.value = val; + this._disableTrim = false; + + // Completing a result should simulate the user typing the result, so + // fire an input event. + let evt = document.createEvent("UIEvents"); + evt.initUIEvent("input", true, false, window, 0); + this.mIgnoreInput = true; + this.dispatchEvent(evt); + this.mIgnoreInput = false; + + return this.value; + ]]> + </setter> + </property> + + <method name="_parseActionUrl"> + <parameter name="aUrl"/> + <body><![CDATA[ + if (!aUrl.startsWith("moz-action:")) + return null; + + // url is in the format moz-action:ACTION,PARAM + let [, action, param] = aUrl.match(/^moz-action:([^,]+),(.*)$/); + return {type: action, param: param}; + ]]></body> + </method> + + <field name="_noActionsKeys"><![CDATA[ + new Set(); + ]]></field> + + <method name="_clearNoActions"> + <parameter name="aURL"/> + <body><![CDATA[ + this._noActionsKeys.clear(); + this.popup.removeAttribute("noactions"); + let action = this._parseActionUrl(this._value); + if (action) + this.setAttribute("actiontype", action.type); + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="keydown"><![CDATA[ + if ((event.keyCode === KeyEvent.DOM_VK_ALT || + event.keyCode === KeyEvent.DOM_VK_SHIFT) && + this.popup.selectedIndex >= 0 && + !this._noActionsKeys.has(event.keyCode)) { + if (this._noActionsKeys.size == 0) { + this.popup.setAttribute("noactions", "true"); + this.removeAttribute("actiontype"); + } + this._noActionsKeys.add(event.keyCode); + } + ]]></handler> + + <handler event="keyup"><![CDATA[ + if ((event.keyCode === KeyEvent.DOM_VK_ALT || + event.keyCode === KeyEvent.DOM_VK_SHIFT) && + this._noActionsKeys.has(event.keyCode)) { + this._noActionsKeys.delete(event.keyCode); + if (this._noActionsKeys.size == 0) + this._clearNoActions(); + } + ]]></handler> + + <handler event="blur"><![CDATA[ + if (event.originalTarget != this.inputField) + return; + this._clearNoActions(); + this.formatValue(); + ]]></handler> + + <handler event="dragstart" phase="capturing"><![CDATA[ + // Drag only if the gesture starts from the input field. + if (event.originalTarget != this.inputField) + 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 = content.location.href; + var title = content.document.title || 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="focus" phase="capturing"><![CDATA[ + if (event.originalTarget != this.inputField) + return; + this._hideURLTooltip(); + this._clearFormatting(); + ]]></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; + + // Check if this selection was actually user-generated, and exit if not + // to prevent copying the selection (e.g autofill) to clipboard/primary + 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, document); + ]]></handler> + </handlers> + + </binding> + + <!-- Note: this binding is applied to the autocomplete popup used in the Search bar and in web page content --> + <binding id="browser-autocomplete-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-result-popup"> + <implementation> + <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); + + // Check for unmodified left-click, and use default behavior + if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey && + !aEvent.altKey && !aEvent.metaKey) { + controller.handleEnter(true); + return; + } + + // Check for middle-click or modified clicks on the search bar + var searchBar = BrowserSearch.searchBar; + if (searchBar && searchBar.textbox == this.mInput) { + // Handle search bar popup clicks + var search = controller.getValueAt(this.selectedIndex); + + // close the autocomplete popup and revert the entered search term + this.closePopup(); + controller.handleEscape(); + + // Fill in the search bar's value + searchBar.value = search; + + // open the search results according to the clicking subtlety + var where = whereToOpenLink(aEvent, false, true); + searchBar.doSearch(search, where); + } + ]]></body> + </method> + </implementation> + </binding> + + <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup"> + <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> + + <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; + + // this method is defined on the base binding + this._openAutocompletePopup(aInput, aElement); + ]]></body> + </method> + + <method name="onPopupClick"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + // Ignore right-clicks + if (aEvent.button == 2) + return; + + var controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController); + + // Check for unmodified left-click, and use default behavior + if (aEvent.button == 0 && !aEvent.shiftKey && !aEvent.ctrlKey && + !aEvent.altKey && !aEvent.metaKey) { + controller.handleEnter(true); + return; + } + + // Check for middle-click or modified clicks on the URL bar + if (gURLBar && this.mInput == gURLBar) { + var url = controller.getValueAt(this.selectedIndex); + + // close the autocomplete popup and revert the entered address + this.closePopup(); + controller.handleEscape(); + + // Check if this is meant to be an action + let action = this.mInput._parseActionUrl(url); + if (action) { + if (action.type == "switchtab") + url = action.param; + else + return; + } + + // respect the usual clicking subtleties + openUILink(url, aEvent); + } + ]]> + </body> + </method> + + <method name="createResultLabel"> + <parameter name="aTitle"/> + <parameter name="aUrl"/> + <parameter name="aType"/> + <body> + <![CDATA[ + var label = aTitle + " " + aUrl; + // convert aType (ex: "ac-result-type-<aType>") to text to be spoke aloud + // by screen readers. convert "tag" and "bookmark" to the localized versions, + // but don't do anything for "favicon" (the default) + if (aType != "favicon") { + label += " " + this._bundle.GetStringFromName(aType + "ResultLabel"); + } + return label; + ]]> + </body> + </method> + + </implementation> + </binding> + + <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification"> + <content align="start"> + <xul:image class="popup-notification-icon" + xbl:inherits="popupid,src=icon"/> + <xul:vbox flex="1"> + <xul:description class="popup-notification-description addon-progress-description" + xbl:inherits="xbl:text=label"/> + <xul:spacer flex="1"/> + <xul:hbox align="center"> + <xul:progressmeter anonid="progressmeter" flex="1" mode="undetermined" class="popup-progress-meter"/> + <xul:button anonid="cancel" class="popup-progress-cancel" oncommand="document.getBindingParent(this).cancel()"/> + </xul:hbox> + <xul:label anonid="progresstext" class="popup-progress-label"/> + <xul:hbox class="popup-notification-button-container" + pack="end" align="center"> + <xul:button anonid="button" + class="popup-notification-menubutton" + type="menu-button" + xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey"> + <xul:menupopup anonid="menupopup" + xbl:inherits="oncommand=menucommand"> + <children/> + <xul:menuitem class="menuitem-iconic popup-notification-closeitem close-icon" + label="&closeNotificationItem.label;" + xbl:inherits="oncommand=closeitemcommand"/> + </xul:menupopup> + </xul:button> + </xul:hbox> + </xul:vbox> + <xul:vbox pack="start"> + <xul:toolbarbutton anonid="closebutton" + class="messageCloseButton close-icon popup-notification-closebutton tabbable" + xbl:inherits="oncommand=closebuttoncommand" + tooltiptext="&closeNotification.tooltip;"/> + </xul:vbox> + </content> + <implementation> + <constructor><![CDATA[ + this.cancelbtn.setAttribute("tooltiptext", gNavigatorBundle.getString("addonDownloadCancelTooltip")); + + 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.getAnonymousElementByAttribute(this, "anonid", "progressmeter"); + </field> + <field name="progresstext" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "progresstext"); + </field> + <field name="cancelbtn" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "cancel"); + </field> + <field name="DownloadUtils" readonly="true"> + let utils = {}; + Components.utils.import("resource://gre/modules/DownloadUtils.jsm", utils); + utils.DownloadUtils; + </field> + + <method name="destroy"> + <body><![CDATA[ + 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.mode = "undetermined"; + } + else { + this.progressmeter.mode = "determined"; + this.progressmeter.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.value = status; + ]]></body> + </method> + + <method name="cancel"> + <body><![CDATA[ + // Cache these as cancelling the installs will remove this + // notification which will drop these references + let browser = this.notification.browser; + let contentWindow = this.notification.options.contentWindow; + let sourceURI = this.notification.options.sourceURI; + + 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); + + let anchorID = "addons-notification-icon"; + let notificationID = "addon-install-cancelled"; + let messageString = gNavigatorBundle.getString("addonDownloadCancelled"); + messageString = PluralForm.get(installs.length, messageString); + let buttonText = gNavigatorBundle.getString("addonDownloadRestart"); + buttonText = PluralForm.get(installs.length, buttonText); + + let action = { + label: buttonText, + accessKey: gNavigatorBundle.getString("addonDownloadRestart.accessKey"), + callback: function() { + let weblistener = Cc["@mozilla.org/addons/web-install-listener;1"]. + getService(Ci.amIWebInstallListener); + if (weblistener.onWebInstallRequested(contentWindow, sourceURI, + installs, installs.length)) { + installs.forEach(function(aInstall) { + aInstall.install(); + }); + } + } + }; + + PopupNotifications.show(browser, notificationID, messageString, + anchorID, action); + ]]></body> + </method> + + <method name="updateProgress"> + <body><![CDATA[ + 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(); + 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; + + 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" class="click-to-play-plugins-notification-content"> + <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 close-icon popup-notification-closebutton tabbable" + 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 primary-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"; + for (let action of this.notification.options.centerActions) { + 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.notification.options.centerActions.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 host = gPluginHandler._getHostFromPrincipal(this.notification.browser.contentWindow.document.nodePrincipal); + this._setupDescription("pluginActivateMultiple.message", null, host); + + 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.notification.options.centerActions[0]; + var host = action.pluginPermissionHost; + + let label, linkLabel, linkUrl, 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")); + } + } + 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")); + } + } + this._setupDescription(label, action.pluginName, host); + 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="host" /> + <body><![CDATA[ + var bsn = this._brandShortName; + var span = document.getAnonymousElementByAttribute(this, "anonid", "click-to-play-plugins-notification-description"); + while (span.lastChild) { + span.removeChild(span.lastChild); + } + + var args = ["__host__", this._brandShortName]; + if (pluginName) { + args.unshift(pluginName); + } + var bases = gNavigatorBundle.getFormattedString(baseString, args). + split("__host__", 2); + + span.appendChild(document.createTextNode(bases[0])); + var hostSpan = document.createElementNS("http://www.w3.org/1999/xhtml", "em"); + hostSpan.appendChild(document.createTextNode(host)); + span.appendChild(hostSpan); + 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.notification.options.centerActions[0], + "allownow"); + this._cancel(); + ]]></body> + </method> + <method name="_singleBlock"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this.notification.options.centerActions[0], + "block"); + this._cancel(); + ]]></body> + </method> + <method name="_singleActivateAlways"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this.notification.options.centerActions[0], + "allowalways"); + this._cancel(); + ]]></body> + </method> + <method name="_singleContinue"> + <body><![CDATA[ + gPluginHandler._updatePluginPermission(this.notification, + this.notification.options.centerActions[0], + "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_ENTER" group="system" action="this._accept(event);"/> + <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> + + <binding id="promobox"> + <content> + <xul:hbox class="panel-promo-box" align="start" flex="1"> + <xul:hbox align="center" flex="1"> + <xul:image class="panel-promo-icon"/> + <xul:description anonid="promo-message" class="panel-promo-message" flex="1"> + <xul:description anonid="promo-link" + class="plain text-link inline-link" + onclick="document.getBindingParent(this).onLinkClick();"/> + </xul:description> + </xul:hbox> + <xul:toolbarbutton class="panel-promo-closebutton close-icon" + oncommand="document.getBindingParent(this).onCloseButtonCommand();" + tooltiptext="&closeNotification.tooltip;"/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor><![CDATA[ + this._panel.addEventListener("popupshowing", this, false); + ]]></constructor> + + <destructor><![CDATA[ + this._panel.removeEventListener("popupshowing", this, false); + ]]></destructor> + + <field name="_panel" readonly="true"><![CDATA[ + let node = this.parentNode; + while(node && node.localName != "panel") { + node = node.parentNode; + } + node; + ]]></field> + <field name="_promomessage" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "promo-message"); + </field> + <field name="_promolink" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "promo-link"); + </field> + <field name="_brandBundle" readonly="true"> + Services.strings.createBundle("chrome://branding/locale/brand.properties"); + </field> + <property name="_viewsLeftMap"> + <getter><![CDATA[ + let viewsLeftMap = {}; + try { + viewsLeftMap = JSON.parse(Services.prefs.getCharPref("browser.syncPromoViewsLeftMap")); + } catch (ex) { + // If the old preference exists, migrate it to the new one. + try { + let oldPref = Services.prefs.getIntPref("browser.syncPromoViewsLeft"); + Services.prefs.clearUserPref("browser.syncPromoViewsLeft"); + viewsLeftMap.bookmarks = oldPref; + viewsLeftMap.passwords = oldPref; + Services.prefs.setCharPref("browser.syncPromoViewsLeftMap", + JSON.stringify(viewsLeftMap)); + } catch (ex2) {} + } + return viewsLeftMap; + ]]></getter> + </property> + <property name="_viewsLeft"> + <getter><![CDATA[ + let views = 5; + let map = this._viewsLeftMap; + if (this._notificationType in map) { + views = map[this._notificationType]; + } + return views; + ]]></getter> + <setter><![CDATA[ + let map = this._viewsLeftMap; + map[this._notificationType] = val; + Services.prefs.setCharPref("browser.syncPromoViewsLeftMap", + JSON.stringify(map)); + return val; + ]]></setter> + </property> + <property name="_notificationType"> + <getter><![CDATA[ + // Use the popupid attribute to identify the notification type, + // otherwise just rely on the panel id for common arrowpanels. + let type = this._panel.firstChild.getAttribute("popupid") || + this._panel.id; + if (type.startsWith("password-")) + return "passwords"; + if (type == "editBookmarkPanel") + return "bookmarks"; + if (type == "addon-install-complete") { + if (!Services.prefs.prefHasUserValue("services.sync.username")) + return "addons"; + if (!Services.prefs.getBoolPref("services.sync.engine.addons")) + return "addons-sync-disabled"; + } + return null; + ]]></getter> + </property> + <property name="_notificationMessage"> + <getter><![CDATA[ + return gNavigatorBundle.getFormattedString( + "syncPromoNotification." + this._notificationType + ".description", + [this._brandBundle.GetStringFromName("syncBrandShortName")] + ); + ]]></getter> + </property> + <property name="_notificationLink"> + <getter><![CDATA[ + if (this._notificationType == "addons-sync-disabled") { + return "https://forum.palemoon.org/viewforum.php?f=52"; + } + return "http://www.palemoon.org/sync/"; + ]]></getter> + </property> + <method name="onCloseButtonCommand"> + <body><![CDATA[ + this._viewsLeft = 0; + this.hidden = true; + ]]></body> + </method> + <method name="onLinkClick"> + <body><![CDATA[ + // Open a new selected tab and close the current panel. + gBrowser.loadOneTab(this._promolink.getAttribute("href"), + { inBackground: false }); + this._panel.hidePopup(); + ]]></body> + </method> + <method name="handleEvent"> + <parameter name="event"/> + <body><![CDATA[ + if (event.type != "popupshowing" || event.target != this._panel) + return; + + // A previous notification may have unhidden this. + this.hidden = true; + + // Only handle supported notification panels. + if (!this._notificationType) { + return; + } + + let viewsLeft = this._viewsLeft; + if (viewsLeft) { + // XXX: Short-circuit this for now. + // TO-DO: Clean up this code for sync promotion + this._viewsLeft = 0; + viewsLeft = 0; + return; + // XXX + + if (Services.prefs.prefHasUserValue("services.sync.username") && + this._notificationType != "addons-sync-disabled") { + // If the user has already setup Sync, don't show the notification. + this._viewsLeft = 0; + // Be sure to hide the panel, in case it was visible and the user + // decided to setup Sync after noticing it. + viewsLeft = 0; + // The panel is still hidden, just bail out. + return; + } + else { + this._viewsLeft = viewsLeft - 1; + } + + this._promolink.setAttribute("href", this._notificationLink); + this._promolink.value = gNavigatorBundle.getString("syncPromoNotification.learnMoreLinkText"); + + this.hidden = false; + + // HACK: The description element doesn't wrap correctly in panels, + // thus set a width on it, based on the available space, before + // setting its textContent. Then set its height as well, to + // fix wrong height calculation on Linux (bug 659578). + this._panel.addEventListener("popupshown", function panelShown() { + this._panel.removeEventListener("popupshown", panelShown, true); + // Previous popupShown events may close the panel or change + // its contents, so ensure this is still valid. + if (this._panel.state != "open" || !this._notificationType) + return; + this._promomessage.width = this._promomessage.getBoundingClientRect().width; + this._promomessage.firstChild.textContent = this._notificationMessage; + this._promomessage.height = this._promomessage.getBoundingClientRect().height; + }.bind(this), true); + } + ]]></body> + </method> + </implementation> + </binding> + + <binding id="toolbarbutton-badged" display="xul:button" + extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"> + <content> + <children includes="observes|template|menupopup|panel|tooltip"/> + <xul:hbox class="toolbarbutton-badge-container" align="start" pack="end" flex="1"> + <xul:hbox class="toolbarbutton-badge" xbl:inherits="badge"/> + <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label"/> + </xul:hbox> + <xul:label class="toolbarbutton-text" crop="right" flex="1" + xbl:inherits="value=label,accesskey,crop"/> + </content> + </binding> + +</bindings> diff --git a/application/palemoon/base/content/utilityOverlay.js b/application/palemoon/base/content/utilityOverlay.js new file mode 100644 index 0000000000..b1e78d6a95 --- /dev/null +++ b/application/palemoon/base/content/utilityOverlay.js @@ -0,0 +1,677 @@ +# -*- Mode: javascript; 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/. + +// Services = object with smart getters for common XPCOM services +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.defineLazyGetter(this, "BROWSER_NEW_TAB_URL", function () { + const PREF = "browser.newtab.url"; + + function getNewTabPageURL() { + if (!Services.prefs.prefHasUserValue(PREF)) { + if (PrivateBrowsingUtils.isWindowPrivate(window) && + !PrivateBrowsingUtils.permanentPrivateBrowsing) + return "about:privatebrowsing"; + } + return Services.prefs.getCharPref(PREF) || "about:blank"; + } + + function update() { + BROWSER_NEW_TAB_URL = getNewTabPageURL(); + } + + Services.prefs.addObserver(PREF, update, false); + + addEventListener("unload", function onUnload() { + removeEventListener("unload", onUnload); + Services.prefs.removeObserver(PREF, update); + }); + + return getNewTabPageURL(); +}); + +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) { + // Pale Moon: Only make "about:blank", the logopage, or "about:newtab" be + // a "blank page" to fix focus issues. + // Original code: return aURL == "about:blank" || aURL == BROWSER_NEW_TAB_URL; + 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, + 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. + +#ifdef XP_MACOSX + if (meta || (middle && middleUsesTabs)) +#else + if (ctrl || (middle && middleUsesTabs)) +#endif + 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) + */ +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 + }; + } + + params.fromChrome = true; + + openLinkIn(url, where, params); +} + +function openLinkIn(url, where, params) { + if (!where || !url) + return; + + var aFromChrome = params.fromChrome; + var aAllowThirdPartyFixup = params.allowThirdPartyFixup; + var aPostData = params.postData; + var aCharset = params.charset; + var aReferrerURI = params.referrerURI; + var aRelatedToCurrent = params.relatedToCurrent; + var aInBackground = params.inBackground; + var aDisallowInheritPrincipal = params.disallowInheritPrincipal; + var aInitiatingDoc = params.initiatingDoc; + var aIsPrivate = params.private; + var sendReferrerURI = true; + + if (where == "save") { + if (!aInitiatingDoc) { + Components.utils.reportError("openUILink/openLinkIn was called with " + + "where == 'save' but without initiatingDoc. See bug 814264."); + return; + } + saveURL(url, null, null, true, null, aReferrerURI, aInitiatingDoc); + return; + } + const Cc = Components.classes; + const Ci = Components.interfaces; + + var w = getTopWin(); + if ((where == "tab" || where == "tabshifted") && + w && !w.toolbar.visible) { + w = getTopWin(true); + aRelatedToCurrent = false; + } + + if (!w || where == "window") { + // Strip referrer data when opening a new private window, to prevent + // regular browsing data from leaking into it. + if (aIsPrivate) { + sendReferrerURI = false; + } + + var sa = Cc["@mozilla.org/supports-array;1"]. + createInstance(Ci.nsISupportsArray); + + 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; + + sa.AppendElement(wuri); + sa.AppendElement(charset); + if (sendReferrerURI) + sa.AppendElement(aReferrerURI); + sa.AppendElement(aPostData); + sa.AppendElement(allowThirdPartyFixupSupports); + + 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"); + } + + if (where == "current" && w.gBrowser.selectedTab.pinned) { + try { + let uriObj = Services.io.newURI(url, null, null); + if (!uriObj.schemeIs("javascript") && + w.gBrowser.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(); + + 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; + } + if (aDisallowInheritPrincipal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; + w.gBrowser.loadURIWithFlags(url, flags, aReferrerURI, null, aPostData); + break; + case "tabshifted": + loadInBackground = !loadInBackground; + // fall through + case "tab": + let browser = w.gBrowser; + browser.loadOneTab(url, { + referrerURI: aReferrerURI, + charset: aCharset, + postData: aPostData, + inBackground: loadInBackground, + allowThirdPartyFixup: aAllowThirdPartyFixup, + relatedToCurrent: aRelatedToCurrent}); + break; + } + + w.gBrowser.selectedBrowser.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); + } +} + +// 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); + } +} + +// 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, use that. + var altText = node.getAttribute( "alt" ); + if ( altText && altText != "" ) { + text = altText; + break; + } + } + // 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 whitespace. + text = text.replace( /^\s+/, "" ); + // Strip trailing whitespace. + text = text.replace( /\s+$/, "" ); + // Compress remaining whitespace. + text = text.replace( /\s+/g, " " ); + return text; +} + +function getShellService() +{ + var shell = null; + try { + shell = Components.classes["@mozilla.org/browser/shell-service;1"] + .getService(Components.interfaces.nsIShellService); + } catch (e) { + } + return shell; +} + +function isBidiEnabled() { + // first check the pref. + if (getBoolPref("bidi.browser.ui", false)) + return true; + + // if the pref isn't set, check for an RTL locale and force the pref to true + // if we find 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 "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(); + win.focus(); + return; + } + +#ifdef XP_WIN + var features = "chrome,centerscreen,dependent"; +#elifdef XP_MACOSX + var features = "chrome,resizable=no,minimizable=no"; +#else + var features = "chrome,centerscreen,dependent,dialog=no"; +#endif + window.openDialog("chrome://browser/content/aboutDialog.xul", "", features); +} + +function openPreferences(paneID, extraArgs) +{ + var instantApply = getBoolPref("browser.preferences.instantApply", false); + var features = "chrome,titlebar,toolbar,centerscreen" + (instantApply ? ",dialog=no" : ",modal"); + + var win = Services.wm.getMostRecentWindow("Browser:Preferences"); + if (win) { + win.focus(); + if (paneID) { + var pane = win.document.getElementById(paneID); + win.document.documentElement.showPane(pane); + } + + if (extraArgs && extraArgs["advancedTab"]) { + var advancedPaneTabs = win.document.getElementById("advancedPrefs"); + advancedPaneTabs.selectedTab = win.document.getElementById(extraArgs["advancedTab"]); + } + + return win; + } + + return openDialog("chrome://browser/content/preferences/preferences.xul", + "Preferences", features, paneID, extraArgs); +} + +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 feedback page for this version of the application. + */ +function openFeedbackPage() +{ + openUILinkIn(Services.prefs.getCharPref("browser.feedback.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 + * The document from which the URL came, or null. This is used to set the + * referrer header and to do a security check of whether the document is + * allowed to reference the URL. If null, there will be no referrer + * header and no security check. + * @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 + * If aDocument is null, then this will be used as the referrer. + * There will be no security check. + */ +function openNewTabWith(aURL, aDocument, aPostData, aEvent, + aAllowThirdPartyFixup, aReferrer) { + if (aDocument) + urlSecurityCheck(aURL, aDocument.nodePrincipal); + + // As in openNewWindowWith(), we want to pass the charset of the + // current document over to a new tab. + var originCharset = aDocument && aDocument.characterSet; + if (!originCharset && + document.documentElement.getAttribute("windowtype") == "navigator:browser") + originCharset = window.content.document.characterSet; + + openLinkIn(aURL, aEvent && aEvent.shiftKey ? "tabshifted" : "tab", + { charset: originCharset, + postData: aPostData, + allowThirdPartyFixup: aAllowThirdPartyFixup, + referrerURI: aDocument ? aDocument.documentURIObject : aReferrer }); +} + +function openNewWindowWith(aURL, aDocument, aPostData, aAllowThirdPartyFixup, aReferrer) { + if (aDocument) + urlSecurityCheck(aURL, aDocument.nodePrincipal); + + // 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 originCharset = aDocument && aDocument.characterSet; + if (!originCharset && + document.documentElement.getAttribute("windowtype") == "navigator:browser") + originCharset = window.content.document.characterSet; + + openLinkIn(aURL, "window", + { charset: originCharset, + postData: aPostData, + allowThirdPartyFixup: aAllowThirdPartyFixup, + referrerURI: aDocument ? aDocument.documentURIObject : aReferrer }); +} + +/** + * isValidFeed: checks whether the given data represents a valid feed. + * + * @param aLink + * An object representing a feed with title, href and type. + * @param aPrincipal + * The principal of the document, used for security check. + * @param aIsFeed + * Whether this is already a known feed or not, if true only a security + * check will be performed. + */ +function isValidFeed(aLink, aPrincipal, aIsFeed) +{ + if (!aLink || !aPrincipal) + return false; + + var type = aLink.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, ""); + if (!aIsFeed) { + aIsFeed = (type == "application/rss+xml" || + type == "application/atom+xml"); + } + + if (aIsFeed) { + try { + urlSecurityCheck(aLink.href, aPrincipal, + Components.interfaces.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + return type || "application/rss+xml"; + } + catch(ex) { + } + } + + return null; +} + +// aCalledFromModal is optional +function openHelpLink(aHelpTopic, aCalledFromModal) { + var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"] + .getService(Components.interfaces.nsIURLFormatter) + .formatURLPref("app.support.baseURL"); + url += aHelpTopic; + + var 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. + return aURL /* remove single trailing slash for http/https/ftp URLs */ + .replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1") + /* remove http:// unless the host starts with "ftp\d*\." or contains "@" */ + .replace(/^http:\/\/((?!ftp\d*\.)[^\/@]+(?:\/|$))/, "$1"); +} diff --git a/application/palemoon/base/content/viewSourceOverlay.xul b/application/palemoon/base/content/viewSourceOverlay.xul new file mode 100644 index 0000000000..8b40ddfd28 --- /dev/null +++ b/application/palemoon/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/palemoon/base/content/web-panels.js b/application/palemoon/base/content/web-panels.js new file mode 100644 index 0000000000..6e2bf5bc43 --- /dev/null +++ b/application/palemoon/base/content/web-panels.js @@ -0,0 +1,102 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const 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); + 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/palemoon/base/content/web-panels.xul b/application/palemoon/base/content/web-panels.xul new file mode 100644 index 0000000000..ea1e2eba74 --- /dev/null +++ b/application/palemoon/base/content/web-panels.xul @@ -0,0 +1,84 @@ +<?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://global/content/inlineSpellCheckUI.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();"/> + <command id="Browser:BackOrBackDuplicate" + oncommand="getPanelBrowser().webNavigation.goBack(event);" + disabled="true"> + <observes element="Browser:Back" attribute="disabled"/> + </command> + <command id="Browser:ForwardOrForwardDuplicate" + oncommand="getPanelBrowser().webNavigation.goForward(event);" + disabled="true"> + <observes element="Browser:Forward" attribute="disabled"/> + </command> + <command id="Browser:ReloadOrDuplicate" + oncommand="PanelBrowserReload(event)" + disabled="true"> + <observes element="Browser:Reload" attribute="disabled"/> + </command> + </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/palemoon/base/content/win6BrowserOverlay.xul b/application/palemoon/base/content/win6BrowserOverlay.xul new file mode 100644 index 0000000000..a69e3f6bd7 --- /dev/null +++ b/application/palemoon/base/content/win6BrowserOverlay.xul @@ -0,0 +1,12 @@ +<?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/. --> + +<overlay id="win6-browser-overlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <toolbar id="toolbar-menubar" + autohide="true"/> +</overlay> |