diff options
Diffstat (limited to 'browser/base')
535 files changed, 76579 insertions, 0 deletions
diff --git a/browser/base/Makefile.in b/browser/base/Makefile.in new file mode 100644 index 000000000..285ef15f0 --- /dev/null +++ b/browser/base/Makefile.in @@ -0,0 +1,43 @@ +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +include $(topsrcdir)/config/config.mk + +abs_srcdir = $(call core_abspath,$(srcdir)) + +CHROME_DEPS += $(abs_srcdir)/content/overrides/app-license.html + +include $(topsrcdir)/config/rules.mk + +PRE_RELEASE_SUFFIX := "" + +DEFINES += \ + -DMOZ_APP_VERSION=$(MOZ_APP_VERSION) \ + -DAPP_LICENSE_BLOCK=$(abs_srcdir)/content/overrides/app-license.html \ + -DPRE_RELEASE_SUFFIX="$(PRE_RELEASE_SUFFIX)" \ + $(NULL) + +ifneq (,$(filter windows gtk2 gtk3 cocoa, $(MOZ_WIDGET_TOOLKIT))) +DEFINES += -DHAVE_SHELL_SERVICE=1 +endif + +ifneq (,$(filter windows cocoa gtk2 gtk3, $(MOZ_WIDGET_TOOLKIT))) +DEFINES += -DCONTEXT_COPY_IMAGE_CONTENTS=1 +endif + +ifneq (,$(filter windows cocoa, $(MOZ_WIDGET_TOOLKIT))) +DEFINES += -DCAN_DRAW_IN_TITLEBAR=1 +endif + +ifneq (,$(filter windows gtk2 gtk3, $(MOZ_WIDGET_TOOLKIT))) +DEFINES += -DMENUBAR_CAN_AUTOHIDE=1 +endif diff --git a/browser/base/content/aboutDialog.css b/browser/base/content/aboutDialog.css new file mode 100644 index 000000000..7145ef1c7 --- /dev/null +++ b/browser/base/content/aboutDialog.css @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; +} + +#currentChannel { + margin: 0; + padding: 0; + font-weight: bold; +} diff --git a/browser/base/content/aboutDialog.js b/browser/base/content/aboutDialog.js new file mode 100644 index 000000000..ea8350cbb --- /dev/null +++ b/browser/base/content/aboutDialog.js @@ -0,0 +1,608 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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"); + +const PREF_EM_HOTFIX_ID = "extensions.hotfix.id"; + +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"; + + // This must be set last because it might not exist due to bug 895473. + var distroAbout = Services.prefs.getComplexValue("distribution.about", + Components.interfaces.nsISupportsString); + var distroField = document.getElementById("distribution"); + distroField.value = distroAbout; + distroField.style.display = "block"; + } + } + catch (e) { + // Pref is unset + Components.utils.reportError(e); + } + + // Include the build ID and display warning if this is an "a#" (nightly or aurora) build + let version = Services.appinfo.version; + if (/a\d+$/.test(version)) { + let buildID = Services.appinfo.appBuildID; + let buildDate = buildID.slice(0,4) + "-" + buildID.slice(4,6) + "-" + buildID.slice(6,8); + document.getElementById("version").textContent += " (" + buildDate + ")"; + document.getElementById("experimental").hidden = false; + document.getElementById("communityDesc").hidden = true; + } + +#ifdef MOZ_UPDATER + gAppUpdater = new appUpdater(); + +#if MOZ_UPDATE_CHANNEL != release + let defaults = Services.prefs.getDefaultBranch(""); + let channelLabel = document.getElementById("currentChannel"); + channelLabel.value = defaults.getCharPref("app.update.channel"); +#endif +#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 +} + +#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 && (this.update.billboardURL || this.update.licenseURL || + this.addons.length != 0)) { + 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; + } + + // Firefox no longer displays a license for updates and the licenseURL + // check is just in case a distibution does. + if (gAppUpdater.update.billboardURL || gAppUpdater.update.licenseURL) { + gAppUpdater.selectPanel("updateButtonBox"); + gAppUpdater.setupUpdateButton("update.openUpdateUI." + + (this.isMajor ? "upgradeButton" + : "applyButton")); + return; + } + + if (!gAppUpdater.update.appVersion || + Services.vc.compare(gAppUpdater.update.appVersion, + Services.appinfo.version) == 0) { + gAppUpdater.startDownload(); + return; + } + + gAppUpdater.checkAddonCompatibility(); + }, + + /** + * 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() { + try { + var hotfixID = Services.prefs.getCharPref(PREF_EM_HOTFIX_ID); + } + catch (e) { } + + 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). The hotfix add-on is also ignored as it shouldn't + // block the user from upgrading. + try { + if (aAddon.type != "plugin" && aAddon.id != hotfixID && + !aAddon.appDisabled && !aAddon.userDisabled && + aAddon.scope != AddonManager.SCOPE_APPLICATION && + aAddon.isCompatible && + !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/browser/base/content/aboutDialog.xul b/browser/base/content/aboutDialog.xul new file mode 100644 index 000000000..5ce8212f4 --- /dev/null +++ b/browser/base/content/aboutDialog.xul @@ -0,0 +1,135 @@ +<?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_OS +#expand <label id="PMversion">Version: __MOZ_APP_VERSION__ (x64)</label> +#else +#expand <label id="PMversion">Version: __MOZ_APP_VERSION__ (x86)</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> + +#ifdef MOZ_UPDATER +# <description class="text-blurb" id="currentChannelText"> +# &channel.description.start;<label id="currentChannel"/>&channel.description.end; +# </description> +#endif +# <vbox id="experimental" hidden="true"> +# <description class="text-blurb" id="warningDesc"> +# &warningDesc.version; +#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT +# &warningDesc.telemetryDesc; +#endif +# </description> +# </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"> + Testers and moral support: Jose Arellano, Cole Hughes, Tobias Olsson, Duco Quanjer, Gerardo Rubio, Daryl Sprint, Jason Sullivan. + </description> + <description class="text-pmcreds"> + Special thanks to: Lee Brown, Jacob M. Ross, Phil Chan, Colin Moran, and all other supporters! + </description> + <description class="text-blurb"> + If you wish to contribute, please consider helping out by providing support to other users or becoming a beta tester on the <label class="text-link" href="http://forum.palemoon.org/">Pale Moon forum</label> + </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" href="http://www.palemoon.org/releasenotes-ng.shtml">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/browser/base/content/aboutRobots-icon.png b/browser/base/content/aboutRobots-icon.png Binary files differnew file mode 100644 index 000000000..1c4899aaf --- /dev/null +++ b/browser/base/content/aboutRobots-icon.png diff --git a/browser/base/content/aboutRobots-widget-left.png b/browser/base/content/aboutRobots-widget-left.png Binary files differnew file mode 100644 index 000000000..3a1e48d5f --- /dev/null +++ b/browser/base/content/aboutRobots-widget-left.png diff --git a/browser/base/content/aboutRobots.xhtml b/browser/base/content/aboutRobots.xhtml new file mode 100644 index 000000000..23fe3ba17 --- /dev/null +++ b/browser/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/browser/base/content/aboutSocialError.xhtml b/browser/base/content/aboutSocialError.xhtml new file mode 100644 index 000000000..6bef2d7bd --- /dev/null +++ b/browser/base/content/aboutSocialError.xhtml @@ -0,0 +1,124 @@ +<?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; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&loadError.label;</title> + <link rel="stylesheet" type="text/css" media="all" + href="chrome://browser/skin/aboutSocialError.css"/> + </head> + + <body> + <div id="error-box"> + <p id="main-error-msg"></p> + <p id="helper-error-msg"></p> + </div> + <div id="button-box"> + <button id="btnTryAgain" onclick="tryAgainButton()"/> + <button id="btnCloseSidebar" onclick="closeSidebarButton()"/> + </div> + </body> + + <script type="text/javascript;version=1.8"><![CDATA[ + const Cu = Components.utils; + + Cu.import("resource://gre/modules/Services.jsm"); + Cu.import("resource:///modules/Social.jsm"); + + let config = { + tryAgainCallback: reloadProvider + } + + function parseQueryString() { + let url = document.documentURI; + let queryString = url.replace(/^about:socialerror\??/, ""); + + let modeMatch = queryString.match(/mode=([^&]+)/); + let mode = modeMatch && modeMatch[1] ? modeMatch[1] : ""; + let originMatch = queryString.match(/origin=([^&]+)/); + config.origin = originMatch && originMatch[1] ? decodeURIComponent(originMatch[1]) : ""; + + switch (mode) { + case "compactInfo": + document.getElementById("btnTryAgain").style.display = 'none'; + document.getElementById("btnCloseSidebar").style.display = 'none'; + break; + case "tryAgainOnly": + document.getElementById("btnCloseSidebar").style.display = 'none'; + //intentional fall-through + case "tryAgain": + let urlMatch = queryString.match(/url=([^&]+)/); + let encodedURL = urlMatch && urlMatch[1] ? urlMatch[1] : ""; + let url = decodeURIComponent(encodedURL); + + config.tryAgainCallback = loadQueryURL; + config.queryURL = url; + break; + case "workerFailure": + config.tryAgainCallback = reloadProvider; + break; + default: + break; + } + } + + function setUpStrings() { + let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties"); + let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); + + let productName = brandBundle.GetStringFromName("brandShortName"); + let provider = Social && Social.provider; + if (config.origin) { + provider = Social && Social._getProviderFromOrigin(config.origin); + } + let providerName = provider && provider.name; + + // Sets up the error message + let msg = browserBundle.formatStringFromName("social.error.message", [productName, providerName], 2); + document.getElementById("main-error-msg").textContent = msg; + + // Sets up the buttons' labels and accesskeys + let btnTryAgain = document.getElementById("btnTryAgain"); + btnTryAgain.textContent = browserBundle.GetStringFromName("social.error.tryAgain.label"); + btnTryAgain.accessKey = browserBundle.GetStringFromName("social.error.tryAgain.accesskey"); + + let btnCloseSidebar = document.getElementById("btnCloseSidebar"); + btnCloseSidebar.textContent = browserBundle.GetStringFromName("social.error.closeSidebar.label"); + btnCloseSidebar.accessKey = browserBundle.GetStringFromName("social.error.closeSidebar.accesskey"); + } + + function closeSidebarButton() { + Social.toggleSidebar(); + } + + function tryAgainButton() { + config.tryAgainCallback(); + } + + function loadQueryURL() { + window.location.href = config.queryURL; + } + + function reloadProvider() { + Social.enabled = false; + Services.tm.mainThread.dispatch(function() { + Social.enabled = true; + }, Components.interfaces.nsIThread.DISPATCH_NORMAL); + } + + parseQueryString(); + setUpStrings(); + ]]></script> +</html> diff --git a/browser/base/content/abouthealthreport/abouthealth.css b/browser/base/content/abouthealthreport/abouthealth.css new file mode 100644 index 000000000..3dd40fc24 --- /dev/null +++ b/browser/base/content/abouthealthreport/abouthealth.css @@ -0,0 +1,15 @@ +* { + margin: 0; + padding: 0; +} + +html, body { + height: 100%; +} + +#remote-report { + width: 100%; + height: 100%; + border: 0; + display: flex; +} diff --git a/browser/base/content/abouthealthreport/abouthealth.js b/browser/base/content/abouthealthreport/abouthealth.js new file mode 100644 index 000000000..84c054bca --- /dev/null +++ b/browser/base/content/abouthealthreport/abouthealth.js @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const reporter = Cc["@mozilla.org/datareporting/service;1"] + .getService(Ci.nsISupports) + .wrappedJSObject + .healthReporter; + +const policy = Cc["@mozilla.org/datareporting/service;1"] + .getService(Ci.nsISupports) + .wrappedJSObject + .policy; + +const prefs = new Preferences("datareporting.healthreport."); + + +let healthReportWrapper = { + init: function () { + reporter.onInit().then(healthReportWrapper.refreshPayload, + healthReportWrapper.handleInitFailure); + + let iframe = document.getElementById("remote-report"); + iframe.addEventListener("load", healthReportWrapper.initRemotePage, false); + let report = this._getReportURI(); + iframe.src = report.spec; + prefs.observe("uploadEnabled", this.updatePrefState, healthReportWrapper); + }, + + uninit: function () { + prefs.ignore("uploadEnabled", this.updatePrefState, healthReportWrapper); + }, + + _getReportURI: function () { + let url = Services.urlFormatter.formatURLPref("datareporting.healthreport.about.reportUrl"); + return Services.io.newURI(url, null, null); + }, + + onOptIn: function () { + policy.recordHealthReportUploadEnabled(true, + "Health report page sent opt-in command."); + this.updatePrefState(); + }, + + onOptOut: function () { + policy.recordHealthReportUploadEnabled(false, + "Health report page sent opt-out command."); + this.updatePrefState(); + }, + + updatePrefState: function () { + try { + let prefs = { + enabled: policy.healthReportUploadEnabled, + } + this.injectData("prefs", prefs); + } catch (e) { + this.reportFailure(this.ERROR_PREFS_FAILED); + } + }, + + refreshPayload: function () { + reporter.collectAndObtainJSONPayload().then(healthReportWrapper.updatePayload, + healthReportWrapper.handlePayloadFailure); + }, + + updatePayload: function (data) { + healthReportWrapper.injectData("payload", data); + }, + + injectData: function (type, content) { + let report = this._getReportURI(); + + // file URIs can't be used for targetOrigin, so we use "*" for this special case + // in all other cases, pass in the URL to the report so we properly restrict the message dispatch + let reportUrl = report.scheme == "file" ? "*" : report.spec; + + let data = { + type: type, + content: content + } + + let iframe = document.getElementById("remote-report"); + iframe.contentWindow.postMessage(data, reportUrl); + }, + + handleRemoteCommand: function (evt) { + switch (evt.detail.command) { + case "DisableDataSubmission": + this.onOptOut(); + break; + case "EnableDataSubmission": + this.onOptIn(); + break; + case "RequestCurrentPrefs": + this.updatePrefState(); + break; + case "RequestCurrentPayload": + this.refreshPayload(); + break; + default: + Cu.reportError("Unexpected remote command received: " + evt.detail.command + ". Ignoring command."); + break; + } + }, + + initRemotePage: function () { + let iframe = document.getElementById("remote-report").contentDocument; + iframe.addEventListener("RemoteHealthReportCommand", + function onCommand(e) {healthReportWrapper.handleRemoteCommand(e);}, + false); + healthReportWrapper.updatePrefState(); + }, + + // error handling + ERROR_INIT_FAILED: 1, + ERROR_PAYLOAD_FAILED: 2, + ERROR_PREFS_FAILED: 3, + + reportFailure: function (error) { + let details = { + errorType: error, + } + healthReportWrapper.injectData("error", details); + }, + + handleInitFailure: function () { + healthReportWrapper.reportFailure(healthReportWrapper.ERROR_INIT_FAILED); + }, + + handlePayloadFailure: function () { + healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PAYLOAD_FAILED); + }, +} diff --git a/browser/base/content/abouthealthreport/abouthealth.xhtml b/browser/base/content/abouthealthreport/abouthealth.xhtml new file mode 100644 index 000000000..62b27e266 --- /dev/null +++ b/browser/base/content/abouthealthreport/abouthealth.xhtml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + %brandDTD; + <!ENTITY % securityPrefsDTD SYSTEM "chrome://browser/locale/preferences/security.dtd"> + %securityPrefsDTD; + <!ENTITY % aboutHealthReportDTD SYSTEM "chrome://browser/locale/aboutHealthReport.dtd"> + %aboutHealthReportDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <title>&abouthealth.pagetitle;</title> + <link rel="icon" type="image/png" id="favicon" + href="chrome://branding/content/icon32.png"/> + <link rel="stylesheet" + href="chrome://browser/content/abouthealthreport/abouthealth.css" + type="text/css" /> + <script type="text/javascript;version=1.8" + src="chrome://browser/content/abouthealthreport/abouthealth.js" /> + </head> + <body onload="healthReportWrapper.init();" + onunload="healthReportWrapper.uninit();"> + <iframe id="remote-report"/> + </body> +</html> + diff --git a/browser/base/content/abouthome/aboutHome.css b/browser/base/content/abouthome/aboutHome.css new file mode 100644 index 000000000..ce8db3cce --- /dev/null +++ b/browser/base/content/abouthome/aboutHome.css @@ -0,0 +1,431 @@ +%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, +#snippets { + 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; +} + +#defaultSnippet1, +#defaultSnippet2, +#rightsSnippet { + display: block; + min-height: 38px; + background: 30px center no-repeat; + padding: 6px 0; + -moz-padding-start: 79px; +} + +#rightsSnippet[hidden] { + display: none; +} + +#defaultSnippet1:-moz-dir(rtl), +#defaultSnippet2:-moz-dir(rtl), +#rightsSnippet:-moz-dir(rtl) { + background-position: right 30px center; +} + +#defaultSnippet1 { + background-image: url("chrome://browser/content/abouthome/snippet1.png"); +} + +#defaultSnippet2 { + background-image: url("chrome://browser/content/abouthome/snippet2.png"); +} + +#snippets { + display: inline-block; + text-align: start; + margin: 12px 0; + color: #3c3c3c; + font-size: 75%; + /* 12px is the computed font size, 15px the computed line height of the snippets + with Segoe UI on a default Windows 7 setup. The 15/12 multiplier approximately + converts em from units of font-size to units of line-height. The goal is to + preset the height of a three-line snippet to avoid visual moving/flickering as + the snippets load. */ + min-height: calc(15/12 * 3em); +} + +#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"); +} + +#apps::before { + content: url("chrome://browser/content/abouthome/apps.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; +} + +#aboutMozilla { + display: block; + position: relative; /* pin wordmark to edge of document, not of viewport */ + -moz-box-ordinal-group: 0; + opacity: .5; + transition: opacity 150ms; +} + +#aboutMozilla:hover { + opacity: 1; +} + +#aboutMozilla::before { + content: url("chrome://browser/content/abouthome/mozilla.png"); + display: block; + position: absolute; + top: 12px; + right: 12px; + width: 69px; + height: 19px; +} + +/* [HiDPI] + * At resolutions above 1dppx, prefer downscaling the 2x Retina graphics + * rather than upscaling the original-size ones (bug 818940). + */ +@media not all and (max-resolution: 1dppx) { + #brandLogo { + background-image: url("chrome://branding/content/about-logo@2x.png"); + } + + #defaultSnippet1, + #defaultSnippet2, + #rightsSnippet { + background-size: 40px; + } + + #defaultSnippet1 { + background-image: url("chrome://browser/content/abouthome/snippet1@2x.png"); + } + + #defaultSnippet2 { + background-image: url("chrome://browser/content/abouthome/snippet2@2x.png"); + } + + .launchButton::before, + #aboutMozilla::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"); + } + + #apps::before { + content: url("chrome://browser/content/abouthome/apps@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"); + } + + #aboutMozilla::before { + content: url("chrome://browser/content/abouthome/mozilla@2x.png"); + } +} + diff --git a/browser/base/content/abouthome/aboutHome.js b/browser/base/content/abouthome/aboutHome.js new file mode 100644 index 000000000..003755d22 --- /dev/null +++ b/browser/base/content/abouthome/aboutHome.js @@ -0,0 +1,565 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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==" + } +}; + +// The process of adding a new default snippet involves: +// * add a new entity to aboutHome.dtd +// * add a <span/> for it in aboutHome.xhtml +// * add an entry here in the proper ordering (based on spans) +// The <a/> part of the snippet will be linked to the corresponding url. +const DEFAULT_SNIPPETS_URLS = [ + "https://www.mozilla.org/firefox/features/?utm_source=snippet&utm_medium=snippet&utm_campaign=default+feature+snippet" +, "https://addons.mozilla.org/firefox/?utm_source=snippet&utm_medium=snippet&utm_campaign=addons" +]; + +const SNIPPETS_UPDATE_INTERVAL_MS = 86400000; // 1 Day. + +// 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) { + ensureSnippetsMapThen(loadSnippets); + 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); +}); + +// This object has the same interface as Map and is used to store and retrieve +// the snippets data. It is lazily initialized by ensureSnippetsMapThen(), so +// be sure its callback returned before trying to use it. +let gSnippetsMap; +let gSnippetsMapCallbacks = []; + +/** + * Ensure the snippets map is properly initialized. + * + * @param aCallback + * Invoked once the map has been initialized, gets the map as argument. + * @note Snippets should never directly manage the underlying storage, since + * it may change inadvertently. + */ +function ensureSnippetsMapThen(aCallback) +{ + if (gSnippetsMap) { + aCallback(gSnippetsMap); + return; + } + + // Handle multiple requests during the async initialization. + gSnippetsMapCallbacks.push(aCallback); + if (gSnippetsMapCallbacks.length > 1) { + // We are already updating, the callbacks will be invoked when done. + return; + } + + // TODO (bug 789348): use a real asynchronous storage here. This setTimeout + // is done just to catch bugs with the asynchronous behavior. + setTimeout(function() { + // Populate the cache from the persistent storage. + let cache = new Map(); + for (let key of [ "snippets-last-update", + "snippets-cached-version", + "snippets" ]) { + cache.set(key, localStorage[key]); + } + + gSnippetsMap = Object.freeze({ + get: function (aKey) cache.get(aKey), + set: function (aKey, aValue) { + localStorage[aKey] = aValue; + return cache.set(aKey, aValue); + }, + has: function(aKey) cache.has(aKey), + delete: function(aKey) { + delete localStorage[aKey]; + return cache.delete(aKey); + }, + clear: function() { + localStorage.clear(); + return cache.clear(); + }, + get size() cache.size + }); + + for (let callback of gSnippetsMapCallbacks) { + callback(gSnippetsMap); + } + gSnippetsMapCallbacks.length = 0; + }, 0); +} + +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; + } + +} + +/** + * Update the local snippets from the remote storage, then show them through + * showSnippets. + */ +function loadSnippets() +{ + if (!gSnippetsMap) + throw new Error("Snippets map has not properly been initialized"); + + // Check cached snippets version. + let cachedVersion = gSnippetsMap.get("snippets-cached-version") || 0; + let currentVersion = document.documentElement.getAttribute("snippetsVersion"); + if (cachedVersion < currentVersion) { + // The cached snippets are old and unsupported, restart from scratch. + gSnippetsMap.clear(); + } + + // Check last snippets update. + let lastUpdate = gSnippetsMap.get("snippets-last-update"); + let updateURL = document.documentElement.getAttribute("snippetsURL"); + let shouldUpdate = !lastUpdate || + Date.now() - lastUpdate > SNIPPETS_UPDATE_INTERVAL_MS; + if (updateURL && shouldUpdate) { + // Try to update from network. + let xhr = new XMLHttpRequest(); + try { + xhr.open("GET", updateURL, true); + } catch (ex) { + showSnippets(); + return; + } + // Even if fetching should fail we don't want to spam the server, thus + // set the last update time regardless its results. Will retry tomorrow. + gSnippetsMap.set("snippets-last-update", Date.now()); + xhr.onerror = function (event) { + showSnippets(); + }; + xhr.onload = function (event) + { + if (xhr.status == 200) { + gSnippetsMap.set("snippets", xhr.responseText); + gSnippetsMap.set("snippets-cached-version", currentVersion); + } + showSnippets(); + }; + xhr.send(null); + } else { + showSnippets(); + } +} + +/** + * Shows locally cached remote snippets, or default ones when not available. + * + * @note: snippets should never invoke showSnippets(), or they may cause + * a "too much recursion" exception. + */ +let _snippetsShown = false; +function showSnippets() +{ + let snippetsElt = document.getElementById("snippets"); + + // Show about:rights notification, if needed. + let showRights = document.documentElement.getAttribute("showKnowYourRights"); + if (showRights) { + let rightsElt = document.getElementById("rightsSnippet"); + let anchor = rightsElt.getElementsByTagName("a")[0]; + anchor.href = "about:rights"; + snippetsElt.appendChild(rightsElt); + rightsElt.removeAttribute("hidden"); + return; + } + + if (!gSnippetsMap) + throw new Error("Snippets map has not properly been initialized"); + if (_snippetsShown) { + // There's something wrong with the remote snippets, just in case fall back + // to the default snippets. + showDefaultSnippets(); + throw new Error("showSnippets should never be invoked multiple times"); + } + _snippetsShown = true; + + let snippets = gSnippetsMap.get("snippets"); + // If there are remotely fetched snippets, try to to show them. + if (snippets) { + // Injecting snippets can throw if they're invalid XML. + try { + snippetsElt.innerHTML = snippets; + // Scripts injected by innerHTML are inactive, so we have to relocate them + // through DOM manipulation to activate their contents. + Array.forEach(snippetsElt.getElementsByTagName("script"), function(elt) { + let relocatedScript = document.createElement("script"); + relocatedScript.type = "text/javascript;version=1.8"; + relocatedScript.text = elt.text; + elt.parentNode.replaceChild(relocatedScript, elt); + }); + return; + } catch (ex) { + // Bad content, continue to show default snippets. + } + } + + showDefaultSnippets(); +} + +/** + * Clear snippets element contents and show default snippets. + */ +function showDefaultSnippets() +{ + // Clear eventual contents... + let snippetsElt = document.getElementById("snippets"); + snippetsElt.innerHTML = ""; + + // ...then show default snippets. + let defaultSnippetsElt = document.getElementById("defaultSnippets"); + let entries = defaultSnippetsElt.querySelectorAll("span"); + // Choose a random snippet. Assume there is always at least one. + let randIndex = Math.floor(Math.random() * entries.length); + let entry = entries[randIndex]; + // Inject url in the eventual link. + if (DEFAULT_SNIPPETS_URLS[randIndex]) { + let links = entry.getElementsByTagName("a"); + // Default snippets can have only one link, otherwise something is messed + // up in the translation. + if (links.length == 1) { + links[0].href = DEFAULT_SNIPPETS_URLS[randIndex]; + } + } + // Move the default snippet to the snippets element. + snippetsElt.appendChild(entry); +} + +function fitToWidth() { + if (window.scrollMaxX) { + document.body.setAttribute("narrow", "true"); + } else if (document.body.hasAttribute("narrow")) { + document.body.removeAttribute("narrow"); + fitToWidth(); + } +} diff --git a/browser/base/content/abouthome/aboutHome.xhtml b/browser/base/content/abouthome/aboutHome.xhtml new file mode 100644 index 000000000..17ff83945 --- /dev/null +++ b/browser/base/content/abouthome/aboutHome.xhtml @@ -0,0 +1,72 @@ +<?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 id="snippetContainer"> +<!-- <div id="defaultSnippets" hidden="true"> + <span id="defaultSnippet1">&abouthome.defaultSnippet1.v1;</span> + <span id="defaultSnippet2">&abouthome.defaultSnippet2.v1;</span> + </div> + <span id="rightsSnippet" hidden="true">&abouthome.rightsSnippet;</span> + <div id="snippets"/> --> + </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="apps" hidden="true">&abouthome.appsButton.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> + +<!-- <a id="aboutMozilla" href="http://www.mozilla.org/about/"/> --> + </body> +</html> diff --git a/browser/base/content/abouthome/addons.png b/browser/base/content/abouthome/addons.png Binary files differnew file mode 100644 index 000000000..41519ce49 --- /dev/null +++ b/browser/base/content/abouthome/addons.png diff --git a/browser/base/content/abouthome/addons@2x.png b/browser/base/content/abouthome/addons@2x.png Binary files differnew file mode 100644 index 000000000..d4d04ee8c --- /dev/null +++ b/browser/base/content/abouthome/addons@2x.png diff --git a/browser/base/content/abouthome/apps.png b/browser/base/content/abouthome/apps.png Binary files differnew file mode 100644 index 000000000..79fc95d49 --- /dev/null +++ b/browser/base/content/abouthome/apps.png diff --git a/browser/base/content/abouthome/apps@2x.png b/browser/base/content/abouthome/apps@2x.png Binary files differnew file mode 100644 index 000000000..cbe7a6d53 --- /dev/null +++ b/browser/base/content/abouthome/apps@2x.png diff --git a/browser/base/content/abouthome/bookmarks.png b/browser/base/content/abouthome/bookmarks.png Binary files differnew file mode 100644 index 000000000..5c7e194a6 --- /dev/null +++ b/browser/base/content/abouthome/bookmarks.png diff --git a/browser/base/content/abouthome/bookmarks@2x.png b/browser/base/content/abouthome/bookmarks@2x.png Binary files differnew file mode 100644 index 000000000..7ede00744 --- /dev/null +++ b/browser/base/content/abouthome/bookmarks@2x.png diff --git a/browser/base/content/abouthome/downloads.png b/browser/base/content/abouthome/downloads.png Binary files differnew file mode 100644 index 000000000..3d4d10e7a --- /dev/null +++ b/browser/base/content/abouthome/downloads.png diff --git a/browser/base/content/abouthome/downloads@2x.png b/browser/base/content/abouthome/downloads@2x.png Binary files differnew file mode 100644 index 000000000..d384a22c6 --- /dev/null +++ b/browser/base/content/abouthome/downloads@2x.png diff --git a/browser/base/content/abouthome/history.png b/browser/base/content/abouthome/history.png Binary files differnew file mode 100644 index 000000000..ae742b1aa --- /dev/null +++ b/browser/base/content/abouthome/history.png diff --git a/browser/base/content/abouthome/history@2x.png b/browser/base/content/abouthome/history@2x.png Binary files differnew file mode 100644 index 000000000..696902e7c --- /dev/null +++ b/browser/base/content/abouthome/history@2x.png diff --git a/browser/base/content/abouthome/mozilla.png b/browser/base/content/abouthome/mozilla.png Binary files differnew file mode 100644 index 000000000..f2c348d13 --- /dev/null +++ b/browser/base/content/abouthome/mozilla.png diff --git a/browser/base/content/abouthome/mozilla@2x.png b/browser/base/content/abouthome/mozilla@2x.png Binary files differnew file mode 100644 index 000000000..f8fc622d0 --- /dev/null +++ b/browser/base/content/abouthome/mozilla@2x.png diff --git a/browser/base/content/abouthome/noise.png b/browser/base/content/abouthome/noise.png Binary files differnew file mode 100644 index 000000000..3467cf4d4 --- /dev/null +++ b/browser/base/content/abouthome/noise.png diff --git a/browser/base/content/abouthome/restore-large.png b/browser/base/content/abouthome/restore-large.png Binary files differnew file mode 100644 index 000000000..ef593e6e1 --- /dev/null +++ b/browser/base/content/abouthome/restore-large.png diff --git a/browser/base/content/abouthome/restore-large@2x.png b/browser/base/content/abouthome/restore-large@2x.png Binary files differnew file mode 100644 index 000000000..d5c71d0b0 --- /dev/null +++ b/browser/base/content/abouthome/restore-large@2x.png diff --git a/browser/base/content/abouthome/restore.png b/browser/base/content/abouthome/restore.png Binary files differnew file mode 100644 index 000000000..5c3d6f437 --- /dev/null +++ b/browser/base/content/abouthome/restore.png diff --git a/browser/base/content/abouthome/restore@2x.png b/browser/base/content/abouthome/restore@2x.png Binary files differnew file mode 100644 index 000000000..5acb63052 --- /dev/null +++ b/browser/base/content/abouthome/restore@2x.png diff --git a/browser/base/content/abouthome/settings.png b/browser/base/content/abouthome/settings.png Binary files differnew file mode 100644 index 000000000..4b0c30990 --- /dev/null +++ b/browser/base/content/abouthome/settings.png diff --git a/browser/base/content/abouthome/settings@2x.png b/browser/base/content/abouthome/settings@2x.png Binary files differnew file mode 100644 index 000000000..c77cb9a92 --- /dev/null +++ b/browser/base/content/abouthome/settings@2x.png diff --git a/browser/base/content/abouthome/snippet1.png b/browser/base/content/abouthome/snippet1.png Binary files differnew file mode 100644 index 000000000..ce2ec55c2 --- /dev/null +++ b/browser/base/content/abouthome/snippet1.png diff --git a/browser/base/content/abouthome/snippet1@2x.png b/browser/base/content/abouthome/snippet1@2x.png Binary files differnew file mode 100644 index 000000000..f57cd0a82 --- /dev/null +++ b/browser/base/content/abouthome/snippet1@2x.png diff --git a/browser/base/content/abouthome/snippet2.png b/browser/base/content/abouthome/snippet2.png Binary files differnew file mode 100644 index 000000000..e0724fb6d --- /dev/null +++ b/browser/base/content/abouthome/snippet2.png diff --git a/browser/base/content/abouthome/snippet2@2x.png b/browser/base/content/abouthome/snippet2@2x.png Binary files differnew file mode 100644 index 000000000..40577f52f --- /dev/null +++ b/browser/base/content/abouthome/snippet2@2x.png diff --git a/browser/base/content/abouthome/sync.png b/browser/base/content/abouthome/sync.png Binary files differnew file mode 100644 index 000000000..11e40cc93 --- /dev/null +++ b/browser/base/content/abouthome/sync.png diff --git a/browser/base/content/abouthome/sync@2x.png b/browser/base/content/abouthome/sync@2x.png Binary files differnew file mode 100644 index 000000000..6354f5bf9 --- /dev/null +++ b/browser/base/content/abouthome/sync@2x.png diff --git a/browser/base/content/baseMenuOverlay.xul b/browser/base/content/baseMenuOverlay.xul new file mode 100644 index 000000000..4b8a7fd11 --- /dev/null +++ b/browser/base/content/baseMenuOverlay.xul @@ -0,0 +1,107 @@ +<?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 +#ifdef MOZ_SERVICES_HEALTHREPORT + <menuitem id="healthReport" + label="&healthReport.label;" + accesskey="&healthReport.accesskey;" + oncommand="openHealthReport()" + onclick="checkForMiddleClick(this, event);"/> +#endif + <menuitem id="troubleShooting" + accesskey="&helpTroubleshootingInfo.accesskey;" + label="&helpTroubleshootingInfo.label;" + oncommand="openTroubleshootingPage()" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="feedbackPage" + accesskey="&helpFeedbackPage.accesskey;" + label="&helpFeedbackPage.label;" + oncommand="openFeedbackPage()" + onclick="checkForMiddleClick(this, event);"/> + <menuitem id="helpSafeMode" + accesskey="&helpSafeMode.accesskey;" + label="&helpSafeMode.label;" + oncommand="safeModeRestart();"/> + <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/browser/base/content/blockedSite.xhtml b/browser/base/content/blockedSite.xhtml new file mode 100644 index 000000000..b56875eb6 --- /dev/null +++ b/browser/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/browser/base/content/browser-addons.js b/browser/base/content/browser-addons.js new file mode 100644 index 000000000..b638f31f9 --- /dev/null +++ b/browser/base/content/browser-addons.js @@ -0,0 +1,417 @@ +# -*- 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 gXPInstallObserver = { + _findChildShell: function (aDocShell, aSoughtShell) + { + if (aDocShell == aSoughtShell) + return aDocShell; + + var node = aDocShell.QueryInterface(Components.interfaces.nsIDocShellTreeNode); + 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 win = installInfo.originatingWindow; + var shell = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsIDocShell); + var browser = this._getBrowser(shell); + if (!browser) + 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-blocked": + messageString = gNavigatorBundle.getFormattedString("xpinstallPromptWarning", + [brandShortName, installInfo.originatingURI.host]); + + let secHistogram = Components.classes["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry).getHistogramById("SECURITY_UI"); + action = { + label: gNavigatorBundle.getString("xpinstallPromptAllowButton"), + accessKey: gNavigatorBundle.getString("xpinstallPromptAllowButton.accesskey"), + callback: function() { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH); + installInfo.install(); + } + }; + + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED); + PopupNotifications.show(browser, notificationID, messageString, anchorID, + action, null, options); + 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.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); + } +} diff --git a/browser/base/content/browser-appmenu.inc b/browser/base/content/browser-appmenu.inc new file mode 100644 index 000000000..87276a537 --- /dev/null +++ b/browser/base/content/browser-appmenu.inc @@ -0,0 +1,400 @@ +# -*- 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"> + <splitmenu id="appmenu_newTab" + label="&tabCmd.label;" + command="cmd_newNavigatorTab"> + <menupopup> + <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"/> + <menuseparator/> + <menuitem id="appmenu_openFile" + label="&openFileCmd.label;" + command="Browser:OpenFile" + key="openFileKb"/> + </menupopup> + </splitmenu> + <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_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"> + <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_scratchpad" + observes="devtoolsMenuBroadcaster_Scratchpad"/> + <menuitem id="appmenu_pageSource" + observes="devtoolsMenuBroadcaster_PageSource"/> + <menuitem id="appmenu_errorConsole" + observes="devtoolsMenuBroadcaster_ErrorConsole"/> + <menuitem id="appmenu_devtools_connect" + observes="devtoolsMenuBroadcaster_connect"/> + <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"/> +#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 + <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" + label="&palemoon.menu.allBookmarks.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" + label="&appMenuUnsorted.label;" + oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');" + class="menuitem-iconic"/> + </menupopup> + </splitmenu> + <splitmenu id="appmenu_history" + iconic="true" + label="&historyMenu.label;" + command="Browser:ShowAllHistory"> + <menupopup id="appmenu_historyMenupopup" + placespopup="true" + 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" + label="&showAllHistoryCmd2.label;" + command="Browser:ShowAllHistory" + key="showAllHistoryKb"/> + <menuseparator/> + <menuitem id="appmenu_sanitizeHistory" + 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"/> + <splitmenu id="appmenu_customize" +#ifdef XP_UNIX + label="&preferencesCmdUnix.label;" +#else + label="&preferencesCmd2.label;" +#endif + oncommand="openPreferences();"> + <menupopup id="appmenu_customizeMenu" + onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('appmenu_toggleToolbarsSeparator'));"> + <menuitem id="appmenu_preferences" +#ifdef XP_UNIX + label="&preferencesCmdUnix.label;" +#else + label="&preferencesCmd2.label;" +#endif + 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_gettingStarted" + label="&appMenuGettingStarted.label;" + oncommand="gBrowser.loadOneTab('https://www.mozilla.org/firefox/central/', {inBackground: false});" + onclick="checkForMiddleClick(this, event);"/> +#ifdef MOZ_SERVICES_HEALTHREPORT + <menuitem id="appmenu_healthReport" + label="&healthReport.label;" + oncommand="openHealthReport()" + onclick="checkForMiddleClick(this, event);"/> +#endif + <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="safeModeRestart();"/> + <menuseparator/> + <menuitem id="appmenu_about" + label="&aboutProduct.label;" + oncommand="openAboutDialog();"/> + </menupopup> + </splitmenu> + </vbox> + </hbox> +</menupopup> diff --git a/browser/base/content/browser-charsetmenu.inc b/browser/base/content/browser-charsetmenu.inc new file mode 100644 index 000000000..c6ac63e3b --- /dev/null +++ b/browser/base/content/browser-charsetmenu.inc @@ -0,0 +1,145 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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 + datasources="rdf:charset-menu" + ref="NC:BrowserCharsetMenuRoot" + oncommand="MultiplexHandler(event)" + onpopupshowing="CreateMenu('browser'); CreateMenu('more-menu');" + onpopupshown="UpdateMenus(event);" + observes="isImage"> + <template> + <rule rdf:type="http://home.netscape.com/NC-rdf#BookmarkSeparator"> + <menupopup> + <menuseparator uri="..." /> + </menupopup> + </rule> + <rule> + <menupopup> + <menuitem type="radio" name="charsetGroup" checked="rdf:http://home.netscape.com/NC-rdf#Checked" uri="..." label="rdf:http://home.netscape.com/NC-rdf#Name"/> + </menupopup> + </rule> + </template> + + <menupopup> + <menu label="&charsetMenuAutodet.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuAutodet.accesskey;" +#endif + datasources="rdf:charset-menu" ref="NC:BrowserAutodetMenuRoot"> + <template> + <rule rdf:type="http://home.netscape.com/NC-rdf#CharsetDetector"> + <menupopup> + <menuitem type="radio" name="detectorGroup" checked="rdf:http://home.netscape.com/NC-rdf#Checked" uri="..." label="rdf:http://home.netscape.com/NC-rdf#Name"/> + </menupopup> + </rule> + </template> + <menupopup> + </menupopup> + </menu> + <menu label="&charsetMenuMore.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuMore.accesskey;" +#endif + datasources="rdf:charset-menu" ref="NC:BrowserMoreCharsetMenuRoot"> + <template> + <rule> + <menupopup> + <menuitem uri="..." label="rdf:http://home.netscape.com/NC-rdf#Name"/> + </menupopup> + </rule> + </template> + <menupopup> + <menu label="&charsetMenuMore1.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuMore1.accesskey;" +#endif + datasources="rdf:charset-menu" ref="NC:BrowserMore1CharsetMenuRoot"> + <template> + <rule> + <menupopup> + <menuitem uri="..." label="rdf:http://home.netscape.com/NC-rdf#Name"/> + </menupopup> + </rule> + </template> + <menupopup> + </menupopup> + </menu> + <menu label="&charsetMenuMore2.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuMore2.accesskey;" +#endif + datasources="rdf:charset-menu" ref="NC:BrowserMore2CharsetMenuRoot"> + <template> + <rule> + <menupopup> + <menuitem uri="..." label="rdf:http://home.netscape.com/NC-rdf#Name"/> + </menupopup> + </rule> + </template> + <menupopup> + </menupopup> + </menu> + <menu label="&charsetMenuMore3.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuMore3.accesskey;" +#endif + datasources="rdf:charset-menu" ref="NC:BrowserMore3CharsetMenuRoot"> + <template> + <rule> + <menupopup> + <menuitem uri="..." label="rdf:http://home.netscape.com/NC-rdf#Name"/> + </menupopup> + </rule> + </template> + <menupopup> + </menupopup> + </menu> + <menu label="&charsetMenuMore4.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuMore4.accesskey;" +#endif + datasources="rdf:charset-menu" ref="NC:BrowserMore4CharsetMenuRoot"> + <template> + <rule> + <menupopup> + <menuitem uri="..." label="rdf:http://home.netscape.com/NC-rdf#Name"/> + </menupopup> + </rule> + </template> + <menupopup> + </menupopup> + </menu> + <menu label="&charsetMenuMore5.label;" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetMenuMore5.accesskey;" +#endif + datasources="rdf:charset-menu" ref="NC:BrowserMore5CharsetMenuRoot"> + <template> + <rule> + <menupopup> + <menuitem uri="..." label="rdf:http://home.netscape.com/NC-rdf#Name"/> + </menupopup> + </rule> + </template> + <menupopup> + </menupopup> + </menu> + <menuseparator /> + </menupopup> + </menu> + <menuitem name="charsetCustomize" +#ifndef OMIT_ACCESSKEYS + accesskey="&charsetCustomize.accesskey;" +#endif + label="&charsetCustomize.label;" + oncommand="window.openDialog('chrome://global/content/customizeCharset.xul', 'PrefWindow', 'chrome,modal=yes,resizable=yes', 'browser');"/> + </menupopup> +</menu> diff --git a/browser/base/content/browser-context.inc b/browser/base/content/browser-context.inc new file mode 100644 index 000000000..90f045ead --- /dev/null +++ b/browser/base/content/browser-context.inc @@ -0,0 +1,403 @@ +# -*- 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-openlinkincurrent" + label="&openLinkCmdInCurrent.label;" + accesskey="&openLinkCmdInCurrent.accesskey;" + oncommand="gContextMenu.openLinkInCurrent();"/> + <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();"/> + <menuseparator id="context-sep-open"/> + <menuitem id="context-bookmarklink" + label="&bookmarkThisLinkCmd.label;" + accesskey="&bookmarkThisLinkCmd.accesskey;" + oncommand="gContextMenu.bookmarkLink();"/> + <menuitem id="context-marklink" + accesskey="&social.marklink.accesskey;" + oncommand="gContextMenu.markLink();"/> + <menuitem id="context-sharelink" + label="&shareLinkCmd.label;" + accesskey="&shareLinkCmd.accesskey;" + oncommand="gContextMenu.shareLink();"/> + <menuitem id="context-savelink" + label="&saveLinkCmd.label;" + accesskey="&saveLinkCmd.accesskey;" + oncommand="gContextMenu.saveLink();"/> + <menuitem id="context-sendlink" + label="&palemoon.sendLinkCmd.label;" + accesskey="&palemoon.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-shareimage" + label="&shareImageCmd.label;" + accesskey="&shareImageCmd.accesskey;" + oncommand="gContextMenu.shareImage();"/> + <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-sharevideo" + label="&shareVideoCmd.label;" + accesskey="&shareVideoCmd.accesskey;" + oncommand="gContextMenu.shareVideo();"/> + <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-markpage" + accesskey="&social.markpage.accesskey;" + command="Social:TogglePageMark"/> + <menuitem id="context-sharepage" + label="&sharePageCmd.label;" + accesskey="&sharePageCmd.accesskey;" + oncommand="SocialShare.sharePage();"/> + <menuitem id="context-savepage" + label="&savePageCmd.label;" + accesskey="&savePageCmd.accesskey2;" + oncommand="gContextMenu.savePageAs();"/> + <menuitem id="context-sendpage" + label="&palemoon.sendPageCmd.label;" + accesskey="&palemoon.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(getBrowserSelection());"/> + <menuitem id="context-shareselect" + label="&shareSelectCmd.label;" + accesskey="&shareSelectCmd.accesskey;" + oncommand="gContextMenu.shareSelect(getBrowserSelection());"/> + <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/> + <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();"/> + <menuseparator id="inspect-separator" hidden="true"/> + <menuitem id="context-inspect" + hidden="true" + label="&inspectContextMenu.label;" + accesskey="&inspectContextMenu.accesskey;" + oncommand="gContextMenu.inspectNode();"/> diff --git a/browser/base/content/browser-data-submission-info-bar.js b/browser/base/content/browser-data-submission-info-bar.js new file mode 100644 index 000000000..f58334e76 --- /dev/null +++ b/browser/base/content/browser-data-submission-info-bar.js @@ -0,0 +1,136 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/** + * Represents an info bar that shows a data submission notification. + */ +let gDataNotificationInfoBar = { + _OBSERVERS: [ + "datareporting:notify-data-policy:request", + "datareporting:notify-data-policy:close", + ], + + _DATA_REPORTING_NOTIFICATION: "data-reporting", + + get _notificationBox() { + delete this._notificationBox; + return this._notificationBox = document.getElementById("global-notificationbox"); + }, + + get _log() { + let log4moz = Cu.import("resource://services-common/log4moz.js", {}).Log4Moz; + delete this._log; + return this._log = log4moz.repository.getLogger("Services.DataReporting.InfoBar"); + }, + + init: function() { + window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload, false); + + for (let o of this._OBSERVERS) { + Services.obs.removeObserver(this, o); + } + }.bind(this), false); + + for (let o of this._OBSERVERS) { + Services.obs.addObserver(this, o, true); + } + }, + + _getDataReportingNotification: function (name=this._DATA_REPORTING_NOTIFICATION) { + return this._notificationBox.getNotificationWithValue(name); + }, + + _displayDataPolicyInfoBar: function (request) { + if (this._getDataReportingNotification()) { + return; + } + + let brandBundle = document.getElementById("bundle_brand"); + let appName = brandBundle.getString("brandShortName"); + let vendorName = brandBundle.getString("vendorShortName"); + + let message = gNavigatorBundle.getFormattedString( + "dataReportingNotification.message", + [appName, vendorName]); + + this._actionTaken = false; + + let buttons = [{ + label: gNavigatorBundle.getString("dataReportingNotification.button.label"), + accessKey: gNavigatorBundle.getString("dataReportingNotification.button.accessKey"), + popup: null, + callback: function () { + // Clicking the button to go to the preferences tab constitutes + // acceptance of the data upload policy for Firefox Health Report. + // This will ensure the checkbox is checked. The user has the option of + // unchecking it. + request.onUserAccept("info-bar-button-pressed"); + this._actionTaken = true; + window.openAdvancedPreferences("dataChoicesTab"); + }.bind(this), + }]; + + this._log.info("Creating data reporting policy notification."); + let notification = this._notificationBox.appendNotification( + message, + this._DATA_REPORTING_NOTIFICATION, + null, + this._notificationBox.PRIORITY_INFO_HIGH, + buttons, + function onEvent(event) { + if (event == "removed") { + if (!this._actionTaken) { + request.onUserAccept("info-bar-dismissed"); + } + + Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close", null); + } + }.bind(this) + ); + + // Tell the notification request we have displayed the notification. + request.onUserNotifyComplete(); + }, + + _clearPolicyNotification: function () { + let notification = this._getDataReportingNotification(); + if (notification) { + this._log.debug("Closing notification."); + notification.close(); + } + }, + + onNotifyDataPolicy: function (request) { + try { + this._displayDataPolicyInfoBar(request); + } catch (ex) { + request.onUserNotifyFailed(ex); + } + }, + + observe: function(subject, topic, data) { + switch (topic) { + case "datareporting:notify-data-policy:request": + this.onNotifyDataPolicy(subject.wrappedJSObject.object); + break; + + case "datareporting:notify-data-policy:close": + // If this observer fires, it means something else took care of + // responding. Therefore, we don't need to do anything. So, we + // act like we took action and clear state. + this._actionTaken = true; + this._clearPolicyNotification(); + break; + + default: + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIObserver, + Ci.nsISupportsWeakReference, + ]), +}; + diff --git a/browser/base/content/browser-doctype.inc b/browser/base/content/browser-doctype.inc new file mode 100644 index 000000000..ddfe0dcef --- /dev/null +++ b/browser/base/content/browser-doctype.inc @@ -0,0 +1,25 @@ +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" > +%browserDTD; +<!ENTITY % palemoonDTD SYSTEM "chrome://browser/locale/palemoon.dtd" > +%palemoonDTD; +<!ENTITY % baseMenuDTD SYSTEM "chrome://browser/locale/baseMenuOverlay.dtd" > +%baseMenuDTD; +<!ENTITY % charsetDTD SYSTEM "chrome://global/locale/charsetOverlay.dtd" > +%charsetDTD; +<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" > +%textcontextDTD; +<!ENTITY % customizeToolbarDTD SYSTEM "chrome://global/locale/customizeToolbar.dtd"> + %customizeToolbarDTD; +<!ENTITY % placesDTD SYSTEM "chrome://browser/locale/places/places.dtd"> +%placesDTD; +#ifdef MOZ_SAFE_BROWSING +<!ENTITY % safebrowsingDTD SYSTEM "chrome://browser/locale/safebrowsing/phishing-afterload-warning-message.dtd"> +%safebrowsingDTD; +#endif +<!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd"> +%aboutHomeDTD; +]> + diff --git a/browser/base/content/browser-feeds.js b/browser/base/content/browser-feeds.js new file mode 100644 index 000000000..c67877269 --- /dev/null +++ b/browser/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/browser/base/content/browser-fullScreen.js b/browser/base/content/browser-fullScreen.js new file mode 100644 index 000000000..26f1d9551 --- /dev/null +++ b/browser/base/content/browser-fullScreen.js @@ -0,0 +1,605 @@ +# -*- 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; + + 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; + } + + // Ensure the sidebar is hidden. + if (!document.getElementById("sidebar-box").hidden) + toggleSidebar(); + + 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 Gecko 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 Gecko 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; + } +}; +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/browser/base/content/browser-fullZoom.js b/browser/base/content/browser-fullZoom.js new file mode 100644 index 000000000..4f0af5663 --- /dev/null +++ b/browser/base/content/browser-fullZoom.js @@ -0,0 +1,557 @@ +/* +#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 browser outer window IDs to monotonically increasing integer + // tokens. _browserTokenMap[outerID] is increased each time the zoom is + // changed in the browser whose outer window ID is outerID. See + // _getBrowserToken and _ignorePendingZoomAccesses. + _browserTokenMap: new Map(), + + 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() { + // Bug 691614 - zooming support for electrolysis + if (gMultiProcessBrowser) + return; + + // 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); + + Services.obs.addObserver(this, "outer-window-destroyed", false); + }, + + destroy: function FullZoom_destroy() { + // Bug 691614 - zooming support for electrolysis + if (gMultiProcessBrowser) + return; + + gPrefService.removeObserver("browser.zoom.", this); + this._cps2.removeObserverForName(this.name, this); + window.removeEventListener("DOMMouseScroll", this, false); + Services.obs.removeObserver(this, "outer-window-destroyed"); + }, + + + //**************************************************************************// + // 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; + case "outer-window-destroyed": + let outerID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; + this._browserTokenMap.delete(outerID); + 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._loadContextFromWindow(browser.contentWindow); + 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) { + // 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. + let browser = aBrowser || gBrowser.selectedBrowser; + 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.contentDocument.mozSyntheticDocument) { + 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._loadContextFromWindow(browser.contentWindow); + 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.contentWindow, 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; + } + + // aBrowser.contentDocument is sometimes gone 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.contentDocument || + aBrowser.contentDocument.mozSyntheticDocument) { + 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.contentWindow, 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.contentDocument.mozSyntheticDocument) + return; + + this._cps2.set(browser.currentURI.spec, this.name, + ZoomManager.getZoomForBrowser(browser), + this._loadContextFromWindow(browser.contentWindow), { + 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.contentDocument.mozSyntheticDocument) + return; + let ctxt = this._loadContextFromWindow(browser.contentWindow); + 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 outerID = this._browserOuterID(browser); + let map = this._browserTokenMap; + if (!map.has(outerID)) + map.set(outerID, 0); + return { + token: map.get(outerID), + 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(outerID) === this.token && browser.docShell; + }, + }; + }, + + /** + * 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 outerID = this._browserOuterID(browser); + let map = this._browserTokenMap; + map.set(outerID, (map.get(outerID) || 0) + 1); + }, + + _browserOuterID: function FullZoom__browserOuterID(browser) { + return browser. + contentWindow. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils). + outerWindowID; + }, + + _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 window The content window 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(window, 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._loadContextFromWindow(window), { + 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 window. + * + * @param window The window whose load context will be returned. + * @return The nsILoadContext of the given window. + */ + _loadContextFromWindow: function FullZoom__loadContextFromWindow(window) { + return window. + QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsILoadContext); + }, + + /** + * 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/browser/base/content/browser-gestureSupport.js b/browser/base/content/browser-gestureSupport.js new file mode 100644 index 000000000..d88f47c79 --- /dev/null +++ b/browser/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/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc new file mode 100644 index 000000000..891e9e82a --- /dev/null +++ b/browser/base/content/browser-menubar.inc @@ -0,0 +1,623 @@ +# -*- 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_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"/> +#ifdef XP_UNIX +#ifndef XP_MACOSX + <menuseparator/> + <menuitem id="menu_preferences" + label="&preferencesCmdUnix.label;" + accesskey="&preferencesCmdUnix.accesskey;" + oncommand="openPreferences();"/> +#endif +#endif + </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"/> + <menuitem id="menu_historySidebar" + key="key_gotoHistory" + observes="viewHistorySidebar" + label="&historyButton.label;"/> + <menuitem id="menu_socialSidebar" + type="checkbox" + autocheck="false" + command="Social:ToggleSidebar"/> + </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 + 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 + key="showAllHistoryKb" +#endif + command="Browser:ShowAllHistory"/> + <menuitem id="sanitizeItem" + 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" + label="&palemoon.menu.allBookmarks.label;" + command="Browser:ShowAllBookmarks" + key="manBookmarkKb"/> + <menuseparator id="organizeBookmarksSeparator"/> + <menuitem id="menu_bookmarkThisPage" + 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" + 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"/> + <menu id="menu_socialAmbientMenu" + observes="socialActiveBroadcaster"> + <menupopup id="menu_social-statusarea-popup"> + <menuitem class="social-statusarea-user menuitem-iconic" pack="start" align="center" + observes="socialBroadcaster_userDetails" + oncommand="SocialUI.showProfile(); document.getElementById('social-statusarea-popup').hidePopup();"> + <image class="social-statusarea-user-portrait" + observes="socialBroadcaster_userDetails"/> + <vbox> + <label class="social-statusarea-loggedInStatus" + observes="socialBroadcaster_userDetails"/> + </vbox> + </menuitem> +#ifndef XP_WIN + <menuseparator class="social-statusarea-separator"/> +#endif + <menuseparator id="socialAmbientMenuSeparator" + hidden="true"/> + <menuitem class="social-toggle-sidebar-menuitem" + type="checkbox" + autocheck="false" + command="Social:ToggleSidebar" + label="&social.toggleSidebar.label;" + accesskey="&social.toggleSidebar.accesskey;"/> + <menuitem class="social-toggle-notifications-menuitem" + type="checkbox" + autocheck="false" + command="Social:ToggleNotifications" + label="&social.toggleNotifications.label;" + accesskey="&social.toggleNotifications.accesskey;"/> + <menuitem id="menu_focusChatBar" + label="&social.chatBar.label;" + accesskey="&social.chatBar.accesskey;" + key="focusChatBar" + command="Social:FocusChat" + class="show-only-for-keyboard"/> + <menuitem class="social-toggle-menuitem" command="Social:Toggle"/> + <menuseparator class="social-statusarea-separator"/> + <menuseparator class="social-provider-menu" hidden="true"/> + <menuitem class="social-addons-menuitem" command="Social:Addons" + label="&social.addons.label;"/> + </menupopup> + </menu> +#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"> + <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_scratchpad" + observes="devtoolsMenuBroadcaster_Scratchpad" + accesskey="&scratchpad.accesskey;"/> + <menuitem id="menu_pageSource" + observes="devtoolsMenuBroadcaster_PageSource" + accesskey="&pageSourceCmd.accesskey;"/> + <menuitem id="javascriptConsole" + observes="devtoolsMenuBroadcaster_ErrorConsole" + accesskey="&errorConsoleCmd.accesskey;"/> + <menuitem id="menu_devtools_connect" + observes="devtoolsMenuBroadcaster_connect"/> + <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"/> +#ifndef XP_UNIX + <menuseparator id="prefSep"/> + <menuitem id="menu_preferences" + label="&preferencesCmd2.label;" + accesskey="&preferencesCmd2.accesskey;" + oncommand="openPreferences();"/> +#endif + </menupopup> + </menu> + +#ifdef XP_MACOSX + <menu id="windowMenu" /> +#endif + <menu id="helpMenu" /> + </menubar> diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js new file mode 100644 index 000000000..d475000e9 --- /dev/null +++ b/browser/base/content/browser-places.js @@ -0,0 +1,1286 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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) { + var feedURI = makeURI(url); + + var doc = gBrowser.contentDocument; + var title = (arguments.length > 1) ? feedTitle : doc.title; + + var description; + if (arguments.length > 2) + description = feedSubtitle; + else + description = PlacesUIUtils.getDescriptionFromDocument(doc); + + var toolbarIP = new InsertionPoint(PlacesUtils.toolbarFolderId, -1); + 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"); + if (!organizer) { + // 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 = { + 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.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 row = {}, column = {}; + var tbo = tree.treeBoxObject; + tbo.getCellAt(aEvent.clientX, aEvent.clientY, row, column, {}); + if (row.value == -1) + return false; + node = tree.view.nodeForTreeIndex(row.value); + cropped = tbo.isCellCropped(row.value, column.value); + } + 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(); + } + }, + + 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; + } + }, + + updateStarState: function BUI_updateStarState() { + if (!this.star || (this._uri && gBrowser.currentURI.equals(this._uri))) { + return; + } + + // 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, function (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/browser/base/content/browser-plugins.js b/browser/base/content/browser-plugins.js new file mode 100644 index 000000000..02c299d39 --- /dev/null +++ b/browser/base/content/browser-plugins.js @@ -0,0 +1,1053 @@ +# -*- 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 kPrefNotifyMissingFlash = "plugins.notifyMissingFlash"; +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, className) { + return plugin.ownerDocument. + getAnonymousElementByAttribute(plugin, "class", className); + }, + +#ifdef MOZ_CRASHREPORTER + get CrashSubmit() { + delete this.CrashSubmit; + Cu.import("resource://gre/modules/CrashSubmit.jsm", this); + return this.CrashSubmit; + }, +#endif + + _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. + let overflows = (overlay.scrollWidth > pluginRect.width) || + (overlay.scrollHeight - 5 > 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; + } + }, + + supportedPlugins: { + "mimetypes": { + "application/x-shockwave-flash": "flash", + "application/futuresplash": "flash", + "application/x-java-.*": "java", + "application/x-director": "shockwave", + "application/(sdp|x-(mpeg|rtsp|sdp))": "quicktime", + "audio/(3gpp(2)?|AMR|aiff|basic|mid(i)?|mp4|mpeg|vnd\.qcelp|wav|x-(aiff|m4(a|b|p)|midi|mpeg|wav))": "quicktime", + "image/(pict|png|tiff|x-(macpaint|pict|png|quicktime|sgi|targa|tiff))": "quicktime", + "video/(3gpp(2)?|flc|mp4|mpeg|quicktime|sd-video|x-mpeg)": "quicktime", + "application/x-unknown": "test", + }, + + "plugins": { + "flash": { + "displayName": "Flash", + "installWINNT": true, + "installDarwin": true, + "installLinux": true, + }, + "java": { + "displayName": "Java", + "installWINNT": true, + "installDarwin": true, + "installLinux": true, + }, + "shockwave": { + "displayName": "Shockwave", + "installWINNT": true, + "installDarwin": true, + }, + "quicktime": { + "displayName": "QuickTime", + "installWINNT": true, + }, + "test": { + "displayName": "Test plugin", + "installWINNT": true, + "installLinux": true, + "installDarwin": true, + } + } + }, + + nameForSupportedPlugin: function (aMimeType) { + for (let type in this.supportedPlugins.mimetypes) { + let re = new RegExp(type); + if (re.test(aMimeType)) { + return this.supportedPlugins.mimetypes[type]; + } + } + return null; + }, + + canInstallThisMimeType: function (aMimeType) { + let os = Services.appinfo.OS; + let pluginName = this.nameForSupportedPlugin(aMimeType); + if (pluginName && "install" + os in this.supportedPlugins.plugins[pluginName]) { + return true; + } + return false; + }, + + 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 = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + 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); + + switch (eventType) { + case "PluginCrashed": + this.pluginInstanceCrashed(plugin, event); + break; + + case "PluginNotFound": + let installable = this.showInstallNotification(plugin, eventType); + // For non-object plugin tags, register a click handler to install the + // plugin. Object tags can, and often do, deal with that themselves, + // so don't stomp on the page developers toes. + if (installable && !(plugin instanceof HTMLObjectElement)) { + let installStatus = doc.getAnonymousElementByAttribute(plugin, "class", "installStatus"); + installStatus.setAttribute("installable", "true"); + let iconStatus = doc.getAnonymousElementByAttribute(plugin, "class", "icon"); + iconStatus.setAttribute("installable", "true"); + + let installLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "installPluginLink"); + this.addLinkClickCallback(installLink, "installSinglePlugin", plugin); + } + break; + + case "PluginBlocklisted": + case "PluginOutdated": + shouldShowNotification = true; + break; + + case "PluginVulnerableUpdatable": + let updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink"); + this.addLinkClickCallback(updateLink, "openPluginUpdatePage"); + /* FALLTHRU */ + + case "PluginVulnerableNoUpdate": + case "PluginClickToPlay": + this._handleClickToPlayEvent(plugin); + let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + let pluginName = this._getPluginInfo(plugin).pluginName; + let messageString = gNavigatorBundle.getFormattedString("PluginClickToActivate", [pluginName]); + let overlayText = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgClickToPlay"); + overlayText.textContent = messageString; + if (eventType == "PluginVulnerableUpdatable" || + eventType == "PluginVulnerableNoUpdate") { + let vulnerabilityString = gNavigatorBundle.getString(eventType); + let vulnerabilityText = doc.getAnonymousElementByAttribute(plugin, "anonid", "vulnerabilityStatus"); + vulnerabilityText.textContent = vulnerabilityString; + } + shouldShowNotification = true; + break; + + case "PluginPlayPreview": + this._handlePlayPreviewEvent(plugin); + break; + + case "PluginDisabled": + let manageLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "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; + } + + // Hide the in-content UI if it's too big. The crashed plugin handler already did this. + if (eventType != "PluginCrashed" && eventType != "PluginRemoved") { + let overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + if (overlay != null && this.isTooSmall(plugin, overlay)) + overlay.style.visibility = "hidden"; + } + + // 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 = aPlugin.ownerDocument.getAnonymousElementByAttribute(aPlugin, "class", "mainBox"); + 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(); + }, + + newPluginInstalled : function(event) { + // browser elements are anonymous so we can't just use target. + var browser = event.originalTarget; + // clear the plugin list, now that at least one plugin has been installed + browser.missingPlugins = null; + + var notificationBox = gBrowser.getNotificationBox(browser); + var notification = notificationBox.getNotificationWithValue("missing-plugins"); + if (notification) + notificationBox.removeNotification(notification); + + // reload the browser to make the new plugin show. + browser.reload(); + }, + + // Callback for user clicking on a missing (unsupported) plugin. + installSinglePlugin: function (plugin) { + var missingPlugins = new Map(); + + var pluginInfo = this._getPluginInfo(plugin); + missingPlugins.set(pluginInfo.mimetype, pluginInfo); + + openDialog("chrome://mozapps/content/plugins/pluginInstallerWizard.xul", + "PFSWindow", "chrome,centerscreen,resizable=yes", + {plugins: missingPlugins, browser: gBrowser.selectedBrowser}); + }, + + // 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")); + }, + +#ifdef MOZ_CRASHREPORTER + submitReport: function submitReport(pluginDumpID, browserDumpID, plugin) { + let keyVals = {}; + if (plugin) { + let userComment = this.getPluginUI(plugin, "submitComment").value.trim(); + if (userComment) + keyVals.PluginUserComment = userComment; + if (this.getPluginUI(plugin, "submitURLOptIn").checked) + keyVals.PluginContentURL = plugin.ownerDocument.URL; + } + this.CrashSubmit.submit(pluginDumpID, { extraExtraKeyVals: keyVals }); + if (browserDumpID) + this.CrashSubmit.submit(browserDumpID); + }, +#endif + + // 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); + }, + + showInstallNotification: function (aPlugin) { + let browser = gBrowser.getBrowserForDocument(aPlugin.ownerDocument + .defaultView.top.document); + if (!browser.missingPlugins) + browser.missingPlugins = new Map(); + + let pluginInfo = this._getPluginInfo(aPlugin); + browser.missingPlugins.set(pluginInfo.mimetype, pluginInfo); + + // only show notification for small subset of plugins + let mimetype = pluginInfo.mimetype.split(";")[0]; + if (!this.canInstallThisMimeType(mimetype)) + return false; + + let pluginIdentifier = this.nameForSupportedPlugin(mimetype); + if (!pluginIdentifier) + return false; + + let displayName = this.supportedPlugins.plugins[pluginIdentifier].displayName; + + // don't show several notifications + let notification = PopupNotifications.getNotification("plugins-not-found", browser); + if (notification) + return true; + + let messageString = gNavigatorBundle.getString("installPlugin.message"); + let mainAction = { + label: gNavigatorBundle.getFormattedString("installPlugin.button.label", + [displayName]), + accessKey: gNavigatorBundle.getString("installPlugin.button.accesskey"), + callback: function () { + openDialog("chrome://mozapps/content/plugins/pluginInstallerWizard.xul", + "PFSWindow", "chrome,centerscreen,resizable=yes", + {plugins: browser.missingPlugins, browser: browser}); + } + }; + let secondaryActions = null; + let options = { dismissed: true }; + + let showForFlash = Services.prefs.getBoolPref(kPrefNotifyMissingFlash); + if (pluginIdentifier == "flash" && showForFlash) { + secondaryActions = [{ + label: gNavigatorBundle.getString("installPlugin.ignoreButton.label"), + accessKey: gNavigatorBundle.getString("installPlugin.ignoreButton.accesskey"), + callback: function () { + Services.prefs.setBoolPref(kPrefNotifyMissingFlash, false); + } + }]; + options.dismissed = false; + } + PopupNotifications.show(browser, "plugins-not-found", + messageString, "plugin-install-notification-icon", + mainAction, secondaryActions, options); + return true; + }, + // 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 = doc.getAnonymousElementByAttribute(aPlugin, "class", "mainBox"); + + if (pluginPermission == Ci.nsIPermissionManager.DENY_ACTION) { + if (overlay) + overlay.style.visibility = "hidden"; + return; + } + + if (overlay) { + overlay.addEventListener("click", gPluginHandler._overlayClickListener, true); + let closeIcon = doc.getAnonymousElementByAttribute(aPlugin, "anonid", "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 = doc.getAnonymousElementByAttribute(aPlugin, "class", "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, "class", "mainBox"); + 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, "class", "mainBox"); + 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; + +#ifdef MOZ_CRASHREPORTER + let pluginDumpID = propertyBag.getPropertyAsAString("pluginDumpID"); + let browserDumpID= propertyBag.getPropertyAsAString("browserDumpID"); + let shouldSubmit = gCrashReporter.submitReports; + let doPrompt = true; // XXX followup to get via gCrashReporter + + // Submit automatically when appropriate. + if (pluginDumpID && shouldSubmit && !doPrompt) { + this.submitReport(pluginDumpID, browserDumpID); + // Submission is async, so we can't easily show failure UI. + propertyBag.setPropertyAsBool("submittedCrashReport", true); + } +#endif + }, + + // 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"); +#ifdef MOZ_CRASHREPORTER + let status; + + // Determine which message to show regarding crash reports. + if (submittedReport) { // submitReports && !doPrompt, handled in observer + status = "submitted"; + } + else if (!submitReports && !doPrompt) { + status = "noSubmit"; + } + else { // doPrompt + status = "please"; + this.getPluginUI(plugin, "submitButton").addEventListener("click", + function (event) { + if (event.button != 0 || !event.isTrusted) + return; + this.submitReport(pluginDumpID, browserDumpID, plugin); + pref.setBoolPref("", optInCB.checked); + }.bind(this)); + let optInCB = this.getPluginUI(plugin, "submitURLOptIn"); + let pref = Services.prefs.getBranch("dom.ipc.plugins.reportCrashURL"); + optInCB.checked = pref.getBoolPref(""); + } + + // If we don't have a minidumpID, we can't (or didn't) submit anything. + // This can happen if the plugin is killed from the task manager. + if (!pluginDumpID) { + status = "noReport"; + } + + statusDiv.setAttribute("status", status); + + let helpIcon = doc.getAnonymousElementByAttribute(plugin, "class", "helpIcon"); + this.addLinkClickCallback(helpIcon, "openHelpPage"); + + // If we're showing the link to manually trigger report submission, we'll + // want to be able to update all the instances of the UI for this crash to + // show an updated message when a report is submitted. + if (doPrompt) { + let observer = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + observe : function(subject, topic, data) { + let propertyBag = subject; + if (!(propertyBag instanceof Ci.nsIPropertyBag2)) + return; + // Ignore notifications for other crashes. + if (propertyBag.get("minidumpID") != pluginDumpID) + return; + statusDiv.setAttribute("status", data); + }, + + handleEvent : function(event) { + // Not expected to be called, just here for the closure. + } + } + + // Use a weak reference, so we don't have to remove it... + Services.obs.addObserver(observer, "crash-report-status", true); + // ...alas, now we need something to hold a strong reference to prevent + // it from being GC. But I don't want to manually manage the reference's + // lifetime (which should be no greater than the page). + // Clever solution? Use a closue with an event listener on the document. + // When the doc goes away, so do the listener references and the closure. + doc.addEventListener("mozCleverClosureHack", observer, false); + } +#endif + + 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(); }, + }]; +#ifdef MOZ_CRASHREPORTER + let submitButton = { + label: submitLabel, + accessKey: submitKey, + popup: null, + callback: function() { gPluginHandler.submitReport(pluginDumpID, browserDumpID); }, + }; + if (pluginDumpID) + buttons.push(submitButton); +#endif + + let 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/browser/base/content/browser-safebrowsing.js b/browser/base/content/browser-safebrowsing.js new file mode 100644 index 000000000..e40a31957 --- /dev/null +++ b/browser/base/content/browser-safebrowsing.js @@ -0,0 +1,53 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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 MOZ_SAFE_BROWSING +var gSafeBrowsing = { + + setReportPhishingMenu: function() { + + // A phishing page will have a specific about:blocked content documentURI + var isPhishingPage = content.document.documentURI.startsWith("about:blocked?e=phishingBlocked"); + + // Show/hide the appropriate menu item. + document.getElementById("menu_HelpPopup_reportPhishingtoolmenu") + .hidden = isPhishingPage; + document.getElementById("menu_HelpPopup_reportPhishingErrortoolmenu") + .hidden = !isPhishingPage; + + var broadcasterId = isPhishingPage + ? "reportPhishingErrorBroadcaster" + : "reportPhishingBroadcaster"; + + var broadcaster = document.getElementById(broadcasterId); + if (!broadcaster) + return; + + var uri = getBrowser().currentURI; + if (uri && (uri.schemeIs("http") || uri.schemeIs("https"))) + broadcaster.removeAttribute("disabled"); + else + broadcaster.setAttribute("disabled", true); + }, + + /** + * Used to report a phishing page or a false positive + * @param name String One of "Phish", "Error", "Malware" or "MalwareError" + * @return String the report phishing URL. + */ + getReportURL: function(name) { + var reportUrl = SafeBrowsing.getReportURL(name); + + var pageUri = gBrowser.currentURI.clone(); + + // Remove the query to avoid including potentially sensitive data + if (pageUri instanceof Ci.nsIURL) + pageUri.query = ''; + + reportUrl += "&url=" + encodeURIComponent(pageUri.asciiSpec); + + return reportUrl; + } +} +#endif diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc new file mode 100644 index 000000000..c42287ed4 --- /dev/null +++ b/browser/base/content/browser-sets.inc @@ -0,0 +1,420 @@ +# -*- 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_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();"/> + <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:ChromeDebugger" oncommand="BrowserDebuggerProcess.init();" disabled="true" hidden="true"/> + <command id="Tools:BrowserConsole" oncommand="HUDConsoleUI.toggleBrowserConsole();"/> + <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:Addons" oncommand="BrowserOpenAddonsMgr();"/> + <command id="Tools:ErrorConsole" oncommand="toJavaScriptConsole()" disabled="true" hidden="true"/> + <command id="Tools:DevToolsConnect" oncommand="gDevToolsBrowser.openConnectScreen(gBrowser)" disabled="true" hidden="true"/> + <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();"/> + <command id="Social:TogglePageMark" oncommand="SocialMark.togglePageMark();" disabled="true"/> + <command id="Social:SharePage" oncommand="SocialShare.sharePage();" disabled="true"/> + <command id="Social:ToggleSidebar" oncommand="Social.toggleSidebar();"/> + <command id="Social:ToggleNotifications" oncommand="Social.toggleNotifications();" hidden="true"/> + <command id="Social:FocusChat" oncommand="SocialChatBar.focus();" hidden="true" disabled="true"/> + <command id="Social:Toggle" oncommand="Social.toggle();" hidden="true"/> + <command id="Social:Addons" oncommand="BrowserOpenAddonsMgr('addons://list/service');"/> + </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" label="&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"/> + <broadcaster id="socialSidebarBroadcaster" hidden="true"/> + <broadcaster id="socialActiveBroadcaster" hidden="true"/> + + <!-- DevTools broadcasters --> + <broadcaster id="devtoolsMenuBroadcaster_DevToolbox" + label="&devToolboxMenuItem.label;" + type="checkbox" autocheck="false" + command="Tools:DevToolbox"/> + <broadcaster id="devtoolsMenuBroadcaster_DevToolbar" + label="&devToolbarMenu.label;" + type="checkbox" autocheck="false" + command="Tools:DevToolbar" + key="key_devToolbar"/> + <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_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('https://addons.mozilla.org/firefox/collections/mozilla/webdeveloper/', 'tab');"/> + <broadcaster id="devtoolsMenuBroadcaster_connect" + label="&devtoolsConnect.label;" + command="Tools:DevToolsConnect"/> + + <!-- SocialAPI broadcasters --> + <broadcaster id="socialBroadcaster_userDetails" + notLoggedInLabel="&social.notLoggedIn.label;"/> + </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"/> + <key id="key_browserConsole" key="&browserConsoleCmd.commandkey;" command="Tools:BrowserConsole" modifiers="accel,shift"/> + <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"/> + <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 + + <key id="markPage" key="&markPageCmd.commandkey;" command="Social:TogglePageMark" modifiers="accel,shift"/> + <key id="focusChatBar" key="&social.chatBar.commandkey;" command="Social:FocusChat" modifiers="accel,shift"/> + + <key id="key_stop" keycode="VK_ESCAPE" command="Browser:Stop"/> + +#ifdef XP_MACOSX + <key id="key_stop_mac" modifiers="accel" key="&stopCmd.macCommandKey;" command="Browser:Stop"/> +#endif + + <key id="key_gotoHistory" + key="&historySidebarCmd.commandKey;" +#ifdef XP_MACOSX + modifiers="accel,shift" +#else + modifiers="accel" +#endif + command="viewHistorySidebar"/> + + <key id="key_fullZoomReduce" key="&fullZoomReduceCmd.commandkey;" command="cmd_fullZoomReduce" modifiers="accel"/> + <key key="&fullZoomReduceCmd.commandkey2;" command="cmd_fullZoomReduce" modifiers="accel"/> + <key id="key_fullZoomEnlarge" key="&fullZoomEnlargeCmd.commandkey;" command="cmd_fullZoomEnlarge" modifiers="accel"/> + <key key="&fullZoomEnlargeCmd.commandkey2;" command="cmd_fullZoomEnlarge" modifiers="accel"/> + <key key="&fullZoomEnlargeCmd.commandkey3;" command="cmd_fullZoomEnlarge" modifiers="accel"/> + <key id="key_fullZoomReset" key="&fullZoomResetCmd.commandkey;" command="cmd_fullZoomReset" modifiers="accel"/> + <key key="&fullZoomResetCmd.commandkey2;" command="cmd_fullZoomReset" modifiers="accel"/> + + <key id="key_showAllTabs" command="Browser:ShowAllTabs" keycode="VK_TAB" modifiers="control,shift"/> + + <key id="key_switchTextDirection" key="&bidiSwitchTextDirectionItem.commandkey;" command="cmd_switchTextDirection" modifiers="accel,shift" /> + + <key id="key_privatebrowsing" command="Tools:PrivateBrowsing" key="&privateBrowsingCmd.commandkey;" modifiers="accel,shift"/> + <key id="key_sanitize" command="Tools:Sanitize" keycode="VK_DELETE" modifiers="accel,shift"/> +#ifdef XP_MACOSX + <key id="key_sanitize_mac" command="Tools:Sanitize" keycode="VK_BACK" modifiers="accel,shift"/> +#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/browser/base/content/browser-social.js b/browser/base/content/browser-social.js new file mode 100644 index 000000000..7a0ab726c --- /dev/null +++ b/browser/base/content/browser-social.js @@ -0,0 +1,1406 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.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 "exported" symbols +let SocialUI, + SocialChatBar, + SocialFlyout, + SocialMark, + SocialShare, + SocialMenu, + SocialToolbar, + SocialSidebar; + +(function() { + +// The minimum sizes for the auto-resize panel code. +const PANEL_MIN_HEIGHT = 100; +const PANEL_MIN_WIDTH = 330; + +XPCOMUtils.defineLazyModuleGetter(this, "SharedFrame", + "resource:///modules/SharedFrame.jsm"); + +XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() { + let tmp = {}; + Cu.import("resource:///modules/Social.jsm", tmp); + return tmp.OpenGraphBuilder; +}); + +SocialUI = { + // Called on delayed startup to initialize the UI + init: function SocialUI_init() { + Services.obs.addObserver(this, "social:ambient-notification-changed", false); + Services.obs.addObserver(this, "social:profile-changed", false); + Services.obs.addObserver(this, "social:page-mark-config", false); + Services.obs.addObserver(this, "social:frameworker-error", false); + Services.obs.addObserver(this, "social:provider-set", false); + Services.obs.addObserver(this, "social:providers-changed", false); + + Services.prefs.addObserver("social.sidebar.open", this, false); + Services.prefs.addObserver("social.toast-notifications.enabled", this, false); + + gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true); + + SocialChatBar.init(); + SocialMark.init(); + SocialShare.init(); + SocialMenu.init(); + SocialToolbar.init(); + SocialSidebar.init(); + + if (!Social.initialized) { + Social.init(); + } else { + // social was previously initialized, so it's not going to notify us of + // anything, so handle that now. + this.observe(null, "social:providers-changed", null); + this.observe(null, "social:provider-set", Social.provider ? Social.provider.origin : null); + } + }, + + // Called on window unload + uninit: function SocialUI_uninit() { + Services.obs.removeObserver(this, "social:ambient-notification-changed"); + Services.obs.removeObserver(this, "social:profile-changed"); + Services.obs.removeObserver(this, "social:page-mark-config"); + Services.obs.removeObserver(this, "social:frameworker-error"); + Services.obs.removeObserver(this, "social:provider-set"); + Services.obs.removeObserver(this, "social:providers-changed"); + + Services.prefs.removeObserver("social.sidebar.open", this); + Services.prefs.removeObserver("social.toast-notifications.enabled", this); + }, + + _matchesCurrentProvider: function (origin) { + return Social.provider && Social.provider.origin == origin; + }, + + observe: function SocialUI_observe(subject, topic, data) { + // Exceptions here sometimes don't get reported properly, report them + // manually :( + try { + switch (topic) { + case "social:provider-set": + // Social.provider has changed (possibly to null), update any state + // which depends on it. + this._updateActiveUI(); + this._updateMenuItems(); + + SocialFlyout.unload(); + SocialChatBar.update(); + SocialShare.update(); + SocialSidebar.update(); + SocialMark.update(); + SocialToolbar.update(); + SocialMenu.populate(); + break; + case "social:providers-changed": + // the list of providers changed - this may impact the "active" UI. + this._updateActiveUI(); + // and the multi-provider menu + SocialToolbar.populateProviderMenus(); + SocialShare.populateProviderMenu(); + break; + + // Provider-specific notifications + case "social:ambient-notification-changed": + if (this._matchesCurrentProvider(data)) { + SocialToolbar.updateButton(); + SocialMenu.populate(); + } + break; + case "social:profile-changed": + if (this._matchesCurrentProvider(data)) { + SocialToolbar.updateProvider(); + SocialMark.update(); + SocialChatBar.update(); + } + break; + case "social:page-mark-config": + if (this._matchesCurrentProvider(data)) { + SocialMark.updateMarkState(); + } + break; + case "social:frameworker-error": + if (this.enabled && Social.provider.origin == data) { + SocialSidebar.setSidebarErrorMessage(); + } + break; + + case "nsPref:changed": + if (data == "social.sidebar.open") { + SocialSidebar.update(); + } else if (data == "social.toast-notifications.enabled") { + SocialToolbar.updateButton(); + } + break; + } + } catch (e) { + Components.utils.reportError(e + "\n" + e.stack); + throw e; + } + }, + + nonBrowserWindowInit: function SocialUI_nonBrowserInit() { + // Disable the social menu item in non-browser windows + document.getElementById("menu_socialAmbientMenu").hidden = true; + }, + + // Miscellaneous helpers + showProfile: function SocialUI_showProfile() { + if (Social.haveLoggedInUser()) + openUILinkIn(Social.provider.profile.profileURL, "tab"); + else { + // XXX Bug 789585 will implement an API for provider-specified login pages. + openUILinkIn(Social.provider.origin, "tab"); + } + }, + + _updateActiveUI: function SocialUI_updateActiveUI() { + // The "active" UI isn't dependent on there being a provider, just on + // social being "active" (but also chromeless/PB) + let enabled = Social.providers.length > 0 && !this._chromeless && + !PrivateBrowsingUtils.isWindowPrivate(window); + let broadcaster = document.getElementById("socialActiveBroadcaster"); + broadcaster.hidden = !enabled; + + let toggleCommand = document.getElementById("Social:Toggle"); + toggleCommand.setAttribute("hidden", enabled ? "false" : "true"); + + if (enabled) { + // enabled == true means we at least have a defaultProvider + let provider = Social.provider || Social.defaultProvider; + // We only need to update the command itself - all our menu items use it. + let label = gNavigatorBundle.getFormattedString(Social.provider ? + "social.turnOff.label" : + "social.turnOn.label", + [provider.name]); + let accesskey = gNavigatorBundle.getString(Social.provider ? + "social.turnOff.accesskey" : + "social.turnOn.accesskey"); + toggleCommand.setAttribute("label", label); + toggleCommand.setAttribute("accesskey", accesskey); + } + }, + + _updateMenuItems: function () { + let provider = Social.provider || Social.defaultProvider; + if (!provider) + return; + // The View->Sidebar and Menubar->Tools menu. + for (let id of ["menu_socialSidebar", "menu_socialAmbientMenu"]) + document.getElementById(id).setAttribute("label", provider.name); + }, + + // This handles "ActivateSocialFeature" events fired against content documents + // in this window. + _activationEventHandler: function SocialUI_activationHandler(e) { + let targetDoc; + let node; + if (e.target instanceof HTMLDocument) { + // version 0 support + targetDoc = e.target; + node = targetDoc.documentElement + } else { + targetDoc = e.target.ownerDocument; + node = e.target; + } + if (!(targetDoc instanceof HTMLDocument)) + return; + + // Ignore events fired in background tabs or iframes + if (targetDoc.defaultView != content) + return; + + // If we are in PB mode, we silently do nothing (bug 829404 exists to + // do something sensible here...) + if (PrivateBrowsingUtils.isWindowPrivate(window)) + return; + + // If the last event was received < 1s ago, ignore this one + let now = Date.now(); + if (now - Social.lastEventReceived < 1000) + return; + Social.lastEventReceived = now; + + // We only want to activate if it is as a result of user input. + let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + if (!dwu.isHandlingUserInput) { + Cu.reportError("attempt to activate provider without user input from " + targetDoc.nodePrincipal.origin); + return; + } + + let data = node.getAttribute("data-service"); + if (data) { + try { + data = JSON.parse(data); + } catch(e) { + Cu.reportError("Social Service manifest parse error: "+e); + return; + } + } + Social.installProvider(targetDoc, data, function(manifest) { + this.doActivation(manifest.origin); + }.bind(this)); + }, + + doActivation: function SocialUI_doActivation(origin) { + // Keep track of the old provider in case of undo + let oldOrigin = Social.provider ? Social.provider.origin : ""; + + // Enable the social functionality, and indicate that it was activated + Social.activateFromOrigin(origin, function(provider) { + // Provider to activate may not have been found + if (!provider) + return; + + // Show a warning, allow undoing the activation + let description = document.getElementById("social-activation-message"); + let labels = description.getElementsByTagName("label"); + let uri = Services.io.newURI(provider.origin, null, null) + labels[0].setAttribute("value", uri.host); + labels[1].setAttribute("onclick", "BrowserOpenAddonsMgr('addons://list/service'); SocialUI.activationPanel.hidePopup();") + + let icon = document.getElementById("social-activation-icon"); + if (provider.icon64URL || provider.icon32URL) { + icon.setAttribute('src', provider.icon64URL || provider.icon32URL); + icon.hidden = false; + } else { + icon.removeAttribute('src'); + icon.hidden = true; + } + + let notificationPanel = SocialUI.activationPanel; + // Set the origin being activated and the previously active one, to allow undo + notificationPanel.setAttribute("origin", provider.origin); + notificationPanel.setAttribute("oldorigin", oldOrigin); + + // Show the panel + notificationPanel.hidden = false; + setTimeout(function () { + notificationPanel.openPopup(SocialToolbar.button, "bottomcenter topright"); + }, 0); + }); + }, + + undoActivation: function SocialUI_undoActivation() { + let origin = this.activationPanel.getAttribute("origin"); + let oldOrigin = this.activationPanel.getAttribute("oldorigin"); + Social.deactivateFromOrigin(origin, oldOrigin); + this.activationPanel.hidePopup(); + Social.uninstallProvider(origin); + }, + + showLearnMore: function() { + this.activationPanel.hidePopup(); + let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api"; + openUILinkIn(url, "tab"); + }, + + get activationPanel() { + return document.getElementById("socialActivatedNotification"); + }, + + closeSocialPanelForLinkTraversal: function (target, linkNode) { + // No need to close the panel if this traversal was not retargeted + if (target == "" || target == "_self") + return; + + // Check to see whether this link traversal was in a social panel + let win = linkNode.ownerDocument.defaultView; + let container = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + let containerParent = container.parentNode; + if (containerParent.classList.contains("social-panel") && + containerParent instanceof Ci.nsIDOMXULPopupElement) { + // allow the link traversal to finish before closing the panel + setTimeout(() => { + containerParent.hidePopup(); + }, 0); + } + }, + + get _chromeless() { + // Is this a popup window that doesn't want chrome shown? + let docElem = document.documentElement; + // extrachrome is not restored during session restore, so we need + // to check for the toolbar as well. + let chromeless = docElem.getAttribute("chromehidden").contains("extrachrome") || + docElem.getAttribute('chromehidden').contains("toolbar"); + // This property is "fixed" for a window, so avoid doing the check above + // multiple times... + delete this._chromeless; + this._chromeless = chromeless; + return chromeless; + }, + + get enabled() { + // Returns whether social is enabled *for this window*. + if (this._chromeless || PrivateBrowsingUtils.isWindowPrivate(window)) + return false; + return !!Social.provider; + }, + +} + +SocialChatBar = { + init: function() { + }, + get chatbar() { + return document.getElementById("pinnedchats"); + }, + // Whether the chatbar is available for this window. Note that in full-screen + // mode chats are available, but not shown. + get isAvailable() { + return SocialUI.enabled && Social.haveLoggedInUser(); + }, + // Does this chatbar have any chats (whether minimized, collapsed or normal) + get hasChats() { + return !!this.chatbar.firstElementChild; + }, + openChat: function(aProvider, aURL, aCallback, aMode) { + if (!this.isAvailable) + return false; + this.chatbar.openChat(aProvider, aURL, aCallback, aMode); + // We only want to focus the chat if it is as a result of user input. + let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + if (dwu.isHandlingUserInput) + this.chatbar.focus(); + return true; + }, + update: function() { + let command = document.getElementById("Social:FocusChat"); + if (!this.isAvailable) { + this.chatbar.removeAll(); + this.chatbar.hidden = command.hidden = true; + } else { + this.chatbar.hidden = command.hidden = false; + } + command.setAttribute("disabled", command.hidden ? "true" : "false"); + }, + focus: function SocialChatBar_focus() { + this.chatbar.focus(); + } +} + +function sizeSocialPanelToContent(panel, iframe) { + // FIXME: bug 764787: Maybe we can use nsIDOMWindowUtils.getRootBounds() here? + let doc = iframe.contentDocument; + if (!doc || !doc.body) { + return; + } + // We need an element to use for sizing our panel. See if the body defines + // an id for that element, otherwise use the body itself. + let body = doc.body; + let bodyId = body.getAttribute("contentid"); + if (bodyId) { + body = doc.getElementById(bodyId) || doc.body; + } + // offsetHeight/Width don't include margins, so account for that. + let cs = doc.defaultView.getComputedStyle(body); + let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom); + let height = Math.max(computedHeight, PANEL_MIN_HEIGHT); + let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight); + let width = Math.max(computedWidth, PANEL_MIN_WIDTH); + iframe.style.width = width + "px"; + iframe.style.height = height + "px"; + // since we do not use panel.sizeTo, we need to adjust the arrow ourselves + if (panel.state == "open") + panel.adjustArrowPosition(); +} + +function DynamicResizeWatcher() { + this._mutationObserver = null; +} + +DynamicResizeWatcher.prototype = { + start: function DynamicResizeWatcher_start(panel, iframe) { + this.stop(); // just in case... + let doc = iframe.contentDocument; + this._mutationObserver = new iframe.contentWindow.MutationObserver(function(mutations) { + sizeSocialPanelToContent(panel, iframe); + }); + // Observe anything that causes the size to change. + let config = {attributes: true, characterData: true, childList: true, subtree: true}; + this._mutationObserver.observe(doc, config); + // and since this may be setup after the load event has fired we do an + // initial resize now. + sizeSocialPanelToContent(panel, iframe); + }, + stop: function DynamicResizeWatcher_stop() { + if (this._mutationObserver) { + try { + this._mutationObserver.disconnect(); + } catch (ex) { + // may get "TypeError: can't access dead object" which seems strange, + // but doesn't seem to indicate a real problem, so ignore it... + } + this._mutationObserver = null; + } + } +} + +SocialFlyout = { + get panel() { + return document.getElementById("social-flyout-panel"); + }, + + get iframe() { + if (!this.panel.firstChild) + this._createFrame(); + return this.panel.firstChild; + }, + + dispatchPanelEvent: function(name) { + let doc = this.iframe.contentDocument; + let evt = doc.createEvent("CustomEvent"); + evt.initCustomEvent(name, true, true, {}); + doc.documentElement.dispatchEvent(evt); + }, + + _createFrame: function() { + let panel = this.panel; + if (!SocialUI.enabled || panel.firstChild) + return; + // create and initialize the panel for this window + let iframe = document.createElement("iframe"); + iframe.setAttribute("type", "content"); + iframe.setAttribute("class", "social-panel-frame"); + iframe.setAttribute("flex", "1"); + iframe.setAttribute("tooltip", "aHTMLTooltip"); + iframe.setAttribute("origin", Social.provider.origin); + panel.appendChild(iframe); + }, + + setFlyoutErrorMessage: function SF_setFlyoutErrorMessage() { + this.iframe.removeAttribute("src"); + this.iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo", null, null, null, null); + sizeSocialPanelToContent(this.panel, this.iframe); + }, + + unload: function() { + let panel = this.panel; + panel.hidePopup(); + if (!panel.firstChild) + return + let iframe = panel.firstChild; + if (iframe.socialErrorListener) + iframe.socialErrorListener.remove(); + panel.removeChild(iframe); + }, + + onShown: function(aEvent) { + let panel = this.panel; + let iframe = this.iframe; + this._dynamicResizer = new DynamicResizeWatcher(); + iframe.docShell.isActive = true; + iframe.docShell.isAppTab = true; + if (iframe.contentDocument.readyState == "complete") { + this._dynamicResizer.start(panel, iframe); + this.dispatchPanelEvent("socialFrameShow"); + } else { + // first time load, wait for load and dispatch after load + iframe.addEventListener("load", function panelBrowserOnload(e) { + iframe.removeEventListener("load", panelBrowserOnload, true); + setTimeout(function() { + if (SocialFlyout._dynamicResizer) { // may go null if hidden quickly + SocialFlyout._dynamicResizer.start(panel, iframe); + SocialFlyout.dispatchPanelEvent("socialFrameShow"); + } + }, 0); + }, true); + } + }, + + onHidden: function(aEvent) { + this._dynamicResizer.stop(); + this._dynamicResizer = null; + this.iframe.docShell.isActive = false; + this.dispatchPanelEvent("socialFrameHide"); + }, + + load: function(aURL, cb) { + if (!Social.provider) + return; + + this.panel.hidden = false; + let iframe = this.iframe; + // same url with only ref difference does not cause a new load, so we + // want to go right to the callback + let src = iframe.contentDocument && iframe.contentDocument.documentURIObject; + if (!src || !src.equalsExceptRef(Services.io.newURI(aURL, null, null))) { + iframe.addEventListener("load", function documentLoaded() { + iframe.removeEventListener("load", documentLoaded, true); + cb(); + }, true); + // Force a layout flush by calling .clientTop so + // that the docShell of this frame is created + iframe.clientTop; + Social.setErrorListener(iframe, SocialFlyout.setFlyoutErrorMessage.bind(SocialFlyout)) + iframe.setAttribute("src", aURL); + } else { + // we still need to set the src to trigger the contents hashchange event + // for ref changes + iframe.setAttribute("src", aURL); + cb(); + } + }, + + open: function(aURL, yOffset, aCallback) { + // Hide any other social panels that may be open. + document.getElementById("social-notification-panel").hidePopup(); + + if (!SocialUI.enabled) + return; + let panel = this.panel; + let iframe = this.iframe; + + this.load(aURL, function() { + sizeSocialPanelToContent(panel, iframe); + let anchor = document.getElementById("social-sidebar-browser"); + if (panel.state == "open") { + panel.moveToAnchor(anchor, "start_before", 0, yOffset, false); + } else { + panel.openPopup(anchor, "start_before", 0, yOffset, false, false); + } + if (aCallback) { + try { + aCallback(iframe.contentWindow); + } catch(e) { + Cu.reportError(e); + } + } + }); + } +} + +SocialShare = { + // Called once, after window load, when the Social.provider object is initialized + init: function() {}, + + get panel() { + return document.getElementById("social-share-panel"); + }, + + get iframe() { + // first element is our menu vbox. + if (this.panel.childElementCount == 1) + return null; + else + return this.panel.lastChild; + }, + + _createFrame: function() { + let panel = this.panel; + if (!SocialUI.enabled || this.iframe) + return; + this.panel.hidden = false; + // create and initialize the panel for this window + let iframe = document.createElement("iframe"); + iframe.setAttribute("type", "content"); + iframe.setAttribute("class", "social-share-frame"); + iframe.setAttribute("flex", "1"); + panel.appendChild(iframe); + this.populateProviderMenu(); + }, + + getSelectedProvider: function() { + let provider; + let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin"); + if (lastProviderOrigin) { + provider = Social._getProviderFromOrigin(lastProviderOrigin); + } + if (!provider) + provider = Social.provider || Social.defaultProvider; + // if our provider has no shareURL, select the first one that does + if (provider && !provider.shareURL) { + let providers = [p for (p of Social.providers) if (p.shareURL)]; + provider = providers.length > 0 && providers[0]; + } + return provider; + }, + + populateProviderMenu: function() { + if (!this.iframe) + return; + let providers = [p for (p of Social.providers) if (p.shareURL)]; + let hbox = document.getElementById("social-share-provider-buttons"); + // selectable providers are inserted before the provider-menu seperator, + // remove any menuitems in that area + while (hbox.firstChild) { + hbox.removeChild(hbox.firstChild); + } + // reset our share toolbar + // only show a selection if there is more than one + if (!SocialUI.enabled || providers.length < 2) { + this.panel.firstChild.hidden = true; + return; + } + let selectedProvider = this.getSelectedProvider(); + for (let provider of providers) { + let button = document.createElement("toolbarbutton"); + button.setAttribute("class", "toolbarbutton share-provider-button"); + button.setAttribute("type", "radio"); + button.setAttribute("group", "share-providers"); + button.setAttribute("image", provider.iconURL); + button.setAttribute("tooltiptext", provider.name); + button.setAttribute("origin", provider.origin); + button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin')); this.checked=true;"); + if (provider == selectedProvider) { + this.defaultButton = button; + } + hbox.appendChild(button); + } + if (!this.defaultButton) { + this.defaultButton = hbox.firstChild + } + this.defaultButton.setAttribute("checked", "true"); + this.panel.firstChild.hidden = false; + }, + + get shareButton() { + return document.getElementById("social-share-button"); + }, + + canSharePage: function(aURI) { + // we do not enable sharing from private sessions + if (PrivateBrowsingUtils.isWindowPrivate(window)) + return false; + + if (!aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https'))) + return false; + return true; + }, + + update: function() { + let shareButton = this.shareButton; + shareButton.hidden = !SocialUI.enabled || + [p for (p of Social.providers) if (p.shareURL)].length == 0; + shareButton.disabled = shareButton.hidden || !this.canSharePage(gBrowser.currentURI); + + // also update the relevent command's disabled state so the keyboard + // shortcut only works when available. + let cmd = document.getElementById("Social:SharePage"); + cmd.setAttribute("disabled", shareButton.disabled ? "true" : "false"); + }, + + onShowing: function() { + this.shareButton.setAttribute("open", "true"); + }, + + onHidden: function() { + this.shareButton.removeAttribute("open"); + this.iframe.setAttribute("src", "data:text/plain;charset=utf8,"); + this.currentShare = null; + }, + + setErrorMessage: function() { + let iframe = this.iframe; + if (!iframe) + return; + + iframe.removeAttribute("src"); + iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" + + encodeURIComponent(iframe.getAttribute("origin")), + null, null, null, null); + sizeSocialPanelToContent(this.panel, iframe); + }, + + sharePage: function(providerOrigin, graphData) { + // if providerOrigin is undefined, we use the last-used provider, or the + // current/default provider. The provider selection in the share panel + // will call sharePage with an origin for us to switch to. + this._createFrame(); + let iframe = this.iframe; + let provider; + if (providerOrigin) + provider = Social._getProviderFromOrigin(providerOrigin); + else + provider = this.getSelectedProvider(); + if (!provider || !provider.shareURL) + return; + + // graphData is an optional param that either defines the full set of data + // to be shared, or partial data about the current page. It is set by a call + // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST + // define at least url. If it is undefined, we're sharing the current url in + // the browser tab. + let sharedURI = graphData ? Services.io.newURI(graphData.url, null, null) : + gBrowser.currentURI; + if (!this.canSharePage(sharedURI)) + return; + + // the point of this action type is that we can use existing share + // endpoints (e.g. oexchange) that do not support additional + // socialapi functionality. One tweak is that we shoot an event + // containing the open graph data. + let pageData = graphData ? graphData : this.currentShare; + if (!pageData || sharedURI == gBrowser.currentURI) { + pageData = OpenGraphBuilder.getData(gBrowser); + if (graphData) { + // overwrite data retreived from page with data given to us as a param + for (let p in graphData) { + pageData[p] = graphData[p]; + } + } + } + this.currentShare = pageData; + + let shareEndpoint = this._generateShareEndpointURL(provider.shareURL, pageData); + + this._dynamicResizer = new DynamicResizeWatcher(); + // if we've already loaded this provider/page share endpoint, we don't want + // to add another load event listener. + let reload = true; + let endpointMatch = shareEndpoint == iframe.getAttribute("src"); + let docLoaded = iframe.contentDocument && iframe.contentDocument.readyState == "complete"; + if (endpointMatch && docLoaded) { + reload = shareEndpoint != iframe.contentDocument.location.spec; + } + if (!reload) { + this._dynamicResizer.start(this.panel, iframe); + iframe.docShell.isActive = true; + iframe.docShell.isAppTab = true; + let evt = iframe.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData)); + iframe.contentDocument.documentElement.dispatchEvent(evt); + } else { + // first time load, wait for load and dispatch after load + iframe.addEventListener("load", function panelBrowserOnload(e) { + iframe.removeEventListener("load", panelBrowserOnload, true); + iframe.docShell.isActive = true; + iframe.docShell.isAppTab = true; + setTimeout(function() { + if (SocialShare._dynamicResizer) { // may go null if hidden quickly + SocialShare._dynamicResizer.start(iframe.parentNode, iframe); + } + }, 0); + let evt = iframe.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData)); + iframe.contentDocument.documentElement.dispatchEvent(evt); + }, true); + } + // always ensure that origin belongs to the endpoint + let uri = Services.io.newURI(shareEndpoint, null, null); + iframe.setAttribute("origin", provider.origin); + iframe.setAttribute("src", shareEndpoint); + + let navBar = document.getElementById("nav-bar"); + let anchor = navBar.getAttribute("mode") == "text" ? + document.getAnonymousElementByAttribute(this.shareButton, "class", "toolbarbutton-text") : + document.getAnonymousElementByAttribute(this.shareButton, "class", "toolbarbutton-icon"); + this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); + Social.setErrorListener(iframe, this.setErrorMessage.bind(this)); + }, + + _generateShareEndpointURL: function(shareURL, pageData) { + // support for existing share endpoints by supporting their querystring + // arguments. parse the query string template and do replacements where + // necessary the query names may be different than ours, so we could see + // u=%{url} or url=%{url} + let [shareEndpoint, queryString] = shareURL.split("?"); + let query = {}; + if (queryString) { + queryString.split('&').forEach(function (val) { + let [name, value] = val.split('='); + let p = /%\{(.+)\}/.exec(value); + if (!p) { + // preserve non-template query vars + query[name] = value; + } else if (pageData[p[1]]) { + query[name] = pageData[p[1]]; + } else if (p[1] == "body") { + // build a body for emailers + let body = ""; + if (pageData.title) + body += pageData.title + "\n\n"; + if (pageData.description) + body += pageData.description + "\n\n"; + if (pageData.text) + body += pageData.text + "\n\n"; + body += pageData.url; + query["body"] = body; + } + }); + } + var str = []; + for (let p in query) + str.push(p + "=" + encodeURIComponent(query[p])); + if (str.length) + shareEndpoint = shareEndpoint + "?" + str.join("&"); + return shareEndpoint; + } +}; + +SocialMark = { + // Called once, after window load, when the Social.provider object is initialized + init: function SSB_init() { + }, + + get button() { + return document.getElementById("social-mark-button"); + }, + + canMarkPage: function SSB_canMarkPage(aURI) { + // We only allow sharing of http or https + return aURI && (aURI.schemeIs('http') || aURI.schemeIs('https')); + }, + + // Called when the Social.provider changes + update: function SSB_updateButtonState() { + let markButton = this.button; + // always show button if provider supports marks + markButton.hidden = !SocialUI.enabled || Social.provider.pageMarkInfo == null; + markButton.disabled = markButton.hidden || !this.canMarkPage(gBrowser.currentURI); + + // also update the relevent command's disabled state so the keyboard + // shortcut only works when available. + let cmd = document.getElementById("Social:TogglePageMark"); + cmd.setAttribute("disabled", markButton.disabled ? "true" : "false"); + }, + + togglePageMark: function(aCallback) { + if (this.button.disabled) + return; + this.toggleURIMark(gBrowser.currentURI, aCallback) + }, + + toggleURIMark: function(aURI, aCallback) { + let update = function(marked) { + this._updateMarkState(marked); + if (aCallback) + aCallback(marked); + }.bind(this); + Social.isURIMarked(aURI, function(marked) { + if (marked) { + Social.unmarkURI(aURI, update); + } else { + Social.markURI(aURI, update); + } + }); + }, + + updateMarkState: function SSB_updateMarkState() { + this.update(); + if (!this.button.hidden) + Social.isURIMarked(gBrowser.currentURI, this._updateMarkState.bind(this)); + }, + + _updateMarkState: function(currentPageMarked) { + // callback for isURIMarked + let markButton = this.button; + let pageMarkInfo = SocialUI.enabled ? Social.provider.pageMarkInfo : null; + + // Update the mark button, if present + if (!markButton || markButton.hidden || !pageMarkInfo) + return; + + let imageURL; + if (!markButton.disabled && currentPageMarked) { + markButton.setAttribute("marked", "true"); + markButton.setAttribute("label", pageMarkInfo.messages.markedLabel); + markButton.setAttribute("tooltiptext", pageMarkInfo.messages.markedTooltip); + imageURL = pageMarkInfo.images.marked; + } else { + markButton.removeAttribute("marked"); + markButton.setAttribute("label", pageMarkInfo.messages.unmarkedLabel); + markButton.setAttribute("tooltiptext", pageMarkInfo.messages.unmarkedTooltip); + imageURL = pageMarkInfo.images.unmarked; + } + markButton.style.listStyleImage = "url(" + imageURL + ")"; + } +}; + +SocialMenu = { + init: function SocialMenu_init() { + }, + + populate: function SocialMenu_populate() { + let submenu = document.getElementById("menu_social-statusarea-popup"); + let ambientMenuItems = submenu.getElementsByClassName("ambient-menuitem"); + while (ambientMenuItems.length) + submenu.removeChild(ambientMenuItems.item(0)); + + let separator = document.getElementById("socialAmbientMenuSeparator"); + separator.hidden = true; + let provider = SocialUI.enabled ? Social.provider : null; + if (!provider) + return; + + let iconNames = Object.keys(provider.ambientNotificationIcons); + for (let name of iconNames) { + let icon = provider.ambientNotificationIcons[name]; + if (!icon.label || !icon.menuURL) + continue; + separator.hidden = false; + let menuitem = document.createElement("menuitem"); + menuitem.setAttribute("label", icon.label); + menuitem.classList.add("ambient-menuitem"); + menuitem.addEventListener("command", function() { + openUILinkIn(icon.menuURL, "tab"); + }, false); + submenu.insertBefore(menuitem, separator); + } + } +}; + +// XXX Need to audit that this is being initialized correctly +SocialToolbar = { + // Called once, after window load, when the Social.provider object is + // initialized. + init: function SocialToolbar_init() { + this._dynamicResizer = new DynamicResizeWatcher(); + }, + + update: function() { + this._updateButtonHiddenState(); + this.updateProvider(); + this.populateProviderMenus(); + }, + + // Called when the Social.provider changes + updateProvider: function () { + let provider = Social.provider; + if (provider) { + this.button.setAttribute("label", provider.name); + this.button.setAttribute("tooltiptext", provider.name); + this.button.style.listStyleImage = "url(" + provider.iconURL + ")"; + + this.updateProfile(); + } else { + this.button.setAttribute("label", gNavigatorBundle.getString("service.toolbarbutton.label")); + this.button.setAttribute("tooltiptext", gNavigatorBundle.getString("service.toolbarbutton.tooltiptext")); + this.button.style.removeProperty("list-style-image"); + } + this.updateButton(); + }, + + get button() { + return document.getElementById("social-provider-button"); + }, + + // Note: this doesn't actually handle hiding the toolbar button, + // socialActiveBroadcaster is responsible for that. + _updateButtonHiddenState: function SocialToolbar_updateButtonHiddenState() { + let socialEnabled = SocialUI.enabled; + for (let className of ["social-statusarea-separator", "social-statusarea-user"]) { + for (let element of document.getElementsByClassName(className)) + element.hidden = !socialEnabled; + } + let toggleNotificationsCommand = document.getElementById("Social:ToggleNotifications"); + toggleNotificationsCommand.setAttribute("hidden", !socialEnabled); + + if (!Social.haveLoggedInUser() || !socialEnabled) { + let parent = document.getElementById("social-notification-panel"); + while (parent.hasChildNodes()) { + let frame = parent.firstChild; + SharedFrame.forgetGroup(frame.id); + parent.removeChild(frame); + } + + let tbi = document.getElementById("social-toolbar-item"); + if (tbi) { + // SocialMark is the last button allways + let next = SocialMark.button.previousSibling; + while (next != this.button) { + tbi.removeChild(next); + next = SocialMark.button.previousSibling; + } + } + } + }, + + updateProfile: function SocialToolbar_updateProfile() { + // Profile may not have been initialized yet, since it depends on a worker + // response. In that case we'll be called again when it's available, via + // social:profile-changed + if (!Social.provider) + return; + let profile = Social.provider.profile || {}; + let userPortrait = profile.portrait; + + let userDetailsBroadcaster = document.getElementById("socialBroadcaster_userDetails"); + let loggedInStatusValue = profile.userName || + userDetailsBroadcaster.getAttribute("notLoggedInLabel"); + + // "image" and "label" are used by Mac's native menus that do not render the menuitem's children + // elements. "src" and "value" are used by the image/label children on the other platforms. + if (userPortrait) { + userDetailsBroadcaster.setAttribute("src", userPortrait); + userDetailsBroadcaster.setAttribute("image", userPortrait); + } else { + userDetailsBroadcaster.removeAttribute("src"); + userDetailsBroadcaster.removeAttribute("image"); + } + + userDetailsBroadcaster.setAttribute("value", loggedInStatusValue); + userDetailsBroadcaster.setAttribute("label", loggedInStatusValue); + }, + + updateButton: function SocialToolbar_updateButton() { + this._updateButtonHiddenState(); + let panel = document.getElementById("social-notification-panel"); + panel.hidden = !SocialUI.enabled; + + let command = document.getElementById("Social:ToggleNotifications"); + command.setAttribute("checked", Services.prefs.getBoolPref("social.toast-notifications.enabled")); + + const CACHE_PREF_NAME = "social.cached.ambientNotificationIcons"; + // provider.profile == undefined means no response yet from the provider + // to tell us whether the user is logged in or not. + if (!SocialUI.enabled || + (!Social.haveLoggedInUser() && Social.provider.profile !== undefined)) { + // Either no enabled provider, or there is a provider and it has + // responded with a profile and the user isn't loggedin. The icons + // etc have already been removed by updateButtonHiddenState, so we want + // to nuke any cached icons we have and get out of here! + Services.prefs.clearUserPref(CACHE_PREF_NAME); + return; + } + let icons = Social.provider.ambientNotificationIcons; + let iconNames = Object.keys(icons); + + if (Social.provider.profile === undefined) { + // provider has not told us about the login state yet - see if we have + // a cached version for this provider. + let cached; + try { + cached = JSON.parse(Services.prefs.getComplexValue(CACHE_PREF_NAME, + Ci.nsISupportsString).data); + } catch (ex) {} + if (cached && cached.provider == Social.provider.origin && cached.data) { + icons = cached.data; + iconNames = Object.keys(icons); + // delete the counter data as it is almost certainly stale. + for each(let name in iconNames) { + icons[name].counter = ''; + } + } + } else { + // We have a logged in user - save the current set of icons back to the + // "cache" so we can use them next startup. + let str = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + str.data = JSON.stringify({provider: Social.provider.origin, data: icons}); + Services.prefs.setComplexValue(CACHE_PREF_NAME, + Ci.nsISupportsString, + str); + } + + let toolbarButtons = document.createDocumentFragment(); + + let createdFrames = []; + + for each(let name in iconNames) { + let icon = icons[name]; + + let notificationFrameId = "social-status-" + icon.name; + let notificationFrame = document.getElementById(notificationFrameId); + + if (!notificationFrame) { + notificationFrame = SharedFrame.createFrame( + notificationFrameId, /* frame name */ + panel, /* parent */ + { + "type": "content", + "mozbrowser": "true", + "class": "social-panel-frame", + "id": notificationFrameId, + "tooltip": "aHTMLTooltip", + + // work around bug 793057 - by making the panel roughly the final size + // we are more likely to have the anchor in the correct position. + "style": "width: " + PANEL_MIN_WIDTH + "px;", + + "origin": Social.provider.origin, + "src": icon.contentPanel + } + ); + + createdFrames.push(notificationFrame); + } else { + notificationFrame.setAttribute("origin", Social.provider.origin); + SharedFrame.updateURL(notificationFrameId, icon.contentPanel); + } + + let toolbarButtonId = "social-notification-icon-" + icon.name; + let toolbarButton = document.getElementById(toolbarButtonId); + if (!toolbarButton) { + toolbarButton = document.createElement("toolbarbutton"); + toolbarButton.setAttribute("type", "badged"); + toolbarButton.classList.add("toolbarbutton-1"); + toolbarButton.setAttribute("id", toolbarButtonId); + toolbarButton.setAttribute("notificationFrameId", notificationFrameId); + toolbarButton.addEventListener("mousedown", function (event) { + if (event.button == 0 && panel.state == "closed") + SocialToolbar.showAmbientPopup(toolbarButton); + }); + + toolbarButtons.appendChild(toolbarButton); + } + + toolbarButton.style.listStyleImage = "url(" + icon.iconURL + ")"; + toolbarButton.setAttribute("label", icon.label); + toolbarButton.setAttribute("tooltiptext", icon.label); + + let badge = icon.counter || ""; + toolbarButton.setAttribute("badge", badge); + let ariaLabel = icon.label; + // if there is a badge value, we must use a localizable string to insert it. + if (badge) + ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText", + [ariaLabel, badge]); + toolbarButton.setAttribute("aria-label", ariaLabel); + } + let socialToolbarItem = document.getElementById("social-toolbar-item"); + socialToolbarItem.insertBefore(toolbarButtons, SocialMark.button); + + for (let frame of createdFrames) { + if (frame.socialErrorListener) { + frame.socialErrorListener.remove(); + } + if (frame.docShell) { + frame.docShell.isActive = false; + Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this)); + } + } + }, + + showAmbientPopup: function SocialToolbar_showAmbientPopup(aToolbarButton) { + // Hide any other social panels that may be open. + SocialFlyout.panel.hidePopup(); + + let panel = document.getElementById("social-notification-panel"); + let notificationFrameId = aToolbarButton.getAttribute("notificationFrameId"); + let notificationFrame = document.getElementById(notificationFrameId); + + let wasAlive = SharedFrame.isGroupAlive(notificationFrameId); + SharedFrame.setOwner(notificationFrameId, notificationFrame); + + // Clear dimensions on all browsers so the panel size will + // only use the selected browser. + let frameIter = panel.firstElementChild; + while (frameIter) { + frameIter.collapsed = (frameIter != notificationFrame); + frameIter = frameIter.nextElementSibling; + } + + function dispatchPanelEvent(name) { + let evt = notificationFrame.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent(name, true, true, {}); + notificationFrame.contentDocument.documentElement.dispatchEvent(evt); + } + + let dynamicResizer = this._dynamicResizer; + panel.addEventListener("popuphidden", function onpopuphiding() { + panel.removeEventListener("popuphidden", onpopuphiding); + aToolbarButton.removeAttribute("open"); + aToolbarButton.parentNode.removeAttribute("open"); + dynamicResizer.stop(); + notificationFrame.docShell.isActive = false; + dispatchPanelEvent("socialFrameHide"); + }); + + panel.addEventListener("popupshown", function onpopupshown() { + panel.removeEventListener("popupshown", onpopupshown); + // This attribute is needed on both the button and the + // containing toolbaritem since the buttons on OS X have + // moz-appearance:none, while their container gets + // moz-appearance:toolbarbutton due to the way that toolbar buttons + // get combined on OS X. + aToolbarButton.setAttribute("open", "true"); + aToolbarButton.parentNode.setAttribute("open", "true"); + notificationFrame.docShell.isActive = true; + notificationFrame.docShell.isAppTab = true; + if (notificationFrame.contentDocument.readyState == "complete" && wasAlive) { + dynamicResizer.start(panel, notificationFrame); + dispatchPanelEvent("socialFrameShow"); + } else { + // first time load, wait for load and dispatch after load + notificationFrame.addEventListener("load", function panelBrowserOnload(e) { + notificationFrame.removeEventListener("load", panelBrowserOnload, true); + dynamicResizer.start(panel, notificationFrame); + setTimeout(function() { + dispatchPanelEvent("socialFrameShow"); + }, 0); + }, true); + } + }); + + let navBar = document.getElementById("nav-bar"); + let anchor = navBar.getAttribute("mode") == "text" ? + document.getAnonymousElementByAttribute(aToolbarButton, "class", "toolbarbutton-text") : + document.getAnonymousElementByAttribute(aToolbarButton, "class", "toolbarbutton-badge-container"); + // Bug 849216 - open the popup in a setTimeout so we avoid the auto-rollup + // handling from preventing it being opened in some cases. + setTimeout(function() { + panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); + }, 0); + }, + + setPanelErrorMessage: function SocialToolbar_setPanelErrorMessage(aNotificationFrame) { + if (!aNotificationFrame) + return; + + let src = aNotificationFrame.getAttribute("src"); + aNotificationFrame.removeAttribute("src"); + aNotificationFrame.webNavigation.loadURI("about:socialerror?mode=tryAgainOnly&url=" + + encodeURIComponent(src), null, null, null, null); + let panel = aNotificationFrame.parentNode; + sizeSocialPanelToContent(panel, aNotificationFrame); + }, + + populateProviderMenus: function SocialToolbar_renderProviderMenus() { + let providerMenuSeps = document.getElementsByClassName("social-provider-menu"); + for (let providerMenuSep of providerMenuSeps) + this._populateProviderMenu(providerMenuSep); + }, + + _populateProviderMenu: function SocialToolbar_renderProviderMenu(providerMenuSep) { + let menu = providerMenuSep.parentNode; + // selectable providers are inserted before the provider-menu seperator, + // remove any menuitems in that area + while (providerMenuSep.previousSibling.nodeName == "menuitem") { + menu.removeChild(providerMenuSep.previousSibling); + } + // only show a selection if enabled and there is more than one + let providers = [p for (p of Social.providers) if (p.workerURL || p.sidebarURL)]; + if (providers.length < 2) { + providerMenuSep.hidden = true; + return; + } + for (let provider of providers) { + let menuitem = document.createElement("menuitem"); + menuitem.className = "menuitem-iconic social-provider-menuitem"; + menuitem.setAttribute("image", provider.iconURL); + menuitem.setAttribute("label", provider.name); + menuitem.setAttribute("origin", provider.origin); + if (provider == Social.provider) { + menuitem.setAttribute("checked", "true"); + } else { + menuitem.setAttribute("oncommand", "Social.setProviderByOrigin(this.getAttribute('origin'));"); + } + menu.insertBefore(menuitem, providerMenuSep); + } + providerMenuSep.hidden = false; + } +} + +SocialSidebar = { + // Called once, after window load, when the Social.provider object is initialized + init: function SocialSidebar_init() { + let sbrowser = document.getElementById("social-sidebar-browser"); + Social.setErrorListener(sbrowser, this.setSidebarErrorMessage.bind(this)); + // setting isAppTab causes clicks on untargeted links to open new tabs + sbrowser.docShell.isAppTab = true; + }, + + // Whether the sidebar can be shown for this window. + get canShow() { + return SocialUI.enabled && Social.provider.sidebarURL; + }, + + // Whether the user has toggled the sidebar on (for windows where it can appear) + get opened() { + return Services.prefs.getBoolPref("social.sidebar.open") && !document.mozFullScreen; + }, + + setSidebarVisibilityState: function(aEnabled) { + let sbrowser = document.getElementById("social-sidebar-browser"); + // it's possible we'll be called twice with aEnabled=false so let's + // just assume we may often be called with the same state. + if (aEnabled == sbrowser.docShellIsActive) + return; + sbrowser.docShellIsActive = aEnabled; + let evt = sbrowser.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent(aEnabled ? "socialFrameShow" : "socialFrameHide", true, true, {}); + sbrowser.contentDocument.documentElement.dispatchEvent(evt); + }, + + update: function SocialSidebar_update() { + clearTimeout(this._unloadTimeoutId); + // Hide the toggle menu item if the sidebar cannot appear + let command = document.getElementById("Social:ToggleSidebar"); + command.setAttribute("hidden", this.canShow ? "false" : "true"); + + // Hide the sidebar if it cannot appear, or has been toggled off. + // Also set the command "checked" state accordingly. + let hideSidebar = !this.canShow || !this.opened; + let broadcaster = document.getElementById("socialSidebarBroadcaster"); + broadcaster.hidden = hideSidebar; + command.setAttribute("checked", !hideSidebar); + + let sbrowser = document.getElementById("social-sidebar-browser"); + + if (hideSidebar) { + sbrowser.removeEventListener("load", SocialSidebar._loadListener, true); + this.setSidebarVisibilityState(false); + // If we've been disabled, unload the sidebar content immediately; + // if the sidebar was just toggled to invisible, wait a timeout + // before unloading. + if (!this.canShow) { + this.unloadSidebar(); + } else { + this._unloadTimeoutId = setTimeout( + this.unloadSidebar, + Services.prefs.getIntPref("social.sidebar.unload_timeout_ms") + ); + } + } else { + sbrowser.setAttribute("origin", Social.provider.origin); + if (Social.provider.errorState == "frameworker-error") { + SocialSidebar.setSidebarErrorMessage(); + return; + } + + // Make sure the right sidebar URL is loaded + if (sbrowser.getAttribute("src") != Social.provider.sidebarURL) { + sbrowser.setAttribute("src", Social.provider.sidebarURL); + PopupNotifications.locationChange(sbrowser); + } + + // if the document has not loaded, delay until it is + if (sbrowser.contentDocument.readyState != "complete") { + sbrowser.addEventListener("load", SocialSidebar._loadListener, true); + } else { + this.setSidebarVisibilityState(true); + } + } + }, + + _loadListener: function SocialSidebar_loadListener() { + let sbrowser = document.getElementById("social-sidebar-browser"); + sbrowser.removeEventListener("load", SocialSidebar._loadListener, true); + SocialSidebar.setSidebarVisibilityState(true); + }, + + unloadSidebar: function SocialSidebar_unloadSidebar() { + let sbrowser = document.getElementById("social-sidebar-browser"); + if (!sbrowser.hasAttribute("origin")) + return; + + sbrowser.stop(); + sbrowser.removeAttribute("origin"); + sbrowser.setAttribute("src", "about:blank"); + SocialFlyout.unload(); + }, + + _unloadTimeoutId: 0, + + setSidebarErrorMessage: function() { + let sbrowser = document.getElementById("social-sidebar-browser"); + // a frameworker error "trumps" a sidebar error. + if (Social.provider.errorState == "frameworker-error") { + sbrowser.setAttribute("src", "about:socialerror?mode=workerFailure"); + } else { + let url = encodeURIComponent(Social.provider.sidebarURL); + sbrowser.loadURI("about:socialerror?mode=tryAgain&url=" + url, null, null); + } + } +} + +})(); diff --git a/browser/base/content/browser-syncui.js b/browser/base/content/browser-syncui.js new file mode 100644 index 000000000..7294794e7 --- /dev/null +++ b/browser/base/content/browser-syncui.js @@ -0,0 +1,467 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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("https://services.mozilla.com/update/", "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/browser/base/content/browser-tabPreviews.js b/browser/base/content/browser-tabPreviews.js new file mode 100644 index 000000000..6d639f16e --- /dev/null +++ b/browser/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.contains(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(Ci.nsIPopupBoxObject.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/browser/base/content/browser-tabPreviews.xml b/browser/base/content/browser-tabPreviews.xml new file mode 100644 index 000000000..e957649e7 --- /dev/null +++ b/browser/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/browser/base/content/browser-thumbnails.js b/browser/base/content/browser-thumbnails.js new file mode 100644 index 000000000..dbe33e3ed --- /dev/null +++ b/browser/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/browser/base/content/browser-title.css b/browser/base/content/browser-title.css new file mode 100644 index 000000000..664d4bb93 --- /dev/null +++ b/browser/base/content/browser-title.css @@ -0,0 +1,181 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; +} + +/* 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; +} + + +/* 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; + } + +} + + +/* Windows XP Blue/Olive */ + +@media all and (-moz-windows-theme: luna-blue), all and (-moz-windows-theme: luna-olive) { + + #main-window::after { + font-family: trebuchet MS; + font-size: 13px; + text-shadow: 1px 1px rgba(0, 0, 0, .6); + top: -9px; + background-position: 2px 17px; + } + + #main-window:-moz-window-inactive::after { + text-shadow: none; + } + +} + + +/* Windows XP Silver, Royale, Zune, generic other themes */ + +@media all and (-moz-windows-theme: luna-silver), all and (-moz-windows-theme: royale), all and (-moz-windows-theme: zune), 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 */ + +@media all and (-moz-windows-compositor) { + @media not all and (-moz-windows-glass) { + + #main-window::after { + top: -13px; + font-size: 15px; + background-position: 4px 17px; + text-align: center; + } + + #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/browser/base/content/browser-webrtcUI.js b/browser/base/content/browser-webrtcUI.js new file mode 100644 index 000000000..a6c9008ca --- /dev/null +++ b/browser/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/browser/base/content/browser.css b/browser/base/content/browser.css new file mode 100644 index 000000000..b04241bdc --- /dev/null +++ b/browser/base/content/browser.css @@ -0,0 +1,723 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 + +.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 { + 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(.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; +} + +#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; +} + +#addon-progress-notification { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#addon-progress-notification"); +} + +#identity-request-notification { + -moz-binding: url("chrome://browser/content/urlbarBindings.xml#identity-request-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"); +} + +/* Note the chatbox 'width' values are duplicated in socialchat.xml */ +chatbox { + -moz-binding: url("chrome://browser/content/socialchat.xml#chatbox"); + transition: height 150ms ease-out, width 150ms ease-out; + height: 285px; + width: 260px; /* CHAT_WIDTH_OPEN in socialchat.xml */ +} + +chatbox[minimized="true"] { + width: 160px; + height: 20px; /* CHAT_WIDTH_MINIMIZED in socialchat.xml */ +} + +chatbar { + -moz-binding: url("chrome://browser/content/socialchat.xml#chatbar"); + height: 0; + max-height: 0; +} + +/* full screen chat window support */ +chatbar:-moz-full-screen-ancestor, +chatbox:-moz-full-screen-ancestor { + border: none; + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100% !important; + height: 100% !important; + margin: 0 !important; + min-width: 0 !important; + max-width: none !important; + min-height: 0 !important; + max-height: none !important; + -moz-box-sizing: border-box !important; +} + +/* hide chat chrome when chat is fullscreen */ +chatbox:-moz-full-screen-ancestor > .chat-titlebar { + display: none; +} + +/* hide chatbar if browser tab is fullscreen */ +*:-moz-full-screen-ancestor chatbar:not(:-moz-full-screen-ancestor) { + display: none; +} + +/* hide sidebar when fullscreen */ +*:-moz-full-screen-ancestor #social-sidebar-box:not(:-moz-full-screen-ancestor) { + display: none; +} diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js new file mode 100644 index 000000000..520c74ecc --- /dev/null +++ b/browser/base/content/browser.js @@ -0,0 +1,7265 @@ +# -*- 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"); + +const nsIWebNavigation = Ci.nsIWebNavigation; + +var gCharsetMenu = null; +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, "TelemetryStopwatch", + "resource://gre/modules/TelemetryStopwatch.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils", + "resource:///modules/AboutHomeUtils.jsm"); + +#ifdef MOZ_SERVICES_SYNC +XPCOMUtils.defineLazyModuleGetter(this, "Weave", + "resource://services-sync/main.js"); +#endif + +XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () { + let tmp = {}; + Cu.import("resource://gre/modules/PopupNotifications.jsm", tmp); + try { + return new tmp.PopupNotifications(gBrowser, + document.getElementById("notification-popup"), + document.getElementById("notification-popup-box")); + } catch (ex) { + Cu.reportError(ex); + return null; + } +}); + +XPCOMUtils.defineLazyGetter(this, "DeveloperToolbar", function() { + let tmp = {}; + Cu.import("resource:///modules/devtools/DeveloperToolbar.jsm", tmp); + return new tmp.DeveloperToolbar(window, document.getElementById("developer-toolbar")); +}); + +XPCOMUtils.defineLazyGetter(this, "BrowserDebuggerProcess", function() { + let tmp = {}; + Cu.import("resource:///modules/devtools/DebuggerProcess.jsm", tmp); + return tmp.BrowserDebuggerProcess; +}); + +XPCOMUtils.defineLazyModuleGetter(this, "Social", + "resource:///modules/Social.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs", + "resource://gre/modules/PageThumbs.jsm"); + +#ifdef MOZ_SAFE_BROWSING +XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", + "resource://gre/modules/SafeBrowsing.jsm"); +#endif + +XPCOMUtils.defineLazyModuleGetter(this, "gBrowserNewTabPreloader", + "resource:///modules/BrowserNewTabPreloader.jsm", "BrowserNewTabPreloader"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +let gInitialPages = [ + "about:blank", + "about:newtab", + "about:home", + "about:privatebrowsing", + "about:sessionrestore" +]; + +#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-safebrowsing.js +#include browser-social.js +#include browser-tabPreviews.js +#include browser-thumbnails.js +#include browser-webrtcUI.js +#include browser-gestureSupport.js + +#ifdef MOZ_DATA_REPORTING +#include browser-data-submission-info-bar.js +#endif + +#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; +}); + +#ifdef MOZ_CRASHREPORTER +XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter", + "@mozilla.org/xre/app-info;1", + "nsICrashReporter"); +#endif + +XPCOMUtils.defineLazyGetter(this, "PageMenu", function() { + let tmp = {}; + Cu.import("resource://gre/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.nsIDocShellTreeNode); + 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.pageReport) { + // 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.pageReport.reported) { + if (gPrefService.getBoolPref("privacy.popups.showBrowserMessage")) { + var brandBundle = document.getElementById("bundle_brand"); + var brandShortName = brandBundle.getString("brandShortName"); + var popupCount = gBrowser.pageReport.length; +#ifdef XP_WIN + var popupButtonText = gNavigatorBundle.getString("popupWarningButton"); + var popupButtonAccesskey = gNavigatorBundle.getString("popupWarningButton.accesskey"); +#else + var popupButtonText = gNavigatorBundle.getString("popupWarningButtonUnix"); + var popupButtonAccesskey = gNavigatorBundle.getString("popupWarningButtonUnix.accesskey"); +#endif + 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.pageReport.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 pageReport 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. + var uri = gBrowser.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 pageReports = gBrowser.pageReport; + if (pageReports) { + for (let pageReport of pageReports) { + // 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 (!pageReport.popupWindowURI) + continue; + var popupURIspec = pageReport.popupWindowURI.spec; + + // Sometimes the popup URI that we get back from the pageReport + // 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", pageReport.popupWindowFeatures); + menuitem.setAttribute("popupWindowName", pageReport.popupWindowName); + menuitem.setAttribute("oncommand", "gPopupBlockerObserver.showBlockedPopup(event);"); + menuitem.requestingWindow = pageReport.requestingWindow; + menuitem.requestingDocument = pageReport.requestingDocument; + 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 popupWindowURI = target.getAttribute("popupWindowURI"); + var features = target.getAttribute("popupWindowFeatures"); + var name = target.getAttribute("popupWindowName"); + + var dwi = target.requestingWindow; + + // If we have a requesting window and the requesting document is + // still the current document, open the popup. + if (dwi && dwi.document == target.requestingDocument) { + dwi.open(popupWindowURI, name, features); + } + }, + + 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 gFormSubmitObserver = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), + + panel: null, + + init: function() + { + this.panel = document.getElementById('invalid-form-popup'); + }, + + notifyInvalidSubmit : function (aFormElement, aInvalidElements) + { + // We are going to handle invalid form submission attempt by focusing the + // first invalid element and show the corresponding validation message in a + // panel attached to the element. + if (!aInvalidElements.length) { + return; + } + + // Don't show the popup if the current tab doesn't contain the invalid form. + if (gBrowser.contentDocument != + aFormElement.ownerDocument.defaultView.top.document) { + return; + } + + let element = aInvalidElements.queryElementAt(0, Ci.nsISupports); + + if (!(element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement || + element instanceof HTMLSelectElement || + element instanceof HTMLButtonElement)) { + return; + } + + this.panel.firstChild.textContent = element.validationMessage; + + element.focus(); + + // If the user interacts with the element and makes it valid or leaves it, + // we want to remove the popup. + // We could check for clicks but a click is already removing the popup. + function blurHandler() { + gFormSubmitObserver.panel.hidePopup(); + }; + function inputHandler(e) { + if (e.originalTarget.validity.valid) { + gFormSubmitObserver.panel.hidePopup(); + } else { + // If the element is now invalid for a new reason, we should update the + // error message. + if (gFormSubmitObserver.panel.firstChild.textContent != + e.originalTarget.validationMessage) { + gFormSubmitObserver.panel.firstChild.textContent = + e.originalTarget.validationMessage; + } + } + }; + element.addEventListener("input", inputHandler, false); + element.addEventListener("blur", blurHandler, false); + + // One event to bring them all and in the darkness bind them. + this.panel.addEventListener("popuphiding", function onPopupHiding(aEvent) { + aEvent.target.removeEventListener("popuphiding", onPopupHiding, false); + element.removeEventListener("input", inputHandler, false); + element.removeEventListener("blur", blurHandler, false); + }, false); + + this.panel.hidden = false; + + // We want to show the popup at the middle of checkbox and radio buttons + // and where the content begin for the other elements. + let offset = 0; + let position = ""; + + if (element.tagName == 'INPUT' && + (element.type == 'radio' || element.type == 'checkbox')) { + position = "bottomcenter topleft"; + } else { + let win = element.ownerDocument.defaultView; + let style = win.getComputedStyle(element, null); + let utils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils); + + if (style.direction == 'rtl') { + offset = parseInt(style.paddingRight) + parseInt(style.borderRightWidth); + } else { + offset = parseInt(style.paddingLeft) + parseInt(style.borderLeftWidth); + } + + offset = Math.round(offset * utils.fullZoom); + + position = "after_start"; + } + + this.panel.openPopup(element, position, offset, 0); + } +}; + +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); + + gBrowser.addEventListener("NewPluginInstalled", gPluginHandler.newPluginInstalled, 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 + 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); + // listen for offline apps on social + let socialBrowser = document.getElementById("social-sidebar-browser"); + socialBrowser.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(); + + // 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 = {}; + Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", tmp); + let TelemetryTimestamps = tmp.TelemetryTimestamps; + TelemetryTimestamps.add("delayedStartupStarted"); + + 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); + } + +#ifdef MOZ_SAFE_BROWSING + // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008. + setTimeout(function() { SafeBrowsing.init(); }, 2000); +#endif + + Services.obs.addObserver(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-failed", false); + Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false); + Services.obs.addObserver(gFormSubmitObserver, "invalidformsubmit", false); + + BrowserOffline.init(); + OfflineApps.init(); + IndexedDBPromptHelper.init(); + gFormSubmitObserver.init(); + SocialUI.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); + ss.init(window); + + // Enable the Restore Last Session command if needed + if (ss.canRestoreLastSession && + !PrivateBrowsingUtils.isWindowPrivate(window)) + goSetCommandEnabled("Browser:RestoreLastSession", true); + + 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() { + Services.downloads; + let DownloadTaskbarProgress = + Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress; + DownloadTaskbarProgress.onBrowserWindowLoad(window); + }, 10000); + + // 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 + +#ifdef MOZ_DATA_REPORTING + gDataNotificationInfoBar.init(); +#endif + + gBrowserThumbnails.init(); + + setUrlAndSearchBarWidthForConditionalForwardButton(); + window.addEventListener("resize", function resizeHandler(event) { + if (event.target == window) + setUrlAndSearchBarWidthForConditionalForwardButton(); + }); + + // Enable developer toolbar? + let devToolbarEnabled = gPrefService.getBoolPref("devtools.toolbar.enabled"); + if (devToolbarEnabled) { + let cmd = document.getElementById("Tools:DevToolbar"); + cmd.removeAttribute("disabled"); + cmd.removeAttribute("hidden"); + document.getElementById("Tools:DevToolbarFocus").removeAttribute("disabled"); + + // Show the toolbar if it was previously visible + if (gPrefService.getBoolPref("devtools.toolbar.visible")) { + DeveloperToolbar.show(false); + } + } + + // 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 Error Console? + let consoleEnabled = gPrefService.getBoolPref("devtools.errorconsole.enabled"); + if (consoleEnabled) { + let cmd = document.getElementById("Tools:ErrorConsole"); + 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"); + } + + // Enable DevTools connection screen, if the preference allows this. + let devtoolsRemoteEnabled = gPrefService.getBoolPref("devtools.debugger.remote-enabled"); + if (devtoolsRemoteEnabled) { + let cmd = document.getElementById("Tools:DevToolsConnect"); + 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 + + // 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); + + 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; + Services.telemetry.getHistogramById("FX_APP_MENU_OPEN_MS").add(duration); + }, 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); + } + +#ifdef XP_WIN +#ifdef MOZ_METRO + gMetroPrefs.prefDomain.forEach(function(prefName) { + gMetroPrefs.pushDesktopControlledPrefToMetro(prefName); + Services.prefs.addObserver(prefName, gMetroPrefs, false); + }, this); +#endif +#endif + + Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); + setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0); + TelemetryTimestamps.add("delayedStartupFinished"); + }, + + // 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; + + gDevToolsBrowser.forgetBrowserWindow(window); + + let desc = Object.getOwnPropertyDescriptor(window, "DeveloperToolbar"); + if (desc && !desc.get) { + DeveloperToolbar.destroy(); + } + + // 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(); + + 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(); + 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-failed"); + Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete"); + Services.obs.removeObserver(gFormSubmitObserver, "invalidformsubmit"); + + try { + gPrefService.removeObserver(gHomeButton.prefDomain, gHomeButton); + } catch (ex) { + Cu.reportError(ex); + } + +#ifdef XP_WIN +#ifdef MOZ_METRO + gMetroPrefs.prefDomain.forEach(function(prefName) { + Services.prefs.removeObserver(prefName, gMetroPrefs); + }); +#endif +#endif + + BrowserOffline.uninit(); + OfflineApps.uninit(); + IndexedDBPromptHelper.uninit(); + AddonManager.removeAddonListener(AddonsMgrListener); + SocialUI.uninit(); + } + + // 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) { + } + } + } + + SocialUI.nonBrowserWindowInit(); + + 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; + + 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); + } + + gURLBar.value = value; + gURLBar.valueIsTyped = !valid; + SetPageProxyState(valid ? "valid" : "invalid"); +} + +function losslessDecodeURI(aURI) { + 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)) + 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 (line and paragraph separator, + // object replacement character) (bug 452979) + value = value.replace(/[\v\x0c\x1c\x1d\x1e\x1f\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/content/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") { + // XXX bug 738646 - when Marketplace is launched, remove this statement and + // the hidden attribute set on the apps button in aboutHome.xhtml + if (getBoolPref("browser.aboutHome.apps", false)) + doc.getElementById("apps").removeAttribute("hidden"); + + 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); + +#ifdef MOZ_SERVICES_HEALTHREPORT + doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) { + BrowserSearch.recordSearchInHealthReport(e.detail, "abouthome"); + }, true, true); +#endif + } +} + +/** + * 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:blocked")) { + this.onAboutBlocked(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 secHistogram = Services.telemetry.getHistogramById("SECURITY_UI"); + let isTopFrame = (aOwnerDoc.defaultView.parent === aOwnerDoc.defaultView); + + switch (elmId) { + case "exceptionDialogButton": + if (isTopFrame) { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_CLICK_ADD_EXCEPTION); + } + 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": + if (isTopFrame) { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_GET_ME_OUT_OF_HERE); + } + getMeOutOfHere(); + break; + + case "technicalContent": + if (isTopFrame) { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_TECHNICAL_DETAILS); + } + break; + + case "expertContent": + if (isTopFrame) { + secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_UNDERSTAND_RISKS); + } + break; + + } + }, + + onAboutBlocked: function BrowserOnClick_onAboutBlocked(aTargetElm, aOwnerDoc) { + let elmId = aTargetElm.getAttribute("id"); + let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI"); + + // The event came from a button on a malware/phishing block page + // First check whether it's malware or phishing, so that we can + // use the right strings/links + let isMalware = /e=malwareBlocked/.test(aOwnerDoc.documentURI); + let bucketName = isMalware ? "WARNING_MALWARE_PAGE_":"WARNING_PHISHING_PAGE_"; + let nsISecTel = Ci.nsISecurityUITelemetry; + let isIframe = (aOwnerDoc.defaultView.parent === aOwnerDoc.defaultView); + bucketName += isIframe ? "TOP_" : "FRAME_"; + + switch (elmId) { + case "getMeOutButton": + secHistogram.add(nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]); + getMeOutOfHere(); + break; + + case "reportButton": + // This is the "Why is this site blocked" button. For malware, + // we can fetch a site-specific report, for phishing, we redirect + // to the generic page describing phishing protection. + + // We log even if malware/phishing info URL couldn't be found: + // the measurement is for how many users clicked the WHY BLOCKED button + secHistogram.add(nsISecTel[bucketName + "WHY_BLOCKED"]); + + if (isMalware) { + // Get the stop badware "why is this blocked" report url, + // append the current url, and go there. + try { + let reportURL = formatURL("browser.safebrowsing.malware.reportURL", true); + reportURL += aOwnerDoc.location.href; + content.location = reportURL; + } catch (e) { + Components.utils.reportError("Couldn't get malware report URL: " + e); + } + } + else { // It's a phishing site, not malware + try { + content.location = formatURL("browser.safebrowsing.warning.infoURL", true); + } catch (e) { + Components.utils.reportError("Couldn't get phishing info URL: " + e); + } + } + break; + + case "ignoreWarningButton": + secHistogram.add(nsISecTel[bucketName + "IGNORE_WARNING"]); + this.ignoreWarningButton(isMalware); + break; + } + }, + + ignoreWarningButton: function BrowserOnClick_ignoreWarningButton(aIsMalware) { + // Allow users to override and continue through to the site, + // but add a notify bar as a reminder, so that they don't lose + // track after, e.g., tab switching. + gBrowser.loadURIWithFlags(content.location.href, + nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, + null, null, null); + + Services.perms.add(makeURI(content.location.href), "safe-browsing", + Ci.nsIPermissionManager.ALLOW_ACTION, + Ci.nsIPermissionManager.EXPIRE_SESSION); + + let buttons = [{ + label: gNavigatorBundle.getString("safebrowsing.getMeOutOfHereButton.label"), + accessKey: gNavigatorBundle.getString("safebrowsing.getMeOutOfHereButton.accessKey"), + callback: function() { getMeOutOfHere(); } + }]; + + let title; + if (aIsMalware) { + title = gNavigatorBundle.getString("safebrowsing.reportedAttackSite"); + buttons[1] = { + label: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.label"), + accessKey: gNavigatorBundle.getString("safebrowsing.notAnAttackButton.accessKey"), + callback: function() { + openUILinkIn(gSafeBrowsing.getReportURL('MalwareError'), 'tab'); + } + }; + } else { + title = gNavigatorBundle.getString("safebrowsing.reportedWebForgery"); + buttons[1] = { + label: gNavigatorBundle.getString("safebrowsing.notAForgeryButton.label"), + accessKey: gNavigatorBundle.getString("safebrowsing.notAForgeryButton.accessKey"), + callback: function() { + openUILinkIn(gSafeBrowsing.getReportURL('Error'), 'tab'); + } + }; + } + + let notificationBox = gBrowser.getNotificationBox(); + let value = "blocked-badware-page"; + + let previousNotification = notificationBox.getNotificationWithValue(value); + if (previousNotification) { + notificationBox.removeNotification(previousNotification); + } + + let notification = notificationBox.appendNotification( + title, + value, + "chrome://global/skin/icons/blacklist_favicon.png", + notificationBox.PRIORITY_CRITICAL_HIGH, + buttons + ); + // Persist the notification until the user removes so it + // doesn't get removed on redirects. + notification.persistence = -1; + }, + + 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 "apps": + openUILinkIn("https://marketplace.mozilla.org/", "tab"); + 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() { + // 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.contains("|")) + 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 () { + 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, rels.feed)) { + 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"); + if (engine) { + BrowserSearch.recordSearchInHealthReport(engine, "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); + }, + + /** + * Helper to record a search with Firefox Health Report. + * + * FHR records only search counts and nothing pertaining to the search itself. + * + * @param engine + * (string) The name of the engine used to perform the search. This + * is typically nsISearchEngine.name. + * @param source + * (string) Where the search originated from. See the FHR + * SearchesProvider for allowed values. + */ + recordSearchInHealthReport: function (engine, source) { +#ifdef MOZ_SERVICES_HEALTHREPORT + let reporter = Cc["@mozilla.org/datareporting/service;1"] + .getService() + .wrappedJSObject + .healthReporter; + + // This can happen if the FHR component of the data reporting service is + // disabled. This is controlled by a pref that most will never use. + if (!reporter) { + return; + } + + reporter.onInit().then(function record() { + try { + reporter.getProvider("org.mozilla.searches").recordSearch(engine, source); + } catch (ex) { + Cu.reportError(ex); + } + }); +#endif + }, +}; + +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.contains(" ") && + !/[\x00-\x1F]/.test(aUrlToAdd)) + PlacesUIUtils.markPageAsTyped(aUrlToAdd); +} + +function toJavaScriptConsole() +{ + toOpenWindowByType("global:console", "chrome://global/content/console.xul"); +} + +function BrowserDownloadsUI() +{ + Cc["@mozilla.org/download-manager-ui;1"]. + getService(Ci.nsIDownloadManagerUI).show(window); +} + +function toOpenWindowByType(inType, uri, features) +{ + var topWindow = Services.wm.getMostRecentWindow(inType); + + if (topWindow) + topWindow.focus(); + else if (features) + window.open(uri, "_blank", features); + else + window.open(uri, "_blank", "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar"); +} + +function OpenBrowserWindow(options) +{ + var telemetryObj = {}; + TelemetryStopwatch.start("FX_NEW_WINDOW_MS", telemetryObj); + + function newDocumentShown(doc, topic, data) { + if (topic == "document-shown" && + doc != document && + doc.defaultView == win) { + Services.obs.removeObserver(newDocumentShown, "document-shown"); + TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj); + } + }; + 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(); + SocialMark.updateMarkState(); + SocialShare.update(); + } + + 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", "about:preferences"],*/ + 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); + SocialUI.closeSocialPanelForLinkTraversal(target, linkNode); + 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; + + // Hide the form invalid popup. + if (gFormSubmitObserver.panel) { + gFormSubmitObserver.panel.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(); + SocialMark.updateMarkState(); + SocialShare.update(); + } + + // 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(); + } + + // 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) { +#ifdef MOZ_CRASHREPORTER + if (aRequest instanceof Ci.nsIChannel && + aStateFlags & Ci.nsIWebProgressListener.STATE_START && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT && + gCrashReporter.enabled) { + gCrashReporter.annotateCrashReport("URL", aRequest.URI.spec); + } +#endif + + // Collect telemetry data about tab load times. + if (aWebProgress.isTopLevel) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) + TelemetryStopwatch.start("FX_PAGE_LOAD_MS", aBrowser); + else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) + TelemetryStopwatch.finish("FX_PAGE_LOAD_MS", aBrowser); + } else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStatus == Cr.NS_BINDING_ABORTED) { + TelemetryStopwatch.cancel("FX_PAGE_LOAD_MS", aBrowser); + } + } + + // 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); + + 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); + + PlacesToolbarHelper.init(); + BookmarkingUI.onToolbarVisibilityChange(); + gBrowser.updateWindowResizers(); + +#ifdef MENUBAR_CAN_AUTOHIDE + updateAppButtonDisplay(); +#endif +} + +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 = ""; + } + }, + + _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); +} + +#ifdef XP_WIN +#ifdef MOZ_METRO +/** + * Some prefs that have consequences in both Metro and Desktop such as + * app-update prefs, are automatically pushed from Desktop here for use + * in Metro. + */ +var gMetroPrefs = { + prefDomain: ["app.update.auto", "app.update.enabled", + "app.update.service.enabled", + "app.update.metro.enabled"], + observe: function (aSubject, aTopic, aPrefName) + { + if (aTopic != "nsPref:changed") + return; + + this.pushDesktopControlledPrefToMetro(aPrefName); + }, + + /** + * Writes the pref to HKCU in the registry and adds a pref-observer to keep + * the registry in sync with changes to the value. + */ + pushDesktopControlledPrefToMetro: function(aPrefName) { + let registry = Cc["@mozilla.org/windows-registry-key;1"]. + createInstance(Ci.nsIWindowsRegKey); + try { + var prefType = Services.prefs.getPrefType(aPrefName); + let prefFunc; + if (prefType == Components.interfaces.nsIPrefBranch.PREF_INT) + prefFunc = "getIntPref"; + else if (prefType == Components.interfaces.nsIPrefBranch.PREF_BOOL) + prefFunc = "getBoolPref"; + else if (prefType == Components.interfaces.nsIPrefBranch.PREF_STRING) + prefFunc = "getCharPref"; + else + throw "Unsupported pref type"; + + let prefValue = Services.prefs[prefFunc](aPrefName); + registry.create(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, + "Software\\Mozilla\\Firefox\\Metro\\Prefs\\" + prefType, + Ci.nsIWindowsRegKey.ACCESS_WRITE); + // Always write as string, but the registry subfolder will determine + // how Metro interprets that string value. + registry.writeStringValue(aPrefName, prefValue); + } catch (ex) { + Components.utils.reportError("Couldn't push pref " + aPrefName + ": " + ex); + } finally { + registry.close(); + } + } +}; +#endif +#endif + +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) { + href = node.getAttributeNS("http://www.w3.org/1999/xlink", "href"); + if (href) + baseURI = node.baseURI; + } + 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 gecko 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.'.length, 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('chardet.'.length, uri.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.gatherCharsetMenuTelemetry(); + 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 CreateMenu(node) { + Services.obs.notifyObservers(null, "charsetmenu-selected", node); +} + +function charsetLoadListener() { + var charset = window.content.document.characterSet; + + if (charset.length > 0 && (charset != gLastBrowserCharset)) { + if (!gCharsetMenu) + gCharsetMenu = Cc['@mozilla.org/rdf/datasource;1?name=charset-menu'].getService(Ci.nsICurrentCharsetListener); + gCharsetMenu.SetCurrentCharset(charset); + 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 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 helplink = document.getElementById("mixed-content-blocked-helplink"); + helplink.href = Services.urlFormatter.formatURLPref("browser.mixedcontent.warning.infoURL"); + + 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, + }; + 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() { + if (!this._IDNService) + this._IDNService = Cc["@mozilla.org/network/idn-service;1"] + .getService(Ci.nsIIDNService); + 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"; + + switch (newMode) { + case this.IDENTITY_MODE_DOMAIN_VERIFIED: { + let iData = this.getIdentityData(); + + //Pale Moon: honor browser.identity.ssl_domain_display! + switch (gPrefService.getIntPref("browser.identity.ssl_domain_display")) { + case 2 : // Show full domain + icon_label = this._lastLocation.hostname; + break; + case 1 : // Show eTLD. + icon_label = this.getEffectiveHost(); + } + + // 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"); + } + + // 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) { + TelemetryStopwatch.start("FX_IDENTITY_POPUP_OPEN_MS"); + event.stopPropagation(); + + if ((event.type == "click" && event.button != 0) || + (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE && + event.keyCode != KeyEvent.DOM_VK_RETURN)) { + TelemetryStopwatch.cancel("FX_IDENTITY_POPUP_OPEN_MS"); + 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") { + TelemetryStopwatch.cancel("FX_IDENTITY_POPUP_OPEN_MS"); + 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) { + TelemetryStopwatch.finish("FX_IDENTITY_POPUP_OPEN_MS"); + 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(); + } +}; + +XPCOMUtils.defineLazyModuleGetter(this, "gDevTools", + "resource:///modules/devtools/gDevTools.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "gDevToolsBrowser", + "resource:///modules/devtools/gDevTools.jsm"); + +XPCOMUtils.defineLazyGetter(this, "HUDConsoleUI", function () { + return Cu.import("resource:///modules/HUDService.jsm", {}).HUDService.consoleUI; +}); + +// Prompt user to restart the browser in safe mode +function safeModeRestart() +{ + // prompt the user to confirm + let promptTitle = gNavigatorBundle.getString("safeModeRestartPromptTitle"); + let promptMessage = + gNavigatorBundle.getString("safeModeRestartPromptMessage"); + let restartText = gNavigatorBundle.getString("safeModeRestartButton"); + 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) { + Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit); + } +} + +/* 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); +} + +var Scratchpad = { + prefEnabledName: "devtools.scratchpad.enabled", + + openScratchpad: function SP_openScratchpad() { + return this.ScratchpadManager.openScratchpad(); + } +}; + +XPCOMUtils.defineLazyGetter(Scratchpad, "ScratchpadManager", function() { + let tmp = {}; + Cu.import("resource:///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:///modules/devtools/responsivedesign.jsm", tmp); + return tmp.ResponsiveUIManager; +}); + +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; + } +}; diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul new file mode 100644 index 000000000..69bf474ca --- /dev/null +++ b/browser/base/content/browser.xul @@ -0,0 +1,1116 @@ +#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"?> + +# Pale Moon: 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"?> +<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?> +<?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"?> + +# Pale Moon: padlock feature +<?xul-overlay href="chrome://browser/content/padlock.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;@PRE_RELEASE_SUFFIX@" + title_normal="&mainWindow.title;@PRE_RELEASE_SUFFIX@" +#ifdef XP_MACOSX + title_privatebrowsing="&mainWindow.title;@PRE_RELEASE_SUFFIX@&mainWindow.titlemodifiermenuseparator;&mainWindow.titlePrivateBrowsingSuffix;" + titledefault="&mainWindow.title;@PRE_RELEASE_SUFFIX@" + titlemodifier="" + titlemodifier_normal="" + titlemodifier_privatebrowsing="&mainWindow.titlePrivateBrowsingSuffix;" +#else + title_privatebrowsing="&mainWindow.titlemodifier;@PRE_RELEASE_SUFFIX@ &mainWindow.titlePrivateBrowsingSuffix;" + titlemodifier="&mainWindow.titlemodifier;@PRE_RELEASE_SUFFIX@" + titlemodifier_normal="&mainWindow.titlemodifier;@PRE_RELEASE_SUFFIX@" + titlemodifier_privatebrowsing="&mainWindow.titlemodifier;@PRE_RELEASE_SUFFIX@ &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> + + <panel id="socialActivatedNotification" + type="arrow" + hidden="true" + consumeoutsideclicks="true" + align="start" + orient="horizontal" + role="alert"> + <image id="social-activation-icon" class="popup-notification-icon"/> + <vbox flex="1"> + <description id="social-activation-message" class="popup-notification-description">&social.activated.description;</description> + <spacer flex="1"/> + <hbox pack="start" align="center" class="popup-notification-button-container"> + <label id="social-undoactivation-button" + class="text-link" + value="&social.activated.undo.label;" + accesskey="&social.activated.undo.accesskey;" + onclick="SocialUI.undoActivation(this);"/> + <spacer flex="1"/> + <button id="social-activation-button" + default="true" + autofocus="autofocus" + label="&social.ok.label;" + accesskey="&social.ok.accesskey;" + oncommand="SocialUI.activationPanel.hidePopup();"/> + </hbox> + </vbox> + </panel> + + <panel id="social-share-panel" + class="social-panel" + type="arrow" + orient="horizontal" + onpopupshowing="SocialShare.onShowing()" + onpopuphidden="SocialShare.onHidden()" + consumeoutsideclicks="true" + hidden="true"> + <vbox class="social-share-toolbar"> + <vbox id="social-share-provider-buttons" flex="1"/> + </vbox> + </panel> + + <panel id="social-notification-panel" + class="social-panel" + type="arrow" + hidden="true" + noautofocus="true"/> + <panel id="social-flyout-panel" + class="social-panel" + onpopupshown="SocialFlyout.onShown()" + onpopuphidden="SocialFlyout.onHidden()" + side="right" + type="arrow" + hidden="true" + flip="slide" + rolluponmousewheel="true" + consumeoutsideclicks="false" + noautofocus="true" + position="topcenter topright"/> + + <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" + 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;" + 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,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="identity-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="plugin-install-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;" + 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="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" + label="&palemoon.menu.allBookmarks.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" + label="&bookmarksMenuButton.unsorted.label;" + oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');" + class="menuitem-iconic"/> + </menupopup> + </toolbarbutton> + + <toolbarbutton id="social-share-button" + class="toolbarbutton-1 chromeclass-toolbar-additional" + hidden="true" + label="&sharePageCmd.label;" + tooltiptext="&sharePageCmd.label;" + command="Social:SharePage"/> + + <toolbaritem id="social-toolbar-item" + class="chromeclass-toolbar-additional" + removable="false" + title="&socialToolbar.title;" + hidden="true" + skipintoolbarset="true" + observes="socialActiveBroadcaster"> + <toolbarbutton id="social-notification-icon" class="default-notification-icon toolbarbutton-1 notification-anchor-icon" + oncommand="PopupNotifications._reshowNotifications(this, + document.getElementById('social-sidebar-browser'));"/> + <toolbarbutton id="social-provider-button" + class="toolbarbutton-1" + type="menu"> + <menupopup id="social-statusarea-popup"> + <menuitem class="social-statusarea-user menuitem-iconic" pack="start" align="center" + observes="socialBroadcaster_userDetails" + oncommand="SocialUI.showProfile(); document.getElementById('social-statusarea-popup').hidePopup();"> + <image class="social-statusarea-user-portrait" + observes="socialBroadcaster_userDetails"/> + <vbox> + <label class="social-statusarea-loggedInStatus" + observes="socialBroadcaster_userDetails"/> + </vbox> + </menuitem> +#ifndef XP_WIN + <menuseparator class="social-statusarea-separator"/> +#endif + <menuitem class="social-toggle-sidebar-menuitem" + type="checkbox" + autocheck="false" + command="Social:ToggleSidebar" + label="&social.toggleSidebar.label;" + accesskey="&social.toggleSidebar.accesskey;"/> + <menuitem class="social-toggle-notifications-menuitem" + type="checkbox" + autocheck="false" + command="Social:ToggleNotifications" + label="&social.toggleNotifications.label;" + accesskey="&social.toggleNotifications.accesskey;"/> + <menuitem class="social-toggle-menuitem" command="Social:Toggle"/> + <menuseparator/> + <menuseparator class="social-provider-menu" hidden="true"/> + <menuitem class="social-addons-menuitem" command="Social:Addons" + label="&social.addons.label;"/> + <menuitem label="&social.learnMore.label;" + accesskey="&social.learnMore.accesskey;" + oncommand="SocialUI.showLearnMore();"/> + </menupopup> + </toolbarbutton> + <toolbarbutton id="social-mark-button" + class="toolbarbutton-1" + hidden="true" + disabled="true" + command="Social:TogglePageMark"/> + </toolbaritem> + + <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" + stopwatchid="FX_TAB_CLICK_MS"> + <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" + 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" + 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" 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> + <splitter id="social-sidebar-splitter" + class="chromeclass-extrachrome sidebar-splitter" + observes="socialSidebarBroadcaster"/> + <vbox id="social-sidebar-box" + class="chromeclass-extrachrome" + observes="socialSidebarBroadcaster" + persist="width"> + <browser id="social-sidebar-browser" + type="content" + context="contentAreaContextMenu" + disableglobalhistory="true" + tooltip="aHTMLTooltip" + popupnotificationanchor="social-notification-icon" + flex="1" + style="min-width: 14em; width: 18em; max-width: 36em;"/> + </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"/> + <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> + + <toolbar id="addon-bar" + toolbarname="&palemoon.menu.statusBar.label;" accesskey="&palemoon.menu.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" + 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/browser/base/content/browserMountPoints.inc b/browser/base/content/browserMountPoints.inc new file mode 100644 index 000000000..e4315b04a --- /dev/null +++ b/browser/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/browser/base/content/chatWindow.xul b/browser/base/content/chatWindow.xul new file mode 100644 index 000000000..4b240acf6 --- /dev/null +++ b/browser/base/content/chatWindow.xul @@ -0,0 +1,109 @@ +#filter substitution +<?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/content/browser.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?> +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +#include browser-doctype.inc + +<window id="chat-window" + windowtype="Social:Chat" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&mainWindow.title;@PRE_RELEASE_SUFFIX@" + onload="gChatWindow.onLoad();" + onunload="gChatWindow.onUnload();" + macanimationtype="document" + fullscreenbutton="true" +# width and height are also used in socialchat.xml: chatbar dragend handler + width="400px" + height="420px" + 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://browser/content/nsContextMenu.js"/> + +#include global-scripts.inc + +<script type="application/javascript"> + +var gChatWindow = { + // cargo-culted from browser.js for nonBrowserStartup, but we're slightly + // different what what we need to leave enabled + onLoad: function() { + // Disable inappropriate commands / submenus + var disabledItems = ['Browser:SavePage', + 'Browser:SendLink', 'cmd_pageSetup', 'cmd_print', 'cmd_find', 'cmd_findAgain', + 'viewToolbarsMenu', 'viewSidebarMenuMenu', + 'viewFullZoomMenu', 'pageStyleMenu', 'charsetMenu', + 'viewHistorySidebar', 'Browser:AddBookmarkAs', 'Browser:BookmarkAllTabs', + 'Browser:ToggleTabView', 'Browser:ToggleAddonBar']; + + for (let disabledItem of disabledItems) { + document.getElementById(disabledItem).setAttribute("disabled", "true"); + } + + // initialise the offline listener + BrowserOffline.init(); + }, + + onUnload: function() { + BrowserOffline.uninit(); + } +} + +// define a popupnotifications handler for this window. we don't use +// an iconbox here, and only support the browser frame for chat. +XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () { + let tmp = {}; + Cu.import("resource://gre/modules/PopupNotifications.jsm", tmp); + try { + return new tmp.PopupNotifications(document.getElementById("chatter").content, + document.getElementById("notification-popup"), + null); + } catch (ex) { + console.error(ex); + return null; + } +}); + +</script> + +#include browser-sets.inc + +#ifdef XP_MACOSX +#include browser-menubar.inc +#endif + + <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> + +#include popup-notifications.inc + + </popupset> + + <commandset id="editMenuCommands"/> + <chatbox id="chatter" flex="1"/> +</window> diff --git a/browser/base/content/content.js b/browser/base/content/content.js new file mode 100644 index 000000000..314d9eb98 --- /dev/null +++ b/browser/base/content/content.js @@ -0,0 +1,45 @@ +/* -*- 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, + "LoginManagerContent", "resource://gre/modules/LoginManagerContent.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); +}); + +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("DOMContentLoaded", function(event) { + LoginManagerContent.onContentLoaded(event); +}); +addEventListener("DOMAutoComplete", function(event) { + LoginManagerContent.onUsernameInput(event); +}); +addEventListener("blur", function(event) { + LoginManagerContent.onUsernameInput(event); +}); diff --git a/browser/base/content/downloadManagerOverlay.xul b/browser/base/content/downloadManagerOverlay.xul new file mode 100644 index 000000000..9987820cb --- /dev/null +++ b/browser/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/browser/base/content/global-scripts.inc b/browser/base/content/global-scripts.inc new file mode 100644 index 000000000..b4de574ae --- /dev/null +++ b/browser/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/browser/base/content/hiddenWindow.xul b/browser/base/content/hiddenWindow.xul new file mode 100644 index 000000000..bf201fd60 --- /dev/null +++ b/browser/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/browser/base/content/highlighter.css b/browser/base/content/highlighter.css new file mode 100644 index 000000000..8fb9d8085 --- /dev/null +++ b/browser/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/browser/base/content/imagedocument.png b/browser/base/content/imagedocument.png Binary files differnew file mode 100644 index 000000000..ff4f21f9a --- /dev/null +++ b/browser/base/content/imagedocument.png diff --git a/browser/base/content/jsConsoleOverlay.xul b/browser/base/content/jsConsoleOverlay.xul new file mode 100644 index 000000000..1bc518d4f --- /dev/null +++ b/browser/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/browser/base/content/macBrowserOverlay.xul b/browser/base/content/macBrowserOverlay.xul new file mode 100644 index 000000000..a4d583e16 --- /dev/null +++ b/browser/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/browser/base/content/newtab/cells.js b/browser/base/content/newtab/cells.js new file mode 100644 index 000000000..23cbd23bf --- /dev/null +++ b/browser/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() 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/browser/base/content/newtab/drag.js b/browser/base/content/newtab/drag.js new file mode 100644 index 000000000..dfbe8199a --- /dev/null +++ b/browser/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() this._draggedSite, + + /** + * The cell width/height at the point the drag started. + */ + _cellWidth: null, + _cellHeight: null, + get cellWidth() this._cellWidth, + get cellHeight() 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(function () scrollbox.removeChild(dragElement), 0); + } +}; diff --git a/browser/base/content/newtab/dragDataHelper.js b/browser/base/content/newtab/dragDataHelper.js new file mode 100644 index 000000000..a66e4e87e --- /dev/null +++ b/browser/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/browser/base/content/newtab/drop.js b/browser/base/content/newtab/drop.js new file mode 100644 index 000000000..7363396e8 --- /dev/null +++ b/browser/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(function (aSite) aSite.pin(sites.indexOf(aSite)), this); + }, + + /** + * 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/browser/base/content/newtab/dropPreview.js b/browser/base/content/newtab/dropPreview.js new file mode 100644 index 000000000..903762345 --- /dev/null +++ b/browser/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/browser/base/content/newtab/dropTargetShim.js b/browser/base/content/newtab/dropTargetShim.js new file mode 100644 index 000000000..a85a6ccd6 --- /dev/null +++ b/browser/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/browser/base/content/newtab/grid.js b/browser/base/content/newtab/grid.js new file mode 100644 index 000000000..120691951 --- /dev/null +++ b/browser/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() this._node, + + /** + * The cached DOM fragment for sites. + */ + _siteFragment: null, + + /** + * All cells contained in the grid. + */ + _cells: null, + get cells() this._cells, + + /** + * All sites contained in the grid's cells. Sites may be empty. + */ + get sites() [cell.site for each (cell in this.cells)], + + // Tells whether the grid has already been initialized. + get ready() !!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/browser/base/content/newtab/newTab.css b/browser/base/content/newtab/newTab.css new file mode 100644 index 000000000..830e4a8c1 --- /dev/null +++ b/browser/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/browser/base/content/newtab/newTab.js b/browser/base/content/newtab/newTab.js new file mode 100644 index 000000000..42bbaf09f --- /dev/null +++ b/browser/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://gre/modules/commonjs/sdk/core/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/browser/base/content/newtab/newTab.xul b/browser/base/content/newtab/newTab.xul new file mode 100644 index 000000000..bab2c28e7 --- /dev/null +++ b/browser/base/content/newtab/newTab.xul @@ -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/. --> + +<?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" + 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/browser/base/content/newtab/page.js b/browser/base/content/newtab/page.js new file mode 100644 index 000000000..afe5bfba8 --- /dev/null +++ b/browser/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/browser/base/content/newtab/sites.js b/browser/base/content/newtab/sites.js new file mode 100644 index 000000000..d1745aff9 --- /dev/null +++ b/browser/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() this._node, + + /** + * The site's link. + */ + get link() this._link, + + /** + * The url of the site's link. + */ + get url() this.link.url, + + /** + * The title of the site's link. + */ + get title() 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/browser/base/content/newtab/transformations.js b/browser/base/content/newtab/transformations.js new file mode 100644 index 000000000..6d1554f5f --- /dev/null +++ b/browser/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/browser/base/content/newtab/undo.js b/browser/base/content/newtab/undo.js new file mode 100644 index 000000000..5f619e980 --- /dev/null +++ b/browser/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/browser/base/content/newtab/updater.js b/browser/base/content/newtab/updater.js new file mode 100644 index 000000000..7b483e037 --- /dev/null +++ b/browser/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/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js new file mode 100644 index 000000000..b6b894dff --- /dev/null +++ b/browser/base/content/nsContextMenu.js @@ -0,0 +1,1623 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.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.isTextSelected = this.isTextSelection(); + 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]+$/i.test(linkText)) { + // Now let's see if this is an intentional link selection. Our guess is + // based on whether the selection begins/ends with whitespace or is + // preceded/followed by a non-word character. + + // selection.toString() trims trailing whitespace, so we look for + // that explicitly in the first and last ranges. + let beginRange = selection.getRangeAt(0); + let delimitedAtStart = /^\s/.test(beginRange); + if (!delimitedAtStart) { + let container = beginRange.startContainer; + let offset = beginRange.startOffset; + if (container.nodeType == Node.TEXT_NODE && offset > 0) + delimitedAtStart = /\W/.test(container.textContent[offset - 1]); + else + delimitedAtStart = true; + } + + let delimitedAtEnd = false; + if (delimitedAtStart) { + let endRange = selection.getRangeAt(selection.rangeCount - 1); + delimitedAtEnd = /\s$/.test(endRange); + if (!delimitedAtEnd) { + let container = endRange.endContainer; + let offset = endRange.endOffset; + if (container.nodeType == Node.TEXT_NODE && + offset < container.textContent.length) + delimitedAtEnd = /\W/.test(container.textContent[offset]); + else + delimitedAtEnd = true; + } + } + + if (delimitedAtStart && delimitedAtEnd) { + 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", this.onPlainTextLink); + this.showItem("context-sep-open", shouldShow); + }, + + initNavigationItems: function CM_initNavigationItems() { + var shouldShow = !(this.isContentSelected || this.onLink || this.onImage || + this.onCanvas || this.onVideo || this.onAudio || + this.onTextInput || this.onSocial); + this.showItem("context-back", shouldShow); + this.showItem("context-forward", shouldShow); + + let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true"; + + let stopReloadItem = ""; + if (shouldShow || this.onSocial) { + stopReloadItem = (stopped || this.onSocial) ? "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); + var showInspect = !this.onSocial && gPrefService.getBoolPref("devtools.inspector.enabled"); + this.showItem("context-viewsource", shouldShow); + this.showItem("context-viewinfo", shouldShow); + this.showItem("inspect-separator", showInspect); + this.showItem("context-inspect", showInspect); + + this.showItem("context-sep-viewsource", shouldShow); + + // Set as Desktop background depends on whether an image was clicked on, + // and only works if we have a shell service. + var haveSetDesktopBackground = false; +#ifdef HAVE_SHELL_SERVICE + // Only enable Set as Desktop Background if we can get the shell service. + var shell = getShellService(); + if (shell) + haveSetDesktopBackground = shell.canSetDesktopBackground; +#endif + this.showItem("context-setDesktopBackground", + haveSetDesktopBackground && this.onLoadedImage); + + if (haveSetDesktopBackground && this.onLoadedImage) { + document.getElementById("context-setDesktopBackground") + .disabled = 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() { + var isTextSelected = this.isTextSelected; + + // 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.onSocial)); + this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink && + !this.onSocial) || this.onPlainTextLink); + this.showItem("context-searchselect", isTextSelected); + this.showItem("context-keywordfield", + this.onTextInput && this.onKeywordField); + this.showItem("frame", this.inFrame); + this.showItem("frame-sep", this.inFrame && 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); + + // SocialMarks + let marksEnabled = SocialUI.enabled && Social.provider.pageMarkInfo; + let enablePageMark = marksEnabled && !(this.isContentSelected || + this.onTextInput || this.onLink || this.onImage || + this.onVideo || this.onAudio || this.onSocial); + let enableLinkMark = marksEnabled && ((this.onLink && !this.onMailtoLink && + !this.onSocial) || this.onPlainTextLink); + if (enablePageMark) { + Social.isURIMarked(gBrowser.currentURI, function(marked) { + let label = marked ? "social.unmarkpage.label" : "social.markpage.label"; + let provider = Social.provider || Social.defaultProvider; + let menuLabel = gNavigatorBundle.getFormattedString(label, [provider.name]); + this.setItemAttr("context-markpage", "label", menuLabel); + }.bind(this)); + } + this.showItem("context-markpage", enablePageMark); + if (enableLinkMark) { + Social.isURIMarked(this.linkURI, function(marked) { + let label = marked ? "social.unmarklink.label" : "social.marklink.label"; + let provider = Social.provider || Social.defaultProvider; + let menuLabel = gNavigatorBundle.getFormattedString(label, [provider.name]); + this.setItemAttr("context-marklink", "label", menuLabel); + }.bind(this)); + } + this.showItem("context-marklink", enableLinkMark); + + // SocialShare + let shareButton = SocialShare.shareButton; + let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial; + let pageShare = shareEnabled && !(this.isContentSelected || + this.onTextInput || this.onLink || this.onImage || + this.onVideo || this.onAudio); + this.showItem("context-sharepage", pageShare); + this.showItem("context-shareselect", shareEnabled && this.isContentSelected); + this.showItem("context-sharelink", shareEnabled && (this.onLink || this.onPlainTextLink) && !this.onMailtoLink); + this.showItem("context-shareimage", shareEnabled && this.onImage); + this.showItem("context-sharevideo", shareEnabled && this.onVideo); + this.setItemAttr("context-sharevideo", "disabled", !this.mediaURL); + }, + + 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.onMathML = false; + this.inFrame = false; + this.inSyntheticDoc = false; + this.hasBGImage = false; + this.bgImageURL = ""; + this.onEditableArea = false; + this.isDesignMode = false; + this.onCTPPlugin = false; + this.canSpellCheck = false; + + // 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; + this.onSocial = !!this.browser.getAttribute("origin"); + + // 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.mozMatchesSelector(":-moz-handler-clicktoplay")) { + 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 ); + } + + // 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 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.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) { + if (this.onSocial) { + // full reload of social provider + Social.enabled = false; + Services.tm.mainThread.dispatch(function() { + Social.enabled = true; + }, Components.interfaces.nsIThread.DISPATCH_NORMAL); + } else { + 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; + + if (this.onCanvas) + viewURL = this.target.toDataURL(); + else { + viewURL = this.mediaURL; + urlSecurityCheck(viewURL, + this.browser.contentPrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + } + + var 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) { + // 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 (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); + }, + + 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 data: URL. + saveImageURL(this.target.toDataURL(), "canvas.png", "SaveImageTitle", + true, false, doc.documentURIObject, doc); + } + 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; + }, + + // Get selected text. Only display the first 15 chars. + isTextSelection: function() { + // Get 16 characters, so that we can trim the selection if it's greater + // than 15 chars + var selectedText = getBrowserSelection(16); + + if (!selectedText) + return false; + + 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]); + document.getElementById("context-searchselect").label = menuLabel; + document.getElementById("context-searchselect").accessKey = + gNavigatorBundle.getString("contextMenuSearch.accesskey"); + + return true; + }, + + // 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); + } + }, + + markLink: function CM_markLink() { + // send link to social + SocialMark.toggleURIMark(this.linkURI); + }, + + shareLink: function CM_shareLink() { + SocialShare.sharePage(null, { url: this.linkURI.spec }); + }, + + shareImage: function CM_shareImage() { + SocialShare.sharePage(null, { url: this.imageURL, previews: [ this.mediaURL ] }); + }, + + shareVideo: function CM_shareVideo() { + SocialShare.sharePage(null, { url: this.mediaURL, source: this.mediaURL }); + }, + + shareSelect: function CM_shareSelect(selection) { + SocialShare.sharePage(null, { url: this.browser.currentURI.spec, text: selection }); + }, + + 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 ""; + } +}; diff --git a/browser/base/content/openLocation.js b/browser/base/content/openLocation.js new file mode 100644 index 000000000..06f769fca --- /dev/null +++ b/browser/base/content/openLocation.js @@ -0,0 +1,133 @@ +/* -*- 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; + 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/browser/base/content/openLocation.xul b/browser/base/content/openLocation.xul new file mode 100644 index 000000000..7bafed0fe --- /dev/null +++ b/browser/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/browser/base/content/overrides/app-license.html b/browser/base/content/overrides/app-license.html new file mode 100644 index 000000000..e7a158c79 --- /dev/null +++ b/browser/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.mozilla.org/">Mozilla Project</a> under the Mozilla + Public License 2.0 (MPL). <a href="about:rights">Know your rights</a>.</p> diff --git a/browser/base/content/padlock.css b/browser/base/content/padlock.css new file mode 100644 index 000000000..9c4e978de --- /dev/null +++ b/browser/base/content/padlock.css @@ -0,0 +1,193 @@ +#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"] { + 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"] { + 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"] { + 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"] { + 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"] { + 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"] { + list-style-image: url("chrome://browser/content/padlock_classic_https.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/browser/base/content/padlock.js b/browser/base/content/padlock.js new file mode 100644 index 000000000..6ba610ac7 --- /dev/null +++ b/browser/base/content/padlock.js @@ -0,0 +1,193 @@ +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: + 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 (highlight_urlbar) { + ub.setAttribute("security_level", level); + } else { + ub.removeAttribute("security_level"); + } + + padlock_PadLock.setPadlockLevel("padlock-ib", level); + padlock_PadLock.setPadlockLevel("padlock-ib-left", level); + padlock_PadLock.setPadlockLevel("padlock-ub-right", level); + padlock_PadLock.setPadlockLevel("padlock-sb", level); + padlock_PadLock.setPadlockLevel("padlock-tab", level); + }, + setPadlockLevel: function(item, level) { + let secbut = document.getElementById(item); + + if (level) { + secbut.setAttribute("level", level); + secbut.hidden = false; + } else { + secbut.hidden = true; + secbut.removeAttribute("level"); + } + + secbut.setAttribute("tooltiptext", level); + }, + 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("", this); + }, + 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"); + if (colprefval == 1) { + colshow = "y"; + } + else { // 0 or anything else_ default + colshow = ""; + } + + var lockenabled; + var lockenabled = padlock_PadLock.prefbranch.getBoolPref("shown"); + if (lockenabled) + padshow = position; + else + padshow = ""; + + document.getElementById("padlock-ib").setAttribute("padshow", padshow); + document.getElementById("padlock-ib-left").setAttribute("padshow", padshow); + document.getElementById("padlock-ub-right").setAttribute("padshow", padshow); + document.getElementById("padlock-sb").setAttribute("padshow", padshow); + document.getElementById("padlock-tab").setAttribute("padshow", padshow); + + document.getElementById("padlock-ib").setAttribute("padstyle", padstyle); + document.getElementById("padlock-ib-left").setAttribute("padstyle", padstyle); + document.getElementById("padlock-ub-right").setAttribute("padstyle", padstyle); + document.getElementById("padlock-sb").setAttribute("padstyle", padstyle); + document.getElementById("padlock-tab").setAttribute("padstyle", padstyle); + + document.getElementById("urlbar").setAttribute("https_color", colshow); + } +}; + +window.addEventListener("load", padlock_PadLock.onLoad, false ); +window.addEventListener("unload", padlock_PadLock.onUnLoad, false ); diff --git a/browser/base/content/padlock.xul b/browser/base/content/padlock.xul new file mode 100644 index 000000000..e820c19c7 --- /dev/null +++ b/browser/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/browser/base/content/padlock_classic_broken.png b/browser/base/content/padlock_classic_broken.png Binary files differnew file mode 100644 index 000000000..437036fe8 --- /dev/null +++ b/browser/base/content/padlock_classic_broken.png diff --git a/browser/base/content/padlock_classic_ev.png b/browser/base/content/padlock_classic_ev.png Binary files differnew file mode 100644 index 000000000..b3f80c0da --- /dev/null +++ b/browser/base/content/padlock_classic_ev.png diff --git a/browser/base/content/padlock_classic_https.png b/browser/base/content/padlock_classic_https.png Binary files differnew file mode 100644 index 000000000..86026c04f --- /dev/null +++ b/browser/base/content/padlock_classic_https.png diff --git a/browser/base/content/padlock_mod_broken.png b/browser/base/content/padlock_mod_broken.png Binary files differnew file mode 100644 index 000000000..33a6c0645 --- /dev/null +++ b/browser/base/content/padlock_mod_broken.png diff --git a/browser/base/content/padlock_mod_ev.png b/browser/base/content/padlock_mod_ev.png Binary files differnew file mode 100644 index 000000000..3dfdcbde5 --- /dev/null +++ b/browser/base/content/padlock_mod_ev.png diff --git a/browser/base/content/padlock_mod_https.png b/browser/base/content/padlock_mod_https.png Binary files differnew file mode 100644 index 000000000..d494b42b0 --- /dev/null +++ b/browser/base/content/padlock_mod_https.png diff --git a/browser/base/content/padlock_mod_low.png b/browser/base/content/padlock_mod_low.png Binary files differnew file mode 100644 index 000000000..fc60fcca6 --- /dev/null +++ b/browser/base/content/padlock_mod_low.png diff --git a/browser/base/content/pageinfo/feeds.js b/browser/base/content/pageinfo/feeds.js new file mode 100644 index 000000000..a15516bf7 --- /dev/null +++ b/browser/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, rels.feed); + 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/browser/base/content/pageinfo/feeds.xml b/browser/base/content/pageinfo/feeds.xml new file mode 100644 index 000000000..782c05a73 --- /dev/null +++ b/browser/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/browser/base/content/pageinfo/pageInfo.css b/browser/base/content/pageinfo/pageInfo.css new file mode 100644 index 000000000..622b56bb5 --- /dev/null +++ b/browser/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/browser/base/content/pageinfo/pageInfo.js b/browser/base/content/pageinfo/pageInfo.js new file mode 100644 index 000000000..7846bd36b --- /dev/null +++ b/browser/base/content/pageinfo/pageInfo.js @@ -0,0 +1,1276 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//******** define 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) { + 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 nsICacheService = Components.interfaces.nsICacheService; +const ACCESS_READ = Components.interfaces.nsICache.ACCESS_READ; +const cacheService = Components.classes["@mozilla.org/network/cache-service;1"].getService(nsICacheService); +var httpCacheSession = cacheService.createSession("HTTP", 0, true); +httpCacheSession.doomEntriesIfExpired = false; +var ftpCacheSession = cacheService.createSession("FTP", 0, true); +ftpCacheSession.doomEntriesIfExpired = 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 +try { + const gClipboardHelper = Components.classes["@mozilla.org/widget/clipboardhelper;1"].getService(Components.interfaces.nsIClipboardHelper); +} +catch(e) { + // do nothing, later code will handle the error +} + +// 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); +} + +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 tries = 0; + var checkCacheListener = { + onCacheEntryAvailable: function(entry, access, status) { + if (entry || tries == 1) { + cb(entry); + } + else { + tries++; + ftpCacheSession.asyncOpenCacheEntry(key, ACCESS_READ, this, true); + } + } + }; + httpCacheSession.asyncOpenCacheEntry(key, ACCESS_READ, checkCacheListener, true); +} + +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.treeBoxObject.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/browser/base/content/pageinfo/pageInfo.xml b/browser/base/content/pageinfo/pageInfo.xml new file mode 100644 index 000000000..20d330046 --- /dev/null +++ b/browser/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/browser/base/content/pageinfo/pageInfo.xul b/browser/base/content/pageinfo/pageInfo.xul new file mode 100644 index 000000000..b1c480413 --- /dev/null +++ b/browser/base/content/pageinfo/pageInfo.xul @@ -0,0 +1,558 @@ +<?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#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#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"> + <!-- Ask and Allow are purposefully reversed here! --> + <radio id="indexedDB#1" command="cmd_indexedDBToggle" label="&permAskAlways;"/> + <radio id="indexedDB#0" command="cmd_indexedDBToggle" label="&permAllow;"/> + <radio id="indexedDB#2" command="cmd_indexedDBToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + <hbox id="permIndexedDBBox2"> + <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/browser/base/content/pageinfo/permissions.js b/browser/base/content/pageinfo/permissions.js new file mode 100644 index 000000000..dc53ef449 --- /dev/null +++ b/browser/base/content/pageinfo/permissions.js @@ -0,0 +1,357 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 BLOCK = nsIPermissionManager.DENY_ACTION; // 2 +const SESSION = nsICookiePermission.ACCESS_SESSION; // 8 + +const nsIQuotaManager = Components.interfaces.nsIQuotaManager; + +var gPermURI; +var gPrefs; +var gUsageRequest; + +var gPermObj = { + image: function getImageDefaultPermission() + { + if (gPrefs.getIntPref("permissions.default.image") == 2) + return BLOCK; + return ALLOW; + }, + cookie: function getCookieDefaultPermission() + { + if (gPrefs.getIntPref("network.cookie.cookieBehavior") == 2) + return BLOCK; + + if (gPrefs.getIntPref("network.cookie.lifetimePolicy") == 2) + return SESSION; + return ALLOW; + }, + "desktop-notification": function getNotificationDefaultPermission() + { + return BLOCK; + }, + popup: function getPopupDefaultPermission() + { + if (gPrefs.getBoolPref("dom.disable_open_during_load")) + return BLOCK; + return ALLOW; + }, + install: function getInstallDefaultPermission() + { + try { + if (!gPrefs.getBoolPref("xpinstall.whitelist.required")) + return ALLOW; + } + catch (e) { + } + return BLOCK; + }, + geo: function getGeoDefaultPermissions() + { + return BLOCK; + }, + indexedDB: function getIndexedDBDefaultPermissions() + { + return UNKNOWN; + }, + plugins: function getPluginsDefaultPermissions() + { + return UNKNOWN; + }, + fullscreen: function getFullscreenDefaultPermissions() + { + return UNKNOWN; + }, + pointerLock: function getPointerLockPermissions() + { + return BLOCK; + }, +}; + +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"); + // Geolocation and PointerLock permission consumers use testExactPermission, not testPermission. + var perm; + if (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); + } + if (aPartId == "indexedDB" && + (permission == ALLOW || permission == BLOCK)) { + permissionManager.remove(gPermURI.host, "indexedDB-unlimited"); + } +} + +function setRadioState(aPartId, aValue) +{ + var radio = document.getElementById(aPartId + "#" + aValue); + radio.radioGroup.selectedItem = radio; +} + +function initIndexedDBRow() +{ + 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-unlimited"); + 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(aPluginName, aPermissionString) { + let permPluginTemplate = document.getElementById("permPluginTemplate").cloneNode(true); + permPluginTemplate.setAttribute("permString", aPermissionString); + let attrs = [ + [ ".permPluginTemplateLabel", "value", aPluginName ], + [ ".permPluginTemplateRadioGroup", "id", aPermissionString + "RadioGroup" ], + [ ".permPluginTemplateRadioDefault", "id", aPermissionString + "#0" ], + [ ".permPluginTemplateRadioAsk", "id", aPermissionString + "#3" ], + [ ".permPluginTemplateRadioAllow", "id", aPermissionString + "#1" ], + [ ".permPluginTemplateRadioBlock", "id", aPermissionString + "#2" ] + ]; + + for (let attr of attrs) { + permPluginTemplate.querySelector(attr[0]).setAttribute(attr[1], attr[2]); + } + + return permPluginTemplate; +} + +function clearPluginPermissionTemplate() { + let permPluginTemplate = document.getElementById("permPluginTemplate"); + permPluginTemplate.hidden = true; + permPluginTemplate.removeAttribute("permString"); + document.querySelector(".permPluginTemplateLabel").removeAttribute("value"); + document.querySelector(".permPluginTemplateRadioGroup").removeAttribute("id"); + document.querySelector(".permPluginTemplateRadioAsk").removeAttribute("id"); + document.querySelector(".permPluginTemplateRadioAllow").removeAttribute("id"); + document.querySelector(".permPluginTemplateRadioBlock").removeAttribute("id"); +} + +function initPluginsRow() { + let vulnerableLabel = document.getElementById("browserBundle").getString("pluginActivateVulnerable.label"); + let pluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost); + + let permissionMap = Map(); + + for (let plugin of pluginHost.getPluginTags()) { + if (plugin.disabled) { + continue; + } + for (let mimeType of plugin.getMimeTypes()) { + let permString = pluginHost.getPermissionStringForType(mimeType); + if (!permissionMap.has(permString)) { + var name = makeNicePluginName(plugin.name); + if (permString.startsWith("plugin-vulnerable:")) { + name += " \u2014 " + vulnerableLabel; + } + permissionMap.set(permString, name); + } + } + } + + let entries = [{name: item[1], permission: item[0]} for (item of permissionMap)]; + entries.sort(function(a, b) { + return a.name < b.name ? -1 : (a.name == b.name ? 0 : 1); + }); + + let permissionEntries = [ + fillInPluginPermissionTemplate(p.name, p.permission) 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/browser/base/content/pageinfo/security.js b/browser/base/content/pageinfo/security.js new file mode 100644 index 000000000..a9c4dd496 --- /dev/null +++ b/browser/base/content/pageinfo/security.js @@ -0,0 +1,345 @@ +/* -*- 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 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, + isBroken : isBroken, + isEV : isEV, + cert : cert, + fullLocation : gWindow.location + }; + + try { + retval.encryptionAlgorithm = status.cipherName; + retval.encryptionStrength = status.secretKeyLength; + } + catch (e) { + } + + return retval; + } else { + return { + hostName : hName, + cAName : "", + encryptionAlgorithm : "", + encryptionStrength : 0, + isBroken : isBroken, + 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) { + hdr = pkiBundle.getString("pageInfo_MixedContent"); + msg1 = pkiBundle.getString("pageInfo_Privacy_Mixed1"); + msg2 = pkiBundle.getString("pageInfo_Privacy_None2"); + } + else if (info.encryptionStrength >= 90) { + hdr = pkiBundle.getFormattedString("pageInfo_StrongEncryptionWithBits", + [info.encryptionAlgorithm, info.encryptionStrength + ""]); + msg1 = pkiBundle.getString("pageInfo_Privacy_Strong1"); + msg2 = pkiBundle.getString("pageInfo_Privacy_Strong2"); + security._cert = info.cert; + } + else if (info.encryptionStrength > 0) { + hdr = pkiBundle.getFormattedString("pageInfo_WeakEncryptionWithBits", + [info.encryptionAlgorithm, info.encryptionStrength + ""]); + msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_Weak1", [info.hostName]); + msg2 = pkiBundle.getString("pageInfo_Privacy_Weak2"); + } + 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/browser/base/content/popup-notifications.inc b/browser/base/content/popup-notifications.inc new file mode 100644 index 000000000..172b5407a --- /dev/null +++ b/browser/base/content/popup-notifications.inc @@ -0,0 +1,108 @@ +# 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="geolocation-notification" hidden="true"> + <popupnotificationcontent orient="vertical" align="start"> + <separator class="thin"/> + <label id="geolocation-learnmore-link" class="text-link"/> + </popupnotificationcontent> + </popupnotification> + + <popupnotification id="servicesInstall-notification" hidden="true"> + <popupnotificationcontent orient="vertical" align="start"> + <separator class="thin"/> + <label id="servicesInstall-learnmore-link" class="text-link"/> + </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> + <separator/> + <label id="mixed-content-blocked-helplink" class="text-link" + value="&mixedContentBlocked.helplink;"/> + </popupnotificationcontent> + </popupnotification> diff --git a/browser/base/content/report-phishing-overlay.xul b/browser/base/content/report-phishing-overlay.xul new file mode 100644 index 000000000..76baf01da --- /dev/null +++ b/browser/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/browser/base/content/safeMode.css b/browser/base/content/safeMode.css new file mode 100644 index 000000000..4f093a452 --- /dev/null +++ b/browser/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/browser/base/content/safeMode.js b/browser/base/content/safeMode.js new file mode 100644 index 000000000..b01925b75 --- /dev/null +++ b/browser/base/content/safeMode.js @@ -0,0 +1,152 @@ +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Mike Connor. +# Portions created by the Initial Developer are Copyright (C) 2005 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Connor <mconnor@steelgryphon.com> +# Asaf Romano <mano@mozilla.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +Components.utils.import("resource://gre/modules/AddonManager.jsm"); + +function restartApp() { + var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"] + .getService(Components.interfaces.nsIAppStartup); + appStartup.quit(appStartup.eForceQuit | appStartup.eRestart); +} + +function clearAllPrefs() { + var prefService = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefService); + prefService.resetUserPrefs(); + + // Remove the pref-overrides dir, if it exists + try { + var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties); + const NS_APP_PREFS_OVERRIDE_DIR = "PrefDOverride"; + var prefOverridesDir = fileLocator.get(NS_APP_PREFS_OVERRIDE_DIR, + Components.interfaces.nsIFile); + prefOverridesDir.remove(true); + } catch (ex) { + Components.utils.reportError(ex); + } +} + +function restoreDefaultBookmarks() { + var prefBranch = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + prefBranch.setBoolPref("browser.bookmarks.restore_default_bookmarks", true); +} + +function deleteLocalstore() { + const nsIDirectoryServiceContractID = "@mozilla.org/file/directory_service;1"; + const nsIProperties = Components.interfaces.nsIProperties; + var directoryService = Components.classes[nsIDirectoryServiceContractID] + .getService(nsIProperties); + var localstoreFile = directoryService.get("LStoreS", Components.interfaces.nsIFile); + try { + 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 = Components.classes["@mozilla.org/browser/search-service;1"] + .getService(Components.interfaces.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() { + var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"] + .getService(Components.interfaces.nsIAppStartup); + appStartup.quit(appStartup.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/browser/base/content/safeMode.xul b/browser/base/content/safeMode.xul new file mode 100644 index 000000000..846906a25 --- /dev/null +++ b/browser/base/content/safeMode.xul @@ -0,0 +1,87 @@ +<?xml version="1.0"?> +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is mozilla.org code. +# +# The Initial Developer of the Original Code is +# Mike Connor. +# Portions created by the Initial Developer are Copyright (C) 2005 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Mike Connor <mconnor@steelgryphon.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +<!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/browser/base/content/sanitize.js b/browser/base/content/sanitize.js new file mode 100644 index 000000000..42d7514cc --- /dev/null +++ b/browser/base/content/sanitize.js @@ -0,0 +1,518 @@ +# -*- 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, "Promise", + "resource://gre/modules/commonjs/sdk/core/promise.js"); + +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: "", + + 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; + 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; + Cu.reportError("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 cacheService = Cc["@mozilla.org/network/cache-service;1"]. + getService(Ci.nsICacheService); + try { + // Cache doesn't consult timespan, nor does it have the + // facility for timespan-based eviction. Wipe it. + cacheService.evictEntries(Ci.nsICache.STORE_ANYWHERE); + } 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(); + }, + + 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: function () + { + var dlMgr = Components.classes["@mozilla.org/download-manager;1"] + .getService(Components.interfaces.nsIDownloadManager); + + var dlsToRemove = []; + if (this.range) { + // First, remove the completed/cancelled downloads + dlMgr.removeDownloadsByTimeframe(this.range[0], this.range[1]); + + // Queue up any active downloads that started in the time span as well + for (let dlsEnum of [dlMgr.activeDownloads, dlMgr.activePrivateDownloads]) { + while (dlsEnum.hasMoreElements()) { + var dl = dlsEnum.next(); + if (dl.startTime >= this.range[0]) + dlsToRemove.push(dl); + } + } + } + else { + // Clear all completed/cancelled downloads + dlMgr.cleanUp(); + dlMgr.cleanUpPrivate(); + + // Queue up all active ones as well + for (let dlsEnum of [dlMgr.activeDownloads, dlMgr.activePrivateDownloads]) { + while (dlsEnum.hasMoreElements()) { + dlsToRemove.push(dlsEnum.next()); + } + } + } + + // Remove any queued up active downloads + dlsToRemove.forEach(function (dl) { + dl.remove(); + }); + }, + + get canClear() + { + var dlMgr = Components.classes["@mozilla.org/download-manager;1"] + .getService(Components.interfaces.nsIDownloadManager); + return dlMgr.canCleanUp || dlMgr.canCleanUpPrivate; + } + }, + + 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; + } + } + } +}; + + + +// "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; + +// 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(); +}; + +// this is called on startup and shutdown, to perform pending sanitizations +Sanitizer._checkAndSanitize = function() +{ + 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.sanitize().then(function() { + prefs.setBoolPref(Sanitizer.prefDidShutdown, true); + }); + } +}; diff --git a/browser/base/content/sanitize.xul b/browser/base/content/sanitize.xul new file mode 100644 index 000000000..10b3813a8 --- /dev/null +++ b/browser/base/content/sanitize.xul @@ -0,0 +1,183 @@ +<?xml version="1.0"?> + +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<?xml-stylesheet href="chrome://global/skin/"?> +<?xml-stylesheet href="chrome://browser/skin/sanitizeDialog.css"?> + +#ifdef CRH_DIALOG_TREE_VIEW +<?xml-stylesheet href="chrome://browser/skin/places/places.css"?> +#endif + +<?xml-stylesheet href="chrome://browser/content/sanitizeDialog.css"?> + +<!DOCTYPE prefwindow [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + <!ENTITY % sanitizeDTD SYSTEM "chrome://browser/locale/sanitize.dtd"> + %brandDTD; + %sanitizeDTD; +]> + +<prefwindow id="SanitizeDialog" type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + dlgbuttons="accept,cancel" + title="&sanitizeDialog2.title;" + noneverythingtitle="&sanitizeDialog2.title;" + style="width: &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"/> + </preferences> + + <preferences id="nonItemPreferences"> + <preference id="privacy.sanitize.timeSpan" + name="privacy.sanitize.timeSpan" + type="int"/> + </preferences> + + <hbox id="SanitizeDurationBox" align="center"> + <label value="&clearTimeDuration.label;" + accesskey="&clearTimeDuration.accesskey;" + control="sanitizeDurationChoice" + id="sanitizeDurationLabel"/> + <menulist id="sanitizeDurationChoice" + preference="privacy.sanitize.timeSpan" + onselect="gSanitizePromptDialog.selectByTimespan();" + flex="1"> + <menupopup id="sanitizeDurationPopup"> +#ifdef CRH_DIALOG_TREE_VIEW + <menuitem label="" value="-1" id="sanitizeDurationCustom"/> +#endif + <menuitem label="&clearTimeDuration.lastHour;" value="1"/> + <menuitem label="&clearTimeDuration.last2Hours;" value="2"/> + <menuitem label="&clearTimeDuration.last4Hours;" value="3"/> + <menuitem label="&clearTimeDuration.today;" value="4"/> + <menuseparator/> + <menuitem label="&clearTimeDuration.everything;" value="0"/> + </menupopup> + </menulist> + <label id="sanitizeDurationSuffixLabel" + value="&clearTimeDuration.suffix;"/> + </hbox> + + <separator class="thin"/> + +#ifdef CRH_DIALOG_TREE_VIEW + <deck id="durationDeck"> + <tree id="placesTree" flex="1" hidecolumnpicker="true" rows="10" + disabled="true" disableKeyNavigation="true"> + <treecols> + <treecol id="date" label="&clearTimeDuration.dateColumn;" flex="1"/> + <splitter class="tree-splitter"/> + <treecol id="title" label="&clearTimeDuration.nameColumn;" flex="5"/> + </treecols> + <treechildren id="placesTreechildren" + ondragstart="gSanitizePromptDialog.grippyMoved('ondragstart', event);" + ondragover="gSanitizePromptDialog.grippyMoved('ondragover', event);" + onkeypress="gSanitizePromptDialog.grippyMoved('onkeypress', event);" + onmousedown="gSanitizePromptDialog.grippyMoved('onmousedown', event);"/> + </tree> +#endif + + <vbox id="sanitizeEverythingWarningBox"> + <spacer flex="1"/> + <hbox align="center"> + <image id="sanitizeEverythingWarningIcon"/> + <vbox id="sanitizeEverythingWarningDescBox" flex="1"> + <description id="sanitizeEverythingWarning"/> + <description id="sanitizeEverythingUndoWarning">&sanitizeEverythingUndoWarning;</description> + </vbox> + </hbox> + <spacer flex="1"/> + </vbox> + +#ifdef CRH_DIALOG_TREE_VIEW + </deck> +#endif + + <separator class="thin"/> + + <hbox id="detailsExpanderWrapper" align="center"> + <button type="image" + id="detailsExpander" + class="expander-down" + persist="class" + oncommand="gSanitizePromptDialog.toggleItemList();"/> + <label id="detailsExpanderLabel" + value="&detailsProgressiveDisclosure.label;" + accesskey="&detailsProgressiveDisclosure.accesskey;" + control="detailsExpander"/> + </hbox> + <listbox id="itemList" rows="7" collapsed="true" persist="collapsed"> + <listitem label="&itemHistoryAndDownloads.label;" + type="checkbox" + accesskey="&itemHistoryAndDownloads.accesskey;" + preference="privacy.cpd.history" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemFormSearchHistory.label;" + type="checkbox" + accesskey="&itemFormSearchHistory.accesskey;" + preference="privacy.cpd.formdata" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemCookies.label;" + type="checkbox" + accesskey="&itemCookies.accesskey;" + preference="privacy.cpd.cookies" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemCache.label;" + type="checkbox" + accesskey="&itemCache.accesskey;" + preference="privacy.cpd.cache" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemActiveLogins.label;" + type="checkbox" + accesskey="&itemActiveLogins.accesskey;" + preference="privacy.cpd.sessions" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemOfflineApps.label;" + type="checkbox" + accesskey="&itemOfflineApps.accesskey;" + preference="privacy.cpd.offlineApps" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + <listitem label="&itemSitePreferences.label;" + type="checkbox" + accesskey="&itemSitePreferences.accesskey;" + preference="privacy.cpd.siteSettings" + noduration="true" + onsyncfrompreference="return gSanitizePromptDialog.onReadGeneric();"/> + </listbox> + + </prefpane> +</prefwindow> diff --git a/browser/base/content/sanitizeDialog.css b/browser/base/content/sanitizeDialog.css new file mode 100644 index 000000000..27c3c0866 --- /dev/null +++ b/browser/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/browser/base/content/sanitizeDialog.js b/browser/base/content/sanitizeDialog.js new file mode 100644 index 000000000..18df5e4a4 --- /dev/null +++ b/browser/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/browser/base/content/socialchat.xml b/browser/base/content/socialchat.xml new file mode 100644 index 000000000..a4843eefa --- /dev/null +++ b/browser/base/content/socialchat.xml @@ -0,0 +1,747 @@ +<?xml version="1.0"?> + +<bindings id="socialChatBindings" + 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="chatbox"> + <content orient="vertical" mousethrough="never"> + <xul:hbox class="chat-titlebar" xbl:inherits="minimized,selected,activity" align="baseline"> + <xul:hbox flex="1" onclick="document.getBindingParent(this).onTitlebarClick(event);"> + <xul:image class="chat-status-icon" xbl:inherits="src=image"/> + <xul:label class="chat-title" flex="1" xbl:inherits="value=label" crop="center"/> + </xul:hbox> + <xul:toolbarbutton id="notification-icon" class="notification-anchor-icon chat-toolbarbutton" + oncommand="document.getBindingParent(this).showNotifications(); event.stopPropagation();"/> + <xul:toolbarbutton anonid="minimize" class="chat-minimize-button chat-toolbarbutton" + oncommand="document.getBindingParent(this).toggle();"/> + <xul:toolbarbutton anonid="swap" class="chat-swap-button chat-toolbarbutton" + oncommand="document.getBindingParent(this).swapWindows();"/> + <xul:toolbarbutton anonid="close" class="chat-close-button chat-toolbarbutton" + oncommand="document.getBindingParent(this).close();"/> + </xul:hbox> + <xul:browser anonid="content" class="chat-frame" flex="1" + context="contentAreaContextMenu" + disableglobalhistory="true" + tooltip="aHTMLTooltip" + xbl:inherits="src,origin" type="content"/> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor><![CDATA[ + let Social = Components.utils.import("resource:///modules/Social.jsm", {}).Social; + this.content.__defineGetter__("popupnotificationanchor", + () => document.getAnonymousElementByAttribute(this, "id", "notification-icon")); + Social.setErrorListener(this.content, function(aBrowser) { + aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo", null, null, null, null); + }); + if (!this.chatbar) { + document.getAnonymousElementByAttribute(this, "anonid", "minimize").hidden = true; + document.getAnonymousElementByAttribute(this, "anonid", "close").hidden = true; + } + let contentWindow = this.contentWindow; + this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) { + if (event.target != this.contentDocument) + return; + this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true); + this.isActive = !this.minimized; + // process this._callbacks, then set to null so the chatbox creator + // knows to make new callbacks immediately. + if (this._callbacks) { + for (let callback of this._callbacks) { + if (callback) + callback(contentWindow); + } + this._callbacks = null; + } + + // content can send a socialChatActivity event to have the UI update. + let chatActivity = function() { + this.setAttribute("activity", true); + if (this.chatbar) + this.chatbar.updateTitlebar(this); + }.bind(this); + contentWindow.addEventListener("socialChatActivity", chatActivity); + contentWindow.addEventListener("unload", function unload() { + contentWindow.removeEventListener("unload", unload); + contentWindow.removeEventListener("socialChatActivity", chatActivity); + }); + }, true); + if (this.src) + this.setAttribute("src", this.src); + ]]></constructor> + + <field name="content" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "content"); + </field> + + <property name="contentWindow"> + <getter> + return this.content.contentWindow; + </getter> + </property> + + <property name="contentDocument"> + <getter> + return this.content.contentDocument; + </getter> + </property> + + <property name="minimized"> + <getter> + return this.getAttribute("minimized") == "true"; + </getter> + <setter><![CDATA[ + // Note that this.isActive is set via our transitionend handler so + // the content doesn't see intermediate values. + let parent = this.chatbar; + if (val) { + this.setAttribute("minimized", "true"); + // If this chat is the selected one a new one needs to be selected. + if (parent && parent.selectedChat == this) + parent._selectAnotherChat(); + } else { + this.removeAttribute("minimized"); + // this chat gets selected. + if (parent) + parent.selectedChat = this; + } + ]]></setter> + </property> + + <property name="chatbar"> + <getter> + if (this.parentNode.nodeName == "chatbar") + return this.parentNode; + return null; + </getter> + </property> + + <property name="isActive"> + <getter> + return this.content.docShell.isActive; + </getter> + <setter> + this.content.docShell.isActive = !!val; + + // let the chat frame know if it is being shown or hidden + let evt = this.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent(val ? "socialFrameShow" : "socialFrameHide", true, true, {}); + this.contentDocument.documentElement.dispatchEvent(evt); + </setter> + </property> + + <method name="showNotifications"> + <body><![CDATA[ + PopupNotifications._reshowNotifications(this.content.popupnotificationanchor, + this.content); + ]]></body> + </method> + + <method name="swapDocShells"> + <parameter name="aTarget"/> + <body><![CDATA[ + aTarget.setAttribute('label', this.contentDocument.title); + aTarget.content.setAttribute("origin", this.content.getAttribute("origin")); + aTarget.content.popupnotificationanchor.className = this.content.popupnotificationanchor.className; + this.content.socialErrorListener.remove(); + aTarget.content.socialErrorListener.remove(); + this.content.swapDocShells(aTarget.content); + Social.setErrorListener(this.content, function(aBrowser) {}); // 'this' will be destroyed soon. + Social.setErrorListener(aTarget.content, function(aBrowser) { + aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo", null, null, null, null); + }); + ]]></body> + </method> + + <method name="onTitlebarClick"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!this.chatbar) + return; + if (aEvent.button == 0) { // left-click: toggle minimized. + this.toggle(); + // if we restored it, we want to focus it. + if (!this.minimized) + this.chatbar.focus(); + } else if (aEvent.button == 1) // middle-click: close chat + this.close(); + ]]></body> + </method> + + <method name="close"> + <body><![CDATA[ + if (this.chatbar) + this.chatbar.remove(this); + else + window.close(); + ]]></body> + </method> + + <method name="swapWindows"> + <body><![CDATA[ + let provider = Social._getProviderFromOrigin(this.content.getAttribute("origin")); + if (this.chatbar) { + this.chatbar.detachChatbox(this, { "centerscreen": "yes" }, win => { + win.document.title = provider.name; + }); + } else { + // attach this chatbox to the topmost browser window + let findChromeWindowForChats = Cu.import("resource://gre/modules/MozSocialAPI.jsm").findChromeWindowForChats; + let win = findChromeWindowForChats(); + let chatbar = win.SocialChatBar.chatbar; + chatbar.openChat(provider, "about:blank", win => { + this.swapDocShells(chatbar.selectedChat); + chatbar.focus(); + this.close(); + }); + } + ]]></body> + </method> + + <method name="toggle"> + <body><![CDATA[ + this.minimized = !this.minimized; + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="focus" phase="capturing"> + if (this.chatbar) + this.chatbar.selectedChat = this; + </handler> + <handler event="DOMTitleChanged"><![CDATA[ + this.setAttribute('label', this.contentDocument.title); + if (this.chatbar) + this.chatbar.updateTitlebar(this); + ]]></handler> + <handler event="DOMLinkAdded"><![CDATA[ + // much of this logic is from DOMLinkHandler in browser.js + // this sets the presence icon for a chat user, we simply use favicon style updating + let link = event.originalTarget; + let rel = link.rel && link.rel.toLowerCase(); + if (!link || !link.ownerDocument || !rel || !link.href) + return; + if (link.rel.indexOf("icon") < 0) + return; + + let uri = DOMLinkHandler.getLinkIconURI(link); + if (!uri) + return; + + // we made it this far, use it + this.setAttribute('image', uri.spec); + if (this.chatbar) + this.chatbar.updateTitlebar(this); + ]]></handler> + <handler event="transitionend"> + if (this.isActive == this.minimized) + this.isActive = !this.minimized; + </handler> + </handlers> + </binding> + + <binding id="chatbar"> + <content> + <xul:hbox align="end" pack="end" anonid="innerbox" class="chatbar-innerbox" mousethrough="always" flex="1"> + <xul:spacer flex="1" anonid="spacer" class="chatbar-overflow-spacer"/> + <xul:toolbarbutton anonid="nub" class="chatbar-button" type="menu" collapsed="true" mousethrough="never"> + <xul:menupopup anonid="nubMenu" oncommand="document.getBindingParent(this).showChat(event.target.chat)"/> + </xul:toolbarbutton> + <children/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor> + // to avoid reflows we cache the width of the nub. + this.cachedWidthNub = 0; + this._selectedChat = null; + </constructor> + + <field name="innerbox" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "innerbox"); + </field> + + <field name="menupopup" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "nubMenu"); + </field> + + <field name="nub" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "nub"); + </field> + + <method name="focus"> + <body><![CDATA[ + if (!this.selectedChat) + return; + Services.focus.focusedWindow = this.selectedChat.contentWindow; + ]]></body> + </method> + + <method name="_isChatFocused"> + <parameter name="aChatbox"/> + <body><![CDATA[ + // If there are no XBL bindings for the chat it can't be focused. + if (!aChatbox.content) + return false; + let fw = Services.focus.focusedWindow; + if (!fw) + return false; + // We want to see if the focused window is in the subtree below our browser... + let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + return containingBrowser == aChatbox.content; + ]]></body> + </method> + + <property name="selectedChat"> + <getter><![CDATA[ + return this._selectedChat; + ]]></getter> + <setter><![CDATA[ + // this is pretty horrible, but we: + // * want to avoid doing touching 'selected' attribute when the + // specified chat is already selected. + // * remove 'activity' attribute on newly selected tab *even if* + // newly selected is already selected. + // * need to handle either current or new being null. + if (this._selectedChat != val) { + if (this._selectedChat) { + this._selectedChat.removeAttribute("selected"); + } + this._selectedChat = val; + if (val) { + this._selectedChat.setAttribute("selected", "true"); + } + } + if (val) { + this._selectedChat.removeAttribute("activity"); + } + ]]></setter> + </property> + + <field name="menuitemMap">new WeakMap()</field> + <field name="chatboxForURL">new Map();</field> + + <property name="hasCollapsedChildren"> + <getter><![CDATA[ + return !!this.querySelector("[collapsed]"); + ]]></getter> + </property> + + <property name="collapsedChildren"> + <getter><![CDATA[ + // A generator yielding all collapsed chatboxes, in the order in + // which they should be restored. + let child = this.lastElementChild; + while (child) { + if (child.collapsed) + yield child; + child = child.previousElementSibling; + } + ]]></getter> + </property> + + <property name="visibleChildren"> + <getter><![CDATA[ + // A generator yielding all non-collapsed chatboxes. + let child = this.firstElementChild; + while (child) { + if (!child.collapsed) + yield child; + child = child.nextElementSibling; + } + ]]></getter> + </property> + + <property name="collapsibleChildren"> + <getter><![CDATA[ + // A generator yielding all children which are able to be collapsed + // in the order in which they should be collapsed. + // (currently this is all visible ones other than the selected one.) + for (let child of this.visibleChildren) + if (child != this.selectedChat) + yield child; + ]]></getter> + </property> + + <method name="_selectAnotherChat"> + <body><![CDATA[ + // Select a different chat (as the currently selected one is no + // longer suitable as the selection - maybe it is being minimized or + // closed.) We only select non-minimized and non-collapsed chats, + // and if none are found, set the selectedChat to null. + // It's possible in the future we will track most-recently-selected + // chats or similar to find the "best" candidate - for now though + // the choice is somewhat arbitrary. + let moveFocus = this.selectedChat && this._isChatFocused(this.selectedChat); + for (let other of this.children) { + if (other != this.selectedChat && !other.minimized && !other.collapsed) { + this.selectedChat = other; + if (moveFocus) + this.focus(); + return; + } + } + // can't find another - so set no chat as selected. + this.selectedChat = null; + ]]></body> + </method> + + <method name="updateTitlebar"> + <parameter name="aChatbox"/> + <body><![CDATA[ + if (aChatbox.collapsed) { + let menuitem = this.menuitemMap.get(aChatbox); + if (aChatbox.getAttribute("activity")) { + menuitem.setAttribute("activity", true); + this.nub.setAttribute("activity", true); + } + menuitem.setAttribute("label", aChatbox.getAttribute("label")); + menuitem.setAttribute("image", aChatbox.getAttribute("image")); + } + ]]></body> + </method> + + <method name="calcTotalWidthOf"> + <parameter name="aElement"/> + <body><![CDATA[ + let cs = document.defaultView.getComputedStyle(aElement); + let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight); + return aElement.getBoundingClientRect().width + margins; + ]]></body> + </method> + + <method name="getTotalChildWidth"> + <parameter name="aChatbox"/> + <body><![CDATA[ + // These are from the CSS for the chatbox and must be kept in sync. + // We can't use calcTotalWidthOf due to the transitions... + const CHAT_WIDTH_OPEN = 260; + const CHAT_WIDTH_MINIMIZED = 160; + return aChatbox.minimized ? CHAT_WIDTH_MINIMIZED : CHAT_WIDTH_OPEN; + ]]></body> + </method> + + <method name="collapseChat"> + <parameter name="aChatbox"/> + <body><![CDATA[ + // we ensure that the cached width for a child of this type is + // up-to-date so we can use it when resizing. + this.getTotalChildWidth(aChatbox); + aChatbox.collapsed = true; + aChatbox.isActive = false; + let menu = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem"); + menu.setAttribute("class", "menuitem-iconic"); + menu.setAttribute("label", aChatbox.contentDocument.title); + menu.setAttribute("image", aChatbox.getAttribute("image")); + menu.chat = aChatbox; + this.menuitemMap.set(aChatbox, menu); + this.menupopup.appendChild(menu); + this.nub.collapsed = false; + ]]></body> + </method> + + <method name="showChat"> + <parameter name="aChatbox"/> + <parameter name="aMode"/> + <body><![CDATA[ + if ((aMode != "minimized") && aChatbox.minimized) + aChatbox.minimized = false; + if (this.selectedChat != aChatbox) + this.selectedChat = aChatbox; + if (!aChatbox.collapsed) + return; // already showing - no more to do. + this._showChat(aChatbox); + // showing a collapsed chat might mean another needs to be collapsed + // to make room... + this.resize(); + ]]></body> + </method> + + <method name="_showChat"> + <parameter name="aChatbox"/> + <body><![CDATA[ + // the actual implementation - doesn't check for overflow, assumes + // collapsed, etc. + let menuitem = this.menuitemMap.get(aChatbox); + this.menuitemMap.delete(aChatbox); + this.menupopup.removeChild(menuitem); + aChatbox.collapsed = false; + aChatbox.isActive = !aChatbox.minimized; + ]]></body> + </method> + + <method name="remove"> + <parameter name="aChatbox"/> + <body><![CDATA[ + this._remove(aChatbox); + // The removal of a chat may mean a collapsed one can spring up, + // or that the popup should be hidden. We also defer the selection + // of another chat until after a resize, as a new candidate may + // become uncollapsed after the resize. + this.resize(); + if (this.selectedChat == aChatbox) { + this._selectAnotherChat(); + } + ]]></body> + </method> + + <method name="_remove"> + <parameter name="aChatbox"/> + <body><![CDATA[ + aChatbox.content.socialErrorListener.remove(); + this.removeChild(aChatbox); + // child might have been collapsed. + let menuitem = this.menuitemMap.get(aChatbox); + if (menuitem) { + this.menuitemMap.delete(aChatbox); + this.menupopup.removeChild(menuitem); + } + this.chatboxForURL.delete(aChatbox.getAttribute('src')); + ]]></body> + </method> + + <method name="removeAll"> + <body><![CDATA[ + this.selectedChat = null; + while (this.firstElementChild) { + this._remove(this.firstElementChild); + } + // and the nub/popup must also die. + this.nub.collapsed = true; + ]]></body> + </method> + + <method name="openChat"> + <parameter name="aProvider"/> + <parameter name="aURL"/> + <parameter name="aCallback"/> + <parameter name="aMode"/> + <body><![CDATA[ + let cb = this.chatboxForURL.get(aURL); + if (cb) { + cb = cb.get(); + if (cb.parentNode) { + this.showChat(cb, aMode); + if (aCallback) { + if (cb._callbacks == null) { + // DOMContentLoaded has already fired, so callback now. + aCallback(cb.contentWindow); + } else { + // DOMContentLoaded for this chat is yet to fire... + cb._callbacks.push(aCallback); + } + } + return; + } + this.chatboxForURL.delete(aURL); + } + cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox"); + // _callbacks is a javascript property instead of a <field> as it + // must exist before the (possibly delayed) bindings are created. + cb._callbacks = [aCallback]; + // src also a javascript property; the src attribute is set in the ctor. + cb.src = aURL; + if (aMode == "minimized") + cb.setAttribute("minimized", "true"); + cb.setAttribute("origin", aProvider.origin); + this.insertBefore(cb, this.firstChild); + this.selectedChat = cb; + this.chatboxForURL.set(aURL, Cu.getWeakReference(cb)); + this.resize(); + ]]></body> + </method> + + <method name="resize"> + <body><![CDATA[ + // Checks the current size against the collapsed state of children + // and collapses or expands as necessary such that as many as possible + // are shown. + // So 2 basic strategies: + // * Collapse/Expand one at a time until we can't collapse/expand any + // more - but this is one reflow per change. + // * Calculate the dimensions ourself and choose how many to collapse + // or expand based on this, then do them all in one go. This is one + // reflow regardless of how many we change. + // So we go the more complicated but more efficient second option... + let availWidth = this.getBoundingClientRect().width; + let currentWidth = 0; + if (!this.nub.collapsed) { // the nub is visible. + if (!this.cachedWidthNub) + this.cachedWidthNub = this.calcTotalWidthOf(this.nub); + currentWidth += this.cachedWidthNub; + } + for (let child of this.visibleChildren) { + currentWidth += this.getTotalChildWidth(child); + } + + if (currentWidth > availWidth) { + // we need to collapse some. + let toCollapse = []; + for (let child of this.collapsibleChildren) { + if (currentWidth <= availWidth) + break; + toCollapse.push(child); + currentWidth -= this.getTotalChildWidth(child); + } + if (toCollapse.length) { + for (let child of toCollapse) + this.collapseChat(child); + } + } else if (currentWidth < availWidth) { + // we *might* be able to expand some - see how many. + // XXX - if this was clever, it could know when removing the nub + // leaves enough space to show all collapsed + let toShow = []; + for (let child of this.collapsedChildren) { + currentWidth += this.getTotalChildWidth(child); + if (currentWidth > availWidth) + break; + toShow.push(child); + } + for (let child of toShow) + this._showChat(child); + + // If none remain collapsed remove the nub. + if (!this.hasCollapsedChildren) { + this.nub.collapsed = true; + } + } + // else: achievement unlocked - we are pixel-perfect! + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.type == "resize" && aEvent.eventPhase == aEvent.BUBBLING_PHASE) { + this.resize(); + } + ]]></body> + </method> + + <method name="_getDragTarget"> + <parameter name="event"/> + <body><![CDATA[ + return event.target.localName == "chatbox" ? event.target : null; + ]]></body> + </method> + + <!-- Moves a chatbox to a new window. --> + <method name="detachChatbox"> + <parameter name="aChatbox"/> + <parameter name="aOptions"/> + <parameter name="aCallback"/> + <body><![CDATA[ + let options = ""; + for (let name in aOptions) + options += "," + name + "=" + aOptions[name]; + + let otherWin = window.openDialog("chrome://browser/content/chatWindow.xul", null, "chrome,all" + options); + + otherWin.addEventListener("load", function _chatLoad(event) { + if (event.target != otherWin.document) + return; + + otherWin.removeEventListener("load", _chatLoad, true); + let otherChatbox = otherWin.document.getElementById("chatter"); + aChatbox.swapDocShells(otherChatbox); + aChatbox.close(); + if (aCallback) + aCallback(otherWin); + }, true); + ]]></body> + </method> + + </implementation> + + <handlers> + <handler event="popupshown"><![CDATA[ + this.nub.removeAttribute("activity"); + ]]></handler> + <handler event="load"><![CDATA[ + window.addEventListener("resize", this); + ]]></handler> + <handler event="unload"><![CDATA[ + window.removeEventListener("resize", this); + ]]></handler> + + <handler event="dragstart"><![CDATA[ + // chat window dragging is essentially duplicated from tabbrowser.xml + // to acheive the same visual experience + let chatbox = this._getDragTarget(event); + if (!chatbox) { + return; + } + + let dt = event.dataTransfer; + // we do not set a url in the drag data to prevent moving to tabbrowser + // or otherwise having unexpected drop handlers do something with our + // chatbox + dt.mozSetDataAt("application/x-moz-chatbox", chatbox, 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(chatbox.contentWindow, canvas); + dt.setDragImage(canvas, -16 * scale, -16 * scale); + + event.stopPropagation(); + ]]></handler> + + <handler event="dragend"><![CDATA[ + let dt = event.dataTransfer; + let draggedChat = dt.mozGetDataAt("application/x-moz-chatbox", 0); + if (dt.mozUserCancelled || dt.dropEffect != "none") { + return; + } + + let eX = event.screenX; + let eY = event.screenY; + // 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. + let sX = {}, sY = {}, sWidth = {}, sHeight = {}; + Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager) + .screenForRect(eX, eY, 1, 1) + .GetAvailRect(sX, sY, sWidth, sHeight); + // default size for the chat window as used in chatWindow.xul, use them + // here to attempt to keep the window fully within the screen when + // opening at the drop point. If the user has resized the window to + // something larger (which gets persisted), at least a good portion of + // the window should still be within the screen. + let winWidth = 400; + let winHeight = 420; + // ensure new window entirely within screen + let left = Math.min(Math.max(eX, sX.value), + sX.value + sWidth.value - winWidth); + let top = Math.min(Math.max(eY, sY.value), + sY.value + sHeight.value - winHeight); + + let provider = Social._getProviderFromOrigin(draggedChat.content.getAttribute("origin")); + this.detachChatbox(draggedChat, { screenX: left, screenY: top }, win => { + win.document.title = provider.name; + }); + + event.stopPropagation(); + ]]></handler> + </handlers> + </binding> + +</bindings> diff --git a/browser/base/content/softwareUpdateOverlay.xul b/browser/base/content/softwareUpdateOverlay.xul new file mode 100644 index 000000000..01170e46c --- /dev/null +++ b/browser/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/browser/base/content/sync/aboutSyncTabs-bindings.xml b/browser/base/content/sync/aboutSyncTabs-bindings.xml new file mode 100644 index 000000000..e6108209a --- /dev/null +++ b/browser/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/browser/base/content/sync/aboutSyncTabs.css b/browser/base/content/sync/aboutSyncTabs.css new file mode 100644 index 000000000..5a353175b --- /dev/null +++ b/browser/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/browser/base/content/sync/aboutSyncTabs.js b/browser/base/content/sync/aboutSyncTabs.js new file mode 100644 index 000000000..815f6120c --- /dev/null +++ b/browser/base/content/sync/aboutSyncTabs.js @@ -0,0 +1,251 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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-sync/main.js"); +Cu.import("resource:///modules/PlacesUIUtils.jsm"); +Cu.import("resource://gre/modules/Services.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"); + }, + + buildList: function(force) { + if (!Weave.Service.isLoggedIn || !this._refetchTabs(force)) + return; + //XXXzpao We should say something about not being logged in & not having data + // or tell the appropriate condition. (bug 583344) + + this._generateTabList(); + }, + + 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().contains(val) && + !item.getAttribute("title").toLowerCase().contains(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); + } + }, + + _generateTabList: function() { + let engine = Weave.Service.engineManager.get("tabs"); + 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); + } + + 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; + let seenURLs = {}; + client.tabs.forEach(function({title, urlHistory, icon}) { + let url = urlHistory[0]; + if (engine.locallyOpenTabMatchesURL(url) || url in seenURLs) + return; + + seenURLs[url] = null; + + 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: Weave.Utils.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, 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._generateTabList(); + 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/browser/base/content/sync/aboutSyncTabs.xul b/browser/base/content/sync/aboutSyncTabs.xul new file mode 100644 index 000000000..a4aa0032f --- /dev/null +++ b/browser/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/browser/base/content/sync/addDevice.js b/browser/base/content/sync/addDevice.js new file mode 100644 index 000000000..556e75768 --- /dev/null +++ b/browser/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/browser/base/content/sync/addDevice.xul b/browser/base/content/sync/addDevice.xul new file mode 100644 index 000000000..83c3b7b3c --- /dev/null +++ b/browser/base/content/sync/addDevice.xul @@ -0,0 +1,129 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd"> +%brandDTD; +%syncBrandDTD; +%syncSetupDTD; +]> +<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + id="wizard" + title="&pairDevice.title.label;" + windowtype="Sync:AddDevice" + persist="screenX screenY" + onwizardnext="return gSyncAddDevice.onWizardAdvance();" + onwizardback="return gSyncAddDevice.onWizardBack();" + onwizardcancel="gSyncAddDevice.onWizardCancel();" + onload="gSyncAddDevice.init();"> + + <script type="application/javascript" + src="chrome://browser/content/sync/addDevice.js"/> + <script type="application/javascript" + src="chrome://browser/content/sync/utils.js"/> + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/printUtils.js"/> + + <wizardpage id="addDevicePage" + label="&pairDevice.title.label;" + onpageshow="gSyncAddDevice.onPageShow();"> + <description> + &pairDevice.dialog.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/add-device"/> + </description> + <separator class="groove-thin"/> + <description> + &addDevice.dialog.enterCode.label; + </description> + <separator class="groove-thin"/> + <vbox align="center"> + <textbox id="pin1" + class="pin" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();" + /> + <textbox id="pin2" + class="pin" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();" + /> + <textbox id="pin3" + class="pin" + oninput="gSyncAddDevice.onTextBoxInput(this);" + onfocus="this.select();" + /> + </vbox> + <separator class="groove-thin"/> + <vbox id="pairDeviceThrobber" align="center" hidden="true"> + <image/> + </vbox> + <hbox id="errorRow" pack="center" hidden="true"> + <image class="statusIcon" status="error"/> + <label class="status" + value="&addDevice.dialog.tryAgain.label;"/> + </hbox> + <spacer flex="3"/> + <label class="text-link" + value="&addDevice.dontHaveDevice.label;" + onclick="gSyncAddDevice.goToSyncKeyPage();"/> + </wizardpage> + + <!-- Need a non-empty label here, otherwise we get a default label on Mac --> + <wizardpage id="syncKeyPage" + label=" " + onpageshow="gSyncAddDevice.onPageShow();"> + <description> + &addDevice.dialog.recoveryKey.label; + </description> + <spacer/> + + <groupbox> + <label value="&recoveryKeyEntry.label;" + accesskey="&recoveryKeyEntry.accesskey;" + control="weavePassphrase"/> + <textbox id="weavePassphrase" + readonly="true"/> + </groupbox> + + <groupbox align="center"> + <description>&recoveryKeyBackup.description;</description> + <hbox> + <button id="printSyncKeyButton" + label="&button.syncKeyBackup.print.label;" + accesskey="&button.syncKeyBackup.print.accesskey;" + oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/> + <button id="saveSyncKeyButton" + label="&button.syncKeyBackup.save.label;" + accesskey="&button.syncKeyBackup.save.accesskey;" + oncommand="gSyncUtils.passphraseSave('weavePassphrase');"/> + </hbox> + </groupbox> + </wizardpage> + + <wizardpage id="deviceConnectedPage" + label="&addDevice.dialog.connected.label;" + onpageshow="gSyncAddDevice.onPageShow();"> + <vbox align="center"> + <image id="successPageIcon"/> + </vbox> + <separator/> + <description class="normal"> + &addDevice.dialog.successful.label; + </description> + </wizardpage> + +</wizard> diff --git a/browser/base/content/sync/genericChange.js b/browser/base/content/sync/genericChange.js new file mode 100644 index 000000000..6d1ce9485 --- /dev/null +++ b/browser/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/browser/base/content/sync/genericChange.xul b/browser/base/content/sync/genericChange.xul new file mode 100644 index 000000000..a35b1a20e --- /dev/null +++ b/browser/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="https://services.mozilla.com/sync/help/manual-setup"> + &addDevice.showMeHow.label; + </label> + </description> + </vbox> + + <spacer id="passphraseSpacer" + flex="1" + hidden="true"/> + + <description id="warningText" class="data"> + </description> + + <spacer flex="1"/> + </wizardpage> +</wizard> diff --git a/browser/base/content/sync/key.xhtml b/browser/base/content/sync/key.xhtml new file mode 100644 index 000000000..1363132e7 --- /dev/null +++ b/browser/base/content/sync/key.xhtml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html [ + <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd"> + %htmlDTD; + <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> + %syncBrandDTD; + <!ENTITY % syncKeyDTD SYSTEM "chrome://browser/locale/syncKey.dtd"> + %syncKeyDTD; + <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" > + %globalDTD; +]> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>&syncKey.page.title;</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> + <meta name="robots" content="noindex"/> + <style type="text/css"> + #synckey { font-size: 150% } + footer { font-size: 70% } + /* Bug 575675: Need to have an a:visited rule in a chrome document. */ + a:visited { color: purple; } + </style> +</head> + +<body dir="&locale.dir;"> +<h1>&syncKey.page.title;</h1> + +<p id="synckey" dir="ltr">SYNCKEY</p> + +<p>&syncKey.page.description2;</p> + +<div id="column1"> + <h2>&syncKey.keepItSecret.heading;</h2> + <p>&syncKey.keepItSecret.description;</p> +</div> + +<div id="column2"> + <h2>&syncKey.keepItSafe.heading;</h2> + <p><em>&syncKey.keepItSafe1.description;</em>&syncKey.keepItSafe2.description;<em>&syncKey.keepItSafe3.description;</em>&syncKey.keepItSafe4a.description;</p> +</div> + +<p>&syncKey.findOutMore1.label;<a href="https://services.mozilla.com">https://services.mozilla.com</a>&syncKey.findOutMore2.label;</p> + +<footer> + &syncKey.footer1.label;<a id="tosLink" href="termsURL">termsURL</a>&syncKey.footer2.label;<a id="ppLink" href="privacyURL">privacyURL</a>&syncKey.footer3.label; +</footer> + +</body> +</html> diff --git a/browser/base/content/sync/notification.xml b/browser/base/content/sync/notification.xml new file mode 100644 index 000000000..94e83f141 --- /dev/null +++ b/browser/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 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/browser/base/content/sync/progress.js b/browser/base/content/sync/progress.js new file mode 100644 index 000000000..2063f612a --- /dev/null +++ b/browser/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/browser/base/content/sync/progress.xhtml b/browser/base/content/sync/progress.xhtml new file mode 100644 index 000000000..d403cb20d --- /dev/null +++ b/browser/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/browser/base/content/sync/quota.js b/browser/base/content/sync/quota.js new file mode 100644 index 000000000..04858fa3b --- /dev/null +++ b/browser/base/content/sync/quota.js @@ -0,0 +1,268 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 row = {}, col = {}; + this.treeBox.getCellAt(event.clientX, event.clientY, row, col, {}); + if (col.value && col.value.id == "enabled") + this.toggle(row.value); + }, + + /* + * 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/browser/base/content/sync/quota.xul b/browser/base/content/sync/quota.xul new file mode 100644 index 000000000..99e6ed78b --- /dev/null +++ b/browser/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/browser/base/content/sync/setup.js b/browser/base/content/sync/setup.js new file mode 100644 index 000000000..3b07c9df0 --- /dev/null +++ b/browser/base/content/sync/setup.js @@ -0,0 +1,1088 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; + } + }, + +#ifdef XP_WIN +#ifdef MOZ_METRO + _securelyStoreForMetroSync: function(weaveEmail, weavePassword, weaveKey) { + try { + let metroUtils = Cc["@mozilla.org/windows-metroutils;1"]. + createInstance(Ci.nsIWinMetroUtils); + if (!metroUtils) + return; + metroUtils.storeSyncInfo(weaveEmail, weavePassword, weaveKey); + } catch (ex) { + } + }, +#endif +#endif + + 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"); +#ifdef XP_WIN +#ifdef MOZ_METRO + if (document.getElementById("metroSetupCheckbox").checked) { + this._securelyStoreForMetroSync(email, password, Weave.Service.identity.syncKey); + } +#endif +#endif + 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])); + } + 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/browser/base/content/sync/setup.xul b/browser/base/content/sync/setup.xul new file mode 100644 index 000000000..f01dad9f9 --- /dev/null +++ b/browser/base/content/sync/setup.xul @@ -0,0 +1,504 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd"> +<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd"> +%brandDTD; +%syncBrandDTD; +%syncSetupDTD; +]> +<wizard id="wizard" + title="&accountSetupTitle.label;" + windowtype="Weave:AccountSetup" + persist="screenX screenY" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onwizardnext="return gSyncSetup.onWizardAdvance()" + onwizardback="return gSyncSetup.onWizardBack()" + onwizardcancel="gSyncSetup.onWizardCancel()" + onload="gSyncSetup.init()"> + + <script type="application/javascript" + src="chrome://browser/content/sync/setup.js"/> + <script type="application/javascript" + src="chrome://browser/content/sync/utils.js"/> + <script type="application/javascript" + src="chrome://browser/content/utilityOverlay.js"/> + <script type="application/javascript" + src="chrome://global/content/printUtils.js"/> + + <wizardpage id="addDevicePage" + label="&pairDevice.title.label;" + onpageshow="gSyncSetup.onPageShow()"> + <description> + &pairDevice.dialog.description.label; + <label class="text-link" + value="&addDevice.showMeHow.label;" + href="https://services.mozilla.com/sync/help/add-device"/> + </description> + <separator class="groove-thin"/> + <description> + &addDevice.dialog.enterCode.label; + </description> + <separator class="groove-thin"/> + <vbox align="center"> + <textbox id="pin1" + class="pin" + oninput="gSyncSetup.onPINInput(this);" + onfocus="this.select();" + /> + <textbox id="pin2" + class="pin" + oninput="gSyncSetup.onPINInput(this);" + onfocus="this.select();" + /> + <textbox id="pin3" + class="pin" + oninput="gSyncSetup.onPINInput(this);" + onfocus="this.select();" + /> + </vbox> + <separator class="groove-thin"/> + <vbox id="pairDeviceThrobber" align="center" hidden="true"> + <image/> + </vbox> + <hbox id="pairDeviceErrorRow" pack="center" hidden="true"> + <image class="statusIcon" status="error"/> + <label class="status" + value="&addDevice.dialog.tryAgain.label;"/> + </hbox> + </wizardpage> + + <wizardpage id="pickSetupType" + label="&syncBrand.fullName.label;" + onpageshow="gSyncSetup.onPageShow()"> + <vbox align="center" flex="1"> + <description style="padding: 0 7em;"> + &setup.pickSetupType.description2; + </description> + <spacer flex="3"/> + <button id="newAccount" + class="accountChoiceButton" + label="&button.createNewAccount.label;" + oncommand="gSyncSetup.startNewAccountSetup()" + align="center"/> + <spacer flex="1"/> + </vbox> + <separator class="groove"/> + <vbox align="center" flex="1"> + <spacer flex="1"/> + <button id="existingAccount" + class="accountChoiceButton" + label="&button.haveAccount.label;" + oncommand="gSyncSetup.useExistingAccount()"/> + <spacer flex="3"/> + </vbox> + </wizardpage> + + <wizardpage label="&setup.newAccountDetailsPage.title.label;" + id="newAccountStart" + onextra1="gSyncSetup.onSyncOptions()" + onpageshow="gSyncSetup.onPageShow();"> + <grid> + <columns> + <column/> + <column class="inputColumn" flex="1"/> + </columns> + <rows> + <row id="emailRow" align="center"> + <label value="&setup.emailAddress.label;" + accesskey="&setup.emailAddress.accesskey;" + control="weaveEmail"/> + <textbox id="weaveEmail" + oninput="gSyncSetup.onEmailInput()"/> + </row> + <row id="emailFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row id="passwordRow" align="center"> + <label value="&setup.choosePassword.label;" + accesskey="&setup.choosePassword.accesskey;" + control="weavePassword"/> + <textbox id="weavePassword" + type="password" + onchange="gSyncSetup.onPasswordChange()"/> + </row> + <row id="confirmRow" align="center"> + <label value="&setup.confirmPassword.label;" + accesskey="&setup.confirmPassword.accesskey;" + control="weavePasswordConfirm"/> + <textbox id="weavePasswordConfirm" + type="password" + onchange="gSyncSetup.onPasswordChange()"/> + </row> + <row id="passwordFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row align="center"> + <label control="server" + value="&server.label;"/> + <menulist id="server" + oncommand="gSyncSetup.onServerCommand()" + oninput="gSyncSetup.onServerInput()"> + <menupopup> + <menuitem label="&serverType.default.label;" + value="main"/> + <menuitem label="&serverType.custom2.label;" + value="custom"/> + </menupopup> + </menulist> + </row> + <row id="serverFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> +#ifdef XP_WIN +#ifdef MOZ_METRO + <row id="metroRow" align="center"> + <spacer/> + <hbox align="center"> + <checkbox label="&setup.setupMetro.label;" + accesskey="&setup.setupMetro.accesskey;" + control="weavePasswordConfirm" + id="metroSetupCheckbox" + checked="true"/> + </hbox> + </row> +#endif +#endif + <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="https://services.mozilla.com/sync/help/easy-setup"/> + </description> + <label value="&addDevice.setup.enterCode.label;" + control="easySetupPIN1"/> + <spacer flex="1"/> + <vbox align="center" flex="1"> + <textbox id="easySetupPIN1" + class="pin" + value="" + readonly="true" + /> + <textbox id="easySetupPIN2" + class="pin" + value="" + readonly="true" + /> + <textbox id="easySetupPIN3" + class="pin" + value="" + readonly="true" + /> + </vbox> + <spacer flex="3"/> + <label class="text-link" + value="&addDevice.dontHaveDevice.label;" + onclick="gSyncSetup.manualSetup();"/> + </wizardpage> + + <wizardpage id="existingAccount" + label="&setup.signInPage.title.label;" + onextra1="gSyncSetup.onSyncOptions()" + onpageshow="gSyncSetup.onPageShow()"> + <grid> + <columns> + <column/> + <column class="inputColumn" flex="1"/> + </columns> + <rows> + <row id="existingAccountRow" align="center"> + <label id="existingAccountLabel" + value="&signIn.account2.label;" + accesskey="&signIn.account2.accesskey;" + control="existingAccount"/> + <textbox id="existingAccountName" + oninput="gSyncSetup.checkFields(event)" + onchange="gSyncSetup.checkFields(event)"/> + </row> + <row id="existingPasswordRow" align="center"> + <label id="existingPasswordLabel" + value="&signIn.password.label;" + accesskey="&signIn.password.accesskey;" + control="existingPassword"/> + <textbox id="existingPassword" + type="password" + onkeyup="gSyncSetup.checkFields(event)" + onchange="gSyncSetup.checkFields(event)"/> + </row> + <row id="existingPasswordFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </row> + <row align="center"> + <spacer/> + <label class="text-link" + value="&resetPassword.label;" + onclick="gSyncUtils.resetPassword(); return false;"/> + </row> + <row align="center"> + <label control="existingServer" + value="&server.label;"/> + <menulist id="existingServer" + oncommand="gSyncSetup.onExistingServerCommand()" + oninput="gSyncSetup.onExistingServerInput()"> + <menupopup> + <menuitem label="&serverType.default.label;" + value="main"/> + <menuitem label="&serverType.custom2.label;" + value="custom"/> + </menupopup> + </menulist> + </row> + <row id="existingServerFeedbackRow" align="center" hidden="true"> + <spacer/> + <hbox> + <image class="statusIcon"/> + <vbox> + <label class="status" value=" "/> + </vbox> + </hbox> + </row> + </rows> + </grid> + + <groupbox> + <label id="existingPassphraseLabel" + value="&signIn.recoveryKey.label;" + accesskey="&signIn.recoveryKey.accesskey;" + control="existingPassphrase"/> + <textbox id="existingPassphrase" + oninput="gSyncSetup.checkFields()"/> + <hbox id="login-throbber" hidden="true"> + <image/> + <label value="&verifying.label;"/> + </hbox> + <vbox align="left" id="existingPassphraseFeedbackRow" hidden="true"> + <hbox> + <image class="statusIcon"/> + <label class="status" value=" "/> + </hbox> + </vbox> + </groupbox> + + <vbox id="passphraseHelpBox"> + <description> + &existingRecoveryKey.description; + <label class="text-link" + href="https://services.mozilla.com/sync/help/manual-setup"> + &addDevice.showMeHow.label; + </label> + <spacer id="passphraseHelpSpacer"/> + <label class="text-link" + onclick="gSyncSetup.resetPassphrase(); return false;"> + &resetSyncKey.label; + </label> + </description> + </vbox> + </wizardpage> + + <wizardpage id="syncOptionsPage" + label="&setup.optionsPage.title;" + onpageshow="gSyncSetup.onPageShow()"> + <groupbox id="syncOptions"> + <grid> + <columns> + <column/> + <column flex="1" style="-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="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/browser/base/content/sync/utils.js b/browser/base/content/sync/utils.js new file mode 100644 index 000000000..af4c8a811 --- /dev/null +++ b/browser/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 0600 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/browser/base/content/tabbrowser.css b/browser/base/content/tabbrowser.css new file mode 100644 index 000000000..2dd6a0529 --- /dev/null +++ b/browser/base/content/tabbrowser.css @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.tabbrowser-tabbox { + -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-tabbox"); +} + +.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/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml new file mode 100644 index 000000000..6c76120a8 --- /dev/null +++ b/browser/base/content/tabbrowser.xml @@ -0,0 +1,4801 @@ +<?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="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> + + <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="_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> + + <method name="_getTabForBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + for (let i = 0; i < this.tabs.length; i++) { + if (this.tabs[i].linkedBrowser == aBrowser) + return this.tabs[i]; + } + return null; + ]]> + </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 (!(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) { + // The document loaded correctly, clear the value if we should + if (this.mBrowser.userTypedClear > 0 || + (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE)) + this.mBrowser.userTypedValue = null; + + // Clear out the missing plugins list since it's related to the + // previous location. + this.mBrowser.missingPlugins = 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.contains("#") ? "&" : "#") + + "-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").contains("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) { + TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS"); + 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; + } + + var updatePageReport = false; + if (!oldBrowser || + (oldBrowser.pageReport && !newBrowser.pageReport) || + (!oldBrowser.pageReport && newBrowser.pageReport)) + updatePageReport = true; + + newBrowser.setAttribute("type", "content-primary"); + newBrowser.docShellIsActive = + (window.windowState != window.STATE_MINIMIZED); + this.mCurrentBrowser = newBrowser; + this.mCurrentTab = this.tabContainer.selectedItem; + 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 (updatePageReport) + this.mCurrentBrowser.updatePageReport(); + + // 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 = document.createEvent("Events"); + event.initEvent("TabSelect", true, false); + 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(); + + if (!aForceUpdate) + TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS"); + ]]> + </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; + var aIsUTF8; + 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; + aIsUTF8 = params.isUTF8; + } + + 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, + isUTF8: aIsUTF8}); + 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; + var aIsUTF8; + 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; + aIsUTF8 = params.isUTF8; + } + + // 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; + 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; + if (aFromExternal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL; + if (aIsUTF8) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_URI_IS_UTF8; + 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; + + // 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.preventFurtherDialogs(); + } + + // 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); + + // 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. + 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); + + // Swap the docshells + ourBrowser.swapDocShells(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[ + // 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="pageReport" + onget="return this.mCurrentBrowser.pageReport;" + readonly="true"/> + + <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> + + <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"/> + + <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; + + // 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); + ]]> + </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); + 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 + // 2 - no close buttons at all + // 3 - close button at the end of the tabstrip + switch (this.mCloseButtons) { + case 0: + if (numTabs == 1 && this._closeWindowWithLastTab) + this.setAttribute("closebuttons", "hidden"); + else + this.setAttribute("closebuttons", "activetab"); + break; + case 1: + if (numTabs == 1) { + if (this._closeWindowWithLastTab) + this.setAttribute("closebuttons", "hidden"); + else + 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") { + // Don't need to do anything if we're in overflow mode and aren't scrolled + // all the way to the right, or if we're closing the last tab. + if (isEndTab || !this.mTabstrip._scrollButtonDown.disabled) + return; + + // If the tab has an owner that will become the active tab, the owner will + // be to the left of it, so we actually want the left tab to slide over. + // This can't be done as easily in non-overflow mode, so we don't bother. + if (aTab.owner) + return; + + //Pale Moon: Resize immediately if preffed + 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; + + //Pale Moon: Resize immediately if preffed + 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 (Services.telemetry.canRecord || 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; + } + + Services.telemetry.getHistogramById(aTab.closing ? + "FX_TAB_ANIM_CLOSE_MS" : + "FX_TAB_ANIM_OPEN_MS") + .add(Date.now() - aTab._animStartTime); + 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); + + Services.telemetry.getHistogramById("FX_TAB_ANIM_ANY_FRAME_INTERVAL_MS").add(averageInterval); + Services.telemetry.getHistogramById("FX_TAB_ANIM_ANY_FRAME_PAINT_MS").add(averagePaint); + + if (aTab._recordingTabOpenPlain) { + delete aTab._recordingTabOpenPlain; + // While we do have a telemetry probe NEWTAB_PAGE_ENABLED to monitor newtab preview, it'll be + // easier to overview the data without slicing by it. Hence the additional histograms with _PREVIEW. + let preview = this._browserNewtabpageEnabled ? "_PREVIEW" : ""; + Services.telemetry.getHistogramById("FX_TAB_ANIM_OPEN" + preview + "_FRAME_INTERVAL_MS").add(averageInterval); + Services.telemetry.getHistogramById("FX_TAB_ANIM_OPEN" + preview + "_FRAME_PAINT_MS").add(averagePaint); + } + } + ]]> + </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.contentWindow, 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.contains(" ")) //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 { + this.tabbrowser.getBrowserForTab(tab).loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP); + 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"/> + </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/browser/base/content/test/Makefile.in b/browser/base/content/test/Makefile.in new file mode 100644 index 000000000..104ad5972 --- /dev/null +++ b/browser/base/content/test/Makefile.in @@ -0,0 +1,372 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = @relativesrcdir@ + +include $(DEPTH)/config/autoconf.mk + +MOCHITEST_FILES = \ + head_plain.js \ + test_feed_discovery.html \ + feed_discovery.html \ + test_bug395533.html \ + bug395533-data.txt \ + ctxmenu-image.png \ + video.ogg \ + test_offlineNotification.html \ + offlineChild.html \ + offlineChild.cacheManifest \ + offlineChild.cacheManifest^headers^ \ + offlineChild2.html \ + offlineChild2.cacheManifest \ + offlineChild2.cacheManifest^headers^ \ + offlineEvent.html \ + offlineEvent.cacheManifest \ + offlineEvent.cacheManifest^headers^ \ + test_bug364677.html \ + bug364677-data.xml \ + bug364677-data.xml^headers^ \ + test_offline_gzip.html \ + gZipOfflineChild.html \ + gZipOfflineChild.html^headers^ \ + gZipOfflineChild.cacheManifest \ + gZipOfflineChild.cacheManifest^headers^ \ + $(NULL) + +# test_contextmenu.html is disabled on Linux due to bug 513558 +ifndef MOZ_WIDGET_GTK +MOCHITEST_FILES += \ + audio.ogg \ + test_contextmenu.html \ + subtst_contextmenu.html \ + privateBrowsingMode.js \ + $(NULL) +endif + +# The following tests are disabled because they are unreliable: +# browser_bug423833.js is bug 428712 +# browser_sanitize-download-history.js is bug 432425 +# +# browser_sanitizeDialog_treeView.js is disabled until the tree view is added +# back to the clear recent history dialog (sanitize.xul), if it ever is (bug +# 480169) + +# browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638. + +# browser_bug321000.js is disabled because newline handling is shaky (bug 592528) + +MOCHITEST_BROWSER_FILES = \ + head.js \ + browser_typeAheadFind.js \ + browser_keywordSearch.js \ + browser_keywordSearch_postData.js \ + POSTSearchEngine.xml \ + print_postdata.sjs \ + browser_aboutHome.js \ + browser_alltabslistener.js \ + browser_bug304198.js \ + title_test.svg \ + browser_bug329212.js \ + browser_bug356571.js \ + browser_bug380960.js \ + browser_bug386835.js \ + browser_bug405137.js \ + browser_bug406216.js \ + browser_bug409481.js \ + browser_bug409624.js \ + browser_bug413915.js \ + browser_bug416661.js \ + browser_bug417483.js \ + browser_bug419612.js \ + browser_identity_UI.js \ + browser_bug422590.js \ + browser_bug424101.js \ + browser_bug427559.js \ + browser_bug432599.js \ + browser_bug435035.js \ + browser_bug435325.js \ + browser_bug441778.js \ + browser_bug455852.js \ + browser_bug460146.js \ + browser_bug462673.js \ + browser_bug477014.js \ + browser_bug479408.js \ + browser_bug479408_sample.html \ + browser_bug481560.js \ + browser_bug484315.js \ + browser_bug491431.js \ + browser_bug495058.js \ + browser_bug517902.js \ + browser_bug519216.js \ + browser_bug520538.js \ + browser_bug521216.js \ + browser_bug533232.js \ + browser_bug537474.js \ + browser_bug550565.js \ + browser_bug553455.js \ + browser_bug555224.js \ + browser_bug555767.js \ + browser_bug556061.js \ + browser_bug559991.js \ + browser_bug561623.js \ + browser_bug561636.js \ + browser_bug562649.js \ + browser_bug563588.js \ + browser_bug565575.js \ + browser_bug567306.js \ + browser_zbug569342.js \ + browser_bug575561.js \ + browser_bug575830.js \ + browser_bug577121.js \ + browser_bug578534.js \ + browser_bug579872.js \ + browser_bug580638.js \ + browser_bug580956.js \ + browser_bug581242.js \ + browser_bug581253.js \ + browser_bug581947.js \ + browser_bug585558.js \ + browser_bug585785.js \ + browser_bug585830.js \ + browser_bug590206.js \ + browser_bug592338.js \ + browser_bug594131.js \ + browser_bug595507.js \ + browser_bug596687.js \ + browser_bug597218.js \ + browser_bug598923.js \ + browser_bug599325.js \ + browser_bug609700.js \ + browser_bug616836.js \ + browser_bug623155.js \ + browser_bug623893.js \ + browser_bug624734.js \ + browser_bug647886.js \ + browser_bug655584.js \ + browser_bug664672.js \ + browser_bug678392.js \ + browser_bug678392-1.html \ + browser_bug678392-2.html \ + browser_bug710878.js \ + browser_bug719271.js \ + browser_bug724239.js \ + browser_bug735471.js \ + browser_bug743421.js \ + browser_bug749738.js \ + browser_bug752516.js \ + browser_bug763468_perwindowpb.js \ + browser_bug767836_perwindowpb.js \ + browser_bug771331.js \ + browser_bug783614.js \ + browser_bug797677.js \ + browser_bug816527.js \ + browser_bug817947.js \ + browser_bug822367.js \ + browser_bug902156.js \ + browser_bug832435.js \ + browser_bug839103.js \ + browser_bug880101.js \ + browser_canonizeURL.js \ + browser_customize.js \ + browser_findbarClose.js \ + browser_homeDrop.js \ + browser_keywordBookmarklets.js \ + browser_contextSearchTabPosition.js \ + browser_ctrlTab.js \ + browser_customize_popupNotification.js \ + browser_disablechrome.js \ + browser_discovery.js \ + browser_duplicateIDs.js \ + browser_fullscreen-window-open.js \ + file_fullscreen-window-open.html \ + browser_gestureSupport.js \ + browser_getshortcutoruri.js \ + browser_hide_removing.js \ + browser_overflowScroll.js \ + browser_locationBarCommand.js \ + browser_locationBarExternalLoad.js \ + browser_page_style_menu.js \ + browser_pinnedTabs.js \ + browser_plainTextLinks.js \ + browser_pluginnotification.js \ + browser_plugins_added_dynamically.js \ + browser_CTP_drag_drop.js \ + browser_CTP_data_urls.js \ + browser_pluginplaypreview.js \ + browser_pluginplaypreview2.js \ + browser_private_browsing_window.js \ + browser_relatedTabs.js \ + browser_removeTabsToTheEnd.js \ + browser_sanitize-passwordDisabledHosts.js \ + browser_sanitize-sitepermissions.js \ + browser_sanitize-timespans.js \ + browser_tabopen_reflows.js \ + browser_clearplugindata.js \ + browser_clearplugindata.html \ + browser_clearplugindata_noage.html \ + browser_popupUI.js \ + browser_sanitizeDialog.js \ + browser_save_link-perwindowpb.js \ + browser_save_private_link_perwindowpb.js \ + browser_save_video.js \ + browser_tabMatchesInAwesomebar_perwindowpb.js \ + browser_tab_drag_drop_perwindow.js \ + bug564387.html \ + bug564387_video1.ogv \ + bug564387_video1.ogv^headers^ \ + bug792517.html \ + bug792517-2.html \ + bug792517.sjs \ + test_bug839103.html \ + bug839103.css \ + browser_scope.js \ + browser_selectTabAtIndex.js \ + browser_tab_dragdrop.js \ + browser_tab_dragdrop2.js \ + browser_tab_dragdrop2_frame1.xul \ + browser_tabfocus.js \ + browser_tabs_isActive.js \ + browser_tabs_owner.js \ + browser_unloaddialogs.js \ + browser_urlbarAutoFillTrimURLs.js \ + browser_urlbarCopying.js \ + browser_urlbarEnter.js \ + browser_urlbarRevert.js \ + browser_urlbarStop.js \ + browser_urlbarTrimURLs.js \ + browser_urlbar_search_healthreport.js \ + browser_urlHighlight.js \ + browser_visibleFindSelection.js \ + browser_visibleTabs.js \ + browser_visibleTabs_contextMenu.js \ + browser_visibleTabs_bookmarkAllPages.js \ + browser_visibleTabs_bookmarkAllTabs.js \ + browser_visibleTabs_tabPreview.js \ + bug592338.html \ + disablechrome.html \ + discovery.html \ + domplate_test.js \ + file_bug822367_1.html \ + file_bug822367_1.js \ + file_bug822367_2.html \ + file_bug822367_3.html \ + file_bug822367_4.html \ + file_bug822367_4.js \ + file_bug822367_4B.html \ + file_bug822367_5.html \ + file_bug822367_6.html \ + file_bug902156_1.html \ + file_bug902156_2.html \ + file_bug902156_3.html \ + file_bug902156.js \ + moz.png \ + video.ogg \ + test_bug435035.html \ + test_bug462673.html \ + page_style_sample.html \ + plugin_unknown.html \ + plugin_test.html \ + plugin_test2.html \ + plugin_test3.html \ + plugin_alternate_content.html \ + plugin_both.html \ + plugin_both2.html \ + plugin_add_dynamically.html \ + plugin_clickToPlayAllow.html \ + plugin_clickToPlayDeny.html \ + plugin_bug744745.html \ + plugin_bug749455.html \ + plugin_bug752516.html \ + plugin_bug787619.html \ + plugin_bug797677.html \ + plugin_bug820497.html \ + plugin_hidden_to_visible.html \ + plugin_two_types.html \ + plugin_data_url.html \ + alltabslistener.html \ + zoom_test.html \ + dummy_page.html \ + file_bug550565_popup.html \ + file_bug550565_favicon.ico \ + app_bug575561.html \ + app_subframe_bug575561.html \ + browser_contentAreaClick.js \ + browser_addon_bar_close_button.js \ + browser_addon_bar_shortcut.js \ + browser_addon_bar_aomlistener.js \ + test_bug628179.html \ + browser_wyciwyg_urlbarCopying.js \ + test_wyciwyg_copying.html \ + authenticate.sjs \ + browser_minimize.js \ + browser_aboutSyncProgress.js \ + browser_middleMouse_inherit.js \ + redirect_bug623155.sjs \ + browser_tabDrop.js \ + browser_lastAccessedTab.js \ + browser_bug734076.js \ + browser_bug744745.js \ + browser_bug787619.js \ + browser_bug812562.js \ + browser_bug818118.js \ + browser_bug820497.js \ + blockPluginVulnerableUpdatable.xml \ + blockPluginVulnerableNoUpdate.xml \ + blockNoPlugins.xml \ + blockPluginHard.xml \ + browser_utilityOverlay.js \ + browser_bug676619.js \ + download_page.html \ + browser_URLBarSetURI.js \ + browser_pageInfo_plugins.js \ + browser_pageInfo.js \ + feed_tab.html \ + browser_pluginCrashCommentAndURL.js \ + pluginCrashCommentAndURL.html \ + browser_private_no_prompt.js \ + browser_blob-channelname.js \ + healthreport_testRemoteCommands.html \ + browser_offlineQuotaNotification.js \ + offlineQuotaNotification.html \ + offlineQuotaNotification.cacheManifest \ + $(NULL) + +# Disable tests on Windows due to frequent failures (bugs 825739, 841341) +ifneq (windows,$(MOZ_WIDGET_TOOLKIT)) +MOCHITEST_BROWSER_FILES += \ + browser_bookmark_titles.js \ + browser_popupNotification.js \ + $(NULL) +endif + +ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT)) +MOCHITEST_BROWSER_FILES += \ + browser_bug462289.js \ + $(NULL) +else +MOCHITEST_BROWSER_FILES += \ + browser_bug565667.js \ + $(NULL) +endif + +ifdef MOZ_DATA_REPORTING +MOCHITEST_BROWSER_FILES += \ + browser_datareporting_notification.js \ + $(NULL) +endif + +# browser_aboutHealthReport.js disabled for frequent failures on Linux (bug 924307) +# browser_CTP_context_menu.js fails intermittently on Linux (bug 909342) +ifndef MOZ_WIDGET_GTK +MOCHITEST_BROWSER_FILES += \ + browser_aboutHealthReport.js \ + browser_CTP_context_menu.js \ + $(NULL) +endif + +include $(topsrcdir)/config/rules.mk diff --git a/browser/base/content/test/POSTSearchEngine.xml b/browser/base/content/test/POSTSearchEngine.xml new file mode 100644 index 000000000..85557d854 --- /dev/null +++ b/browser/base/content/test/POSTSearchEngine.xml @@ -0,0 +1,6 @@ +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>POST Search</ShortName> + <Url type="text/html" method="POST" template="http://mochi.test:8888/browser/browser/base/content/test/print_postdata.sjs"> + <Param name="searchterms" value="{searchTerms}"/> + </Url> +</OpenSearchDescription> diff --git a/browser/base/content/test/alltabslistener.html b/browser/base/content/test/alltabslistener.html new file mode 100644 index 000000000..166c31037 --- /dev/null +++ b/browser/base/content/test/alltabslistener.html @@ -0,0 +1,8 @@ +<html> +<head> +<title>Test page for bug 463387</title> +</head> +<body> +<p>Test page for bug 463387</p> +</body> +</html> diff --git a/browser/base/content/test/app_bug575561.html b/browser/base/content/test/app_bug575561.html new file mode 100644 index 000000000..00f2e5488 --- /dev/null +++ b/browser/base/content/test/app_bug575561.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=575561
+-->
+ <head>
+ <title>Test for links in app tabs</title>
+ </head>
+ <body>
+ <a href="http://example.com/browser/browser/base/content/test/dummy_page.html">same domain</a>
+ <a href="http://test1.example.com/browser/browser/base/content/test/dummy_page.html">same domain (different subdomain)</a>
+ <a href="http://example.org/browser/browser/base/content/test/dummy_page.html">different domain</a>
+ <a href="http://example.org/browser/browser/base/content/test/dummy_page.html" target="foo">different domain (with target)</a>
+ <a href="http://www.example.com/browser/browser/base/content/test/dummy_page.html">same domain (www prefix)</a>
+ <a href="data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>">data: URI</a>
+ <a href="about:mozilla">about: URI</a>
+ <iframe src="app_subframe_bug575561.html"></iframe>
+ </body>
+</html>
diff --git a/browser/base/content/test/app_subframe_bug575561.html b/browser/base/content/test/app_subframe_bug575561.html new file mode 100644 index 000000000..754f3806e --- /dev/null +++ b/browser/base/content/test/app_subframe_bug575561.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=575561 +--> + <head> + <title>Test for links in app tab subframes</title> + </head> + <body> + <a href="http://example.org/browser/browser/base/content/test/dummy_page.html">different domain</a> + </body> +</html> diff --git a/browser/base/content/test/audio.ogg b/browser/base/content/test/audio.ogg Binary files differnew file mode 100644 index 000000000..7e6ef77ec --- /dev/null +++ b/browser/base/content/test/audio.ogg diff --git a/browser/base/content/test/authenticate.sjs b/browser/base/content/test/authenticate.sjs new file mode 100644 index 000000000..58da655cf --- /dev/null +++ b/browser/base/content/test/authenticate.sjs @@ -0,0 +1,220 @@ +function handleRequest(request, response) +{ + try { + reallyHandleRequest(request, response); + } catch (e) { + response.setStatusLine("1.0", 200, "AlmostOK"); + response.write("Error handling request: " + e); + } +} + + +function reallyHandleRequest(request, response) { + var match; + var requestAuth = true, requestProxyAuth = true; + + // Allow the caller to drive how authentication is processed via the query. + // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar + // The extra ? allows the user/pass/realm checks to succeed if the name is + // at the beginning of the query string. + var query = "?" + request.queryString; + + var expected_user = "", expected_pass = "", realm = "mochitest"; + var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy"; + var huge = false, plugin = false, anonymous = false; + var authHeaderCount = 1; + // user=xxx + match = /[^_]user=([^&]*)/.exec(query); + if (match) + expected_user = match[1]; + + // pass=xxx + match = /[^_]pass=([^&]*)/.exec(query); + if (match) + expected_pass = match[1]; + + // realm=xxx + match = /[^_]realm=([^&]*)/.exec(query); + if (match) + realm = match[1]; + + // proxy_user=xxx + match = /proxy_user=([^&]*)/.exec(query); + if (match) + proxy_expected_user = match[1]; + + // proxy_pass=xxx + match = /proxy_pass=([^&]*)/.exec(query); + if (match) + proxy_expected_pass = match[1]; + + // proxy_realm=xxx + match = /proxy_realm=([^&]*)/.exec(query); + if (match) + proxy_realm = match[1]; + + // huge=1 + match = /huge=1/.exec(query); + if (match) + huge = true; + + // plugin=1 + match = /plugin=1/.exec(query); + if (match) + plugin = true; + + // multiple=1 + match = /multiple=([^&]*)/.exec(query); + if (match) + authHeaderCount = match[1]+0; + + // anonymous=1 + match = /anonymous=1/.exec(query); + if (match) + anonymous = true; + + // Look for an authentication header, if any, in the request. + // + // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + // + // This test only supports Basic auth. The value sent by the client is + // "username:password", obscured with base64 encoding. + + var actual_user = "", actual_pass = "", authHeader, authPresent = false; + if (request.hasHeader("Authorization")) { + authPresent = true; + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw "Couldn't parse auth header: " + authHeader; + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw "Couldn't decode auth header: " + userpass; + actual_user = match[1]; + actual_pass = match[2]; + } + + var proxy_actual_user = "", proxy_actual_pass = ""; + if (request.hasHeader("Proxy-Authorization")) { + authHeader = request.getHeader("Proxy-Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw "Couldn't parse auth header: " + authHeader; + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw "Couldn't decode auth header: " + userpass; + proxy_actual_user = match[1]; + proxy_actual_pass = match[2]; + } + + // Don't request authentication if the credentials we got were what we + // expected. + if (expected_user == actual_user && + expected_pass == actual_pass) { + requestAuth = false; + } + if (proxy_expected_user == proxy_actual_user && + proxy_expected_pass == proxy_actual_pass) { + requestProxyAuth = false; + } + + if (anonymous) { + if (authPresent) { + response.setStatusLine("1.0", 400, "Unexpected authorization header found"); + } else { + response.setStatusLine("1.0", 200, "Authorization header not found"); + } + } else { + if (requestProxyAuth) { + response.setStatusLine("1.0", 407, "Proxy authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true); + } else if (requestAuth) { + response.setStatusLine("1.0", 401, "Authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true); + } else { + response.setStatusLine("1.0", 200, "OK"); + } + } + + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write("<html xmlns='http://www.w3.org/1999/xhtml'>"); + response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n"); + response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n"); + response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n"); + response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n"); + response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n"); + + if (huge) { + response.write("<div style='display: none'>"); + for (i = 0; i < 100000; i++) { + response.write("123456789\n"); + } + response.write("</div>"); + response.write("<span id='footnote'>This is a footnote after the huge content fill</span>"); + } + + if (plugin) { + response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " + + "type='application/x-test'></embed>\n"); + } + + response.write("</html>"); +} + + +// base64 decoder +// +// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa() +// doesn't seem to exist. :-( +/* Convert Base64 data to a string */ +const toBinaryTable = [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 +]; +const base64Pad = '='; + +function base64ToString(data) { + + var result = ''; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + + // Convert one by one. + for (var i = 0; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data[i] == base64Pad); + // Skip illegal characters and whitespace + if (c == -1) continue; + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) + result += String.fromCharCode((leftdata >> leftbits) & 0xff); + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) + throw Components.Exception('Corrupted base64 string'); + + return result; +} diff --git a/browser/base/content/test/blockNoPlugins.xml b/browser/base/content/test/blockNoPlugins.xml new file mode 100644 index 000000000..e4e191b37 --- /dev/null +++ b/browser/base/content/test/blockNoPlugins.xml @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1336406310001"> + <emItems> + </emItems> + <pluginItems> + </pluginItems> +</blocklist> diff --git a/browser/base/content/test/blockPluginHard.xml b/browser/base/content/test/blockPluginHard.xml new file mode 100644 index 000000000..24eb5bc6f --- /dev/null +++ b/browser/base/content/test/blockPluginHard.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1336406310000"> + <emItems> + </emItems> + <pluginItems> + <pluginItem blockID="p9999"> + <match name="filename" exp="libnptest\.so|nptest\.dll|Test\.plugin" /> + <versionRange severity="2"></versionRange> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/browser/base/content/test/blockPluginVulnerableNoUpdate.xml b/browser/base/content/test/blockPluginVulnerableNoUpdate.xml new file mode 100644 index 000000000..bf8545afe --- /dev/null +++ b/browser/base/content/test/blockPluginVulnerableNoUpdate.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1336406310000"> + <emItems> + </emItems> + <pluginItems> + <pluginItem blockID="p9999"> + <match name="filename" exp="libnptest\.so|nptest\.dll|Test\.plugin" /> + <versionRange severity="0" vulnerabilitystatus="2"></versionRange> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/browser/base/content/test/blockPluginVulnerableUpdatable.xml b/browser/base/content/test/blockPluginVulnerableUpdatable.xml new file mode 100644 index 000000000..5545162b1 --- /dev/null +++ b/browser/base/content/test/blockPluginVulnerableUpdatable.xml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1336406310000"> + <emItems> + </emItems> + <pluginItems> + <pluginItem blockID="p9999"> + <match name="filename" exp="libnptest\.so|nptest\.dll|Test\.plugin" /> + <versionRange severity="0" vulnerabilitystatus="1"></versionRange> + </pluginItem> + </pluginItems> +</blocklist> diff --git a/browser/base/content/test/browser_CTP_context_menu.js b/browser/base/content/test/browser_CTP_context_menu.js new file mode 100644 index 000000000..3dcff2b8b --- /dev/null +++ b/browser/base/content/test/browser_CTP_context_menu.js @@ -0,0 +1,103 @@ +var rootDir = getRootDirectory(gTestPath); +const gTestRoot = rootDir; +const gHttpTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + +var gTestBrowser = null; +var gNextTest = null; +var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost); + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + clearAllPluginPermissions(); + Services.prefs.clearUserPref("extensions.blocklist.suppressUI"); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true); + + let newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + + Services.prefs.setBoolPref("plugins.click_to_play", true); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + prepareTest(runAfterPluginBindingAttached(test1), gHttpTestRoot + "plugin_test.html"); +} + +function finishTest() { + clearAllPluginPermissions(); + gTestBrowser.removeEventListener("load", pageLoad, true); + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} + +function pageLoad() { + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + executeSoon(gNextTest); +} + +function prepareTest(nextTest, url) { + gNextTest = nextTest; + gTestBrowser.contentWindow.location = url; +} + +// Due to layout being async, "PluginBindAttached" may trigger later. +// This wraps a function to force a layout flush, thus triggering it, +// and schedules the function execution so they're definitely executed +// afterwards. +function runAfterPluginBindingAttached(func) { + return function() { + let doc = gTestBrowser.contentDocument; + let elems = doc.getElementsByTagName('embed'); + if (elems.length < 1) { + elems = doc.getElementsByTagName('object'); + } + elems[0].clientTop; + executeSoon(func); + }; +} + +// Test that the activate action in content menus for CTP plugins works +function test1() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 1, Should have a click-to-play notification"); + + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 1, Plugin should not be activated"); + + window.document.addEventListener("popupshown", test2, false); + EventUtils.synthesizeMouseAtCenter(plugin, + { type: "contextmenu", button: 2 }, + gTestBrowser.contentWindow); +} + +function test2() { + window.document.removeEventListener("popupshown", test2, false); + let activate = window.document.getElementById("context-ctp-play"); + ok(activate, "Test 2, Should have a context menu entry for activating the plugin"); + + // Trigger the click-to-play popup + activate.doCommand(); + + let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "Test 2, Should have a click-to-play notification"); + ok(!notification.dismissed, "Test 2, The click-to-play notification should not be dismissed"); + + // Activate the plugin + PopupNotifications.panel.firstChild._primaryButton.click(); + + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + waitForCondition(() => objLoadingContent.activated, test3, "Waited too long for plugin to activate"); +} + +function test3() { + finishTest(); +} diff --git a/browser/base/content/test/browser_CTP_data_urls.js b/browser/base/content/test/browser_CTP_data_urls.js new file mode 100644 index 000000000..188f488ba --- /dev/null +++ b/browser/base/content/test/browser_CTP_data_urls.js @@ -0,0 +1,256 @@ +var rootDir = getRootDirectory(gTestPath); +const gTestRoot = rootDir; +const gHttpTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + +var gTestBrowser = null; +var gNextTest = null; +var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost); + +Components.utils.import("resource://gre/modules/Services.jsm"); + +// This listens for the next opened tab and checks it is of the right url. +// opencallback is called when the new tab is fully loaded +// closecallback is called when the tab is closed +function TabOpenListener(url, opencallback, closecallback) { + this.url = url; + this.opencallback = opencallback; + this.closecallback = closecallback; + + gBrowser.tabContainer.addEventListener("TabOpen", this, false); +} + +TabOpenListener.prototype = { + url: null, + opencallback: null, + closecallback: null, + tab: null, + browser: null, + + handleEvent: function(event) { + if (event.type == "TabOpen") { + gBrowser.tabContainer.removeEventListener("TabOpen", this, false); + this.tab = event.originalTarget; + this.browser = this.tab.linkedBrowser; + gBrowser.addEventListener("pageshow", this, false); + } else if (event.type == "pageshow") { + if (event.target.location.href != this.url) + return; + gBrowser.removeEventListener("pageshow", this, false); + this.tab.addEventListener("TabClose", this, false); + var url = this.browser.contentDocument.location.href; + is(url, this.url, "Should have opened the correct tab"); + this.opencallback(this.tab, this.browser.contentWindow); + } else if (event.type == "TabClose") { + if (event.originalTarget != this.tab) + return; + this.tab.removeEventListener("TabClose", this, false); + this.opencallback = null; + this.tab = null; + this.browser = null; + // Let the window close complete + executeSoon(this.closecallback); + this.closecallback = null; + } + } +}; + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + clearAllPluginPermissions(); + Services.prefs.clearUserPref("extensions.blocklist.suppressUI"); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true); + + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + + Services.prefs.setBoolPref("plugins.click_to_play", true); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + prepareTest(test1a, gHttpTestRoot + "plugin_data_url.html"); +} + +function finishTest() { + clearAllPluginPermissions(); + gTestBrowser.removeEventListener("load", pageLoad, true); + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} + +function pageLoad() { + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + executeSoon(gNextTest); +} + +function prepareTest(nextTest, url) { + gNextTest = nextTest; + gTestBrowser.contentWindow.location = url; +} + +// Due to layout being async, "PluginBindAttached" may trigger later. +// This wraps a function to force a layout flush, thus triggering it, +// and schedules the function execution so they're definitely executed +// afterwards. +function runAfterPluginBindingAttached(func) { + return function() { + let doc = gTestBrowser.contentDocument; + let elems = doc.getElementsByTagName('embed'); + if (elems.length < 1) { + elems = doc.getElementsByTagName('object'); + } + elems[0].clientTop; + executeSoon(func); + }; +} + +// Test that the click-to-play doorhanger still works when navigating to data URLs +function test1a() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 1a, Should have a click-to-play notification"); + + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 1a, Plugin should not be activated"); + + gNextTest = runAfterPluginBindingAttached(test1b); + gTestBrowser.contentDocument.getElementById("data-link-1").click(); +} + +function test1b() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 1b, Should have a click-to-play notification"); + + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 1b, Plugin should not be activated"); + + // Simulate clicking the "Allow Always" button. + popupNotification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + let condition = function() objLoadingContent.activated; + waitForCondition(condition, test1c, "Test 1b, Waited too long for plugin to activate"); +} + +function test1c() { + clearAllPluginPermissions(); + prepareTest(runAfterPluginBindingAttached(test2a), gHttpTestRoot + "plugin_data_url.html"); +} + +// Test that the click-to-play notification doesn't break when navigating to data URLs with multiple plugins +function test2a() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 2a, Should have a click-to-play notification"); + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 2a, Plugin should not be activated"); + + gNextTest = runAfterPluginBindingAttached(test2b); + gTestBrowser.contentDocument.getElementById("data-link-2").click(); +} + +function test2b() { + let notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "Test 2b, Should have a click-to-play notification"); + + // Simulate choosing "Allow now" for the test plugin + notification.reshow(); + is(notification.options.centerActions.length, 2, "Test 2b, Should have two types of plugin in the notification"); + + var centerAction = null; + for (var action of notification.options.centerActions) { + if (action.pluginName == "Test") { + centerAction = action; + break; + } + } + ok(centerAction, "Test 2b, found center action for the Test plugin"); + + var centerItem = null; + for (var item of PopupNotifications.panel.firstChild.childNodes) { + is(item.value, "block", "Test 2b, all plugins should start out blocked"); + if (item.action == centerAction) { + centerItem = item; + break; + } + } + ok(centerItem, "Test 2b, found center item for the Test plugin"); + + // "click" the button to activate the Test plugin + centerItem.value = "allownow"; + PopupNotifications.panel.firstChild._primaryButton.click(); + + let plugin = gTestBrowser.contentDocument.getElementById("test1"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + let condition = function() objLoadingContent.activated; + waitForCondition(condition, test2c, "Test 2b, Waited too long for plugin to activate"); +} + +function test2c() { + let plugin = gTestBrowser.contentDocument.getElementById("test1"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 2c, Plugin should be activated"); + + clearAllPluginPermissions(); + prepareTest(runAfterPluginBindingAttached(test3a), gHttpTestRoot + "plugin_data_url.html"); +} + +// Test that when navigating to a data url, the plugin permission is inherited +function test3a() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 3a, Should have a click-to-play notification"); + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 3a, Plugin should not be activated"); + + // Simulate clicking the "Allow Always" button. + popupNotification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + let condition = function() objLoadingContent.activated; + waitForCondition(condition, test3b, "Test 3a, Waited too long for plugin to activate"); +} + +function test3b() { + gNextTest = test3c; + gTestBrowser.contentDocument.getElementById("data-link-1").click(); +} + +function test3c() { + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 3c, Plugin should be activated"); + + clearAllPluginPermissions(); + prepareTest(runAfterPluginBindingAttached(test4b), + 'data:text/html,<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>'); +} + +// Test that the click-to-play doorhanger still works when directly navigating to data URLs +function test4a() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 4a, Should have a click-to-play notification"); + let plugin = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 4a, Plugin should not be activated"); + + // Simulate clicking the "Allow Always" button. + popupNotification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + let condition = function() objLoadingContent.activated; + waitForCondition(condition, test4b, "Test 4a, Waited too long for plugin to activate"); +} + +function test4b() { + clearAllPluginPermissions(); + finishTest(); +} diff --git a/browser/base/content/test/browser_CTP_drag_drop.js b/browser/base/content/test/browser_CTP_drag_drop.js new file mode 100644 index 000000000..85305bd94 --- /dev/null +++ b/browser/base/content/test/browser_CTP_drag_drop.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + +let gNextTest = null; +let gNewWindow = null; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + clearAllPluginPermissions(); + Services.prefs.clearUserPref("plugins.click_to_play"); + let plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + Services.prefs.setBoolPref("plugins.click_to_play", true); + let plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("PluginBindingAttached", handleEvent, true, true); + gNextTest = part1; + gBrowser.selectedBrowser.contentDocument.location = gHttpTestRoot + "plugin_test.html"; +} + +function handleEvent() { + gNextTest(); +} + +function part1() { + gBrowser.selectedBrowser.removeEventListener("PluginBindingAttached", handleEvent); + ok(PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should have a click-to-play notification in the initial tab"); + + gNextTest = part2; + gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab); + gNewWindow.addEventListener("load", handleEvent, true); +} + +function part2() { + gNewWindow.removeEventListener("load", handleEvent); + let condition = function() PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser); + waitForCondition(condition, part3, "Waited too long for click-to-play notification"); +} + +function part3() { + ok(PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser), "Should have a click-to-play notification in the tab in the new window"); + ok(!PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should not have a click-to-play notification in the old window now"); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, gNewWindow.gBrowser.selectedTab); + let condition = function() PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser); + waitForCondition(condition, part4, "Waited too long for click-to-play notification"); +} + +function part4() { + ok(PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should have a click-to-play notification in the initial tab again"); + + gBrowser.selectedBrowser.addEventListener("PluginBindingAttached", handleEvent, true, true); + gNextTest = part5; + gBrowser.selectedBrowser.contentDocument.location = gHttpTestRoot + "plugin_test.html"; +} + +function part5() { + gBrowser.selectedBrowser.removeEventListener("PluginBindingAttached", handleEvent); + ok(PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should have a click-to-play notification in the initial tab"); + + gNextTest = part6; + gNewWindow = gBrowser.replaceTabWithWindow(gBrowser.selectedTab); + gNewWindow.addEventListener("load", handleEvent, true); +} + +function part6() { + gNewWindow.removeEventListener("load", handleEvent); + let condition = function() PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser); + waitForCondition(condition, part7, "Waited too long for click-to-play notification"); +} + +function part7() { + ok(PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser), "Should have a click-to-play notification in the tab in the new window"); + ok(!PopupNotifications.getNotification("click-to-play-plugins", gBrowser.selectedBrowser), "Should not have a click-to-play notification in the old window now"); + + let plugin = gNewWindow.gBrowser.selectedBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "plugin should not be activated"); + + EventUtils.synthesizeMouseAtCenter(plugin, {}, gNewWindow.gBrowser.selectedBrowser.contentWindow); + let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gNewWindow.gBrowser.selectedBrowser).dismissed && gNewWindow.PopupNotifications.panel.firstChild; + waitForCondition(condition, part8, "waited too long for plugin to activate"); +} + +function part8() { + // Click the activate button on doorhanger to make sure it works + gNewWindow.PopupNotifications.panel.firstChild._primaryButton.click(); + + let plugin = gNewWindow.gBrowser.selectedBrowser.contentDocument.getElementById("test"); + let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "plugin should be activated now"); + + gNewWindow.close(); + finish(); +} diff --git a/browser/base/content/test/browser_URLBarSetURI.js b/browser/base/content/test/browser_URLBarSetURI.js new file mode 100644 index 000000000..f98dbc5f0 --- /dev/null +++ b/browser/base/content/test/browser_URLBarSetURI.js @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + // avoid prompting about phishing + Services.prefs.setIntPref(phishyUserPassPref, 32); + registerCleanupFunction(function () { + Services.prefs.clearUserPref(phishyUserPassPref); + }); + + nextTest(); +} + +const phishyUserPassPref = "network.http.phishy-userpass-length"; + +function nextTest() { + let test = tests.shift(); + if (test) { + test(function () { + executeSoon(nextTest); + }); + } else { + executeSoon(finish); + } +} + +let tests = [ + function revert(next) { + loadTabInWindow(window, function (tab) { + gURLBar.handleRevert(); + is(gURLBar.value, "example.com", "URL bar had user/pass stripped after reverting"); + gBrowser.removeTab(tab); + next(); + }); + }, + function customize(next) { + whenNewWindowLoaded(undefined, function (win) { + // Need to wait for delayedStartup for the customization part of the test, + // since that's where BrowserToolboxCustomizeDone is set. + whenDelayedStartupFinished(win, function () { + loadTabInWindow(win, function () { + openToolbarCustomizationUI(function () { + closeToolbarCustomizationUI(function () { + is(win.gURLBar.value, "example.com", "URL bar had user/pass stripped after customize"); + win.close(); + next(); + }, win); + }, win); + }); + }); + }); + }, + function pageloaderror(next) { + loadTabInWindow(window, function (tab) { + // Load a new URL and then immediately stop it, to simulate a page load + // error. + tab.linkedBrowser.loadURI("http://test1.example.com"); + tab.linkedBrowser.stop(); + is(gURLBar.value, "example.com", "URL bar had user/pass stripped after load error"); + gBrowser.removeTab(tab); + next(); + }); + } +]; + +function loadTabInWindow(win, callback) { + info("Loading tab"); + let url = "http://user:pass@example.com/"; + let tab = win.gBrowser.selectedTab = win.gBrowser.addTab(url); + tab.linkedBrowser.addEventListener("load", function listener() { + info("Tab loaded"); + if (tab.linkedBrowser.currentURI.spec != url) + return; + tab.linkedBrowser.removeEventListener("load", listener, true); + + is(win.gURLBar.value, "example.com", "URL bar had user/pass stripped initially"); + callback(tab); + }, true); +} diff --git a/browser/base/content/test/browser_aboutHealthReport.js b/browser/base/content/test/browser_aboutHealthReport.js new file mode 100644 index 000000000..b2e74140b --- /dev/null +++ b/browser/base/content/test/browser_aboutHealthReport.js @@ -0,0 +1,105 @@ +/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/commonjs/sdk/core/promise.js");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+registerCleanupFunction(function() {
+ // Ensure we don't pollute prefs for next tests.
+ try {
+ Services.prefs.clearUserPref("datareporting.healthreport.about.reportUrl");
+ let policy = Cc["@mozilla.org/datareporting/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject
+ .policy;
+ policy.recordHealthReportUploadEnabled(true,
+ "Resetting after tests.");
+ } catch (ex) {}
+});
+
+let gTests = [
+
+{
+ desc: "Test the remote commands",
+ setup: function ()
+ {
+ Services.prefs.setCharPref("datareporting.healthreport.about.reportUrl",
+ "https://example.com/browser/browser/base/content/test/healthreport_testRemoteCommands.html");
+ },
+ run: function ()
+ {
+ let deferred = Promise.defer();
+
+ let policy = Cc["@mozilla.org/datareporting/service;1"]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject
+ .policy;
+
+ let results = 0;
+ try {
+ let win = gBrowser.contentWindow;
+ win.addEventListener("message", function testLoad(e) {
+ if (e.data.type == "testResult") {
+ ok(e.data.pass, e.data.info);
+ results++;
+ }
+ else if (e.data.type == "testsComplete") {
+ is(results, e.data.count, "Checking number of results received matches the number of tests that should have run");
+ win.removeEventListener("message", testLoad, false, true);
+ deferred.resolve();
+ }
+
+ }, false, true);
+
+ } catch(e) {
+ ok(false, "Failed to get all commands");
+ deferred.reject();
+ }
+ return deferred.promise;
+ }
+},
+
+
+]; // gTests
+
+function test()
+{
+ waitForExplicitFinish();
+
+ // xxxmpc leaving this here until we resolve bug 854038 and bug 854060
+ requestLongerTimeout(10);
+
+ Task.spawn(function () {
+ for (let test of gTests) {
+ info(test.desc);
+ test.setup();
+
+ yield promiseNewTabLoadEvent("about:healthreport");
+
+ yield test.run();
+
+ gBrowser.removeCurrentTab();
+ }
+
+ finish();
+ });
+}
+
+function promiseNewTabLoadEvent(aUrl, aEventType="load")
+{
+ let deferred = Promise.defer();
+ let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+ tab.linkedBrowser.addEventListener(aEventType, function load(event) {
+ tab.linkedBrowser.removeEventListener(aEventType, load, true);
+ let iframe = tab.linkedBrowser.contentDocument.getElementById("remote-report");
+ iframe.addEventListener("load", function frameLoad(e) {
+ iframe.removeEventListener("load", frameLoad, false);
+ deferred.resolve();
+ }, false);
+ }, true);
+ return deferred.promise;
+}
+
diff --git a/browser/base/content/test/browser_aboutHome.js b/browser/base/content/test/browser_aboutHome.js new file mode 100644 index 000000000..3edbc5613 --- /dev/null +++ b/browser/base/content/test/browser_aboutHome.js @@ -0,0 +1,520 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/commonjs/sdk/core/promise.js"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils", + "resource:///modules/AboutHomeUtils.jsm"); + +let gRightsVersion = Services.prefs.getIntPref("browser.rights.version"); + +registerCleanupFunction(function() { + // Ensure we don't pollute prefs for next tests. + Services.prefs.clearUserPref("network.cookies.cookieBehavior"); + Services.prefs.clearUserPref("network.cookie.lifetimePolicy"); + Services.prefs.clearUserPref("browser.rights.override"); + Services.prefs.clearUserPref("browser.rights." + gRightsVersion + ".shown"); +}); + +let gTests = [ + +{ + desc: "Check that clearing cookies does not clear storage", + setup: function () + { + Cc["@mozilla.org/observer-service;1"] + .getService(Ci.nsIObserverService) + .notifyObservers(null, "cookie-changed", "cleared"); + }, + run: function (aSnippetsMap) + { + isnot(aSnippetsMap.get("snippets-last-update"), null, + "snippets-last-update should have a value"); + } +}, + +{ + desc: "Check default snippets are shown", + setup: function () { }, + run: function () + { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let snippetsElt = doc.getElementById("snippets"); + ok(snippetsElt, "Found snippets element") + is(snippetsElt.getElementsByTagName("span").length, 1, + "A default snippet is present."); + } +}, + +{ + desc: "Check default snippets are shown if snippets are invalid xml", + setup: function (aSnippetsMap) + { + // This must be some incorrect xhtml code. + aSnippetsMap.set("snippets", "<p><b></p></b>"); + }, + run: function (aSnippetsMap) + { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + + let snippetsElt = doc.getElementById("snippets"); + ok(snippetsElt, "Found snippets element"); + is(snippetsElt.getElementsByTagName("span").length, 1, + "A default snippet is present."); + + aSnippetsMap.delete("snippets"); + } +}, + +{ + desc: "Check that search engine logo has alt text", + setup: function () { }, + run: function () + { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + + let searchEngineLogoElt = doc.getElementById("searchEngineLogo"); + ok(searchEngineLogoElt, "Found search engine logo"); + + let altText = searchEngineLogoElt.alt; + ok(typeof altText == "string" && altText.length > 0, + "Search engine logo's alt text is a nonempty string"); + + isnot(altText, "undefined", + "Search engine logo's alt text shouldn't be the string 'undefined'"); + } +}, + +// Disabled on Linux for intermittent issues with FHR, see Bug 945667. +// Disabled always due to bug 992485 +{ + desc: "Check that performing a search fires a search event and records to " + + "Firefox Health Report.", + setup: function () { }, + run: function () { + // Skip this test always for now since it loads google.com and that causes bug 992485 + return; + + // Skip this test on Linux. + if (navigator.platform.indexOf("Linux") == 0) { return; } + + try { + let cm = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); + cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider"); + } catch (ex) { + // Health Report disabled, or no SearchesProvider. + return Promise.resolve(); + } + + let numSearchesBefore = 0; + let deferred = Promise.defer(); + let doc = gBrowser.contentDocument; + let engineName = doc.documentElement.getAttribute("searchEngineName"); + + // We rely on the listener in browser.js being installed and fired before + // this one. If this ever changes, we should add an executeSoon() or similar. + doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) { + is(e.detail, engineName, "Detail is search engine name"); + + getNumberOfSearches(engineName).then(num => { + is(num, numSearchesBefore + 1, "One more search recorded."); + deferred.resolve(); + }); + }, true, true); + + // Get the current number of recorded searches. + getNumberOfSearches(engineName).then(num => { + numSearchesBefore = num; + + info("Perform a search."); + doc.getElementById("searchText").value = "a search"; + doc.getElementById("searchSubmit").click(); + gBrowser.stop(); + }); + + return deferred.promise; + } +}, + +{ + desc: "Check snippets map is cleared if cached version is old", + setup: function (aSnippetsMap) + { + aSnippetsMap.set("snippets", "test"); + aSnippetsMap.set("snippets-cached-version", 0); + }, + run: function (aSnippetsMap) + { + ok(!aSnippetsMap.has("snippets"), "snippets have been properly cleared"); + ok(!aSnippetsMap.has("snippets-cached-version"), + "cached-version has been properly cleared"); + } +}, + +{ + desc: "Check cached snippets are shown if cached version is current", + setup: function (aSnippetsMap) + { + aSnippetsMap.set("snippets", "test"); + }, + run: function (aSnippetsMap) + { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + + let snippetsElt = doc.getElementById("snippets"); + ok(snippetsElt, "Found snippets element"); + is(snippetsElt.innerHTML, "test", "Cached snippet is present."); + + is(aSnippetsMap.get("snippets"), "test", "snippets still cached"); + is(aSnippetsMap.get("snippets-cached-version"), + AboutHomeUtils.snippetsVersion, + "cached-version is correct"); + ok(aSnippetsMap.has("snippets-last-update"), "last-update still exists"); + } +}, + +{ + desc: "Check if the 'Know Your Rights default snippet is shown when 'browser.rights.override' pref is set", + beforeRun: function () + { + Services.prefs.setBoolPref("browser.rights.override", false); + }, + setup: function () { }, + run: function (aSnippetsMap) + { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let showRights = AboutHomeUtils.showKnowYourRights; + + ok(showRights, "AboutHomeUtils.showKnowYourRights should be TRUE"); + + let snippetsElt = doc.getElementById("snippets"); + ok(snippetsElt, "Found snippets element"); + is(snippetsElt.getElementsByTagName("a")[0].href, "about:rights", "Snippet link is present."); + + Services.prefs.clearUserPref("browser.rights.override"); + } +}, + +{ + desc: "Check if the 'Know Your Rights default snippet is NOT shown when 'browser.rights.override' pref is NOT set", + beforeRun: function () + { + Services.prefs.setBoolPref("browser.rights.override", true); + }, + setup: function () { }, + run: function (aSnippetsMap) + { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let rightsData = AboutHomeUtils.knowYourRightsData; + + ok(!rightsData, "AboutHomeUtils.knowYourRightsData should be FALSE"); + + let snippetsElt = doc.getElementById("snippets"); + ok(snippetsElt, "Found snippets element"); + ok(snippetsElt.getElementsByTagName("a")[0].href != "about:rights", "Snippet link should not point to about:rights."); + + Services.prefs.clearUserPref("browser.rights.override"); + } +}, + +{ + desc: "Check that the search UI/ action is updated when the search engine is changed", + setup: function() {}, + run: function() + { + let currEngine = Services.search.currentEngine; + let unusedEngines = [].concat(Services.search.getVisibleEngines()).filter(x => x != currEngine); + let searchbar = document.getElementById("searchbar"); + + function checkSearchUI(engine) { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let searchText = doc.getElementById("searchText"); + let logoElt = doc.getElementById("searchEngineLogo"); + let engineName = doc.documentElement.getAttribute("searchEngineName"); + + is(engineName, engine.name, "Engine name should've been updated"); + + if (!logoElt.parentNode.hidden) { + is(logoElt.alt, engineName, "Alt text of logo image should match search engine name") + } else { + is(searchText.placeholder, engineName, "Placeholder text should match search engine name"); + } + } + // Do a sanity check that all attributes are correctly set to begin with + checkSearchUI(currEngine); + + let deferred = Promise.defer(); + promiseBrowserAttributes(gBrowser.selectedTab).then(function() { + // Test if the update propagated + checkSearchUI(unusedEngines[0]); + searchbar.currentEngine = currEngine; + deferred.resolve(); + }); + + // The following cleanup function will set currentEngine back to the previous + // engine if we fail to do so above. + registerCleanupFunction(function() { + searchbar.currentEngine = currEngine; + }); + // Set the current search engine to an unused one + searchbar.currentEngine = unusedEngines[0]; + searchbar.select(); + return deferred.promise; + } +}, + +{ + desc: "Check POST search engine support", + setup: function() {}, + run: function() + { + let deferred = Promise.defer(); + let currEngine = Services.search.defaultEngine; + let searchObserver = function search_observer(aSubject, aTopic, aData) { + let engine = aSubject.QueryInterface(Ci.nsISearchEngine); + info("Observer: " + aData + " for " + engine.name); + + if (aData != "engine-added") + return; + + if (engine.name != "POST Search") + return; + + Services.search.defaultEngine = engine; + + registerCleanupFunction(function() { + Services.search.removeEngine(engine); + Services.search.defaultEngine = currEngine; + }); + + let needle = "Search for something awesome."; + + // Ready to execute the tests! + promiseBrowserAttributes(gBrowser.selectedTab).then(function() { + let document = gBrowser.selectedTab.linkedBrowser.contentDocument; + let searchText = document.getElementById("searchText"); + + waitForLoad(function() { + let loadedText = gBrowser.contentDocument.body.textContent; + ok(loadedText, "search page loaded"); + is(loadedText, "searchterms=" + escape(needle.replace(/\s/g, "+")), + "Search text should arrive correctly"); + deferred.resolve(); + }); + + searchText.value = needle; + searchText.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}); + }); + }; + Services.obs.addObserver(searchObserver, "browser-search-engine-modified", false); + registerCleanupFunction(function () { + Services.obs.removeObserver(searchObserver, "browser-search-engine-modified"); + }); + Services.search.addEngine("http://test:80/browser/browser/base/content/test/POSTSearchEngine.xml", + Ci.nsISearchEngine.DATA_XML, null, false); + return deferred.promise; + } +} + +]; + +function test() +{ + waitForExplicitFinish(); + requestLongerTimeout(2); + ignoreAllUncaughtExceptions(); + + Task.spawn(function () { + for (let test of gTests) { + info(test.desc); + + if (test.beforeRun) + yield test.beforeRun(); + + let tab = yield promiseNewTabLoadEvent("about:home", "DOMContentLoaded"); + + // Must wait for both the snippets map and the browser attributes, since + // can't guess the order they will happen. + // So, start listening now, but verify the promise is fulfilled only + // after the snippets map setup. + let promise = promiseBrowserAttributes(tab); + // Prepare the snippets map with default values, then run the test setup. + let snippetsMap = yield promiseSetupSnippetsMap(tab, test.setup); + // Ensure browser has set attributes already, or wait for them. + yield promise; + info("Running test"); + yield test.run(snippetsMap); + info("Cleanup"); + gBrowser.removeCurrentTab(); + } + }).then(finish, ex => { + ok(false, "Unexpected Exception: " + ex); + finish(); + }); +} + +/** + * Creates a new tab and waits for a load event. + * + * @param aUrl + * The url to load in a new tab. + * @param aEvent + * The load event type to wait for. Defaults to "load". + * @return {Promise} resolved when the event is handled. Gets the new tab. + */ +function promiseNewTabLoadEvent(aUrl, aEventType="load") +{ + let deferred = Promise.defer(); + let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl); + info("Wait tab event: " + aEventType); + tab.linkedBrowser.addEventListener(aEventType, function load(event) { + if (event.originalTarget != tab.linkedBrowser.contentDocument || + event.target.location.href == "about:blank") { + info("skipping spurious load event"); + return; + } + tab.linkedBrowser.removeEventListener(aEventType, load, true); + info("Tab event received: " + aEventType); + deferred.resolve(tab); + }, true); + return deferred.promise; +} + +/** + * Cleans up snippets and ensures that by default we don't try to check for + * remote snippets since that may cause network bustage or slowness. + * + * @param aTab + * The tab containing about:home. + * @param aSetupFn + * The setup function to be run. + * @return {Promise} resolved when the snippets are ready. Gets the snippets map. + */ +function promiseSetupSnippetsMap(aTab, aSetupFn) +{ + let deferred = Promise.defer(); + let cw = aTab.linkedBrowser.contentWindow.wrappedJSObject; + info("Waiting for snippets map"); + cw.ensureSnippetsMapThen(function (aSnippetsMap) { + info("Got snippets map: " + + "{ last-update: " + aSnippetsMap.get("snippets-last-update") + + ", cached-version: " + aSnippetsMap.get("snippets-cached-version") + + " }"); + // Don't try to update. + aSnippetsMap.set("snippets-last-update", Date.now()); + aSnippetsMap.set("snippets-cached-version", AboutHomeUtils.snippetsVersion); + // Clear snippets. + aSnippetsMap.delete("snippets"); + aSetupFn(aSnippetsMap); + // Must be sure to continue after the page snippets map setup. + executeSoon(function() deferred.resolve(aSnippetsMap)); + }); + return deferred.promise; +} + +/** + * Waits for the attributes being set by browser.js and overwrites snippetsURL + * to ensure we won't try to hit the network and we can force xhr to throw. + * + * @param aTab + * The tab containing about:home. + * @return {Promise} resolved when the attributes are ready. + */ +function promiseBrowserAttributes(aTab) +{ + let deferred = Promise.defer(); + + let docElt = aTab.linkedBrowser.contentDocument.documentElement; + //docElt.setAttribute("snippetsURL", "nonexistent://test"); + let observer = new MutationObserver(function (mutations) { + for (let mutation of mutations) { + info("Got attribute mutation: " + mutation.attributeName + + " from " + mutation.oldValue); + if (mutation.attributeName == "snippetsURL" && + docElt.getAttribute("snippetsURL") != "nonexistent://test") { + docElt.setAttribute("snippetsURL", "nonexistent://test"); + } + + // Now we just have to wait for the last attribute. + if (mutation.attributeName == "searchEngineURL") { + info("Remove attributes observer"); + observer.disconnect(); + // Must be sure to continue after the page mutation observer. + executeSoon(function() deferred.resolve()); + break; + } + } + }); + info("Add attributes observer"); + observer.observe(docElt, { attributes: true }); + + return deferred.promise; +} + +/** + * Retrieves the number of about:home searches recorded for the current day. + * + * @param aEngineName + * name of the setup search engine. + * + * @return {Promise} Returns a promise resolving to the number of searches. + */ +function getNumberOfSearches(aEngineName) { + let reporter = Components.classes["@mozilla.org/datareporting/service;1"] + .getService() + .wrappedJSObject + .healthReporter; + ok(reporter, "Health Reporter instance available."); + + return reporter.onInit().then(function onInit() { + let provider = reporter.getProvider("org.mozilla.searches"); + ok(provider, "Searches provider is available."); + + let m = provider.getMeasurement("counts", 2); + return m.getValues().then(data => { + let now = new Date(); + let yday = new Date(now); + yday.setDate(yday.getDate() - 1); + + // Add the number of searches recorded yesterday to the number of searches + // recorded today. This makes the test not fail intermittently when it is + // run at midnight and we accidentally compare the number of searches from + // different days. Tests are always run with an empty profile so there + // are no searches from yesterday, normally. Should the test happen to run + // past midnight we make sure to count them in as well. + return getNumberOfSearchesByDate(aEngineName, data, now) + + getNumberOfSearchesByDate(aEngineName, data, yday); + }); + }); +} + +function getNumberOfSearchesByDate(aEngineName, aData, aDate) { + if (aData.days.hasDay(aDate)) { + let id = Services.search.getEngineByName(aEngineName).identifier; + + let day = aData.days.getDay(aDate); + let field = id + ".abouthome"; + + if (day.has(field)) { + return day.get(field) || 0; + } + } + + return 0; // No records found. +} + +function waitForLoad(cb) { + let browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function listener() { + if (browser.currentURI.spec == "about:blank") + return; + info("Page loaded: " + browser.currentURI.spec); + browser.removeEventListener("load", listener, true); + + cb(); + }, true); +} diff --git a/browser/base/content/test/browser_aboutSyncProgress.js b/browser/base/content/test/browser_aboutSyncProgress.js new file mode 100644 index 000000000..49a2fd803 --- /dev/null +++ b/browser/base/content/test/browser_aboutSyncProgress.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; +Cu.import("resource://services-sync/main.js"); + +let gTests = [ { + desc: "Makes sure the progress bar appears if firstSync pref is set", + setup: function () { + Services.prefs.setCharPref("services.sync.firstSync", "newAccount"); + }, + run: function () { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let progressBar = doc.getElementById("uploadProgressBar"); + + let win = doc.defaultView; + isnot(win.getComputedStyle(progressBar).display, "none", "progress bar should be visible"); + executeSoon(runNextTest); + } +}, + +{ + desc: "Makes sure the progress bar is hidden if firstSync pref is not set", + setup: function () { + Services.prefs.clearUserPref("services.sync.firstSync"); + is(Services.prefs.getPrefType("services.sync.firstSync"), + Ci.nsIPrefBranch.PREF_INVALID, "pref DNE" ); + }, + run: function () { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let progressBar = doc.getElementById("uploadProgressBar"); + + let win = doc.defaultView; + is(win.getComputedStyle(progressBar).display, "none", + "progress bar should not be visible"); + executeSoon(runNextTest); + } +}, +{ + desc: "Makes sure the observer updates are reflected in the progress bar", + setup: function () { + }, + run: function () { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let progressBar = doc.getElementById("uploadProgressBar"); + + Services.obs.notifyObservers(null, "weave:engine:sync:finish", null); + Services.obs.notifyObservers(null, "weave:engine:sync:error", null); + + let received = progressBar.getAttribute("value"); + + is(received, 2, "progress bar received correct notifications"); + executeSoon(runNextTest); + } +}, +{ + desc: "Close button should close tab", + setup: function (){ + }, + run: function () { + function onTabClosed() { + ok(true, "received TabClose notification"); + gBrowser.tabContainer.removeEventListener("TabClose", onTabClosed, false); + executeSoon(runNextTest); + } + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let button = doc.getElementById('closeButton'); + let window = doc.defaultView; + gBrowser.tabContainer.addEventListener("TabClose", onTabClosed, false); + EventUtils.sendMouseEvent({type: "click"}, button, window); + } +}, +]; + +function test () { + waitForExplicitFinish(); + executeSoon(runNextTest); +} + +function runNextTest() +{ + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } + + if (gTests.length) { + let test = gTests.shift(); + info(test.desc); + test.setup(); + let tab = gBrowser.selectedTab = gBrowser.addTab("about:sync-progress"); + tab.linkedBrowser.addEventListener("load", function (event) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + // Some part of the page is populated on load, so enqueue on it. + executeSoon(test.run); + }, true); + } + else { + finish(); + } +} + diff --git a/browser/base/content/test/browser_addon_bar.js b/browser/base/content/test/browser_addon_bar.js new file mode 100644 index 000000000..3607eb122 --- /dev/null +++ b/browser/base/content/test/browser_addon_bar.js @@ -0,0 +1,63 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let addonbar = document.getElementById("addon-bar"); + ok(addonbar.collapsed, "addon bar is collapsed by default"); + + let topMenu, toolbarMenu; + + function onTopMenuShown(event) { + ok(1, "top menu popupshown listener called"); + event.currentTarget.removeEventListener("popupshown", arguments.callee, false); + // open the customize or toolbars menu + toolbarMenu = document.getElementById("appmenu_customizeMenu") || + document.getElementById("viewToolbarsMenu").firstElementChild; + toolbarMenu.addEventListener("popupshown", onToolbarMenuShown, false); + toolbarMenu.addEventListener("popuphidden", onToolbarMenuHidden, false); + toolbarMenu.openPopup(); + } + + function onTopMenuHidden(event) { + ok(1, "top menu popuphidden listener called"); + event.currentTarget.removeEventListener("popuphidden", arguments.callee, false); + finish(); + } + + function onToolbarMenuShown(event) { + ok(1, "sub menu popupshown listener called"); + event.currentTarget.removeEventListener("popupshown", arguments.callee, false); + + // test the menu item's default state + let menuitem = document.getElementById("toggle_addon-bar"); + ok(menuitem, "found the menu item"); + is(menuitem.getAttribute("checked"), "false", "menuitem is not checked by default"); + + // click on the menu item + // TODO: there's got to be a way to check+command in one shot + menuitem.setAttribute("checked", "true"); + menuitem.click(); + + // now the addon bar should be visible and the menu checked + is(addonbar.getAttribute("collapsed"), "false", "addon bar is visible after executing the command"); + is(menuitem.getAttribute("checked"), "true", "menuitem is checked after executing the command"); + + toolbarMenu.hidePopup(); + } + + function onToolbarMenuHidden(event) { + ok(1, "toolbar menu popuphidden listener called"); + event.currentTarget.removeEventListener("popuphidden", arguments.callee, false); + topMenu.hidePopup(); + } + + // open the appmenu or view menu + topMenu = document.getElementById("appmenu-popup") || + document.getElementById("menu_viewPopup"); + topMenu.addEventListener("popupshown", onTopMenuShown, false); + topMenu.addEventListener("popuphidden", onTopMenuHidden, false); + topMenu.openPopup(); +} diff --git a/browser/base/content/test/browser_addon_bar_aomlistener.js b/browser/base/content/test/browser_addon_bar_aomlistener.js new file mode 100644 index 000000000..75a539e07 --- /dev/null +++ b/browser/base/content/test/browser_addon_bar_aomlistener.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + + let addonbar = document.getElementById("addon-bar"); + ok(addonbar.collapsed, "addon bar is collapsed by default"); + + function addItem(id) { + let button = document.createElement("toolbarbutton"); + button.id = id; + let palette = document.getElementById("navigator-toolbox").palette; + palette.appendChild(button); + addonbar.insertItem(id, null, null, false); + } + + // call onInstalling + AddonsMgrListener.onInstalling(); + + // add item to the bar + let id = "testbutton"; + addItem(id); + + // call onInstalled + AddonsMgrListener.onInstalled(); + + // confirm bar is visible + ok(!addonbar.collapsed, "addon bar is not collapsed after toggle"); + + // call onUninstalling + AddonsMgrListener.onUninstalling(); + + // remove item from the bar + addonbar.currentSet = addonbar.currentSet.replace("," + id, ""); + + // call onUninstalled + AddonsMgrListener.onUninstalled(); + + // confirm bar is not visible + ok(addonbar.collapsed, "addon bar is collapsed after toggle"); + + // call onEnabling + AddonsMgrListener.onEnabling(); + + // add item to the bar + let id = "testbutton"; + addItem(id); + + // call onEnabled + AddonsMgrListener.onEnabled(); + + // confirm bar is visible + ok(!addonbar.collapsed, "addon bar is not collapsed after toggle"); + + // call onDisabling + AddonsMgrListener.onDisabling(); + + // remove item from the bar + addonbar.currentSet = addonbar.currentSet.replace("," + id, ""); + + // call onDisabled + AddonsMgrListener.onDisabled(); + + // confirm bar is not visible + ok(addonbar.collapsed, "addon bar is collapsed after toggle"); +} diff --git a/browser/base/content/test/browser_addon_bar_close_button.js b/browser/base/content/test/browser_addon_bar_close_button.js new file mode 100644 index 000000000..7d3afb333 --- /dev/null +++ b/browser/base/content/test/browser_addon_bar_close_button.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + let addonbar = document.getElementById("addon-bar"); + ok(addonbar.collapsed, "addon bar is collapsed by default"); + + // make add-on bar visible + setToolbarVisibility(addonbar, true); + ok(!addonbar.collapsed, "addon bar is not collapsed after toggle"); + + // click the close button + let closeButton = document.getElementById("addonbar-closebutton"); + EventUtils.synthesizeMouseAtCenter(closeButton, {}); + + // confirm addon bar is closed + ok(addonbar.collapsed, "addon bar is collapsed after clicking close button"); +} diff --git a/browser/base/content/test/browser_addon_bar_shortcut.js b/browser/base/content/test/browser_addon_bar_shortcut.js new file mode 100644 index 000000000..847f96721 --- /dev/null +++ b/browser/base/content/test/browser_addon_bar_shortcut.js @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + let addonbar = document.getElementById("addon-bar"); + ok(addonbar.collapsed, "addon bar is collapsed by default"); + + // show the add-on bar + EventUtils.synthesizeKey("/", { accelKey: true }, window); + ok(!addonbar.collapsed, "addon bar is not collapsed after toggle"); + + // hide the add-on bar + EventUtils.synthesizeKey("/", { accelKey: true }, window); + + // confirm addon bar is closed + ok(addonbar.collapsed, "addon bar is collapsed after toggle"); +} diff --git a/browser/base/content/test/browser_allTabsPanel.js b/browser/base/content/test/browser_allTabsPanel.js new file mode 100644 index 000000000..eb82c0d1a --- /dev/null +++ b/browser/base/content/test/browser_allTabsPanel.js @@ -0,0 +1,162 @@ +function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref(allTabs.prefName, true); + registerCleanupFunction(function () { + Services.prefs.clearUserPref(allTabs.prefName); + }); + + allTabs.init(); + nextSequence(); +} + +var sequences = 3; +var chars = "ABCDEFGHI"; +var closedTabs; +var history; +var steps; +var whenOpen = [ + startSearch, + clearSearch, clearSearch, + closeTab, + moveTab, + closePanel, +]; +var whenClosed = [ + openPanel, openPanel, openPanel, openPanel, openPanel, openPanel, + closeTab, closeTab, closeTab, + moveTab, moveTab, moveTab, + selectTab, selectTab, + undoCloseTab, + openTab, +]; + +function rand(min, max) { + return min + Math.floor(Math.random() * (max - min + 1)); +} +function pickOne(array) { + return array[rand(0, array.length - 1)]; +} +function pickOneTab() { + var tab = pickOne(gBrowser.tabs); + return [tab, Array.indexOf(gBrowser.tabs, tab)]; +} +function nextSequence() { + while (gBrowser.browsers.length > 1) + gBrowser.removeCurrentTab(); + if (sequences-- <= 0) { + allTabs.close(); + gBrowser.addTab(); + gBrowser.removeCurrentTab(); + finish(); + return; + } + closedTabs = 0; + steps = rand(10, 20); + var initialTabs = ""; + while (gBrowser.browsers.length < rand(3, 20)) { + let tabChar = pickOne(chars); + initialTabs += tabChar; + gBrowser.addTab("data:text/plain," + tabChar); + } + history = [initialTabs]; + gBrowser.removeCurrentTab(); + next(); +} +function next() { + executeSoon(function () { + is(allTabs.previews.length, gBrowser.browsers.length, + history.join(", ")); + if (steps-- <= 0) { + nextSequence(); + return; + } + var step; + var rv; + do { + step = pickOne(allTabs.isOpen ? whenOpen : whenClosed); + info(step.name); + rv = step(); + } while (rv === false); + history.push(step.name + (rv !== true && rv !== undefined ? " " + rv : "")); + }); +} + +function openPanel() { + if (allTabs.isOpen) + return false; + allTabs.panel.addEventListener("popupshown", function () { + allTabs.panel.removeEventListener("popupshown", arguments.callee, false); + next(); + }, false); + allTabs.open(); + return true; +} + +function closePanel() { + allTabs.panel.addEventListener("popuphidden", function () { + allTabs.panel.removeEventListener("popuphidden", arguments.callee, false); + next(); + }, false); + allTabs.close(); +} + +function closeTab() { + if (gBrowser.browsers.length == 1) + return false; + var [tab, index] = pickOneTab(); + gBrowser.removeTab(tab); + closedTabs++; + next(); + return index; +} + +function startSearch() { + allTabs.filterField.value = pickOne(chars); + info(allTabs.filterField.value); + allTabs.filter(); + next(); + return allTabs.filterField.value; +} + +function clearSearch() { + if (!allTabs.filterField.value) + return false; + allTabs.filterField.value = ""; + allTabs.filter(); + next(); + return true; +} + +function undoCloseTab() { + if (!closedTabs) + return false; + window.undoCloseTab(0); + closedTabs--; + next(); + return true; +} + +function selectTab() { + var [tab, index] = pickOneTab(); + gBrowser.selectedTab = tab; + next(); + return index; +} + +function openTab() { + BrowserOpenTab(); + next(); +} + +function moveTab() { + if (gBrowser.browsers.length == 1) + return false; + var [tab, currentIndex] = pickOneTab(); + do { + var [, newIndex] = pickOneTab(); + } while (newIndex == currentIndex); + gBrowser.moveTabTo(tab, newIndex); + next(); + return currentIndex + "->" + newIndex; +} diff --git a/browser/base/content/test/browser_alltabslistener.js b/browser/base/content/test/browser_alltabslistener.js new file mode 100644 index 000000000..4d7d11435 --- /dev/null +++ b/browser/base/content/test/browser_alltabslistener.js @@ -0,0 +1,204 @@ +const Ci = Components.interfaces; + +const gCompleteState = Ci.nsIWebProgressListener.STATE_STOP + + Ci.nsIWebProgressListener.STATE_IS_NETWORK; + +var gFrontProgressListener = { + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + }, + + onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) { + var state = "onStateChange"; + info("FrontProgress: " + state + " 0x" + aStateFlags.toString(16)); + ok(gFrontNotificationsPos < gFrontNotifications.length, "Got an expected notification for the front notifications listener"); + is(state, gFrontNotifications[gFrontNotificationsPos], "Got a notification for the front notifications listener"); + gFrontNotificationsPos++; + }, + + onLocationChange: function (aWebProgress, aRequest, aLocationURI, aFlags) { + var state = "onLocationChange"; + info("FrontProgress: " + state + " " + aLocationURI.spec); + ok(gFrontNotificationsPos < gFrontNotifications.length, "Got an expected notification for the front notifications listener"); + is(state, gFrontNotifications[gFrontNotificationsPos], "Got a notification for the front notifications listener"); + gFrontNotificationsPos++; + }, + + onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) { + }, + + onSecurityChange: function (aWebProgress, aRequest, aState) { + var state = "onSecurityChange"; + info("FrontProgress: " + state + " 0x" + aState.toString(16)); + ok(gFrontNotificationsPos < gFrontNotifications.length, "Got an expected notification for the front notifications listener"); + is(state, gFrontNotifications[gFrontNotificationsPos], "Got a notification for the front notifications listener"); + gFrontNotificationsPos++; + } +} + +var gAllProgressListener = { + onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + var state = "onStateChange"; + info("AllProgress: " + state + " 0x" + aStateFlags.toString(16)); + ok(aBrowser == gTestBrowser, state + " notification came from the correct browser"); + ok(gAllNotificationsPos < gAllNotifications.length, "Got an expected notification for the all notifications listener"); + is(state, gAllNotifications[gAllNotificationsPos], "Got a notification for the all notifications listener"); + gAllNotificationsPos++; + + if ((aStateFlags & gCompleteState) == gCompleteState) { + ok(gAllNotificationsPos == gAllNotifications.length, "Saw the expected number of notifications"); + ok(gFrontNotificationsPos == gFrontNotifications.length, "Saw the expected number of frontnotifications"); + executeSoon(gNextTest); + } + }, + + onLocationChange: function (aBrowser, aWebProgress, aRequest, aLocationURI, + aFlags) { + var state = "onLocationChange"; + info("AllProgress: " + state + " " + aLocationURI.spec); + ok(aBrowser == gTestBrowser, state + " notification came from the correct browser"); + ok(gAllNotificationsPos < gAllNotifications.length, "Got an expected notification for the all notifications listener"); + is(state, gAllNotifications[gAllNotificationsPos], "Got a notification for the all notifications listener"); + gAllNotificationsPos++; + }, + + onStatusChange: function (aBrowser, aWebProgress, aRequest, aStatus, aMessage) { + var state = "onStatusChange"; + ok(aBrowser == gTestBrowser, state + " notification came from the correct browser"); + }, + + onSecurityChange: function (aBrowser, aWebProgress, aRequest, aState) { + var state = "onSecurityChange"; + info("AllProgress: " + state + " 0x" + aState.toString(16)); + ok(aBrowser == gTestBrowser, state + " notification came from the correct browser"); + ok(gAllNotificationsPos < gAllNotifications.length, "Got an expected notification for the all notifications listener"); + is(state, gAllNotifications[gAllNotificationsPos], "Got a notification for the all notifications listener"); + gAllNotificationsPos++; + } +} + +var gFrontNotifications, gAllNotifications, gFrontNotificationsPos, gAllNotificationsPos; +var gBackgroundTab, gForegroundTab, gBackgroundBrowser, gForegroundBrowser, gTestBrowser; +var gTestPage = "/browser/browser/base/content/test/alltabslistener.html"; +var gNextTest; + +function test() { + waitForExplicitFinish(); + + gBackgroundTab = gBrowser.addTab("about:blank"); + gForegroundTab = gBrowser.addTab("about:blank"); + gBackgroundBrowser = gBrowser.getBrowserForTab(gBackgroundTab); + gForegroundBrowser = gBrowser.getBrowserForTab(gForegroundTab); + gBrowser.selectedTab = gForegroundTab; + + // We must wait until the about:blank page has completed loading before + // starting tests or we get notifications from that + gForegroundBrowser.addEventListener("load", startTests, true); +} + +function runTest(browser, url, next) { + gFrontNotificationsPos = 0; + gAllNotificationsPos = 0; + gNextTest = next; + gTestBrowser = browser; + browser.loadURI(url); +} + +function startTests() { + gForegroundBrowser.removeEventListener("load", startTests, true); + executeSoon(startTest1); +} + +function startTest1() { + info("\nTest 1"); + gBrowser.addProgressListener(gFrontProgressListener); + gBrowser.addTabsProgressListener(gAllProgressListener); + + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = gAllNotifications; + runTest(gForegroundBrowser, "http://example.org" + gTestPage, startTest2); +} + +function startTest2() { + info("\nTest 2"); + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = gAllNotifications; + runTest(gForegroundBrowser, "https://example.com" + gTestPage, startTest3); +} + +function startTest3() { + info("\nTest 3"); + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = []; + runTest(gBackgroundBrowser, "http://example.org" + gTestPage, startTest4); +} + +function startTest4() { + info("\nTest 4"); + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = []; + runTest(gBackgroundBrowser, "https://example.com" + gTestPage, startTest5); +} + +function startTest5() { + info("\nTest 5"); + // Switch the foreground browser + [gForegroundBrowser, gBackgroundBrowser] = [gBackgroundBrowser, gForegroundBrowser]; + [gForegroundTab, gBackgroundTab] = [gBackgroundTab, gForegroundTab]; + // Avoid the onLocationChange this will fire + gBrowser.removeProgressListener(gFrontProgressListener); + gBrowser.selectedTab = gForegroundTab; + gBrowser.addProgressListener(gFrontProgressListener); + + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = gAllNotifications; + runTest(gForegroundBrowser, "http://example.org" + gTestPage, startTest6); +} + +function startTest6() { + info("\nTest 6"); + gAllNotifications = [ + "onStateChange", + "onLocationChange", + "onSecurityChange", + "onStateChange" + ]; + gFrontNotifications = []; + runTest(gBackgroundBrowser, "http://example.org" + gTestPage, finishTest); +} + +function finishTest() { + gBrowser.removeProgressListener(gFrontProgressListener); + gBrowser.removeTabsProgressListener(gAllProgressListener); + gBrowser.removeTab(gBackgroundTab); + gBrowser.removeTab(gForegroundTab); + finish(); +} diff --git a/browser/base/content/test/browser_blob-channelname.js b/browser/base/content/test/browser_blob-channelname.js new file mode 100644 index 000000000..fbeaeeb21 --- /dev/null +++ b/browser/base/content/test/browser_blob-channelname.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +Cu.import("resource://gre/modules/NetUtil.jsm"); + +function test() { + var file = new File(new Blob(['test'], {type: 'text/plain'}), {name: 'test-name'}); + var url = URL.createObjectURL(file); + var channel = NetUtil.newChannel(url); + + is(channel.contentDispositionFilename, 'test-name', "filename matches"); +} diff --git a/browser/base/content/test/browser_bookmark_titles.js b/browser/base/content/test/browser_bookmark_titles.js new file mode 100644 index 000000000..5f7c11053 --- /dev/null +++ b/browser/base/content/test/browser_bookmark_titles.js @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file is tests for the default titles that new bookmarks get. + +let tests = [ + // Common page. + ['http://example.com/browser/browser/base/content/test/dummy_page.html', + 'Dummy test page'], + // Data URI. + ['data:text/html;charset=utf-8,<title>test%20data:%20url</title>', + 'test data: url'], + // about:neterror + ['data:application/vnd.mozilla.xul+xml,', + 'data:application/vnd.mozilla.xul+xml,'], + // about:certerror + ['https://untrusted.example.com/somepage.html', + 'https://untrusted.example.com/somepage.html'] +]; + +function generatorTest() { + gBrowser.selectedTab = gBrowser.addTab(); + let browser = gBrowser.selectedBrowser; + browser.stop(); // stop the about:blank load. + + browser.addEventListener("DOMContentLoaded", event => { + if (event.originalTarget != browser.contentDocument || + event.target.location.href == "about:blank") { + info("skipping spurious load event"); + return; + } + nextStep(); + }, true); + registerCleanupFunction(function () { + browser.removeEventListener("DOMContentLoaded", nextStep, true); + gBrowser.removeCurrentTab(); + }); + + // Test that a bookmark of each URI gets the corresponding default title. + for (let i = 0; i < tests.length; ++i) { + let [uri, title] = tests[i]; + content.location = uri; + yield; + checkBookmark(uri, title); + } + + // Network failure test: now that dummy_page.html is in history, bookmarking + // it should give the last known page title as the default bookmark title. + + // Simulate a network outage with offline mode. (Localhost is still + // accessible in offline mode, so disable the test proxy as well.) + BrowserOffline.toggleOfflineStatus(); + let proxy = Services.prefs.getIntPref('network.proxy.type'); + Services.prefs.setIntPref('network.proxy.type', 0); + registerCleanupFunction(function () { + BrowserOffline.toggleOfflineStatus(); + Services.prefs.setIntPref('network.proxy.type', proxy); + }); + + // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache. + Services.cache.evictEntries(Services.cache.STORE_ANYWHERE); + + let [uri, title] = tests[0]; + content.location = uri; + yield; + // The offline mode test is only good if the page failed to load. + is(content.document.documentURI.substring(0, 14), 'about:neterror', + "Offline mode successfully simulated network outage."); + checkBookmark(uri, title); +} + +// Bookmark the current page and confirm that the new bookmark has the expected +// title. (Then delete the bookmark.) +function checkBookmark(uri, expected_title) { + is(gBrowser.selectedBrowser.currentURI.spec, uri, + "Trying to bookmark the expected uri"); + PlacesCommandHook.bookmarkCurrentPage(false); + + let id = PlacesUtils.getMostRecentBookmarkForURI(PlacesUtils._uri(uri)); + ok(id > 0, "Found the expected bookmark"); + let title = PlacesUtils.bookmarks.getItemTitle(id); + is(title, expected_title, "Bookmark got a good default title."); + + PlacesUtils.bookmarks.removeItem(id); +} diff --git a/browser/base/content/test/browser_bug304198.js b/browser/base/content/test/browser_bug304198.js new file mode 100644 index 000000000..a40034148 --- /dev/null +++ b/browser/base/content/test/browser_bug304198.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let charsToDelete, deletedURLTab, fullURLTab, partialURLTab, testPartialURL, testURL; + + charsToDelete = 5; + deletedURLTab = gBrowser.addTab(); + fullURLTab = gBrowser.addTab(); + partialURLTab = gBrowser.addTab(); + testURL = "http://example.org/browser/browser/base/content/test/dummy_page.html"; + + function cleanUp() { + gBrowser.removeTab(fullURLTab); + gBrowser.removeTab(partialURLTab); + gBrowser.removeTab(deletedURLTab); + } + + function cycleTabs() { + gBrowser.selectedTab = fullURLTab; + is(gURLBar.value, testURL, 'gURLBar.value should be testURL after switching back to fullURLTab'); + + gBrowser.selectedTab = partialURLTab; + is(gURLBar.value, testPartialURL, 'gURLBar.value should be testPartialURL after switching back to partialURLTab'); + + gBrowser.selectedTab = deletedURLTab; + is(gURLBar.value, '', 'gURLBar.value should be "" after switching back to deletedURLTab'); + + gBrowser.selectedTab = fullURLTab; + is(gURLBar.value, testURL, 'gURLBar.value should be testURL after switching back to fullURLTab'); + } + + // function borrowed from browser_bug386835.js + function load(tab, url, cb) { + tab.linkedBrowser.addEventListener("load", function (event) { + event.currentTarget.removeEventListener("load", arguments.callee, true); + cb(); + }, true); + tab.linkedBrowser.loadURI(url); + } + + function urlbarBackspace(cb) { + gBrowser.selectedBrowser.focus(); + gURLBar.addEventListener("focus", function () { + gURLBar.removeEventListener("focus", arguments.callee, false); + gURLBar.addEventListener("input", function () { + gURLBar.removeEventListener("input", arguments.callee, false); + cb(); + }, false); + executeSoon(function () { + EventUtils.synthesizeKey("VK_BACK_SPACE", {}); + }); + }, false); + gURLBar.focus(); + } + + function prepareDeletedURLTab(cb) { + gBrowser.selectedTab = deletedURLTab; + is(gURLBar.value, testURL, 'gURLBar.value should be testURL after initial switch to deletedURLTab'); + + // simulate the user removing the whole url from the location bar + gPrefService.setBoolPref("browser.urlbar.clickSelectsAll", true); + + urlbarBackspace(function () { + is(gURLBar.value, "", 'gURLBar.value should be "" (just set)'); + if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll")) + gPrefService.clearUserPref("browser.urlbar.clickSelectsAll"); + cb(); + }); + } + + function prepareFullURLTab(cb) { + gBrowser.selectedTab = fullURLTab; + is(gURLBar.value, testURL, 'gURLBar.value should be testURL after initial switch to fullURLTab'); + cb(); + } + + function preparePartialURLTab(cb) { + gBrowser.selectedTab = partialURLTab; + is(gURLBar.value, testURL, 'gURLBar.value should be testURL after initial switch to partialURLTab'); + + // simulate the user removing part of the url from the location bar + gPrefService.setBoolPref("browser.urlbar.clickSelectsAll", false); + + var deleted = 0; + urlbarBackspace(function () { + deleted++; + if (deleted < charsToDelete) { + urlbarBackspace(arguments.callee); + } else { + is(gURLBar.value, testPartialURL, "gURLBar.value should be testPartialURL (just set)"); + if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll")) + gPrefService.clearUserPref("browser.urlbar.clickSelectsAll"); + cb(); + } + }); + } + + function runTests() { + testURL = gURLBar.trimValue(testURL); + testPartialURL = testURL.substr(0, (testURL.length - charsToDelete)); + + // prepare the three tabs required by this test + prepareFullURLTab(function () { + preparePartialURLTab(function () { + prepareDeletedURLTab(function () { + // now cycle the tabs and make sure everything looks good + cycleTabs(); + cleanUp(); + finish(); + }); + }); + }); + } + + load(deletedURLTab, testURL, function() { + load(fullURLTab, testURL, function() { + load(partialURLTab, testURL, runTests); + }); + }); +} + diff --git a/browser/base/content/test/browser_bug321000.js b/browser/base/content/test/browser_bug321000.js new file mode 100644 index 000000000..99c1d51a5 --- /dev/null +++ b/browser/base/content/test/browser_bug321000.js @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 kTestString = " hello hello \n world\nworld "; + +var gTests = [ + + { desc: "Urlbar strips newlines and surrounding whitespace", + element: gURLBar, + expected: kTestString.replace(/\s*\n\s*/g,'') + }, + + { desc: "Searchbar replaces newlines with spaces", + element: document.getElementById('searchbar'), + expected: kTestString.replace('\n',' ','g') + }, + +]; + +// Test for bug 23485 and bug 321000. +// Urlbar should strip newlines, +// search bar should replace newlines with spaces. +function test() { + waitForExplicitFinish(); + + let cbHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + + // Put a multi-line string in the clipboard. + // Setting the clipboard value is an async OS operation, so we need to poll + // the clipboard for valid data before going on. + waitForClipboard(kTestString, function() { cbHelper.copyString(kTestString, document); }, + next_test, finish); +} + +function next_test() { + if (gTests.length) + test_paste(gTests.shift()); + else + finish(); +} + +function test_paste(aCurrentTest) { + var element = aCurrentTest.element; + + // Register input listener. + var inputListener = { + test: aCurrentTest, + handleEvent: function(event) { + element.removeEventListener(event.type, this, false); + + is(element.value, this.test.expected, this.test.desc); + + // Clear the field and go to next test. + element.value = ""; + setTimeout(next_test, 0); + } + } + element.addEventListener("input", inputListener, false); + + // Focus the window. + window.focus(); + gBrowser.selectedBrowser.focus(); + + // Focus the element and wait for focus event. + info("About to focus " + element.id); + element.addEventListener("focus", function() { + element.removeEventListener("focus", arguments.callee, false); + executeSoon(function() { + // Pasting is async because the Accel+V codepath ends up going through + // nsDocumentViewer::FireClipboardEvent. + info("Pasting into " + element.id); + EventUtils.synthesizeKey("v", { accelKey: true }); + }); + }, false); + element.focus(); +} diff --git a/browser/base/content/test/browser_bug329212.js b/browser/base/content/test/browser_bug329212.js new file mode 100644 index 000000000..5dc22c4aa --- /dev/null +++ b/browser/base/content/test/browser_bug329212.js @@ -0,0 +1,43 @@ +function test () { + + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + let doc = gBrowser.contentDocument; + let tooltip = document.getElementById("aHTMLTooltip"); + + ok(tooltip.fillInPageTooltip(doc.getElementById("svg1")), "should get title"); + is(tooltip.getAttribute("label"), "This is a non-root SVG element title"); + + ok(tooltip.fillInPageTooltip(doc.getElementById("text1")), "should get title"); + is(tooltip.getAttribute("label"), "\n\n\n This is a title\n\n "); + + ok(!tooltip.fillInPageTooltip(doc.getElementById("text2")), "should not get title"); + + ok(!tooltip.fillInPageTooltip(doc.getElementById("text3")), "should not get title"); + + ok(tooltip.fillInPageTooltip(doc.getElementById("link1")), "should get title"); + is(tooltip.getAttribute("label"), "\n This is a title\n "); + ok(tooltip.fillInPageTooltip(doc.getElementById("text4")), "should get title"); + is(tooltip.getAttribute("label"), "\n This is a title\n "); + + ok(!tooltip.fillInPageTooltip(doc.getElementById("link2")), "should not get title"); + + ok(tooltip.fillInPageTooltip(doc.getElementById("link3")), "should get title"); + isnot(tooltip.getAttribute("label"), ""); + + ok(tooltip.fillInPageTooltip(doc.getElementById("link4")), "should get title"); + is(tooltip.getAttribute("label"), "This is an xlink:title attribute"); + + ok(!tooltip.fillInPageTooltip(doc.getElementById("text5")), "should not get title"); + + gBrowser.removeCurrentTab(); + finish(); + }, true); + + content.location = + "http://mochi.test:8888/browser/browser/base/content/test/title_test.svg"; +} + diff --git a/browser/base/content/test/browser_bug356571.js b/browser/base/content/test/browser_bug356571.js new file mode 100644 index 000000000..aeb07c785 --- /dev/null +++ b/browser/base/content/test/browser_bug356571.js @@ -0,0 +1,91 @@ +// Bug 356571 - loadOneOrMoreURIs gives up if one of the URLs has an unknown protocol + +const Cr = Components.results; +const Cm = Components.manager; + +// Set to true when docShell alerts for unknown protocol error +var didFail = false; + +// Override Alert to avoid blocking the test due to unknown protocol error +const kPromptServiceUUID = "{6cc9c9fe-bc0b-432b-a410-253ef8bcc699}"; +const kPromptServiceContractID = "@mozilla.org/embedcomp/prompt-service;1"; + +// Save original prompt service factory +const kPromptServiceFactory = Cm.getClassObject(Cc[kPromptServiceContractID], + Ci.nsIFactory); + +let fakePromptServiceFactory = { + createInstance: function(aOuter, aIid) { + if (aOuter != null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return promptService.QueryInterface(aIid); + } +}; + +let promptService = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]), + alert: function() { + didFail = true; + } +}; + +/* FIXME +Cm.QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory(Components.ID(kPromptServiceUUID), "Prompt Service", + kPromptServiceContractID, fakePromptServiceFactory); +*/ + +const kCompleteState = Ci.nsIWebProgressListener.STATE_STOP + + Ci.nsIWebProgressListener.STATE_IS_NETWORK; + +const kDummyPage = "http://example.org/browser/browser/base/content/test/dummy_page.html"; +const kURIs = [ + "bad://www.mozilla.org/", + kDummyPage, + kDummyPage, +]; + +var gProgressListener = { + _runCount: 0, + onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) { + if ((aStateFlags & kCompleteState) == kCompleteState) { + if (++this._runCount != kURIs.length) + return; + // Check we failed on unknown protocol (received an alert from docShell) + ok(didFail, "Correctly failed on unknown protocol"); + // Check we opened all tabs + ok(gBrowser.tabs.length == kURIs.length, "Correctly opened all expected tabs"); + finishTest(); + } + } +} + +function test() { + todo(false, "temp. disabled"); + return; /* FIXME */ + waitForExplicitFinish(); + // Wait for all tabs to finish loading + gBrowser.addTabsProgressListener(gProgressListener); + loadOneOrMoreURIs(kURIs.join("|")); +} + +function finishTest() { + // Unregister the factory so we do not leak + Cm.QueryInterface(Ci.nsIComponentRegistrar) + .unregisterFactory(Components.ID(kPromptServiceUUID), + fakePromptServiceFactory); + + // Restore the original factory + Cm.QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory(Components.ID(kPromptServiceUUID), "Prompt Service", + kPromptServiceContractID, kPromptServiceFactory); + + // Remove the listener + gBrowser.removeTabsProgressListener(gProgressListener); + + // Close opened tabs + for (var i = gBrowser.tabs.length-1; i > 0; i--) + gBrowser.removeTab(gBrowser.tabs[i]); + + finish(); +} diff --git a/browser/base/content/test/browser_bug380960.js b/browser/base/content/test/browser_bug380960.js new file mode 100644 index 000000000..e5be919b1 --- /dev/null +++ b/browser/base/content/test/browser_bug380960.js @@ -0,0 +1,91 @@ +function test() { + gBrowser.tabContainer.addEventListener("TabOpen", tabAdded, false); + + var tab = gBrowser.addTab("about:blank", { skipAnimation: true }); + gBrowser.removeTab(tab); + is(tab.parentNode, null, "tab removed immediately"); + + tab = gBrowser.addTab("about:blank", { skipAnimation: true }); + gBrowser.removeTab(tab, { animate: true }); + gBrowser.removeTab(tab); + is(tab.parentNode, null, "tab removed immediately when calling removeTab again after the animation was kicked off"); + + waitForExplicitFinish(); + + Services.prefs.setBoolPref("browser.tabs.animate", true); + +// preperForNextText(); + todo(false, "async tests disabled because of intermittent failures (bug 585361)"); + cleanup(); +} + +function tabAdded() { + info("tab added"); +} + +function cleanup() { + if (Services.prefs.prefHasUserValue("browser.tabs.animate")) + Services.prefs.clearUserPref("browser.tabs.animate"); + gBrowser.tabContainer.removeEventListener("TabOpen", tabAdded, false); + finish(); +} + +var asyncTests = [ + function (tab) { + info("closing tab with middle click"); + EventUtils.synthesizeMouse(tab, 2, 2, { button: 1 }); + }, + function (tab) { + info("closing tab with accel+w"); + gBrowser.selectedTab = tab; + gBrowser.selectedBrowser.focus(); + EventUtils.synthesizeKey("w", { accelKey: true }); + }, + function (tab) { + info("closing tab by clicking the tab close button"); + gBrowser.selectedTab = tab; + var button = document.getAnonymousElementByAttribute(tab, "anonid", "close-button"); + EventUtils.synthesizeMouse(button, 2, 2, {}); + } +]; + +function preperForNextText() { + info("tests left: " + asyncTests.length + "; starting next"); + var tab = gBrowser.addTab("about:blank", { skipAnimation: true }); + executeSoon(function () { + nextAsyncText(tab); + }); +} + +function nextAsyncText(tab) { + var gotCloseEvent = false; + + tab.addEventListener("TabClose", function () { + tab.removeEventListener("TabClose", arguments.callee, false); + info("got TabClose event"); + gotCloseEvent = true; + + const DEFAULT_ANIMATION_LENGTH = 250; + const MAX_WAIT_TIME = DEFAULT_ANIMATION_LENGTH * 7; + var polls = Math.ceil(MAX_WAIT_TIME / DEFAULT_ANIMATION_LENGTH); + var pollTabRemoved = setInterval(function () { + --polls; + if (tab.parentNode && polls > 0) + return; + clearInterval(pollTabRemoved); + + is(tab.parentNode, null, "tab removed after at most " + MAX_WAIT_TIME + " ms"); + + if (asyncTests.length) + preperForNextText(); + else + cleanup(); + }, DEFAULT_ANIMATION_LENGTH); + }, false); + + asyncTests.shift()(tab); + + ok(gotCloseEvent, "got the close event syncronously"); + + is(tab.parentNode, gBrowser.tabContainer, "tab still exists when it's about to be removed asynchronously"); +} diff --git a/browser/base/content/test/browser_bug386835.js b/browser/base/content/test/browser_bug386835.js new file mode 100644 index 000000000..0f0bae2aa --- /dev/null +++ b/browser/base/content/test/browser_bug386835.js @@ -0,0 +1,89 @@ +var gTestPage = "http://example.org/browser/browser/base/content/test/dummy_page.html"; +var gTestImage = "http://example.org/browser/browser/base/content/test/moz.png"; +var gTab1, gTab2, gTab3; +var gLevel; +const BACK = 0; +const FORWARD = 1; + +function test() { + waitForExplicitFinish(); + + Task.spawn(function () { + gTab1 = gBrowser.addTab(gTestPage); + gTab2 = gBrowser.addTab(); + gTab3 = gBrowser.addTab(); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + yield FullZoomHelper.load(gTab1, gTestPage); + yield FullZoomHelper.load(gTab2, gTestPage); + }).then(secondPageLoaded, FullZoomHelper.failAndContinue(finish)); +} + +function secondPageLoaded() { + Task.spawn(function () { + FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1"); + FullZoomHelper.zoomTest(gTab3, 1, "Initial zoom of tab 3 should be 1"); + + // Now have three tabs, two with the test page, one blank. Tab 1 is selected + // Zoom tab 1 + FullZoom.enlarge(); + gLevel = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1)); + + ok(gLevel > 1, "New zoom for tab 1 should be greater than 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2"); + FullZoomHelper.zoomTest(gTab3, 1, "Zooming tab 1 should not affect tab 3"); + + yield FullZoomHelper.load(gTab3, gTestPage); + }).then(thirdPageLoaded, FullZoomHelper.failAndContinue(finish)); +} + +function thirdPageLoaded() { + Task.spawn(function () { + FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed"); + FullZoomHelper.zoomTest(gTab2, 1, "Tab 2 should still not be affected"); + FullZoomHelper.zoomTest(gTab3, gLevel, "Tab 3 should have zoomed as it was loading in the background"); + + // Switching to tab 2 should update its zoom setting. + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoomHelper.zoomTest(gTab1, gLevel, "Tab 1 should still be zoomed"); + FullZoomHelper.zoomTest(gTab2, gLevel, "Tab 2 should be zoomed now"); + FullZoomHelper.zoomTest(gTab3, gLevel, "Tab 3 should still be zoomed"); + + yield FullZoomHelper.load(gTab1, gTestImage); + }).then(imageLoaded, FullZoomHelper.failAndContinue(finish)); +} + +function imageLoaded() { + Task.spawn(function () { + FullZoomHelper.zoomTest(gTab1, 1, "Zoom should be 1 when image was loaded in the background"); + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + FullZoomHelper.zoomTest(gTab1, 1, "Zoom should still be 1 when tab with image is selected"); + }).then(imageZoomSwitch, FullZoomHelper.failAndContinue(finish)); +} + +function imageZoomSwitch() { + Task.spawn(function () { + yield FullZoomHelper.navigate(BACK); + yield FullZoomHelper.navigate(FORWARD); + FullZoomHelper.zoomTest(gTab1, 1, "Tab 1 should not be zoomed when an image loads"); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoomHelper.zoomTest(gTab1, 1, "Tab 1 should still not be zoomed when deselected"); + }).then(finishTest, FullZoomHelper.failAndContinue(finish)); +} + +var finishTestStarted = false; +function finishTest() { + Task.spawn(function () { + ok(!finishTestStarted, "finishTest called more than once"); + finishTestStarted = true; + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + FullZoom.reset(); + yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab1); + FullZoom.reset(); + yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab2); + FullZoom.reset(); + yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab3); + }).then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/browser/base/content/test/browser_bug405137.js b/browser/base/content/test/browser_bug405137.js new file mode 100644 index 000000000..a28d1c030 --- /dev/null +++ b/browser/base/content/test/browser_bug405137.js @@ -0,0 +1,5 @@ +function test(){ + var tab = gBrowser.addTab(); + ok(tab.getAttribute("closetabtext") != "", "tab has non-empty closetabtext"); + gBrowser.removeTab(tab); +} diff --git a/browser/base/content/test/browser_bug406216.js b/browser/base/content/test/browser_bug406216.js new file mode 100644 index 000000000..db3b1bffa --- /dev/null +++ b/browser/base/content/test/browser_bug406216.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * "TabClose" event is possibly used for closing related tabs of the current. + * "removeTab" method should work correctly even if the number of tabs are + * changed while "TabClose" event. + */ + +var count = 0; +const URIS = ["about:config", + "about:plugins", + "about:buildconfig", + "data:text/html,<title>OK</title>"]; + +function test() { + waitForExplicitFinish(); + URIS.forEach(addTab); +} + +function addTab(aURI, aIndex) { + var tab = gBrowser.addTab(aURI); + if (aIndex == 0) + gBrowser.removeTab(gBrowser.tabs[0]); + + tab.linkedBrowser.addEventListener("load", function (event) { + event.currentTarget.removeEventListener("load", arguments.callee, true); + if (++count == URIS.length) + executeSoon(doTabsTest); + }, true); +} + +function doTabsTest() { + is(gBrowser.tabs.length, URIS.length, "Correctly opened all expected tabs"); + + // sample of "close related tabs" feature + gBrowser.tabContainer.addEventListener("TabClose", function (event) { + event.currentTarget.removeEventListener("TabClose", arguments.callee, true); + var closedTab = event.originalTarget; + var scheme = closedTab.linkedBrowser.currentURI.scheme; + Array.slice(gBrowser.tabs).forEach(function (aTab) { + if (aTab != closedTab && aTab.linkedBrowser.currentURI.scheme == scheme) + gBrowser.removeTab(aTab); + }); + }, true); + + gBrowser.removeTab(gBrowser.tabs[0]); + is(gBrowser.tabs.length, 1, "Related tabs are not closed unexpectedly"); + + gBrowser.addTab("about:blank"); + gBrowser.removeTab(gBrowser.tabs[0]); + finish(); +} diff --git a/browser/base/content/test/browser_bug409481.js b/browser/base/content/test/browser_bug409481.js new file mode 100644 index 000000000..ade9d2099 --- /dev/null +++ b/browser/base/content/test/browser_bug409481.js @@ -0,0 +1,83 @@ +function test() { + waitForExplicitFinish(); + + // XXX This looks a bit odd, but is needed to avoid throwing when removing the + // event listeners below. See bug 310955. + document.getElementById("sidebar").addEventListener("load", delayedOpenUrl, true); + toggleSidebar("viewWebPanelsSidebar", true); +} + +function delayedOpenUrl() { + ok(true, "Ran delayedOpenUrl"); + setTimeout(openPanelUrl, 100); +} + +function openPanelUrl(event) { + ok(!document.getElementById("sidebar-box").hidden, "Sidebar showing"); + + var sidebar = document.getElementById("sidebar"); + var root = sidebar.contentDocument.documentElement; + ok(root.nodeName != "parsererror", "Sidebar is well formed"); + + sidebar.removeEventListener("load", delayedOpenUrl, true); + // XXX See comment above + sidebar.contentDocument.addEventListener("load", delayedRunTest, true); + var url = 'data:text/html,<div%20id="test_bug409481">Content!</div><a id="link" href="http://www.example.com/ctest">Link</a><input id="textbox">'; + sidebar.contentWindow.loadWebPanel(url); +} + +function delayedRunTest() { + ok(true, "Ran delayedRunTest"); + setTimeout(runTest, 100); +} + +function runTest(event) { + var sidebar = document.getElementById("sidebar"); + sidebar.contentDocument.removeEventListener("load", delayedRunTest, true); + + var browser = sidebar.contentDocument.getElementById("web-panels-browser"); + var div = browser && browser.contentDocument.getElementById("test_bug409481"); + ok(div && div.textContent == "Content!", "Sidebar content loaded"); + + var link = browser && browser.contentDocument.getElementById("link"); + sidebar.contentDocument.addEventListener("popupshown", contextMenuOpened, false); + + EventUtils.synthesizeMouseAtCenter(link, { type: "contextmenu", button: 2 }, browser.contentWindow); +} + +function contextMenuOpened() +{ + var sidebar = document.getElementById("sidebar"); + sidebar.contentDocument.removeEventListener("popupshown", contextMenuOpened, false); + + var copyLinkCommand = sidebar.contentDocument.getElementById("context-copylink"); + copyLinkCommand.addEventListener("command", copyLinkCommandExecuted, false); + copyLinkCommand.doCommand(); +} + +function copyLinkCommandExecuted(event) +{ + event.target.removeEventListener("command", copyLinkCommandExecuted, false); + + var sidebar = document.getElementById("sidebar"); + var browser = sidebar.contentDocument.getElementById("web-panels-browser"); + var textbox = browser && browser.contentDocument.getElementById("textbox"); + textbox.focus(); + document.commandDispatcher.getControllerForCommand("cmd_paste").doCommand("cmd_paste"); + is(textbox.value, "http://www.example.com/ctest", "copy link command"); + + sidebar.contentDocument.addEventListener("popuphidden", contextMenuClosed, false); + event.target.parentNode.hidePopup(); +} + +function contextMenuClosed() +{ + var sidebar = document.getElementById("sidebar"); + sidebar.contentDocument.removeEventListener("popuphidden", contextMenuClosed, false); + + toggleSidebar("viewWebPanelsSidebar"); + + ok(document.getElementById("sidebar-box").hidden, "Sidebar successfully hidden"); + + finish(); +} diff --git a/browser/base/content/test/browser_bug409624.js b/browser/base/content/test/browser_bug409624.js new file mode 100644 index 000000000..2ea177794 --- /dev/null +++ b/browser/base/content/test/browser_bug409624.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); + +function test() { + waitForExplicitFinish(); + + // This test relies on the form history being empty to start with delete + // all the items first. + FormHistory.update({ op: "remove" }, + { handleError: function (error) { + do_throw("Error occurred updating form history: " + error); + }, + handleCompletion: function (reason) { if (!reason) test2(); }, + }); +} + +function test2() +{ + let prefService = Cc["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch2); + + let findBar = gFindBar; + let textbox = gFindBar.getElement("findbar-textbox"); + + let tempScope = {}; + Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tempScope); + let Sanitizer = tempScope.Sanitizer; + let s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + let prefBranch = prefService.getBranch(s.prefDomain); + + prefBranch.setBoolPref("cache", false); + prefBranch.setBoolPref("cookies", false); + prefBranch.setBoolPref("downloads", false); + prefBranch.setBoolPref("formdata", true); + prefBranch.setBoolPref("history", false); + prefBranch.setBoolPref("offlineApps", false); + prefBranch.setBoolPref("passwords", false); + prefBranch.setBoolPref("sessions", false); + prefBranch.setBoolPref("siteSettings", false); + + // Sanitize now so we can test that canClear is correct. Formdata is cleared asynchronously. + s.sanitize().then(function() { + s.canClearItem("formdata", clearDone1, s); + }); +} + +function clearDone1(aItemName, aResult, aSanitizer) +{ + ok(!aResult, "pre-test baseline for sanitizer"); + gFindBar.getElement("findbar-textbox").value = "m"; + aSanitizer.canClearItem("formdata", inputEntered, aSanitizer); +} + +function inputEntered(aItemName, aResult, aSanitizer) +{ + ok(aResult, "formdata can be cleared after input"); + aSanitizer.sanitize().then(function() { + aSanitizer.canClearItem("formdata", clearDone2); + }); +} + +function clearDone2(aItemName, aResult) +{ + is(gFindBar.getElement("findbar-textbox").value, "", "findBar textbox should be empty after sanitize"); + ok(!aResult, "canClear now false after sanitize"); + finish(); +} diff --git a/browser/base/content/test/browser_bug413915.js b/browser/base/content/test/browser_bug413915.js new file mode 100644 index 000000000..1f22043b9 --- /dev/null +++ b/browser/base/content/test/browser_bug413915.js @@ -0,0 +1,59 @@ +function test() { + var exampleUri = makeURI("http://example.com/"); + var secman = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager); + var principal = secman.getSimpleCodebasePrincipal(exampleUri); + + function testIsFeed(aTitle, aHref, aType, aKnown) { + var link = { title: aTitle, href: aHref, type: aType }; + return isValidFeed(link, principal, aKnown); + } + + var href = "http://example.com/feed/"; + var atomType = "application/atom+xml"; + var funkyAtomType = " aPPLICAtion/Atom+XML "; + var rssType = "application/rss+xml"; + var funkyRssType = " Application/RSS+XML "; + var rdfType = "application/rdf+xml"; + var texmlType = "text/xml"; + var appxmlType = "application/xml"; + var noRss = "Foo"; + var rss = "RSS"; + + // things that should be valid + ok(testIsFeed(noRss, href, atomType, false) == atomType, + "detect Atom feed"); + ok(testIsFeed(noRss, href, funkyAtomType, false) == atomType, + "clean up and detect Atom feed"); + ok(testIsFeed(noRss, href, rssType, false) == rssType, + "detect RSS feed"); + ok(testIsFeed(noRss, href, funkyRssType, false) == rssType, + "clean up and detect RSS feed"); + + // things that should not be feeds + ok(testIsFeed(noRss, href, rdfType, false) == null, + "should not detect RDF non-feed"); + ok(testIsFeed(rss, href, rdfType, false) == null, + "should not detect RDF feed from type and title"); + ok(testIsFeed(noRss, href, texmlType, false) == null, + "should not detect text/xml non-feed"); + ok(testIsFeed(rss, href, texmlType, false) == null, + "should not detect text/xml feed from type and title"); + ok(testIsFeed(noRss, href, appxmlType, false) == null, + "should not detect application/xml non-feed"); + ok(testIsFeed(rss, href, appxmlType, false) == null, + "should not detect application/xml feed from type and title"); + + // security check only, returns cleaned up type or "application/rss+xml" + ok(testIsFeed(noRss, href, atomType, true) == atomType, + "feed security check should return Atom type"); + ok(testIsFeed(noRss, href, funkyAtomType, true) == atomType, + "feed security check should return cleaned up Atom type"); + ok(testIsFeed(noRss, href, rssType, true) == rssType, + "feed security check should return RSS type"); + ok(testIsFeed(noRss, href, funkyRssType, true) == rssType, + "feed security check should return cleaned up RSS type"); + ok(testIsFeed(noRss, href, "", true) == rssType, + "feed security check without type should return RSS type"); + ok(testIsFeed(noRss, href, "garbage", true) == "garbage", + "feed security check with garbage type should return garbage"); +} diff --git a/browser/base/content/test/browser_bug416661.js b/browser/base/content/test/browser_bug416661.js new file mode 100644 index 000000000..0324ce0f3 --- /dev/null +++ b/browser/base/content/test/browser_bug416661.js @@ -0,0 +1,43 @@ +var tabElm, zoomLevel; +function start_test_prefNotSet() { + Task.spawn(function () { + is(ZoomManager.zoom, 1, "initial zoom level should be 1"); + FullZoom.enlarge(); + + //capture the zoom level to test later + zoomLevel = ZoomManager.zoom; + isnot(zoomLevel, 1, "zoom level should have changed"); + + yield FullZoomHelper.load(gBrowser.selectedTab, "http://mochi.test:8888/browser/browser/base/content/test/moz.png"); + }).then(continue_test_prefNotSet, FullZoomHelper.failAndContinue(finish)); +} + +function continue_test_prefNotSet () { + Task.spawn(function () { + is(ZoomManager.zoom, 1, "zoom level pref should not apply to an image"); + FullZoom.reset(); + + yield FullZoomHelper.load(gBrowser.selectedTab, "http://mochi.test:8888/browser/browser/base/content/test/zoom_test.html"); + }).then(end_test_prefNotSet, FullZoomHelper.failAndContinue(finish)); +} + +function end_test_prefNotSet() { + Task.spawn(function () { + is(ZoomManager.zoom, zoomLevel, "the zoom level should have persisted"); + + // Reset the zoom so that other tests have a fresh zoom level + FullZoom.reset(); + yield FullZoomHelper.removeTabAndWaitForLocationChange(); + finish(); + }); +} + +function test() { + waitForExplicitFinish(); + + Task.spawn(function () { + tabElm = gBrowser.addTab(); + yield FullZoomHelper.selectTabAndWaitForLocationChange(tabElm); + yield FullZoomHelper.load(tabElm, "http://mochi.test:8888/browser/browser/base/content/test/zoom_test.html"); + }).then(start_test_prefNotSet, FullZoomHelper.failAndContinue(finish)); +} diff --git a/browser/base/content/test/browser_bug417483.js b/browser/base/content/test/browser_bug417483.js new file mode 100644 index 000000000..ab6d73ae5 --- /dev/null +++ b/browser/base/content/test/browser_bug417483.js @@ -0,0 +1,26 @@ +function test() { + waitForExplicitFinish(); + + var htmlContent = "data:text/html, <iframe src='data:text/html,text text'></iframe>"; + gBrowser.addEventListener("pageshow", onPageShow, false); + gBrowser.loadURI(htmlContent); +} + +function onPageShow() { + gBrowser.removeEventListener("pageshow", onPageShow, false); + var frame = content.frames[0]; + var sel = frame.getSelection(); + var range = frame.document.createRange(); + var tn = frame.document.body.childNodes[0]; + range.setStart(tn , 4); + range.setEnd(tn , 5); + sel.addRange(range); + frame.focus(); + + document.popupNode = frame.document.body; + var contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + var contextMenu = new nsContextMenu(contentAreaContextMenu); + + ok(document.getElementById("frame-sep").hidden, "'frame-sep' should be hidden if the selection contains only spaces"); + finish(); +} diff --git a/browser/base/content/test/browser_bug419612.js b/browser/base/content/test/browser_bug419612.js new file mode 100644 index 000000000..1dee59ece --- /dev/null +++ b/browser/base/content/test/browser_bug419612.js @@ -0,0 +1,32 @@ +function test() { + waitForExplicitFinish(); + + Task.spawn(function () { + let testPage = "http://example.org/browser/browser/base/content/test/dummy_page.html"; + let tab1 = gBrowser.addTab(); + yield FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + yield FullZoomHelper.load(tab1, testPage); + + let tab2 = gBrowser.addTab(); + yield FullZoomHelper.load(tab2, testPage); + + FullZoom.enlarge(); + let tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + let tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser); + is(tab2Zoom, tab1Zoom, "Zoom should affect background tabs"); + + gPrefService.setBoolPref("browser.zoom.updateBackgroundTabs", false); + FullZoom.reset(); + gBrowser.selectedTab = tab1; + tab1Zoom = ZoomManager.getZoomForBrowser(tab1.linkedBrowser); + tab2Zoom = ZoomManager.getZoomForBrowser(tab2.linkedBrowser); + isnot(tab1Zoom, tab2Zoom, "Zoom should not affect background tabs"); + + if (gPrefService.prefHasUserValue("browser.zoom.updateBackgroundTabs")) + gPrefService.clearUserPref("browser.zoom.updateBackgroundTabs"); + yield FullZoomHelper.removeTabAndWaitForLocationChange(tab1); + yield FullZoomHelper.removeTabAndWaitForLocationChange(tab2); + }).then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/browser/base/content/test/browser_bug422590.js b/browser/base/content/test/browser_bug422590.js new file mode 100644 index 000000000..2ecb4c0b9 --- /dev/null +++ b/browser/base/content/test/browser_bug422590.js @@ -0,0 +1,50 @@ +function test() { + waitForExplicitFinish(); + // test the main (normal) browser window + testCustomize(window, testChromeless); +} + +function testChromeless() { + // test a chromeless window + var newWin = openDialog(getBrowserURL(), "_blank", + "chrome,dialog=no,toolbar=no", "about:blank"); + ok(newWin, "got new window"); + + whenDelayedStartupFinished(newWin, function () { + // Check that the search bar is hidden + var searchBar = newWin.BrowserSearch.searchBar; + ok(searchBar, "got search bar"); + + var searchBarBO = searchBar.boxObject; + is(searchBarBO.width, 0, "search bar hidden"); + is(searchBarBO.height, 0, "search bar hidden"); + + testCustomize(newWin, function () { + newWin.close(); + finish(); + }); + }); +} + +function testCustomize(aWindow, aCallback) { + var fileMenu = aWindow.document.getElementById("file-menu"); + ok(fileMenu, "got file menu"); + is(fileMenu.disabled, false, "file menu initially enabled"); + + openToolbarCustomizationUI(function () { + // Can't use the property, since the binding may have since been removed + // if the element is hidden (see bug 422590) + is(fileMenu.getAttribute("disabled"), "true", + "file menu is disabled during toolbar customization"); + + closeToolbarCustomizationUI(onClose, aWindow); + }, aWindow); + + function onClose() { + is(fileMenu.getAttribute("disabled"), "false", + "file menu is enabled after toolbar customization"); + + if (aCallback) + aCallback(); + } +} diff --git a/browser/base/content/test/browser_bug423833.js b/browser/base/content/test/browser_bug423833.js new file mode 100644 index 000000000..d4069338b --- /dev/null +++ b/browser/base/content/test/browser_bug423833.js @@ -0,0 +1,138 @@ +/* Tests for proper behaviour of "Show this frame" context menu options */ + +// Two frames, one with text content, the other an error page +var invalidPage = 'http://127.0.0.1:55555/'; +var validPage = 'http://example.com/'; +var testPage = 'data:text/html,<frameset cols="400,400"><frame src="' + validPage + '"><frame src="' + invalidPage + '"></frameset>'; + +// Store the tab and window created in tests 2 and 3 respectively +var test2tab; +var test3window; + +// We use setInterval instead of setTimeout to avoid race conditions on error doc loads +var intervalID; + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", test1Setup, true); + content.location = testPage; +} + +function test1Setup() { + if (content.frames.length < 2 || + content.frames[1].location != invalidPage) + // The error frame hasn't loaded yet + return; + + gBrowser.selectedBrowser.removeEventListener("load", test1Setup, true); + + var badFrame = content.frames[1]; + document.popupNode = badFrame.document.firstChild; + + var contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + var contextMenu = new nsContextMenu(contentAreaContextMenu); + + // We'd like to use another load listener here, but error pages don't fire load events + contextMenu.showOnlyThisFrame(); + intervalID = setInterval(testShowOnlyThisFrame, 3000); +} + +function testShowOnlyThisFrame() { + if (content.location.href == testPage) + // This is a stale event from the original page loading + return; + + // We should now have loaded the error page frame content directly + // in the tab, make sure the URL is right. + clearInterval(intervalID); + + is(content.location.href, invalidPage, "Should navigate to page url, not about:neterror"); + + // Go back to the frames page + gBrowser.addEventListener("load", test2Setup, true); + content.location = testPage; +} + +function test2Setup() { + if (content.frames.length < 2 || + content.frames[1].location != invalidPage) + // The error frame hasn't loaded yet + return; + + gBrowser.removeEventListener("load", test2Setup, true); + + // Now let's do the whole thing again, but this time for "Open frame in new tab" + var badFrame = content.frames[1]; + + document.popupNode = badFrame.document.firstChild; + + var contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + var contextMenu = new nsContextMenu(contentAreaContextMenu); + + gBrowser.tabContainer.addEventListener("TabOpen", function (event) { + test2tab = event.target; + gBrowser.tabContainer.removeEventListener("TabOpen", arguments.callee, false); + }, false); + contextMenu.openFrameInTab(); + ok(test2tab, "openFrameInTab() opened a tab"); + + gBrowser.selectedTab = test2tab; + + intervalID = setInterval(testOpenFrameInTab, 3000); +} + +function testOpenFrameInTab() { + if (gBrowser.contentDocument.location.href == "about:blank") + // Wait another cycle + return; + + clearInterval(intervalID); + + // We should now have the error page in a new, active tab. + is(gBrowser.contentDocument.location.href, invalidPage, "New tab should have page url, not about:neterror"); + + // Clear up the new tab, and punt to test 3 + gBrowser.removeCurrentTab(); + + test3Setup(); +} + +function test3Setup() { + // One more time, for "Open frame in new window" + var badFrame = content.frames[1]; + document.popupNode = badFrame.document.firstChild; + + var contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + var contextMenu = new nsContextMenu(contentAreaContextMenu); + + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") + test3window = aSubject; + Services.ww.unregisterNotification(arguments.callee); + }); + + contextMenu.openFrame(); + + intervalID = setInterval(testOpenFrame, 3000); +} + +function testOpenFrame() { + if (!test3window || test3window.content.location.href == "about:blank") { + info("testOpenFrame: Wait another cycle"); + return; + } + + clearInterval(intervalID); + + is(test3window.content.location.href, invalidPage, "New window should have page url, not about:neterror"); + + test3window.close(); + cleanup(); +} + +function cleanup() { + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/browser/base/content/test/browser_bug424101.js b/browser/base/content/test/browser_bug424101.js new file mode 100644 index 000000000..7c9599e69 --- /dev/null +++ b/browser/base/content/test/browser_bug424101.js @@ -0,0 +1,53 @@ +/* Make sure that the context menu appears on form elements */ + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + + gBrowser.selectedBrowser.addEventListener("load", function() { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + let doc = gBrowser.contentDocument; + let testInput = function(type, expected) { + let element = doc.createElement("input"); + element.setAttribute("type", type); + doc.body.appendChild(element); + document.popupNode = element; + + let contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + let contextMenu = new nsContextMenu(contentAreaContextMenu); + + is(contextMenu.shouldDisplay, expected, "context menu behavior for <input type=" + type + "> is wrong"); + }; + let testElement = function(tag, expected) { + let element = doc.createElement(tag); + doc.body.appendChild(element); + document.popupNode = element; + + let contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + let contextMenu = new nsContextMenu(contentAreaContextMenu); + + is(contextMenu.shouldDisplay, expected, "context menu behavior for <" + tag + "> is wrong"); + }; + + testInput("text", true); + testInput("password", true); + testInput("image", true); + testInput("button", true); + testInput("submit", true); + testInput("reset", true); + testInput("checkbox", true); + testInput("radio", true); + testElement("button", true); + testElement("select", true); + testElement("option", true); + testElement("optgroup", true); + + // cleanup + document.popupNode = null; + gBrowser.removeCurrentTab(); + finish(); + }, true); + content.location = "data:text/html,test"; +} diff --git a/browser/base/content/test/browser_bug427559.js b/browser/base/content/test/browser_bug427559.js new file mode 100644 index 000000000..50993f9b9 --- /dev/null +++ b/browser/base/content/test/browser_bug427559.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test bug 427559 to make sure focused elements that are no longer on the page + * will have focus transferred to the window when changing tabs back to that + * tab with the now-gone element. + */ + +// Default focus on a button and have it kill itself on blur +let testPage = 'data:text/html,<body><button onblur="this.parentNode.removeChild(this);"><script>document.body.firstChild.focus();</script></body>'; + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + setTimeout(function () { + var testPageWin = content; + + // The test page loaded, so open an empty tab, select it, then restore + // the test tab. This causes the test page's focused element to be removed + // from its document. + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.removeCurrentTab(); + + // Make sure focus is given to the window because the element is now gone + is(document.commandDispatcher.focusedWindow, testPageWin, + "content window is focused"); + + gBrowser.removeCurrentTab(); + finish(); + }, 0); + }, true); + + content.location = testPage; +} diff --git a/browser/base/content/test/browser_bug432599.js b/browser/base/content/test/browser_bug432599.js new file mode 100644 index 000000000..d23df4bbe --- /dev/null +++ b/browser/base/content/test/browser_bug432599.js @@ -0,0 +1,127 @@ +function invokeUsingCtrlD(phase) { + switch (phase) { + case 1: + EventUtils.synthesizeKey("d", { accelKey: true }); + break; + case 2: + case 4: + EventUtils.synthesizeKey("VK_ESCAPE", {}); + break; + case 3: + EventUtils.synthesizeKey("d", { accelKey: true }); + EventUtils.synthesizeKey("d", { accelKey: true }); + break; + } +} + +function invokeUsingStarButton(phase) { + switch (phase) { + case 1: + EventUtils.synthesizeMouseAtCenter(BookmarkingUI.star, {}); + break; + case 2: + case 4: + EventUtils.synthesizeKey("VK_ESCAPE", {}); + break; + case 3: + EventUtils.synthesizeMouseAtCenter(BookmarkingUI.star, + { clickCount: 2 }); + break; + } +} + +var testURL = "data:text/plain,Content"; +var bookmarkId; + +function add_bookmark(aURI, aTitle) { + return PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + aURI, PlacesUtils.bookmarks.DEFAULT_INDEX, + aTitle); +} + +// test bug 432599 +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onLoad() { + gBrowser.selectedBrowser.removeEventListener("load", onLoad, true); + waitForStarChange(false, initTest); + }, true); + + content.location = testURL; +} + +function initTest() { + // First, bookmark the page. + bookmarkId = add_bookmark(makeURI(testURL), "Bug 432599 Test"); + + checkBookmarksPanel(invokers[currentInvoker], 1); +} + +function waitForStarChange(aValue, aCallback) { + let expectedStatus = aValue ? BookmarkingUI.STATUS_STARRED + : BookmarkingUI.STATUS_UNSTARRED; + if (BookmarkingUI.status == BookmarkingUI.STATUS_UPDATING || + BookmarkingUI.status != expectedStatus) { + info("Waiting for star button change."); + setTimeout(waitForStarChange, 50, aValue, aCallback); + return; + } + aCallback(); +} + +let invokers = [invokeUsingStarButton, invokeUsingCtrlD]; +let currentInvoker = 0; + +let initialValue; +let initialRemoveHidden; + +let popupElement = document.getElementById("editBookmarkPanel"); +let titleElement = document.getElementById("editBookmarkPanelTitle"); +let removeElement = document.getElementById("editBookmarkPanelRemoveButton"); + +function checkBookmarksPanel(invoker, phase) +{ + let onPopupShown = function(aEvent) { + if (aEvent.originalTarget == popupElement) { + popupElement.removeEventListener("popupshown", arguments.callee, false); + checkBookmarksPanel(invoker, phase + 1); + } + }; + let onPopupHidden = function(aEvent) { + if (aEvent.originalTarget == popupElement) { + popupElement.removeEventListener("popuphidden", arguments.callee, false); + if (phase < 4) { + checkBookmarksPanel(invoker, phase + 1); + } else { + ++currentInvoker; + if (currentInvoker < invokers.length) { + checkBookmarksPanel(invokers[currentInvoker], 1); + } else { + gBrowser.removeCurrentTab(); + PlacesUtils.bookmarks.removeItem(bookmarkId); + executeSoon(finish); + } + } + } + }; + + switch (phase) { + case 1: + case 3: + popupElement.addEventListener("popupshown", onPopupShown, false); + break; + case 2: + popupElement.addEventListener("popuphidden", onPopupHidden, false); + initialValue = titleElement.value; + initialRemoveHidden = removeElement.hidden; + break; + case 4: + popupElement.addEventListener("popuphidden", onPopupHidden, false); + is(titleElement.value, initialValue, "The bookmark panel's title should be the same"); + is(removeElement.hidden, initialRemoveHidden, "The bookmark panel's visibility should not change"); + break; + } + invoker(phase); +} diff --git a/browser/base/content/test/browser_bug435035.js b/browser/base/content/test/browser_bug435035.js new file mode 100644 index 000000000..ae865ef12 --- /dev/null +++ b/browser/base/content/test/browser_bug435035.js @@ -0,0 +1,16 @@ +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + is(document.getElementById("identity-box").className, + gIdentityHandler.IDENTITY_MODE_MIXED_CONTENT, + "identity box has class name for mixed content"); + + gBrowser.removeCurrentTab(); + finish(); + }, true); + + content.location = "https://example.com/browser/browser/base/content/test/test_bug435035.html"; +} diff --git a/browser/base/content/test/browser_bug435325.js b/browser/base/content/test/browser_bug435325.js new file mode 100644 index 000000000..fe05c757a --- /dev/null +++ b/browser/base/content/test/browser_bug435325.js @@ -0,0 +1,72 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* Ensure that clicking the button in the Offline mode neterror page makes the browser go online. See bug 435325. */ + +let proxyPrefValue; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + // Go offline and disable the proxy and cache, then try to load the test URL. + Services.io.offline = true; + + // Tests always connect to localhost, and per bug 87717, localhost is now + // reachable in offline mode. To avoid this, disable any proxy. + proxyPrefValue = Services.prefs.getIntPref("network.proxy.type"); + Services.prefs.setIntPref("network.proxy.type", 0); + + Services.prefs.setBoolPref("browser.cache.disk.enable", false); + Services.prefs.setBoolPref("browser.cache.memory.enable", false); + content.location = "http://example.com/"; + + window.addEventListener("DOMContentLoaded", function load() { + if (content.location == "about:blank") { + info("got about:blank, which is expected once, so return"); + return; + } + window.removeEventListener("DOMContentLoaded", load, false); + + let observer = new MutationObserver(function (mutations) { + for (let mutation of mutations) { + if (mutation.attributeName == "hasBrowserHandlers") { + observer.disconnect(); + checkPage(); + return; + } + } + }); + let docElt = tab.linkedBrowser.contentDocument.documentElement; + observer.observe(docElt, { attributes: true }); + }, false); +} + +function checkPage() { + ok(Services.io.offline, "Setting Services.io.offline to true."); + is(gBrowser.contentDocument.documentURI.substring(0,27), + "about:neterror?e=netOffline", "Loading the Offline mode neterror page."); + + // Now press the "Try Again" button + ok(gBrowser.contentDocument.getElementById("errorTryAgain"), + "The error page has got a #errorTryAgain element"); + + // Re-enable the proxy so example.com is resolved to localhost, rather than + // the actual example.com. + Services.prefs.setIntPref("network.proxy.type", proxyPrefValue); + + gBrowser.contentDocument.getElementById("errorTryAgain").click(); + + ok(!Services.io.offline, "After clicking the Try Again button, we're back " + + "online."); + + finish(); +} + +registerCleanupFunction(function() { + Services.prefs.setBoolPref("browser.cache.disk.enable", true); + Services.prefs.setBoolPref("browser.cache.memory.enable", true); + Services.io.offline = false; + gBrowser.removeCurrentTab(); +}); diff --git a/browser/base/content/test/browser_bug441778.js b/browser/base/content/test/browser_bug441778.js new file mode 100644 index 000000000..ef68018a0 --- /dev/null +++ b/browser/base/content/test/browser_bug441778.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test the fix for bug 441778 to ensure site-specific page zoom doesn't get + * modified by sub-document loads of content from a different domain. + */ + +function test() { + waitForExplicitFinish(); + + const TEST_PAGE_URL = 'data:text/html,<body><iframe src=""></iframe></body>'; + const TEST_IFRAME_URL = "http://test2.example.org/"; + + Task.spawn(function () { + // Prepare the test tab + let tab = gBrowser.addTab(); + yield FullZoomHelper.selectTabAndWaitForLocationChange(tab); + + let testBrowser = tab.linkedBrowser; + + yield FullZoomHelper.load(tab, TEST_PAGE_URL); + + // Change the zoom level and then save it so we can compare it to the level + // after loading the sub-document. + FullZoom.enlarge(); + var zoomLevel = ZoomManager.zoom; + + // Start the sub-document load. + let deferred = Promise.defer(); + executeSoon(function () { + testBrowser.addEventListener("load", function (e) { + testBrowser.removeEventListener("load", arguments.callee, true); + + is(e.target.defaultView.location, TEST_IFRAME_URL, "got the load event for the iframe"); + is(ZoomManager.zoom, zoomLevel, "zoom is retained after sub-document load"); + + FullZoomHelper.removeTabAndWaitForLocationChange(). + then(() => deferred.resolve()); + }, true); + content.document.querySelector("iframe").src = TEST_IFRAME_URL; + }); + yield deferred.promise; + }).then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/browser/base/content/test/browser_bug455852.js b/browser/base/content/test/browser_bug455852.js new file mode 100644 index 000000000..52798f102 --- /dev/null +++ b/browser/base/content/test/browser_bug455852.js @@ -0,0 +1,16 @@ +function test() { + is(gBrowser.tabs.length, 1, "one tab is open"); + + gBrowser.selectedBrowser.focus(); + isnot(document.activeElement, gURLBar.inputField, "location bar is not focused"); + + var tab = gBrowser.selectedTab; + gPrefService.setBoolPref("browser.tabs.closeWindowWithLastTab", false); + EventUtils.synthesizeKey("w", { accelKey: true }); + is(tab.parentNode, null, "ctrl+w removes the tab"); + is(gBrowser.tabs.length, 1, "a new tab has been opened"); + is(document.activeElement, gURLBar.inputField, "location bar is focused for the new tab"); + + if (gPrefService.prefHasUserValue("browser.tabs.closeWindowWithLastTab")) + gPrefService.clearUserPref("browser.tabs.closeWindowWithLastTab"); +} diff --git a/browser/base/content/test/browser_bug460146.js b/browser/base/content/test/browser_bug460146.js new file mode 100644 index 000000000..3ddaae97e --- /dev/null +++ b/browser/base/content/test/browser_bug460146.js @@ -0,0 +1,51 @@ +/* Check proper image url retrieval from all kinds of elements/styles */ + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + var doc = gBrowser.contentDocument; + var pageInfo = BrowserPageInfo(doc, "mediaTab"); + + pageInfo.addEventListener("load", function () { + pageInfo.removeEventListener("load", arguments.callee, true); + pageInfo.onFinished.push(function () { + executeSoon(function () { + var imageTree = pageInfo.document.getElementById("imagetree"); + var imageRowsNum = imageTree.view.rowCount; + + ok(imageTree, "Image tree is null (media tab is broken)"); + + ok(imageRowsNum == 7, "Number of images listed: " + + imageRowsNum + ", should be 7"); + + pageInfo.close(); + gBrowser.removeCurrentTab(); + finish(); + }); + }); + }, true); + }, true); + + content.location = + "data:text/html," + + "<html>" + + " <head>" + + " <title>Test for media tab</title>" + + " <link rel='shortcut icon' href='file:///dummy_icon.ico'>" + // Icon + " </head>" + + " <body style='background-image:url(about:logo?a);'>" + // Background + " <img src='file:///dummy_image.gif'>" + // Image + " <ul>" + + " <li style='list-style:url(about:logo?b);'>List Item 1</li>" + // Bullet + " </ul> " + + " <div style='-moz-border-image: url(about:logo?c) 20 20 20 20;'>test</div>" + // Border + " <a href='' style='cursor: url(about:logo?d),default;'>test link</a>" + // Cursor + " <object type='image/svg+xml' width=20 height=20 data='file:///dummy_object.svg'></object>" + // Object + " </body>" + + "</html>"; +} diff --git a/browser/base/content/test/browser_bug462289.js b/browser/base/content/test/browser_bug462289.js new file mode 100644 index 000000000..fcde120ff --- /dev/null +++ b/browser/base/content/test/browser_bug462289.js @@ -0,0 +1,85 @@ +var tab1, tab2; + +function focus_in_navbar() +{ + var parent = document.activeElement.parentNode; + while (parent && parent.id != "nav-bar") + parent = parent.parentNode; + + return parent != null; +} + +function test() +{ + waitForExplicitFinish(); + + tab1 = gBrowser.addTab("about:blank", {skipAnimation: true}); + tab2 = gBrowser.addTab("about:blank", {skipAnimation: true}); + + EventUtils.synthesizeMouseAtCenter(tab1, {}); + setTimeout(step2, 0); +} + +function step2() +{ + is(gBrowser.selectedTab, tab1, "1st click on tab1 selects tab"); + isnot(document.activeElement, tab1, "1st click on tab1 does not activate tab"); + + EventUtils.synthesizeMouseAtCenter(tab1, {}); + setTimeout(step3, 0); +} + +function step3() +{ + is(gBrowser.selectedTab, tab1, "2nd click on selected tab1 keeps tab selected"); + isnot(document.activeElement, tab1, "2nd click on selected tab1 does not activate tab"); + + if (gNavToolbox.getAttribute("tabsontop") == "true") { + ok(true, "[tabsontop=true] focusing URLBar then sending 1 Shift+Tab."); + gURLBar.focus(); + EventUtils.synthesizeKey("VK_TAB", {shiftKey: true}); + } else { + ok(true, "[tabsontop=false] focusing SearchBar then sending Tab(s) until out of nav-bar."); + document.getElementById("searchbar").focus(); + while (focus_in_navbar()) + EventUtils.synthesizeKey("VK_TAB", { }); + } + is(gBrowser.selectedTab, tab1, "tab key to selected tab1 keeps tab selected"); + is(document.activeElement, tab1, "tab key to selected tab1 activates tab"); + + EventUtils.synthesizeMouseAtCenter(tab1, {}); + setTimeout(step4, 0); +} + +function step4() +{ + is(gBrowser.selectedTab, tab1, "3rd click on activated tab1 keeps tab selected"); + is(document.activeElement, tab1, "3rd click on activated tab1 keeps tab activated"); + + EventUtils.synthesizeMouseAtCenter(tab2, {}); + setTimeout(step5, 0); +} + +function step5() +{ + // The tabbox selects a tab within a setTimeout in a bubbling mousedown event + // listener, and focuses the current tab if another tab previously had focus. + is(gBrowser.selectedTab, tab2, "click on tab2 while tab1 is activated selects tab"); + is(document.activeElement, tab2, "click on tab2 while tab1 is activated activates tab"); + + ok(true, "focusing content then sending middle-button mousedown to tab2."); + gBrowser.selectedBrowser.focus(); + EventUtils.synthesizeMouseAtCenter(tab2, {button: 1, type: "mousedown"}); + setTimeout(step6, 0); +} + +function step6() +{ + is(gBrowser.selectedTab, tab2, "middle-button mousedown on selected tab2 keeps tab selected"); + isnot(document.activeElement, tab2, "middle-button mousedown on selected tab2 does not activate tab"); + + gBrowser.removeTab(tab2); + gBrowser.removeTab(tab1); + + finish(); +} diff --git a/browser/base/content/test/browser_bug462673.js b/browser/base/content/test/browser_bug462673.js new file mode 100644 index 000000000..83bec6f6f --- /dev/null +++ b/browser/base/content/test/browser_bug462673.js @@ -0,0 +1,53 @@ +var runs = [ + function (win, tabbrowser, tab) { + is(tabbrowser.browsers.length, 2, "test_bug462673.html has opened a second tab"); + is(tabbrowser.selectedTab, tab.nextSibling, "dependent tab is selected"); + tabbrowser.removeTab(tab); + ok(win.closed, "Window is closed"); + }, + function (win, tabbrowser, tab) { + var newTab = tabbrowser.addTab(); + var newBrowser = newTab.linkedBrowser; + tabbrowser.removeTab(tab); + ok(!win.closed, "Window stays open"); + if (!win.closed) { + is(tabbrowser.tabContainer.childElementCount, 1, "Window has one tab"); + is(tabbrowser.browsers.length, 1, "Window has one browser"); + is(tabbrowser.selectedTab, newTab, "Remaining tab is selected"); + is(tabbrowser.selectedBrowser, newBrowser, "Browser for remaining tab is selected"); + is(tabbrowser.mTabBox.selectedPanel, newBrowser.parentNode.parentNode.parentNode.parentNode, "Panel for remaining tab is selected"); + } + } +]; + +function test() { + waitForExplicitFinish(); + runOneTest(); +} + +function runOneTest() { + var win = openDialog(getBrowserURL(), "_blank", "chrome,all,dialog=no"); + + win.addEventListener("load", function () { + win.removeEventListener("load", arguments.callee, false); + + var tab = win.gBrowser.tabContainer.firstChild; + var browser = tab.linkedBrowser; + + browser.addEventListener("load", function () { + browser.removeEventListener("load", arguments.callee, true); + + executeSoon(function () { + runs.shift()(win, win.gBrowser, tab); + win.close(); + if (runs.length) + runOneTest(); + else + finish(); + }); + }, true); + + var rootDir = getRootDirectory(gTestPath); + browser.contentWindow.location = rootDir + "test_bug462673.html" + }, false); +} diff --git a/browser/base/content/test/browser_bug477014.js b/browser/base/content/test/browser_bug477014.js new file mode 100644 index 000000000..77770e199 --- /dev/null +++ b/browser/base/content/test/browser_bug477014.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// That's a gecko! +const iconURLSpec = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="; +var testPage="data:text/plain,test bug 477014"; + +function test() { + waitForExplicitFinish(); + + var newWindow; + var tabToDetach; + var documentToDetach; + + function onPageShow(event) { + // we get here if the test is executed before the pageshow + // event for the window's first tab + if (!tabToDetach || documentToDetach != event.target) + return; + + event.currentTarget.removeEventListener("pageshow", onPageShow, false); + + if (!newWindow) { + // prepare the tab (set icon and busy state) + // we have to set these only after onState* notification, otherwise + // they're overriden + setTimeout(function() { + gBrowser.setIcon(tabToDetach, iconURLSpec); + tabToDetach.setAttribute("busy", "true"); + + // detach and set the listener on the new window + newWindow = gBrowser.replaceTabWithWindow(tabToDetach); + // wait for gBrowser to come along + newWindow.addEventListener("load", function () { + newWindow.removeEventListener("load", arguments.callee, false); + newWindow.gBrowser.addEventListener("pageshow", onPageShow, false); + }, false); + }, 0); + return; + } + + is(newWindow.gBrowser.selectedTab.hasAttribute("busy"), true); + is(newWindow.gBrowser.getIcon(), iconURLSpec); + newWindow.close(); + finish(); + } + + tabToDetach = gBrowser.addTab(testPage); + tabToDetach.linkedBrowser.addEventListener("load", function onLoad() { + tabToDetach.linkedBrowser.removeEventListener("load", onLoad, true); + documentToDetach = tabToDetach.linkedBrowser.contentDocument; + gBrowser.addEventListener("pageshow", onPageShow, false); + }, true); +} diff --git a/browser/base/content/test/browser_bug479408.js b/browser/base/content/test/browser_bug479408.js new file mode 100644 index 000000000..0a14e2259 --- /dev/null +++ b/browser/base/content/test/browser_bug479408.js @@ -0,0 +1,17 @@ +function test() { + waitForExplicitFinish(); + let tab = gBrowser.selectedTab = gBrowser.addTab( + "http://mochi.test:8888/browser/browser/base/content/test/browser_bug479408_sample.html"); + + gBrowser.addEventListener("DOMLinkAdded", function(aEvent) { + gBrowser.removeEventListener("DOMLinkAdded", arguments.callee, true); + + executeSoon(function() { + ok(!tab.linkedBrowser.engines, + "the subframe's search engine wasn't detected"); + + gBrowser.removeTab(tab); + finish(); + }); + }, true); +} diff --git a/browser/base/content/test/browser_bug479408_sample.html b/browser/base/content/test/browser_bug479408_sample.html new file mode 100644 index 000000000..f83f02bb9 --- /dev/null +++ b/browser/base/content/test/browser_bug479408_sample.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<title>Testcase for bug 479408</title> + +<iframe src='data:text/html,<link%20rel="search"%20type="application/opensearchdescription+xml"%20title="Search%20bug%20479408"%20href="http://example.com/search.xml">'> diff --git a/browser/base/content/test/browser_bug481560.js b/browser/base/content/test/browser_bug481560.js new file mode 100644 index 000000000..e3d281b1b --- /dev/null +++ b/browser/base/content/test/browser_bug481560.js @@ -0,0 +1,27 @@ +function test() { + waitForExplicitFinish(); + + var win = openDialog(getBrowserURL(), "_blank", "chrome,all,dialog=no"); + + win.addEventListener("load", function () { + win.removeEventListener("load", arguments.callee, false); + + win.content.addEventListener("focus", function () { + win.content.removeEventListener("focus", arguments.callee, false); + + function onTabClose() { + ok(false, "shouldn't have gotten the TabClose event for the last tab"); + } + var tab = win.gBrowser.selectedTab; + tab.addEventListener("TabClose", onTabClose, false); + + EventUtils.synthesizeKey("w", { accelKey: true }, win); + + ok(win.closed, "accel+w closed the window immediately"); + + tab.removeEventListener("TabClose", onTabClose, false); + + finish(); + }, false); + }, false); +} diff --git a/browser/base/content/test/browser_bug484315.js b/browser/base/content/test/browser_bug484315.js new file mode 100644 index 000000000..fb23ae33a --- /dev/null +++ b/browser/base/content/test/browser_bug484315.js @@ -0,0 +1,23 @@ +function test() { + var contentWin = window.open("about:blank", "", "width=100,height=100"); + var enumerator = Services.wm.getEnumerator("navigator:browser"); + + while (enumerator.hasMoreElements()) { + let win = enumerator.getNext(); + if (win.content == contentWin) { + gPrefService.setBoolPref("browser.tabs.closeWindowWithLastTab", false); + win.gBrowser.removeCurrentTab(); + ok(win.closed, "popup is closed"); + + // clean up + if (!win.closed) + win.close(); + if (gPrefService.prefHasUserValue("browser.tabs.closeWindowWithLastTab")) + gPrefService.clearUserPref("browser.tabs.closeWindowWithLastTab"); + + return; + } + } + + throw "couldn't find the content window"; +} diff --git a/browser/base/content/test/browser_bug491431.js b/browser/base/content/test/browser_bug491431.js new file mode 100644 index 000000000..357e55bfc --- /dev/null +++ b/browser/base/content/test/browser_bug491431.js @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 testPage = "data:text/plain,test bug 491431 Page"; + +function test() { + waitForExplicitFinish(); + + let newWin, tabA, tabB; + + // test normal close + tabA = gBrowser.addTab(testPage); + gBrowser.tabContainer.addEventListener("TabClose", function(aEvent) { + gBrowser.tabContainer.removeEventListener("TabClose", arguments.callee, true); + ok(!aEvent.detail, "This was a normal tab close"); + + // test tab close by moving + tabB = gBrowser.addTab(testPage); + gBrowser.tabContainer.addEventListener("TabClose", function(aEvent) { + gBrowser.tabContainer.removeEventListener("TabClose", arguments.callee, true); + executeSoon(function() { + ok(aEvent.detail, "This was a tab closed by moving"); + + // cleanup + newWin.close(); + executeSoon(finish); + }); + }, true); + newWin = gBrowser.replaceTabWithWindow(tabB); + }, true); + gBrowser.removeTab(tabA); +} + diff --git a/browser/base/content/test/browser_bug495058.js b/browser/base/content/test/browser_bug495058.js new file mode 100644 index 000000000..0d4680058 --- /dev/null +++ b/browser/base/content/test/browser_bug495058.js @@ -0,0 +1,43 @@ +function test() { + waitForExplicitFinish(); + next(); +} + +var uris = [ + "about:blank", + "about:sessionrestore", + "about:privatebrowsing", +]; + +function next() { + var tab = gBrowser.addTab(); + var uri = uris.shift(); + + if (uri == "about:blank") { + detach(); + } else { + let browser = tab.linkedBrowser; + browser.addEventListener("load", function () { + browser.removeEventListener("load", arguments.callee, true); + detach(); + }, true); + browser.loadURI(uri); + } + + function detach() { + var win = gBrowser.replaceTabWithWindow(tab); + + whenDelayedStartupFinished(win, function () { + is(win.gBrowser.currentURI.spec, uri, uri + ": uri loaded in detached tab"); + is(win.document.activeElement, win.gBrowser.selectedBrowser, uri + ": browser is focused"); + is(win.gURLBar.value, "", uri + ": urlbar is empty"); + ok(win.gURLBar.placeholder, uri + ": placeholder text is present"); + + win.close(); + if (uris.length) + next(); + else + executeSoon(finish); + }); + } +} diff --git a/browser/base/content/test/browser_bug517902.js b/browser/base/content/test/browser_bug517902.js new file mode 100644 index 000000000..525b68aab --- /dev/null +++ b/browser/base/content/test/browser_bug517902.js @@ -0,0 +1,41 @@ +/* Make sure that "View Image Info" loads the correct image data */ + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + var doc = gBrowser.contentDocument; + var testImg = doc.getElementById("test-image"); + var pageInfo = BrowserPageInfo(doc, "mediaTab", testImg); + + pageInfo.addEventListener("load", function () { + pageInfo.removeEventListener("load", arguments.callee, true); + pageInfo.onImagePreviewShown.push(function () { + executeSoon(function () { + var pageInfoImg = pageInfo.document.getElementById("thepreviewimage"); + + is(pageInfoImg.src, testImg.src, "selected image has the correct source"); + is(pageInfoImg.width, testImg.width, "selected image has the correct width"); + is(pageInfoImg.height, testImg.height, "selected image has the correct height"); + + pageInfo.close(); + gBrowser.removeCurrentTab(); + finish(); + }); + }); + }, true); + }, true); + + content.location = + "data:text/html," + + "<style type='text/css'>%23test-image,%23not-test-image {background-image: url('about:logo?c');}</style>" + + "<img src='about:logo?b' height=300 width=350 alt=2 id='not-test-image'>" + + "<img src='about:logo?b' height=300 width=350 alt=2>" + + "<img src='about:logo?a' height=200 width=250>" + + "<img src='about:logo?b' height=200 width=250 alt=1>" + + "<img src='about:logo?b' height=100 width=150 alt=2 id='test-image'>"; +} diff --git a/browser/base/content/test/browser_bug519216.js b/browser/base/content/test/browser_bug519216.js new file mode 100644 index 000000000..a924f7a09 --- /dev/null +++ b/browser/base/content/test/browser_bug519216.js @@ -0,0 +1,50 @@ +function test() { + waitForExplicitFinish(); + gBrowser.stop(); + gBrowser.addProgressListener(progressListener1); + gBrowser.addProgressListener(progressListener2); + gBrowser.addProgressListener(progressListener3); + gBrowser.loadURI("data:text/plain,bug519216"); +} + +var calledListener1 = false; +var progressListener1 = { + onLocationChange: function onLocationChange() { + calledListener1 = true; + gBrowser.removeProgressListener(this); + } +}; + +var calledListener2 = false; +var progressListener2 = { + onLocationChange: function onLocationChange() { + ok(calledListener1, "called progressListener1 before progressListener2"); + calledListener2 = true; + gBrowser.removeProgressListener(this); + } +}; + +var progressListener3 = { + onLocationChange: function onLocationChange() { + ok(calledListener2, "called progressListener2 before progressListener3"); + gBrowser.removeProgressListener(this); + gBrowser.addProgressListener(progressListener4); + executeSoon(function () { + expectListener4 = true; + gBrowser.reload(); + }); + } +}; + +var expectListener4 = false; +var progressListener4 = { + onLocationChange: function onLocationChange() { + ok(expectListener4, "didn't call progressListener4 for the first location change"); + gBrowser.removeProgressListener(this); + executeSoon(function () { + gBrowser.addTab(); + gBrowser.removeCurrentTab(); + finish(); + }); + } +}; diff --git a/browser/base/content/test/browser_bug520538.js b/browser/base/content/test/browser_bug520538.js new file mode 100644 index 000000000..4489b64c3 --- /dev/null +++ b/browser/base/content/test/browser_bug520538.js @@ -0,0 +1,15 @@ +function test() { + var tabCount = gBrowser.tabs.length; + gBrowser.selectedBrowser.focus(); + browserDOMWindow.openURI(makeURI("about:blank"), + null, + Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, + Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + is(gBrowser.tabs.length, tabCount + 1, + "'-new-tab about:blank' opens a new tab"); + is(gBrowser.selectedTab, gBrowser.tabs[tabCount], + "'-new-tab about:blank' selects the new tab"); + is(document.activeElement, gURLBar.inputField, + "'-new-tab about:blank' focuses the location bar"); + gBrowser.removeCurrentTab(); +} diff --git a/browser/base/content/test/browser_bug521216.js b/browser/base/content/test/browser_bug521216.js new file mode 100644 index 000000000..c39ff741f --- /dev/null +++ b/browser/base/content/test/browser_bug521216.js @@ -0,0 +1,47 @@ +var expected = ["TabOpen", "onStateChange", "onLocationChange", "onLinkIconAvailable"]; +var actual = []; +var tabIndex = -1; +this.__defineGetter__("tab", function () gBrowser.tabs[tabIndex]); + +function test() { + waitForExplicitFinish(); + tabIndex = gBrowser.tabs.length; + gBrowser.addTabsProgressListener(progressListener); + gBrowser.tabContainer.addEventListener("TabOpen", TabOpen, false); + gBrowser.addTab("data:text/html,<html><head><link href='about:logo' rel='shortcut icon'>"); +} + +function record(aName) { + info("got " + aName); + if (actual.indexOf(aName) == -1) + actual.push(aName); + if (actual.length == expected.length) { + is(actual.toString(), expected.toString(), + "got events and progress notifications in expected order"); + gBrowser.removeTab(tab); + gBrowser.removeTabsProgressListener(progressListener); + gBrowser.tabContainer.removeEventListener("TabOpen", TabOpen, false); + finish(); + } +} + +function TabOpen(aEvent) { + if (aEvent.target == tab) + record(arguments.callee.name); +} + +var progressListener = { + onLocationChange: function onLocationChange(aBrowser) { + if (aBrowser == tab.linkedBrowser) + record(arguments.callee.name); + }, + onStateChange: function onStateChange(aBrowser) { + if (aBrowser == tab.linkedBrowser) + record(arguments.callee.name); + }, + onLinkIconAvailable: function onLinkIconAvailable(aBrowser, aIconURL) { + if (aBrowser == tab.linkedBrowser && + aIconURL == "about:logo") + record(arguments.callee.name); + } +}; diff --git a/browser/base/content/test/browser_bug533232.js b/browser/base/content/test/browser_bug533232.js new file mode 100644 index 000000000..fdee75ba2 --- /dev/null +++ b/browser/base/content/test/browser_bug533232.js @@ -0,0 +1,36 @@ +function test() { + var tab1 = gBrowser.selectedTab; + var tab2 = gBrowser.addTab(); + var childTab1; + var childTab2; + + childTab1 = gBrowser.addTab("about:blank", { relatedToCurrent: true }); + gBrowser.selectedTab = childTab1; + gBrowser.removeCurrentTab(); + is(idx(gBrowser.selectedTab), idx(tab1), + "closing a tab next to its parent selects the parent"); + + childTab1 = gBrowser.addTab("about:blank", { relatedToCurrent: true }); + gBrowser.selectedTab = tab2; + gBrowser.selectedTab = childTab1; + gBrowser.removeCurrentTab(); + is(idx(gBrowser.selectedTab), idx(tab2), + "closing a tab next to its parent doesn't select the parent if another tab had been selected ad interim"); + + gBrowser.selectedTab = tab1; + childTab1 = gBrowser.addTab("about:blank", { relatedToCurrent: true }); + childTab2 = gBrowser.addTab("about:blank", { relatedToCurrent: true }); + gBrowser.selectedTab = childTab1; + gBrowser.removeCurrentTab(); + is(idx(gBrowser.selectedTab), idx(childTab2), + "closing a tab next to its parent selects the next tab with the same parent"); + gBrowser.removeCurrentTab(); + is(idx(gBrowser.selectedTab), idx(tab2), + "closing the last tab in a set of child tabs doesn't go back to the parent"); + + gBrowser.removeTab(tab2); +} + +function idx(tab) { + return Array.indexOf(gBrowser.tabs, tab); +} diff --git a/browser/base/content/test/browser_bug537474.js b/browser/base/content/test/browser_bug537474.js new file mode 100644 index 000000000..3c471e1d2 --- /dev/null +++ b/browser/base/content/test/browser_bug537474.js @@ -0,0 +1,8 @@ +function test() { + var currentWin = content; + var newWin = + browserDOMWindow.openURI(makeURI("about:"), null, + Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW, null) + is(newWin, currentWin, "page loads in the current content window"); + gBrowser.stop(); +} diff --git a/browser/base/content/test/browser_bug550565.js b/browser/base/content/test/browser_bug550565.js new file mode 100644 index 000000000..0dfa4ed4a --- /dev/null +++ b/browser/base/content/test/browser_bug550565.js @@ -0,0 +1,21 @@ +function test() { + waitForExplicitFinish(); + + let testPath = getRootDirectory(gTestPath); + + let tab = gBrowser.addTab(testPath + "file_bug550565_popup.html"); + + tab.linkedBrowser.addEventListener("DOMContentLoaded", function() { + tab.linkedBrowser.removeEventListener("DOMContentLoaded", arguments.callee, true); + + let expectedIcon = testPath + "file_bug550565_favicon.ico"; + + is(gBrowser.getIcon(tab), expectedIcon, "Correct icon before pushState."); + tab.linkedBrowser.contentWindow.history.pushState("page2", "page2", "page2"); + is(gBrowser.getIcon(tab), expectedIcon, "Correct icon after pushState."); + + gBrowser.removeTab(tab); + + finish(); + }, true); +} diff --git a/browser/base/content/test/browser_bug553455.js b/browser/base/content/test/browser_bug553455.js new file mode 100644 index 000000000..ddfba9fd3 --- /dev/null +++ b/browser/base/content/test/browser_bug553455.js @@ -0,0 +1,903 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TESTROOT = "http://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/"; +const TESTROOT2 = "http://example.org/browser/toolkit/mozapps/extensions/test/xpinstall/"; +const SECUREROOT = "https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/"; +const XPINSTALL_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; +const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts"; + +var rootDir = getRootDirectory(gTestPath); +var path = rootDir.split('/'); +var chromeName = path[0] + '//' + path[2]; +var croot = chromeName + "/content/browser/toolkit/mozapps/extensions/test/xpinstall/"; +var jar = getJar(croot); +if (jar) { + var tmpdir = extractJarToTmp(jar); + croot = 'file://' + tmpdir.path + '/'; +} +const CHROMEROOT = croot; + +var gApp = document.getElementById("bundle_brand").getString("brandShortName"); +var gVersion = Services.appinfo.version; +var check_notification; + +function wait_for_notification(aCallback) { + info("Waiting for notification"); + check_notification = function() { + PopupNotifications.panel.removeEventListener("popupshown", check_notification, false); + info("Saw notification"); + is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification"); + aCallback(PopupNotifications.panel); + }; + PopupNotifications.panel.addEventListener("popupshown", check_notification, false); +} + +function wait_for_notification_close(aCallback) { + info("Waiting for notification to close"); + PopupNotifications.panel.addEventListener("popuphidden", function() { + PopupNotifications.panel.removeEventListener("popuphidden", arguments.callee, false); + aCallback(); + }, false); +} + +function wait_for_install_dialog(aCallback) { + info("Waiting for install dialog"); + Services.wm.addListener({ + onOpenWindow: function(aXULWindow) { + info("Install dialog opened, waiting for focus"); + Services.wm.removeListener(this); + + var domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + waitForFocus(function() { + info("Saw install dialog"); + is(domwindow.document.location.href, XPINSTALL_URL, "Should have seen the right window open"); + + // Override the countdown timer on the accept button + var button = domwindow.document.documentElement.getButton("accept"); + button.disabled = false; + + aCallback(domwindow); + }, domwindow); + }, + + onCloseWindow: function(aXULWindow) { + }, + + onWindowTitleChange: function(aXULWindow, aNewTitle) { + } + }); +} + +function wait_for_single_notification(aCallback) { + function inner_waiter() { + info("Waiting for single notification"); + // Notification should never close while we wait + ok(PopupNotifications.isPanelOpen, "Notification should still be open"); + if (PopupNotifications.panel.childNodes.length == 2) { + executeSoon(inner_waiter); + return; + } + + aCallback(); + } + + executeSoon(inner_waiter); +} + +function setup_redirect(aSettings) { + var url = "https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs?mode=setup"; + for (var name in aSettings) { + url += "&" + name + "=" + aSettings[name]; + } + + var req = new XMLHttpRequest(); + req.open("GET", url, false); + req.send(null); +} + +var TESTS = [ +function test_disabled_install() { + Services.prefs.setBoolPref("xpinstall.enabled", false); + + // Wait for the disabled notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "xpinstall-disabled-notification", "Should have seen installs disabled"); + is(notification.button.label, "Enable", "Should have seen the right button"); + is(notification.getAttribute("label"), + "Software installation is currently disabled. Click Enable and try again."); + + wait_for_notification_close(function() { + try { + ok(Services.prefs.getBoolPref("xpinstall.enabled"), "Installation should be enabled"); + } + catch (e) { + ok(false, "xpinstall.enabled should be set"); + } + + gBrowser.removeTab(gBrowser.selectedTab); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 1, "Should have been one install created"); + aInstalls[0].cancel(); + + runNextTest(); + }); + }); + + // Click on Enable + EventUtils.synthesizeMouseAtCenter(notification.button, {}); + }); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "unsigned.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_blocked_install() { + // Wait for the blocked notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-blocked-notification", "Should have seen the install blocked"); + is(notification.button.label, "Allow", "Should have seen the right button"); + is(notification.getAttribute("label"), + gApp + " prevented this site (example.com) from asking you to install " + + "software on your computer.", + "Should have seen the right message"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the install complete"); + is(notification.button.label, "Restart Now", "Should have seen the right button"); + is(notification.getAttribute("label"), + "XPI Test will be installed after you restart " + gApp + ".", + "Should have seen the right message"); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 1, "Should be one pending install"); + aInstalls[0].cancel(); + + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + + // Click on Allow + EventUtils.synthesizeMouse(notification.button, 20, 10, {}); + + // Notification should have changed to progress notification + ok(PopupNotifications.isPanelOpen, "Notification should still be open"); + notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + }); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "unsigned.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_whitelisted_install() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the install complete"); + is(notification.button.label, "Restart Now", "Should have seen the right button"); + is(notification.getAttribute("label"), + "XPI Test will be installed after you restart " + gApp + ".", + "Should have seen the right message"); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 1, "Should be one pending install"); + aInstalls[0].cancel(); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "unsigned.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_failed_download() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the failed notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-failed-notification", "Should have seen the install fail"); + is(notification.getAttribute("label"), + "The add-on could not be downloaded because of a connection failure " + + "on example.com.", + "Should have seen the right message"); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "missing.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_corrupt_file() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the failed notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-failed-notification", "Should have seen the install fail"); + is(notification.getAttribute("label"), + "The add-on downloaded from example.com could not be installed " + + "because it appears to be corrupt.", + "Should have seen the right message"); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "corrupt.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_incompatible() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the failed notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-failed-notification", "Should have seen the install fail"); + is(notification.getAttribute("label"), + "XPI Test could not be installed because it is not compatible with " + + gApp + " " + gVersion + ".", + "Should have seen the right message"); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "incompatible.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_restartless() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the install complete"); + is(notification.getAttribute("label"), + "XPI Test has been installed successfully.", + "Should have seen the right message"); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 0, "Should be no pending installs"); + + AddonManager.getAddonByID("restartless-xpi@tests.mozilla.org", function(aAddon) { + aAddon.uninstall(); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "restartless.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_multiple() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the install complete"); + is(notification.button.label, "Restart Now", "Should have seen the right button"); + is(notification.getAttribute("label"), + "2 add-ons will be installed after you restart " + gApp + ".", + "Should have seen the right message"); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 1, "Should be one pending install"); + aInstalls[0].cancel(); + + AddonManager.getAddonByID("restartless-xpi@tests.mozilla.org", function(aAddon) { + aAddon.uninstall(); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "Unsigned XPI": "unsigned.xpi", + "Restartless XPI": "restartless.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_url() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the install complete"); + is(notification.button.label, "Restart Now", "Should have seen the right button"); + is(notification.getAttribute("label"), + "XPI Test will be installed after you restart " + gApp + ".", + "Should have seen the right message"); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 1, "Should be one pending install"); + aInstalls[0].cancel(); + + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + }); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "unsigned.xpi"); +}, + +function test_localfile() { + // Wait for the install to fail + Services.obs.addObserver(function() { + Services.obs.removeObserver(arguments.callee, "addon-install-failed"); + + // Wait for the browser code to add the failure notification + wait_for_single_notification(function() { + let notification = PopupNotifications.panel.childNodes[0]; + is(notification.id, "addon-install-failed-notification", "Should have seen the install fail"); + is(notification.getAttribute("label"), + "This add-on could not be installed because it appears to be corrupt.", + "Should have seen the right message"); + + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }, "addon-install-failed", false); + + var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"] + .getService(Components.interfaces.nsIChromeRegistry); + try { + var path = cr.convertChromeURL(makeURI(CHROMEROOT + "corrupt.xpi")).spec; + } catch (ex) { + var path = CHROMEROOT + "corrupt.xpi"; + } + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(path); +}, + +function test_wronghost() { + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.addEventListener("load", function() { + if (gBrowser.currentURI.spec != TESTROOT2 + "enabled.html") + return; + + gBrowser.removeEventListener("load", arguments.callee, true); + + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-failed-notification", "Should have seen the install fail"); + is(notification.getAttribute("label"), + "The add-on downloaded from example.com could not be installed " + + "because it appears to be corrupt.", + "Should have seen the right message"); + + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + gBrowser.loadURI(TESTROOT + "corrupt.xpi"); + }, true); + gBrowser.loadURI(TESTROOT2 + "enabled.html"); +}, + +function test_reload() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the install complete"); + is(notification.button.label, "Restart Now", "Should have seen the right button"); + is(notification.getAttribute("label"), + "XPI Test will be installed after you restart " + gApp + ".", + "Should have seen the right message"); + + function test_fail() { + ok(false, "Reloading should not have hidden the notification"); + } + + PopupNotifications.panel.addEventListener("popuphiding", test_fail, false); + + gBrowser.addEventListener("load", function() { + if (gBrowser.currentURI.spec != TESTROOT2 + "enabled.html") + return; + + gBrowser.removeEventListener("load", arguments.callee, true); + + PopupNotifications.panel.removeEventListener("popuphiding", test_fail, false); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 1, "Should be one pending install"); + aInstalls[0].cancel(); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }, true); + gBrowser.loadURI(TESTROOT2 + "enabled.html"); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "Unsigned XPI": "unsigned.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_theme() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the install complete"); + is(notification.button.label, "Restart Now", "Should have seen the right button"); + is(notification.getAttribute("label"), + "Theme Test will be installed after you restart " + gApp + ".", + "Should have seen the right message"); + + AddonManager.getAddonByID("{972ce4c6-7e08-4474-a285-3208198ce6fd}", function(aAddon) { + ok(aAddon.userDisabled, "Should be switching away from the default theme."); + // Undo the pending theme switch + aAddon.userDisabled = false; + + AddonManager.getAddonByID("theme-xpi@tests.mozilla.org", function(aAddon) { + isnot(aAddon, null, "Test theme will have been installed"); + aAddon.uninstall(); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "Theme XPI": "theme.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_renotify_blocked() { + // Wait for the blocked notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-blocked-notification", "Should have seen the install blocked"); + + wait_for_notification_close(function () { + info("Timeouts after this probably mean bug 589954 regressed"); + executeSoon(function () { + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-blocked-notification", + "Should have seen the install blocked - 2nd time"); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 2, "Should be two pending installs"); + aInstalls[0].cancel(); + aInstalls[1].cancel(); + + info("Closing browser tab"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); + }); + }); + + // hide the panel (this simulates the user dismissing it) + aPanel.hidePopup(); + }); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "unsigned.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_renotify_installed() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the install complete"); + + // Dismiss the notification + wait_for_notification_close(function () { + // Install another + executeSoon(function () { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + info("Timeouts after this probably mean bug 589954 regressed"); + + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the second install complete"); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 1, "Should be one pending installs"); + aInstalls[0].cancel(); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + }); + + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); + }); + }); + + // hide the panel (this simulates the user dismissing it) + aPanel.hidePopup(); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "unsigned.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_cancel_restart() { + // Wait for the progress notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Close the notification + let anchor = document.getElementById("addons-notification-icon"); + anchor.click(); + // Reopen the notification + anchor.click(); + + ok(PopupNotifications.isPanelOpen, "Notification should still be open"); + is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification"); + isnot(notification, aPanel.childNodes[0], "Should have reconstructed the notification UI"); + notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + let button = document.getAnonymousElementByAttribute(notification, "anonid", "cancel"); + + // Cancel the download + EventUtils.synthesizeMouse(button, 2, 2, {}); + + // Notification should have changed to cancelled + notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-cancelled-notification", "Should have seen the cancelled notification"); + + // Wait for the install confirmation dialog + wait_for_install_dialog(function(aWindow) { + // Wait for the complete notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-complete-notification", "Should have seen the install complete"); + is(notification.button.label, "Restart Now", "Should have seen the right button"); + is(notification.getAttribute("label"), + "XPI Test will be installed after you restart " + gApp + ".", + "Should have seen the right message"); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 1, "Should be one pending install"); + aInstalls[0].cancel(); + + Services.perms.remove("example.com", "install"); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }); + + aWindow.document.documentElement.acceptDialog(); + }); + + // Restart the download + EventUtils.synthesizeMouse(notification.button, 20, 10, {}); + + // Should be back to a progress notification + ok(PopupNotifications.isPanelOpen, "Notification should still be open"); + is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification"); + notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + }); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "unsigned.xpi" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers); +}, + +function test_failed_security() { + Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false); + + setup_redirect({ + "Location": TESTROOT + "unsigned.xpi" + }); + + // Wait for the blocked notification + wait_for_notification(function(aPanel) { + let notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-blocked-notification", "Should have seen the install blocked"); + + // Click on Allow + EventUtils.synthesizeMouse(notification.button, 20, 10, {}); + + // Notification should have changed to progress notification + ok(PopupNotifications.isPanelOpen, "Notification should still be open"); + is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification"); + notification = aPanel.childNodes[0]; + is(notification.id, "addon-progress-notification", "Should have seen the progress notification"); + + // Wait for it to fail + Services.obs.addObserver(function() { + Services.obs.removeObserver(arguments.callee, "addon-install-failed"); + + // Allow the browser code to add the failure notification and then wait + // for the progress notification to dismiss itself + wait_for_single_notification(function() { + is(PopupNotifications.panel.childNodes.length, 1, "Should be only one notification"); + notification = aPanel.childNodes[0]; + is(notification.id, "addon-install-failed-notification", "Should have seen the install fail"); + + Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, true); + wait_for_notification_close(runNextTest); + gBrowser.removeTab(gBrowser.selectedTab); + }); + }, "addon-install-failed", false); + }); + + var triggers = encodeURIComponent(JSON.stringify({ + "XPI": "redirect.sjs?mode=redirect" + })); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(SECUREROOT + "installtrigger.html?" + triggers); +} +]; + +var gTestStart = null; + +function runNextTest() { + if (gTestStart) + info("Test part took " + (Date.now() - gTestStart) + "ms"); + + ok(!PopupNotifications.isPanelOpen, "Notification should be closed"); + + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 0, "Should be no active installs"); + + if (TESTS.length == 0) { + finish(); + return; + } + + info("Running " + TESTS[0].name); + gTestStart = Date.now(); + TESTS.shift()(); + }); +}; + +var XPInstallObserver = { + observe: function (aSubject, aTopic, aData) { + var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo); + info("Observed " + aTopic + " for " + installInfo.installs.length + " installs"); + installInfo.installs.forEach(function(aInstall) { + info("Install of " + aInstall.sourceURI.spec + " was in state " + aInstall.state); + }); + } +}; + +function test() { + requestLongerTimeout(4); + waitForExplicitFinish(); + + Services.prefs.setBoolPref("extensions.logging.enabled", true); + Services.prefs.setBoolPref("extensions.strictCompatibility", true); + + Services.obs.addObserver(XPInstallObserver, "addon-install-started", false); + Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false); + Services.obs.addObserver(XPInstallObserver, "addon-install-failed", false); + Services.obs.addObserver(XPInstallObserver, "addon-install-complete", false); + + registerCleanupFunction(function() { + // Make sure no more test parts run in case we were timed out + TESTS = []; + PopupNotifications.panel.removeEventListener("popupshown", check_notification, false); + + AddonManager.getAllInstalls(function(aInstalls) { + aInstalls.forEach(function(aInstall) { + aInstall.cancel(); + }); + }); + + Services.prefs.clearUserPref("extensions.logging.enabled"); + Services.prefs.clearUserPref("extensions.strictCompatibility"); + + Services.obs.removeObserver(XPInstallObserver, "addon-install-started"); + Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked"); + Services.obs.removeObserver(XPInstallObserver, "addon-install-failed"); + Services.obs.removeObserver(XPInstallObserver, "addon-install-complete"); + }); + + runNextTest(); +} diff --git a/browser/base/content/test/browser_bug555224.js b/browser/base/content/test/browser_bug555224.js new file mode 100644 index 000000000..dbf3464a5 --- /dev/null +++ b/browser/base/content/test/browser_bug555224.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 TEST_PAGE = "/browser/browser/base/content/test/dummy_page.html"; +var gTestTab, gBgTab, gTestZoom; + +function testBackgroundLoad() { + Task.spawn(function () { + is(ZoomManager.zoom, gTestZoom, "opening a background tab should not change foreground zoom"); + + yield FullZoomHelper.removeTabAndWaitForLocationChange(gBgTab); + + FullZoom.reset(); + yield FullZoomHelper.removeTabAndWaitForLocationChange(gTestTab); + finish(); + }); +} + +function testInitialZoom() { + Task.spawn(function () { + is(ZoomManager.zoom, 1, "initial zoom level should be 1"); + FullZoom.enlarge(); + + gTestZoom = ZoomManager.zoom; + isnot(gTestZoom, 1, "zoom level should have changed"); + + gBgTab = gBrowser.addTab(); + yield FullZoomHelper.load(gBgTab, "http://mochi.test:8888" + TEST_PAGE); + }).then(testBackgroundLoad, FullZoomHelper.failAndContinue(finish)); +} + +function test() { + waitForExplicitFinish(); + + Task.spawn(function () { + gTestTab = gBrowser.addTab(); + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTestTab); + yield FullZoomHelper.load(gTestTab, "http://example.org" + TEST_PAGE); + }).then(testInitialZoom, FullZoomHelper.failAndContinue(finish)); +} diff --git a/browser/base/content/test/browser_bug555767.js b/browser/base/content/test/browser_bug555767.js new file mode 100644 index 000000000..cfd1904a0 --- /dev/null +++ b/browser/base/content/test/browser_bug555767.js @@ -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/. */ + +function test() { + waitForExplicitFinish(); + + let testURL = "http://example.org/browser/browser/base/content/test/dummy_page.html"; + let tabSelected = false; + + // Open the base tab + let baseTab = gBrowser.addTab(testURL); + baseTab.linkedBrowser.addEventListener("load", function() { + // Wait for the tab to be fully loaded so matching happens correctly + if (baseTab.linkedBrowser.currentURI.spec == "about:blank") + return; + baseTab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + let testTab = gBrowser.addTab(); + + // Select the testTab + gBrowser.selectedTab = testTab; + + // Ensure that this tab has no history entries + ok(testTab.linkedBrowser.sessionHistory.count < 2, + "The test tab has 1 or less history entries"); + // Ensure that this tab is on about:blank + is(testTab.linkedBrowser.currentURI.spec, "about:blank", + "The test tab is on about:blank"); + // Ensure that this tab's document has no child nodes + ok(!testTab.linkedBrowser.contentDocument.body.hasChildNodes(), + "The test tab has no child nodes"); + ok(!testTab.hasAttribute("busy"), + "The test tab doesn't have the busy attribute"); + + // Set the urlbar to include the moz-action + gURLBar.value = "moz-action:switchtab," + testURL; + // Focus the urlbar so we can press enter + gURLBar.focus(); + + // Functions for TabClose and TabSelect + function onTabClose(aEvent) { + gBrowser.tabContainer.removeEventListener("TabClose", onTabClose, false); + // Make sure we get the TabClose event for testTab + is(aEvent.originalTarget, testTab, "Got the TabClose event for the right tab"); + // Confirm that we did select the tab + ok(tabSelected, "Confirming that the tab was selected"); + gBrowser.removeTab(baseTab); + finish(); + } + function onTabSelect(aEvent) { + gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect, false); + // Make sure we got the TabSelect event for baseTab + is(aEvent.originalTarget, baseTab, "Got the TabSelect event for the right tab"); + // Confirm that the selected tab is in fact base tab + is(gBrowser.selectedTab, baseTab, "We've switched to the correct tab"); + tabSelected = true; + } + + // Add the TabClose, TabSelect event listeners before we press enter + gBrowser.tabContainer.addEventListener("TabClose", onTabClose, false); + gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect, false); + + // Press enter! + EventUtils.synthesizeKey("VK_RETURN", {}); + }, true); +} + diff --git a/browser/base/content/test/browser_bug556061.js b/browser/base/content/test/browser_bug556061.js new file mode 100644 index 000000000..21046d079 --- /dev/null +++ b/browser/base/content/test/browser_bug556061.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 testURL = "http://example.org/browser/browser/base/content/test/dummy_page.html"; +let testActionURL = "moz-action:switchtab," + testURL; +testURL = gURLBar.trimValue(testURL); +let testTab; + +function runNextTest() { + if (tests.length) { + let t = tests.shift(); + waitForClipboard(t.expected, t.setup, function() { + t.success(); + runNextTest(); + }, cleanup); + } + else { + cleanup(); + } +} + +function cleanup() { + gBrowser.removeTab(testTab); + finish(); +} + +let tests = [ + { + expected: testURL, + setup: function() { + gURLBar.value = testActionURL; + gURLBar.valueIsTyped = true; + is(gURLBar.value, testActionURL, "gURLBar.value starts with correct value"); + + // Focus the urlbar so we can select it all & copy + gURLBar.focus(); + gURLBar.select(); + goDoCommand("cmd_copy"); + }, + success: function() { + is(gURLBar.value, testActionURL, "gURLBar.value didn't change when copying"); + } + }, + { + expected: testURL.substring(0, 10), + setup: function() { + // Set selectionStart/End manually and make sure it matches the substring + gURLBar.selectionStart = 0; + gURLBar.selectionEnd = 10; + goDoCommand("cmd_copy"); + }, + success: function() { + is(gURLBar.value, testActionURL, "gURLBar.value didn't change when copying"); + } + }, + { + expected: testURL, + setup: function() { + // Setup for cut test... + // Select all + gURLBar.select(); + goDoCommand("cmd_cut"); + }, + success: function() { + is(gURLBar.value, "", "gURLBar.value is now empty"); + } + }, + { + expected: testURL.substring(testURL.length - 10, testURL.length), + setup: function() { + // Reset urlbar value + gURLBar.value = testActionURL; + gURLBar.valueIsTyped = true; + // Sanity check that we have the right value + is(gURLBar.value, testActionURL, "gURLBar.value starts with correct value"); + + // Now just select part of the value & cut that. + gURLBar.selectionStart = testURL.length - 10; + gURLBar.selectionEnd = testURL.length; + goDoCommand("cmd_cut"); + }, + success: function() { + is(gURLBar.value, testURL.substring(0, testURL.length - 10), "gURLBar.value has the correct value"); + } + } +]; + +function test() { + waitForExplicitFinish(); + testTab = gBrowser.addTab(); + gBrowser.selectedTab = testTab; + + // Kick off the testing + runNextTest(); +} diff --git a/browser/base/content/test/browser_bug559991.js b/browser/base/content/test/browser_bug559991.js new file mode 100644 index 000000000..60b2621d0 --- /dev/null +++ b/browser/base/content/test/browser_bug559991.js @@ -0,0 +1,42 @@ +var tab; + +function test() { + + // ---------- + // Test setup + + waitForExplicitFinish(); + + gPrefService.setBoolPref("browser.zoom.updateBackgroundTabs", true); + gPrefService.setBoolPref("browser.zoom.siteSpecific", true); + + let uri = "http://example.org/browser/browser/base/content/test/dummy_page.html"; + + Task.spawn(function () { + tab = gBrowser.addTab(); + yield FullZoomHelper.load(tab, uri); + + // ------------------------------------------------------------------- + // Test - Trigger a tab switch that should update the zoom level + yield FullZoomHelper.selectTabAndWaitForLocationChange(tab); + ok(true, "applyPrefToSetting was called"); + }).then(endTest, FullZoomHelper.failAndContinue(endTest)); +} + +// ------------- +// Test clean-up +function endTest() { + Task.spawn(function () { + yield FullZoomHelper.removeTabAndWaitForLocationChange(tab); + + tab = null; + + if (gPrefService.prefHasUserValue("browser.zoom.updateBackgroundTabs")) + gPrefService.clearUserPref("browser.zoom.updateBackgroundTabs"); + + if (gPrefService.prefHasUserValue("browser.zoom.siteSpecific")) + gPrefService.clearUserPref("browser.zoom.siteSpecific"); + + finish(); + }); +} diff --git a/browser/base/content/test/browser_bug561623.js b/browser/base/content/test/browser_bug561623.js new file mode 100644 index 000000000..6f7fc85c7 --- /dev/null +++ b/browser/base/content/test/browser_bug561623.js @@ -0,0 +1,29 @@ +function test() { + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + let doc = gBrowser.contentDocument; + let tooltip = document.getElementById("aHTMLTooltip"); + let i = doc.getElementById("i"); + + ok(!tooltip.fillInPageTooltip(i), + "No tooltip should be shown when @title is null"); + + i.title = "foo"; + ok(tooltip.fillInPageTooltip(i), + "A tooltip should be shown when @title is not the empty string"); + + i.pattern = "bar"; + ok(tooltip.fillInPageTooltip(i), + "A tooltip should be shown when @title is not the empty string"); + + gBrowser.removeCurrentTab(); + finish(); + }, true); + + content.location = + "data:text/html,<!DOCTYPE html><html><body><input id='i'></body></html>"; +} + diff --git a/browser/base/content/test/browser_bug561636.js b/browser/base/content/test/browser_bug561636.js new file mode 100644 index 000000000..1cf689390 --- /dev/null +++ b/browser/base/content/test/browser_bug561636.js @@ -0,0 +1,474 @@ +var gInvalidFormPopup = document.getElementById('invalid-form-popup'); +ok(gInvalidFormPopup, + "The browser should have a popup to show when a form is invalid"); + +function checkPopupShow() +{ + ok(gInvalidFormPopup.state == 'showing' || gInvalidFormPopup.state == 'open', + "[Test " + testId + "] The invalid form popup should be shown"); +} + +function checkPopupHide() +{ + ok(gInvalidFormPopup.state != 'showing' && gInvalidFormPopup.state != 'open', + "[Test " + testId + "] The invalid form popup should not be shown"); +} + +function checkPopupMessage(doc) +{ + is(gInvalidFormPopup.firstChild.textContent, + doc.getElementById('i').validationMessage, + "[Test " + testId + "] The panel should show the message from validationMessage"); +} + +let gObserver = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), + + notifyInvalidSubmit : function (aFormElement, aInvalidElements) + { + } +}; + +var testId = -1; + +function nextTest() +{ + testId++; + if (testId >= tests.length) { + finish(); + return; + } + executeSoon(tests[testId]); +} + +function test() +{ + waitForExplicitFinish(); + waitForFocus(nextTest); +} + +var tests = [ + +/** + * In this test, we check that no popup appears if the form is valid. + */ +function() +{ + let uri = "data:text/html,<html><body><iframe name='t'></iframe><form target='t' action='data:text/html,'><input><input id='s' type='submit'></form></body></html>"; + let tab = gBrowser.addTab(); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + let doc = gBrowser.contentDocument; + + doc.getElementById('s').click(); + + executeSoon(function() { + checkPopupHide(); + + // Clean-up + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that, when an invalid form is submitted, + * the invalid element is focused and a popup appears. + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input required id='i'><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that, when an invalid form is submitted, + * the first invalid element is focused and a popup appears. + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input><input id='i' required><input required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that, we hide the popup by interacting with the + * invalid element if the element becomes valid. + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + EventUtils.synthesizeKey("a", {}); + + executeSoon(function () { + checkPopupHide(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that, we don't hide the popup by interacting with the + * invalid element if the element is still invalid. + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input type='email' id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + EventUtils.synthesizeKey("a", {}); + + executeSoon(function () { + checkPopupShow(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that we can hide the popup by blurring the invalid + * element. + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + doc.getElementById('i').blur(); + + executeSoon(function () { + checkPopupHide(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that we can hide the popup by pressing TAB. + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + EventUtils.synthesizeKey("VK_TAB", {}); + + executeSoon(function () { + checkPopupHide(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that the popup will hide if we move to another tab. + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + checkPopupMessage(doc); + + // Create a new tab and move to it. + gBrowser.selectedTab = gBrowser.addTab("about:blank", {skipAnimation: true}); + + executeSoon(function() { + checkPopupHide(); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that nothing happen (no focus nor popup) if the + * invalid form is submitted in another tab than the current focused one + * (submitted in background). + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input id='i' required><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gObserver.notifyInvalidSubmit = function() { + executeSoon(function() { + let doc = tab.linkedBrowser.contentDocument; + isnot(doc.activeElement, doc.getElementById('i'), + "We should not focus the invalid element when the form is submitted in background"); + + checkPopupHide(); + + // Clean-up + Services.obs.removeObserver(gObserver, "invalidformsubmit"); + gObserver.notifyInvalidSubmit = function () {}; + gBrowser.removeTab(tab); + + nextTest(); + }); + }; + + Services.obs.addObserver(gObserver, "invalidformsubmit", false); + + tab.linkedBrowser.addEventListener("load", function(e) { + // Ignore load events from the iframe. + if (tab.linkedBrowser.contentDocument == e.target) { + let browser = e.currentTarget; + browser.removeEventListener("load", arguments.callee, true); + + isnot(gBrowser.selectedTab.linkedBrowser, browser, + "This tab should have been loaded in background"); + browser.contentDocument.getElementById('s').click(); + } + }, true); + + tab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that the author defined error message is shown. + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input x-moz-errormessage='foo' required id='i'><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + checkPopupShow(); + + is(gInvalidFormPopup.firstChild.textContent, "foo", + "The panel should show the author defined error message"); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +/** + * In this test, we check that the message is correctly updated when it changes. + */ +function() +{ + let uri = "data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input type='email' required id='i'><input id='s' type='submit'></form>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument; + let input = doc.getElementById('i'); + is(doc.activeElement, input, "First invalid element should be focused"); + + checkPopupShow(); + + is(gInvalidFormPopup.firstChild.textContent, input.validationMessage, + "The panel should show the current validation message"); + + input.addEventListener('input', function() { + input.removeEventListener('input', arguments.callee, false); + + executeSoon(function() { + // Now, the element suffers from another error, the message should have + // been updated. + is(gInvalidFormPopup.firstChild.textContent, input.validationMessage, + "The panel should show the current validation message"); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab); + nextTest(); + }); + }, false); + + EventUtils.synthesizeKey('f', {}); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +}, + +]; diff --git a/browser/base/content/test/browser_bug562649.js b/browser/base/content/test/browser_bug562649.js new file mode 100644 index 000000000..af7cdaeb1 --- /dev/null +++ b/browser/base/content/test/browser_bug562649.js @@ -0,0 +1,27 @@ +function test() { + const URI = "data:text/plain,bug562649"; + browserDOMWindow.openURI(makeURI(URI), + null, + Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, + Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); + + ok(XULBrowserWindow.isBusy, "window is busy loading a page"); + is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI"); + is(gURLBar.value, URI, "location bar value matches test URI"); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.removeCurrentTab(); + is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI after switching tabs"); + is(gURLBar.value, URI, "location bar value matches test URI after switching tabs"); + + waitForExplicitFinish(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + is(gBrowser.userTypedValue, null, "userTypedValue is null as the page has loaded"); + is(gURLBar.value, URI, "location bar value matches test URI as the page has loaded"); + + gBrowser.removeCurrentTab(); + finish(); + }, true); +} diff --git a/browser/base/content/test/browser_bug563588.js b/browser/base/content/test/browser_bug563588.js new file mode 100644 index 000000000..a1774fb7e --- /dev/null +++ b/browser/base/content/test/browser_bug563588.js @@ -0,0 +1,30 @@ +function press(key, expectedPos) { + var originalSelectedTab = gBrowser.selectedTab; + EventUtils.synthesizeKey("VK_" + key.toUpperCase(), { accelKey: true }); + is(gBrowser.selectedTab, originalSelectedTab, + "accel+" + key + " doesn't change which tab is selected"); + is(gBrowser.tabContainer.selectedIndex, expectedPos, + "accel+" + key + " moves the tab to the expected position"); + is(document.activeElement, gBrowser.selectedTab, + "accel+" + key + " leaves the selected tab focused"); +} + +function test() { + gBrowser.addTab(); + gBrowser.addTab(); + is(gBrowser.tabs.length, 3, "got three tabs"); + is(gBrowser.tabs[0], gBrowser.selectedTab, "first tab is selected"); + + gBrowser.selectedTab.focus(); + is(document.activeElement, gBrowser.selectedTab, "selected tab is focused"); + + press("right", 1); + press("down", 2); + press("left", 1); + press("up", 0); + press("end", 2); + press("home", 0); + + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +} diff --git a/browser/base/content/test/browser_bug565575.js b/browser/base/content/test/browser_bug565575.js new file mode 100644 index 000000000..a6a227010 --- /dev/null +++ b/browser/base/content/test/browser_bug565575.js @@ -0,0 +1,13 @@ +function test() { + gBrowser.selectedBrowser.focus(); + BrowserOpenTab(); + ok(gURLBar.focused, "location bar is focused for a new tab"); + + gBrowser.selectedTab = gBrowser.tabs[0]; + ok(!gURLBar.focused, "location bar isn't focused for the previously selected tab"); + + gBrowser.selectedTab = gBrowser.tabs[1]; + ok(gURLBar.focused, "location bar is re-focused when selecting the new tab"); + + gBrowser.removeCurrentTab(); +} diff --git a/browser/base/content/test/browser_bug565667.js b/browser/base/content/test/browser_bug565667.js new file mode 100644 index 000000000..6fac026c8 --- /dev/null +++ b/browser/base/content/test/browser_bug565667.js @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + +function test() { + waitForExplicitFinish(); + // Open the javascript console. It has the mac menu overlay, so browser.js is + // loaded in it. + let consoleWin = window.open("chrome://global/content/console.xul", "_blank", + "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar"); + testWithOpenWindow(consoleWin); +} + +function testWithOpenWindow(consoleWin) { + // Add a tab so we don't open the url into the current tab + let newTab = gBrowser.addTab("http://example.com"); + gBrowser.selectedTab = newTab; + + let numTabs = gBrowser.tabs.length; + + waitForFocus(function() { + // Sanity check + is(fm.activeWindow, consoleWin, + "the console window is focused"); + + gBrowser.tabContainer.addEventListener("TabOpen", function(aEvent) { + gBrowser.tabContainer.removeEventListener("TabOpen", arguments.callee, true); + let browser = aEvent.originalTarget.linkedBrowser; + browser.addEventListener("pageshow", function(event) { + if (event.target.location.href != "about:addons") + return; + browser.removeEventListener("pageshow", arguments.callee, true); + + is(fm.activeWindow, window, + "the browser window was focused"); + is(browser.currentURI.spec, "about:addons", + "about:addons was loaded in the window"); + is(gBrowser.tabs.length, numTabs + 1, + "a new tab was added"); + + // Cleanup. + executeSoon(function() { + consoleWin.close(); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.removeTab(newTab); + finish(); + }); + }, true); + }, true); + + // Open the addons manager, uses switchToTabHavingURI. + consoleWin.BrowserOpenAddonsMgr(); + }, consoleWin); +} + +// Ideally we'd also check that the case for no open windows works, but we can't +// due to limitations with the testing framework. diff --git a/browser/base/content/test/browser_bug567306.js b/browser/base/content/test/browser_bug567306.js new file mode 100644 index 000000000..1fa35efa8 --- /dev/null +++ b/browser/base/content/test/browser_bug567306.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +let Ci = Components.interfaces; + +function test() { + waitForExplicitFinish(); + + whenNewWindowLoaded(undefined, function (win) { + whenDelayedStartupFinished(win, function () { + let selectedBrowser = win.gBrowser.selectedBrowser; + selectedBrowser.addEventListener("pageshow", function() { + selectedBrowser.removeEventListener("pageshow", arguments.callee, true); + ok(true, "pageshow listener called: " + win.content.location); + waitForFocus(function () { + onFocus(win); + }, selectedBrowser.contentWindow); + }, true); + selectedBrowser.loadURI("data:text/html,<h1 id='h1'>Select Me</h1>"); + }); + }); +} + +function selectText(win) { + let elt = win.document.getElementById("h1"); + let selection = win.getSelection(); + let range = win.document.createRange(); + range.setStart(elt, 0); + range.setEnd(elt, 1); + selection.removeAllRanges(); + selection.addRange(range); +} + +function onFocus(win) { + ok(!win.gFindBarInitialized, "find bar is not yet initialized"); + let findBar = win.gFindBar; + selectText(win.content); + findBar.onFindCommand(); + is(findBar._findField.value, "Select Me", "Findbar is initialized with selection"); + findBar.close(); + win.close(); + finish(); +} diff --git a/browser/base/content/test/browser_bug575561.js b/browser/base/content/test/browser_bug575561.js new file mode 100644 index 000000000..61c2676e4 --- /dev/null +++ b/browser/base/content/test/browser_bug575561.js @@ -0,0 +1,90 @@ +function test() {
+ waitForExplicitFinish();
+
+ // Pinned: Link to the same domain should not open a new tab
+ // Tests link to http://example.com/browser/browser/base/content/test/dummy_page.html
+ testLink(0, true, false, function() {
+ // Pinned: Link to a different subdomain should open a new tab
+ // Tests link to http://test1.example.com/browser/browser/base/content/test/dummy_page.html
+ testLink(1, true, true, function() {
+ // Pinned: Link to a different domain should open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/dummy_page.html
+ testLink(2, true, true, function() {
+ // Not Pinned: Link to a different domain should not open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/dummy_page.html
+ testLink(2, false, false, function() {
+ // Pinned: Targetted link should open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/dummy_page.html with target="foo"
+ testLink(3, true, true, function() {
+ // Pinned: Link in a subframe should not open a new tab
+ // Tests link to http://example.org/browser/browser/base/content/test/dummy_page.html in subframe
+ testLink(0, true, false, function() {
+ // Pinned: Link to the same domain (with www prefix) should not open a new tab
+ // Tests link to http://www.example.com/browser/browser/base/content/test/dummy_page.html
+ testLink(4, true, false, function() {
+ // Pinned: Link to a data: URI should not open a new tab
+ // Tests link to data:text/html,<!DOCTYPE html><html><body>Another Page</body></html>
+ testLink(5, true, false, function() {
+ // Pinned: Link to an about: URI should not open a new tab
+ // Tests link to about:mozilla
+ testLink(6, true, false, finish);
+ });
+ });
+ }, true);
+ });
+ });
+ });
+ });
+ });
+}
+
+function testLink(aLinkIndex, pinTab, expectNewTab, nextTest, testSubFrame) {
+ let appTab = gBrowser.addTab("http://example.com/browser/browser/base/content/test/app_bug575561.html", {skipAnimation: true});
+ if (pinTab)
+ gBrowser.pinTab(appTab);
+ gBrowser.selectedTab = appTab;
+ appTab.linkedBrowser.addEventListener("load", onLoad, true);
+
+ let loadCount = 0;
+ function onLoad() {
+ loadCount++;
+ if (loadCount < 2)
+ return;
+
+ appTab.linkedBrowser.removeEventListener("load", onLoad, true);
+
+ let browser = gBrowser.getBrowserForTab(appTab);
+ if (testSubFrame)
+ browser = browser.contentDocument.getElementsByTagName("iframe")[0];
+
+ let links = browser.contentDocument.getElementsByTagName("a");
+
+ if (expectNewTab)
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true);
+ else
+ browser.addEventListener("load", onPageLoad, true);
+
+ info("Clicking " + links[aLinkIndex].textContent);
+ EventUtils.sendMouseEvent({type:"click"}, links[aLinkIndex], browser.contentWindow);
+ let linkLocation = links[aLinkIndex].href;
+
+ function onPageLoad() {
+ browser.removeEventListener("load", onPageLoad, true);
+ is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab");
+ executeSoon(function(){
+ gBrowser.removeTab(appTab);
+ nextTest();
+ });
+ }
+
+ function onTabOpen(event) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true);
+ ok(true, "Link should open a new tab");
+ executeSoon(function(){
+ gBrowser.removeTab(appTab);
+ gBrowser.removeCurrentTab();
+ nextTest();
+ });
+ }
+ }
+}
diff --git a/browser/base/content/test/browser_bug575830.js b/browser/base/content/test/browser_bug575830.js new file mode 100644 index 000000000..506da4a17 --- /dev/null +++ b/browser/base/content/test/browser_bug575830.js @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"; + +function test() { + let tab1, tab2; + const TEST_IMAGE = "http://example.org/browser/browser/base/content/test/moz.png"; + + waitForExplicitFinish(); + + Task.spawn(function () { + tab1 = gBrowser.addTab(); + tab2 = gBrowser.addTab(); + yield FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + yield FullZoomHelper.load(tab1, TEST_IMAGE); + + is(ZoomManager.zoom, 1, "initial zoom level for first should be 1"); + + FullZoom.enlarge(); + let zoom = ZoomManager.zoom; + isnot(zoom, 1, "zoom level should have changed"); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(tab2); + is(ZoomManager.zoom, 1, "initial zoom level for second tab should be 1"); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(tab1); + is(ZoomManager.zoom, zoom, "zoom level for first tab should not have changed"); + + yield FullZoomHelper.removeTabAndWaitForLocationChange(tab1); + yield FullZoomHelper.removeTabAndWaitForLocationChange(tab2); + }).then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/browser/base/content/test/browser_bug577121.js b/browser/base/content/test/browser_bug577121.js new file mode 100644 index 000000000..94d169be0 --- /dev/null +++ b/browser/base/content/test/browser_bug577121.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + // Open 2 other tabs, and pin the second one. Like that, the initial tab + // should get closed. + let testTab1 = gBrowser.addTab(); + let testTab2 = gBrowser.addTab(); + gBrowser.pinTab(testTab2); + + // Now execute "Close other Tabs" on the first manually opened tab (tab1). + // -> tab2 ist pinned, tab1 should remain open and the initial tab should + // get closed. + gBrowser.removeAllTabsBut(testTab1); + + is(gBrowser.tabs.length, 2, "there are two remaining tabs open"); + is(gBrowser.tabs[0], testTab2, "pinned tab2 stayed open"); + is(gBrowser.tabs[1], testTab1, "tab1 stayed open"); + + // Cleanup. Close only one tab because we need an opened tab at the end of + // the test. + gBrowser.removeTab(testTab2); +} diff --git a/browser/base/content/test/browser_bug578534.js b/browser/base/content/test/browser_bug578534.js new file mode 100644 index 000000000..5cf83cc66 --- /dev/null +++ b/browser/base/content/test/browser_bug578534.js @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + let uriString = "http://example.com/"; + let cookieBehavior = "network.cookie.cookieBehavior"; + let uriObj = Services.io.newURI(uriString, null, null) + let cp = Components.classes["@mozilla.org/cookie/permission;1"] + .getService(Components.interfaces.nsICookiePermission); + + Services.prefs.setIntPref(cookieBehavior, 2); + + cp.setAccess(uriObj, cp.ACCESS_ALLOW); + gBrowser.selectedTab = gBrowser.addTab(uriString); + waitForExplicitFinish(); + gBrowser.selectedBrowser.addEventListener("load", onTabLoaded, true); + + function onTabLoaded() { + is(gBrowser.selectedBrowser.contentWindow.navigator.cookieEnabled, true, + "navigator.cookieEnabled should be true"); + // Clean up + gBrowser.selectedBrowser.removeEventListener("load", onTabLoaded, true); + gBrowser.removeTab(gBrowser.selectedTab); + Services.prefs.setIntPref(cookieBehavior, 0); + cp.setAccess(uriObj, cp.ACCESS_DEFAULT); + finish(); + } +} diff --git a/browser/base/content/test/browser_bug579872.js b/browser/base/content/test/browser_bug579872.js new file mode 100644 index 000000000..63f64598d --- /dev/null +++ b/browser/base/content/test/browser_bug579872.js @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + let newTab = gBrowser.addTab(); + waitForExplicitFinish(); + newTab.linkedBrowser.addEventListener("load", mainPart, true); + + function mainPart() { + gBrowser.pinTab(newTab); + gBrowser.selectedTab = newTab; + + openUILinkIn("javascript:var x=0;", "current"); + is(gBrowser.tabs.length, 2, "Should open in current tab"); + + openUILinkIn("http://example.com/1", "current"); + is(gBrowser.tabs.length, 2, "Should open in current tab"); + + openUILinkIn("http://example.org/", "current"); + is(gBrowser.tabs.length, 3, "Should open in new tab"); + + newTab.removeEventListener("load", mainPart, true); + gBrowser.removeTab(newTab); + gBrowser.removeTab(gBrowser.tabs[1]); // example.org tab + finish(); + } + newTab.linkedBrowser.loadURI("http://example.com"); +} diff --git a/browser/base/content/test/browser_bug580638.js b/browser/base/content/test/browser_bug580638.js new file mode 100644 index 000000000..ead51105e --- /dev/null +++ b/browser/base/content/test/browser_bug580638.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + function testState(aPinned) { + function elemAttr(id, attr) document.getElementById(id).getAttribute(attr); + + if (aPinned) { + is(elemAttr("key_close", "disabled"), "true", + "key_close should be disabled when a pinned-tab is selected"); + is(elemAttr("menu_close", "key"), "", + "menu_close shouldn't have a key set when a pinned is selected"); + } + else { + is(elemAttr("key_close", "disabled"), "", + "key_closed shouldn't have disabled state set when a non-pinned tab is selected"); + is(elemAttr("menu_close", "key"), "key_close", + "menu_close should have key_close set as its key when a non-pinned tab is selected"); + } + } + + let lastSelectedTab = gBrowser.selectedTab; + ok(!lastSelectedTab.pinned, "We should have started with a regular tab selected"); + + testState(false); + + let pinnedTab = gBrowser.addTab("about:blank"); + gBrowser.pinTab(pinnedTab); + + // Just pinning the tab shouldn't change the key state. + testState(false); + + // Test updating key state after selecting a tab. + gBrowser.selectedTab = pinnedTab; + testState(true); + + gBrowser.selectedTab = lastSelectedTab; + testState(false); + + gBrowser.selectedTab = pinnedTab; + testState(true); + + // Test updating the key state after un/pinning the tab. + gBrowser.unpinTab(pinnedTab); + testState(false); + + gBrowser.pinTab(pinnedTab); + testState(true); + + // Test updating the key state after removing the tab. + gBrowser.removeTab(pinnedTab); + testState(false); + + finish(); +} diff --git a/browser/base/content/test/browser_bug580956.js b/browser/base/content/test/browser_bug580956.js new file mode 100644 index 000000000..f7cc3c3fe --- /dev/null +++ b/browser/base/content/test/browser_bug580956.js @@ -0,0 +1,29 @@ +function numClosedTabs() + Cc["@mozilla.org/browser/sessionstore;1"]. + getService(Ci.nsISessionStore). + getClosedTabCount(window); + +function isUndoCloseEnabled() { + updateTabContextMenu(); + return !document.getElementById("context_undoCloseTab").disabled; +} + +function test() { + waitForExplicitFinish(); + + gPrefService.setIntPref("browser.sessionstore.max_tabs_undo", 0); + gPrefService.clearUserPref("browser.sessionstore.max_tabs_undo"); + is(numClosedTabs(), 0, "There should be 0 closed tabs."); + ok(!isUndoCloseEnabled(), "Undo Close Tab should be disabled."); + + var tab = gBrowser.addTab("http://mochi.test:8888/"); + var browser = gBrowser.getBrowserForTab(tab); + browser.addEventListener("load", function() { + browser.removeEventListener("load", arguments.callee, true); + + gBrowser.removeTab(tab); + ok(isUndoCloseEnabled(), "Undo Close Tab should be enabled."); + + finish(); + }, true); +} diff --git a/browser/base/content/test/browser_bug581242.js b/browser/base/content/test/browser_bug581242.js new file mode 100644 index 000000000..668c0cd41 --- /dev/null +++ b/browser/base/content/test/browser_bug581242.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + // Create a new tab and load about:addons + let blanktab = gBrowser.addTab(); + gBrowser.selectedTab = blanktab; + BrowserOpenAddonsMgr(); + + is(blanktab, gBrowser.selectedTab, "Current tab should be blank tab"); + // Verify that about:addons loads + waitForExplicitFinish(); + gBrowser.selectedBrowser.addEventListener("load", function() { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + let browser = blanktab.linkedBrowser; + is(browser.currentURI.spec, "about:addons", "about:addons should load into blank tab."); + gBrowser.removeTab(blanktab); + finish(); + }, true); +} diff --git a/browser/base/content/test/browser_bug581253.js b/browser/base/content/test/browser_bug581253.js new file mode 100644 index 000000000..3d2575118 --- /dev/null +++ b/browser/base/content/test/browser_bug581253.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +let testURL = "data:text/plain,nothing but plain text"; +let testTag = "581253_tag"; +let timerID = -1; + +function test() { + registerCleanupFunction(function() { + PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId); + if (timerID > 0) { + clearTimeout(timerID); + } + }); + waitForExplicitFinish(); + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + tab.linkedBrowser.addEventListener("load", (function(event) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + let uri = makeURI(testURL); + let bmTxn = + new PlacesCreateBookmarkTransaction(uri, + PlacesUtils.unfiledBookmarksFolderId, + -1, "", null, []); + PlacesUtils.transactionManager.doTransaction(bmTxn); + + ok(PlacesUtils.bookmarks.isBookmarked(uri), "the test url is bookmarked"); + waitForStarChange(true, onStarred); + }), true); + + content.location = testURL; +} + +function waitForStarChange(aValue, aCallback) { + let expectedStatus = aValue ? BookmarkingUI.STATUS_STARRED + : BookmarkingUI.STATUS_UNSTARRED; + if (BookmarkingUI.status == BookmarkingUI.STATUS_UPDATING || + BookmarkingUI.status != expectedStatus) { + info("Waiting for star button change."); + setTimeout(waitForStarChange, 50, aValue, aCallback); + return; + } + aCallback(); +} + +function onStarred() { + is(BookmarkingUI.status, BookmarkingUI.STATUS_STARRED, + "star button indicates that the page is bookmarked"); + + let uri = makeURI(testURL); + let tagTxn = new PlacesTagURITransaction(uri, [testTag]); + PlacesUtils.transactionManager.doTransaction(tagTxn); + + StarUI.panel.addEventListener("popupshown", onPanelShown, false); + BookmarkingUI.star.click(); +} + +function onPanelShown(aEvent) { + if (aEvent.target == StarUI.panel) { + StarUI.panel.removeEventListener("popupshown", arguments.callee, false); + let tagsField = document.getElementById("editBMPanel_tagsField"); + ok(tagsField.value == testTag, "tags field value was set"); + tagsField.focus(); + + StarUI.panel.addEventListener("popuphidden", onPanelHidden, false); + let removeButton = document.getElementById("editBookmarkPanelRemoveButton"); + removeButton.click(); + } +} + +/** + * Clears history invoking callback when done. + */ +function waitForClearHistory(aCallback) +{ + let observer = { + observe: function(aSubject, aTopic, aData) + { + Services.obs.removeObserver(this, PlacesUtils.TOPIC_EXPIRATION_FINISHED); + aCallback(aSubject, aTopic, aData); + } + }; + Services.obs.addObserver(observer, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); + PlacesUtils.bhistory.removeAllPages(); +} + +function onPanelHidden(aEvent) { + if (aEvent.target == StarUI.panel) { + StarUI.panel.removeEventListener("popuphidden", arguments.callee, false); + + executeSoon(function() { + ok(!PlacesUtils.bookmarks.isBookmarked(makeURI(testURL)), + "the bookmark for the test url has been removed"); + is(BookmarkingUI.status, BookmarkingUI.STATUS_UNSTARRED, + "star button indicates that the bookmark has been removed"); + gBrowser.removeCurrentTab(); + waitForClearHistory(finish); + }); + } +} diff --git a/browser/base/content/test/browser_bug581947.js b/browser/base/content/test/browser_bug581947.js new file mode 100644 index 000000000..458cb7473 --- /dev/null +++ b/browser/base/content/test/browser_bug581947.js @@ -0,0 +1,90 @@ +function check(aElementName, aBarred) { + let doc = gBrowser.contentDocument; + let tooltip = document.getElementById("aHTMLTooltip"); + let content = doc.getElementById('content'); + + let e = doc.createElement(aElementName); + content.appendChild(e); + + ok(!tooltip.fillInPageTooltip(e), + "No tooltip should be shown when the element is valid"); + + e.setCustomValidity('foo'); + if (aBarred) { + ok(!tooltip.fillInPageTooltip(e), + "No tooltip should be shown when the element is barred from constraint validation"); + } else { + ok(tooltip.fillInPageTooltip(e), + e.tagName + " " +"A tooltip should be shown when the element isn't valid"); + } + + e.setAttribute('title', ''); + ok (!tooltip.fillInPageTooltip(e), + "No tooltip should be shown if the title attribute is set"); + + e.removeAttribute('title'); + content.setAttribute('novalidate', ''); + ok (!tooltip.fillInPageTooltip(e), + "No tooltip should be shown if the novalidate attribute is set on the form owner"); + content.removeAttribute('novalidate'); + + content.removeChild(e); +} + +function todo_check(aElementName, aBarred) { + let doc = gBrowser.contentDocument; + let tooltip = document.getElementById("aHTMLTooltip"); + let content = doc.getElementById('content'); + + let e = doc.createElement(aElementName); + content.appendChild(e); + + let cought = false; + try { + e.setCustomValidity('foo'); + } catch (e) { + cought = true; + } + + todo(!cought, "setCustomValidity should exist for " + aElementName); + + content.removeChild(e); +} + +function test () { + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + let testData = [ + /* element name, barred */ + [ 'input', false ], + [ 'textarea', false ], + [ 'button', true ], + [ 'select', false ], + [ 'output', true ], + [ 'fieldset', true ], + [ 'object', true ], + ]; + + for each (let data in testData) { + check(data[0], data[1]); + } + + let todo_testData = [ + [ 'keygen', 'false' ], + ]; + + for each(let data in todo_testData) { + todo_check(data[0], data[1]); + } + + gBrowser.removeCurrentTab(); + finish(); + }, true); + + content.location = + "data:text/html,<!DOCTYPE html><html><body><form id='content'></form></body></html>"; +} + diff --git a/browser/base/content/test/browser_bug585558.js b/browser/base/content/test/browser_bug585558.js new file mode 100644 index 000000000..f2ed7fd94 --- /dev/null +++ b/browser/base/content/test/browser_bug585558.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +let tabs = []; + +function addTab(aURL) { + tabs.push(gBrowser.addTab(aURL, {skipAnimation: true})); +} + +function testAttrib(elem, attrib, attribValue, msg) { + is(elem.hasAttribute(attrib), attribValue, msg); +} + +function test() { + waitForExplicitFinish(); + is(gBrowser.tabs.length, 1, "one tab is open initially"); + + // Add several new tabs in sequence, hiding some, to ensure that the + // correct attributes get set + + addTab("http://mochi.test:8888/#0"); + addTab("http://mochi.test:8888/#1"); + addTab("http://mochi.test:8888/#2"); + addTab("http://mochi.test:8888/#3"); + + gBrowser.selectedTab = gBrowser.tabs[0]; + testAttrib(gBrowser.tabs[0], "first-visible-tab", true, + "First tab marked first-visible-tab!"); + testAttrib(gBrowser.tabs[4], "last-visible-tab", true, + "Fifth tab marked last-visible-tab!"); + testAttrib(gBrowser.tabs[0], "selected", true, "First tab marked selected!"); + testAttrib(gBrowser.tabs[0], "afterselected-visible", false, + "First tab not marked afterselected-visible!"); + testAttrib(gBrowser.tabs[1], "afterselected-visible", true, + "Second tab marked afterselected-visible!"); + gBrowser.hideTab(gBrowser.tabs[1]); + executeSoon(test_hideSecond); +} + +function test_hideSecond() { + testAttrib(gBrowser.tabs[2], "afterselected-visible", true, + "Third tab marked afterselected-visible!"); + gBrowser.showTab(gBrowser.tabs[1]) + executeSoon(test_showSecond); +} + +function test_showSecond() { + testAttrib(gBrowser.tabs[1], "afterselected-visible", true, + "Second tab marked afterselected-visible!"); + testAttrib(gBrowser.tabs[2], "afterselected-visible", false, + "Third tab not marked as afterselected-visible!"); + gBrowser.selectedTab = gBrowser.tabs[1]; + gBrowser.hideTab(gBrowser.tabs[0]); + executeSoon(test_hideFirst); +} + +function test_hideFirst() { + testAttrib(gBrowser.tabs[0], "first-visible-tab", false, + "Hidden first tab not marked first-visible-tab!"); + testAttrib(gBrowser.tabs[1], "first-visible-tab", true, + "Second tab marked first-visible-tab!"); + gBrowser.showTab(gBrowser.tabs[0]); + executeSoon(test_showFirst); +} + +function test_showFirst() { + testAttrib(gBrowser.tabs[0], "first-visible-tab", true, + "First tab marked first-visible-tab!"); + gBrowser.selectedTab = gBrowser.tabs[2]; + testAttrib(gBrowser.tabs[3], "afterselected-visible", true, + "Fourth tab marked afterselected-visible!"); + + gBrowser.moveTabTo(gBrowser.selectedTab, 1); + executeSoon(test_movedLower); +} + +function test_movedLower() { + testAttrib(gBrowser.tabs[2], "afterselected-visible", true, + "Third tab marked afterselected-visible!"); + test_hoverOne(); +} + +function test_hoverOne() { + EventUtils.synthesizeMouseAtCenter(gBrowser.tabs[4], { type: "mousemove" }); + testAttrib(gBrowser.tabs[3], "beforehovered", true, "Fourth tab marked beforehovered"); + EventUtils.synthesizeMouseAtCenter(gBrowser.tabs[3], { type: "mousemove" }); + testAttrib(gBrowser.tabs[2], "beforehovered", true, "Third tab marked beforehovered!"); + testAttrib(gBrowser.tabs[2], "afterhovered", false, "Third tab not marked afterhovered!"); + testAttrib(gBrowser.tabs[4], "afterhovered", true, "Fifth tab marked afterhovered!"); + testAttrib(gBrowser.tabs[4], "beforehovered", false, "Fifth tab not marked beforehovered!"); + testAttrib(gBrowser.tabs[0], "beforehovered", false, "First tab not marked beforehovered!"); + testAttrib(gBrowser.tabs[0], "afterhovered", false, "First tab not marked afterhovered!"); + testAttrib(gBrowser.tabs[1], "beforehovered", false, "Second tab not marked beforehovered!"); + testAttrib(gBrowser.tabs[1], "afterhovered", false, "Second tab not marked afterhovered!"); + testAttrib(gBrowser.tabs[3], "beforehovered", false, "Fourth tab not marked beforehovered!"); + testAttrib(gBrowser.tabs[3], "afterhovered", false, "Fourth tab not marked afterhovered!"); + gBrowser.removeTab(tabs.pop()); + executeSoon(test_hoverStatePersistence); +} + +function test_hoverStatePersistence() { + // Test that the afterhovered and beforehovered attributes are still there when + // a tab is selected and then unselected again. See bug 856107. + + function assertState() { + testAttrib(gBrowser.tabs[0], "beforehovered", true, "First tab still marked beforehovered!"); + testAttrib(gBrowser.tabs[0], "afterhovered", false, "First tab not marked afterhovered!"); + testAttrib(gBrowser.tabs[2], "afterhovered", true, "Third tab still marked afterhovered!"); + testAttrib(gBrowser.tabs[2], "beforehovered", false, "Third tab not marked afterhovered!"); + testAttrib(gBrowser.tabs[1], "beforehovered", false, "Second tab not marked beforehovered!"); + testAttrib(gBrowser.tabs[1], "afterhovered", false, "Second tab not marked afterhovered!"); + testAttrib(gBrowser.tabs[3], "beforehovered", false, "Fourth tab not marked beforehovered!"); + testAttrib(gBrowser.tabs[3], "afterhovered", false, "Fourth tab not marked afterhovered!"); + } + + gBrowser.selectedTab = gBrowser.tabs[3]; + EventUtils.synthesizeMouseAtCenter(gBrowser.tabs[1], { type: "mousemove" }); + assertState(); + gBrowser.selectedTab = gBrowser.tabs[1]; + assertState(); + gBrowser.selectedTab = gBrowser.tabs[3]; + assertState(); + executeSoon(test_pinning); +} + +function test_pinning() { + gBrowser.selectedTab = gBrowser.tabs[3]; + testAttrib(gBrowser.tabs[3], "last-visible-tab", true, + "Fourth tab marked last-visible-tab!"); + testAttrib(gBrowser.tabs[3], "selected", true, "Fourth tab marked selected!"); + testAttrib(gBrowser.tabs[3], "afterselected-visible", false, + "Fourth tab not marked afterselected-visible!"); + // Causes gBrowser.tabs to change indices + gBrowser.pinTab(gBrowser.tabs[3]); + testAttrib(gBrowser.tabs[3], "last-visible-tab", true, + "Fourth tab marked last-visible-tab!"); + testAttrib(gBrowser.tabs[1], "afterselected-visible", true, + "Second tab marked afterselected-visible!"); + testAttrib(gBrowser.tabs[0], "first-visible-tab", true, + "First tab marked first-visible-tab!"); + testAttrib(gBrowser.tabs[0], "selected", true, "First tab marked selected!"); + gBrowser.selectedTab = gBrowser.tabs[1]; + testAttrib(gBrowser.tabs[2], "afterselected-visible", true, + "Third tab marked afterselected-visible!"); + test_cleanUp(); +} + +function test_cleanUp() { + tabs.forEach(gBrowser.removeTab, gBrowser); + finish(); +} diff --git a/browser/base/content/test/browser_bug585785.js b/browser/base/content/test/browser_bug585785.js new file mode 100644 index 000000000..adbb5a47e --- /dev/null +++ b/browser/base/content/test/browser_bug585785.js @@ -0,0 +1,35 @@ +var tab; + +function test() { + waitForExplicitFinish(); + + tab = gBrowser.addTab(); + isnot(tab.getAttribute("fadein"), "true", "newly opened tab is yet to fade in"); + + // Try to remove the tab right before the opening animation's first frame + window.mozRequestAnimationFrame(checkAnimationState); +} + +function checkAnimationState() { + is(tab.getAttribute("fadein"), "true", "tab opening animation initiated"); + + info(window.getComputedStyle(tab).maxWidth); + gBrowser.removeTab(tab, { animate: true }); + if (!tab.parentNode) { + ok(true, "tab removed synchronously since the opening animation hasn't moved yet"); + finish(); + return; + } + + info("tab didn't close immediately, so the tab opening animation must have started moving"); + info("waiting for the tab to close asynchronously"); + tab.addEventListener("transitionend", function (event) { + if (event.propertyName == "max-width") { + tab.removeEventListener("transitionend", arguments.callee, false); + executeSoon(function () { + ok(!tab.parentNode, "tab removed asynchronously"); + finish(); + }); + } + }, false); +} diff --git a/browser/base/content/test/browser_bug585830.js b/browser/base/content/test/browser_bug585830.js new file mode 100644 index 000000000..bf99c2c90 --- /dev/null +++ b/browser/base/content/test/browser_bug585830.js @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + let tab1 = gBrowser.selectedTab; + let tab2 = gBrowser.addTab("about:blank", {skipAnimation: true}); + let tab3 = gBrowser.addTab(); + gBrowser.selectedTab = tab2; + + gBrowser.removeCurrentTab({animate: true}); + gBrowser.tabContainer.advanceSelectedTab(-1, true); + is(gBrowser.selectedTab, tab1, "First tab should be selected"); + gBrowser.removeTab(tab2); + + // test for "null has no properties" fix. See Bug 585830 Comment 13 + gBrowser.removeCurrentTab({animate: true}); + try { + gBrowser.tabContainer.advanceSelectedTab(-1, false); + } catch(err) { + ok(false, "Shouldn't throw"); + } + + gBrowser.removeTab(tab1); +} diff --git a/browser/base/content/test/browser_bug590206.js b/browser/base/content/test/browser_bug590206.js new file mode 100644 index 000000000..dc9e48ad8 --- /dev/null +++ b/browser/base/content/test/browser_bug590206.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const DUMMY = "browser/browser/base/content/test/dummy_page.html"; + +function loadNewTab(aURL, aCallback) { + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.loadURI(aURL); + + gBrowser.selectedBrowser.addEventListener("load", function() { + if (gBrowser.selectedBrowser.currentURI.spec != aURL) + return; + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + aCallback(gBrowser.selectedTab); + }, true); +} + +function getIdentityMode() { + return document.getElementById("identity-box").className; +} + +var TESTS = [ +function test_webpage() { + let oldTab = gBrowser.selectedTab; + + loadNewTab("http://example.com/" + DUMMY, function(aNewTab) { + is(getIdentityMode(), "unknownIdentity", "Identity should be unknown"); + + gBrowser.selectedTab = oldTab; + is(getIdentityMode(), "unknownIdentity", "Identity should be unknown"); + + gBrowser.selectedTab = aNewTab; + is(getIdentityMode(), "unknownIdentity", "Identity should be unknown"); + + gBrowser.removeTab(aNewTab); + + runNextTest(); + }); +}, + +function test_blank() { + let oldTab = gBrowser.selectedTab; + + loadNewTab("about:blank", function(aNewTab) { + is(getIdentityMode(), "unknownIdentity", "Identity should be unknown"); + + gBrowser.selectedTab = oldTab; + is(getIdentityMode(), "unknownIdentity", "Identity should be unknown"); + + gBrowser.selectedTab = aNewTab; + is(getIdentityMode(), "unknownIdentity", "Identity should be unknown"); + + gBrowser.removeTab(aNewTab); + + runNextTest(); + }); +}, + +function test_chrome() { + let oldTab = gBrowser.selectedTab; + + loadNewTab("chrome://mozapps/content/extensions/extensions.xul", function(aNewTab) { + is(getIdentityMode(), "chromeUI", "Identity should be chrome"); + + gBrowser.selectedTab = oldTab; + is(getIdentityMode(), "unknownIdentity", "Identity should be unknown"); + + gBrowser.selectedTab = aNewTab; + is(getIdentityMode(), "chromeUI", "Identity should be chrome"); + + gBrowser.removeTab(aNewTab); + + runNextTest(); + }); +}, + +function test_https() { + let oldTab = gBrowser.selectedTab; + + loadNewTab("https://example.com/" + DUMMY, function(aNewTab) { + is(getIdentityMode(), "verifiedDomain", "Identity should be verified"); + + gBrowser.selectedTab = oldTab; + is(getIdentityMode(), "unknownIdentity", "Identity should be unknown"); + + gBrowser.selectedTab = aNewTab; + is(getIdentityMode(), "verifiedDomain", "Identity should be verified"); + + gBrowser.removeTab(aNewTab); + + runNextTest(); + }); +}, + +function test_addons() { + let oldTab = gBrowser.selectedTab; + + loadNewTab("about:addons", function(aNewTab) { + is(getIdentityMode(), "chromeUI", "Identity should be chrome"); + + gBrowser.selectedTab = oldTab; + is(getIdentityMode(), "unknownIdentity", "Identity should be unknown"); + + gBrowser.selectedTab = aNewTab; + is(getIdentityMode(), "chromeUI", "Identity should be chrome"); + + gBrowser.removeTab(aNewTab); + + runNextTest(); + }); +} +]; + +var gTestStart = null; + +function runNextTest() { + if (gTestStart) + info("Test part took " + (Date.now() - gTestStart) + "ms"); + + if (TESTS.length == 0) { + finish(); + return; + } + + info("Running " + TESTS[0].name); + gTestStart = Date.now(); + TESTS.shift()(); +}; + +function test() { + waitForExplicitFinish(); + + runNextTest(); +} diff --git a/browser/base/content/test/browser_bug592338.js b/browser/base/content/test/browser_bug592338.js new file mode 100644 index 000000000..a9ec62566 --- /dev/null +++ b/browser/base/content/test/browser_bug592338.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TESTROOT = "http://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/"; + +var tempScope = {}; +Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", tempScope); +var LightweightThemeManager = tempScope.LightweightThemeManager; + +function wait_for_notification(aCallback) { + PopupNotifications.panel.addEventListener("popupshown", function() { + PopupNotifications.panel.removeEventListener("popupshown", arguments.callee, false); + aCallback(PopupNotifications.panel); + }, false); +} + +var TESTS = [ +function test_install_lwtheme() { + is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected"); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + gBrowser.selectedTab = gBrowser.addTab("http://example.com/browser/browser/base/content/test/bug592338.html"); + gBrowser.selectedBrowser.addEventListener("pageshow", function() { + if (gBrowser.contentDocument.location.href == "about:blank") + return; + + gBrowser.selectedBrowser.removeEventListener("pageshow", arguments.callee, false); + + executeSoon(function() { + var link = gBrowser.contentDocument.getElementById("theme-install"); + EventUtils.synthesizeMouse(link, 2, 2, {}, gBrowser.contentWindow); + + is(LightweightThemeManager.currentTheme.id, "test", "Should have installed the test theme"); + + LightweightThemeManager.currentTheme = null; + gBrowser.removeTab(gBrowser.selectedTab); + + Services.perms.remove("example.com", "install"); + + runNextTest(); + }); + }, false); +}, + +function test_lwtheme_switch_theme() { + is(LightweightThemeManager.currentTheme, null, "Should be no lightweight theme selected"); + + AddonManager.getAddonByID("theme-xpi@tests.mozilla.org", function(aAddon) { + aAddon.userDisabled = false; + ok(aAddon.isActive, "Theme should have immediately enabled"); + Services.prefs.setBoolPref("extensions.dss.enabled", false); + + var pm = Services.perms; + pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION); + + gBrowser.selectedTab = gBrowser.addTab("http://example.com/browser/browser/base/content/test/bug592338.html"); + gBrowser.selectedBrowser.addEventListener("pageshow", function() { + if (gBrowser.contentDocument.location.href == "about:blank") + return; + + gBrowser.selectedBrowser.removeEventListener("pageshow", arguments.callee, false); + + executeSoon(function() { + var link = gBrowser.contentDocument.getElementById("theme-install"); + wait_for_notification(function(aPanel) { + is(LightweightThemeManager.currentTheme, null, "Should not have installed the test lwtheme"); + ok(aAddon.isActive, "Test theme should still be active"); + + let notification = aPanel.childNodes[0]; + is(notification.button.label, "Restart Now", "Should have seen the right button"); + + ok(aAddon.userDisabled, "Should be waiting to disable the test theme"); + aAddon.userDisabled = false; + Services.prefs.setBoolPref("extensions.dss.enabled", true); + + gBrowser.removeTab(gBrowser.selectedTab); + + Services.perms.remove("example.com", "install"); + + runNextTest(); + }); + EventUtils.synthesizeMouse(link, 2, 2, {}, gBrowser.contentWindow); + }); + }, false); + }); +} +]; + +function runNextTest() { + AddonManager.getAllInstalls(function(aInstalls) { + is(aInstalls.length, 0, "Should be no active installs"); + + if (TESTS.length == 0) { + AddonManager.getAddonByID("theme-xpi@tests.mozilla.org", function(aAddon) { + aAddon.uninstall(); + + Services.prefs.setBoolPref("extensions.logging.enabled", false); + Services.prefs.setBoolPref("extensions.dss.enabled", false); + + finish(); + }); + return; + } + + info("Running " + TESTS[0].name); + TESTS.shift()(); + }); +}; + +function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref("extensions.logging.enabled", true); + + AddonManager.getInstallForURL(TESTROOT + "theme.xpi", function(aInstall) { + aInstall.addListener({ + onInstallEnded: function(aInstall, aAddon) { + AddonManager.getAddonByID("theme-xpi@tests.mozilla.org", function(aAddon) { + isnot(aAddon, null, "Should have installed the test theme."); + + // In order to switch themes while the test is running we turn on dynamic + // theme switching. This means the test isn't exactly correct but should + // do some good + Services.prefs.setBoolPref("extensions.dss.enabled", true); + + runNextTest(); + }); + } + }); + + aInstall.install(); + }, "application/x-xpinstall"); +} diff --git a/browser/base/content/test/browser_bug594131.js b/browser/base/content/test/browser_bug594131.js new file mode 100644 index 000000000..d68d1979a --- /dev/null +++ b/browser/base/content/test/browser_bug594131.js @@ -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/. */ + +function test() { + let newTab = gBrowser.addTab("http://example.com"); + waitForExplicitFinish(); + newTab.linkedBrowser.addEventListener("load", mainPart, true); + + function mainPart() { + newTab.linkedBrowser.removeEventListener("load", mainPart, true); + + gBrowser.pinTab(newTab); + gBrowser.selectedTab = newTab; + + openUILinkIn("http://example.org/", "current", { inBackground: true }); + isnot(gBrowser.selectedTab, newTab, "shouldn't load in background"); + + gBrowser.removeTab(newTab); + gBrowser.removeTab(gBrowser.tabs[1]); // example.org tab + finish(); + } +} diff --git a/browser/base/content/test/browser_bug595507.js b/browser/base/content/test/browser_bug595507.js new file mode 100644 index 000000000..65a10eace --- /dev/null +++ b/browser/base/content/test/browser_bug595507.js @@ -0,0 +1,39 @@ +var gInvalidFormPopup = document.getElementById('invalid-form-popup'); +ok(gInvalidFormPopup, + "The browser should have a popup to show when a form is invalid"); + +/** + * Make sure that the form validation error message shows even if the form is in an iframe. + */ +function test() +{ + waitForExplicitFinish(); + + let uri = "data:text/html,<iframe src=\"data:text/html,<iframe name='t'></iframe><form target='t' action='data:text/html,'><input required id='i'><input id='s' type='submit'></form>\"</iframe>"; + let tab = gBrowser.addTab(); + + gInvalidFormPopup.addEventListener("popupshown", function() { + gInvalidFormPopup.removeEventListener("popupshown", arguments.callee, false); + + let doc = gBrowser.contentDocument.getElementsByTagName('iframe')[0].contentDocument; + is(doc.activeElement, doc.getElementById('i'), + "First invalid element should be focused"); + + ok(gInvalidFormPopup.state == 'showing' || gInvalidFormPopup.state == 'open', + "The invalid form popup should be shown"); + + // Clean-up and next test. + gBrowser.removeTab(gBrowser.selectedTab, {animate: false}); + executeSoon(finish); + }, false); + + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + gBrowser.contentDocument.getElementsByTagName('iframe')[0].contentDocument + .getElementById('s').click(); + }, true); + + gBrowser.selectedTab = tab; + gBrowser.selectedTab.linkedBrowser.loadURI(uri); +} diff --git a/browser/base/content/test/browser_bug596687.js b/browser/base/content/test/browser_bug596687.js new file mode 100644 index 000000000..ccf6f5e35 --- /dev/null +++ b/browser/base/content/test/browser_bug596687.js @@ -0,0 +1,26 @@ +function test() { + var tab = gBrowser.addTab(null, {skipAnimation: true}); + gBrowser.selectedTab = tab; + + var gotTabAttrModified = false; + var gotTabClose = false; + + function onTabClose() { + gotTabClose = true; + tab.addEventListener("TabAttrModified", onTabAttrModified, false); + } + + function onTabAttrModified() { + gotTabAttrModified = true; + } + + tab.addEventListener("TabClose", onTabClose, false); + + gBrowser.removeTab(tab); + + ok(gotTabClose, "should have got the TabClose event"); + ok(!gotTabAttrModified, "shouldn't have got the TabAttrModified event after TabClose"); + + tab.removeEventListener("TabClose", onTabClose, false); + tab.removeEventListener("TabAttrModified", onTabAttrModified, false); +} diff --git a/browser/base/content/test/browser_bug597218.js b/browser/base/content/test/browser_bug597218.js new file mode 100644 index 000000000..f00e99f72 --- /dev/null +++ b/browser/base/content/test/browser_bug597218.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + // establish initial state + is(gBrowser.tabs.length, 1, "we start with one tab"); + + // create a tab + let tab = gBrowser.loadOneTab("about:blank"); + ok(!tab.hidden, "tab starts out not hidden"); + is(gBrowser.tabs.length, 2, "we now have two tabs"); + + // make sure .hidden is read-only + tab.hidden = true; + ok(!tab.hidden, "can't set .hidden directly"); + + // hide the tab + gBrowser.hideTab(tab); + ok(tab.hidden, "tab is hidden"); + + // now pin it and make sure it gets unhidden + gBrowser.pinTab(tab); + ok(tab.pinned, "tab was pinned"); + ok(!tab.hidden, "tab was unhidden"); + + // try hiding it now that it's pinned; shouldn't be able to + gBrowser.hideTab(tab); + ok(!tab.hidden, "tab did not hide"); + + // clean up + gBrowser.removeTab(tab); + is(gBrowser.tabs.length, 1, "we finish with one tab"); + + finish(); +} diff --git a/browser/base/content/test/browser_bug598923.js b/browser/base/content/test/browser_bug598923.js new file mode 100644 index 000000000..35e9c09f0 --- /dev/null +++ b/browser/base/content/test/browser_bug598923.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test: +// * if add-on is installed to the add-on bar, the bar is made visible. +// * if add-on is uninstalled from the add-on bar, and no more add-ons there, +// the bar is hidden. + +function test() { + let aml = AddonsMgrListener; + ok(aml, "AddonsMgrListener exists"); + // check is hidden + is(aml.addonBar.collapsed, true, "add-on bar is hidden initially"); + // aob gets the count + AddonsMgrListener.onInstalling(); + // add an item + let element = document.createElement("toolbaritem"); + element.id = "bug598923-addon-item"; + aml.addonBar.appendChild(element); + // aob checks the count, makes visible + AddonsMgrListener.onInstalled(); + // check is visible + is(aml.addonBar.collapsed, false, "add-on bar has been made visible"); + // aob gets the count + AddonsMgrListener.onUninstalling(); + // remove an item + aml.addonBar.removeChild(element); + // aob checks the count, makes hidden + AddonsMgrListener.onUninstalled(); + // check is hidden + is(aml.addonBar.collapsed, true, "add-on bar is hidden again"); +} diff --git a/browser/base/content/test/browser_bug599325.js b/browser/base/content/test/browser_bug599325.js new file mode 100644 index 000000000..d721fc663 --- /dev/null +++ b/browser/base/content/test/browser_bug599325.js @@ -0,0 +1,21 @@ +function test() { + waitForExplicitFinish(); + + let addonBar = document.getElementById("addon-bar"); + ok(addonBar, "got addon bar"); + ok(!isElementVisible(addonBar), "addon bar initially hidden"); + + openToolbarCustomizationUI(function () { + ok(isElementVisible(addonBar), + "add-on bar is visible during toolbar customization"); + + closeToolbarCustomizationUI(onClose); + }); + + function onClose() { + ok(!isElementVisible(addonBar), + "addon bar is hidden after toolbar customization"); + + finish(); + } +} diff --git a/browser/base/content/test/browser_bug609700.js b/browser/base/content/test/browser_bug609700.js new file mode 100644 index 000000000..8b4f1ea91 --- /dev/null +++ b/browser/base/content/test/browser_bug609700.js @@ -0,0 +1,20 @@ +function test() { + waitForExplicitFinish(); + + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + Services.ww.unregisterNotification(arguments.callee); + + ok(true, "duplicateTabIn opened a new window"); + + whenDelayedStartupFinished(aSubject, function () { + executeSoon(function () { + aSubject.close(); + finish(); + }); + }, false); + } + }); + + duplicateTabIn(gBrowser.selectedTab, "window"); +} diff --git a/browser/base/content/test/browser_bug616836.js b/browser/base/content/test/browser_bug616836.js new file mode 100644 index 000000000..efaaa837a --- /dev/null +++ b/browser/base/content/test/browser_bug616836.js @@ -0,0 +1,4 @@ +function test() { + is(document.querySelectorAll("#appmenu-popup [accesskey]").length, 0, + "there should be no items with access keys in the app menu popup"); +} diff --git a/browser/base/content/test/browser_bug623155.js b/browser/base/content/test/browser_bug623155.js new file mode 100644 index 000000000..52ee73b07 --- /dev/null +++ b/browser/base/content/test/browser_bug623155.js @@ -0,0 +1,136 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const REDIRECT_FROM = "https://example.com/browser/browser/base/content/test/" + + "redirect_bug623155.sjs"; + +const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host. + +function isRedirectedURISpec(aURISpec) { + return isRedirectedURI(Services.io.newURI(aURISpec, null, null)); +} + +function isRedirectedURI(aURI) { + // Compare only their before-hash portion. + return Services.io.newURI(REDIRECT_TO, null, null) + .equalsExceptRef(aURI); +} + +/* + Test. + +1. Load +https://example.com/browser/browser/base/content/test/redirect_bug623155.sjs#BG + in a background tab. + +2. The redirected URI is <https://www.bank1.com/#BG>, which displayes a cert + error page. + +3. Switch the tab to foreground. + +4. Check the URLbar's value, expecting <https://www.bank1.com/#BG> + +5. Load +https://example.com/browser/browser/base/content/test/redirect_bug623155.sjs#FG + in the foreground tab. + +6. The redirected URI is <https://www.bank1.com/#FG>. And this is also + a cert-error page. + +7. Check the URLbar's value, expecting <https://www.bank1.com/#FG> + +8. End. + + */ + +var gNewTab; + +function test() { + waitForExplicitFinish(); + + // Load a URI in the background. + gNewTab = gBrowser.addTab(REDIRECT_FROM + "#BG"); + gBrowser.getBrowserForTab(gNewTab) + .webProgress + .addProgressListener(gWebProgressListener, + Components.interfaces.nsIWebProgress + .NOTIFY_LOCATION); +} + +var gWebProgressListener = { + QueryInterface: function(aIID) { + if (aIID.equals(Components.interfaces.nsIWebProgressListener) || + aIID.equals(Components.interfaces.nsISupportsWeakReference) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + // --------------------------------------------------------------------------- + // NOTIFY_LOCATION mode should work fine without these methods. + // + //onStateChange: function() {}, + //onStatusChange: function() {}, + //onProgressChange: function() {}, + //onSecurityChange: function() {}, + //---------------------------------------------------------------------------- + + onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) { + if (!aRequest) { + // This is bug 673752, or maybe initial "about:blank". + return; + } + + ok(gNewTab, "There is a new tab."); + ok(isRedirectedURI(aLocation), + "onLocationChange catches only redirected URI."); + + if (aLocation.ref == "BG") { + // This is background tab's request. + isnot(gNewTab, gBrowser.selectedTab, "This is a background tab."); + } else if (aLocation.ref == "FG") { + // This is foreground tab's request. + is(gNewTab, gBrowser.selectedTab, "This is a foreground tab."); + } + else { + // We shonuld not reach here. + ok(false, "This URI hash is not expected:" + aLocation.ref); + } + + let isSelectedTab = gNewTab.selected; + setTimeout(delayed, 0, isSelectedTab); + } +}; + +function delayed(aIsSelectedTab) { + // Switch tab and confirm URL bar. + if (!aIsSelectedTab) { + gBrowser.selectedTab = gNewTab; + } + + ok(isRedirectedURISpec(content.location.href), + "The content area is redirected. aIsSelectedTab:" + aIsSelectedTab); + is(gURLBar.value, content.location.href, + "The URL bar shows the content URI. aIsSelectedTab:" + aIsSelectedTab); + + if (!aIsSelectedTab) { + // If this was a background request, go on a foreground request. + content.location = REDIRECT_FROM + "#FG"; + } + else { + // Othrewise, nothing to do remains. + finish(); + } +} + +/* Cleanup */ +registerCleanupFunction(function() { + if (gNewTab) { + gBrowser.getBrowserForTab(gNewTab) + .webProgress + .removeProgressListener(gWebProgressListener); + + gBrowser.removeTab(gNewTab); + } + gNewTab = null; +}); diff --git a/browser/base/content/test/browser_bug623893.js b/browser/base/content/test/browser_bug623893.js new file mode 100644 index 000000000..800b8fad5 --- /dev/null +++ b/browser/base/content/test/browser_bug623893.js @@ -0,0 +1,46 @@ +function test() { + waitForExplicitFinish(); + + loadAndWait("data:text/plain,1", function () { + loadAndWait("data:text/plain,2", function () { + loadAndWait("data:text/plain,3", runTests); + }); + }); +} + +function runTests() { + duplicate(0, "maintained the original index", function () { + gBrowser.removeCurrentTab(); + + duplicate(-1, "went back", function () { + duplicate(1, "went forward", function () { + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); + gBrowser.addTab(); + gBrowser.removeCurrentTab(); + finish(); + }); + }); + }); +} + +function duplicate(delta, msg, cb) { + var start = gBrowser.sessionHistory.index; + + duplicateTabIn(gBrowser.selectedTab, "tab", delta); + + gBrowser.selectedBrowser.addEventListener("pageshow", function () { + gBrowser.selectedBrowser.removeEventListener("pageshow", arguments.callee, false); + is(gBrowser.sessionHistory.index, start + delta, msg); + executeSoon(cb); + }, false); +} + +function loadAndWait(url, cb) { + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + executeSoon(cb); + }, true); + + gBrowser.loadURI(url); +} diff --git a/browser/base/content/test/browser_bug624734.js b/browser/base/content/test/browser_bug624734.js new file mode 100644 index 000000000..13369a310 --- /dev/null +++ b/browser/base/content/test/browser_bug624734.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Bug 624734 - Star UI has no tooltip until bookmarked page is visited + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + tab.linkedBrowser.addEventListener("load", (function(event) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + is(BookmarkingUI.star.getAttribute("tooltiptext"), + BookmarkingUI._unstarredTooltip, + "Star icon should have the unstarred tooltip text"); + + gBrowser.removeCurrentTab(); + finish(); + }), true); + + tab.linkedBrowser.loadURI("http://example.com/browser/browser/base/content/test/dummy_page.html"); +} diff --git a/browser/base/content/test/browser_bug647886.js b/browser/base/content/test/browser_bug647886.js new file mode 100644 index 000000000..4a41fc6ad --- /dev/null +++ b/browser/base/content/test/browser_bug647886.js @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + content.history.pushState({}, "2", "2.html"); + + testBackButton(); + }, true); + + loadURI("http://example.com"); +} + +function testBackButton() { + var backButton = document.getElementById("back-button"); + var rect = backButton.getBoundingClientRect(); + + info("waiting for the history menu to open"); + + backButton.addEventListener("popupshown", function (event) { + backButton.removeEventListener("popupshown", arguments.callee, false); + + ok(true, "history menu opened"); + event.target.hidePopup(); + gBrowser.removeTab(gBrowser.selectedTab); + finish(); + }, false); + + EventUtils.synthesizeMouseAtCenter(backButton, {type: "mousedown"}); + EventUtils.synthesizeMouse(backButton, rect.width / 2, rect.height, {type: "mouseup"}); +} diff --git a/browser/base/content/test/browser_bug655584.js b/browser/base/content/test/browser_bug655584.js new file mode 100644 index 000000000..2fb1b4b43 --- /dev/null +++ b/browser/base/content/test/browser_bug655584.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Bug 655584 - awesomebar suggestions don't update after tab is closed + +function test() { + var tab1 = gBrowser.addTab(); + var tab2 = gBrowser.addTab(); + + // When urlbar in a new tab is focused, and a tab switch occurs, + // the urlbar popup should be closed + gBrowser.selectedTab = tab2; + gURLBar.focus(); // focus the urlbar in the tab we will switch to + gBrowser.selectedTab = tab1; + gURLBar.openPopup(); + gBrowser.selectedTab = tab2; + ok(!gURLBar.popupOpen, "urlbar focused in tab to switch to, close popup"); + + // cleanup + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +} diff --git a/browser/base/content/test/browser_bug664672.js b/browser/base/content/test/browser_bug664672.js new file mode 100644 index 000000000..2064f77d0 --- /dev/null +++ b/browser/base/content/test/browser_bug664672.js @@ -0,0 +1,19 @@ +function test() { + waitForExplicitFinish(); + + var tab = gBrowser.addTab(); + + tab.addEventListener("TabClose", function () { + tab.removeEventListener("TabClose", arguments.callee, false); + + ok(tab.linkedBrowser, "linkedBrowser should still exist during the TabClose event"); + + executeSoon(function () { + ok(!tab.linkedBrowser, "linkedBrowser should be gone after the TabClose event"); + + finish(); + }); + }, false); + + gBrowser.removeTab(tab); +} diff --git a/browser/base/content/test/browser_bug676619.js b/browser/base/content/test/browser_bug676619.js new file mode 100644 index 000000000..23dd66e87 --- /dev/null +++ b/browser/base/content/test/browser_bug676619.js @@ -0,0 +1,121 @@ +function test () { + waitForExplicitFinish(); + + var isHTTPS = false; + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + if (isHTTPS) { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + } + let doc = gBrowser.contentDocument; + + + function testLocation(link, url, next) { + var tabOpenListener = new TabOpenListener(url, function () { + gBrowser.removeTab(this.tab); + }, function () { + next(); + }); + + doc.getElementById(link).click(); + } + + function testLink(link, name, next) { + addWindowListener("chrome://mozapps/content/downloads/unknownContentType.xul", function (win) { + is(doc.getElementById("unload-flag").textContent, "Okay", "beforeunload shouldn't have fired"); + is(win.document.getElementById("location").value, name, "file name should match"); + win.close(); + next(); + }); + + doc.getElementById(link).click(); + } + + testLink("link1", "test.txt", + testLink.bind(null, "link2", "video.ogg", + testLink.bind(null, "link3", "just some video", + testLink.bind(null, "link4", "with-target.txt", + testLink.bind(null, "link5", "javascript.txt", + testLink.bind(null, "link6", "test.blob", + testLocation.bind(null, "link7", "http://example.com/", + function () { + if (isHTTPS) { + gBrowser.removeCurrentTab(); + finish(); + } else { + // same test again with https: + isHTTPS = true; + content.location = "https://example.com:443/browser/browser/base/content/test/download_page.html"; + } + }))))))); + + }, true); + + content.location = "http://mochi.test:8888/browser/browser/base/content/test/download_page.html"; +} + + +function addWindowListener(aURL, aCallback) { + Services.wm.addListener({ + onOpenWindow: function(aXULWindow) { + info("window opened, waiting for focus"); + Services.wm.removeListener(this); + + var domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + waitForFocus(function() { + is(domwindow.document.location.href, aURL, "should have seen the right window open"); + aCallback(domwindow); + }, domwindow); + }, + onCloseWindow: function(aXULWindow) { }, + onWindowTitleChange: function(aXULWindow, aNewTitle) { } + }); +} + +// This listens for the next opened tab and checks it is of the right url. +// opencallback is called when the new tab is fully loaded +// closecallback is called when the tab is closed +function TabOpenListener(url, opencallback, closecallback) { + this.url = url; + this.opencallback = opencallback; + this.closecallback = closecallback; + + gBrowser.tabContainer.addEventListener("TabOpen", this, false); +} + +TabOpenListener.prototype = { + url: null, + opencallback: null, + closecallback: null, + tab: null, + browser: null, + + handleEvent: function(event) { + if (event.type == "TabOpen") { + gBrowser.tabContainer.removeEventListener("TabOpen", this, false); + this.tab = event.originalTarget; + this.browser = this.tab.linkedBrowser; + gBrowser.addEventListener("pageshow", this, false); + } else if (event.type == "pageshow") { + if (event.target.location.href != this.url) + return; + gBrowser.removeEventListener("pageshow", this, false); + this.tab.addEventListener("TabClose", this, false); + var url = this.browser.contentDocument.location.href; + is(url, this.url, "Should have opened the correct tab"); + this.opencallback(this.tab, this.browser.contentWindow); + } else if (event.type == "TabClose") { + if (event.originalTarget != this.tab) + return; + this.tab.removeEventListener("TabClose", this, false); + this.opencallback = null; + this.tab = null; + this.browser = null; + // Let the window close complete + executeSoon(this.closecallback); + this.closecallback = null; + } + } +}; diff --git a/browser/base/content/test/browser_bug678392-1.html b/browser/base/content/test/browser_bug678392-1.html new file mode 100644 index 000000000..c3b235dd0 --- /dev/null +++ b/browser/base/content/test/browser_bug678392-1.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Strict//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html> + <head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + <title>bug678392 - 1</title> + </head> + <body> +bug 678392 test page 1 + </body> +</html>
\ No newline at end of file diff --git a/browser/base/content/test/browser_bug678392-2.html b/browser/base/content/test/browser_bug678392-2.html new file mode 100644 index 000000000..9b18efcf7 --- /dev/null +++ b/browser/base/content/test/browser_bug678392-2.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC"-//W3C//DTD XHTML 1.0 Strict//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html> + <head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + <title>bug678392 - 2</title> + </head> + <body> +bug 678392 test page 2 + </body> +</html>
\ No newline at end of file diff --git a/browser/base/content/test/browser_bug678392.js b/browser/base/content/test/browser_bug678392.js new file mode 100644 index 000000000..3b670dc4a --- /dev/null +++ b/browser/base/content/test/browser_bug678392.js @@ -0,0 +1,192 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 HTTPROOT = "http://example.com/browser/browser/base/content/test/"; + +function maxSnapshotOverride() { + return 5; +} + +function test() { + waitForExplicitFinish(); + + BrowserOpenTab(); + let tab = gBrowser.selectedTab; + registerCleanupFunction(function () { gBrowser.removeTab(tab); }); + + ok(gHistorySwipeAnimation, "gHistorySwipeAnimation exists."); + + if (!gHistorySwipeAnimation._isSupported()) { + is(gHistorySwipeAnimation.active, false, "History swipe animation is not " + + "active when not supported by the platform."); + finish(); + return; + } + + gHistorySwipeAnimation._getMaxSnapshots = maxSnapshotOverride; + gHistorySwipeAnimation.init(); + + is(gHistorySwipeAnimation.active, true, "History swipe animation support " + + "was successfully initialized when supported."); + + cleanupArray(); + load(gBrowser.selectedTab, HTTPROOT + "browser_bug678392-2.html", test0); +} + +function load(aTab, aUrl, aCallback) { + aTab.linkedBrowser.addEventListener("load", function onload(aEvent) { + aEvent.currentTarget.removeEventListener("load", onload, true); + waitForFocus(aCallback, content); + }, true); + aTab.linkedBrowser.loadURI(aUrl); +} + +function cleanupArray() { + let arr = gHistorySwipeAnimation._trackedSnapshots; + while (arr.length > 0) { + delete arr[0].browser.snapshots[arr[0].index]; // delete actual snapshot + arr.splice(0, 1); + } +} + +function testArrayCleanup() { + // Test cleanup of array of tracked snapshots. + let arr = gHistorySwipeAnimation._trackedSnapshots; + is(arr.length, 0, "Snapshots were removed correctly from the array of " + + "tracked snapshots."); +} + +function test0() { + // Test growing of array of tracked snapshots. + let tab = gBrowser.selectedTab; + + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + ok(gHistorySwipeAnimation._trackedSnapshots, "Array for snapshot " + + "tracking is initialized."); + is(gHistorySwipeAnimation._trackedSnapshots.length, 1, "Snapshot array " + + "has correct length of 1 after loading one page."); + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 2, "Snapshot array " + + " has correct length of 2 after loading two pages."); + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 3, "Snapshot " + + "array has correct length of 3 after loading three pages."); + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 4, "Snapshot " + + "array has correct length of 4 after loading four pages."); + cleanupArray(); + testArrayCleanup(); + test1(); + }); + }); + }); + }); +} + +function verifyRefRemoved(aIndex, aBrowser) { + let wasFound = false; + let arr = gHistorySwipeAnimation._trackedSnapshots; + for (let i = 0; i < arr.length; i++) { + if (arr[i].index == aIndex && arr[i].browser == aBrowser) + wasFound = true; + } + is(wasFound, false, "The reference that was previously removed was " + + "still found in the array of tracked snapshots."); +} + +function test1() { + // Test presence of snpashots in per-tab array of snapshots and removal of + // individual snapshots (and corresponding references in the array of + // tracked snapshots). + let tab = gBrowser.selectedTab; + + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + var historyIndex = gBrowser.webNavigation.sessionHistory.index - 1; + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + let browser = gBrowser.selectedBrowser; + ok(browser.snapshots, "Array of snapshots exists in browser."); + ok(browser.snapshots[historyIndex], "First page exists in snapshot " + + "array."); + ok(browser.snapshots[historyIndex + 1], "Second page exists in " + + "snapshot array."); + ok(browser.snapshots[historyIndex + 2], "Third page exists in " + + "snapshot array."); + ok(browser.snapshots[historyIndex + 3], "Fourth page exists in " + + "snapshot array."); + is(gHistorySwipeAnimation._trackedSnapshots.length, 4, "Length of " + + "array of tracked snapshots is equal to 4 after loading four " + + "pages."); + + // Test removal of reference in the middle of the array. + gHistorySwipeAnimation._removeTrackedSnapshot(historyIndex + 1, + browser); + verifyRefRemoved(historyIndex + 1, browser); + is(gHistorySwipeAnimation._trackedSnapshots.length, 3, "Length of " + + "array of tracked snapshots is equal to 3 after removing one" + + "reference from the array with length 4."); + + // Test removal of reference at end of array. + gHistorySwipeAnimation._removeTrackedSnapshot(historyIndex + 3, + browser); + verifyRefRemoved(historyIndex + 3, browser); + is(gHistorySwipeAnimation._trackedSnapshots.length, 2, "Length of " + + "array of tracked snapshots is equal to 2 after removing two" + + "references from the array with length 4."); + + // Test removal of reference at head of array. + gHistorySwipeAnimation._removeTrackedSnapshot(historyIndex, + browser); + verifyRefRemoved(historyIndex, browser); + is(gHistorySwipeAnimation._trackedSnapshots.length, 1, "Length of " + + "array of tracked snapshots is equal to 1 after removing three" + + "references from the array with length 4."); + + cleanupArray(); + test2(); + }); + }); + }); + }); +} + +function test2() { + // Test growing of snapshot array across tabs. + let tab = gBrowser.selectedTab; + + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + var historyIndex = gBrowser.webNavigation.sessionHistory.index - 1; + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 2, "Length of " + + "snapshot array is equal to 2 after loading two pages"); + let prevTab = tab; + tab = gBrowser.addTab("about:newtab"); + gBrowser.selectedTab = tab; + load(tab, HTTPROOT + "browser_bug678392-2.html" /* initial page */, + function() { + load(tab, HTTPROOT + "browser_bug678392-1.html", function() { + load(tab, HTTPROOT + "browser_bug678392-2.html", function() { + is(gHistorySwipeAnimation._trackedSnapshots.length, 4, "Length " + + "of snapshot array is equal to 4 after loading two pages in " + + "two tabs each."); + gBrowser.removeCurrentTab(); + gBrowser.selectedTab = prevTab; + cleanupArray(); + test3(); + }); + }); + }); + }); + }); +} + +function test3() { + // Test uninit of gHistorySwipeAnimation. + // This test MUST be the last one to execute. + gHistorySwipeAnimation.uninit(); + is(gHistorySwipeAnimation.active, false, "History swipe animation support " + + "was successfully uninitialized"); + finish(); +} diff --git a/browser/base/content/test/browser_bug710878.js b/browser/base/content/test/browser_bug710878.js new file mode 100644 index 000000000..aaec82787 --- /dev/null +++ b/browser/base/content/test/browser_bug710878.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() +{ + waitForExplicitFinish(); + + let doc; + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + doc = content.document; + waitForFocus(performTest, content); + }, true); + + content.location = "data:text/html,<a href='%23xxx'><span>word1 <span> word2 </span></span><span> word3</span></a>"; + + function performTest() + { + let link = doc.querySelector("a");; + let text = gatherTextUnder(link); + is(text, "word1 word2 word3", "Text under link is correctly computed."); + doc = null; + gBrowser.removeCurrentTab(); + finish(); + } +} + diff --git a/browser/base/content/test/browser_bug719271.js b/browser/base/content/test/browser_bug719271.js new file mode 100644 index 000000000..1c0076ac1 --- /dev/null +++ b/browser/base/content/test/browser_bug719271.js @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"; + +const TEST_PAGE = "http://example.org/browser/browser/base/content/test/zoom_test.html"; +const TEST_VIDEO = "http://example.org/browser/browser/base/content/test/video.ogg"; + +var gTab1, gTab2, gLevel1, gLevel2; + +function test() { + waitForExplicitFinish(); + + Task.spawn(function () { + gTab1 = gBrowser.addTab(); + gTab2 = gBrowser.addTab(); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + yield FullZoomHelper.load(gTab1, TEST_PAGE); + yield FullZoomHelper.load(gTab2, TEST_VIDEO); + }).then(zoomTab1, FullZoomHelper.failAndContinue(finish)); +} + +function zoomTab1() { + Task.spawn(function () { + is(gBrowser.selectedTab, gTab1, "Tab 1 is selected"); + FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1"); + + FullZoom.enlarge(); + gLevel1 = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab1)); + + ok(gLevel1 > 1, "New zoom for tab 1 should be greater than 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Zooming tab 1 should not affect tab 2"); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoomHelper.zoomTest(gTab2, 1, "Tab 2 is still unzoomed after it is selected"); + FullZoomHelper.zoomTest(gTab1, gLevel1, "Tab 1 is still zoomed"); + }).then(zoomTab2, FullZoomHelper.failAndContinue(finish)); +} + +function zoomTab2() { + Task.spawn(function () { + is(gBrowser.selectedTab, gTab2, "Tab 2 is selected"); + + FullZoom.reduce(); + let gLevel2 = ZoomManager.getZoomForBrowser(gBrowser.getBrowserForTab(gTab2)); + + ok(gLevel2 < 1, "New zoom for tab 2 should be less than 1"); + FullZoomHelper.zoomTest(gTab1, gLevel1, "Zooming tab 2 should not affect tab 1"); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + FullZoomHelper.zoomTest(gTab1, gLevel1, "Tab 1 should have the same zoom after it's selected"); + }).then(testNavigation, FullZoomHelper.failAndContinue(finish)); +} + +function testNavigation() { + Task.spawn(function () { + yield FullZoomHelper.load(gTab1, TEST_VIDEO); + FullZoomHelper.zoomTest(gTab1, 1, "Zoom should be 1 when a video was loaded"); + yield waitForNextTurn(); // trying to fix orange bug 806046 + yield FullZoomHelper.navigate(FullZoomHelper.BACK); + FullZoomHelper.zoomTest(gTab1, gLevel1, "Zoom should be restored when a page is loaded"); + yield waitForNextTurn(); // trying to fix orange bug 806046 + yield FullZoomHelper.navigate(FullZoomHelper.FORWARD); + FullZoomHelper.zoomTest(gTab1, 1, "Zoom should be 1 again when navigating back to a video"); + }).then(finishTest, FullZoomHelper.failAndContinue(finish)); +} + +function waitForNextTurn() { + let deferred = Promise.defer(); + setTimeout(function () deferred.resolve(), 0); + return deferred.promise; +} + +var finishTestStarted = false; +function finishTest() { + Task.spawn(function () { + ok(!finishTestStarted, "finishTest called more than once"); + finishTestStarted = true; + + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + FullZoom.reset(); + yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab1); + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoom.reset(); + yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab2); + }).then(finish, FullZoomHelper.failAndContinue(finish)); +} diff --git a/browser/base/content/test/browser_bug724239.js b/browser/base/content/test/browser_bug724239.js new file mode 100644 index 000000000..766002eca --- /dev/null +++ b/browser/base/content/test/browser_bug724239.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + BrowserOpenTab(); + + let tab = gBrowser.selectedTab; + let browser = tab.linkedBrowser; + + registerCleanupFunction(function () { gBrowser.removeTab(tab); }); + + whenBrowserLoaded(browser, function () { + browser.loadURI("http://example.com/"); + + whenBrowserLoaded(browser, function () { + ok(!gBrowser.canGoBack, "about:newtab wasn't added to the session history"); + finish(); + }); + }); +} + +function whenBrowserLoaded(aBrowser, aCallback) { + if (aBrowser.contentDocument.readyState == "complete") { + executeSoon(aCallback); + return; + } + + aBrowser.addEventListener("load", function onLoad() { + aBrowser.removeEventListener("load", onLoad, true); + executeSoon(aCallback); + }, true); +} diff --git a/browser/base/content/test/browser_bug734076.js b/browser/base/content/test/browser_bug734076.js new file mode 100644 index 000000000..3e1cae716 --- /dev/null +++ b/browser/base/content/test/browser_bug734076.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + }); + + let browser = tab.linkedBrowser; + browser.stop(); // stop the about:blank load + + let writeDomainURL = encodeURI("data:text/html,<script>document.write(document.domain);</script>"); + let tests = [ + { + name: "view background image", + url: "http://mochi.test:8888/", + go: function (cb) { + let contentBody = browser.contentDocument.body; + contentBody.style.backgroundImage = "url('" + writeDomainURL + "')"; + doOnLoad(function () { + let domain = browser.contentDocument.body.textContent; + is(domain, "", "no domain was inherited for view background image"); + cb(); + }); + + let contextMenu = initContextMenu(contentBody); + contextMenu.viewBGImage(); + } + }, + { + name: "view image", + url: "http://mochi.test:8888/", + go: function (cb) { + doOnLoad(function () { + let domain = browser.contentDocument.body.textContent; + is(domain, "", "no domain was inherited for view image"); + cb(); + }); + + let doc = browser.contentDocument; + let img = doc.createElement("img"); + img.setAttribute("src", writeDomainURL); + doc.body.appendChild(img); + + let contextMenu = initContextMenu(img); + contextMenu.viewMedia(); + } + }, + { + name: "show only this frame", + url: "http://mochi.test:8888/", + go: function (cb) { + doOnLoad(function () { + let domain = browser.contentDocument.body.textContent; + is(domain, "", "no domain was inherited for 'show only this frame'"); + cb(); + }); + + let doc = browser.contentDocument; + let iframe = doc.createElement("iframe"); + iframe.setAttribute("src", writeDomainURL); + doc.body.appendChild(iframe); + + iframe.addEventListener("load", function onload() { + let contextMenu = initContextMenu(iframe.contentDocument.body); + contextMenu.showOnlyThisFrame(); + }, false); + } + } + ]; + + function doOnLoad(cb) { + browser.addEventListener("load", function onLoad(e) { + if (e.target != browser.contentDocument) + return; + browser.removeEventListener("load", onLoad, true); + cb(); + }, true); + } + + function doNext() { + let test = tests.shift(); + if (test) { + info("Running test: " + test.name); + doOnLoad(function () { + test.go(function () { + executeSoon(doNext); + }); + }); + browser.contentDocument.location = test.url; + } else { + executeSoon(finish); + } + } + + doNext(); +} + +function initContextMenu(aNode) { + document.popupNode = aNode; + let contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + let contextMenu = new nsContextMenu(contentAreaContextMenu); + return contextMenu; +} diff --git a/browser/base/content/test/browser_bug735471.js b/browser/base/content/test/browser_bug735471.js new file mode 100644 index 000000000..eb9e7d338 --- /dev/null +++ b/browser/base/content/test/browser_bug735471.js @@ -0,0 +1,59 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + // Reset pref to its default + Services.prefs.clearUserPref("browser.preferences.inContent"); + }); + + // Verify that about:preferences tab is displayed when + // browser.preferences.inContent is set to true + Services.prefs.setBoolPref("browser.preferences.inContent", true); + + gBrowser.tabContainer.addEventListener("TabOpen", function(aEvent) { + + gBrowser.tabContainer.removeEventListener("TabOpen", arguments.callee, true); + let browser = aEvent.originalTarget.linkedBrowser; + browser.addEventListener("load", function(aEvent) { + browser.removeEventListener("load", arguments.callee, true); + + is(Services.prefs.getBoolPref("browser.preferences.inContent"), true, "In-content prefs are enabled"); + is(browser.contentWindow.location.href, "about:preferences", "Checking if the preferences tab was opened"); + + gBrowser.removeCurrentTab(); + Services.prefs.setBoolPref("browser.preferences.inContent", false); + openPreferences(); + + }, true); + }, true); + + + let observer = { + observe: function(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + windowWatcher.unregisterNotification(observer); + + let win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow); + win.addEventListener("load", function() { + win.removeEventListener("load", arguments.callee, false); + is(Services.prefs.getBoolPref("browser.preferences.inContent"), false, "In-content prefs are disabled"); + is(win.location.href, "chrome://browser/content/preferences/preferences.xul", "Checking if the preferences window was opened"); + win.close(); + finish(); + }, false); + } + } + } + + var windowWatcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIWindowWatcher); + windowWatcher.registerNotification(observer); + + openPreferences(); +} diff --git a/browser/base/content/test/browser_bug743421.js b/browser/base/content/test/browser_bug743421.js new file mode 100644 index 000000000..38437589d --- /dev/null +++ b/browser/base/content/test/browser_bug743421.js @@ -0,0 +1,118 @@ +const gTestRoot = "http://mochi.test:8888/browser/browser/base/content/test/"; + +var gTestBrowser = null; +var gNextTest = null; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + clearAllPluginPermissions(); + Services.prefs.clearUserPref("plugins.click_to_play"); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + Services.prefs.setBoolPref("plugins.click_to_play", true); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + prepareTest(test1a, gTestRoot + "plugin_add_dynamically.html"); +} + +function finishTest() { + gTestBrowser.removeEventListener("load", pageLoad, true); + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} + +function pageLoad() { + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + executeSoon(gNextTest); +} + +function prepareTest(nextTest, url) { + gNextTest = nextTest; + gTestBrowser.contentWindow.location = url; +} + +// Tests that navigation within the page and the window.history API doesn't break click-to-play state. +function test1a() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(!popupNotification, "Test 1a, Should not have a click-to-play notification"); + var plugin = new XPCNativeWrapper(XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addPlugin()); + + var condition = function() PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + waitForCondition(condition, test1b, "Test 1a, Waited too long for plugin notification"); +} + +function test1b() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 1b, Should have a click-to-play notification"); + var plugin = gTestBrowser.contentDocument.getElementsByTagName("embed")[0]; + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 1b, Plugin should not be activated"); + + // Click the activate button on doorhanger to make sure it works + popupNotification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + ok(objLoadingContent.activated, "Test 1b, Doorhanger should activate plugin"); + + test1c(); +} + +function test1c() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 1c, Should still have a click-to-play notification"); + var plugin = new XPCNativeWrapper(XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addPlugin()); + + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test1d, "Test 1c, Waited too long for plugin activation"); +} + +function test1d() { + var plugin = gTestBrowser.contentDocument.getElementsByTagName("embed")[1]; + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 1d, Plugin should be activated"); + + gNextTest = test1e; + gTestBrowser.contentWindow.addEventListener("hashchange", test1e, false); + gTestBrowser.contentWindow.location += "#anchorNavigation"; +} + +function test1e() { + gTestBrowser.contentWindow.removeEventListener("hashchange", test1e, false); + + var plugin = new XPCNativeWrapper(XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addPlugin()); + + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test1f, "Test 1e, Waited too long for plugin activation"); +} + +function test1f() { + var plugin = gTestBrowser.contentDocument.getElementsByTagName("embed")[2]; + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 1f, Plugin should be activated"); + + gTestBrowser.contentWindow.history.replaceState({}, "", "replacedState"); + var plugin = new XPCNativeWrapper(XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addPlugin()); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test1g, "Test 1f, Waited too long for plugin activation"); +} + +function test1g() { + var plugin = gTestBrowser.contentDocument.getElementsByTagName("embed")[3]; + var objLoadingContent2 = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent2.activated, "Test 1g, Plugin should be activated"); + finishTest(); +} diff --git a/browser/base/content/test/browser_bug744745.js b/browser/base/content/test/browser_bug744745.js new file mode 100644 index 000000000..916480c50 --- /dev/null +++ b/browser/base/content/test/browser_bug744745.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 gTestBrowser = null; +var gNumPluginBindingsAttached = 0; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("plugins.click_to_play"); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + gTestBrowser.removeEventListener("PluginBindingAttached", pluginBindingAttached, true, true); + gBrowser.removeCurrentTab(); + window.focus(); + }); + + Services.prefs.setBoolPref("plugins.click_to_play", true); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + gBrowser.selectedTab = gBrowser.addTab(); + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("PluginBindingAttached", pluginBindingAttached, true, true); + var gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + gTestBrowser.contentWindow.location = gHttpTestRoot + "plugin_bug744745.html"; +} + +function pluginBindingAttached() { + gNumPluginBindingsAttached++; + + if (gNumPluginBindingsAttached == 1) { + var doc = gTestBrowser.contentDocument; + var testplugin = doc.getElementById("test"); + ok(testplugin, "should have test plugin"); + var style = getComputedStyle(testplugin); + ok('opacity' in style, "style should have opacity set"); + is(style.opacity, 1, "opacity should be 1"); + finish(); + } else { + ok(false, "if we've gotten here, something is quite wrong"); + } +} diff --git a/browser/base/content/test/browser_bug749738.js b/browser/base/content/test/browser_bug749738.js new file mode 100644 index 000000000..2372e7cb3 --- /dev/null +++ b/browser/base/content/test/browser_bug749738.js @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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"; + +const DUMMY_PAGE = "http://example.org/browser/browser/base/content/test/dummy_page.html"; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + + load(tab, DUMMY_PAGE, function() { + gFindBar.onFindCommand(); + EventUtils.sendString("Dummy"); + gBrowser.removeTab(tab); + + try { + gFindBar.close(); + ok(true, "findbar.close should not throw an exception"); + } catch(e) { + ok(false, "findbar.close threw exception: " + e); + } + finish(); + }); +} + +function load(aTab, aUrl, aCallback) { + aTab.linkedBrowser.addEventListener("load", function onload(aEvent) { + aEvent.currentTarget.removeEventListener("load", onload, true); + waitForFocus(aCallback, content); + }, true); + aTab.linkedBrowser.loadURI(aUrl); +} diff --git a/browser/base/content/test/browser_bug752516.js b/browser/base/content/test/browser_bug752516.js new file mode 100644 index 000000000..8cd69767c --- /dev/null +++ b/browser/base/content/test/browser_bug752516.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); + +var gTestBrowser = null; + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("plugins.click_to_play"); + let plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + gBrowser.removeCurrentTab(); + window.focus(); + }); + + Services.prefs.setBoolPref("plugins.click_to_play", true); + let plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + gBrowser.selectedTab = gBrowser.addTab(); + gTestBrowser = gBrowser.selectedBrowser; + let gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + gTestBrowser.contentWindow.location = gHttpTestRoot + "plugin_bug752516.html"; + + gTestBrowser.addEventListener("load", tabLoad, true); +} + +function tabLoad() { + // Due to layout being async, "PluginBindAttached" may trigger later. + // This forces a layout flush, thus triggering it, and schedules the + // test so it is definitely executed afterwards. + gTestBrowser.contentDocument.getElementById('test').clientTop; + executeSoon(actualTest); +} + +function actualTest() { + let doc = gTestBrowser.contentDocument; + let plugin = doc.getElementById("test"); + ok(!plugin.activated, "Plugin should not be activated"); + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed, "Doorhanger should not be open"); + + EventUtils.synthesizeMouseAtCenter(plugin, {}, gTestBrowser.contentWindow); + let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed; + waitForCondition(condition, finish, "Waited too long for plugin doorhanger to activate"); +} diff --git a/browser/base/content/test/browser_bug763468_perwindowpb.js b/browser/base/content/test/browser_bug763468_perwindowpb.js new file mode 100644 index 000000000..bdd6943d9 --- /dev/null +++ b/browser/base/content/test/browser_bug763468_perwindowpb.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This test makes sure that opening a new tab in private browsing mode opens about:privatebrowsing +function test() { + // initialization + waitForExplicitFinish(); + let windowsToClose = []; + let newTab; + let newTabPrefName = "browser.newtab.url"; + let newTabURL; + let mode; + + function doTest(aIsPrivateMode, aWindow, aCallback) { + whenNewTabLoaded(aWindow, function () { + if (aIsPrivateMode) { + mode = "per window private browsing"; + newTabURL = "about:privatebrowsing"; + } else { + mode = "normal"; + newTabURL = Services.prefs.getCharPref(newTabPrefName) || "about:blank"; + } + + is(aWindow.gBrowser.currentURI.spec, newTabURL, + "URL of NewTab should be " + newTabURL + " in " + mode + " mode"); + + aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab); + aCallback() + }); + }; + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function(aWin) { + windowsToClose.push(aWin); + // execute should only be called when need, like when you are opening + // web pages on the test. If calling executeSoon() is not necesary, then + // call whenNewWindowLoaded() instead of testOnWindow() on your test. + executeSoon(function() aCallback(aWin)); + }); + }; + + // this function is called after calling finish() on the test. + registerCleanupFunction(function() { + windowsToClose.forEach(function(aWin) { + aWin.close(); + }); + }); + + // test first when not on private mode + testOnWindow({}, function(aWin) { + doTest(false, aWin, function() { + // then test when on private mode + testOnWindow({private: true}, function(aWin) { + doTest(true, aWin, function() { + // then test again when not on private mode + testOnWindow({}, function(aWin) { + doTest(false, aWin, finish); + }); + }); + }); + }); + }); +} diff --git a/browser/base/content/test/browser_bug767836_perwindowpb.js b/browser/base/content/test/browser_bug767836_perwindowpb.js new file mode 100644 index 000000000..aef355e61 --- /dev/null +++ b/browser/base/content/test/browser_bug767836_perwindowpb.js @@ -0,0 +1,83 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + //initialization + waitForExplicitFinish(); + let newTabPrefName = "browser.newtab.url"; + let newTabURL; + let testURL = "http://example.com/"; + let mode; + + function doTest(aIsPrivateMode, aWindow, aCallback) { + openNewTab(aWindow, function () { + if (aIsPrivateMode) { + mode = "per window private browsing"; + newTabURL = "about:privatebrowsing"; + } else { + mode = "normal"; + newTabURL = Services.prefs.getCharPref(newTabPrefName) || "about:blank"; + } + + // Check the new tab opened while in normal/private mode + is(aWindow.gBrowser.selectedBrowser.currentURI.spec, newTabURL, + "URL of NewTab should be " + newTabURL + " in " + mode + " mode"); + // Set the custom newtab url + Services.prefs.setCharPref(newTabPrefName, testURL); + ok(Services.prefs.prefHasUserValue(newTabPrefName), "Custom newtab url is set"); + + // Open a newtab after setting the custom newtab url + openNewTab(aWindow, function () { + is(aWindow.gBrowser.selectedBrowser.currentURI.spec, testURL, + "URL of NewTab should be the custom url"); + + // clear the custom url preference + Services.prefs.clearUserPref(newTabPrefName); + ok(!Services.prefs.prefHasUserValue(newTabPrefName), "No custom newtab url is set"); + + aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab); + aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab); + aWindow.close(); + aCallback() + }); + }); + } + + function testOnWindow(aIsPrivate, aCallback) { + whenNewWindowLoaded({private: aIsPrivate}, function(win) { + executeSoon(function() aCallback(win)); + }); + } + + // check whether any custom new tab url has been configured + ok(!Services.prefs.prefHasUserValue(newTabPrefName), "No custom newtab url is set"); + + // test normal mode + testOnWindow(false, function(aWindow) { + doTest(false, aWindow, function() { + // test private mode + testOnWindow(true, function(aWindow) { + doTest(true, aWindow, function() { + finish(); + }); + }); + }); + }); +} + +function openNewTab(aWindow, aCallback) { + // Open a new tab + aWindow.BrowserOpenTab(); + + let browser = aWindow.gBrowser.selectedBrowser; + if (browser.contentDocument.readyState == "complete") { + executeSoon(aCallback); + return; + } + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + executeSoon(aCallback); + }, true); +}
\ No newline at end of file diff --git a/browser/base/content/test/browser_bug771331.js b/browser/base/content/test/browser_bug771331.js new file mode 100644 index 000000000..3b1ab552c --- /dev/null +++ b/browser/base/content/test/browser_bug771331.js @@ -0,0 +1,82 @@ +const HTML_NS = "http://www.w3.org/1999/xhtml"; + +const INPUT_ID = "input1"; +const FORM1_ID = "form1"; +const FORM2_ID = "form2"; +const CHANGE_INPUT_ID = "input2"; + +function test() { + waitForExplicitFinish(); + let tab = gBrowser.selectedTab = + gBrowser.addTab("data:text/html;charset=utf-8," + + "<html><body>" + + "<form id='" + FORM1_ID + "'><input id='" + CHANGE_INPUT_ID + "'></form>" + + "<form id='" + FORM2_ID + "'></form>" + + "</body></html>"); + tab.linkedBrowser.addEventListener("load", tabLoad, true); +} + +function unexpectedContentEvent(evt) { + ok(false, "Received a " + evt.type + " event on content"); +} + +var gDoc = null; + +function tabLoad() { + let tab = gBrowser.selectedTab; + tab.linkedBrowser.removeEventListener("load", tabLoad, true); + gDoc = gBrowser.selectedBrowser.contentDocument; + // These events shouldn't escape to content. + gDoc.addEventListener("DOMFormHasPassword", unexpectedContentEvent, false); + gDoc.defaultView.setTimeout(test_inputAdd, 0); +} + +function test_inputAdd() { + gBrowser.addEventListener("DOMFormHasPassword", test_inputAddHandler, false); + let input = gDoc.createElementNS(HTML_NS, "input"); + input.setAttribute("type", "password"); + input.setAttribute("id", INPUT_ID); + input.setAttribute("data-test", "unique-attribute"); + gDoc.getElementById(FORM1_ID).appendChild(input); + info("Done appending the input element"); +} + +function test_inputAddHandler(evt) { + gBrowser.removeEventListener(evt.type, test_inputAddHandler, false); + is(evt.target.id, FORM1_ID, + evt.type + " event targets correct form element (added password element)"); + gDoc.defaultView.setTimeout(test_inputChangeForm, 0); +} + +function test_inputChangeForm() { + gBrowser.addEventListener("DOMFormHasPassword", test_inputChangeFormHandler, false); + let input = gDoc.getElementById(INPUT_ID); + input.setAttribute("form", FORM2_ID); +} + +function test_inputChangeFormHandler(evt) { + gBrowser.removeEventListener(evt.type, test_inputChangeFormHandler, false); + is(evt.target.id, FORM2_ID, + evt.type + " event targets correct form element (changed form)"); + gDoc.defaultView.setTimeout(test_inputChangesType, 0); +} + +function test_inputChangesType() { + gBrowser.addEventListener("DOMFormHasPassword", test_inputChangesTypeHandler, false); + let input = gDoc.getElementById(CHANGE_INPUT_ID); + input.setAttribute("type", "password"); +} + +function test_inputChangesTypeHandler(evt) { + gBrowser.removeEventListener(evt.type, test_inputChangesTypeHandler, false); + is(evt.target.id, FORM1_ID, + evt.type + " event targets correct form element (changed type)"); + gDoc.defaultView.setTimeout(completeTest, 0); +} + +function completeTest() { + ok(true, "Test completed"); + gDoc.removeEventListener("DOMFormHasPassword", unexpectedContentEvent, false); + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/browser/base/content/test/browser_bug783614.js b/browser/base/content/test/browser_bug783614.js new file mode 100644 index 000000000..ebc62e8fa --- /dev/null +++ b/browser/base/content/test/browser_bug783614.js @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + gURLBar.focus(); + gURLBar.inputField.value = "https://example.com/"; + gURLBar.selectionStart = 4; + gURLBar.selectionEnd = 5; + goDoCommand("cmd_cut"); + is(gURLBar.inputField.value, "http://example.com/", "location bar value after cutting 's' from https"); + gURLBar.handleRevert(); +} diff --git a/browser/base/content/test/browser_bug787619.js b/browser/base/content/test/browser_bug787619.js new file mode 100644 index 000000000..1917100eb --- /dev/null +++ b/browser/base/content/test/browser_bug787619.js @@ -0,0 +1,52 @@ +const gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + +let gTestBrowser = null; +let gWrapperClickCount = 0; + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("plugins.click_to_play"); + let plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + Services.prefs.setBoolPref("plugins.click_to_play", true); + let plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + gBrowser.selectedTab = gBrowser.addTab(); + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + gTestBrowser.contentWindow.location = gHttpTestRoot + "plugin_bug787619.html"; +} + +function pageLoad() { + // Due to layout being async, "PluginBindAttached" may trigger later. + // This forces a layout flush, thus triggering it, and schedules the + // test so it is definitely executed afterwards. + gTestBrowser.contentDocument.getElementById('plugin').clientTop; + executeSoon(part1); +} + +function part1() { + let wrapper = gTestBrowser.contentDocument.getElementById('wrapper'); + wrapper.addEventListener('click', function() ++gWrapperClickCount, false); + + let plugin = gTestBrowser.contentDocument.getElementById('plugin'); + ok(plugin, 'got plugin element'); + ok(!plugin.activated, 'plugin should not be activated'); + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed, "Doorhanger should not be open"); + + EventUtils.synthesizeMouseAtCenter(plugin, {}, gTestBrowser.contentWindow); + let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed; + waitForCondition(condition, part2, + 'waited too long for plugin to activate'); +} + +function part2() { + is(gWrapperClickCount, 0, 'wrapper should not have received any clicks'); + gTestBrowser.removeEventListener("load", pageLoad, true); + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} diff --git a/browser/base/content/test/browser_bug797677.js b/browser/base/content/test/browser_bug797677.js new file mode 100644 index 000000000..ff9637306 --- /dev/null +++ b/browser/base/content/test/browser_bug797677.js @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 rootDir = getRootDirectory(gTestPath); +const gHttpTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); +const Cc = Components.classes; +const Ci = Components.interfaces; +var gTestBrowser = null; +var gConsoleErrors = 0; + +function test() { + waitForExplicitFinish(); + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("PluginBindingAttached", pluginBindingAttached, true, true); + var consoleService = Cc["@mozilla.org/consoleservice;1"] + .getService(Ci.nsIConsoleService); + var errorListener = { + observe: function(aMessage) { + if (aMessage.message.contains("NS_ERROR")) + gConsoleErrors++; + } + }; + consoleService.registerListener(errorListener); + registerCleanupFunction(function() { + gTestBrowser.removeEventListener("PluginBindingAttached", pluginBindingAttached, true); + consoleService.unregisterListener(errorListener); + gBrowser.removeCurrentTab(); + window.focus(); + }); + gTestBrowser.contentWindow.location = gHttpTestRoot + "plugin_bug797677.html"; +} + +function pluginBindingAttached() { + // Let browser-plugins.js handle the PluginNotFound event, then run the test + executeSoon(runTest); +} + +function runTest() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("plugin"); + ok(plugin, "plugin should be in the page"); + is(gConsoleErrors, 0, "should have no console errors"); + finish(); +} diff --git a/browser/base/content/test/browser_bug812562.js b/browser/base/content/test/browser_bug812562.js new file mode 100644 index 000000000..677558cbe --- /dev/null +++ b/browser/base/content/test/browser_bug812562.js @@ -0,0 +1,96 @@ +var gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); +var gTestBrowser = null; +var gNextTest = null; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("plugins.click_to_play"); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + Services.prefs.setBoolPref("plugins.click_to_play", true); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml", + function() { + prepareTest(function() { + // Due to layout being async, "PluginBindAttached" may trigger later. + // This forces a layout flush, thus triggering it, and schedules the + // test so it is definitely executed afterwards. + gTestBrowser.contentDocument.getElementById('test').clientTop; + testPart1(); + }, + gHttpTestRoot + "plugin_test.html"); + }); +} + +function finishTest() { + gTestBrowser.removeEventListener("load", pageLoad, true); + gBrowser.removeCurrentTab(); + window.focus(); + setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml", + function() { + resetBlocklist(); + finish(); + }); +} + +function pageLoad(aEvent) { + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + if (gNextTest != null) + executeSoon(gNextTest); +} + +function prepareTest(nextTest, url) { + gNextTest = nextTest; + gTestBrowser.contentWindow.location = url; +} + +// Tests that the going back will reshow the notification for click-to-play +// blocklisted plugins (part 1/4) +function testPart1() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "test part 1: Should have a click-to-play notification"); + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "test part 1: plugin fallback type should be PLUGIN_VULNERABLE_UPDATABLE"); + ok(!objLoadingContent.activated, "test part 1: plugin should not be activated"); + + prepareTest(testPart2, "about:blank"); +} + +function testPart2() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(!popupNotification, "test part 2: Should not have a click-to-play notification"); + var plugin = gTestBrowser.contentDocument.getElementById("test"); + ok(!plugin, "test part 2: Should not have a plugin in this page"); + + Services.obs.addObserver(testPart3, "PopupNotifications-updateNotShowing", false); + gTestBrowser.contentWindow.history.back(); +} + +function testPart3() { + Services.obs.removeObserver(testPart3, "PopupNotifications-updateNotShowing"); + var condition = function() PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + waitForCondition(condition, testPart4, "test part 3: waited too long for click-to-play-plugin notification"); +} + +function testPart4() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "test part 4: Should have a click-to-play notification"); + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "test part 4: plugin fallback type should be PLUGIN_VULNERABLE_UPDATABLE"); + ok(!objLoadingContent.activated, "test part 4: plugin should not be activated"); + + finishTest(); +} diff --git a/browser/base/content/test/browser_bug816527.js b/browser/base/content/test/browser_bug816527.js new file mode 100644 index 000000000..30a4090f2 --- /dev/null +++ b/browser/base/content/test/browser_bug816527.js @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let testURL = "http://example.org/browser/browser/base/content/test/dummy_page.html"; + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function(aWin) { + // execute should only be called when need, like when you are opening + // web pages on the test. If calling executeSoon() is not necesary, then + // call whenNewWindowLoaded() instead of testOnWindow() on your test. + executeSoon(function() aCallback(aWin)); + }); + }; + + testOnWindow({}, function(aNormalWindow) { + testOnWindow({private: true}, function(aPrivateWindow) { + runTest(aNormalWindow, aPrivateWindow, false, function() { + aNormalWindow.close(); + aPrivateWindow.close(); + testOnWindow({}, function(aNormalWindow) { + testOnWindow({private: true}, function(aPrivateWindow) { + runTest(aPrivateWindow, aNormalWindow, false, function() { + aNormalWindow.close(); + aPrivateWindow.close(); + testOnWindow({private: true}, function(aPrivateWindow) { + runTest(aPrivateWindow, aPrivateWindow, false, function() { + aPrivateWindow.close(); + testOnWindow({}, function(aNormalWindow) { + runTest(aNormalWindow, aNormalWindow, true, function() { + aNormalWindow.close(); + finish(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + function runTest(aSourceWindow, aDestWindow, aExpectSuccess, aCallback) { + // Open the base tab + let baseTab = aSourceWindow.gBrowser.addTab(testURL); + baseTab.linkedBrowser.addEventListener("load", function() { + // Wait for the tab to be fully loaded so matching happens correctly + if (baseTab.linkedBrowser.currentURI.spec == "about:blank") + return; + baseTab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + let testTab = aDestWindow.gBrowser.addTab(); + + waitForFocus(function() { + // Select the testTab + aDestWindow.gBrowser.selectedTab = testTab; + + // Ensure that this tab has no history entries + ok(testTab.linkedBrowser.sessionHistory.count < 2, + "The test tab has 1 or less history entries"); + // Ensure that this tab is on about:blank + is(testTab.linkedBrowser.currentURI.spec, "about:blank", + "The test tab is on about:blank"); + // Ensure that this tab's document has no child nodes + ok(!testTab.linkedBrowser.contentDocument.body.hasChildNodes(), + "The test tab has no child nodes"); + ok(!testTab.hasAttribute("busy"), + "The test tab doesn't have the busy attribute"); + + // Set the urlbar to include the moz-action + aDestWindow.gURLBar.value = "moz-action:switchtab," + testURL; + // Focus the urlbar so we can press enter + aDestWindow.gURLBar.focus(); + + // We want to see if the switchtab action works. If it does, the + // current tab will get closed, and that's what we detect with the + // TabClose handler. If pressing enter triggers a load in that tab, + // then the load handler will get called. Neither of these are + // the desired effect here. So if the test goes successfully, it is + // the timeout handler which gets called. + // + // The reason that we can't avoid the timeout here is because we are + // trying to test something which should not happen, so we just need + // to wait for a while and then check whether any bad things have + // happened. + + function onTabClose(aEvent) { + aDestWindow.gBrowser.tabContainer.removeEventListener("TabClose", onTabClose, false); + aDestWindow.gBrowser.removeEventListener("load", onLoad, false); + clearTimeout(timeout); + // Should only happen when we expect success + ok(aExpectSuccess, "Tab closed as expected"); + aCallback(); + } + function onLoad(aEvent) { + aDestWindow.gBrowser.tabContainer.removeEventListener("TabClose", onTabClose, false); + aDestWindow.gBrowser.removeEventListener("load", onLoad, false); + clearTimeout(timeout); + // Should only happen when we expect success + ok(aExpectSuccess, "Tab loaded as expected"); + aCallback(); + } + + aDestWindow.gBrowser.tabContainer.addEventListener("TabClose", onTabClose, false); + aDestWindow.gBrowser.addEventListener("load", onLoad, false); + let timeout = setTimeout(function() { + aDestWindow.gBrowser.tabContainer.removeEventListener("TabClose", onTabClose, false); + aDestWindow.gBrowser.removeEventListener("load", onLoad, false); + aCallback(); + }, 500); + + // Press enter! + EventUtils.synthesizeKey("VK_RETURN", {}); + }, aDestWindow); + }, true); + } +} + diff --git a/browser/base/content/test/browser_bug817947.js b/browser/base/content/test/browser_bug817947.js new file mode 100644 index 000000000..061cd8d18 --- /dev/null +++ b/browser/base/content/test/browser_bug817947.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore); + +const URL = "http://mochi.test:8888/browser/"; +const PREF = "browser.sessionstore.restore_on_demand"; + +function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref(PREF, true); + registerCleanupFunction(function () { + Services.prefs.clearUserPref(PREF); + }); + + preparePendingTab(function (aTab) { + let win = gBrowser.replaceTabWithWindow(aTab); + + whenDelayedStartupFinished(win, function () { + let [tab] = win.gBrowser.tabs; + + whenLoaded(tab.linkedBrowser, function () { + is(tab.linkedBrowser.currentURI.spec, URL, "correct url should be loaded"); + ok(!tab.hasAttribute("pending"), "tab should not be pending"); + + win.close(); + finish(); + }); + }); + }); +} + +function preparePendingTab(aCallback) { + let tab = gBrowser.addTab(URL); + + whenLoaded(tab.linkedBrowser, function () { + let state = ss.getTabState(tab); + gBrowser.removeTab(tab); + + tab = gBrowser.addTab("about:blank"); + whenLoaded(tab.linkedBrowser, function () { + ss.setTabState(tab, state); + ok(tab.hasAttribute("pending"), "tab should be pending"); + aCallback(tab); + }); + }); +} + +function whenLoaded(aElement, aCallback) { + aElement.addEventListener("load", function onLoad() { + aElement.removeEventListener("load", onLoad, true); + executeSoon(aCallback); + }, true); +} diff --git a/browser/base/content/test/browser_bug818118.js b/browser/base/content/test/browser_bug818118.js new file mode 100644 index 000000000..3b781333f --- /dev/null +++ b/browser/base/content/test/browser_bug818118.js @@ -0,0 +1,48 @@ +var gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); +var gTestBrowser = null; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("plugins.click_to_play"); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + gTestBrowser.removeEventListener("load", pageLoad, true); + }); + + Services.prefs.setBoolPref("plugins.click_to_play", true); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + gBrowser.selectedTab = gBrowser.addTab(); + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + gTestBrowser.contentWindow.location = gHttpTestRoot + "plugin_both.html"; +} + +function pageLoad(aEvent) { + // Due to layout being async, "PluginBindAttached" may trigger later. + // This forces a layout flush, thus triggering it, and schedules the + // test so it is definitely executed afterwards. + gTestBrowser.contentDocument.getElementById('test').clientTop; + executeSoon(actualTest); +} + +function actualTest() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "should have a click-to-play notification"); + var plugin = gTestBrowser.contentDocument.getElementById("test"); + ok(plugin, "should have known plugin in page"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "plugin fallback type should be PLUGIN_CLICK_TO_PLAY"); + ok(!objLoadingContent.activated, "plugin should not be activated"); + + var unknown = gTestBrowser.contentDocument.getElementById("unknown"); + ok(unknown, "should have unknown plugin in page"); + + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} diff --git a/browser/base/content/test/browser_bug820497.js b/browser/base/content/test/browser_bug820497.js new file mode 100644 index 000000000..f6cc2e702 --- /dev/null +++ b/browser/base/content/test/browser_bug820497.js @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 gTestBrowser = null; +var gNumPluginBindingsAttached = 0; + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("plugins.click_to_play"); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED; + gTestBrowser.removeEventListener("PluginBindingAttached", pluginBindingAttached, true, true); + gBrowser.removeCurrentTab(); + window.focus(); + }); + + Services.prefs.setBoolPref("plugins.click_to_play", true); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + gBrowser.selectedTab = gBrowser.addTab(); + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("PluginBindingAttached", pluginBindingAttached, true, true); + var gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + gTestBrowser.contentWindow.location = gHttpTestRoot + "plugin_bug820497.html"; +} + +function pluginBindingAttached() { + gNumPluginBindingsAttached++; + + if (gNumPluginBindingsAttached == 1) { + var doc = gTestBrowser.contentDocument; + var testplugin = doc.getElementById("test"); + ok(testplugin, "should have test plugin"); + var secondtestplugin = doc.getElementById("secondtest"); + ok(!secondtestplugin, "should not yet have second test plugin"); + var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "should have popup notification"); + // We don't set up the action list until the notification is shown + notification.reshow(); + is(notification.options.centerActions.length, 1, "should be 1 type of plugin in the popup notification"); + XPCNativeWrapper.unwrap(gTestBrowser.contentWindow).addSecondPlugin(); + } else if (gNumPluginBindingsAttached == 2) { + var doc = gTestBrowser.contentDocument; + var testplugin = doc.getElementById("test"); + ok(testplugin, "should have test plugin"); + var secondtestplugin = doc.getElementById("secondtest"); + ok(secondtestplugin, "should have second test plugin"); + var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "should have popup notification"); + notification.reshow(); + is(notification.options.centerActions.length, 2, "should be 2 types of plugin in the popup notification"); + finish(); + } else { + ok(false, "if we've gotten here, something is quite wrong"); + } +} diff --git a/browser/base/content/test/browser_bug822367.js b/browser/base/content/test/browser_bug822367.js new file mode 100644 index 000000000..5ce497919 --- /dev/null +++ b/browser/base/content/test/browser_bug822367.js @@ -0,0 +1,196 @@ +/* + * User Override Mixed Content Block - Tests for Bug 822367 + */ + + +const PREF_DISPLAY = "security.mixed_content.block_display_content"; +const PREF_ACTIVE = "security.mixed_content.block_active_content"; + +// We alternate for even and odd test cases to simulate different hosts +const gHttpTestRoot = "https://example.com/browser/browser/base/content/test/"; +const gHttpTestRoot2 = "https://test1.example.com/browser/browser/base/content/test/"; + +var origBlockDisplay; +var origBlockActive; +var gTestBrowser = null; + +registerCleanupFunction(function() { + // Set preferences back to their original values + Services.prefs.setBoolPref(PREF_DISPLAY, origBlockDisplay); + Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive); +}); + +function MixedTestsCompleted() { + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} + +function test() { + waitForExplicitFinish(); + + origBlockDisplay = Services.prefs.getBoolPref(PREF_DISPLAY); + origBlockActive = Services.prefs.getBoolPref(PREF_ACTIVE); + + Services.prefs.setBoolPref(PREF_DISPLAY, true); + Services.prefs.setBoolPref(PREF_ACTIVE, true); + + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + newTab.linkedBrowser.stop() + + // Mixed Script Test + gTestBrowser.addEventListener("load", MixedTest1A, true); + var url = gHttpTestRoot + "file_bug822367_1.html"; + gTestBrowser.contentWindow.location = url; +} + +// Mixed Script Test +function MixedTest1A() { + gTestBrowser.removeEventListener("load", MixedTest1A, true); + gTestBrowser.addEventListener("load", MixedTest1B, true); + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(notification, "Mixed Content Doorhanger didn't appear"); + notification.secondaryActions[0].callback(); +} +function MixedTest1B() { + waitForCondition(function() content.document.getElementById('p1').innerHTML == "hello", MixedTest1C, "Waited too long for mixed script to run in Test 1"); +} +function MixedTest1C() { + ok(content.document.getElementById('p1').innerHTML == "hello","Mixed script didn't load in Test 1"); + gTestBrowser.removeEventListener("load", MixedTest1B, true); + MixedTest2(); +} + +//Mixed Display Test - Doorhanger should not appear +function MixedTest2() { + gTestBrowser.addEventListener("load", MixedTest2A, true); + var url = gHttpTestRoot2 + "file_bug822367_2.html"; + gTestBrowser.contentWindow.location = url; +} + +function MixedTest2A() { + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(!notification, "Mixed Content Doorhanger appears for mixed display content!"); + MixedTest3(); +} + +// Mixed Script and Display Test - User Override should cause both the script and the image to load. +function MixedTest3() { + gTestBrowser.removeEventListener("load", MixedTest2A, true); + gTestBrowser.addEventListener("load", MixedTest3A, true); + var url = gHttpTestRoot + "file_bug822367_3.html"; + gTestBrowser.contentWindow.location = url; +} +function MixedTest3A() { + gTestBrowser.removeEventListener("load", MixedTest3A, true); + gTestBrowser.addEventListener("load", MixedTest3B, true); + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(notification, "Mixed Content Doorhanger doesn't appear for test 3"); + notification.secondaryActions[0].callback(); +} +function MixedTest3B() { + waitForCondition(function() content.document.getElementById('p1').innerHTML == "hello", MixedTest3C, "Waited too long for mixed script to run in Test 3"); +} +function MixedTest3C() { + waitForCondition(function() content.document.getElementById('p2').innerHTML == "bye", MixedTest3D, "Waited too long for mixed image to load in Test 3"); +} +function MixedTest3D() { + ok(content.document.getElementById('p1').innerHTML == "hello","Mixed script didn't load in Test 3"); + ok(content.document.getElementById('p2').innerHTML == "bye","Mixed image didn't load in Test 3"); + MixedTest4(); +} + +// Location change - User override on one page doesn't propogate to another page after location change. +function MixedTest4() { + gTestBrowser.removeEventListener("load", MixedTest3B, true); + gTestBrowser.addEventListener("load", MixedTest4A, true); + var url = gHttpTestRoot2 + "file_bug822367_4.html"; + gTestBrowser.contentWindow.location = url; +} +function MixedTest4A() { + gTestBrowser.removeEventListener("load", MixedTest4A, true); + gTestBrowser.addEventListener("load", MixedTest4B, true); + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(notification, "Mixed Content Doorhanger doesn't appear for Test 4"); + notification.secondaryActions[0].callback(); +} +function MixedTest4B() { + waitForCondition(function() content.document.location == gHttpTestRoot + "file_bug822367_4B.html", MixedTest4C, "Waited too long for mixed script to run in Test 4"); +} +function MixedTest4C() { + ok(content.document.location == gHttpTestRoot + "file_bug822367_4B.html", "Location didn't change in test 4"); + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(notification, "Mixed Content Doorhanger doesn't appear after location change in Test 4"); + waitForCondition(function() content.document.getElementById('p1').innerHTML == "", MixedTest4D, "Mixed script loaded in test 4 after location change!"); +} +function MixedTest4D() { + ok(content.document.getElementById('p1').innerHTML == "","p1.innerHTML changed; mixed script loaded after location change in Test 4"); + MixedTest5(); +} + +// Mixed script attempts to load in a document.open() +function MixedTest5() { + gTestBrowser.removeEventListener("load", MixedTest4B, true); + gTestBrowser.addEventListener("load", MixedTest5A, true); + var url = gHttpTestRoot + "file_bug822367_5.html"; + gTestBrowser.contentWindow.location = url; +} +function MixedTest5A() { + gTestBrowser.removeEventListener("load", MixedTest5A, true); + gTestBrowser.addEventListener("load", MixedTest5B, true); + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(notification, "Mixed Content Doorhanger doesn't appear for Test 5"); + notification.secondaryActions[0].callback(); +} +function MixedTest5B() { + waitForCondition(function() content.document.getElementById('p1').innerHTML == "hello", MixedTest5C, "Waited too long for mixed script to run in Test 5"); +} +function MixedTest5C() { + ok(content.document.getElementById('p1').innerHTML == "hello","Mixed script didn't load in Test 5"); + MixedTest6(); +} + +// Mixed script attempts to load in a document.open() that is within an iframe. +function MixedTest6() { + gTestBrowser.removeEventListener("load", MixedTest5B, true); + gTestBrowser.addEventListener("load", MixedTest6A, true); + var url = gHttpTestRoot2 + "file_bug822367_6.html"; + gTestBrowser.contentWindow.location = url; +} +function MixedTest6A() { + gTestBrowser.removeEventListener("load", MixedTest6A, true); + waitForCondition(function() PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser), MixedTest6B, "waited to long for doorhanger"); +} + +function MixedTest6B() { + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(notification, "Mixed Content Doorhanger doesn't appear for Test 6"); + gTestBrowser.addEventListener("load", MixedTest6C, true); + notification.secondaryActions[0].callback(); +} + +function MixedTest6C() { + gTestBrowser.removeEventListener("load", MixedTest6C, true); + waitForCondition(function() content.document.getElementById('f1').contentDocument.getElementById('p1').innerHTML == "hello", MixedTest6D, "Waited too long for mixed script to run in Test 6"); +} +function MixedTest6D() { + ok(content.document.getElementById('f1').contentDocument.getElementById('p1').innerHTML == "hello","Mixed script didn't load in Test 6"); + MixedTestsCompleted(); +} + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + if (condition()) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} diff --git a/browser/base/content/test/browser_bug832435.js b/browser/base/content/test/browser_bug832435.js new file mode 100644 index 000000000..6be2604cd --- /dev/null +++ b/browser/base/content/test/browser_bug832435.js @@ -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/. */ + +function test() { + waitForExplicitFinish(); + ok(true, "Starting up"); + + gBrowser.selectedBrowser.focus(); + gURLBar.addEventListener("focus", function onFocus() { + gURLBar.removeEventListener("focus", onFocus); + ok(true, "Invoked onfocus handler"); + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }); + + // javscript: URIs are evaluated async. + SimpleTest.executeSoon(function() { + ok(true, "Evaluated without crashing"); + finish(); + }); + }); + gURLBar.inputField.value = "javascript: var foo = '11111111'; "; + gURLBar.focus(); +} diff --git a/browser/base/content/test/browser_bug839103.js b/browser/base/content/test/browser_bug839103.js new file mode 100644 index 000000000..2b777c00d --- /dev/null +++ b/browser/base/content/test/browser_bug839103.js @@ -0,0 +1,159 @@ +const gTestRoot = getRootDirectory(gTestPath); +const gStyleSheet = "bug839103.css"; + +var gTab = null; +var needsInitialApplicableStateEvent = false; +var needsInitialApplicableStateEventFor = null; + +function test() { + waitForExplicitFinish(); + gBrowser.addEventListener("StyleSheetAdded", initialStylesheetAdded, true); + gTab = gBrowser.selectedTab = gBrowser.addTab(gTestRoot + "test_bug839103.html"); + gTab.linkedBrowser.addEventListener("load", tabLoad, true); +} + +function initialStylesheetAdded(evt) { + gBrowser.removeEventListener("StyleSheetAdded", initialStylesheetAdded, true); + ok(true, "received initial style sheet event"); + is(evt.type, "StyleSheetAdded", "evt.type has expected value"); + is(evt.target, gBrowser.contentDocument, "event targets correct document"); + ok(evt.stylesheet, "evt.stylesheet is defined"); + ok(evt.stylesheet.toString().contains("CSSStyleSheet"), "evt.stylesheet is a stylesheet"); + ok(evt.documentSheet, "style sheet is a document sheet"); +} + +function tabLoad(evt) { + gTab.linkedBrowser.removeEventListener(evt.type, tabLoad, true); + executeSoon(continueTest); +} + +var gLinkElement = null; + +function unexpectedContentEvent(evt) { + ok(false, "Received a " + evt.type + " event on content"); +} + +// We've seen the original stylesheet in the document. +// Now add a stylesheet on the fly and make sure we see it. +function continueTest() { + info("continuing test"); + + let doc = gBrowser.contentDocument; + doc.styleSheetChangeEventsEnabled = true; + doc.addEventListener("StyleSheetAdded", unexpectedContentEvent, false); + doc.addEventListener("StyleSheetRemoved", unexpectedContentEvent, false); + doc.addEventListener("StyleSheetApplicableStateChanged", unexpectedContentEvent, false); + doc.defaultView.addEventListener("StyleSheetAdded", unexpectedContentEvent, false); + doc.defaultView.addEventListener("StyleSheetRemoved", unexpectedContentEvent, false); + doc.defaultView.addEventListener("StyleSheetApplicableStateChanged", unexpectedContentEvent, false); + let link = doc.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('type', 'text/css'); + link.setAttribute('href', gTestRoot + gStyleSheet); + gLinkElement = link; + + gBrowser.addEventListener("StyleSheetAdded", dynamicStylesheetAdded, true); + gBrowser.addEventListener("StyleSheetApplicableStateChanged", dynamicStylesheetApplicableStateChanged, true); + doc.body.appendChild(link); +} + +function dynamicStylesheetAdded(evt) { + gBrowser.removeEventListener("StyleSheetAdded", dynamicStylesheetAdded, true); + ok(true, "received dynamic style sheet event"); + is(evt.type, "StyleSheetAdded", "evt.type has expected value"); + is(evt.target, gBrowser.contentDocument, "event targets correct document"); + ok(evt.stylesheet, "evt.stylesheet is defined"); + ok(evt.stylesheet.toString().contains("CSSStyleSheet"), "evt.stylesheet is a stylesheet"); + ok(evt.documentSheet, "style sheet is a document sheet"); +} + +function dynamicStylesheetApplicableStateChanged(evt) { + gBrowser.removeEventListener("StyleSheetApplicableStateChanged", dynamicStylesheetApplicableStateChanged, true); + ok(true, "received dynamic style sheet applicable state change event"); + is(evt.type, "StyleSheetApplicableStateChanged", "evt.type has expected value"); + is(evt.target, gBrowser.contentDocument, "event targets correct document"); + is(evt.stylesheet, gLinkElement.sheet, "evt.stylesheet has the right value"); + is(evt.applicable, true, "evt.applicable has the right value"); + + gBrowser.addEventListener("StyleSheetApplicableStateChanged", dynamicStylesheetApplicableStateChangedToFalse, true); + gLinkElement.disabled = true; +} + +function dynamicStylesheetApplicableStateChangedToFalse(evt) { + gBrowser.removeEventListener("StyleSheetApplicableStateChanged", dynamicStylesheetApplicableStateChangedToFalse, true); + is(evt.type, "StyleSheetApplicableStateChanged", "evt.type has expected value"); + ok(true, "received dynamic style sheet applicable state change event after media=\"\" changed"); + is(evt.target, gBrowser.contentDocument, "event targets correct document"); + is(evt.stylesheet, gLinkElement.sheet, "evt.stylesheet has the right value"); + is(evt.applicable, false, "evt.applicable has the right value"); + + gBrowser.addEventListener("StyleSheetRemoved", dynamicStylesheetRemoved, true); + gBrowser.contentDocument.body.removeChild(gLinkElement); +} + +function dynamicStylesheetRemoved(evt) { + gBrowser.removeEventListener("StyleSheetRemoved", dynamicStylesheetRemoved, true); + ok(true, "received dynamic style sheet removal"); + is(evt.type, "StyleSheetRemoved", "evt.type has expected value"); + is(evt.target, gBrowser.contentDocument, "event targets correct document"); + ok(evt.stylesheet, "evt.stylesheet is defined"); + ok(evt.stylesheet.toString().contains("CSSStyleSheet"), "evt.stylesheet is a stylesheet"); + ok(evt.stylesheet.href.contains(gStyleSheet), "evt.stylesheet is the removed stylesheet"); + + gBrowser.addEventListener("StyleRuleAdded", styleRuleAdded, true); + gBrowser.contentDocument.querySelector("style").sheet.insertRule("*{color:black}", 0); +} + +function styleRuleAdded(evt) { + gBrowser.removeEventListener("StyleRuleAdded", styleRuleAdded, true); + ok(true, "received style rule added event"); + is(evt.type, "StyleRuleAdded", "evt.type has expected value"); + is(evt.target, gBrowser.contentDocument, "event targets correct document"); + ok(evt.stylesheet, "evt.stylesheet is defined"); + ok(evt.stylesheet.toString().contains("CSSStyleSheet"), "evt.stylesheet is a stylesheet"); + ok(evt.rule, "evt.rule is defined"); + is(evt.rule.cssText, "* { color: black; }", "evt.rule.cssText has expected value"); + + gBrowser.addEventListener("StyleRuleChanged", styleRuleChanged, true); + evt.rule.style.cssText = "color:green"; +} + +function styleRuleChanged(evt) { + gBrowser.removeEventListener("StyleRuleChanged", styleRuleChanged, true); + ok(true, "received style rule changed event"); + is(evt.type, "StyleRuleChanged", "evt.type has expected value"); + is(evt.target, gBrowser.contentDocument, "event targets correct document"); + ok(evt.stylesheet, "evt.stylesheet is defined"); + ok(evt.stylesheet.toString().contains("CSSStyleSheet"), "evt.stylesheet is a stylesheet"); + ok(evt.rule, "evt.rule is defined"); + is(evt.rule.cssText, "* { color: green; }", "evt.rule.cssText has expected value"); + + gBrowser.addEventListener("StyleRuleRemoved", styleRuleRemoved, true); + evt.stylesheet.deleteRule(0); +} + +function styleRuleRemoved(evt) { + gBrowser.removeEventListener("StyleRuleRemoved", styleRuleRemoved, true); + ok(true, "received style rule removed event"); + is(evt.type, "StyleRuleRemoved", "evt.type has expected value"); + is(evt.target, gBrowser.contentDocument, "event targets correct document"); + ok(evt.stylesheet, "evt.stylesheet is defined"); + ok(evt.stylesheet.toString().contains("CSSStyleSheet"), "evt.stylesheet is a stylesheet"); + ok(evt.rule, "evt.rule is defined"); + + executeSoon(concludeTest); +} + +function concludeTest() { + let doc = gBrowser.contentDocument; + doc.removeEventListener("StyleSheetAdded", unexpectedContentEvent, false); + doc.removeEventListener("StyleSheetRemoved", unexpectedContentEvent, false); + doc.removeEventListener("StyleSheetApplicableStateChanged", unexpectedContentEvent, false); + doc.defaultView.removeEventListener("StyleSheetAdded", unexpectedContentEvent, false); + doc.defaultView.removeEventListener("StyleSheetRemoved", unexpectedContentEvent, false); + doc.defaultView.removeEventListener("StyleSheetApplicableStateChanged", unexpectedContentEvent, false); + gBrowser.removeCurrentTab(); + gLinkElement = null; + gTab = null; + finish(); +} diff --git a/browser/base/content/test/browser_bug880101.js b/browser/base/content/test/browser_bug880101.js new file mode 100644 index 000000000..abe05b864 --- /dev/null +++ b/browser/base/content/test/browser_bug880101.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const URL = "about:robots"; + +function test() { + let win; + + let listener = { + onLocationChange: (webProgress, request, uri, flags) => { + ok(webProgress.isTopLevel, "Received onLocationChange from top frame"); + is(uri.spec, URL, "Received onLocationChange for correct URL"); + finish(); + } + }; + + waitForExplicitFinish(); + + // Remove the listener and window when we're done. + registerCleanupFunction(() => { + win.gBrowser.removeProgressListener(listener); + win.close(); + }); + + // Wait for the newly opened window. + whenNewWindowOpened(w => win = w); + + // Open a link in a new window. + openLinkIn(URL, "window", {}); + + // On the next tick, but before the window has finished loading, access the + // window's gBrowser property to force the tabbrowser constructor early. + (function tryAddProgressListener() { + executeSoon(() => { + try { + win.gBrowser.addProgressListener(listener); + } catch (e) { + // win.gBrowser wasn't ready, yet. Try again in a tick. + tryAddProgressListener(); + } + }); + })(); +} + +function whenNewWindowOpened(cb) { + Services.obs.addObserver(function obs(win) { + Services.obs.removeObserver(obs, "domwindowopened"); + cb(win); + }, "domwindowopened", false); +} diff --git a/browser/base/content/test/browser_bug902156.js b/browser/base/content/test/browser_bug902156.js new file mode 100644 index 000000000..d08f30b09 --- /dev/null +++ b/browser/base/content/test/browser_bug902156.js @@ -0,0 +1,213 @@ +/* + * Description of the Tests for + * - Bug 902156: Persist "disable protection" option for Mixed Content Blocker + * + * 1. Navigate to the same domain via document.location + * - Load a html page which has mixed content + * - Doorhanger to disable protection appears - we disable it + * - Load a new page from the same origin using document.location + * - Doorhanger should not appear anymore! + * + * 2. Navigate to the same domain via simulateclick for a link on the page + * - Load a html page which has mixed content + * - Doorhanger to disable protection appears - we disable it + * - Load a new page from the same origin simulating a click + * - Doorhanger should not appear anymore! + * + * 3. Navigate to a differnet domain and show the content is still blocked + * - Load a different html page which has mixed content + * - Doorhanger to disable protection should appear again because + * we navigated away from html page where we disabled the protection. + * + * Note, for all tests we set gHttpTestRoot to use 'https'. + */ + +const PREF_ACTIVE = "security.mixed_content.block_active_content"; + +// We alternate for even and odd test cases to simulate different hosts +const gHttpTestRoot1 = "https://test1.example.com/browser/browser/base/content/test/"; +const gHttpTestRoot2 = "https://test2.example.com/browser/browser/base/content/test/"; + +var origBlockActive; +var gTestBrowser = null; + +registerCleanupFunction(function() { + // Set preferences back to their original values + Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive); +}); + +function cleanUpAfterTests() { + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} +/* + * Whenever we disable the Mixed Content Blocker of the page + * we have to make sure that our condition is properly loaded. + */ +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + if (condition()) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { + clearInterval(interval); nextTest(); + }; +} + +//------------------------ Test 1 ------------------------------ + +function test1A() { + // Removing EventListener because we have to register a new + // one once the page is loaded with mixed content blocker disabled + gTestBrowser.removeEventListener("load", test1A, true); + gTestBrowser.addEventListener("load", test1B, true); + + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(notification, "OK: Mixed Content Doorhanger appeared in Test1A!"); + + // Disable Mixed Content Protection for the page + notification.secondaryActions[0].callback(); +} + +function test1B() { + var expected = "Mixed Content Blocker disabled"; + waitForCondition( + function() content.document.getElementById('mctestdiv').innerHTML == expected, + test1C, "Error: Waited too long for mixed script to run in Test 1B"); +} + +function test1C() { + gTestBrowser.removeEventListener("load", test1B, true); + var actual = content.document.getElementById('mctestdiv').innerHTML; + is(actual, "Mixed Content Blocker disabled", "OK: Executed mixed script in Test 1C"); + + // The Script loaded after we disabled the page, now we are going to reload the + // page and see if our decision is persistent + gTestBrowser.addEventListener("load", test1D, true); + + var url = gHttpTestRoot1 + "file_bug902156_2.html"; + gTestBrowser.contentWindow.location = url; +} + +function test1D() { + gTestBrowser.removeEventListener("load", test1D, true); + + // The Doorhanger should not appear, because our decision of disabling the + // mixed content blocker is persistent. + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(!notification, "OK: Mixed Content Doorhanger did not appear again in Test1D!"); + + var actual = content.document.getElementById('mctestdiv').innerHTML; + is(actual, "Mixed Content Blocker disabled", "OK: Executed mixed script in Test 1D"); + + // move on to Test 2 + test2(); +} + +//------------------------ Test 2 ------------------------------ + +function test2() { + gTestBrowser.addEventListener("load", test2A, true); + var url = gHttpTestRoot2 + "file_bug902156_2.html"; + gTestBrowser.contentWindow.location = url; +} + +function test2A() { + // Removing EventListener because we have to register a new + // one once the page is loaded with mixed content blocker disabled + gTestBrowser.removeEventListener("load", test2A, true); + gTestBrowser.addEventListener("load", test2B, true); + + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(notification, "OK: Mixed Content Doorhanger appeared in Test 2A!"); + + // Disable Mixed Content Protection for the page + notification.secondaryActions[0].callback(); +} + +function test2B() { + var expected = "Mixed Content Blocker disabled"; + waitForCondition( + function() content.document.getElementById('mctestdiv').innerHTML == expected, + test2C, "Error: Waited too long for mixed script to run in Test 2B"); +} + +function test2C() { + gTestBrowser.removeEventListener("load", test2B, true); + var actual = content.document.getElementById('mctestdiv').innerHTML; + is(actual, "Mixed Content Blocker disabled", "OK: Executed mixed script in Test 2C"); + + // The Script loaded after we disabled the page, now we are going to reload the + // page and see if our decision is persistent + gTestBrowser.addEventListener("load", test2D, true); + + // reload the page using the provided link in the html file + var mctestlink = content.document.getElementById("mctestlink"); + mctestlink.click(); +} + +function test2D() { + gTestBrowser.removeEventListener("load", test2D, true); + + // The Doorhanger should not appear, because our decision of disabling the + // mixed content blocker is persistent. + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(!notification, "OK: Mixed Content Doorhanger did not appear again in Test2D!"); + + var actual = content.document.getElementById('mctestdiv').innerHTML; + is(actual, "Mixed Content Blocker disabled", "OK: Executed mixed script in Test 2D"); + + // move on to Test 3 + test3(); +} + +//------------------------ Test 3 ------------------------------ + +function test3() { + gTestBrowser.addEventListener("load", test3A, true); + var url = gHttpTestRoot1 + "file_bug902156_3.html"; + gTestBrowser.contentWindow.location = url; +} + +function test3A() { + // Removing EventListener because we have to register a new + // one once the page is loaded with mixed content blocker disabled + gTestBrowser.removeEventListener("load", test3A, true); + + var notification = PopupNotifications.getNotification("mixed-content-blocked", gTestBrowser); + ok(notification, "OK: Mixed Content Doorhanger appeared in Test 3A!"); + + // We are done with tests, clean up + cleanUpAfterTests(); +} + +//------------------------------------------------------ + +function test() { + // Performing async calls, e.g. 'onload', we have to wait till all of them finished + waitForExplicitFinish(); + + // Store original preferences so we can restore settings after testing + origBlockActive = Services.prefs.getBoolPref(PREF_ACTIVE); + + Services.prefs.setBoolPref(PREF_ACTIVE, true); + + // Not really sure what this is doing + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + newTab.linkedBrowser.stop() + + // Starting Test Number 1: + gTestBrowser.addEventListener("load", test1A, true); + var url = gHttpTestRoot1 + "file_bug902156_1.html"; + gTestBrowser.contentWindow.location = url; +} diff --git a/browser/base/content/test/browser_canonizeURL.js b/browser/base/content/test/browser_canonizeURL.js new file mode 100644 index 000000000..983d0941c --- /dev/null +++ b/browser/base/content/test/browser_canonizeURL.js @@ -0,0 +1,54 @@ +function test() { + waitForExplicitFinish(); + testNext(); +} + +var pairs = [ + ["example", "http://www.example.net/"], + ["ex-ample", "http://www.ex-ample.net/"], + [" example ", "http://www.example.net/"], + [" example/foo ", "http://www.example.net/foo"], + [" example/foo bar ", "http://www.example.net/foo%20bar"], + ["example.net", "http://example.net/"], + ["http://example", "http://example/"], + ["example:8080", "http://example:8080/"], + ["ex-ample.foo", "http://ex-ample.foo/"], + ["example.foo/bar ", "http://example.foo/bar"], + ["1.1.1.1", "http://1.1.1.1/"], + ["ftp://example", "ftp://example/"], + ["ftp.example.bar", "ftp://ftp.example.bar/"], + ["ex ample", Services.search.defaultEngine.getSubmission("ex ample", null, "keyword").uri.spec], +]; + +function testNext() { + if (!pairs.length) { + finish(); + return; + } + + let [inputValue, expectedURL] = pairs.shift(); + + gBrowser.addProgressListener({ + onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + is(aRequest.originalURI.spec, expectedURL, + "entering '" + inputValue + "' loads expected URL"); + + gBrowser.removeProgressListener(this); + gBrowser.stop(); + + executeSoon(testNext); + } + } + }); + + gURLBar.addEventListener("focus", function onFocus() { + gURLBar.removeEventListener("focus", onFocus); + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }); + }); + + gBrowser.selectedBrowser.focus(); + gURLBar.inputField.value = inputValue; + gURLBar.focus(); +} diff --git a/browser/base/content/test/browser_clearplugindata.html b/browser/base/content/test/browser_clearplugindata.html new file mode 100644 index 000000000..d5a6872c8 --- /dev/null +++ b/browser/base/content/test/browser_clearplugindata.html @@ -0,0 +1,32 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> + <head> + <title>Plugin Clear Site Data sanitize test</title> + + <embed id="plugin1" type="application/x-test" width="200" height="200"></embed> + + <script type="application/javascript"> + function testSteps() + { + // Make sure clearing by timerange is supported. + var p = document.getElementById("plugin1"); + p.setSitesWithDataCapabilities(true); + + p.setSitesWithData( + "foo.com:0:5," + + "bar.com:0:100," + + "baz.com:1:5," + + "qux.com:1:100" + ); + + setTimeout(testFinishedCallback, 0); + } + </script> + </head> + + <body onload="testSteps();"></body> + +</html> diff --git a/browser/base/content/test/browser_clearplugindata.js b/browser/base/content/test/browser_clearplugindata.js new file mode 100644 index 000000000..4b79bbbb1 --- /dev/null +++ b/browser/base/content/test/browser_clearplugindata.js @@ -0,0 +1,140 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test clearing plugin data using sanitize.js. +const testURL1 = "http://mochi.test:8888/browser/browser/base/content/test/browser_clearplugindata.html"; +const testURL2 = "http://mochi.test:8888/browser/browser/base/content/test/browser_clearplugindata_noage.html"; + +let tempScope = {}; +Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tempScope); +let Sanitizer = tempScope.Sanitizer; + +const pluginHostIface = Ci.nsIPluginHost; +var pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); +pluginHost.QueryInterface(pluginHostIface); + +var pluginTag; +var s; + +function stored(needles) { + var something = pluginHost.siteHasData(this.pluginTag, null); + if (!needles) + return something; + + if (!something) + return false; + + for (var i = 0; i < needles.length; ++i) { + if (!pluginHost.siteHasData(this.pluginTag, needles[i])) + return false; + } + return true; +} + +function test() { + waitForExplicitFinish(); + + var tags = pluginHost.getPluginTags(); + + // Find the test plugin + for (var i = 0; i < tags.length; i++) + { + if (tags[i].name == "Test Plug-in") + { + pluginTag = tags[i]; + } + } + + s = new Sanitizer(); + s.ignoreTimespan = false; + s.prefDomain = "privacy.cpd."; + var itemPrefs = gPrefService.getBranch(s.prefDomain); + itemPrefs.setBoolPref("history", false); + itemPrefs.setBoolPref("downloads", false); + itemPrefs.setBoolPref("cache", false); + itemPrefs.setBoolPref("cookies", true); // plugin data + itemPrefs.setBoolPref("formdata", false); + itemPrefs.setBoolPref("offlineApps", false); + itemPrefs.setBoolPref("passwords", false); + itemPrefs.setBoolPref("sessions", false); + itemPrefs.setBoolPref("siteSettings", false); + + executeSoon(test_with_age); +} + +function setFinishedCallback(callback) +{ + let testPage = gBrowser.selectedBrowser.contentWindow.wrappedJSObject; + testPage.testFinishedCallback = function() { + setTimeout(function() { + info("got finished callback"); + callback(); + }, 0); + } +} + +function test_with_age() +{ + // Load page to set data for the plugin. + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function() { + ok(stored(["foo.com","bar.com","baz.com","qux.com"]), + "Data stored for sites"); + + // Clear 20 seconds ago + var now_uSec = Date.now() * 1000; + s.range = [now_uSec - 20*1000000, now_uSec]; + s.sanitize(); + + ok(stored(["bar.com","qux.com"]), "Data stored for sites"); + ok(!stored(["foo.com"]), "Data cleared for foo.com"); + ok(!stored(["baz.com"]), "Data cleared for baz.com"); + + // Clear everything + s.range = null; + s.sanitize(); + + ok(!stored(null), "All data cleared"); + + gBrowser.removeCurrentTab(); + + executeSoon(test_without_age); + }); + }, true); + content.location = testURL1; +} + +function test_without_age() +{ + // Load page to set data for the plugin. + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + setFinishedCallback(function() { + ok(stored(["foo.com","bar.com","baz.com","qux.com"]), + "Data stored for sites"); + + // Attempt to clear 20 seconds ago. The plugin will throw + // NS_ERROR_PLUGIN_TIME_RANGE_NOT_SUPPORTED, which should result in us + // clearing all data regardless of age. + var now_uSec = Date.now() * 1000; + s.range = [now_uSec - 20*1000000, now_uSec]; + s.sanitize(); + + ok(!stored(null), "All data cleared"); + + gBrowser.removeCurrentTab(); + + executeSoon(finish); + }); + }, true); + content.location = testURL2; +} + diff --git a/browser/base/content/test/browser_clearplugindata_noage.html b/browser/base/content/test/browser_clearplugindata_noage.html new file mode 100644 index 000000000..75e1f2e1f --- /dev/null +++ b/browser/base/content/test/browser_clearplugindata_noage.html @@ -0,0 +1,32 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> + <head> + <title>Plugin Clear Site Data sanitize test without age</title> + + <embed id="plugin1" type="application/x-test" width="200" height="200"></embed> + + <script type="application/javascript"> + function testSteps() + { + // Make sure clearing by timerange is disabled. + var p = document.getElementById("plugin1"); + p.setSitesWithDataCapabilities(false); + + p.setSitesWithData( + "foo.com:0:5," + + "bar.com:0:100," + + "baz.com:1:5," + + "qux.com:1:100" + ); + + setTimeout(testFinishedCallback, 0); + } + </script> + </head> + + <body onload="testSteps();"></body> + +</html> diff --git a/browser/base/content/test/browser_contentAreaClick.js b/browser/base/content/test/browser_contentAreaClick.js new file mode 100644 index 000000000..ba89240ea --- /dev/null +++ b/browser/base/content/test/browser_contentAreaClick.js @@ -0,0 +1,307 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test for bug 549340. + * Test for browser.js::contentAreaClick() util. + * + * The test opens a new browser window, then replaces browser.js methods invoked + * by contentAreaClick with a mock function that tracks which methods have been + * called. + * Each sub-test synthesizes a mouse click event on links injected in content, + * the event is collected by a click handler that ensures that contentAreaClick + * correctly prevent default events, and follows the correct code path. + */ + +let gTests = [ + + { + desc: "Simple left click", + setup: function() {}, + clean: function() {}, + event: {}, + targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ], + expectedInvokedMethods: [], + preventDefault: false, + }, + + { + desc: "Ctrl/Cmd left click", + setup: function() {}, + clean: function() {}, + event: { ctrlKey: true, + metaKey: true }, + targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ], + expectedInvokedMethods: [ "urlSecurityCheck", "openLinkIn" ], + preventDefault: true, + }, + + // The next test was once handling feedService.forcePreview(). Now it should + // just be like Alt click. + { + desc: "Shift+Alt left click", + setup: function() { + gPrefService.setBoolPref("browser.altClickSave", true); + }, + clean: function() { + gPrefService.clearUserPref("browser.altClickSave"); + }, + event: { shiftKey: true, + altKey: true }, + targets: [ "commonlink", "maplink" ], + expectedInvokedMethods: [ "gatherTextUnder", "saveURL" ], + preventDefault: true, + }, + + { + desc: "Shift+Alt left click on XLinks", + setup: function() { + gPrefService.setBoolPref("browser.altClickSave", true); + }, + clean: function() { + gPrefService.clearUserPref("browser.altClickSave"); + }, + event: { shiftKey: true, + altKey: true }, + targets: [ "mathxlink", "svgxlink"], + expectedInvokedMethods: [ "saveURL" ], + preventDefault: true, + }, + + { + desc: "Shift click", + setup: function() {}, + clean: function() {}, + event: { shiftKey: true }, + targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ], + expectedInvokedMethods: [ "urlSecurityCheck", "openLinkIn" ], + preventDefault: true, + }, + + { + desc: "Alt click", + setup: function() { + gPrefService.setBoolPref("browser.altClickSave", true); + }, + clean: function() { + gPrefService.clearUserPref("browser.altClickSave"); + }, + event: { altKey: true }, + targets: [ "commonlink", "maplink" ], + expectedInvokedMethods: [ "gatherTextUnder", "saveURL" ], + preventDefault: true, + }, + + { + desc: "Alt click on XLinks", + setup: function() { + gPrefService.setBoolPref("browser.altClickSave", true); + }, + clean: function() { + gPrefService.clearUserPref("browser.altClickSave"); + }, + event: { altKey: true }, + targets: [ "mathxlink", "svgxlink" ], + expectedInvokedMethods: [ "saveURL" ], + preventDefault: true, + }, + + { + desc: "Panel click", + setup: function() {}, + clean: function() {}, + event: {}, + targets: [ "panellink" ], + expectedInvokedMethods: [ "urlSecurityCheck", "loadURI" ], + preventDefault: true, + }, + + { + desc: "Simple middle click opentab", + setup: function() {}, + clean: function() {}, + event: { button: 1 }, + targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ], + expectedInvokedMethods: [ "urlSecurityCheck", "openLinkIn" ], + preventDefault: true, + }, + + { + desc: "Simple middle click openwin", + setup: function() { + gPrefService.setBoolPref("browser.tabs.opentabfor.middleclick", false); + }, + clean: function() { + gPrefService.clearUserPref("browser.tabs.opentabfor.middleclick"); + }, + event: { button: 1 }, + targets: [ "commonlink", "mathxlink", "svgxlink", "maplink" ], + expectedInvokedMethods: [ "urlSecurityCheck", "openLinkIn" ], + preventDefault: true, + }, + + { + desc: "Middle mouse paste", + setup: function() { + gPrefService.setBoolPref("middlemouse.contentLoadURL", true); + gPrefService.setBoolPref("general.autoScroll", false); + }, + clean: function() { + gPrefService.clearUserPref("middlemouse.contentLoadURL"); + gPrefService.clearUserPref("general.autoScroll"); + }, + event: { button: 1 }, + targets: [ "emptylink" ], + expectedInvokedMethods: [ "middleMousePaste" ], + preventDefault: true, + }, + +]; + +// Array of method names that will be replaced in the new window. +let gReplacedMethods = [ + "middleMousePaste", + "urlSecurityCheck", + "loadURI", + "gatherTextUnder", + "saveURL", + "openLinkIn", + "getShortcutOrURI", +]; + +// Reference to the new window. +let gTestWin = null; + +// List of methods invoked by a specific call to contentAreaClick. +let gInvokedMethods = []; + +// The test currently running. +let gCurrentTest = null; + +function test() { + waitForExplicitFinish(); + + gTestWin = openDialog(location, "", "chrome,all,dialog=no", "about:blank"); + whenDelayedStartupFinished(gTestWin, function () { + info("Browser window opened"); + waitForFocus(function() { + info("Browser window focused"); + waitForFocus(function() { + info("Setting up browser..."); + setupTestBrowserWindow(); + info("Running tests..."); + executeSoon(runNextTest); + }, gTestWin.content, true); + }, gTestWin); + }); +} + +// Click handler used to steal click events. +let gClickHandler = { + handleEvent: function (event) { + let linkId = event.target.id || event.target.localName; + is(event.type, "click", + gCurrentTest.desc + ":Handler received a click event on " + linkId); + + let isPanelClick = linkId == "panellink"; + gTestWin.contentAreaClick(event, isPanelClick); + let prevent = event.defaultPrevented; + is(prevent, gCurrentTest.preventDefault, + gCurrentTest.desc + ": event.defaultPrevented is correct (" + prevent + ")") + + // Check that all required methods have been called. + gCurrentTest.expectedInvokedMethods.forEach(function(aExpectedMethodName) { + isnot(gInvokedMethods.indexOf(aExpectedMethodName), -1, + gCurrentTest.desc + ":" + aExpectedMethodName + " was invoked"); + }); + + if (gInvokedMethods.length != gCurrentTest.expectedInvokedMethods.length) { + ok(false, "Wrong number of invoked methods"); + gInvokedMethods.forEach(function (method) info(method + " was invoked")); + } + + event.preventDefault(); + event.stopPropagation(); + + executeSoon(runNextTest); + } +} + +// Wraps around the methods' replacement mock function. +function wrapperMethod(aInvokedMethods, aMethodName) { + return function () { + aInvokedMethods.push(aMethodName); + // At least getShortcutOrURI requires to return url that is the first param. + return arguments[0]; + } +} + +function setupTestBrowserWindow() { + // Steal click events and don't propagate them. + gTestWin.addEventListener("click", gClickHandler, true); + + // Replace methods. + gReplacedMethods.forEach(function (aMethodName) { + gTestWin["old_" + aMethodName] = gTestWin[aMethodName]; + gTestWin[aMethodName] = wrapperMethod(gInvokedMethods, aMethodName); + }); + + // Inject links in content. + let doc = gTestWin.content.document; + let mainDiv = doc.createElement("div"); + mainDiv.innerHTML = + '<p><a id="commonlink" href="http://mochi.test/moz/">Common link</a></p>' + + '<p><a id="panellink" href="http://mochi.test/moz/">Panel link</a></p>' + + '<p><a id="emptylink">Empty link</a></p>' + + '<p><math id="mathxlink" xmlns="http://www.w3.org/1998/Math/MathML" xlink:type="simple" xlink:href="http://mochi.test/moz/"><mtext>MathML XLink</mtext></math></p>' + + '<p><svg id="svgxlink" xmlns="http://www.w3.org/2000/svg" width="100px" height="50px" version="1.1"><a xlink:type="simple" xlink:href="http://mochi.test/moz/"><text transform="translate(10, 25)">SVG XLink</text></a></svg></p>' + + '<p><map name="map" id="map"><area href="http://mochi.test/moz/" shape="rect" coords="0,0,128,128" /></map><img id="maplink" usemap="#map" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAAABGdBTUEAALGPC%2FxhBQAAAOtJREFUeF7t0IEAAAAAgKD9qRcphAoDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGDAgAEDBgwYMGBgwIAAAT0N51AAAAAASUVORK5CYII%3D"/></p>' + doc.body.appendChild(mainDiv); +} + +function runNextTest() { + if (!gCurrentTest) { + gCurrentTest = gTests.shift(); + gCurrentTest.setup(); + } + + if (gCurrentTest.targets.length == 0) { + info(gCurrentTest.desc + ": cleaning up...") + gCurrentTest.clean(); + + if (gTests.length > 0) { + gCurrentTest = gTests.shift(); + gCurrentTest.setup(); + } + else { + finishTest(); + return; + } + } + + // Move to next target. + gInvokedMethods.length = 0; + let target = gCurrentTest.targets.shift(); + + info(gCurrentTest.desc + ": testing " + target); + + // Fire click event. + let targetElt = gTestWin.content.document.getElementById(target); + ok(targetElt, gCurrentTest.desc + ": target is valid (" + targetElt.id + ")"); + EventUtils.synthesizeMouseAtCenter(targetElt, gCurrentTest.event, gTestWin.content); +} + +function finishTest() { + info("Restoring browser..."); + gTestWin.removeEventListener("click", gClickHandler, true); + + // Restore original methods. + gReplacedMethods.forEach(function (aMethodName) { + gTestWin[aMethodName] = gTestWin["old_" + aMethodName]; + delete gTestWin["old_" + aMethodName]; + }); + + gTestWin.close(); + finish(); +} diff --git a/browser/base/content/test/browser_contextSearchTabPosition.js b/browser/base/content/test/browser_contextSearchTabPosition.js new file mode 100644 index 000000000..38dbc2adf --- /dev/null +++ b/browser/base/content/test/browser_contextSearchTabPosition.js @@ -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/. */ + +function test() { + waitForExplicitFinish(); + + function tabAdded(event) { + let tab = event.target; + tabs.push(tab); + } + + let tabs = []; + + let container = gBrowser.tabContainer; + container.addEventListener("TabOpen", tabAdded, false); + + gBrowser.addTab("about:blank"); + BrowserSearch.loadSearchFromContext("mozilla"); + BrowserSearch.loadSearchFromContext("firefox"); + + is(tabs[0], gBrowser.tabs[3], "blank tab has been pushed to the end"); + is(tabs[1], gBrowser.tabs[1], "first search tab opens next to the current tab"); + is(tabs[2], gBrowser.tabs[2], "second search tab opens next to the first search tab"); + + container.removeEventListener("TabOpen", tabAdded, false); + tabs.forEach(gBrowser.removeTab, gBrowser); + + try { + let cm = Components.classes["@mozilla.org/categorymanager;1"] + .getService(Components.interfaces.nsICategoryManager); + cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider"); + } catch (ex) { + // Health Report disabled, or no SearchesProvider. + finish(); + return; + } + + let reporter = Components.classes["@mozilla.org/datareporting/service;1"] + .getService() + .wrappedJSObject + .healthReporter; + + // reporter should always be available in automation. + ok(reporter, "Health Reporter available."); + reporter.onInit().then(function onInit() { + let provider = reporter.getProvider("org.mozilla.searches"); + ok(provider, "Searches provider is available."); + + let m = provider.getMeasurement("counts", 2); + m.getValues().then(function onValues(data) { + let now = new Date(); + ok(data.days.hasDay(now), "Have data for today."); + let day = data.days.getDay(now); + + // Will need to be changed if Google isn't the default search engine. + let field = "google.contextmenu"; + ok(day.has(field), "Have search recorded for context menu."); + + // If any other mochitests perform a context menu search, this will fail. + // The solution will be to look up count at test start and ensure it is + // incremented by two. + is(day.get(field), 2, "2 searches recorded in FHR."); + finish(); + }); + }); +} + diff --git a/browser/base/content/test/browser_ctrlTab.js b/browser/base/content/test/browser_ctrlTab.js new file mode 100644 index 000000000..61937a5ff --- /dev/null +++ b/browser/base/content/test/browser_ctrlTab.js @@ -0,0 +1,151 @@ +function test() { + gPrefService.setBoolPref("browser.ctrlTab.previews", true); + + gBrowser.addTab(); + gBrowser.addTab(); + gBrowser.addTab(); + + checkTabs(4); + + ctrlTabTest([2] , 1, 0); + ctrlTabTest([2, 3, 1], 2, 2); + ctrlTabTest([] , 5, 2); + + { + let selectedIndex = gBrowser.tabContainer.selectedIndex; + pressCtrlTab(); + pressCtrlTab(true); + releaseCtrl(); + is(gBrowser.tabContainer.selectedIndex, selectedIndex, + "Ctrl+Tab -> Ctrl+Shift+Tab keeps the selected tab"); + } + + { // test for bug 445369 + let tabs = gBrowser.tabs.length; + pressCtrlTab(); + EventUtils.synthesizeKey("w", { ctrlKey: true }); + is(gBrowser.tabs.length, tabs - 1, "Ctrl+Tab -> Ctrl+W removes one tab"); + releaseCtrl(); + } + + { // test for bug 667314 + let tabs = gBrowser.tabs.length; + pressCtrlTab(); + pressCtrlTab(true); + EventUtils.synthesizeKey("w", { ctrlKey: true }); + is(gBrowser.tabs.length, tabs - 1, "Ctrl+Tab -> Ctrl+W removes the selected tab"); + releaseCtrl(); + } + + gBrowser.addTab(); + checkTabs(3); + ctrlTabTest([2, 1, 0], 9, 1); + + gBrowser.addTab(); + checkTabs(4); + + { // test for bug 445369 + selectTabs([1, 2, 0]); + + let selectedTab = gBrowser.selectedTab; + let tabToRemove = gBrowser.tabs[1]; + + pressCtrlTab(); + pressCtrlTab(); + EventUtils.synthesizeKey("w", { ctrlKey: true }); + ok(!tabToRemove.parentNode, + "Ctrl+Tab*2 -> Ctrl+W removes the second most recently selected tab"); + + pressCtrlTab(true); + pressCtrlTab(true); + releaseCtrl(); + ok(selectedTab.selected, + "Ctrl+Tab*2 -> Ctrl+W -> Ctrl+Shift+Tab*2 keeps the selected tab"); + } + gBrowser.removeTab(gBrowser.tabContainer.lastChild); + checkTabs(2); + + ctrlTabTest([1], 1, 0); + + gBrowser.removeTab(gBrowser.tabContainer.lastChild); + checkTabs(1); + + { // test for bug 445768 + let focusedWindow = document.commandDispatcher.focusedWindow; + let eventConsumed = true; + let detectKeyEvent = function (event) { + eventConsumed = event.defaultPrevented; + }; + document.addEventListener("keypress", detectKeyEvent, false); + pressCtrlTab(); + document.removeEventListener("keypress", detectKeyEvent, false); + ok(eventConsumed, "Ctrl+Tab consumed by the tabbed browser if one tab is open"); + is(focusedWindow, document.commandDispatcher.focusedWindow, + "Ctrl+Tab doesn't change focus if one tab is open"); + } + + // cleanup + if (gPrefService.prefHasUserValue("browser.ctrlTab.previews")) + gPrefService.clearUserPref("browser.ctrlTab.previews"); + + /* private utility functions */ + + function pressCtrlTab(aShiftKey) + EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: !!aShiftKey }); + + function releaseCtrl() + EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }); + + function isOpen() + ctrlTab.isOpen; + + function checkTabs(aTabs) { + var tabs = gBrowser.tabs.length; + if (tabs != aTabs) { + while (gBrowser.tabs.length > 1) + gBrowser.removeCurrentTab(); + throw "expected " + aTabs + " open tabs, got " + tabs; + } + } + + function selectTabs(tabs) { + tabs.forEach(function (index) { + gBrowser.selectedTab = gBrowser.tabs[index]; + }); + } + + function ctrlTabTest(tabsToSelect, tabTimes, expectedIndex) { + selectTabs(tabsToSelect); + + var indexStart = gBrowser.tabContainer.selectedIndex; + var tabCount = gBrowser.tabs.length; + var normalized = tabTimes % tabCount; + var where = normalized == 1 ? "back to the previously selected tab" : + normalized + " tabs back in most-recently-selected order"; + + for (let i = 0; i < tabTimes; i++) { + pressCtrlTab(); + + if (tabCount > 2) + is(gBrowser.tabContainer.selectedIndex, indexStart, + "Selected tab doesn't change while tabbing"); + } + + if (tabCount > 2) { + ok(isOpen(), + "With " + tabCount + " tabs open, Ctrl+Tab opens the preview panel"); + + releaseCtrl(); + + ok(!isOpen(), + "Releasing Ctrl closes the preview panel"); + } else { + ok(!isOpen(), + "With " + tabCount + " tabs open, Ctrl+Tab doesn't open the preview panel"); + } + + is(gBrowser.tabContainer.selectedIndex, expectedIndex, + "With "+ tabCount +" tabs open and tab " + indexStart + + " selected, Ctrl+Tab*" + tabTimes + " goes " + where); + } +} diff --git a/browser/base/content/test/browser_customize.js b/browser/base/content/test/browser_customize.js new file mode 100644 index 000000000..8cff36686 --- /dev/null +++ b/browser/base/content/test/browser_customize.js @@ -0,0 +1,24 @@ +function test() { + waitForExplicitFinish(); + + openToolbarCustomizationUI(customizationWindowLoaded); +} + +function customizationWindowLoaded(win) { + let x = win.screenX; + let iconModeList = win.document.getElementById("modelist"); + + iconModeList.addEventListener("popupshown", function popupshown() { + iconModeList.removeEventListener("popupshown", popupshown, false); + + executeSoon(function () { + is(win.screenX, x, + "toolbar customization window shouldn't move when the iconmode menulist is opened"); + iconModeList.open = false; + + closeToolbarCustomizationUI(finish); + }); + }, false); + + iconModeList.open = true; +} diff --git a/browser/base/content/test/browser_customize_popupNotification.js b/browser/base/content/test/browser_customize_popupNotification.js new file mode 100644 index 000000000..138e0dc47 --- /dev/null +++ b/browser/base/content/test/browser_customize_popupNotification.js @@ -0,0 +1,27 @@ +/* +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ +*/ + +function test() { + waitForExplicitFinish(); + let newWin = OpenBrowserWindow(); + whenDelayedStartupFinished(newWin, function () { + // Remove the URL bar + newWin.gURLBar.parentNode.removeChild(newWin.gURLBar); + + waitForFocus(function () { + let PN = newWin.PopupNotifications; + try { + let notification = PN.show(newWin.gBrowser.selectedBrowser, "some-notification", "Some message"); + ok(notification, "showed the notification"); + ok(PN.isPanelOpen, "panel is open"); + is(PN.panel.anchorNode, newWin.gBrowser.selectedTab, "notification is correctly anchored to the tab"); + } catch (ex) { + ok(false, "threw exception: " + ex); + } + newWin.close(); + finish(); + }, newWin); + }); +} diff --git a/browser/base/content/test/browser_datareporting_notification.js b/browser/base/content/test/browser_datareporting_notification.js new file mode 100644 index 000000000..b892bd009 --- /dev/null +++ b/browser/base/content/test/browser_datareporting_notification.js @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 sendNotifyRequest(name) { + let ns = {}; + Components.utils.import("resource://gre/modules/services/datareporting/policy.jsm", ns); + Components.utils.import("resource://gre/modules/Preferences.jsm", ns); + + let service = Components.classes["@mozilla.org/datareporting/service;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + ok(service.healthReporter, "Health Reporter instance is available."); + + let policyPrefs = new ns.Preferences("testing." + name + "."); + ok(service._prefs, "Health Reporter prefs are available."); + let hrPrefs = service._prefs; + + let policy = new ns.DataReportingPolicy(policyPrefs, hrPrefs, service); + policy.firstRunDate = new Date(Date.now() - 24 * 60 * 60 * 1000); + + is(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED, "Policy is in unnotified state."); + + service.healthReporter.onInit().then(function onInit() { + is(policy.ensureNotifyResponse(new Date()), false, "User has not responded to policy."); + }); + + return policy; +} + +/** + * Wait for a <notification> to be closed then call the specified callback. + */ +function waitForNotificationClose(notification, cb) { + let parent = notification.parentNode; + + let observer = new MutationObserver(function onMutatations(mutations) { + for (let mutation of mutations) { + for (let i = 0; i < mutation.removedNodes.length; i++) { + let node = mutation.removedNodes.item(i); + + if (node != notification) { + continue; + } + + observer.disconnect(); + cb(); + } + } + }); + + observer.observe(parent, {childList: true}); +} + +let dumpAppender, rootLogger; + +function test() { + waitForExplicitFinish(); + + let ns = {}; + Components.utils.import("resource://services-common/log4moz.js", ns); + rootLogger = ns.Log4Moz.repository.rootLogger; + dumpAppender = new ns.Log4Moz.DumpAppender(); + dumpAppender.level = ns.Log4Moz.Level.All; + rootLogger.addAppender(dumpAppender); + + let notification = document.getElementById("global-notificationbox"); + let policy; + + notification.addEventListener("AlertActive", function active() { + notification.removeEventListener("AlertActive", active, true); + + executeSoon(function afterNotification() { + is(policy.notifyState, policy.STATE_NOTIFY_WAIT, "Policy is waiting for user response."); + ok(!policy.dataSubmissionPolicyAccepted, "Data submission policy not yet accepted."); + + waitForNotificationClose(notification.currentNotification, function onClose() { + is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE, "Closing info bar completes user notification."); + ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted."); + is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-dismissed", + "Reason for acceptance was info bar dismissal."); + is(notification.allNotifications.length, 0, "No notifications remain."); + test_multiple_windows(); + }); + notification.currentNotification.close(); + }); + }, true); + + policy = sendNotifyRequest("single_window_notified"); +} + +function test_multiple_windows() { + // Ensure we see the notification on all windows and that action on one window + // results in dismiss on every window. + let window2 = OpenBrowserWindow(); + whenDelayedStartupFinished(window2, function onWindow() { + let notification1 = document.getElementById("global-notificationbox"); + let notification2 = window2.document.getElementById("global-notificationbox"); + ok(notification2, "2nd window has a global notification box."); + + let policy; + + let displayCount = 0; + let prefPaneClosed = false; + let childWindowClosed = false; + + function onAlertDisplayed() { + displayCount++; + + if (displayCount != 2) { + return; + } + + ok(true, "Data reporting info bar displayed on all open windows."); + + // We register two independent observers and we need both to clean up + // properly. This handles gating for test completion. + function maybeFinish() { + if (!prefPaneClosed) { + dump("Not finishing test yet because pref pane isn't closed.\n"); + return; + } + + if (!childWindowClosed) { + dump("Not finishing test yet because child window isn't closed.\n"); + return; + } + + dump("Finishing multiple window test.\n"); + rootLogger.removeAppender(dumpAppender); + delete dumpAppender; + delete rootLogger; + finish(); + } + + let closeCount = 0; + function onAlertClose() { + closeCount++; + + if (closeCount != 2) { + return; + } + + ok(true, "Closing info bar on one window closed them on all."); + + is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE, + "Closing info bar with multiple windows completes notification."); + ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted."); + is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-button-pressed", + "Policy records reason for acceptance was button press."); + is(notification1.allNotifications.length, 0, "No notifications remain on main window."); + is(notification2.allNotifications.length, 0, "No notifications remain on 2nd window."); + + window2.close(); + childWindowClosed = true; + maybeFinish(); + } + + waitForNotificationClose(notification1.currentNotification, onAlertClose); + waitForNotificationClose(notification2.currentNotification, onAlertClose); + + // While we're here, we dual purpose this test to check that pressing the + // button does the right thing. + let buttons = notification2.currentNotification.getElementsByTagName("button"); + is(buttons.length, 1, "There is 1 button in the data reporting notification."); + let button = buttons[0]; + + // Automatically close preferences window when it is opened as part of + // button press. + Services.obs.addObserver(function observer(prefWin, topic, data) { + Services.obs.removeObserver(observer, "advanced-pane-loaded"); + + ok(true, "Pref pane opened on info bar button press."); + executeSoon(function soon() { + dump("Closing pref pane.\n"); + prefWin.close(); + prefPaneClosed = true; + maybeFinish(); + }); + }, "advanced-pane-loaded", false); + + button.click(); + } + + notification1.addEventListener("AlertActive", function active1() { + notification1.removeEventListener("AlertActive", active1, true); + executeSoon(onAlertDisplayed); + }, true); + + notification2.addEventListener("AlertActive", function active2() { + notification2.removeEventListener("AlertActive", active2, true); + executeSoon(onAlertDisplayed); + }, true); + + policy = sendNotifyRequest("multiple_window_behavior"); + }); +} + diff --git a/browser/base/content/test/browser_disablechrome.js b/browser/base/content/test/browser_disablechrome.js new file mode 100644 index 000000000..24b3f4811 --- /dev/null +++ b/browser/base/content/test/browser_disablechrome.js @@ -0,0 +1,216 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tests that the disablechrome attribute gets propogated to the main UI + +const HTTPSRC = "http://example.com/browser/browser/base/content/test/"; + +function is_element_hidden(aElement) { + var style = window.getComputedStyle(document.getElementById("nav-bar"), ""); + if (style.visibility != "visible" || style.display == "none") + return true; + + if (aElement.ownerDocument != aElement.parentNode) + return is_element_hidden(aElement.parentNode); + + return false; +} + +function is_chrome_hidden() { + is(document.documentElement.getAttribute("disablechrome"), "true", "Attribute should be set"); + if (TabsOnTop.enabled) + ok(is_element_hidden(document.getElementById("nav-bar")), "Toolbar should be hidden"); + else + ok(!is_element_hidden(document.getElementById("nav-bar")), "Toolbar should not be hidden"); +} + +function is_chrome_visible() { + isnot(document.getElementById("main-window").getAttribute("disablechrome"), "true", "Attribute should not be set"); + ok(!is_element_hidden(document.getElementById("nav-bar")), "Toolbar should not be hidden"); +} + +function load_page(aURL, aCanHide, aCallback) { + gNewBrowser.addEventListener("pageshow", function() { + // Filter out about:blank loads + if (gNewBrowser.currentURI.spec != aURL) + return; + + gNewBrowser.removeEventListener("pageshow", arguments.callee, false); + + if (aCanHide) + is_chrome_hidden(); + else + is_chrome_visible(); + + if (aURL == "about:addons") { + function check_after_init() { + if (aCanHide) + is_chrome_hidden(); + else + is_chrome_visible(); + + aCallback(); + } + + if (gNewBrowser.contentWindow.gIsInitializing) { + gNewBrowser.contentDocument.addEventListener("Initialized", function() { + gNewBrowser.contentDocument.removeEventListener("Initialized", arguments.callee, false); + + check_after_init(); + }, false); + } + else { + check_after_init(); + } + } + else { + executeSoon(aCallback); + } + }, false); + gNewBrowser.loadURI(aURL); +} + +var gOldTab; +var gNewTab; +var gNewBrowser; + +function test() { + // Opening the add-ons manager and waiting for it to load the discovery pane + // takes more time in windows debug builds + requestLongerTimeout(2); + + var gOldTabsOnTop = TabsOnTop.enabled; + registerCleanupFunction(function() { + TabsOnTop.enabled = gOldTabsOnTop; + }); + + waitForExplicitFinish(); + + gOldTab = gBrowser.selectedTab; + gNewTab = gBrowser.selectedTab = gBrowser.addTab("about:blank"); + gNewBrowser = gBrowser.selectedBrowser; + + info("Tabs on top"); + TabsOnTop.enabled = true; + + run_http_test_1(); +} + +function end_test() { + gBrowser.removeTab(gNewTab); + finish(); +} + +function test_url(aURL, aCanHide, aNextTest) { + is_chrome_visible(); + info("Page load"); + load_page(aURL, aCanHide, function() { + info("Switch away"); + gBrowser.selectedTab = gOldTab; + is_chrome_visible(); + + info("Switch back"); + gBrowser.selectedTab = gNewTab; + if (aCanHide) + is_chrome_hidden(); + else + is_chrome_visible(); + + gBrowser.removeTab(gNewTab); + gNewTab = gBrowser.selectedTab = gBrowser.addTab("about:blank"); + gNewBrowser = gBrowser.selectedBrowser; + + gBrowser.selectedTab = gOldTab; + + info("Background load"); + load_page(aURL, false, function() { + info("Switch back"); + gBrowser.selectedTab = gNewTab; + if (aCanHide) + is_chrome_hidden(); + else + is_chrome_visible(); + + load_page("about:blank", false, aNextTest); + }); + }); +} + +// Should never hide the chrome +function run_http_test_1() { + info("HTTP tests"); + test_url(HTTPSRC + "disablechrome.html", false, run_chrome_about_test); +} + +// Should hide the chrome +function run_chrome_about_test() { + info("Chrome about: tests"); + test_url("about:addons", true, function() { + info("Tabs on bottom"); + TabsOnTop.enabled = false; + run_http_test_2(); + }); +} + +// Should never hide the chrome +function run_http_test_2() { + info("HTTP tests"); + test_url(HTTPSRC + "disablechrome.html", false, run_chrome_about_test_2); +} + +// Should not hide the chrome +function run_chrome_about_test_2() { + info("Chrome about: tests"); + test_url("about:addons", true, run_http_test3); +} + +function run_http_test3() { + info("HTTP tests"); + test_url(HTTPSRC + "disablechrome.html", false, run_chrome_about_test_3); +} + +// Should not hide the chrome +function run_chrome_about_test_3() { + info("Chrome about: tests"); + test_url("about:Addons", true, function(){ + info("Tabs on top"); + TabsOnTop.enabled = true; + run_http_test4(); + }); +} + +function run_http_test4() { + info("HTTP tests"); + test_url(HTTPSRC + "disablechrome.html", false, run_chrome_about_test_4); +} + +function run_chrome_about_test_4() { + info("Chrome about: tests"); + test_url("about:Addons", true, run_http_test5); + } + +function run_http_test5() { + info("HTTP tests"); + test_url(HTTPSRC + "disablechrome.html", false, run_chrome_about_test_5); +} + +// Should hide the chrome +function run_chrome_about_test_5() { + info("Chrome about: tests"); + test_url("about:preferences", true, function(){ + info("Tabs on bottom"); + TabsOnTop.enabled = false; + run_http_test6(); + }); +} + +function run_http_test6() { + info("HTTP tests"); + test_url(HTTPSRC + "disablechrome.html", false, run_chrome_about_test_6); +} + +function run_chrome_about_test_6() { + info("Chrome about: tests"); + test_url("about:preferences", true, end_test); +}
\ No newline at end of file diff --git a/browser/base/content/test/browser_discovery.js b/browser/base/content/test/browser_discovery.js new file mode 100644 index 000000000..61098fbef --- /dev/null +++ b/browser/base/content/test/browser_discovery.js @@ -0,0 +1,159 @@ +var browser; + +function doc() browser.contentDocument; + +function setHandlerFunc(aResultFunc) { + gBrowser.addEventListener("DOMLinkAdded", function (event) { + gBrowser.removeEventListener("DOMLinkAdded", arguments.callee, false); + executeSoon(aResultFunc); + }, false); +} + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function (event) { + event.currentTarget.removeEventListener("load", arguments.callee, true); + iconDiscovery(); + }, true); + var rootDir = getRootDirectory(gTestPath); + content.location = rootDir + "discovery.html"; +} + +var iconDiscoveryTests = [ + { text: "rel icon discovered" }, + { rel: "abcdefg icon qwerty", text: "rel may contain additional rels separated by spaces" }, + { rel: "ICON", text: "rel is case insensitive" }, + { rel: "shortcut-icon", pass: false, text: "rel shortcut-icon not discovered" }, + { href: "moz.png", text: "relative href works" }, + { href: "notthere.png", text: "404'd icon is removed properly" }, + { href: "data:image/x-icon,%00", type: "image/x-icon", text: "data: URIs work" }, + { type: "image/png; charset=utf-8", text: "type may have optional parameters (RFC2046)" } +]; + +function runIconDiscoveryTest() { + var test = iconDiscoveryTests[0]; + var head = doc().getElementById("linkparent"); + var hasSrc = gBrowser.getIcon() != null; + if (test.pass) + ok(hasSrc, test.text); + else + ok(!hasSrc, test.text); + + head.removeChild(head.getElementsByTagName('link')[0]); + iconDiscoveryTests.shift(); + iconDiscovery(); // Run the next test. +} + +function iconDiscovery() { + if (iconDiscoveryTests.length) { + setHandlerFunc(runIconDiscoveryTest); + gBrowser.setIcon(gBrowser.selectedTab, null); + + var test = iconDiscoveryTests[0]; + var head = doc().getElementById("linkparent"); + var link = doc().createElement("link"); + + var rootDir = getRootDirectory(gTestPath); + var rel = test.rel || "icon"; + var href = test.href || rootDir + "moz.png"; + var type = test.type || "image/png"; + if (test.pass == undefined) + test.pass = true; + + link.rel = rel; + link.href = href; + link.type = type; + head.appendChild(link); + } else { + searchDiscovery(); + } +} + +var searchDiscoveryTests = [ + { text: "rel search discovered" }, + { rel: "SEARCH", text: "rel is case insensitive" }, + { rel: "-search-", pass: false, text: "rel -search- not discovered" }, + { rel: "foo bar baz search quux", text: "rel may contain additional rels separated by spaces" }, + { href: "https://not.mozilla.com", text: "HTTPS ok" }, + { href: "ftp://not.mozilla.com", text: "FTP ok" }, + { href: "data:text/foo,foo", pass: false, text: "data URI not permitted" }, + { href: "javascript:alert(0)", pass: false, text: "JS URI not permitted" }, + { type: "APPLICATION/OPENSEARCHDESCRIPTION+XML", text: "type is case insensitve" }, + { type: " application/opensearchdescription+xml ", text: "type may contain extra whitespace" }, + { type: "application/opensearchdescription+xml; charset=utf-8", text: "type may have optional parameters (RFC2046)" }, + { type: "aapplication/opensearchdescription+xml", pass: false, text: "type should not be loosely matched" }, + { rel: "search search search", count: 1, text: "only one engine should be added" } +]; + +function runSearchDiscoveryTest() { + var test = searchDiscoveryTests[0]; + var title = test.title || searchDiscoveryTests.length; + if (browser.engines) { + var hasEngine = (test.count) ? (browser.engines[0].title == title && + browser.engines.length == test.count) : + (browser.engines[0].title == title); + ok(hasEngine, test.text); + browser.engines = null; + } + else + ok(!test.pass, test.text); + + searchDiscoveryTests.shift(); + searchDiscovery(); // Run the next test. +} + +// This handler is called twice, once for each added link element. +// Only want to check once the second link element has been added. +var ranOnce = false; +function runMultipleEnginesTestAndFinalize() { + if (!ranOnce) { + ranOnce = true; + return; + } + ok(browser.engines, "has engines"); + is(browser.engines.length, 1, "only one engine"); + is(browser.engines[0].uri, "http://first.mozilla.com/search.xml", "first engine wins"); + + gBrowser.removeCurrentTab(); + finish(); +} + +function searchDiscovery() { + var head = doc().getElementById("linkparent"); + + if (searchDiscoveryTests.length) { + setHandlerFunc(runSearchDiscoveryTest); + var test = searchDiscoveryTests[0]; + var link = doc().createElement("link"); + + var rel = test.rel || "search"; + var href = test.href || "http://so.not.here.mozilla.com/search.xml"; + var type = test.type || "application/opensearchdescription+xml"; + var title = test.title || searchDiscoveryTests.length; + if (test.pass == undefined) + test.pass = true; + + link.rel = rel; + link.href = href; + link.type = type; + link.title = title; + head.appendChild(link); + } else { + setHandlerFunc(runMultipleEnginesTestAndFinalize); + setHandlerFunc(runMultipleEnginesTestAndFinalize); + // Test multiple engines with the same title + var link = doc().createElement("link"); + link.rel = "search"; + link.href = "http://first.mozilla.com/search.xml"; + link.type = "application/opensearchdescription+xml"; + link.title = "Test Engine"; + var link2 = link.cloneNode(false); + link2.href = "http://second.mozilla.com/search.xml"; + + head.appendChild(link); + head.appendChild(link2); + } +} diff --git a/browser/base/content/test/browser_drag.js b/browser/base/content/test/browser_drag.js new file mode 100644 index 000000000..6aa14bea0 --- /dev/null +++ b/browser/base/content/test/browser_drag.js @@ -0,0 +1,45 @@ +function test() +{ + waitForExplicitFinish(); + + let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + let ChromeUtils = {}; + scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils); + + // ---- Test dragging the proxy icon --- + var value = content.location.href; + var urlString = value + "\n" + content.document.title; + var htmlString = "<a href=\"" + value + "\">" + value + "</a>"; + var expected = [ [ + { type : "text/x-moz-url", + data : urlString }, + { type : "text/uri-list", + data : value }, + { type : "text/plain", + data : value }, + { type : "text/html", + data : htmlString } + ] ]; + // set the valid attribute so dropping is allowed + var oldstate = gURLBar.getAttribute("pageproxystate"); + gURLBar.setAttribute("pageproxystate", "valid"); + var dt = EventUtils.synthesizeDragStart(document.getElementById("identity-box"), expected); + is(dt, null, "drag on proxy icon"); + gURLBar.setAttribute("pageproxystate", oldstate); + // Now, the identity information panel is opened by the proxy icon click. + // We need to close it for next tests. + EventUtils.synthesizeKey("VK_ESCAPE", {}, window); + + // now test dragging onto a tab + var tab = gBrowser.addTab("about:blank", {skipAnimation: true}); + var browser = gBrowser.getBrowserForTab(tab); + + browser.addEventListener("load", function () { + is(browser.contentWindow.location, "http://mochi.test:8888/", "drop on tab"); + gBrowser.removeTab(tab); + finish(); + }, true); + + ChromeUtils.synthesizeDrop(tab, tab, [[{type: "text/uri-list", data: "http://mochi.test:8888/"}]], "copy", window); +} diff --git a/browser/base/content/test/browser_duplicateIDs.js b/browser/base/content/test/browser_duplicateIDs.js new file mode 100644 index 000000000..38fc17820 --- /dev/null +++ b/browser/base/content/test/browser_duplicateIDs.js @@ -0,0 +1,8 @@ +function test() { + var ids = {}; + Array.forEach(document.querySelectorAll("[id]"), function (node) { + var id = node.id; + ok(!(id in ids), id + " should be unique"); + ids[id] = null; + }); +} diff --git a/browser/base/content/test/browser_findbarClose.js b/browser/base/content/test/browser_findbarClose.js new file mode 100644 index 000000000..7a4ff5470 --- /dev/null +++ b/browser/base/content/test/browser_findbarClose.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests find bar auto-close behavior + +let newTab, iframe; + +function test() { + waitForExplicitFinish(); + newTab = gBrowser.addTab("about:blank"); + newTab.linkedBrowser.addEventListener("DOMContentLoaded", + prepareTestFindBarStaysOpenOnSubdocumentLocationChange, false); + newTab.linkedBrowser.contentWindow.location = "http://example.com/browser/" + + "browser/base/content/test/test_bug628179.html"; +} + +function prepareTestFindBarStaysOpenOnSubdocumentLocationChange() { + newTab.linkedBrowser.removeEventListener("DOMContentLoaded", + prepareTestFindBarStaysOpenOnSubdocumentLocationChange, false); + + gFindBar.open(); + + iframe = newTab.linkedBrowser.contentDocument.getElementById("iframe"); + iframe.addEventListener("load", + testFindBarStaysOpenOnSubdocumentLocationChange, false); + iframe.src = "http://example.org/"; +} + +function testFindBarStaysOpenOnSubdocumentLocationChange() { + iframe.removeEventListener("load", + testFindBarStaysOpenOnSubdocumentLocationChange, false); + + ok(!gFindBar.hidden, "the Find bar isn't hidden after the location of a " + + "subdocument changes"); + + gFindBar.close(); + gBrowser.removeTab(newTab); + finish(); +} + diff --git a/browser/base/content/test/browser_fullscreen-window-open.js b/browser/base/content/test/browser_fullscreen-window-open.js new file mode 100644 index 000000000..4504cb104 --- /dev/null +++ b/browser/base/content/test/browser_fullscreen-window-open.js @@ -0,0 +1,394 @@ +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +let Cc = Components.classes; +let Ci = Components.interfaces; + +const PREF_DISABLE_OPEN_NEW_WINDOW = "browser.link.open_newwindow.disabled_in_fullscreen"; +const isOSX = (Services.appinfo.OS === "Darwin"); + +const TEST_FILE = "file_fullscreen-window-open.html"; +const gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", + "http://127.0.0.1:8888/"); + +function test () { + waitForExplicitFinish(); + + Services.prefs.setBoolPref(PREF_DISABLE_OPEN_NEW_WINDOW, true); + + let newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + + let gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", function onLoad(){ + gTestBrowser.removeEventListener("load", onLoad, true, true); + + // Enter browser fullscreen mode. + BrowserFullScreen(); + + runNextTest(); + }, true, true); + gTestBrowser.contentWindow.location.href = gHttpTestRoot + TEST_FILE; +} + +registerCleanupFunction(function(){ + // Exit browser fullscreen mode. + BrowserFullScreen(); + + gBrowser.removeCurrentTab(); + + Services.prefs.clearUserPref(PREF_DISABLE_OPEN_NEW_WINDOW); +}); + +let gTests = [ + test_open, + test_open_with_size, + test_open_with_pos, + test_open_with_outerSize, + test_open_with_innerSize, + test_open_with_dialog, + test_open_when_open_new_window_by_pref, + test_open_with_pref_to_disable_in_fullscreen, + test_open_from_chrome, +]; + +function runNextTest () { + let test = gTests.shift(); + if (test) { + executeSoon(test); + } + else { + finish(); + } +} + + +// Test for window.open() with no feature. +function test_open() { + waitForTabOpen({ + message: { + title: "test_open", + param: "", + }, + finalizeFn: function () {}, + }); +} + +// Test for window.open() with width/height. +function test_open_with_size() { + waitForTabOpen({ + message: { + title: "test_open_with_size", + param: "width=400,height=400", + }, + finalizeFn: function () {}, + }); +} + +// Test for window.open() with top/left. +function test_open_with_pos() { + waitForTabOpen({ + message: { + title: "test_open_with_pos", + param: "top=200,left=200", + }, + finalizeFn: function () {}, + }); +} + +// Test for window.open() with outerWidth/Height. +function test_open_with_outerSize() { + let [outerWidth, outerHeight] = [window.outerWidth, window.outerHeight]; + waitForTabOpen({ + message: { + title: "test_open_with_outerSize", + param: "outerWidth=200,outerHeight=200", + }, + successFn: function () { + is(window.outerWidth, outerWidth, "Don't change window.outerWidth."); + is(window.outerHeight, outerHeight, "Don't change window.outerHeight."); + }, + finalizeFn: function () {}, + }); +} + +// Test for window.open() with innerWidth/Height. +function test_open_with_innerSize() { + let [innerWidth, innerHeight] = [window.innerWidth, window.innerHeight]; + waitForTabOpen({ + message: { + title: "test_open_with_innerSize", + param: "innerWidth=200,innerHeight=200", + }, + successFn: function () { + is(window.innerWidth, innerWidth, "Don't change window.innerWidth."); + is(window.innerHeight, innerHeight, "Don't change window.innerHeight."); + }, + finalizeFn: function () {}, + }); +} + +// Test for window.open() with dialog. +function test_open_with_dialog() { + waitForTabOpen({ + message: { + title: "test_open_with_dialog", + param: "dialog=yes", + }, + finalizeFn: function () {}, + }); +} + +// Test for window.open() +// when "browser.link.open_newwindow" is nsIBrowserDOMWindow.OPEN_NEWWINDOW +function test_open_when_open_new_window_by_pref() { + const PREF_NAME = "browser.link.open_newwindow"; + Services.prefs.setIntPref(PREF_NAME, Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW); + is(Services.prefs.getIntPref(PREF_NAME), Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW, + PREF_NAME + " is nsIBrowserDOMWindow.OPEN_NEWWINDOW at this time"); + + waitForTabOpen({ + message: { + title: "test_open_when_open_new_window_by_pref", + param: "width=400,height=400", + }, + finalizeFn: function () { + Services.prefs.clearUserPref(PREF_NAME); + }, + }); +} + +// Test for the pref, "browser.link.open_newwindow.disabled_in_fullscreen" +function test_open_with_pref_to_disable_in_fullscreen() { + Services.prefs.setBoolPref(PREF_DISABLE_OPEN_NEW_WINDOW, false); + + waitForWindowOpen({ + message: { + title: "test_open_with_pref_disabled_in_fullscreen", + param: "width=400,height=400", + }, + finalizeFn: function () { + Services.prefs.setBoolPref(PREF_DISABLE_OPEN_NEW_WINDOW, true); + }, + }); +} + + +// Test for window.open() called from chrome context. +function test_open_from_chrome() { + waitForWindowOpenFromChrome({ + message: { + title: "test_open_from_chrome", + param: "", + }, + finalizeFn: function () {}, + timeout: 10000, + }); +} + +function waitForTabOpen(aOptions) { + let start = Date.now(); + let timeout = aOptions.timeout || 5000; + let message = aOptions.message; + + if (!message.title) { + ok(false, "Can't get message.title."); + aOptions.finalizeFn(); + runNextTest(); + return; + } + + info("Running test: " + message.title); + + let onTabOpen = function onTabOpen(aEvent) { + gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true); + + let tab = aEvent.target; + tab.linkedBrowser.addEventListener("load", function onLoad(ev){ + let browser = ev.currentTarget; + browser.removeEventListener("load", onLoad, true, true); + clearTimeout(onTimeout); + + is(browser.contentWindow.document.title, message.title, + "Opened Tab is expected: " + message.title); + + if (aOptions.successFn) { + aOptions.successFn(); + } + + gBrowser.removeTab(tab); + finalize(); + }, true, true); + } + gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true); + + let finalize = function () { + aOptions.finalizeFn(); + info("Finished: " + message.title); + runNextTest(); + }; + + let onTimeout = setTimeout(function(){ + gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true); + + ok(false, "Timeout: '"+message.title + "'."); + finalize(); + }, timeout); + + + const URI = "data:text/html;charset=utf-8,<!DOCTYPE html><html><head><title>"+ + message.title + + "<%2Ftitle><%2Fhead><body><%2Fbody><%2Fhtml>"; + + executeWindowOpenInContent({ + uri: URI, + title: message.title, + option: message.param, + }); +} + + +function waitForWindowOpen(aOptions) { + let start = Date.now(); + let timeout = aOptions.timeout || 10000; + let message = aOptions.message; + let url = aOptions.url || getBrowserURL(); + + if (!message.title) { + ok(false, "Can't get message.title"); + aOptions.finalizeFn(); + runNextTest(); + return; + } + + info("Running test: " + message.title); + + let onFinalize = function () { + aOptions.finalizeFn(); + + info("Finished: " + message.title); + runNextTest(); + }; + + let onTimeout = setTimeout(function(){ + Services.wm.removeListener(listener); + ok(false, "Fail: '"+message.title + "'."); + + onFinalize(); + }, timeout); + + let listener = new WindowListener(message.title, url, { + onSuccess: aOptions.successFn, + onTimeout: onTimeout, + onFinalize: onFinalize, + }); + Services.wm.addListener(listener); + + const URI = aOptions.url || "about:blank"; + + executeWindowOpenInContent({ + uri: URI, + title: message.title, + option: message.param, + }); +} + +function executeWindowOpenInContent(aParam) { + var testWindow = gBrowser.selectedBrowser.contentWindow; + var testElm = testWindow.document.getElementById("test"); + + testElm.setAttribute("data-test-param", JSON.stringify(aParam)); + EventUtils.synthesizeMouseAtCenter(testElm, {}, testWindow); +} + +function waitForWindowOpenFromChrome(aOptions) { + let start = Date.now(); + let timeout = aOptions.timeout || 10000; + let message = aOptions.message; + let url = aOptions.url || getBrowserURL(); + + if (!message.title) { + ok(false, "Can't get message.title"); + aOptions.finalizeFn(); + runNextTest(); + return; + } + + info("Running test: " + message.title); + + let onFinalize = function () { + aOptions.finalizeFn(); + + info("Finished: " + message.title); + runNextTest(); + }; + + let onTimeout = setTimeout(function(){ + Services.wm.removeListener(listener); + ok(false, "Fail: '"+message.title + "'."); + + testWindow.close(); + onFinalize(); + }, timeout); + + let listener = new WindowListener(message.title, url, { + onSuccess: aOptions.successFn, + onTimeout: onTimeout, + onFinalize: onFinalize, + }); + Services.wm.addListener(listener); + + + const URI = aOptions.url || "about:blank"; + + let testWindow = window.open(URI, message.title, message.option); +} + +function WindowListener(aTitle, aUrl, aCallBackObj) { + this.test_title = aTitle; + this.test_url = aUrl; + this.callback_onSuccess = aCallBackObj.onSuccess; + this.callBack_onTimeout = aCallBackObj.onTimeout; + this.callBack_onFinalize = aCallBackObj.onFinalize; +} +WindowListener.prototype = { + + test_title: null, + test_url: null, + callback_onSuccess: null, + callBack_onTimeout: null, + callBack_onFinalize: null, + + onOpenWindow: function(aXULWindow) { + Services.wm.removeListener(this); + + let domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + domwindow.addEventListener("load", function onLoad(aEvent) { + is(domwindow.document.location.href, this.test_url, + "Opened Window is expected: "+ this.test_title); + if (this.callback_onSuccess) { + this.callback_onSuccess(); + } + + domwindow.removeEventListener("load", onLoad, true); + clearTimeout(this.callBack_onTimeout); + + // wait for trasition to fullscreen on OSX Lion later + if (isOSX) { + setTimeout(function(){ + domwindow.close(); + executeSoon(this.callBack_onFinalize); + }.bind(this), 3000); + } + else { + domwindow.close(); + executeSoon(this.callBack_onFinalize); + } + }.bind(this), true); + }, + onCloseWindow: function(aXULWindow) {}, + onWindowTitleChange: function(aXULWindow, aNewTitle) {}, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowMediatorListener, + Ci.nsISupports]), +}; diff --git a/browser/base/content/test/browser_gestureSupport.js b/browser/base/content/test/browser_gestureSupport.js new file mode 100644 index 000000000..d60ae58f4 --- /dev/null +++ b/browser/base/content/test/browser_gestureSupport.js @@ -0,0 +1,671 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 tests +// +// These tests require the ability to disable the fact that the +// Firefox chrome intentionally prevents "simple gesture" events from +// reaching web content. + +let test_utils; +let test_commandset; +let test_prefBranch = "browser.gesture."; + +function test() +{ + waitForExplicitFinish(); + + // Disable the default gestures support during the test + gGestureSupport.init(false); + + test_utils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIDOMWindowUtils); + + // Run the tests of "simple gesture" events generally + test_EnsureConstantsAreDisjoint(); + test_TestEventListeners(); + test_TestEventCreation(); + + // Reenable the default gestures support. The remaining tests target + // the Firefox gesture functionality. + gGestureSupport.init(true); + + // Test Firefox's gestures support. + test_commandset = document.getElementById("mainCommandSet"); + test_swipeGestures(); + test_latchedGesture("pinch", "out", "in", "MozMagnifyGesture"); + test_thresholdGesture("pinch", "out", "in", "MozMagnifyGesture"); + test_rotateGestures(); +} + +let test_eventCount = 0; +let test_expectedType; +let test_expectedDirection; +let test_expectedDelta; +let test_expectedModifiers; +let test_expectedClickCount; +let test_imageTab; + +function test_gestureListener(evt) +{ + is(evt.type, test_expectedType, + "evt.type (" + evt.type + ") does not match expected value"); + is(evt.target, test_utils.elementFromPoint(20, 20, false, false), + "evt.target (" + evt.target + ") does not match expected value"); + is(evt.clientX, 20, + "evt.clientX (" + evt.clientX + ") does not match expected value"); + is(evt.clientY, 20, + "evt.clientY (" + evt.clientY + ") does not match expected value"); + isnot(evt.screenX, 0, + "evt.screenX (" + evt.screenX + ") does not match expected value"); + isnot(evt.screenY, 0, + "evt.screenY (" + evt.screenY + ") does not match expected value"); + + is(evt.direction, test_expectedDirection, + "evt.direction (" + evt.direction + ") does not match expected value"); + is(evt.delta, test_expectedDelta, + "evt.delta (" + evt.delta + ") does not match expected value"); + + is(evt.shiftKey, (test_expectedModifiers & Components.interfaces.nsIDOMEvent.SHIFT_MASK) != 0, + "evt.shiftKey did not match expected value"); + is(evt.ctrlKey, (test_expectedModifiers & Components.interfaces.nsIDOMEvent.CONTROL_MASK) != 0, + "evt.ctrlKey did not match expected value"); + is(evt.altKey, (test_expectedModifiers & Components.interfaces.nsIDOMEvent.ALT_MASK) != 0, + "evt.altKey did not match expected value"); + is(evt.metaKey, (test_expectedModifiers & Components.interfaces.nsIDOMEvent.META_MASK) != 0, + "evt.metaKey did not match expected value"); + + if (evt.type == "MozTapGesture") { + is(evt.clickCount, test_expectedClickCount, "evt.clickCount does not match"); + } + + test_eventCount++; +} + +function test_helper1(type, direction, delta, modifiers) +{ + // Setup the expected values + test_expectedType = type; + test_expectedDirection = direction; + test_expectedDelta = delta; + test_expectedModifiers = modifiers; + + let expectedEventCount = test_eventCount + 1; + + document.addEventListener(type, test_gestureListener, true); + test_utils.sendSimpleGestureEvent(type, 20, 20, direction, delta, modifiers); + document.removeEventListener(type, test_gestureListener, true); + + is(expectedEventCount, test_eventCount, "Event (" + type + ") was never received by event listener"); +} + +function test_clicks(type, clicks) +{ + // Setup the expected values + test_expectedType = type; + test_expectedDirection = 0; + test_expectedDelta = 0; + test_expectedModifiers = 0; + test_expectedClickCount = clicks; + + let expectedEventCount = test_eventCount + 1; + + document.addEventListener(type, test_gestureListener, true); + test_utils.sendSimpleGestureEvent(type, 20, 20, 0, 0, 0, clicks); + document.removeEventListener(type, test_gestureListener, true); + + is(expectedEventCount, test_eventCount, "Event (" + type + ") was never received by event listener"); +} + +function test_TestEventListeners() +{ + let e = test_helper1; // easier to type this name + + // Swipe gesture animation events + e("MozSwipeGestureStart", 0, -0.7, 0); + e("MozSwipeGestureUpdate", 0, -0.4, 0); + e("MozSwipeGestureEnd", 0, 0, 0); + e("MozSwipeGestureStart", 0, 0.6, 0); + e("MozSwipeGestureUpdate", 0, 0.3, 0); + e("MozSwipeGestureEnd", 0, 1, 0); + + // Swipe gesture event + e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_LEFT, 0.0, 0); + e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0.0, 0); + e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_UP, 0.0, 0); + e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_DOWN, 0.0, 0); + e("MozSwipeGesture", + SimpleGestureEvent.DIRECTION_UP | SimpleGestureEvent.DIRECTION_LEFT, 0.0, 0); + e("MozSwipeGesture", + SimpleGestureEvent.DIRECTION_DOWN | SimpleGestureEvent.DIRECTION_RIGHT, 0.0, 0); + e("MozSwipeGesture", + SimpleGestureEvent.DIRECTION_UP | SimpleGestureEvent.DIRECTION_RIGHT, 0.0, 0); + e("MozSwipeGesture", + SimpleGestureEvent.DIRECTION_DOWN | SimpleGestureEvent.DIRECTION_LEFT, 0.0, 0); + + // magnify gesture events + e("MozMagnifyGestureStart", 0, 50.0, 0); + e("MozMagnifyGestureUpdate", 0, -25.0, 0); + e("MozMagnifyGestureUpdate", 0, 5.0, 0); + e("MozMagnifyGesture", 0, 30.0, 0); + + // rotate gesture events + e("MozRotateGestureStart", SimpleGestureEvent.ROTATION_CLOCKWISE, 33.0, 0); + e("MozRotateGestureUpdate", SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE, -13.0, 0); + e("MozRotateGestureUpdate", SimpleGestureEvent.ROTATION_CLOCKWISE, 13.0, 0); + e("MozRotateGesture", SimpleGestureEvent.ROTATION_CLOCKWISE, 33.0, 0); + + // Tap and presstap gesture events + test_clicks("MozTapGesture", 1); + test_clicks("MozTapGesture", 2); + test_clicks("MozTapGesture", 3); + test_clicks("MozPressTapGesture", 1); + + // simple delivery test for edgeui gestures + e("MozEdgeUIStarted", 0, 0, 0); + e("MozEdgeUICanceled", 0, 0, 0); + e("MozEdgeUICompleted", 0, 0, 0); + + // event.shiftKey + let modifier = Components.interfaces.nsIDOMEvent.SHIFT_MASK; + e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier); + + // event.metaKey + modifier = Components.interfaces.nsIDOMEvent.META_MASK; + e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier); + + // event.altKey + modifier = Components.interfaces.nsIDOMEvent.ALT_MASK; + e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier); + + // event.ctrlKey + modifier = Components.interfaces.nsIDOMEvent.CONTROL_MASK; + e("MozSwipeGesture", SimpleGestureEvent.DIRECTION_RIGHT, 0, modifier); +} + +function test_eventDispatchListener(evt) +{ + test_eventCount++; + evt.stopPropagation(); +} + +function test_helper2(type, direction, delta, altKey, ctrlKey, shiftKey, metaKey) +{ + let event = null; + let successful; + + try { + event = document.createEvent("SimpleGestureEvent"); + successful = true; + } + catch (ex) { + successful = false; + } + ok(successful, "Unable to create SimpleGestureEvent"); + + try { + event.initSimpleGestureEvent(type, true, true, window, 1, + 10, 10, 10, 10, + ctrlKey, altKey, shiftKey, metaKey, + 1, window, + 0, direction, delta, 0); + successful = true; + } + catch (ex) { + successful = false; + } + ok(successful, "event.initSimpleGestureEvent should not fail"); + + // Make sure the event fields match the expected values + is(event.type, type, "Mismatch on evt.type"); + is(event.direction, direction, "Mismatch on evt.direction"); + is(event.delta, delta, "Mismatch on evt.delta"); + is(event.altKey, altKey, "Mismatch on evt.altKey"); + is(event.ctrlKey, ctrlKey, "Mismatch on evt.ctrlKey"); + is(event.shiftKey, shiftKey, "Mismatch on evt.shiftKey"); + is(event.metaKey, metaKey, "Mismatch on evt.metaKey"); + is(event.view, window, "Mismatch on evt.view"); + is(event.detail, 1, "Mismatch on evt.detail"); + is(event.clientX, 10, "Mismatch on evt.clientX"); + is(event.clientY, 10, "Mismatch on evt.clientY"); + is(event.screenX, 10, "Mismatch on evt.screenX"); + is(event.screenY, 10, "Mismatch on evt.screenY"); + is(event.button, 1, "Mismatch on evt.button"); + is(event.relatedTarget, window, "Mismatch on evt.relatedTarget"); + + // Test event dispatch + let expectedEventCount = test_eventCount + 1; + document.addEventListener(type, test_eventDispatchListener, true); + document.dispatchEvent(event); + document.removeEventListener(type, test_eventDispatchListener, true); + is(expectedEventCount, test_eventCount, "Dispatched event was never received by listener"); +} + +function test_TestEventCreation() +{ + // Event creation + test_helper2("MozMagnifyGesture", SimpleGestureEvent.DIRECTION_RIGHT, 20.0, + true, false, true, false); + test_helper2("MozMagnifyGesture", SimpleGestureEvent.DIRECTION_LEFT, -20.0, + false, true, false, true); +} + +function test_EnsureConstantsAreDisjoint() +{ + let up = SimpleGestureEvent.DIRECTION_UP; + let down = SimpleGestureEvent.DIRECTION_DOWN; + let left = SimpleGestureEvent.DIRECTION_LEFT; + let right = SimpleGestureEvent.DIRECTION_RIGHT; + + let clockwise = SimpleGestureEvent.ROTATION_CLOCKWISE; + let cclockwise = SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE; + + ok(up ^ down, "DIRECTION_UP and DIRECTION_DOWN are not bitwise disjoint"); + ok(up ^ left, "DIRECTION_UP and DIRECTION_LEFT are not bitwise disjoint"); + ok(up ^ right, "DIRECTION_UP and DIRECTION_RIGHT are not bitwise disjoint"); + ok(down ^ left, "DIRECTION_DOWN and DIRECTION_LEFT are not bitwise disjoint"); + ok(down ^ right, "DIRECTION_DOWN and DIRECTION_RIGHT are not bitwise disjoint"); + ok(left ^ right, "DIRECTION_LEFT and DIRECTION_RIGHT are not bitwise disjoint"); + ok(clockwise ^ cclockwise, "ROTATION_CLOCKWISE and ROTATION_COUNTERCLOCKWISE are not bitwise disjoint"); +} + +// Helper for test of latched event processing. Emits the actual +// gesture events to test whether the commands associated with the +// gesture will only trigger once for each direction of movement. +function test_emitLatchedEvents(eventPrefix, initialDelta, cmd) +{ + let cumulativeDelta = 0; + let isIncreasing = initialDelta > 0; + + let expect = {}; + // Reset the call counters and initialize expected values + for (let dir in cmd) + cmd[dir].callCount = expect[dir] = 0; + + let check = function(aDir, aMsg) ok(cmd[aDir].callCount == expect[aDir], aMsg); + let checkBoth = function(aNum, aInc, aDec) { + let prefix = "Step " + aNum + ": "; + check("inc", prefix + aInc); + check("dec", prefix + aDec); + }; + + // Send the "Start" event. + test_utils.sendSimpleGestureEvent(eventPrefix + "Start", 0, 0, 0, initialDelta, 0); + cumulativeDelta += initialDelta; + if (isIncreasing) { + expect.inc++; + checkBoth(1, "Increasing command was not triggered", "Decreasing command was triggered"); + } else { + expect.dec++; + checkBoth(1, "Increasing command was triggered", "Decreasing command was not triggered"); + } + + // Send random values in the same direction and ensure neither + // command triggers. + for (let i = 0; i < 5; i++) { + let delta = Math.random() * (isIncreasing ? 100 : -100); + test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, delta, 0); + cumulativeDelta += delta; + checkBoth(2, "Increasing command was triggered", "Decreasing command was triggered"); + } + + // Now go back in the opposite direction. + test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, + - initialDelta, 0); + cumulativeDelta += - initialDelta; + if (isIncreasing) { + expect.dec++; + checkBoth(3, "Increasing command was triggered", "Decreasing command was not triggered"); + } else { + expect.inc++; + checkBoth(3, "Increasing command was not triggered", "Decreasing command was triggered"); + } + + // Send random values in the opposite direction and ensure neither + // command triggers. + for (let i = 0; i < 5; i++) { + let delta = Math.random() * (isIncreasing ? -100 : 100); + test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, delta, 0); + cumulativeDelta += delta; + checkBoth(4, "Increasing command was triggered", "Decreasing command was triggered"); + } + + // Go back to the original direction. The original command should trigger. + test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, + initialDelta, 0); + cumulativeDelta += initialDelta; + if (isIncreasing) { + expect.inc++; + checkBoth(5, "Increasing command was not triggered", "Decreasing command was triggered"); + } else { + expect.dec++; + checkBoth(5, "Increasing command was triggered", "Decreasing command was not triggered"); + } + + // Send the wrap-up event. No commands should be triggered. + test_utils.sendSimpleGestureEvent(eventPrefix, 0, 0, 0, cumulativeDelta, 0); + checkBoth(6, "Increasing command was triggered", "Decreasing command was triggered"); +} + +function test_addCommand(prefName, id) +{ + let cmd = test_commandset.appendChild(document.createElement("command")); + cmd.setAttribute("id", id); + cmd.setAttribute("oncommand", "this.callCount++;"); + + cmd.origPrefName = prefName; + cmd.origPrefValue = gPrefService.getCharPref(prefName); + gPrefService.setCharPref(prefName, id); + + return cmd; +} + +function test_removeCommand(cmd) +{ + gPrefService.setCharPref(cmd.origPrefName, cmd.origPrefValue); + test_commandset.removeChild(cmd); +} + +// Test whether latched events are only called once per direction of motion. +function test_latchedGesture(gesture, inc, dec, eventPrefix) +{ + let branch = test_prefBranch + gesture + "."; + + // Put the gesture into latched mode. + let oldLatchedValue = gPrefService.getBoolPref(branch + "latched"); + gPrefService.setBoolPref(branch + "latched", true); + + // Install the test commands for increasing and decreasing motion. + let cmd = { + inc: test_addCommand(branch + inc, "test:incMotion"), + dec: test_addCommand(branch + dec, "test:decMotion"), + }; + + // Test the gestures in each direction. + test_emitLatchedEvents(eventPrefix, 500, cmd); + test_emitLatchedEvents(eventPrefix, -500, cmd); + + // Restore the gesture to its original configuration. + gPrefService.setBoolPref(branch + "latched", oldLatchedValue); + for (let dir in cmd) + test_removeCommand(cmd[dir]); +} + +// Test whether non-latched events are triggered upon sufficient motion. +function test_thresholdGesture(gesture, inc, dec, eventPrefix) +{ + let branch = test_prefBranch + gesture + "."; + + // Disable latched mode for this gesture. + let oldLatchedValue = gPrefService.getBoolPref(branch + "latched"); + gPrefService.setBoolPref(branch + "latched", false); + + // Set the triggering threshold value to 50. + let oldThresholdValue = gPrefService.getIntPref(branch + "threshold"); + gPrefService.setIntPref(branch + "threshold", 50); + + // Install the test commands for increasing and decreasing motion. + let cmdInc = test_addCommand(branch + inc, "test:incMotion"); + let cmdDec = test_addCommand(branch + dec, "test:decMotion"); + + // Send the start event but stop short of triggering threshold. + cmdInc.callCount = cmdDec.callCount = 0; + test_utils.sendSimpleGestureEvent(eventPrefix + "Start", 0, 0, 0, 49.5, 0); + ok(cmdInc.callCount == 0, "Increasing command was triggered"); + ok(cmdDec.callCount == 0, "Decreasing command was triggered"); + + // Now trigger the threshold. + cmdInc.callCount = cmdDec.callCount = 0; + test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, 1, 0); + ok(cmdInc.callCount == 1, "Increasing command was not triggered"); + ok(cmdDec.callCount == 0, "Decreasing command was triggered"); + + // The tracking counter should go to zero. Go back the other way and + // stop short of triggering the threshold. + cmdInc.callCount = cmdDec.callCount = 0; + test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, -49.5, 0); + ok(cmdInc.callCount == 0, "Increasing command was triggered"); + ok(cmdDec.callCount == 0, "Decreasing command was triggered"); + + // Now cross the threshold and trigger the decreasing command. + cmdInc.callCount = cmdDec.callCount = 0; + test_utils.sendSimpleGestureEvent(eventPrefix + "Update", 0, 0, 0, -1.5, 0); + ok(cmdInc.callCount == 0, "Increasing command was triggered"); + ok(cmdDec.callCount == 1, "Decreasing command was not triggered"); + + // Send the wrap-up event. No commands should trigger. + cmdInc.callCount = cmdDec.callCount = 0; + test_utils.sendSimpleGestureEvent(eventPrefix, 0, 0, 0, -0.5, 0); + ok(cmdInc.callCount == 0, "Increasing command was triggered"); + ok(cmdDec.callCount == 0, "Decreasing command was triggered"); + + // Restore the gesture to its original configuration. + gPrefService.setBoolPref(branch + "latched", oldLatchedValue); + gPrefService.setIntPref(branch + "threshold", oldThresholdValue); + test_removeCommand(cmdInc); + test_removeCommand(cmdDec); +} + +function test_swipeGestures() +{ + // easier to type names for the direction constants + let up = SimpleGestureEvent.DIRECTION_UP; + let down = SimpleGestureEvent.DIRECTION_DOWN; + let left = SimpleGestureEvent.DIRECTION_LEFT; + let right = SimpleGestureEvent.DIRECTION_RIGHT; + + let branch = test_prefBranch + "swipe."; + + // Install the test commands for the swipe gestures. + let cmdUp = test_addCommand(branch + "up", "test:swipeUp"); + let cmdDown = test_addCommand(branch + "down", "test:swipeDown"); + let cmdLeft = test_addCommand(branch + "left", "test:swipeLeft"); + let cmdRight = test_addCommand(branch + "right", "test:swipeRight"); + + function resetCounts() { + cmdUp.callCount = 0; + cmdDown.callCount = 0; + cmdLeft.callCount = 0; + cmdRight.callCount = 0; + } + + // UP + resetCounts(); + test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, up, 0, 0); + ok(cmdUp.callCount == 1, "Step 1: Up command was not triggered"); + ok(cmdDown.callCount == 0, "Step 1: Down command was triggered"); + ok(cmdLeft.callCount == 0, "Step 1: Left command was triggered"); + ok(cmdRight.callCount == 0, "Step 1: Right command was triggered"); + + // DOWN + resetCounts(); + test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, down, 0, 0); + ok(cmdUp.callCount == 0, "Step 2: Up command was triggered"); + ok(cmdDown.callCount == 1, "Step 2: Down command was not triggered"); + ok(cmdLeft.callCount == 0, "Step 2: Left command was triggered"); + ok(cmdRight.callCount == 0, "Step 2: Right command was triggered"); + + // LEFT + resetCounts(); + test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, left, 0, 0); + ok(cmdUp.callCount == 0, "Step 3: Up command was triggered"); + ok(cmdDown.callCount == 0, "Step 3: Down command was triggered"); + ok(cmdLeft.callCount == 1, "Step 3: Left command was not triggered"); + ok(cmdRight.callCount == 0, "Step 3: Right command was triggered"); + + // RIGHT + resetCounts(); + test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, right, 0, 0); + ok(cmdUp.callCount == 0, "Step 4: Up command was triggered"); + ok(cmdDown.callCount == 0, "Step 4: Down command was triggered"); + ok(cmdLeft.callCount == 0, "Step 4: Left command was triggered"); + ok(cmdRight.callCount == 1, "Step 4: Right command was not triggered"); + + // Make sure combinations do not trigger events. + let combos = [ up | left, up | right, down | left, down | right]; + for (let i = 0; i < combos.length; i++) { + resetCounts(); + test_utils.sendSimpleGestureEvent("MozSwipeGesture", 0, 0, combos[i], 0, 0); + ok(cmdUp.callCount == 0, "Step 5-"+i+": Up command was triggered"); + ok(cmdDown.callCount == 0, "Step 5-"+i+": Down command was triggered"); + ok(cmdLeft.callCount == 0, "Step 5-"+i+": Left command was triggered"); + ok(cmdRight.callCount == 0, "Step 5-"+i+": Right command was triggered"); + } + + // Remove the test commands. + test_removeCommand(cmdUp); + test_removeCommand(cmdDown); + test_removeCommand(cmdLeft); + test_removeCommand(cmdRight); +} + + +function test_rotateHelperGetImageRotation(aImageElement) +{ + // Get the true image rotation from the transform matrix, bounded + // to 0 <= result < 360 + let transformValue = content.window.getComputedStyle(aImageElement, null) + .transform; + if (transformValue == "none") + return 0; + + transformValue = transformValue.split("(")[1] + .split(")")[0] + .split(","); + var rotation = Math.round(Math.atan2(transformValue[1], transformValue[0]) * + (180 / Math.PI)); + return (rotation < 0 ? rotation + 360 : rotation); +} + +function test_rotateHelperOneGesture(aImageElement, aCurrentRotation, + aDirection, aAmount, aStop) +{ + if (aAmount <= 0 || aAmount > 90) // Bound to 0 < aAmount <= 90 + return; + + // easier to type names for the direction constants + let clockwise = SimpleGestureEvent.ROTATION_CLOCKWISE; + let cclockwise = SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE; + + let delta = aAmount * (aDirection == clockwise ? 1 : -1); + + // Kill transition time on image so test isn't wrong and doesn't take 10 seconds + aImageElement.style.transitionDuration = "0s"; + + // Start the gesture, perform an update, and force flush + test_utils.sendSimpleGestureEvent("MozRotateGestureStart", 0, 0, aDirection, .001, 0); + test_utils.sendSimpleGestureEvent("MozRotateGestureUpdate", 0, 0, aDirection, delta, 0); + aImageElement.clientTop; + + // If stop, check intermediate + if (aStop) { + // Send near-zero-delta to stop, and force flush + test_utils.sendSimpleGestureEvent("MozRotateGestureUpdate", 0, 0, aDirection, .001, 0); + aImageElement.clientTop; + + let stopExpectedRotation = (aCurrentRotation + delta) % 360; + if (stopExpectedRotation < 0) + stopExpectedRotation += 360; + + is(stopExpectedRotation, test_rotateHelperGetImageRotation(aImageElement), + "Image rotation at gesture stop/hold: expected=" + stopExpectedRotation + + ", observed=" + test_rotateHelperGetImageRotation(aImageElement) + + ", init=" + aCurrentRotation + + ", amt=" + aAmount + + ", dir=" + (aDirection == clockwise ? "cl" : "ccl")); + } + // End it and force flush + test_utils.sendSimpleGestureEvent("MozRotateGesture", 0, 0, aDirection, 0, 0); + aImageElement.clientTop; + + let finalExpectedRotation; + + if (aAmount < 45 && aStop) { + // Rotate a bit, then stop. Expect no change at end of gesture. + finalExpectedRotation = aCurrentRotation; + } + else { + // Either not stopping (expect 90 degree change in aDirection), OR + // stopping but after 45, (expect 90 degree change in aDirection) + finalExpectedRotation = (aCurrentRotation + + (aDirection == clockwise ? 1 : -1) * 90) % 360; + if (finalExpectedRotation < 0) + finalExpectedRotation += 360; + } + + is(finalExpectedRotation, test_rotateHelperGetImageRotation(aImageElement), + "Image rotation gesture end: expected=" + finalExpectedRotation + + ", observed=" + test_rotateHelperGetImageRotation(aImageElement) + + ", init=" + aCurrentRotation + + ", amt=" + aAmount + + ", dir=" + (aDirection == clockwise ? "cl" : "ccl")); +} + +function test_rotateGesturesOnTab() +{ + gBrowser.selectedBrowser.removeEventListener("load", test_rotateGesturesOnTab, true); + + if (!(content.document instanceof ImageDocument)) { + ok(false, "Image document failed to open for rotation testing"); + gBrowser.removeTab(test_imageTab); + finish(); + return; + } + + // easier to type names for the direction constants + let cl = SimpleGestureEvent.ROTATION_CLOCKWISE; + let ccl = SimpleGestureEvent.ROTATION_COUNTERCLOCKWISE; + + let imgElem = content.document.body && + content.document.body.firstElementChild; + + if (!imgElem) { + ok(false, "Could not get image element on ImageDocument for rotation!"); + gBrowser.removeTab(test_imageTab); + finish(); + return; + } + + // Quick function to normalize rotation to 0 <= r < 360 + var normRot = function(rotation) { + rotation = rotation % 360; + if (rotation < 0) + rotation += 360; + return rotation; + } + + for (var initRot = 0; initRot < 360; initRot += 90) { + // Test each case: at each 90 degree snap; cl/ccl; + // amount more or less than 45; stop and hold or don't (32 total tests) + // The amount added to the initRot is where it is expected to be + test_rotateHelperOneGesture(imgElem, normRot(initRot + 0), cl, 35, true ); + test_rotateHelperOneGesture(imgElem, normRot(initRot + 0), cl, 35, false); + test_rotateHelperOneGesture(imgElem, normRot(initRot + 90), cl, 55, true ); + test_rotateHelperOneGesture(imgElem, normRot(initRot + 180), cl, 55, false); + test_rotateHelperOneGesture(imgElem, normRot(initRot + 270), ccl, 35, true ); + test_rotateHelperOneGesture(imgElem, normRot(initRot + 270), ccl, 35, false); + test_rotateHelperOneGesture(imgElem, normRot(initRot + 180), ccl, 55, true ); + test_rotateHelperOneGesture(imgElem, normRot(initRot + 90), ccl, 55, false); + + // Manually rotate it 90 degrees clockwise to prepare for next iteration, + // and force flush + test_utils.sendSimpleGestureEvent("MozRotateGestureStart", 0, 0, cl, .001, 0); + test_utils.sendSimpleGestureEvent("MozRotateGestureUpdate", 0, 0, cl, 90, 0); + test_utils.sendSimpleGestureEvent("MozRotateGestureUpdate", 0, 0, cl, .001, 0); + test_utils.sendSimpleGestureEvent("MozRotateGesture", 0, 0, cl, 0, 0); + imgElem.clientTop; + } + + gBrowser.removeTab(test_imageTab); + test_imageTab = null; + finish(); +} + +function test_rotateGestures() +{ + test_imageTab = gBrowser.addTab("chrome://branding/content/about-logo.png"); + gBrowser.selectedTab = test_imageTab; + + gBrowser.selectedBrowser.addEventListener("load", test_rotateGesturesOnTab, true); +} diff --git a/browser/base/content/test/browser_getshortcutoruri.js b/browser/base/content/test/browser_getshortcutoruri.js new file mode 100644 index 000000000..39a24f605 --- /dev/null +++ b/browser/base/content/test/browser_getshortcutoruri.js @@ -0,0 +1,146 @@ +function getPostDataString(aIS) { + if (!aIS) + return null; + + var sis = Cc["@mozilla.org/scriptableinputstream;1"]. + createInstance(Ci.nsIScriptableInputStream); + sis.init(aIS); + var dataLines = sis.read(aIS.available()).split("\n"); + + // only want the last line + return dataLines[dataLines.length-1]; +} + +function keywordResult(aURL, aPostData, aIsUnsafe) { + this.url = aURL; + this.postData = aPostData; + this.isUnsafe = aIsUnsafe; +} + +function keyWordData() {} +keyWordData.prototype = { + init: function(aKeyWord, aURL, aPostData, aSearchWord) { + this.keyword = aKeyWord; + this.uri = makeURI(aURL); + this.postData = aPostData; + this.searchWord = aSearchWord; + + this.method = (this.postData ? "POST" : "GET"); + } +} + +function bmKeywordData(aKeyWord, aURL, aPostData, aSearchWord) { + this.init(aKeyWord, aURL, aPostData, aSearchWord); +} +bmKeywordData.prototype = new keyWordData(); + +function searchKeywordData(aKeyWord, aURL, aPostData, aSearchWord) { + this.init(aKeyWord, aURL, aPostData, aSearchWord); +} +searchKeywordData.prototype = new keyWordData(); + +var testData = [ + [new bmKeywordData("bmget", "http://bmget/search=%s", null, "foo"), + new keywordResult("http://bmget/search=foo", null)], + + [new bmKeywordData("bmpost", "http://bmpost/", "search=%s", "foo2"), + new keywordResult("http://bmpost/", "search=foo2")], + + [new bmKeywordData("bmpostget", "http://bmpostget/search1=%s", "search2=%s", "foo3"), + new keywordResult("http://bmpostget/search1=foo3", "search2=foo3")], + + [new bmKeywordData("bmget-nosearch", "http://bmget-nosearch/", null, ""), + new keywordResult("http://bmget-nosearch/", null)], + + [new searchKeywordData("searchget", "http://searchget/?search={searchTerms}", null, "foo4"), + new keywordResult("http://searchget/?search=foo4", null, true)], + + [new searchKeywordData("searchpost", "http://searchpost/", "search={searchTerms}", "foo5"), + new keywordResult("http://searchpost/", "search=foo5", true)], + + [new searchKeywordData("searchpostget", "http://searchpostget/?search1={searchTerms}", "search2={searchTerms}", "foo6"), + new keywordResult("http://searchpostget/?search1=foo6", "search2=foo6", true)], + + // Bookmark keywords that don't take parameters should not be activated if a + // parameter is passed (bug 420328). + [new bmKeywordData("bmget-noparam", "http://bmget-noparam/", null, "foo7"), + new keywordResult(null, null, true)], + [new bmKeywordData("bmpost-noparam", "http://bmpost-noparam/", "not_a=param", "foo8"), + new keywordResult(null, null, true)], + + // Test escaping (%s = escaped, %S = raw) + // UTF-8 default + [new bmKeywordData("bmget-escaping", "http://bmget/?esc=%s&raw=%S", null, "foé"), + new keywordResult("http://bmget/?esc=fo%C3%A9&raw=foé", null)], + // Explicitly-defined ISO-8859-1 + [new bmKeywordData("bmget-escaping2", "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1", null, "foé"), + new keywordResult("http://bmget/?esc=fo%E9&raw=foé", null)], + + // Bug 359809: Test escaping +, /, and @ + // UTF-8 default + [new bmKeywordData("bmget-escaping", "http://bmget/?esc=%s&raw=%S", null, "+/@"), + new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null)], + // Explicitly-defined ISO-8859-1 + [new bmKeywordData("bmget-escaping2", "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1", null, "+/@"), + new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null)], + + // Test using a non-bmKeywordData object, to test the behavior of + // getShortcutOrURI for non-keywords (setupKeywords only adds keywords for + // bmKeywordData objects) + [{keyword: "http://gavinsharp.com"}, + new keywordResult(null, null, true)] +]; + +function test() { + setupKeywords(); + + for each (var item in testData) { + var [data, result] = item; + + var postData = {}; + var query = data.keyword; + if (data.searchWord) + query += " " + data.searchWord; + var mayInheritPrincipal = {}; + var url = getShortcutOrURI(query, postData, mayInheritPrincipal); + + // null result.url means we should expect the same query we sent in + var expected = result.url || query; + is(url, expected, "got correct URL for " + data.keyword); + is(getPostDataString(postData.value), result.postData, "got correct postData for " + data.keyword); + is(mayInheritPrincipal.value, !result.isUnsafe, "got correct mayInheritPrincipal for " + data.keyword); + } + + cleanupKeywords(); +} + +var gBMFolder = null; +var gAddedEngines = []; +function setupKeywords() { + gBMFolder = Application.bookmarks.menu.addFolder("keyword-test"); + for each (var item in testData) { + var data = item[0]; + if (data instanceof bmKeywordData) { + var bm = gBMFolder.addBookmark(data.keyword, data.uri); + bm.keyword = data.keyword; + if (data.postData) + bm.annotations.set("bookmarkProperties/POSTData", data.postData, Ci.nsIAnnotationService.EXPIRE_SESSION); + } + + if (data instanceof searchKeywordData) { + Services.search.addEngineWithDetails(data.keyword, "", data.keyword, "", data.method, data.uri.spec); + var addedEngine = Services.search.getEngineByName(data.keyword); + if (data.postData) { + var [paramName, paramValue] = data.postData.split("="); + addedEngine.addParam(paramName, paramValue, null); + } + + gAddedEngines.push(addedEngine); + } + } +} + +function cleanupKeywords() { + gBMFolder.remove(); + gAddedEngines.map(Services.search.removeEngine); +} diff --git a/browser/base/content/test/browser_hide_removing.js b/browser/base/content/test/browser_hide_removing.js new file mode 100644 index 000000000..d29826556 --- /dev/null +++ b/browser/base/content/test/browser_hide_removing.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Bug 587922: tabs don't get removed if they're hidden + +function test() { + waitForExplicitFinish(); + + // Add a tab that will get removed and hidden + let testTab = gBrowser.addTab("about:blank", {skipAnimation: true}); + is(gBrowser.visibleTabs.length, 2, "just added a tab, so 2 tabs"); + gBrowser.selectedTab = testTab; + + let numVisBeforeHide, numVisAfterHide; + gBrowser.tabContainer.addEventListener("TabSelect", function() { + gBrowser.tabContainer.removeEventListener("TabSelect", arguments.callee, false); + + // While the next tab is being selected, hide the removing tab + numVisBeforeHide = gBrowser.visibleTabs.length; + gBrowser.hideTab(testTab); + numVisAfterHide = gBrowser.visibleTabs.length; + }, false); + gBrowser.removeTab(testTab, {animate: true}); + + // Make sure the tab gets removed at the end of the animation by polling + (function checkRemoved() setTimeout(function() { + if (gBrowser.tabs.length != 1) + return checkRemoved(); + + is(numVisBeforeHide, 1, "animated remove has in 1 tab left"); + is(numVisAfterHide, 1, "hiding a removing tab is also has 1 tab"); + finish(); + }, 50))(); +} diff --git a/browser/base/content/test/browser_homeDrop.js b/browser/base/content/test/browser_homeDrop.js new file mode 100644 index 000000000..e9c977827 --- /dev/null +++ b/browser/base/content/test/browser_homeDrop.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + let str = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + str.data = "about:mozilla"; + Services.prefs.setComplexValue("browser.startup.homepage", + Ci.nsISupportsString, str); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.startup.homepage"); + }); + + // Open a new tab, since starting a drag from the home button activates it and + // we don't want to interfere with future tests by loading the home page. + let newTab = gBrowser.selectedTab = gBrowser.addTab(); + registerCleanupFunction(function () { + gBrowser.removeTab(newTab); + }); + + let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + let ChromeUtils = {}; + scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils); + + let homeButton = document.getElementById("home-button"); + ok(homeButton, "home button present"); + + let dialogListener = new WindowListener("chrome://global/content/commonDialog.xul", function (domwindow) { + ok(true, "dialog appeared in response to home button drop"); + domwindow.document.documentElement.cancelDialog(); + Services.wm.removeListener(dialogListener); + + // Now trigger the invalid URI test + executeSoon(function () { + let consoleListener = { + observe: function (m) { + if (m.message.contains("NS_ERROR_DOM_BAD_URI")) { + ok(true, "drop was blocked"); + executeSoon(finish); + } + } + } + Services.console.registerListener(consoleListener); + registerCleanupFunction(function () { + Services.console.unregisterListener(consoleListener); + }); + + executeSoon(function () { + info("Attempting second drop, of a javascript: URI"); + // The drop handler throws an exception when dragging URIs that inherit + // principal, e.g. javascript: + expectUncaughtException(); + ChromeUtils.synthesizeDrop(homeButton, homeButton, [[{type: "text/plain", data: "javascript:8888"}]], "copy", window); + }); + }) + }); + + Services.wm.addListener(dialogListener); + + ChromeUtils.synthesizeDrop(homeButton, homeButton, [[{type: "text/plain", data: "http://mochi.test:8888/"}]], "copy", window); +} + +function WindowListener(aURL, aCallback) { + this.callback = aCallback; + this.url = aURL; +} +WindowListener.prototype = { + onOpenWindow: function(aXULWindow) { + var domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + var self = this; + domwindow.addEventListener("load", function() { + domwindow.removeEventListener("load", arguments.callee, false); + + ok(true, "domwindow.document.location.href: " + domwindow.document.location.href); + if (domwindow.document.location.href != self.url) + return; + + // Allow other window load listeners to execute before passing to callback + executeSoon(function() { + self.callback(domwindow); + }); + }, false); + }, + onCloseWindow: function(aXULWindow) {}, + onWindowTitleChange: function(aXULWindow, aNewTitle) {} +} + diff --git a/browser/base/content/test/browser_identity_UI.js b/browser/base/content/test/browser_identity_UI.js new file mode 100644 index 000000000..9f07265c9 --- /dev/null +++ b/browser/base/content/test/browser_identity_UI.js @@ -0,0 +1,119 @@ +/* Tests for correct behaviour of getEffectiveHost on identity handler */ +function test() { + waitForExplicitFinish(); + + ok(gIdentityHandler, "gIdentityHandler should exist"); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", checkResult, true); + + nextTest(); +} + +// Greek IDN for 'example.test'. +var idnDomain = "\u03C0\u03B1\u03C1\u03AC\u03B4\u03B5\u03B9\u03B3\u03BC\u03B1.\u03B4\u03BF\u03BA\u03B9\u03BC\u03AE"; +var tests = [ + { + name: "normal domain", + location: "http://test1.example.org/", + host: "test1.example.org", + effectiveHost: "example.org" + }, + { + name: "view-source", + location: "view-source:http://example.com/", + // TODO: these should not be blank, bug 646690 + host: "", + effectiveHost: "" + }, + { + name: "normal HTTPS", + location: "https://example.com/", + host: "example.com", + effectiveHost: "example.com", + isHTTPS: true + }, + { + name: "IDN subdomain", + location: "http://sub1." + idnDomain + "/", + host: "sub1." + idnDomain, + effectiveHost: idnDomain + }, + { + name: "subdomain with port", + location: "http://sub1.test1.example.org:8000/", + host: "sub1.test1.example.org:8000", + effectiveHost: "example.org" + }, + { + name: "subdomain HTTPS", + location: "https://test1.example.com", + host: "test1.example.com", + effectiveHost: "example.com", + isHTTPS: true + }, + { + name: "view-source HTTPS", + location: "view-source:https://example.com/", + // TODO: these should not be blank, bug 646690 + host: "", + effectiveHost: "", + isHTTPS: true + }, + { + name: "IP address", + location: "http://127.0.0.1:8888/", + host: "127.0.0.1:8888", + effectiveHost: "127.0.0.1" + }, +] + +let gCurrentTest, gCurrentTestIndex = -1, gTestDesc; +// Go through the tests in both directions, to add additional coverage for +// transitions between different states. +let gForward = true; +let gCheckETLD = false; +function nextTest() { + if (!gCheckETLD) { + if (gForward) + gCurrentTestIndex++; + else + gCurrentTestIndex--; + + if (gCurrentTestIndex == tests.length) { + // Went too far, reverse + gCurrentTestIndex--; + gForward = false; + } + + if (gCurrentTestIndex == -1) { + gBrowser.selectedBrowser.removeEventListener("load", checkResult, true); + gBrowser.removeCurrentTab(); + finish(); + return; + } + + gCurrentTest = tests[gCurrentTestIndex]; + gTestDesc = "#" + gCurrentTestIndex + " (" + gCurrentTest.name + ")"; + if (!gForward) + gTestDesc += " (second time)"; + if (gCurrentTest.isHTTPS) { + gCheckETLD = true; + } + content.location = gCurrentTest.location; + } else { + gCheckETLD = false; + gTestDesc = "#" + gCurrentTestIndex + " (" + gCurrentTest.name + " without eTLD in identity icon label)"; + if (!gForward) + gTestDesc += " (second time)"; + content.location.reload(true); + } +} + +function checkResult() { + // Sanity check other values, and the value of gIdentityHandler.getEffectiveHost() + is(gIdentityHandler._lastLocation.host, gCurrentTest.host, "host matches for test " + gTestDesc); + is(gIdentityHandler.getEffectiveHost(), gCurrentTest.effectiveHost, "effectiveHost matches for test " + gTestDesc); + + executeSoon(nextTest); +} diff --git a/browser/base/content/test/browser_keywordBookmarklets.js b/browser/base/content/test/browser_keywordBookmarklets.js new file mode 100644 index 000000000..8b075d74c --- /dev/null +++ b/browser/base/content/test/browser_keywordBookmarklets.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + let bmFolder = Application.bookmarks.menu.addFolder("keyword-test"); + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + registerCleanupFunction (function () { + bmFolder.remove(); + gBrowser.removeTab(tab); + }); + + let bm = bmFolder.addBookmark("bookmarklet", makeURI("javascript:1;")); + bm.keyword = "bm"; + + addPageShowListener(function () { + let originalPrincipal = gBrowser.contentPrincipal; + + // Enter bookmarklet keyword in the URL bar + gURLBar.value = "bm"; + gURLBar.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}); + + addPageShowListener(function () { + ok(gBrowser.contentPrincipal.equals(originalPrincipal), "javascript bookmarklet should inherit principal"); + finish(); + }); + }); +} + +function addPageShowListener(func) { + gBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false); + func(); + }); +} diff --git a/browser/base/content/test/browser_keywordSearch.js b/browser/base/content/test/browser_keywordSearch.js new file mode 100644 index 000000000..73efe580e --- /dev/null +++ b/browser/base/content/test/browser_keywordSearch.js @@ -0,0 +1,86 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + **/ + +var gTests = [ + { + name: "normal search (search service)", + testText: "test search", + searchURL: Services.search.defaultEngine.getSubmission("test search", null, "keyword").uri.spec + }, + { + name: "?-prefixed search (search service)", + testText: "? foo ", + searchURL: Services.search.defaultEngine.getSubmission("foo", null, "keyword").uri.spec + } +]; + +function test() { + waitForExplicitFinish(); + + let windowObserver = { + observe: function(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + ok(false, "Alert window opened"); + let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget); + win.addEventListener("load", function() { + win.removeEventListener("load", arguments.callee, false); + win.close(); + }, false); + executeSoon(finish); + } + } + }; + + Services.ww.registerNotification(windowObserver); + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + let listener = { + onStateChange: function onLocationChange(webProgress, req, flags, status) { + // Only care about document starts + let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | + Ci.nsIWebProgressListener.STATE_START; + if (!(flags & docStart)) + return; + + info("received document start"); + + ok(req instanceof Ci.nsIChannel, "req is a channel"); + is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded"); + info("Actual URI: " + req.URI.spec); + + executeSoon(nextTest); + } + } + gBrowser.addProgressListener(listener); + + registerCleanupFunction(function () { + Services.ww.unregisterNotification(windowObserver); + + gBrowser.removeProgressListener(listener); + gBrowser.removeTab(tab); + }); + + nextTest(); +} + +var gCurrTest; +function nextTest() { + if (gTests.length) { + gCurrTest = gTests.shift(); + doTest(); + } else { + finish(); + } +} + +function doTest() { + info("Running test: " + gCurrTest.name); + + // Simulate a user entering search terms + gURLBar.value = gCurrTest.testText; + gURLBar.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}); +} diff --git a/browser/base/content/test/browser_keywordSearch_postData.js b/browser/base/content/test/browser_keywordSearch_postData.js new file mode 100644 index 000000000..48db709c1 --- /dev/null +++ b/browser/base/content/test/browser_keywordSearch_postData.js @@ -0,0 +1,94 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + **/ + +var gTests = [ + { + name: "normal search (search service)", + testText: "test search", + expectText: "test+search" + }, + { + name: "?-prefixed search (search service)", + testText: "? foo ", + expectText: "foo" + } +]; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + let searchObserver = function search_observer(aSubject, aTopic, aData) { + let engine = aSubject.QueryInterface(Ci.nsISearchEngine); + info("Observer: " + aData + " for " + engine.name); + + if (aData != "engine-added") + return; + + if (engine.name != "POST Search") + return; + + Services.search.defaultEngine = engine; + + registerCleanupFunction(function () { + Services.search.removeEngine(engine); + }); + + // ready to execute the tests! + executeSoon(nextTest); + }; + + Services.obs.addObserver(searchObserver, "browser-search-engine-modified", false); + + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + + Services.obs.removeObserver(searchObserver, "browser-search-engine-modified"); + }); + + Services.search.addEngine("http://test:80/browser/browser/base/content/test/POSTSearchEngine.xml", + Ci.nsISearchEngine.DATA_XML, null, false); +} + +var gCurrTest; +function nextTest() { + if (gTests.length) { + gCurrTest = gTests.shift(); + doTest(); + } else { + finish(); + } +} + +function doTest() { + info("Running test: " + gCurrTest.name); + + waitForLoad(function () { + let loadedText = gBrowser.contentDocument.body.textContent; + ok(loadedText, "search page loaded"); + let needle = "searchterms=" + gCurrTest.expectText; + is(loadedText, needle, "The query POST data should be returned in the response"); + nextTest(); + }); + + // Simulate a user entering search terms + gURLBar.value = gCurrTest.testText; + gURLBar.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}); +} + + +function waitForLoad(cb) { + let browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function listener() { + if (browser.currentURI.spec == "about:blank") + return; + info("Page loaded: " + browser.currentURI.spec); + browser.removeEventListener("load", listener, true); + + cb(); + }, true); +} diff --git a/browser/base/content/test/browser_lastAccessedTab.js b/browser/base/content/test/browser_lastAccessedTab.js new file mode 100644 index 000000000..d0ad8b4cd --- /dev/null +++ b/browser/base/content/test/browser_lastAccessedTab.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test for bug 739866. + * + * 1. Adds a new tab (but doesn't select it) + * 2. Checks if timestamp on the new tab is 0 + * 3. Selects the new tab, checks that the timestamp is updated (>0) + * 4. Selects the original tab & checks if new tab's timestamp has remained changed + */ + +function test() { + let originalTab = gBrowser.selectedTab; + let newTab = gBrowser.addTab("about:blank", {skipAnimation: true}); + is(newTab.lastAccessed, 0, "Timestamp on the new tab is 0."); + gBrowser.selectedTab = newTab; + let newTabAccessedDate = newTab.lastAccessed; + ok(newTabAccessedDate > 0, "Timestamp on the selected tab is more than 0."); + // Date.now is not guaranteed to be monotonic, so include one second of fudge. + let now = Date.now() + 1000; + ok(newTabAccessedDate <= now, "Timestamp less than or equal current Date: " + newTabAccessedDate + " <= " + now); + gBrowser.selectedTab = originalTab; + is(newTab.lastAccessed, newTabAccessedDate, "New tab's timestamp remains the same."); + gBrowser.removeTab(newTab); +} diff --git a/browser/base/content/test/browser_locationBarCommand.js b/browser/base/content/test/browser_locationBarCommand.js new file mode 100644 index 000000000..fe70300f9 --- /dev/null +++ b/browser/base/content/test/browser_locationBarCommand.js @@ -0,0 +1,213 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_VALUE = "example.com"; +const START_VALUE = "example.org"; + +let gFocusManager = Cc["@mozilla.org/focus-manager;1"]. + getService(Ci.nsIFocusManager); + +function test() { + waitForExplicitFinish(); + + registerCleanupFunction(function () { + Services.prefs.clearUserPref("browser.altClickSave"); + }); + Services.prefs.setBoolPref("browser.altClickSave", true); + + runAltLeftClickTest(); +} + +// Monkey patch saveURL to avoid dealing with file save code paths +var oldSaveURL = saveURL; +saveURL = function() { + ok(true, "SaveURL was called"); + is(gURLBar.value, "", "Urlbar reverted to original value"); + saveURL = oldSaveURL; + runShiftLeftClickTest(); +} +function runAltLeftClickTest() { + info("Running test: Alt left click"); + triggerCommand(true, { altKey: true }); +} + +function runShiftLeftClickTest() { + let listener = new BrowserWindowListener(getBrowserURL(), function(aWindow) { + Services.wm.removeListener(listener); + addPageShowListener(aWindow.gBrowser.selectedBrowser, function() { + executeSoon(function () { + info("URL should be loaded in a new window"); + is(gURLBar.value, "", "Urlbar reverted to original value"); + is(gFocusManager.focusedElement, null, "There should be no focused element"); + is(gFocusManager.focusedWindow, aWindow.gBrowser.contentWindow, "Content window should be focused"); + is(aWindow.gURLBar.value, TEST_VALUE, "New URL is loaded in new window"); + + aWindow.close(); + + // Continue testing when the original window has focus again. + whenWindowActivated(window, runNextTest); + }); + }, "http://example.com/"); + }); + Services.wm.addListener(listener); + + info("Running test: Shift left click"); + triggerCommand(true, { shiftKey: true }); +} + +function runNextTest() { + let test = gTests.shift(); + if (!test) { + finish(); + return; + } + + info("Running test: " + test.desc); + // Tab will be blank if test.startValue is null + let tab = gBrowser.selectedTab = gBrowser.addTab(test.startValue); + addPageShowListener(gBrowser.selectedBrowser, function() { + triggerCommand(test.click, test.event); + test.check(tab); + + // Clean up + while (gBrowser.tabs.length > 1) + gBrowser.removeTab(gBrowser.selectedTab) + runNextTest(); + }); +} + +let gTests = [ + { desc: "Right click on go button", + click: true, + event: { button: 2 }, + check: function(aTab) { + // Right click should do nothing (context menu will be shown) + is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered"); + } + }, + + { desc: "Left click on go button", + click: true, + event: {}, + check: checkCurrent + }, + + { desc: "Ctrl/Cmd left click on go button", + click: true, + event: { accelKey: true }, + check: checkNewTab + }, + + { desc: "Shift+Ctrl/Cmd left click on go button", + click: true, + event: { accelKey: true, shiftKey: true }, + check: function(aTab) { + info("URL should be loaded in a new background tab"); + is(gURLBar.value, "", "Urlbar reverted to original value"); + ok(!gURLBar.focused, "Urlbar is no longer focused after urlbar command"); + is(gBrowser.selectedTab, aTab, "Focus did not change to the new tab"); + + // Select the new background tab + gBrowser.selectedTab = gBrowser.selectedTab.nextSibling; + is(gURLBar.value, TEST_VALUE, "New URL is loaded in new tab"); + } + }, + + { desc: "Simple return keypress", + event: {}, + check: checkCurrent + }, + + { desc: "Alt+Return keypress in a blank tab", + event: { altKey: true }, + check: checkCurrent + }, + + { desc: "Alt+Return keypress in a dirty tab", + event: { altKey: true }, + check: checkNewTab, + startValue: START_VALUE + }, + + { desc: "Ctrl/Cmd+Return keypress", + event: { accelKey: true }, + check: checkCurrent + } +] + +let gGoButton = document.getElementById("urlbar-go-button"); +function triggerCommand(aClick, aEvent) { + gURLBar.value = TEST_VALUE; + gURLBar.focus(); + + if (aClick) { + is(gURLBar.getAttribute("pageproxystate"), "invalid", + "page proxy state must be invalid for go button to be visible"); + EventUtils.synthesizeMouseAtCenter(gGoButton, aEvent); + } + else + EventUtils.synthesizeKey("VK_RETURN", aEvent); +} + +/* Checks that the URL was loaded in the current tab */ +function checkCurrent(aTab) { + info("URL should be loaded in the current tab"); + is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered"); + is(gFocusManager.focusedElement, null, "There should be no focused element"); + is(gFocusManager.focusedWindow, gBrowser.contentWindow, "Content window should be focused"); + is(gBrowser.selectedTab, aTab, "New URL was loaded in the current tab"); +} + +/* Checks that the URL was loaded in a new focused tab */ +function checkNewTab(aTab) { + info("URL should be loaded in a new focused tab"); + is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered"); + is(gFocusManager.focusedElement, null, "There should be no focused element"); + is(gFocusManager.focusedWindow, gBrowser.contentWindow, "Content window should be focused"); + isnot(gBrowser.selectedTab, aTab, "New URL was loaded in a new tab"); +} + +function addPageShowListener(browser, cb, expectedURL) { + browser.addEventListener("pageshow", function pageShowListener() { + info("pageshow: " + browser.currentURI.spec); + if (expectedURL && browser.currentURI.spec != expectedURL) + return; // ignore pageshows for non-expected URLs + browser.removeEventListener("pageshow", pageShowListener, false); + cb(); + }); +} + +function whenWindowActivated(win, cb) { + if (Services.focus.activeWindow == win) { + executeSoon(cb); + return; + } + + win.addEventListener("activate", function onActivate() { + win.removeEventListener("activate", onActivate); + executeSoon(cb); + }); +} + +function BrowserWindowListener(aURL, aCallback) { + this.callback = aCallback; + this.url = aURL; +} +BrowserWindowListener.prototype = { + onOpenWindow: function(aXULWindow) { + let cb = () => this.callback(domwindow); + let domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + + let numWait = 2; + function maybeRunCallback() { + if (--numWait == 0) + cb(); + } + + whenWindowActivated(domwindow, maybeRunCallback); + whenDelayedStartupFinished(domwindow, maybeRunCallback); + }, + onCloseWindow: function(aXULWindow) {}, + onWindowTitleChange: function(aXULWindow, aNewTitle) {} +} diff --git a/browser/base/content/test/browser_locationBarExternalLoad.js b/browser/base/content/test/browser_locationBarExternalLoad.js new file mode 100644 index 000000000..2bc88a989 --- /dev/null +++ b/browser/base/content/test/browser_locationBarExternalLoad.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + nextTest(); +} + +let urls = [ + "javascript:'foopy';", + "data:text/html,<body>hi" +]; + +function urlEnter(url) { + gURLBar.value = url; + gURLBar.focus(); + EventUtils.synthesizeKey("VK_RETURN", {}); +} + +function urlClick(url) { + gURLBar.value = url; + gURLBar.focus(); + let goButton = document.getElementById("urlbar-go-button"); + EventUtils.synthesizeMouseAtCenter(goButton, {}); +} + +function nextTest() { + let url = urls.shift(); + if (url) { + testURL(url, urlEnter, function () { + testURL(url, urlClick, nextTest); + }); + } + else + finish(); +} + +function testURL(url, loadFunc, endFunc) { + let tab = gBrowser.selectedTab = gBrowser.addTab(); + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + }); + addPageShowListener(function () { + let pagePrincipal = gBrowser.contentPrincipal; + loadFunc(url); + + addPageShowListener(function () { + let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + is(fm.focusedElement, null, "should be no focused element"); + is(fm.focusedWindow, gBrowser.contentWindow, "content window should be focused"); + + ok(!gBrowser.contentPrincipal.equals(pagePrincipal), + "load of " + url + " by " + loadFunc.name + " should produce a page with a different principal"); + endFunc(); + }); + }); +} + +function addPageShowListener(func) { + gBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false); + func(); + }); +} diff --git a/browser/base/content/test/browser_middleMouse_inherit.js b/browser/base/content/test/browser_middleMouse_inherit.js new file mode 100644 index 000000000..891ea2ed0 --- /dev/null +++ b/browser/base/content/test/browser_middleMouse_inherit.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const middleMousePastePref = "middlemouse.contentLoadURL"; +const autoScrollPref = "general.autoScroll"; +function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref(middleMousePastePref, true); + Services.prefs.setBoolPref(autoScrollPref, false); + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + registerCleanupFunction(function () { + Services.prefs.clearUserPref(middleMousePastePref); + Services.prefs.clearUserPref(autoScrollPref); + gBrowser.removeTab(tab); + }); + + addPageShowListener(function () { + let pagePrincipal = gBrowser.contentPrincipal; + + // copy javascript URI to the clipboard + let url = "javascript:1+1"; + waitForClipboard(url, + function() { + Components.classes["@mozilla.org/widget/clipboardhelper;1"] + .getService(Components.interfaces.nsIClipboardHelper) + .copyString(url, document); + }, + function () { + // Middle click on the content area + info("Middle clicking"); + EventUtils.sendMouseEvent({type: "click", button: 1}, gBrowser); + }, + function() { + ok(false, "Failed to copy URL to the clipboard"); + finish(); + } + ); + + addPageShowListener(function () { + is(gBrowser.currentURI.spec, url, "url loaded by middle click"); + ok(!gBrowser.contentPrincipal.equals(pagePrincipal), + "middle click load of " + url + " should produce a page with a different principal"); + finish(); + }); + }); +} + +function addPageShowListener(func) { + gBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false); + func(); + }); +} diff --git a/browser/base/content/test/browser_minimize.js b/browser/base/content/test/browser_minimize.js new file mode 100644 index 000000000..459f84b44 --- /dev/null +++ b/browser/base/content/test/browser_minimize.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +function waitForActive() { + if (!gBrowser.docShell.isActive) { + executeSoon(waitForActive); + return; + } + is(gBrowser.docShell.isActive, true, "Docshell should be active again"); + finish(); +} + +function waitForInactive() { + if (gBrowser.docShell.isActive) { + executeSoon(waitForInactive); + return; + } + is(gBrowser.docShell.isActive, false, "Docshell should be inactive"); + window.restore(); + waitForActive(); +} + +function test() { + registerCleanupFunction(function() { + window.restore(); + }); + + waitForExplicitFinish(); + is(gBrowser.docShell.isActive, true, "Docshell should be active"); + window.minimize(); + // XXX On Linux minimize/restore seem to be very very async, but + // our window.windowState changes sync.... so we can't rely on the + // latter correctly reflecting the state of the former. In + // particular, a restore() call before minimizing is done will not + // actually restore the window, but change the window state. As a + // result, just poll waiting for our expected isActive values. + waitForInactive(); +} diff --git a/browser/base/content/test/browser_offlineQuotaNotification.js b/browser/base/content/test/browser_offlineQuotaNotification.js new file mode 100644 index 000000000..a8aba6b97 --- /dev/null +++ b/browser/base/content/test/browser_offlineQuotaNotification.js @@ -0,0 +1,74 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Test offline quota warnings - must be run as a mochitest-browser test or +// else the test runner gets in the way of notifications due to bug 857897. + +const URL = "http://mochi.test:8888/browser/browser/base/content/test/offlineQuotaNotification.html"; + +registerCleanupFunction(function() { + // Clean up after ourself + let uri = Services.io.newURI(URL, null, null); + var principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri); + Services.perms.removeFromPrincipal(principal, "offline-app"); + Services.prefs.clearUserPref("offline-apps.quota.warn"); +}); + +// Check that the "preferences" UI is opened and showing which websites have +// offline storage permissions - currently this is the "network" tab in the +// "advanced" pane. +function checkPreferences(prefsWin) { + // We expect a 'paneload' event for the 'advanced' pane, then + // a 'select' event on the 'network' tab inside that pane. + prefsWin.addEventListener("paneload", function paneload(evt) { + prefsWin.removeEventListener("paneload", paneload); + is(evt.target.id, "paneAdvanced", "advanced pane loaded"); + let tabPanels = evt.target.getElementsByTagName("tabpanels")[0]; + tabPanels.addEventListener("select", function tabselect() { + tabPanels.removeEventListener("select", tabselect); + is(tabPanels.selectedPanel.id, "networkPanel", "networkPanel is selected"); + // all good, we are done. + prefsWin.close(); + finish(); + }); + }); +} + +function test() { + waitForExplicitFinish(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + gBrowser.selectedBrowser.contentWindow.applicationCache.oncached = function() { + executeSoon(function() { + // We got cached - now we should have provoked the quota warning. + let notification = PopupNotifications.getNotification('offline-app-usage'); + ok(notification, "have offline-app-usage notification"); + // select the default action - this should cause the preferences + // window to open - which we track either via a window watcher (for + // the window-based prefs) or via an "Initialized" event (for + // in-content prefs.) + if (Services.prefs.getBoolPref("browser.preferences.inContent")) { + // Bug 881576 - ensure this works with inContent prefs. + todo(false, "Bug 881576 - this test needs to be updated for inContent prefs"); + } else { + Services.ww.registerNotification(function wwobserver(aSubject, aTopic, aData) { + if (aTopic != "domwindowopened") + return; + Services.ww.unregisterNotification(wwobserver); + checkPreferences(aSubject); + }); + PopupNotifications.panel.firstElementChild.button.click(); + } + }); + }; + Services.prefs.setIntPref("offline-apps.quota.warn", 1); + + // Click the notification panel's "Allow" button. This should kick + // off updates which will call our oncached handler above. + PopupNotifications.panel.firstElementChild.button.click(); + }, true); + + gBrowser.contentWindow.location = URL; +} diff --git a/browser/base/content/test/browser_overflowScroll.js b/browser/base/content/test/browser_overflowScroll.js new file mode 100644 index 000000000..8c0eac709 --- /dev/null +++ b/browser/base/content/test/browser_overflowScroll.js @@ -0,0 +1,87 @@ +var tabstrip = gBrowser.tabContainer.mTabstrip; +var scrollbox = tabstrip._scrollbox; +var originalSmoothScroll = tabstrip.smoothScroll; +var tabs = gBrowser.tabs; + +function rect(ele) ele.getBoundingClientRect(); +function width(ele) rect(ele).width; +function left(ele) rect(ele).left; +function right(ele) rect(ele).right; +function isLeft(ele, msg) is(left(ele), left(scrollbox), msg); +function isRight(ele, msg) is(right(ele), right(scrollbox), msg); +function elementFromPoint(x) tabstrip._elementFromPoint(x); +function nextLeftElement() elementFromPoint(left(scrollbox) - 1); +function nextRightElement() elementFromPoint(right(scrollbox) + 1); +function firstScrollable() tabs[gBrowser._numPinnedTabs]; + +function test() { + requestLongerTimeout(2); + waitForExplicitFinish(); + + // If the previous (or more) test finished with cleaning up the tabs, + // there may be some pending animations. That can cause a failure of + // this tests, so, we should test this in another stack. + setTimeout(doTest, 0); +} + +function doTest() { + tabstrip.smoothScroll = false; + + var tabMinWidth = parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth); + var tabCountForOverflow = Math.ceil(width(tabstrip) / tabMinWidth * 3); + while (tabs.length < tabCountForOverflow) + gBrowser.addTab("about:blank", {skipAnimation: true}); + gBrowser.pinTab(tabs[0]); + + tabstrip.addEventListener("overflow", runOverflowTests, false); +} + +function runOverflowTests(aEvent) { + if (aEvent.detail != 1) + return; + + tabstrip.removeEventListener("overflow", runOverflowTests, false); + + var upButton = tabstrip._scrollButtonUp; + var downButton = tabstrip._scrollButtonDown; + var element; + + gBrowser.selectedTab = firstScrollable(); + ok(left(scrollbox) <= left(firstScrollable()), "Selecting the first tab scrolls it into view " + + "(" + left(scrollbox) + " <= " + left(firstScrollable()) + ")"); + + element = nextRightElement(); + EventUtils.synthesizeMouseAtCenter(downButton, {}); + isRight(element, "Scrolled one tab to the right with a single click"); + + gBrowser.selectedTab = tabs[tabs.length - 1]; + ok(right(gBrowser.selectedTab) <= right(scrollbox), "Selecting the last tab scrolls it into view " + + "(" + right(gBrowser.selectedTab) + " <= " + right(scrollbox) + ")"); + + element = nextLeftElement(); + EventUtils.synthesizeMouse(upButton, 1, 1, {}); + isLeft(element, "Scrolled one tab to the left with a single click"); + + element = elementFromPoint(left(scrollbox) - width(scrollbox)); + EventUtils.synthesizeMouse(upButton, 1, 1, {clickCount: 2}); + isLeft(element, "Scrolled one page of tabs with a double click"); + + EventUtils.synthesizeMouse(upButton, 1, 1, {clickCount: 3}); + var firstScrollableLeft = left(firstScrollable()); + ok(left(scrollbox) <= firstScrollableLeft, "Scrolled to the start with a triple click " + + "(" + left(scrollbox) + " <= " + firstScrollableLeft + ")"); + + for (var i = 2; i; i--) + EventUtils.synthesizeWheel(scrollbox, 1, 1, { deltaX: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + is(left(firstScrollable()), firstScrollableLeft, "Remained at the start with the mouse wheel"); + + element = nextRightElement(); + EventUtils.synthesizeWheel(scrollbox, 1, 1, { deltaX: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}); + isRight(element, "Scrolled one tab to the right with the mouse wheel"); + + while (tabs.length > 1) + gBrowser.removeTab(tabs[0]); + + tabstrip.smoothScroll = originalSmoothScroll; + finish(); +} diff --git a/browser/base/content/test/browser_pageInfo.js b/browser/base/content/test/browser_pageInfo.js new file mode 100644 index 000000000..c0159380c --- /dev/null +++ b/browser/base/content/test/browser_pageInfo.js @@ -0,0 +1,38 @@ +function test() { + waitForExplicitFinish(); + + var pageInfo; + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("load", loadListener, true); + + Services.obs.addObserver(observer, "page-info-dialog-loaded", false); + pageInfo = BrowserPageInfo(); + }, true); + content.location = + "https://example.com/browser/browser/base/content/test/feed_tab.html"; + + function observer(win, topic, data) { + Services.obs.removeObserver(observer, "page-info-dialog-loaded"); + handlePageInfo(); + } + + function handlePageInfo() { + ok(pageInfo.document.getElementById("feedTab"), "Feed tab"); + let feedListbox = pageInfo.document.getElementById("feedListbox"); + ok(feedListbox, "Feed list"); + + var feedRowsNum = feedListbox.getRowCount(); + is(feedRowsNum, 3, "Number of feeds listed"); + + for (var i = 0; i < feedRowsNum; i++) { + let feedItem = feedListbox.getItemAtIndex(i); + is(feedItem.getAttribute("name"), i + 1, "Feed name"); + } + + pageInfo.close(); + gBrowser.removeCurrentTab(); + finish(); + } +} diff --git a/browser/base/content/test/browser_pageInfo_plugins.js b/browser/base/content/test/browser_pageInfo_plugins.js new file mode 100644 index 000000000..58fd82586 --- /dev/null +++ b/browser/base/content/test/browser_pageInfo_plugins.js @@ -0,0 +1,187 @@ +let gHttpTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); +let gPageInfo = null; +let gNextTest = null; +let gTestBrowser = null; +let gPluginHost = Components.classes["@mozilla.org/plugin/host;1"] + .getService(Components.interfaces.nsIPluginHost); +let gPermissionManager = Components.classes["@mozilla.org/permissionmanager;1"] + .getService(Components.interfaces.nsIPermissionManager); +let gTestPermissionString = gPluginHost.getPermissionStringForType("application/x-test"); +let gSecondTestPermissionString = gPluginHost.getPermissionStringForType("application/x-second-test"); + +function doOnPageLoad(url, continuation) { + gNextTest = continuation; + gTestBrowser.addEventListener("load", pageLoad, true); + gTestBrowser.contentWindow.location = url; +} + +function pageLoad() { + gTestBrowser.removeEventListener("load", pageLoad); + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + executeSoon(gNextTest); +} + +function doOnOpenPageInfo(continuation) { + Services.obs.addObserver(pageInfoObserve, "page-info-dialog-loaded", false); + gNextTest = continuation; + // An explanation: it looks like the test harness complains about leaked + // windows if we don't keep a reference to every window we've opened. + // So, don't reuse pointers to opened Page Info windows - simply append + // to this list. + gPageInfo = BrowserPageInfo(null, "permTab"); +} + +function pageInfoObserve(win, topic, data) { + Services.obs.removeObserver(pageInfoObserve, "page-info-dialog-loaded"); + executeSoon(gNextTest); +} + +function finishTest() { + gPermissionManager.remove("127.0.0.1:8888", gTestPermissionString); + gPermissionManager.remove("127.0.0.1:8888", gSecondTestPermissionString); + Services.prefs.clearUserPref("plugins.click_to_play"); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED; + gBrowser.removeCurrentTab(); + finish(); +} + +function test() { + waitForExplicitFinish(); + Services.prefs.setBoolPref("plugins.click_to_play", true); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED; + gBrowser.selectedTab = gBrowser.addTab(); + gTestBrowser = gBrowser.selectedBrowser; + gPermissionManager.remove("127.0.0.1:8888", gTestPermissionString); + gPermissionManager.remove("127.0.0.1:8888", gSecondTestPermissionString); + doOnPageLoad(gHttpTestRoot + "plugin_two_types.html", testPart1a); +} + +// The first test plugin is CtP and the second test plugin is enabled. +function testPart1a() { + let test = gTestBrowser.contentDocument.getElementById("test"); + let objLoadingContent = test.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "part 1a: Test plugin should not be activated"); + let secondtest = gTestBrowser.contentDocument.getElementById("secondtestA"); + let objLoadingContent = secondtest.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "part 1a: Second Test plugin should be activated"); + + doOnOpenPageInfo(testPart1b); +} + +function testPart1b() { + let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup"); + let testRadioDefault = gPageInfo.document.getElementById(gTestPermissionString + "#0"); + + var qString = "#" + gTestPermissionString.replace(':', '\\:') + "\\#0"; + is(testRadioGroup.selectedItem, testRadioDefault, "part 1b: Test radio group should be set to 'Default'"); + let testRadioAllow = gPageInfo.document.getElementById(gTestPermissionString + "#1"); + testRadioGroup.selectedItem = testRadioAllow; + testRadioAllow.doCommand(); + + let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup"); + let secondtestRadioDefault = gPageInfo.document.getElementById(gSecondTestPermissionString + "#0"); + is(secondtestRadioGroup.selectedItem, secondtestRadioDefault, "part 1b: Second Test radio group should be set to 'Default'"); + let secondtestRadioAsk = gPageInfo.document.getElementById(gSecondTestPermissionString + "#3"); + secondtestRadioGroup.selectedItem = secondtestRadioAsk; + secondtestRadioAsk.doCommand(); + + doOnPageLoad(gHttpTestRoot + "plugin_two_types.html", testPart2); +} + +// Now, the Test plugin should be allowed, and the Test2 plugin should be CtP +function testPart2() { + let test = gTestBrowser.contentDocument.getElementById("test"). + QueryInterface(Ci.nsIObjectLoadingContent); + ok(test.activated, "part 2: Test plugin should be activated"); + + let secondtest = gTestBrowser.contentDocument.getElementById("secondtestA"). + QueryInterface(Ci.nsIObjectLoadingContent); + ok(!secondtest.activated, "part 2: Second Test plugin should not be activated"); + is(secondtest.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, + "part 2: Second test plugin should be click-to-play."); + + let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup"); + let testRadioAllow = gPageInfo.document.getElementById(gTestPermissionString + "#1"); + is(testRadioGroup.selectedItem, testRadioAllow, "part 2: Test radio group should be set to 'Allow'"); + let testRadioBlock = gPageInfo.document.getElementById(gTestPermissionString + "#2"); + testRadioGroup.selectedItem = testRadioBlock; + testRadioBlock.doCommand(); + + let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup"); + let secondtestRadioAsk = gPageInfo.document.getElementById(gSecondTestPermissionString + "#3"); + is(secondtestRadioGroup.selectedItem, secondtestRadioAsk, "part 2: Second Test radio group should be set to 'Always Ask'"); + let secondtestRadioBlock = gPageInfo.document.getElementById(gSecondTestPermissionString + "#2"); + secondtestRadioGroup.selectedItem = secondtestRadioBlock; + secondtestRadioBlock.doCommand(); + + doOnPageLoad(gHttpTestRoot + "plugin_two_types.html", testPart3); +} + +// Now, all the things should be blocked +function testPart3() { + let test = gTestBrowser.contentDocument.getElementById("test"). + QueryInterface(Ci.nsIObjectLoadingContent); + ok(!test.activated, "part 3: Test plugin should not be activated"); + is(test.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_DISABLED, + "part 3: Test plugin should be marked as PLUGIN_DISABLED"); + + let secondtest = gTestBrowser.contentDocument.getElementById("secondtestA"). + QueryInterface(Ci.nsIObjectLoadingContent); + + ok(!secondtest.activated, "part 3: Second Test plugin should not be activated"); + is(secondtest.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_DISABLED, + "part 3: Second test plugin should be marked as PLUGIN_DISABLED"); + + // reset permissions + gPermissionManager.remove("127.0.0.1:8888", gTestPermissionString); + gPermissionManager.remove("127.0.0.1:8888", gSecondTestPermissionString); + // check that changing the permissions affects the radio state in the + // open Page Info window + let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup"); + let testRadioDefault = gPageInfo.document.getElementById(gTestPermissionString + "#0"); + is(testRadioGroup.selectedItem, testRadioDefault, "part 3: Test radio group should be set to 'Default'"); + let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup"); + let secondtestRadioDefault = gPageInfo.document.getElementById(gSecondTestPermissionString + "#0"); + is(secondtestRadioGroup.selectedItem, secondtestRadioDefault, "part 3: Second Test radio group should be set to 'Default'"); + + doOnPageLoad(gHttpTestRoot + "plugin_two_types.html", testPart4a); +} + +// Now test that setting permission directly (as from the popup notification) +// immediately influences Page Info. +function testPart4a() { + // simulate "allow" from the doorhanger + gPermissionManager.add(gTestBrowser.currentURI, gTestPermissionString, Ci.nsIPermissionManager.ALLOW_ACTION); + gPermissionManager.add(gTestBrowser.currentURI, gSecondTestPermissionString, Ci.nsIPermissionManager.ALLOW_ACTION); + + // check (again) that changing the permissions affects the radio state in the + // open Page Info window + let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup"); + let testRadioAllow = gPageInfo.document.getElementById(gTestPermissionString + "#1"); + is(testRadioGroup.selectedItem, testRadioAllow, "part 4a: Test radio group should be set to 'Allow'"); + let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup"); + let secondtestRadioAllow = gPageInfo.document.getElementById(gSecondTestPermissionString + "#1"); + is(secondtestRadioGroup.selectedItem, secondtestRadioAllow, "part 4a: Second Test radio group should be set to 'Always Allow'"); + + // now close Page Info and see that it opens with the right settings + gPageInfo.close(); + doOnOpenPageInfo(testPart4b); +} + +// check that "always allow" resulted in the radio buttons getting set to allow +function testPart4b() { + let testRadioGroup = gPageInfo.document.getElementById(gTestPermissionString + "RadioGroup"); + let testRadioAllow = gPageInfo.document.getElementById(gTestPermissionString + "#1"); + is(testRadioGroup.selectedItem, testRadioAllow, "part 4b: Test radio group should be set to 'Allow'"); + + let secondtestRadioGroup = gPageInfo.document.getElementById(gSecondTestPermissionString + "RadioGroup"); + let secondtestRadioAllow = gPageInfo.document.getElementById(gSecondTestPermissionString + "#1"); + is(secondtestRadioGroup.selectedItem, secondtestRadioAllow, "part 4b: Second Test radio group should be set to 'Allow'"); + + Services.prefs.setBoolPref("plugins.click_to_play", false); + gPageInfo.close(); + finishTest(); +} diff --git a/browser/base/content/test/browser_page_style_menu.js b/browser/base/content/test/browser_page_style_menu.js new file mode 100644 index 000000000..99b1bcfc2 --- /dev/null +++ b/browser/base/content/test/browser_page_style_menu.js @@ -0,0 +1,67 @@ +function test() { + waitForExplicitFinish(); + + var tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + tab.linkedBrowser.addEventListener("load", function () { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + checkPageStyleMenu(); + }, true); + let rootDir = getRootDirectory(gTestPath); + content.location = rootDir + "page_style_sample.html"; +} + +function checkPageStyleMenu() { + var menupopup = document.getElementById("pageStyleMenu") + .getElementsByTagName("menupopup")[0]; + gPageStyleMenu.fillPopup(menupopup); + + var items = []; + var current = menupopup.getElementsByTagName("menuseparator")[0]; + while (current.nextSibling) { + current = current.nextSibling; + items.push(current); + } + + var validLinks = 0; + Array.forEach(content.document.getElementsByTagName("link"), function (link) { + var title = link.getAttribute("title"); + var rel = link.getAttribute("rel"); + var media = link.getAttribute("media"); + var idstring = "link " + (title ? title : "without title and") + + " with rel=\"" + rel + "\"" + + (media ? " and media=\"" + media + "\"" : ""); + + var item = items.filter(function (item) item.getAttribute("label") == title); + var found = item.length == 1; + var checked = found && (item[0].getAttribute("checked") == "true"); + + switch (link.getAttribute("data-state")) { + case "0": + ok(!found, idstring + " does not show up in page style menu"); + break; + case "0-todo": + validLinks++; + todo(!found, idstring + " should not show up in page style menu"); + ok(!checked, idstring + " is not selected"); + break; + case "1": + validLinks++; + ok(found, idstring + " shows up in page style menu"); + ok(!checked, idstring + " is not selected"); + break; + case "2": + validLinks++; + ok(found, idstring + " shows up in page style menu"); + ok(checked, idstring + " is selected"); + break; + default: + throw "data-state attribute is missing or has invalid value"; + } + }); + + is(validLinks, items.length, "all valid links found"); + + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/browser/base/content/test/browser_pinnedTabs.js b/browser/base/content/test/browser_pinnedTabs.js new file mode 100644 index 000000000..5dbe941a2 --- /dev/null +++ b/browser/base/content/test/browser_pinnedTabs.js @@ -0,0 +1,73 @@ +var tabs; + +function index(tab) Array.indexOf(gBrowser.tabs, tab); + +function indexTest(tab, expectedIndex, msg) { + var diag = "tab " + tab + " should be at index " + expectedIndex; + if (msg) + msg = msg + " (" + diag + ")"; + else + msg = diag; + is(index(tabs[tab]), expectedIndex, msg); +} + +function PinUnpinHandler(tab, eventName) { + this.eventCount = 0; + var self = this; + tab.addEventListener(eventName, function() { + tab.removeEventListener(eventName, arguments.callee, true); + + self.eventCount++; + }, true); + gBrowser.tabContainer.addEventListener(eventName, function(e) { + gBrowser.tabContainer.removeEventListener(eventName, arguments.callee, true); + + if (e.originalTarget == tab) { + self.eventCount++; + } + }, true); +} + +function test() { + tabs = [gBrowser.selectedTab, gBrowser.addTab(), gBrowser.addTab(), gBrowser.addTab()]; + indexTest(0, 0); + indexTest(1, 1); + indexTest(2, 2); + indexTest(3, 3); + + var eh = new PinUnpinHandler(tabs[3], "TabPinned"); + gBrowser.pinTab(tabs[3]); + is(eh.eventCount, 2, "TabPinned event should be fired"); + indexTest(0, 1); + indexTest(1, 2); + indexTest(2, 3); + indexTest(3, 0); + + eh = new PinUnpinHandler(tabs[1], "TabPinned"); + gBrowser.pinTab(tabs[1]); + is(eh.eventCount, 2, "TabPinned event should be fired"); + indexTest(0, 2); + indexTest(1, 1); + indexTest(2, 3); + indexTest(3, 0); + + gBrowser.moveTabTo(tabs[3], 3); + indexTest(3, 1, "shouldn't be able to mix a pinned tab into normal tabs"); + + gBrowser.moveTabTo(tabs[2], 0); + indexTest(2, 2, "shouldn't be able to mix a normal tab into pinned tabs"); + + eh = new PinUnpinHandler(tabs[1], "TabUnpinned"); + gBrowser.unpinTab(tabs[1]); + is(eh.eventCount, 2, "TabUnpinned event should be fired"); + indexTest(1, 1, "unpinning a tab should move a tab to the start of normal tabs"); + + eh = new PinUnpinHandler(tabs[3], "TabUnpinned"); + gBrowser.unpinTab(tabs[3]); + is(eh.eventCount, 2, "TabUnpinned event should be fired"); + indexTest(3, 0, "unpinning a tab should move a tab to the start of normal tabs"); + + gBrowser.removeTab(tabs[1]); + gBrowser.removeTab(tabs[2]); + gBrowser.removeTab(tabs[3]); +} diff --git a/browser/base/content/test/browser_plainTextLinks.js b/browser/base/content/test/browser_plainTextLinks.js new file mode 100644 index 000000000..4c7c8ee98 --- /dev/null +++ b/browser/base/content/test/browser_plainTextLinks.js @@ -0,0 +1,115 @@ +let doc, range, selection; +function setSelection(el1, el2, index1, index2) { + while (el1.nodeType != Node.TEXT_NODE) + el1 = el1.firstChild; + while (el2.nodeType != Node.TEXT_NODE) + el2 = el2.firstChild; + + selection.removeAllRanges(); + range.setStart(el1, index1); + range.setEnd(el2, index2); + selection.addRange(range); +} + +function initContextMenu(aNode) { + document.popupNode = aNode; + let contentAreaContextMenu = document.getElementById("contentAreaContextMenu"); + let contextMenu = new nsContextMenu(contentAreaContextMenu); + return contextMenu; +} + +function testExpected(expected, msg, aNode) { + let popupNode = aNode || doc.getElementsByTagName("DIV")[0]; + initContextMenu(popupNode); + let linkMenuItem = document.getElementById("context-openlinkincurrent"); + is(linkMenuItem.hidden, expected, msg); +} + +function testLinkExpected(expected, msg, aNode) { + let popupNode = aNode || doc.getElementsByTagName("DIV")[0]; + let contextMenu = initContextMenu(popupNode); + is(contextMenu.linkURL, expected, msg); +} + +function runSelectionTests() { + let mainDiv = doc.createElement("div"); + let div = doc.createElement("div"); + let div2 = doc.createElement("div"); + let span1 = doc.createElement("span"); + let span2 = doc.createElement("span"); + let span3 = doc.createElement("span"); + let span4 = doc.createElement("span"); + let p1 = doc.createElement("p"); + let p2 = doc.createElement("p"); + span1.textContent = "http://index."; + span2.textContent = "example.com example.com"; + span3.textContent = " - Test"; + span4.innerHTML = "<a href='http://www.example.com'>http://www.example.com/example</a>"; + p1.textContent = "mailto:test.com ftp.example.com"; + p2.textContent = "example.com -"; + div.appendChild(span1); + div.appendChild(span2); + div.appendChild(span3); + div.appendChild(span4); + div.appendChild(p1); + div.appendChild(p2); + let p3 = doc.createElement("p"); + p3.textContent = "main.example.com"; + div2.appendChild(p3); + mainDiv.appendChild(div); + mainDiv.appendChild(div2); + doc.body.appendChild(mainDiv); + setSelection(span1.firstChild, span2.firstChild, 0, 11); + testExpected(false, "The link context menu should show for http://www.example.com"); + setSelection(span1.firstChild, span2.firstChild, 7, 11); + testExpected(false, "The link context menu should show for www.example.com"); + setSelection(span1.firstChild, span2.firstChild, 8, 11); + testExpected(true, "The link context menu should not show for ww.example.com"); + setSelection(span2.firstChild, span2.firstChild, 0, 11); + testExpected(false, "The link context menu should show for example.com"); + testLinkExpected("http://example.com/", "url for example.com selection should not prepend www"); + setSelection(span2.firstChild, span2.firstChild, 11, 23); + testExpected(false, "The link context menu should show for example.com"); + setSelection(span2.firstChild, span2.firstChild, 0, 10); + testExpected(true, "Link options should not show for selection that's not at a word boundary"); + setSelection(span2.firstChild, span3.firstChild, 12, 7); + testExpected(true, "Link options should not show for selection that has whitespace"); + setSelection(span2.firstChild, span2.firstChild, 12, 19); + testExpected(true, "Link options should not show unless a url is selected"); + setSelection(p1.firstChild, p1.firstChild, 0, 15); + testExpected(true, "Link options should not show for mailto: links"); + setSelection(p1.firstChild, p1.firstChild, 16, 31); + testExpected(false, "Link options should show for ftp.example.com"); + testLinkExpected("ftp://ftp.example.com/", "ftp.example.com should be preceeded with ftp://"); + setSelection(p2.firstChild, p2.firstChild, 0, 14); + testExpected(false, "Link options should show for www.example.com "); + selection.selectAllChildren(div2); + testExpected(false, "Link options should show for triple-click selections"); + selection.selectAllChildren(span4); + testLinkExpected("http://www.example.com/", "Linkified text should open the correct link", span4.firstChild); + + mainDiv.innerHTML = "(open-suse.ru)"; + setSelection(mainDiv, mainDiv, 1, 13); + testExpected(false, "Link options should show for open-suse.ru"); + testLinkExpected("http://open-suse.ru/", "Linkified text should open the correct link"); + setSelection(mainDiv, mainDiv, 1, 14); + testExpected(true, "Link options should not show for 'open-suse.ru)'"); + + gBrowser.removeCurrentTab(); + finish(); +} + +function test() { + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function() { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + doc = content.document; + range = doc.createRange(); + selection = content.getSelection(); + waitForFocus(runSelectionTests, content); + }, true); + + content.location = + "data:text/html;charset=UTF-8,Test For Non-Hyperlinked url selection"; +} diff --git a/browser/base/content/test/browser_pluginCrashCommentAndURL.js b/browser/base/content/test/browser_pluginCrashCommentAndURL.js new file mode 100644 index 000000000..1243daaca --- /dev/null +++ b/browser/base/content/test/browser_pluginCrashCommentAndURL.js @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/Services.jsm"); + +const CRASH_URL = "http://example.com/browser/browser/base/content/test/pluginCrashCommentAndURL.html"; + +const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs"; + +function test() { + // Crashing the plugin takes up a lot of time, so extend the test timeout. + requestLongerTimeout(runs.length); + waitForExplicitFinish(); + + // The test harness sets MOZ_CRASHREPORTER_NO_REPORT, which disables plugin + // crash reports. This test needs them enabled. The test also needs a mock + // report server, and fortunately one is already set up by toolkit/ + // crashreporter/test/Makefile.in. Assign its URL to MOZ_CRASHREPORTER_URL, + // which CrashSubmit.jsm uses as a server override. + let env = Cc["@mozilla.org/process/environment;1"]. + getService(Components.interfaces.nsIEnvironment); + let noReport = env.get("MOZ_CRASHREPORTER_NO_REPORT"); + let serverURL = env.get("MOZ_CRASHREPORTER_URL"); + env.set("MOZ_CRASHREPORTER_NO_REPORT", ""); + env.set("MOZ_CRASHREPORTER_URL", SERVER_URL); + + let tab = gBrowser.loadOneTab("about:blank", { inBackground: false }); + let browser = gBrowser.getBrowserForTab(tab); + browser.addEventListener("PluginCrashed", onCrash, false); + Services.obs.addObserver(onSubmitStatus, "crash-report-status", false); + + registerCleanupFunction(function cleanUp() { + env.set("MOZ_CRASHREPORTER_NO_REPORT", noReport); + env.set("MOZ_CRASHREPORTER_URL", serverURL); + gBrowser.selectedBrowser.removeEventListener("PluginCrashed", onCrash, + false); + Services.obs.removeObserver(onSubmitStatus, "crash-report-status"); + gBrowser.removeCurrentTab(); + }); + + doNextRun(); +} + +let runs = [ + { + shouldSubmissionUIBeVisible: true, + comment: "", + urlOptIn: false, + }, + { + shouldSubmissionUIBeVisible: true, + comment: "a test comment", + urlOptIn: true, + }, + { + width: 300, + height: 300, + shouldSubmissionUIBeVisible: false, + }, +]; + +let currentRun = null; + +function doNextRun() { + try { + if (!runs.length) { + finish(); + return; + } + currentRun = runs.shift(); + let args = ["width", "height"].reduce(function (memo, arg) { + if (arg in currentRun) + memo[arg] = currentRun[arg]; + return memo; + }, {}); + gBrowser.loadURI(CRASH_URL + "?" + + encodeURIComponent(JSON.stringify(args))); + // And now wait for the crash. + } + catch (err) { + failWithException(err); + finish(); + } +} + +function onCrash() { + try { + let plugin = gBrowser.contentDocument.getElementById("plugin"); + let elt = gPluginHandler.getPluginUI.bind(gPluginHandler, plugin); + let style = + gBrowser.contentWindow.getComputedStyle(elt("msg msgPleaseSubmit")); + is(style.display, + currentRun.shouldSubmissionUIBeVisible ? "block" : "none", + "Submission UI visibility should be correct"); + if (!currentRun.shouldSubmissionUIBeVisible) { + // Done with this run. + doNextRun(); + return; + } + elt("submitComment").value = currentRun.comment; + elt("submitURLOptIn").checked = currentRun.urlOptIn; + elt("submitButton").click(); + // And now wait for the submission status notification. + } + catch (err) { + failWithException(err); + doNextRun(); + } +} + +function onSubmitStatus(subj, topic, data) { + try { + // Wait for success or failed, doesn't matter which. + if (data != "success" && data != "failed") + return; + + let extra = getPropertyBagValue(subj.QueryInterface(Ci.nsIPropertyBag), + "extra"); + ok(extra instanceof Ci.nsIPropertyBag, "Extra data should be property bag"); + + let val = getPropertyBagValue(extra, "PluginUserComment"); + if (currentRun.comment) + is(val, currentRun.comment, + "Comment in extra data should match comment in textbox"); + else + ok(val === undefined, + "Comment should be absent from extra data when textbox is empty"); + + val = getPropertyBagValue(extra, "PluginContentURL"); + if (currentRun.urlOptIn) + is(val, gBrowser.currentURI.spec, + "URL in extra data should match browser URL when opt-in checked"); + else + ok(val === undefined, + "URL should be absent from extra data when opt-in not checked"); + } + catch (err) { + failWithException(err); + } + doNextRun(); +} + +function getPropertyBagValue(bag, key) { + try { + var val = bag.getProperty(key); + } + catch (e if e.result == Cr.NS_ERROR_FAILURE) {} + return val; +} + +function failWithException(err) { + ok(false, "Uncaught exception: " + err + "\n" + err.stack); +} diff --git a/browser/base/content/test/browser_pluginnotification.js b/browser/base/content/test/browser_pluginnotification.js new file mode 100644 index 000000000..1b4fc86cc --- /dev/null +++ b/browser/base/content/test/browser_pluginnotification.js @@ -0,0 +1,841 @@ +var rootDir = getRootDirectory(gTestPath); +const gTestRoot = rootDir; +const gHttpTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/"); + +var gTestBrowser = null; +var gNextTest = null; +var gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost); + +Components.utils.import("resource://gre/modules/Services.jsm"); + +// This listens for the next opened tab and checks it is of the right url. +// opencallback is called when the new tab is fully loaded +// closecallback is called when the tab is closed +function TabOpenListener(url, opencallback, closecallback) { + this.url = url; + this.opencallback = opencallback; + this.closecallback = closecallback; + + gBrowser.tabContainer.addEventListener("TabOpen", this, false); +} + +TabOpenListener.prototype = { + url: null, + opencallback: null, + closecallback: null, + tab: null, + browser: null, + + handleEvent: function(event) { + if (event.type == "TabOpen") { + gBrowser.tabContainer.removeEventListener("TabOpen", this, false); + this.tab = event.originalTarget; + this.browser = this.tab.linkedBrowser; + gBrowser.addEventListener("pageshow", this, false); + } else if (event.type == "pageshow") { + if (event.target.location.href != this.url) + return; + gBrowser.removeEventListener("pageshow", this, false); + this.tab.addEventListener("TabClose", this, false); + var url = this.browser.contentDocument.location.href; + is(url, this.url, "Should have opened the correct tab"); + this.opencallback(this.tab, this.browser.contentWindow); + } else if (event.type == "TabClose") { + if (event.originalTarget != this.tab) + return; + this.tab.removeEventListener("TabClose", this, false); + this.opencallback = null; + this.tab = null; + this.browser = null; + // Let the window close complete + executeSoon(this.closecallback); + this.closecallback = null; + } + } +}; + +function test() { + waitForExplicitFinish(); + requestLongerTimeout(2); + registerCleanupFunction(function() { + clearAllPluginPermissions(); + Services.prefs.clearUserPref("extensions.blocklist.suppressUI"); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + Services.prefs.setBoolPref("extensions.blocklist.suppressUI", true); + + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + prepareTest(runAfterPluginBindingAttached(test1), gTestRoot + "plugin_unknown.html"); +} + +function finishTest() { + clearAllPluginPermissions(); + gTestBrowser.removeEventListener("load", pageLoad, true); + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} + +function pageLoad() { + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + executeSoon(gNextTest); +} + +function prepareTest(nextTest, url) { + gNextTest = nextTest; + gTestBrowser.contentWindow.location = url; +} + +// Due to layout being async, "PluginBindAttached" may trigger later. +// This wraps a function to force a layout flush, thus triggering it, +// and schedules the function execution so they're definitely executed +// afterwards. +function runAfterPluginBindingAttached(func) { + return function() { + let doc = gTestBrowser.contentDocument; + let elems = doc.getElementsByTagName('embed'); + if (elems.length < 1) { + elems = doc.getElementsByTagName('object'); + } + elems[0].clientTop; + executeSoon(func); + }; +} + +// Tests a page with an unknown plugin in it. +function test1() { + ok(PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 1, Should have displayed the missing plugin notification"); + ok(gTestBrowser.missingPlugins, "Test 1, Should be a missing plugin list"); + ok(gTestBrowser.missingPlugins.has("application/x-unknown"), "Test 1, Should know about application/x-unknown"); + ok(!gTestBrowser.missingPlugins.has("application/x-test"), "Test 1, Should not know about application/x-test"); + + var pluginNode = gTestBrowser.contentDocument.getElementById("unknown"); + ok(pluginNode, "Test 1, Found plugin in page"); + var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED, "Test 1, plugin fallback type should be PLUGIN_UNSUPPORTED"); + + var plugin = getTestPlugin(); + ok(plugin, "Should have a test plugin"); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + prepareTest(runAfterPluginBindingAttached(test2), gTestRoot + "plugin_test.html"); +} + +// Tests a page with a working plugin in it. +function test2() { + ok(!PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 2, Should not have displayed the missing plugin notification"); + ok(!gTestBrowser.missingPlugins, "Test 2, Should not be a missing plugin list"); + + var plugin = getTestPlugin(); + ok(plugin, "Should have a test plugin"); + plugin.enabledState = Ci.nsIPluginTag.STATE_DISABLED; + prepareTest(runAfterPluginBindingAttached(test3), gTestRoot + "plugin_test.html"); +} + +// Tests a page with a disabled plugin in it. +function test3() { + ok(!PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 3, Should not have displayed the missing plugin notification"); + ok(!gTestBrowser.missingPlugins, "Test 3, Should not be a missing plugin list"); + + new TabOpenListener("about:addons", test4, prepareTest5); + + var pluginNode = gTestBrowser.contentDocument.getElementById("test"); + ok(pluginNode, "Test 3, Found plugin in page"); + var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_DISABLED, "Test 3, plugin fallback type should be PLUGIN_DISABLED"); + var manageLink = gTestBrowser.contentDocument.getAnonymousElementByAttribute(pluginNode, "anonid", "managePluginsLink"); + ok(manageLink, "Test 3, found 'manage' link in plugin-problem binding"); + + EventUtils.synthesizeMouseAtCenter(manageLink, {}, gTestBrowser.contentWindow); +} + +function test4(tab, win) { + is(win.wrappedJSObject.gViewController.currentViewId, "addons://list/plugin", "Test 4, Should have displayed the plugins pane"); + gBrowser.removeTab(tab); +} + +function prepareTest5() { + info("prepareTest5"); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + setAndUpdateBlocklist(gHttpTestRoot + "blockPluginHard.xml", + function() { + info("prepareTest5 callback"); + prepareTest(runAfterPluginBindingAttached(test5), gTestRoot + "plugin_test.html"); + }); +} + +// Tests a page with a blocked plugin in it. +function test5() { + info("test5"); + ok(!PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 5, Should not have displayed the missing plugin notification"); + let notification = PopupNotifications.getNotification("click-to-play-plugins"); + ok(notification, "Test 5: There should be a plugin notification for blocked plugins"); + ok(notification.dismissed, "Test 5: The plugin notification should be dismissed by default"); + + notification.reshow(); + is(notification.options.centerActions.length, 1, "Test 5: Only the blocked plugin should be present in the notification"); + ok(PopupNotifications.panel.firstChild._buttonContainer.hidden, "Part 5: The blocked plugins notification should not have any buttons visible."); + + ok(!gTestBrowser.missingPlugins, "Test 5, Should not be a missing plugin list"); + var pluginNode = gTestBrowser.contentDocument.getElementById("test"); + ok(pluginNode, "Test 5, Found plugin in page"); + var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_BLOCKLISTED, "Test 5, plugin fallback type should be PLUGIN_BLOCKLISTED"); + + prepareTest(runAfterPluginBindingAttached(test6), gTestRoot + "plugin_both.html"); +} + +// Tests a page with a blocked and unknown plugin in it. +function test6() { + ok(PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 6, Should have displayed the missing plugin notification"); + ok(gTestBrowser.missingPlugins, "Test 6, Should be a missing plugin list"); + ok(gTestBrowser.missingPlugins.has("application/x-unknown"), "Test 6, Should know about application/x-unknown"); + ok(!gTestBrowser.missingPlugins.has("application/x-test"), "Test 6, application/x-test should not be a missing plugin"); + + prepareTest(runAfterPluginBindingAttached(test7), gTestRoot + "plugin_both2.html"); +} + +// Tests a page with a blocked and unknown plugin in it (alternate order to above). +function test7() { + ok(PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 7, Should have displayed the missing plugin notification"); + ok(gTestBrowser.missingPlugins, "Test 7, Should be a missing plugin list"); + ok(gTestBrowser.missingPlugins.has("application/x-unknown"), "Test 7, Should know about application/x-unknown"); + ok(!gTestBrowser.missingPlugins.has("application/x-test"), "Test 7, application/x-test should not be a missing plugin"); + + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml", function() { + prepareTest(runAfterPluginBindingAttached(test8), gTestRoot + "plugin_test.html"); + }); +} + +// Tests a page with a working plugin that is click-to-play +function test8() { + ok(!PopupNotifications.getNotification("plugins-not-found", gTestBrowser), "Test 8, Should not have displayed the missing plugin notification"); + ok(!gTestBrowser.missingPlugins, "Test 8, Should not be a missing plugin list"); + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser), "Test 8, Should have a click-to-play notification"); + + var pluginNode = gTestBrowser.contentDocument.getElementById("test"); + ok(pluginNode, "Test 8, Found plugin in page"); + var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "Test 8, plugin fallback type should be PLUGIN_CLICK_TO_PLAY"); + + prepareTest(runAfterPluginBindingAttached(test11a), gTestRoot + "plugin_test3.html"); +} + +// Tests 9 & 10 removed + +// Tests that the going back will reshow the notification for click-to-play plugins (part 1/4) +function test11a() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 11a, Should have a click-to-play notification"); + + prepareTest(test11b, "about:blank"); +} + +// Tests that the going back will reshow the notification for click-to-play plugins (part 2/4) +function test11b() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(!popupNotification, "Test 11b, Should not have a click-to-play notification"); + + Services.obs.addObserver(test11c, "PopupNotifications-updateNotShowing", false); + gTestBrowser.contentWindow.history.back(); +} + +// Tests that the going back will reshow the notification for click-to-play plugins (part 3/4) +function test11c() { + Services.obs.removeObserver(test11c, "PopupNotifications-updateNotShowing"); + var condition = function() PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + waitForCondition(condition, test11d, "Test 11c, waited too long for click-to-play-plugin notification"); +} + +// Tests that the going back will reshow the notification for click-to-play plugins (part 4/4) +function test11d() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 11d, Should have a click-to-play notification"); + + prepareTest(runAfterPluginBindingAttached(test12a), gHttpTestRoot + "plugin_clickToPlayAllow.html"); +} + +// Tests that the "Allow Always" permission works for click-to-play plugins +function test12a() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 12a, Should have a click-to-play notification"); + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 12a, Plugin should not be activated"); + + // Simulate clicking the "Allow Always" button. + popupNotification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test12b, "Test 12a, Waited too long for plugin to activate"); +} + +function test12b() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 12d, Should have a click-to-play notification"); + prepareTest(runAfterPluginBindingAttached(test12c), gHttpTestRoot + "plugin_two_types.html"); +} + +// Test that the "Always" permission, when set for just the Test plugin, +// does not also allow the Second Test plugin. +function test12c() { + var popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "Test 12d, Should have a click-to-play notification"); + var test = gTestBrowser.contentDocument.getElementById("test"); + var secondtestA = gTestBrowser.contentDocument.getElementById("secondtestA"); + var secondtestB = gTestBrowser.contentDocument.getElementById("secondtestB"); + ok(test.activated, "Test 12d, Test plugin should be activated"); + ok(!secondtestA.activated, "Test 12d, Second Test plugin (A) should not be activated"); + ok(!secondtestB.activated, "Test 12d, Second Test plugin (B) should not be activated"); + + clearAllPluginPermissions(); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + prepareTest(test14, gTestRoot + "plugin_test2.html"); +} + +// Test 13 removed + +// Tests that the plugin's "activated" property is true for working plugins with click-to-play disabled. +function test14() { + var plugin = gTestBrowser.contentDocument.getElementById("test1"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 14, Plugin should be activated"); + + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + prepareTest(runAfterPluginBindingAttached(test15), gTestRoot + "plugin_alternate_content.html"); +} + +// Tests that the overlay is shown instead of alternate content when +// plugins are click to play +function test15() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var doc = gTestBrowser.contentDocument; + var mainBox = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + ok(mainBox, "Test 15, Plugin with id=" + plugin.id + " overlay should exist"); + + prepareTest(runAfterPluginBindingAttached(test17), gTestRoot + "plugin_bug749455.html"); +} + +// Test 16 removed + +// Tests that mContentType is used for click-to-play plugins, and not the +// inspected type. +function test17() { + var clickToPlayNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(clickToPlayNotification, "Test 17, Should have a click-to-play notification"); + var missingNotification = PopupNotifications.getNotification("missing-plugins", gTestBrowser); + ok(!missingNotification, "Test 17, Should not have a missing plugin notification"); + + setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml", + function() { + prepareTest(runAfterPluginBindingAttached(test18a), gHttpTestRoot + "plugin_test.html"); + }); +} + +// Tests a vulnerable, updatable plugin +function test18a() { + var clickToPlayNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(clickToPlayNotification, "Test 18a, Should have a click-to-play notification"); + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + ok(plugin, "Test 18a, Found plugin in page"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "Test 18a, plugin fallback type should be PLUGIN_VULNERABLE_UPDATABLE"); + ok(!objLoadingContent.activated, "Test 18a, Plugin should not be activated"); + var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + ok(overlay.style.visibility != "hidden", "Test 18a, Plugin overlay should exist, not be hidden"); + var updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink"); + ok(updateLink.style.visibility != "hidden", "Test 18a, Plugin should have an update link"); + + var tabOpenListener = new TabOpenListener(Services.urlFormatter.formatURLPref("plugins.update.url"), false, false); + tabOpenListener.handleEvent = function(event) { + if (event.type == "TabOpen") { + gBrowser.tabContainer.removeEventListener("TabOpen", this, false); + this.tab = event.originalTarget; + ok(event.target.label == this.url, "Test 18a, Update link should open up the plugin check page"); + gBrowser.removeTab(this.tab); + test18b(); + } + }; + EventUtils.synthesizeMouseAtCenter(updateLink, {}, gTestBrowser.contentWindow); +} + +function test18b() { + // clicking the update link should not activate the plugin + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 18b, Plugin should not be activated"); + var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + ok(overlay.style.visibility != "hidden", "Test 18b, Plugin overlay should exist, not be hidden"); + + setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableNoUpdate.xml", + function() { + prepareTest(runAfterPluginBindingAttached(test18c), gHttpTestRoot + "plugin_test.html"); + }); +} + +// Tests a vulnerable plugin with no update +function test18c() { + var clickToPlayNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(clickToPlayNotification, "Test 18c, Should have a click-to-play notification"); + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + ok(plugin, "Test 18c, Found plugin in page"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_NO_UPDATE, "Test 18c, plugin fallback type should be PLUGIN_VULNERABLE_NO_UPDATE"); + ok(!objLoadingContent.activated, "Test 18c, Plugin should not be activated"); + var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + ok(overlay.style.visibility != "hidden", "Test 18c, Plugin overlay should exist, not be hidden"); + var updateLink = doc.getAnonymousElementByAttribute(plugin, "anonid", "checkForUpdatesLink"); + ok(updateLink.style.display != "block", "Test 18c, Plugin should not have an update link"); + + // check that click "Always allow" works with blocklisted plugins + clickToPlayNotification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test18d, "Test 18d, Waited too long for plugin to activate"); +} + +// continue testing "Always allow" +function test18d() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 18d, Plugin should be activated"); + + prepareTest(test18e, gHttpTestRoot + "plugin_test.html"); +} + +// continue testing "Always allow" +function test18e() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 18e, Plugin should be activated"); + + clearAllPluginPermissions(); + prepareTest(runAfterPluginBindingAttached(test18f), gHttpTestRoot + "plugin_test.html"); +} + +// clicking the in-content overlay of a vulnerable plugin should bring +// up the notification and not directly activate the plugin +function test18f() { + var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "Test 18f, Should have a click-to-play notification"); + ok(notification.dismissed, "Test 18f, notification should start dismissed"); + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 18f, Plugin should not be activated"); + + // XXXBAD: this code doesn't do what you think it does! it is actually + // observing the "removed" event of the old notification, since we create + // a *new* one when the plugin is clicked. + notification.options.eventCallback = function() { executeSoon(test18g); }; + EventUtils.synthesizeMouseAtCenter(plugin, {}, gTestBrowser.contentWindow); +} + +function test18g() { + var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "Test 18g, Should have a click-to-play notification"); + ok(!notification.dismissed, "Test 18g, notification should be open"); + notification.options.eventCallback = null; + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 18g, Plugin should not be activated"); + + setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml", + function() { + resetBlocklist(); + prepareTest(runAfterPluginBindingAttached(test19a), gTestRoot + "plugin_test.html"); + }); +} + +// Tests that clicking the icon of the overlay activates the doorhanger +function test19a() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 19a, Plugin should not be activated"); + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed, "Test 19a, Doorhanger should start out dismissed"); + + var icon = doc.getAnonymousElementByAttribute(plugin, "class", "icon"); + EventUtils.synthesizeMouseAtCenter(icon, {}, gTestBrowser.contentWindow); + let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed; + waitForCondition(condition, test19b, "Test 19a, Waited too long for doorhanger to activate"); +} + +function test19b() { + prepareTest(runAfterPluginBindingAttached(test19c), gTestRoot + "plugin_test.html"); +} + +// Tests that clicking the text of the overlay activates the plugin +function test19c() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 19c, Plugin should not be activated"); + + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed, "Test 19c, Doorhanger should start out dismissed"); + + var text = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgClickToPlay"); + EventUtils.synthesizeMouseAtCenter(text, {}, gTestBrowser.contentWindow); + let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed; + waitForCondition(condition, test19d, "Test 19c, Waited too long for doorhanger to activate"); +} + +function test19d() { + prepareTest(runAfterPluginBindingAttached(test19e), gTestRoot + "plugin_test.html"); +} + +// Tests that clicking the box of the overlay activates the doorhanger +// (just to be thorough) +function test19e() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 19e, Plugin should not be activated"); + + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed, "Test 19e, Doorhanger should start out dismissed"); + + EventUtils.synthesizeMouse(plugin, 50, 50, {}, gTestBrowser.contentWindow); + let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed; + waitForCondition(condition, test19f, "Test 19e, Waited too long for plugin to activate"); +} + +function test19f() { + prepareTest(test20a, gTestRoot + "plugin_hidden_to_visible.html"); +} + +// Tests that a plugin in a div that goes from style="display: none" to +// "display: block" can be clicked to activate. +function test20a() { + var clickToPlayNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(!clickToPlayNotification, "Test 20a, Should not have a click-to-play notification"); + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("plugin"); + var mainBox = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox"); + ok(mainBox, "Test 20a, plugin overlay should not be null"); + var pluginRect = mainBox.getBoundingClientRect(); + ok(pluginRect.width == 0, "Test 20a, plugin should have an overlay with 0px width"); + ok(pluginRect.height == 0, "Test 20a, plugin should have an overlay with 0px height"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 20a, plugin should not be activated"); + var div = doc.getElementById("container"); + ok(div.style.display == "none", "Test 20a, container div should be display: none"); + + div.style.display = "block"; + var condition = function() { + var pluginRect = mainBox.getBoundingClientRect(); + return (pluginRect.width == 200); + } + waitForCondition(condition, test20b, "Test 20a, Waited too long for plugin to become visible"); +} + +function test20b() { + var clickToPlayNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(clickToPlayNotification, "Test 20b, Should now have a click-to-play notification"); + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("plugin"); + var pluginRect = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox").getBoundingClientRect(); + ok(pluginRect.width == 200, "Test 20b, plugin should have an overlay with 200px width"); + ok(pluginRect.height == 200, "Test 20b, plugin should have an overlay with 200px height"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 20b, plugin should not be activated"); + + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed, "Test 20b, Doorhanger should start out dismissed"); + + EventUtils.synthesizeMouseAtCenter(plugin, {}, gTestBrowser.contentWindow); + let condition = function() !PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser).dismissed; + waitForCondition(condition, test20c, "Test 20b, Waited too long for plugin to activate"); +} + +function test20c() { + PopupNotifications.panel.firstChild._primaryButton.click(); + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("plugin"); + let condition = function() plugin.activated; + waitForCondition(condition, test20d, "Test 20c", "Waiting for plugin to activate"); +} + +function test20d() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("plugin"); + var pluginRect = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox").getBoundingClientRect(); + ok(pluginRect.width == 0, "Test 20d, plugin should have click-to-play overlay with zero width"); + ok(pluginRect.height == 0, "Test 20d, plugin should have click-to-play overlay with zero height"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 20d, plugin should be activated"); + + clearAllPluginPermissions(); + + prepareTest(runAfterPluginBindingAttached(test21a), gTestRoot + "plugin_two_types.html"); +} + +// Test having multiple different types of plugin on one page +function test21a() { + var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "Test 21a, Should have a click-to-play notification"); + + var doc = gTestBrowser.contentDocument; + var ids = ["test", "secondtestA", "secondtestB"]; + for (var id of ids) { + var plugin = doc.getElementById(id); + var rect = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox").getBoundingClientRect(); + ok(rect.width == 200, "Test 21a, Plugin with id=" + plugin.id + " overlay rect should have 200px width before being clicked"); + ok(rect.height == 200, "Test 21a, Plugin with id=" + plugin.id + " overlay rect should have 200px height before being clicked"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 21a, Plugin with id=" + plugin.id + " should not be activated"); + } + + // we have to actually show the panel to get the bindings to instantiate + notification.reshow(); + is(notification.options.centerActions.length, 2, "Test 21a, Should have two types of plugin in the notification"); + + var centerAction = null; + for (var action of notification.options.centerActions) { + if (action.pluginName == "Test") { + centerAction = action; + break; + } + } + ok(centerAction, "Test 21b, found center action for the Test plugin"); + + var centerItem = null; + for (var item of PopupNotifications.panel.firstChild.childNodes) { + is(item.value, "block", "Test 21b, all plugins should start out blocked"); + if (item.action == centerAction) { + centerItem = item; + break; + } + } + ok(centerItem, "Test 21b, found center item for the Test plugin"); + + // "click" the button to activate the Test plugin + centerItem.value = "allownow"; + PopupNotifications.panel.firstChild._primaryButton.click(); + + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test21c, "Test 21b, Waited too long for plugin to activate"); +} + +function test21c() { + var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "Test 21c, Should have a click-to-play notification"); + + notification.reshow(); + ok(notification.options.centerActions.length == 2, "Test 21c, Should have one type of plugin in the notification"); + + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var rect = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox").getBoundingClientRect(); + ok(rect.width == 0, "Test 21c, Plugin with id=" + plugin.id + " overlay rect should have 0px width after being clicked"); + ok(rect.height == 0, "Test 21c, Plugin with id=" + plugin.id + " overlay rect should have 0px height after being clicked"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 21c, Plugin with id=" + plugin.id + " should be activated"); + + var ids = ["secondtestA", "secondtestB"]; + for (var id of ids) { + var plugin = doc.getElementById(id); + var rect = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox").getBoundingClientRect(); + ok(rect.width == 200, "Test 21c, Plugin with id=" + plugin.id + " overlay rect should have 200px width before being clicked"); + ok(rect.height == 200, "Test 21c, Plugin with id=" + plugin.id + " overlay rect should have 200px height before being clicked"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(!objLoadingContent.activated, "Test 21c, Plugin with id=" + plugin.id + " should not be activated"); + } + + var centerAction = null; + for (var action of notification.options.centerActions) { + if (action.pluginName == "Second Test") { + centerAction = action; + break; + } + } + ok(centerAction, "Test 21d, found center action for the Second Test plugin"); + + var centerItem = null; + for (var item of PopupNotifications.panel.firstChild.childNodes) { + if (item.action == centerAction) { + is(item.value, "block", "Test 21d, test plugin 2 should start blocked"); + centerItem = item; + break; + } + else { + is(item.value, "allownow", "Test 21d, test plugin should be enabled"); + } + } + ok(centerItem, "Test 21d, found center item for the Second Test plugin"); + + // "click" the button to activate the Second Test plugins + centerItem.value = "allownow"; + PopupNotifications.panel.firstChild._primaryButton.click(); + + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("secondtestA"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test21e, "Test 21d, Waited too long for plugin to activate"); +} + +function test21e() { + var doc = gTestBrowser.contentDocument; + var ids = ["test", "secondtestA", "secondtestB"]; + for (var id of ids) { + var plugin = doc.getElementById(id); + var rect = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox").getBoundingClientRect(); + ok(rect.width == 0, "Test 21e, Plugin with id=" + plugin.id + " overlay rect should have 0px width after being clicked"); + ok(rect.height == 0, "Test 21e, Plugin with id=" + plugin.id + " overlay rect should have 0px height after being clicked"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 21e, Plugin with id=" + plugin.id + " should be activated"); + } + + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + clearAllPluginPermissions(); + + prepareTest(runAfterPluginBindingAttached(test22), gTestRoot + "plugin_test.html"); +} + +// Tests that a click-to-play plugin retains its activated state upon reloading +function test22() { + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser), "Test 22, Should have a click-to-play notification"); + + // Plugin should start as CTP + var pluginNode = gTestBrowser.contentDocument.getElementById("test"); + ok(pluginNode, "Test 22, Found plugin in page"); + var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "Test 22, plugin fallback type should be PLUGIN_CLICK_TO_PLAY"); + + // Activate + objLoadingContent.playPlugin(); + is(objLoadingContent.displayedType, Ci.nsIObjectLoadingContent.TYPE_PLUGIN, "Test 22, plugin should have started"); + ok(pluginNode.activated, "Test 22, plugin should be activated"); + + // Reload plugin + var oldVal = pluginNode.getObjectValue(); + pluginNode.src = pluginNode.src; + is(objLoadingContent.displayedType, Ci.nsIObjectLoadingContent.TYPE_PLUGIN, "Test 22, Plugin should have retained activated state"); + ok(pluginNode.activated, "Test 22, plugin should have remained activated"); + // Sanity, ensure that we actually reloaded the instance, since this behavior might change in the future. + var pluginsDiffer; + try { + pluginNode.checkObjectValue(oldVal); + } catch (e) { + pluginsDiffer = true; + } + ok(pluginsDiffer, "Test 22, plugin should have reloaded"); + + prepareTest(runAfterPluginBindingAttached(test23), gTestRoot + "plugin_test.html"); +} + +// Tests that a click-to-play plugin resets its activated state when changing types +function test23() { + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser), "Test 23, Should have a click-to-play notification"); + + // Plugin should start as CTP + var pluginNode = gTestBrowser.contentDocument.getElementById("test"); + ok(pluginNode, "Test 23, Found plugin in page"); + var objLoadingContent = pluginNode.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "Test 23, plugin fallback type should be PLUGIN_CLICK_TO_PLAY"); + + // Activate + objLoadingContent.playPlugin(); + is(objLoadingContent.displayedType, Ci.nsIObjectLoadingContent.TYPE_PLUGIN, "Test 23, plugin should have started"); + ok(pluginNode.activated, "Test 23, plugin should be activated"); + + // Reload plugin (this may need RunSoon() in the future when plugins change state asynchronously) + pluginNode.type = null; + // We currently don't properly change state just on type change, + // so rebind the plugin to tree. bug 767631 + pluginNode.parentNode.appendChild(pluginNode); + is(objLoadingContent.displayedType, Ci.nsIObjectLoadingContent.TYPE_NULL, "Test 23, plugin should be unloaded"); + pluginNode.type = "application/x-test"; + pluginNode.parentNode.appendChild(pluginNode); + is(objLoadingContent.displayedType, Ci.nsIObjectLoadingContent.TYPE_NULL, "Test 23, Plugin should not have activated"); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "Test 23, Plugin should be click-to-play"); + ok(!pluginNode.activated, "Test 23, plugin node should not be activated"); + + prepareTest(runAfterPluginBindingAttached(test24a), gHttpTestRoot + "plugin_test.html"); +} + +// Test that "always allow"-ing a plugin will not allow it when it becomes +// blocklisted. +function test24a() { + var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "Test 24a, Should have a click-to-play notification"); + var plugin = gTestBrowser.contentDocument.getElementById("test"); + ok(plugin, "Test 24a, Found plugin in page"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY, "Test 24a, Plugin should be click-to-play"); + ok(!objLoadingContent.activated, "Test 24a, plugin should not be activated"); + + // simulate "always allow" + notification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + prepareTest(test24b, gHttpTestRoot + "plugin_test.html"); +} + +// did the "always allow" work as intended? +function test24b() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + ok(plugin, "Test 24b, Found plugin in page"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 24b, plugin should be activated"); + setAndUpdateBlocklist(gHttpTestRoot + "blockPluginVulnerableUpdatable.xml", + function() { + prepareTest(runAfterPluginBindingAttached(test24c), gHttpTestRoot + "plugin_test.html"); + }); +} + +// the plugin is now blocklisted, so it should not automatically load +function test24c() { + var notification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(notification, "Test 24c, Should have a click-to-play notification"); + var plugin = gTestBrowser.contentDocument.getElementById("test"); + ok(plugin, "Test 24c, Found plugin in page"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_VULNERABLE_UPDATABLE, "Test 24c, Plugin should be vulnerable/updatable"); + ok(!objLoadingContent.activated, "Test 24c, plugin should not be activated"); + + // simulate "always allow" + notification.reshow(); + PopupNotifications.panel.firstChild._primaryButton.click(); + + prepareTest(test24d, gHttpTestRoot + "plugin_test.html"); +} + +// We should still be able to always allow a plugin after we've seen that it's +// blocklisted. +function test24d() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + ok(plugin, "Test 24d, Found plugin in page"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 24d, plugin should be activated"); + + // this resets the vulnerable plugin permission + setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml", + function() { + clearAllPluginPermissions(); + resetBlocklist(); + finishTest(); + }); +} diff --git a/browser/base/content/test/browser_pluginplaypreview.js b/browser/base/content/test/browser_pluginplaypreview.js new file mode 100644 index 000000000..d1d8a53fb --- /dev/null +++ b/browser/base/content/test/browser_pluginplaypreview.js @@ -0,0 +1,317 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 rootDir = getRootDirectory(gTestPath); +const gTestRoot = rootDir; + +var gTestBrowser = null; +var gNextTest = null; +var gNextTestSkip = 0; +var gPlayPreviewPluginActualEvents = 0; +var gPlayPreviewPluginExpectedEvents = 1; + +var gPlayPreviewRegistration = null; + +function registerPlayPreview(mimeType, targetUrl) { + + function StreamConverterFactory() {} + StreamConverterFactory.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory]), + _targetConstructor: null, + + register: function register(targetConstructor) { + this._targetConstructor = targetConstructor; + var proto = targetConstructor.prototype; + var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.registerFactory(proto.classID, proto.classDescription, + proto.contractID, this); + }, + + unregister: function unregister() { + var proto = this._targetConstructor.prototype; + var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + registrar.unregisterFactory(proto.classID, this); + this._targetConstructor = null; + }, + + // nsIFactory + createInstance: function createInstance(aOuter, iid) { + if (aOuter !== null) + throw Cr.NS_ERROR_NO_AGGREGATION; + return (new (this._targetConstructor)).QueryInterface(iid); + }, + + // nsIFactory + lockFactory: function lockFactory(lock) { + // No longer used as of gecko 1.7. + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + }; + + function OverlayStreamConverter() {} + OverlayStreamConverter.prototype = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsISupports, + Ci.nsIStreamConverter, + Ci.nsIStreamListener, + Ci.nsIRequestObserver + ]), + + classID: Components.ID('{4c6030f7-e20a-264f-0f9b-ada3a9e97384}'), + classDescription: 'overlay-test-data Component', + contractID: '@mozilla.org/streamconv;1?from=application/x-moz-playpreview&to=*/*', + + // nsIStreamConverter::convert + convert: function(aFromStream, aFromType, aToType, aCtxt) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + // nsIStreamConverter::asyncConvertData + asyncConvertData: function(aFromType, aToType, aListener, aCtxt) { + var isValidRequest = false; + try { + var request = aCtxt; + request.QueryInterface(Ci.nsIChannel); + var spec = request.URI.spec; + var expectedSpec = 'data:application/x-moz-playpreview;,' + mimeType; + isValidRequest = (spec == expectedSpec); + } catch (e) { } + if (!isValidRequest) + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + + // Store the listener passed to us + this.listener = aListener; + }, + + // nsIStreamListener::onDataAvailable + onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { + // Do nothing since all the data loading is handled by the viewer. + ok(false, "onDataAvailable should not be called"); + }, + + // nsIRequestObserver::onStartRequest + onStartRequest: function(aRequest, aContext) { + + // Setup the request so we can use it below. + aRequest.QueryInterface(Ci.nsIChannel); + // Cancel the request so the viewer can handle it. + aRequest.cancel(Cr.NS_BINDING_ABORTED); + + // Create a new channel that is viewer loaded as a resource. + var ioService = Services.io; + var channel = ioService.newChannel(targetUrl, null, null); + channel.asyncOpen(this.listener, aContext); + }, + + // nsIRequestObserver::onStopRequest + onStopRequest: function(aRequest, aContext, aStatusCode) { + // Do nothing. + } + }; + + var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + ph.registerPlayPreviewMimeType(mimeType, true); // ignoring CTP rules + + var factory = new StreamConverterFactory(); + factory.register(OverlayStreamConverter); + + return (gPlayPreviewRegistration = { + unregister: function() { + ph.unregisterPlayPreviewMimeType(mimeType); + factory.unregister(); + gPlayPreviewRegistration = null; + } + }); +} + +function unregisterPlayPreview() { + gPlayPreviewRegistration.unregister(); +} + +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); +Components.utils.import("resource://gre/modules/Services.jsm"); + + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + if (gPlayPreviewRegistration) + gPlayPreviewRegistration.unregister(); + Services.prefs.clearUserPref("plugins.click_to_play"); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + gTestBrowser.addEventListener("PluginBindingAttached", handleBindingAttached, true, true); + + registerPlayPreview('application/x-test', 'about:'); + prepareTest(test1a, gTestRoot + "plugin_test.html", 1); +} + +function finishTest() { + gTestBrowser.removeEventListener("load", pageLoad, true); + gTestBrowser.removeEventListener("PluginBindingAttached", handleBindingAttached, true, true); + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} + +function handleBindingAttached(evt) { + if (evt.target instanceof Ci.nsIObjectLoadingContent && + evt.target.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW) + gPlayPreviewPluginActualEvents++; +} + +function pageLoad() { + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + + // iframe might triggers load event as well, making sure we skip some to let + // all iframes on the page be loaded as well + if (gNextTestSkip) { + gNextTestSkip--; + return; + } + executeSoon(gNextTest); +} + +function prepareTest(nextTest, url, skip) { + gNextTest = nextTest; + gNextTestSkip = skip; + gTestBrowser.contentWindow.location = url; +} + +// Tests a page with normal play preview registration (1/2) +function test1a() { + var notificationBox = gBrowser.getNotificationBox(gTestBrowser); + ok(!notificationBox.getNotificationWithValue("missing-plugins"), "Test 1a, Should not have displayed the missing plugin notification"); + ok(!notificationBox.getNotificationWithValue("blocked-plugins"), "Test 1a, Should not have displayed the blocked plugin notification"); + + var pluginInfo = getTestPlugin(); + ok(pluginInfo, "Should have a test plugin"); + + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 1a, plugin fallback type should be PLUGIN_PLAY_PREVIEW"); + ok(!objLoadingContent.activated, "Test 1a, Plugin should not be activated"); + + var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent"); + ok(overlay, "Test 1a, the overlay div is expected"); + + var iframe = overlay.getElementsByClassName("previewPluginContentFrame")[0]; + ok(iframe && iframe.localName == "iframe", "Test 1a, the overlay iframe is expected"); + var iframeHref = iframe.contentWindow.location.href; + ok(iframeHref == "about:", "Test 1a, the overlay about: content is expected"); + + var rect = iframe.getBoundingClientRect(); + ok(rect.width == 200, "Test 1a, Plugin with id=" + plugin.id + " overlay rect should have 200px width before being replaced by actual plugin"); + ok(rect.height == 200, "Test 1a, Plugin with id=" + plugin.id + " overlay rect should have 200px height before being replaced by actual plugin"); + + var e = overlay.ownerDocument.createEvent("CustomEvent"); + e.initCustomEvent("MozPlayPlugin", true, true, null); + overlay.dispatchEvent(e); + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test1b, "Test 1a, Waited too long for plugin to stop play preview"); +} + +// Tests that activating via MozPlayPlugin through the notification works (part 2/2) +function test1b() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 1b, Plugin should be activated"); + + is(gPlayPreviewPluginActualEvents, gPlayPreviewPluginExpectedEvents, + "There should be exactly one PluginPlayPreview event"); + + unregisterPlayPreview(); + + prepareTest(test2, gTestRoot + "plugin_test.html"); +} + +// Tests a page with a working plugin in it -- the mime type was just unregistered. +function test2() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 2, Plugin should be activated"); + + registerPlayPreview('application/x-unknown', 'about:'); + + prepareTest(test3, gTestRoot + "plugin_test.html"); +} + +// Tests a page with a working plugin in it -- diffent play preview type is reserved. +function test3() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 3, Plugin should be activated"); + + unregisterPlayPreview(); + + registerPlayPreview('application/x-test', 'about:'); + Services.prefs.setBoolPref("plugins.click_to_play", true); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + prepareTest(test4a, gTestRoot + "plugin_test.html", 1); +} + +// Test a fallback to the click-to-play +function test4a() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 4a, plugin fallback type should be PLUGIN_PLAY_PREVIEW"); + ok(!objLoadingContent.activated, "Test 4a, Plugin should not be activated"); + + var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent"); + ok(overlay, "Test 4a, the overlay div is expected"); + + var e = overlay.ownerDocument.createEvent("CustomEvent"); + e.initCustomEvent("MozPlayPlugin", true, true, true); + overlay.dispatchEvent(e); + var condition = function() objLoadingContent.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY; + waitForCondition(condition, test4b, "Test 4a, Waited too long for plugin to stop play preview"); +} + +function test4b() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.pluginFallbackType != Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 4b, plugin fallback type should not be PLUGIN_PLAY_PREVIEW"); + ok(!objLoadingContent.activated, "Test 4b, Plugin should not be activated"); + + prepareTest(test5a, gTestRoot + "plugin_test.html", 1); +} + +// Test a bypass of the click-to-play +function test5a() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 5a, plugin fallback type should be PLUGIN_PLAY_PREVIEW"); + ok(!objLoadingContent.activated, "Test 5a, Plugin should not be activated"); + + var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent"); + ok(overlay, "Test 5a, the overlay div is expected"); + + var e = overlay.ownerDocument.createEvent("CustomEvent"); + e.initCustomEvent("MozPlayPlugin", true, true, false); + overlay.dispatchEvent(e); + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test5b, "Test 5a, Waited too long for plugin to stop play preview"); +} + +function test5b() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 5b, Plugin should be activated"); + + finishTest(); +} + diff --git a/browser/base/content/test/browser_pluginplaypreview2.js b/browser/base/content/test/browser_pluginplaypreview2.js new file mode 100644 index 000000000..972e62a8d --- /dev/null +++ b/browser/base/content/test/browser_pluginplaypreview2.js @@ -0,0 +1,179 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 rootDir = getRootDirectory(gTestPath); +const gTestRoot = rootDir; + +var gTestBrowser = null; +var gNextTest = null; +var gNextTestSkip = 0; +var gPlayPreviewPluginActualEvents = 0; +var gPlayPreviewPluginExpectedEvents = 1; + +var gPlayPreviewRegistration = null; + +function registerPlayPreview(mimeType, targetUrl) { + var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + ph.registerPlayPreviewMimeType(mimeType, false, targetUrl); + + return (gPlayPreviewRegistration = { + unregister: function() { + ph.unregisterPlayPreviewMimeType(mimeType); + gPlayPreviewRegistration = null; + } + }); +} + +function unregisterPlayPreview() { + gPlayPreviewRegistration.unregister(); +} + +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); +Components.utils.import("resource://gre/modules/Services.jsm"); + + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + if (gPlayPreviewRegistration) + gPlayPreviewRegistration.unregister(); + Services.prefs.clearUserPref("plugins.click_to_play"); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + + var newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + gTestBrowser.addEventListener("PluginBindingAttached", handleBindingAttached, true, true); + + Services.prefs.setBoolPref("plugins.click_to_play", true); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + registerPlayPreview('application/x-test', 'about:'); + prepareTest(test1a, gTestRoot + "plugin_test.html", 1); +} + +function finishTest() { + gTestBrowser.removeEventListener("load", pageLoad, true); + gTestBrowser.removeEventListener("PluginBindingAttached", handleBindingAttached, true, true); + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} + +function handleBindingAttached(evt) { + if (evt.target instanceof Ci.nsIObjectLoadingContent && + evt.target.pluginFallbackType == Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW) + gPlayPreviewPluginActualEvents++; +} + +function pageLoad() { + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + + // iframe might triggers load event as well, making sure we skip some to let + // all iframes on the page be loaded as well + if (gNextTestSkip) { + gNextTestSkip--; + return; + } + executeSoon(gNextTest); +} + +function prepareTest(nextTest, url, skip) { + gNextTest = nextTest; + gNextTestSkip = skip; + gTestBrowser.contentWindow.location = url; +} + +// Tests a page with normal play preview registration (1/2) +function test1a() { + var notificationBox = gBrowser.getNotificationBox(gTestBrowser); + ok(!notificationBox.getNotificationWithValue("missing-plugins"), "Test 1a, Should not have displayed the missing plugin notification"); + ok(!notificationBox.getNotificationWithValue("blocked-plugins"), "Test 1a, Should not have displayed the blocked plugin notification"); + + var pluginInfo = getTestPlugin(); + ok(pluginInfo, "Should have a test plugin"); + + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + is(objLoadingContent.pluginFallbackType, Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 1a, plugin fallback type should be PLUGIN_PLAY_PREVIEW"); + ok(!objLoadingContent.activated, "Test 1a, Plugin should not be activated"); + + var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent"); + ok(overlay, "Test 1a, the overlay div is expected"); + + var iframe = overlay.getElementsByClassName("previewPluginContentFrame")[0]; + ok(iframe && iframe.localName == "iframe", "Test 1a, the overlay iframe is expected"); + var iframeHref = iframe.contentWindow.location.href; + ok(iframeHref == "about:", "Test 1a, the overlay about: content is expected"); + + var rect = iframe.getBoundingClientRect(); + ok(rect.width == 200, "Test 1a, Plugin with id=" + plugin.id + " overlay rect should have 200px width before being replaced by actual plugin"); + ok(rect.height == 200, "Test 1a, Plugin with id=" + plugin.id + " overlay rect should have 200px height before being replaced by actual plugin"); + + var e = overlay.ownerDocument.createEvent("CustomEvent"); + e.initCustomEvent("MozPlayPlugin", true, true, null); + overlay.dispatchEvent(e); + var condition = function() objLoadingContent.activated; + waitForCondition(condition, test1b, "Test 1a, Waited too long for plugin to stop play preview"); +} + +// Tests that activating via MozPlayPlugin through the notification works (part 2/2) +function test1b() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 1b, Plugin should be activated"); + + is(gPlayPreviewPluginActualEvents, gPlayPreviewPluginExpectedEvents, + "There should be exactly one PluginPlayPreview event"); + + unregisterPlayPreview(); + + prepareTest(test2, gTestRoot + "plugin_test.html"); +} + +// Tests a page with a working plugin in it -- the mime type was just unregistered. +function test2() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.pluginFallbackType != Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 2, plugin fallback type should not be PLUGIN_PLAY_PREVIEW"); + ok(!objLoadingContent.activated, "Test 2, Plugin should not be activated"); + + registerPlayPreview('application/x-unknown', 'about:'); + + prepareTest(test3, gTestRoot + "plugin_test.html"); +} + +// Tests a page with a working plugin in it -- diffent play preview type is reserved. +function test3() { + var doc = gTestBrowser.contentDocument; + var plugin = doc.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.pluginFallbackType != Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW, "Test 3, plugin fallback type should not be PLUGIN_PLAY_PREVIEW"); + ok(!objLoadingContent.activated, "Test 3, Plugin should not be activated"); + + unregisterPlayPreview(); + + registerPlayPreview('application/x-test', 'about:'); + Services.prefs.setBoolPref("plugins.click_to_play", false); + var plugin = getTestPlugin(); + plugin.enabledState = Ci.nsIPluginTag.STATE_ENABLED; + prepareTest(test4, gTestRoot + "plugin_test.html"); +} + +// Tests a page with a working plugin in it -- click-to-play is off +function test4() { + var plugin = gTestBrowser.contentDocument.getElementById("test"); + var objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent); + ok(objLoadingContent.activated, "Test 4, Plugin should be activated"); + + finishTest(); +} + diff --git a/browser/base/content/test/browser_plugins_added_dynamically.js b/browser/base/content/test/browser_plugins_added_dynamically.js new file mode 100644 index 000000000..457a72dcc --- /dev/null +++ b/browser/base/content/test/browser_plugins_added_dynamically.js @@ -0,0 +1,179 @@ +const gTestRoot = "http://mochi.test:8888/browser/browser/base/content/test/"; + +let gTestBrowser = null; +let gNextTest = null; +let gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost); + +Components.utils.import("resource://gre/modules/Services.jsm"); + +// Let's do the XPCNativeWrapper dance! +function addPlugin(browser, type) { + let contentWindow = XPCNativeWrapper.unwrap(browser.contentWindow); + contentWindow.addPlugin(type); +} + +function test() { + waitForExplicitFinish(); + registerCleanupFunction(function() { + clearAllPluginPermissions(); + Services.prefs.clearUserPref("plugins.click_to_play"); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED; + }); + Services.prefs.setBoolPref("plugins.click_to_play", true); + getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY; + + let newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + gTestBrowser = gBrowser.selectedBrowser; + gTestBrowser.addEventListener("load", pageLoad, true); + prepareTest(testActivateAddSameTypePart1, gTestRoot + "plugin_add_dynamically.html"); +} + +function finishTest() { + gTestBrowser.removeEventListener("load", pageLoad, true); + gBrowser.removeCurrentTab(); + window.focus(); + finish(); +} + +function pageLoad() { + // The plugin events are async dispatched and can come after the load event + // This just allows the events to fire before we then go on to test the states + executeSoon(gNextTest); +} + +function prepareTest(nextTest, url) { + gNextTest = nextTest; + gTestBrowser.contentWindow.location = url; +} + +// "Activate" of a given type -> plugins of that type dynamically added should +// automatically play. +function testActivateAddSameTypePart1() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(!popupNotification, "testActivateAddSameTypePart1: should not have a click-to-play notification"); + addPlugin(gTestBrowser); + let condition = function() PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + waitForCondition(condition, testActivateAddSameTypePart2, "testActivateAddSameTypePart1: waited too long for click-to-play-plugin notification"); +} + +function testActivateAddSameTypePart2() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "testActivateAddSameTypePart2: should have a click-to-play notification"); + + popupNotification.reshow(); + testActivateAddSameTypePart3(); +} + +function testActivateAddSameTypePart3() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + let centerAction = null; + for (let action of popupNotification.options.centerActions) { + if (action.pluginName == "Test") { + centerAction = action; + break; + } + } + ok(centerAction, "testActivateAddSameTypePart3: found center action for the Test plugin"); + + let centerItem = null; + for (let item of PopupNotifications.panel.firstChild.childNodes) { + if (item.action && item.action == centerAction) { + centerItem = item; + break; + } + } + ok(centerItem, "testActivateAddSameTypePart3: found center item for the Test plugin"); + + let plugin = gTestBrowser.contentDocument.getElementsByTagName("embed")[0]; + ok(!plugin.activated, "testActivateAddSameTypePart3: plugin should not be activated"); + + // Change the state and click the ok button to activate the Test plugin + centerItem.value = "allownow"; + PopupNotifications.panel.firstChild._primaryButton.click(); + + let condition = function() plugin.activated; + waitForCondition(condition, testActivateAddSameTypePart4, "testActivateAddSameTypePart3: waited too long for plugin to activate"); +} + +function testActivateAddSameTypePart4() { + let plugin = gTestBrowser.contentDocument.getElementsByTagName("embed")[0]; + ok(plugin.activated, "testActivateAddSameTypePart4: plugin should be activated"); + + addPlugin(gTestBrowser); + let condition = function() { + let embeds = gTestBrowser.contentDocument.getElementsByTagName("embed"); + let allActivated = true; + for (let embed of embeds) { + if (!embed.activated) + allActivated = false; + } + return allActivated && embeds.length == 2; + }; + waitForCondition(condition, testActivateAddSameTypePart5, "testActivateAddSameTypePart4: waited too long for second plugin to activate"); } + +function testActivateAddSameTypePart5() { + let embeds = gTestBrowser.contentDocument.getElementsByTagName("embed"); + for (let embed of embeds) { + ok(embed.activated, "testActivateAddSameTypePart5: all plugins should be activated"); + } + clearAllPluginPermissions(); + prepareTest(testActivateAddDifferentTypePart1, gTestRoot + "plugin_add_dynamically.html"); +} + +// "Activate" of a given type -> plugins of other types dynamically added +// should not automatically play. +function testActivateAddDifferentTypePart1() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(!popupNotification, "testActivateAddDifferentTypePart1: should not have a click-to-play notification"); + addPlugin(gTestBrowser); + let condition = function() PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + waitForCondition(condition, testActivateAddDifferentTypePart2, "testActivateAddDifferentTypePart1: waited too long for click-to-play-plugin notification"); +} + +function testActivateAddDifferentTypePart2() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + ok(popupNotification, "testActivateAddDifferentTypePart2: should have a click-to-play notification"); + + // we have to actually show the panel to get the bindings to instantiate + popupNotification.reshow(); + testActivateAddDifferentTypePart3(); +} + +function testActivateAddDifferentTypePart3() { + let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + is(popupNotification.options.centerActions.length, 1, "Should be one plugin action"); + + let plugin = gTestBrowser.contentDocument.getElementsByTagName("embed")[0]; + ok(!plugin.activated, "testActivateAddDifferentTypePart3: plugin should not be activated"); + + // "click" the button to activate the Test plugin + PopupNotifications.panel.firstChild._primaryButton.click(); + + let condition = function() plugin.activated; + waitForCondition(condition, testActivateAddDifferentTypePart4, "testActivateAddDifferentTypePart3: waited too long for plugin to activate"); +} + +function testActivateAddDifferentTypePart4() { + let plugin = gTestBrowser.contentDocument.getElementsByTagName("embed")[0]; + ok(plugin.activated, "testActivateAddDifferentTypePart4: plugin should be activated"); + + addPlugin(gTestBrowser); + let condition = function() PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser); + waitForCondition(condition, testActivateAddDifferentTypePart5, "testActivateAddDifferentTypePart5: waited too long for popup notification"); +} + +function testActivateAddDifferentTypePart5() { + ok(PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser), "testActivateAddDifferentTypePart5: should have popup notification"); + let embeds = gTestBrowser.contentDocument.getElementsByTagName("embed"); + for (let embed of embeds) { + if (embed.type == "application/x-test") + ok(embed.activated, "testActivateAddDifferentTypePart5: Test plugin should be activated"); + else if (embed.type == "application/x-second-test") + ok(!embed.activated, "testActivateAddDifferentTypePart5: Second Test plugin should not be activated"); + } + + finishTest(); +} diff --git a/browser/base/content/test/browser_popupNotification.js b/browser/base/content/test/browser_popupNotification.js new file mode 100644 index 000000000..1146b5d7c --- /dev/null +++ b/browser/base/content/test/browser_popupNotification.js @@ -0,0 +1,991 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + ok(PopupNotifications, "PopupNotifications object exists"); + ok(PopupNotifications.panel, "PopupNotifications panel exists"); + + registerCleanupFunction(cleanUp); + + runNextTest(); +} + +function cleanUp() { + for (var topic in gActiveObservers) + Services.obs.removeObserver(gActiveObservers[topic], topic); + for (var eventName in gActiveListeners) + PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false); + PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL; +} + +const PREF_SECURITY_DELAY_INITIAL = Services.prefs.getIntPref("security.notification_enable_delay"); + +var gActiveListeners = {}; +var gActiveObservers = {}; +var gShownState = {}; + +function goNext() { + if (++gTestIndex == tests.length) + executeSoon(finish); + else + executeSoon(runNextTest); +} + +function runNextTest() { + let nextTest = tests[gTestIndex]; + + function addObserver(topic) { + function observer() { + Services.obs.removeObserver(observer, "PopupNotifications-" + topic); + delete gActiveObservers["PopupNotifications-" + topic]; + + info("[Test #" + gTestIndex + "] observer for " + topic + " called"); + nextTest[topic](); + goNext(); + } + Services.obs.addObserver(observer, "PopupNotifications-" + topic, false); + gActiveObservers["PopupNotifications-" + topic] = observer; + } + + if (nextTest.backgroundShow) { + addObserver("backgroundShow"); + } else if (nextTest.updateNotShowing) { + addObserver("updateNotShowing"); + } else if (nextTest.onShown) { + doOnPopupEvent("popupshowing", function () { + info("[Test #" + gTestIndex + "] popup showing"); + }); + doOnPopupEvent("popupshown", function () { + gShownState[gTestIndex] = true; + info("[Test #" + gTestIndex + "] popup shown"); + nextTest.onShown(this); + }); + + // We allow multiple onHidden functions to be defined in an array. They're + // called in the order they appear. + let onHiddenArray = nextTest.onHidden instanceof Array ? + nextTest.onHidden : + [nextTest.onHidden]; + doOnPopupEvent("popuphidden", function () { + if (!gShownState[gTestIndex]) { + // This is expected to happen for test 9, so let's not treat it as a failure. + info("Popup from test " + gTestIndex + " was hidden before its popupshown fired"); + } + + let onHidden = onHiddenArray.shift(); + info("[Test #" + gTestIndex + "] popup hidden (" + onHiddenArray.length + " hides remaining)"); + executeSoon(function () { + onHidden.call(nextTest, this); + if (!onHiddenArray.length) + goNext(); + }.bind(this)); + }, onHiddenArray.length); + info("[Test #" + gTestIndex + "] added listeners; panel state: " + PopupNotifications.isPanelOpen); + } + + info("[Test #" + gTestIndex + "] running test"); + nextTest.run(); +} + +function doOnPopupEvent(eventName, callback, numExpected) { + gActiveListeners[eventName] = function (event) { + if (event.target != PopupNotifications.panel) + return; + if (typeof(numExpected) === "number") + numExpected--; + if (!numExpected) { + PopupNotifications.panel.removeEventListener(eventName, gActiveListeners[eventName], false); + delete gActiveListeners[eventName]; + } + + callback.call(PopupNotifications.panel); + } + PopupNotifications.panel.addEventListener(eventName, gActiveListeners[eventName], false); +} + +var gTestIndex = 0; +var gNewTab; + +function basicNotification() { + var self = this; + this.browser = gBrowser.selectedBrowser; + this.id = "test-notification-" + gTestIndex; + this.message = "This is popup notification " + this.id + " from test " + gTestIndex; + this.anchorID = null; + this.mainAction = { + label: "Main Action", + accessKey: "M", + callback: function () { + self.mainActionClicked = true; + } + }; + this.secondaryActions = [ + { + label: "Secondary Action", + accessKey: "S", + callback: function () { + self.secondaryActionClicked = true; + } + } + ]; + this.options = { + eventCallback: function (eventName) { + switch (eventName) { + case "dismissed": + self.dismissalCallbackTriggered = true; + break; + case "showing": + self.showingCallbackTriggered = true; + break; + case "shown": + self.shownCallbackTriggered = true; + break; + case "removed": + self.removedCallbackTriggered = true; + break; + } + } + }; + this.addOptions = function(options) { + for (let [name, value] in Iterator(options)) + self.options[name] = value; + } +} + +var wrongBrowserNotificationObject = new basicNotification(); +var wrongBrowserNotification; + +var tests = [ + { // Test #0 + run: function () { + this.notifyObj = new basicNotification(); + showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + triggerMainCommand(popup); + }, + onHidden: function (popup) { + ok(this.notifyObj.mainActionClicked, "mainAction was clicked"); + ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered"); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + { // Test #1 + run: function () { + this.notifyObj = new basicNotification(); + showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + triggerSecondaryCommand(popup, 0); + }, + onHidden: function (popup) { + ok(this.notifyObj.secondaryActionClicked, "secondaryAction was clicked"); + ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered"); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + { // Test #2 + run: function () { + this.notifyObj = new basicNotification(); + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // test opening a notification for a background browser + { // Test #3 + run: function () { + gNewTab = gBrowser.addTab("about:blank"); + isnot(gBrowser.selectedTab, gNewTab, "new tab isn't selected"); + wrongBrowserNotificationObject.browser = gBrowser.getBrowserForTab(gNewTab); + wrongBrowserNotification = showNotification(wrongBrowserNotificationObject); + }, + backgroundShow: function () { + is(PopupNotifications.isPanelOpen, false, "panel isn't open"); + ok(!wrongBrowserNotificationObject.mainActionClicked, "main action wasn't clicked"); + ok(!wrongBrowserNotificationObject.secondaryActionClicked, "secondary action wasn't clicked"); + ok(!wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback wasn't called"); + } + }, + // now select that browser and test to see that the notification appeared + { // Test #4 + run: function () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gNewTab; + }, + onShown: function (popup) { + checkPopup(popup, wrongBrowserNotificationObject); + is(PopupNotifications.isPanelOpen, true, "isPanelOpen getter doesn't lie"); + + // switch back to the old browser + gBrowser.selectedTab = this.oldSelectedTab; + }, + onHidden: function (popup) { + // actually remove the notification to prevent it from reappearing + ok(wrongBrowserNotificationObject.dismissalCallbackTriggered, "dismissal callback triggered due to tab switch"); + wrongBrowserNotification.remove(); + ok(wrongBrowserNotificationObject.removedCallbackTriggered, "removed callback triggered"); + wrongBrowserNotification = null; + } + }, + // test that the removed notification isn't shown on browser re-select + { // Test #5 + run: function () { + gBrowser.selectedTab = gNewTab; + }, + updateNotShowing: function () { + is(PopupNotifications.isPanelOpen, false, "panel isn't open"); + gBrowser.removeTab(gNewTab); + } + }, + // Test that two notifications with the same ID result in a single displayed + // notification. + { // Test #6 + run: function () { + this.notifyObj = new basicNotification(); + // Show the same notification twice + this.notification1 = showNotification(this.notifyObj); + this.notification2 = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + this.notification2.remove(); + }, + onHidden: function (popup) { + ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered"); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test that two notifications with different IDs are displayed + { // Test #7 + run: function () { + this.testNotif1 = new basicNotification(); + this.testNotif1.message += " 1"; + showNotification(this.testNotif1); + this.testNotif2 = new basicNotification(); + this.testNotif2.message += " 2"; + this.testNotif2.id += "-2"; + showNotification(this.testNotif2); + }, + onShown: function (popup) { + is(popup.childNodes.length, 2, "two notifications are shown"); + // Trigger the main command for the first notification, and the secondary + // for the second. Need to do mainCommand first since the secondaryCommand + // triggering is async. + triggerMainCommand(popup); + is(popup.childNodes.length, 1, "only one notification left"); + triggerSecondaryCommand(popup, 0); + }, + onHidden: function (popup) { + ok(this.testNotif1.mainActionClicked, "main action #1 was clicked"); + ok(!this.testNotif1.secondaryActionClicked, "secondary action #1 wasn't clicked"); + ok(!this.testNotif1.dismissalCallbackTriggered, "dismissal callback #1 wasn't called"); + + ok(!this.testNotif2.mainActionClicked, "main action #2 wasn't clicked"); + ok(this.testNotif2.secondaryActionClicked, "secondary action #2 was clicked"); + ok(!this.testNotif2.dismissalCallbackTriggered, "dismissal callback #2 wasn't called"); + } + }, + // Test notification without mainAction + { // Test #8 + run: function () { + this.notifyObj = new basicNotification(); + this.notifyObj.mainAction = null; + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function (popup) { + this.notification.remove(); + } + }, + // Test two notifications with different anchors + { // Test #9 + run: function () { + this.notifyObj = new basicNotification(); + this.firstNotification = showNotification(this.notifyObj); + this.notifyObj2 = new basicNotification(); + this.notifyObj2.id += "-2"; + this.notifyObj2.anchorID = "addons-notification-icon"; + // Second showNotification() overrides the first + this.secondNotification = showNotification(this.notifyObj2); + }, + onShown: function (popup) { + // This also checks that only one element is shown. + checkPopup(popup, this.notifyObj2); + is(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor shouldn't be visible"); + dismissNotification(popup); + }, + onHidden: [ + // The second showing triggers a popuphidden event that we should ignore. + function (popup) {}, + function (popup) { + // Remove the notifications + this.firstNotification.remove(); + this.secondNotification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered"); + } + ] + }, + // Test optional params + { // Test #10 + run: function () { + this.notifyObj = new basicNotification(); + this.notifyObj.secondaryActions = undefined; + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test that icons appear + { // Test #11 + run: function () { + this.notifyObj = new basicNotification(); + this.notifyObj.id = "geolocation"; + this.notifyObj.anchorID = "geo-notification-icon"; + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + isnot(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor should be visible"); + dismissNotification(popup); + }, + onHidden: function (popup) { + let icon = document.getElementById("geo-notification-icon"); + isnot(icon.boxObject.width, 0, + "geo anchor should be visible after dismissal"); + this.notification.remove(); + is(icon.boxObject.width, 0, + "geo anchor should not be visible after removal"); + } + }, + // Test that persistence allows the notification to persist across reloads + { // Test #12 + run: function () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + let self = this; + loadURI("http://example.com/", function() { + self.notifyObj = new basicNotification(); + self.notifyObj.addOptions({ + persistence: 2 + }); + self.notification = showNotification(self.notifyObj); + }); + }, + onShown: function (popup) { + this.complete = false; + + let self = this; + loadURI("http://example.org/", function() { + loadURI("http://example.com/", function() { + + // Next load will remove the notification + self.complete = true; + + loadURI("http://example.org/"); + }); + }); + }, + onHidden: function (popup) { + ok(this.complete, "Should only have hidden the notification after 3 page loads"); + ok(this.notifyObj.removedCallbackTriggered, "removal callback triggered"); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + } + }, + // Test that a timeout allows the notification to persist across reloads + { // Test #13 + run: function () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + let self = this; + loadURI("http://example.com/", function() { + self.notifyObj = new basicNotification(); + // Set a timeout of 10 minutes that should never be hit + self.notifyObj.addOptions({ + timeout: Date.now() + 600000 + }); + self.notification = showNotification(self.notifyObj); + }); + }, + onShown: function (popup) { + this.complete = false; + + let self = this; + loadURI("http://example.org/", function() { + loadURI("http://example.com/", function() { + + // Next load will hide the notification + self.notification.options.timeout = Date.now() - 1; + self.complete = true; + + loadURI("http://example.org/"); + }); + }); + }, + onHidden: function (popup) { + ok(this.complete, "Should only have hidden the notification after the timeout was passed"); + this.notification.remove(); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + } + }, + // Test that setting persistWhileVisible allows a visible notification to + // persist across location changes + { // Test #14 + run: function () { + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + let self = this; + loadURI("http://example.com/", function() { + self.notifyObj = new basicNotification(); + self.notifyObj.addOptions({ + persistWhileVisible: true + }); + self.notification = showNotification(self.notifyObj); + }); + }, + onShown: function (popup) { + this.complete = false; + + let self = this; + loadURI("http://example.org/", function() { + loadURI("http://example.com/", function() { + + // Notification should persist across location changes + self.complete = true; + dismissNotification(popup); + }); + }); + }, + onHidden: function (popup) { + ok(this.complete, "Should only have hidden the notification after it was dismissed"); + this.notification.remove(); + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.selectedTab = this.oldSelectedTab; + } + }, + // Test that nested icon nodes correctly activate popups + { // Test #15 + run: function() { + // Add a temporary box as the anchor with a button + this.box = document.createElement("box"); + PopupNotifications.iconBox.appendChild(this.box); + + let button = document.createElement("button"); + button.setAttribute("label", "Please click me!"); + this.box.appendChild(button); + + // The notification should open up on the box + this.notifyObj = new basicNotification(); + this.notifyObj.anchorID = this.box.id = "nested-box"; + this.notifyObj.addOptions({dismissed: true}); + this.notification = showNotification(this.notifyObj); + + EventUtils.synthesizeMouse(button, 1, 1, {}); + }, + onShown: function(popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function(popup) { + this.notification.remove(); + this.box.parentNode.removeChild(this.box); + } + }, + // Test that popupnotifications without popups have anchor icons shown + { // Test #16 + run: function() { + let notifyObj = new basicNotification(); + notifyObj.anchorID = "geo-notification-icon"; + notifyObj.addOptions({neverShow: true}); + showNotification(notifyObj); + }, + updateNotShowing: function() { + isnot(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor should be visible"); + } + }, + // Test notification "Not Now" menu item + { // Test #17 + run: function () { + this.notifyObj = new basicNotification(); + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + triggerSecondaryCommand(popup, 1); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test notification close button + { // Test #18 + run: function () { + this.notifyObj = new basicNotification(); + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + let notification = popup.childNodes[0]; + EventUtils.synthesizeMouseAtCenter(notification.closebutton, {}); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test notification when chrome is hidden + { // Test #19 + run: function () { + window.locationbar.visible = false; + this.notifyObj = new basicNotification(); + this.notification = showNotification(this.notifyObj); + window.locationbar.visible = true; + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + is(popup.anchorNode.className, "tabbrowser-tab", "notification anchored to tab"); + dismissNotification(popup); + }, + onHidden: function (popup) { + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback triggered"); + this.notification.remove(); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test notification is removed when dismissed if removeOnDismissal is true + { // Test #20 + run: function () { + this.notifyObj = new basicNotification(); + this.notifyObj.addOptions({ + removeOnDismissal: true + }); + this.notification = showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + dismissNotification(popup); + }, + onHidden: function (popup) { + ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback wasn't triggered"); + ok(this.notifyObj.removedCallbackTriggered, "removed callback triggered"); + } + }, + // Test multiple notification icons are shown + { // Test #21 + run: function () { + this.notifyObj1 = new basicNotification(); + this.notifyObj1.id += "_1"; + this.notifyObj1.anchorID = "default-notification-icon"; + this.notification1 = showNotification(this.notifyObj1); + + this.notifyObj2 = new basicNotification(); + this.notifyObj2.id += "_2"; + this.notifyObj2.anchorID = "geo-notification-icon"; + this.notification2 = showNotification(this.notifyObj2); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj2); + + // check notifyObj1 anchor icon is showing + isnot(document.getElementById("default-notification-icon").boxObject.width, 0, + "default anchor should be visible"); + // check notifyObj2 anchor icon is showing + isnot(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor should be visible"); + + dismissNotification(popup); + }, + onHidden: [ + function (popup) { + }, + function (popup) { + this.notification1.remove(); + ok(this.notifyObj1.removedCallbackTriggered, "removed callback triggered"); + + this.notification2.remove(); + ok(this.notifyObj2.removedCallbackTriggered, "removed callback triggered"); + } + ] + }, + // Test that multiple notification icons are removed when switching tabs + { // Test #22 + run: function () { + // show the notification on old tab. + this.notifyObjOld = new basicNotification(); + this.notifyObjOld.anchorID = "default-notification-icon"; + this.notificationOld = showNotification(this.notifyObjOld); + + // switch tab + this.oldSelectedTab = gBrowser.selectedTab; + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + + // show the notification on new tab. + this.notifyObjNew = new basicNotification(); + this.notifyObjNew.anchorID = "geo-notification-icon"; + this.notificationNew = showNotification(this.notifyObjNew); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObjNew); + + // check notifyObjOld anchor icon is removed + is(document.getElementById("default-notification-icon").boxObject.width, 0, + "default anchor shouldn't be visible"); + // check notifyObjNew anchor icon is showing + isnot(document.getElementById("geo-notification-icon").boxObject.width, 0, + "geo anchor should be visible"); + + dismissNotification(popup); + }, + onHidden: [ + function (popup) { + }, + function (popup) { + this.notificationNew.remove(); + gBrowser.removeTab(gBrowser.selectedTab); + + gBrowser.selectedTab = this.oldSelectedTab; + this.notificationOld.remove(); + } + ] + }, + { // Test #23 - test security delay - too early + run: function () { + // Set the security delay to 100s + PopupNotifications.buttonDelay = 100000; + + this.notifyObj = new basicNotification(); + showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + triggerMainCommand(popup); + + // Wait to see if the main command worked + executeSoon(function delayedDismissal() { + dismissNotification(popup); + }); + + }, + onHidden: function (popup) { + ok(!this.notifyObj.mainActionClicked, "mainAction was not clicked because it was too soon"); + ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered"); + } + }, + { // Test #24 - test security delay - after delay + run: function () { + // Set the security delay to 10ms + PopupNotifications.buttonDelay = 10; + + this.notifyObj = new basicNotification(); + showNotification(this.notifyObj); + }, + onShown: function (popup) { + checkPopup(popup, this.notifyObj); + + // Wait until after the delay to trigger the main action + setTimeout(function delayedDismissal() { + triggerMainCommand(popup); + }, 500); + + }, + onHidden: function (popup) { + ok(this.notifyObj.mainActionClicked, "mainAction was clicked after the delay"); + ok(!this.notifyObj.dismissalCallbackTriggered, "dismissal callback was not triggered"); + PopupNotifications.buttonDelay = PREF_SECURITY_DELAY_INITIAL; + } + }, + { // Test #25 - reload removes notification + run: function () { + loadURI("http://example.com/", function() { + let notifyObj = new basicNotification(); + notifyObj.options.eventCallback = function (eventName) { + if (eventName == "removed") { + ok(true, "Notification removed in background tab after reloading"); + executeSoon(function () { + goNext(); + }); + } + }; + showNotification(notifyObj); + executeSoon(function () { + gBrowser.selectedBrowser.reload(); + }); + }); + } + }, + { // Test #26 - location change in background tab removes notification + run: function () { + let oldSelectedTab = gBrowser.selectedTab; + let newTab = gBrowser.addTab("about:blank"); + gBrowser.selectedTab = newTab; + + loadURI("http://example.com/", function() { + gBrowser.selectedTab = oldSelectedTab; + let browser = gBrowser.getBrowserForTab(newTab); + + let notifyObj = new basicNotification(); + notifyObj.browser = browser; + notifyObj.options.eventCallback = function (eventName) { + if (eventName == "removed") { + ok(true, "Notification removed in background tab after reloading"); + executeSoon(function () { + gBrowser.removeTab(newTab); + goNext(); + }); + } + }; + showNotification(notifyObj); + executeSoon(function () { + browser.reload(); + }); + }); + } + }, + { // Test #27 - Popup notification anchor shouldn't disappear when a notification with the same ID is re-added in a background tab + run: function () { + loadURI("http://example.com/", function () { + let originalTab = gBrowser.selectedTab; + let bgTab = gBrowser.addTab("about:blank"); + gBrowser.selectedTab = bgTab; + loadURI("http://example.com/", function () { + let anchor = document.createElement("box"); + anchor.id = "test26-anchor"; + anchor.className = "notification-anchor-icon"; + PopupNotifications.iconBox.appendChild(anchor); + + gBrowser.selectedTab = originalTab; + + let fgNotifyObj = new basicNotification(); + fgNotifyObj.anchorID = anchor.id; + fgNotifyObj.options.dismissed = true; + let fgNotification = showNotification(fgNotifyObj); + + let bgNotifyObj = new basicNotification(); + bgNotifyObj.anchorID = anchor.id; + bgNotifyObj.browser = gBrowser.getBrowserForTab(bgTab); + // show the notification in the background tab ... + let bgNotification = showNotification(bgNotifyObj); + // ... and re-show it + bgNotification = showNotification(bgNotifyObj); + + ok(fgNotification.id, "notification has id"); + is(fgNotification.id, bgNotification.id, "notification ids are the same"); + is(anchor.getAttribute("showing"), "true", "anchor still showing"); + + fgNotification.remove(); + gBrowser.removeTab(bgTab); + goNext(); + }); + }); + } + }, + { // Test #28 - location change in embedded frame removes notification + run: function () { + loadURI("data:text/html,<iframe id='iframe' src='http://example.com/'>", function () { + let notifyObj = new basicNotification(); + notifyObj.options.eventCallback = function (eventName) { + if (eventName == "removed") { + ok(true, "Notification removed in background tab after reloading"); + executeSoon(goNext); + } + }; + showNotification(notifyObj); + executeSoon(function () { + content.document.getElementById("iframe") + .setAttribute("src", "http://example.org/"); + }); + }); + } + }, + { // Test #29 - Existing popup notification shouldn't disappear when adding a dismissed notification + run: function () { + this.notifyObj1 = new basicNotification(); + this.notifyObj1.id += "_1"; + this.notifyObj1.anchorID = "default-notification-icon"; + this.notification1 = showNotification(this.notifyObj1); + }, + onShown: function (popup) { + // Now show a dismissed notification, and check that it doesn't clobber + // the showing one. + this.notifyObj2 = new basicNotification(); + this.notifyObj2.id += "_2"; + this.notifyObj2.anchorID = "geo-notification-icon"; + this.notifyObj2.options.dismissed = true; + this.notification2 = showNotification(this.notifyObj2); + + checkPopup(popup, this.notifyObj1); + + // check that both anchor icons are showing + is(document.getElementById("default-notification-icon").getAttribute("showing"), "true", + "notification1 anchor should be visible"); + is(document.getElementById("geo-notification-icon").getAttribute("showing"), "true", + "notification2 anchor should be visible"); + + dismissNotification(popup); + }, + onHidden: function(popup) { + this.notification1.remove(); + this.notification2.remove(); + } + }, + { // Test #30 - Showing should be able to modify the popup data + run: function() { + this.notifyObj = new basicNotification(); + var normalCallback = this.notifyObj.options.eventCallback; + this.notifyObj.options.eventCallback = function (eventName) { + if (eventName == "showing") { + this.mainAction.label = "Alternate Label"; + } + normalCallback.call(this, eventName); + }; + showNotification(this.notifyObj); + }, + onShown: function(popup) { + // checkPopup checks for the matching label. Note that this assumes that + // this.notifyObj.mainAction is the same as notification.mainAction, + // which could be a problem if we ever decided to deep-copy. + checkPopup(popup, this.notifyObj); + triggerMainCommand(popup); + }, + onHidden: function() { } + } +]; + +function showNotification(notifyObj) { + return PopupNotifications.show(notifyObj.browser, + notifyObj.id, + notifyObj.message, + notifyObj.anchorID, + notifyObj.mainAction, + notifyObj.secondaryActions, + notifyObj.options); +} + +function checkPopup(popup, notificationObj) { + info("[Test #" + gTestIndex + "] checking popup"); + + ok(notificationObj.showingCallbackTriggered, "showing callback was triggered"); + ok(notificationObj.shownCallbackTriggered, "shown callback was triggered"); + + let notifications = popup.childNodes; + is(notifications.length, 1, "one notification displayed"); + let notification = notifications[0]; + if (!notification) + return; + let icon = document.getAnonymousElementByAttribute(notification, "class", "popup-notification-icon"); + if (notificationObj.id == "geolocation") { + isnot(icon.boxObject.width, 0, "icon for geo displayed"); + is(popup.anchorNode.className, "notification-anchor-icon", "notification anchored to icon"); + } + is(notification.getAttribute("label"), notificationObj.message, "message matches"); + is(notification.id, notificationObj.id + "-notification", "id matches"); + if (notificationObj.mainAction) { + is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches"); + is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches"); + } + let actualSecondaryActions = Array.filter(notification.childNodes, + function (child) child.nodeName == "menuitem"); + let secondaryActions = notificationObj.secondaryActions || []; + let actualSecondaryActionsCount = actualSecondaryActions.length; + if (secondaryActions.length) { + is(notification.lastChild.tagName, "menuseparator", "menuseparator exists"); + } + is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions"); + secondaryActions.forEach(function (a, i) { + is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches"); + is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches"); + }); +} + +function triggerMainCommand(popup) { + info("[Test #" + gTestIndex + "] triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + let notification = notifications[0]; + + // 20, 10 so that the inner button is hit + EventUtils.synthesizeMouse(notification.button, 20, 10, {}); +} + +function triggerSecondaryCommand(popup, index) { + info("[Test #" + gTestIndex + "] triggering secondary command"); + let notifications = popup.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + let notification = notifications[0]; + + // Cancel the arrow panel slide-in transition (bug 767133) such that + // it won't interfere with us interacting with the dropdown. + document.getAnonymousNodes(popup)[0].style.transition = "none"; + + notification.button.focus(); + + popup.addEventListener("popupshown", function () { + popup.removeEventListener("popupshown", arguments.callee, false); + + // Press down until the desired command is selected + for (let i = 0; i <= index; i++) + EventUtils.synthesizeKey("VK_DOWN", {}); + + // Activate + EventUtils.synthesizeKey("VK_ENTER", {}); + }, false); + + // One down event to open the popup + EventUtils.synthesizeKey("VK_DOWN", { altKey: !navigator.platform.contains("Mac") }); +} + +function loadURI(uri, callback) { + if (callback) { + gBrowser.addEventListener("load", function() { + // Ignore the about:blank load + if (gBrowser.currentURI.spec == "about:blank") + return; + + gBrowser.removeEventListener("load", arguments.callee, true); + + callback(); + }, true); + } + gBrowser.loadURI(uri); +} + +function dismissNotification(popup) { + info("[Test #" + gTestIndex + "] dismissing notification"); + executeSoon(function () { + EventUtils.synthesizeKey("VK_ESCAPE", {}); + }); +} diff --git a/browser/base/content/test/browser_popupUI.js b/browser/base/content/test/browser_popupUI.js new file mode 100644 index 000000000..fd722886d --- /dev/null +++ b/browser/base/content/test/browser_popupUI.js @@ -0,0 +1,57 @@ +function test() { + waitForExplicitFinish(); + gPrefService.setBoolPref("dom.disable_open_during_load", false); + + var browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function () { + browser.removeEventListener("load", arguments.callee, true); + + if (gPrefService.prefHasUserValue("dom.disable_open_during_load")) + gPrefService.clearUserPref("dom.disable_open_during_load"); + + findPopup(); + }, true); + + content.location = + "data:text/html,<html><script>popup=open('about:blank','','width=300,height=200')</script>"; +} + +function findPopup() { + var enumerator = Services.wm.getEnumerator("navigator:browser"); + + while (enumerator.hasMoreElements()) { + let win = enumerator.getNext(); + if (win.content.wrappedJSObject == content.wrappedJSObject.popup) { + testPopupUI(win); + return; + } + } + + throw "couldn't find the popup"; +} + +function testPopupUI(win) { + var doc = win.document; + + ok(win.gURLBar, "location bar exists in the popup"); + isnot(win.gURLBar.clientWidth, 0, "location bar is visible in the popup"); + ok(win.gURLBar.readOnly, "location bar is read-only in the popup"); + isnot(doc.getElementById("Browser:OpenLocation").getAttribute("disabled"), "true", + "'open location' command is not disabled in the popup"); + + let historyButton = doc.getAnonymousElementByAttribute(win.gURLBar, "anonid", + "historydropmarker"); + is(historyButton.clientWidth, 0, "history dropdown button is hidden in the popup"); + + EventUtils.synthesizeKey("t", { accelKey: true }, win); + is(win.gBrowser.browsers.length, 1, "Accel+T doesn't open a new tab in the popup"); + + EventUtils.synthesizeKey("w", { accelKey: true }, win); + ok(win.closed, "Accel+W closes the popup"); + + if (!win.closed) + win.close(); + gBrowser.addTab(); + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/browser/base/content/test/browser_private_browsing_window.js b/browser/base/content/test/browser_private_browsing_window.js new file mode 100644 index 000000000..607a34060 --- /dev/null +++ b/browser/base/content/test/browser_private_browsing_window.js @@ -0,0 +1,65 @@ +// Make sure that we can open private browsing windows + +function test() { + waitForExplicitFinish(); + var nonPrivateWin = OpenBrowserWindow(); + ok(!PrivateBrowsingUtils.isWindowPrivate(nonPrivateWin), "OpenBrowserWindow() should open a normal window"); + nonPrivateWin.close(); + + var privateWin = OpenBrowserWindow({private: true}); + ok(PrivateBrowsingUtils.isWindowPrivate(privateWin), "OpenBrowserWindow({private: true}) should open a private window"); + + nonPrivateWin = OpenBrowserWindow({private: false}); + ok(!PrivateBrowsingUtils.isWindowPrivate(nonPrivateWin), "OpenBrowserWindow({private: false}) should open a normal window"); + nonPrivateWin.close(); + + whenDelayedStartupFinished(privateWin, function() { + nonPrivateWin = privateWin.OpenBrowserWindow({private: false}); + ok(!PrivateBrowsingUtils.isWindowPrivate(nonPrivateWin), "privateWin.OpenBrowserWindow({private: false}) should open a normal window"); + + nonPrivateWin.close(); + + [ + { normal: "menu_newNavigator", private: "menu_newPrivateWindow", accesskey: true }, + { normal: "appmenu_newNavigator", private: "appmenu_newPrivateWindow", accesskey: false }, + ].forEach(function(menu) { + let newWindow = privateWin.document.getElementById(menu.normal); + let newPrivateWindow = privateWin.document.getElementById(menu.private); + if (newWindow && newPrivateWindow) { + ok(!newPrivateWindow.hidden, "New Private Window menu item should be hidden"); + isnot(newWindow.label, newPrivateWindow.label, "New Window's label shouldn't be overwritten"); + if (menu.accesskey) { + isnot(newWindow.accessKey, newPrivateWindow.accessKey, "New Window's accessKey shouldn't be overwritten"); + } + isnot(newWindow.command, newPrivateWindow.command, "New Window's command shouldn't be overwritten"); + } + }); + + privateWin.close(); + + Services.prefs.setBoolPref("browser.privatebrowsing.autostart", true); + privateWin = OpenBrowserWindow({private: true}); + whenDelayedStartupFinished(privateWin, function() { + [ + { normal: "menu_newNavigator", private: "menu_newPrivateWindow", accessKey: true }, + { normal: "appmenu_newNavigator", private: "appmenu_newPrivateWindow", accessKey: false }, + ].forEach(function(menu) { + let newWindow = privateWin.document.getElementById(menu.normal); + let newPrivateWindow = privateWin.document.getElementById(menu.private); + if (newWindow && newPrivateWindow) { + ok(newPrivateWindow.hidden, "New Private Window menu item should be hidden"); + is(newWindow.label, newPrivateWindow.label, "New Window's label should be overwritten"); + if (menu.accesskey) { + is(newWindow.accessKey, newPrivateWindow.accessKey, "New Window's accessKey should be overwritten"); + } + is(newWindow.command, newPrivateWindow.command, "New Window's command should be overwritten"); + } + }); + + privateWin.close(); + Services.prefs.clearUserPref("browser.privatebrowsing.autostart"); + finish(); + }); + }); +} + diff --git a/browser/base/content/test/browser_private_no_prompt.js b/browser/base/content/test/browser_private_no_prompt.js new file mode 100644 index 000000000..17ab1b437 --- /dev/null +++ b/browser/base/content/test/browser_private_no_prompt.js @@ -0,0 +1,13 @@ +function test() { + waitForExplicitFinish(); + var privateWin = OpenBrowserWindow({private: true}); + privateWin.addEventListener("load", function onload() { + privateWin.removeEventListener("load", onload, false); + ok(true, "Load listener called"); + + privateWin.BrowserOpenTab(); + privateWin.BrowserTryToCloseWindow(); + ok(true, "didn't prompt"); + finish(); + }, false); +}
\ No newline at end of file diff --git a/browser/base/content/test/browser_relatedTabs.js b/browser/base/content/test/browser_relatedTabs.js new file mode 100644 index 000000000..f59e0bbbb --- /dev/null +++ b/browser/base/content/test/browser_relatedTabs.js @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + is(gBrowser.tabs.length, 1, "one tab is open initially"); + + // Add several new tabs in sequence, interrupted by selecting a + // different tab, moving a tab around and closing a tab, + // returning a list of opened tabs for verifying the expected order. + // The new tab behaviour is documented in bug 465673 + let tabs = []; + function addTab(aURL, aReferrer) { + tabs.push(gBrowser.addTab(aURL, {referrerURI: aReferrer})); + } + + addTab("http://mochi.test:8888/#0"); + gBrowser.selectedTab = tabs[0]; + addTab("http://mochi.test:8888/#1"); + addTab("http://mochi.test:8888/#2", gBrowser.currentURI); + addTab("http://mochi.test:8888/#3", gBrowser.currentURI); + gBrowser.selectedTab = tabs[tabs.length - 1]; + gBrowser.selectedTab = tabs[0]; + addTab("http://mochi.test:8888/#4", gBrowser.currentURI); + gBrowser.selectedTab = tabs[3]; + addTab("http://mochi.test:8888/#5", gBrowser.currentURI); + gBrowser.removeTab(tabs.pop()); + addTab("about:blank", gBrowser.currentURI); + gBrowser.moveTabTo(gBrowser.selectedTab, 1); + addTab("http://mochi.test:8888/#6", gBrowser.currentURI); + addTab(); + addTab("http://mochi.test:8888/#7"); + + function testPosition(tabNum, expectedPosition, msg) { + is(Array.indexOf(gBrowser.tabs, tabs[tabNum]), expectedPosition, msg); + } + + testPosition(0, 3, "tab without referrer was opened to the far right"); + testPosition(1, 7, "tab without referrer was opened to the far right"); + testPosition(2, 5, "tab with referrer opened immediately to the right"); + testPosition(3, 1, "next tab with referrer opened further to the right"); + testPosition(4, 4, "tab selection changed, tab opens immediately to the right"); + testPosition(5, 6, "blank tab with referrer opens to the right of 3rd original tab where removed tab was"); + testPosition(6, 2, "tab has moved, new tab opens immediately to the right"); + testPosition(7, 8, "blank tab without referrer opens at the end"); + testPosition(8, 9, "tab without referrer opens at the end"); + + tabs.forEach(gBrowser.removeTab, gBrowser); +} diff --git a/browser/base/content/test/browser_removeTabsToTheEnd.js b/browser/base/content/test/browser_removeTabsToTheEnd.js new file mode 100644 index 000000000..856f25aac --- /dev/null +++ b/browser/base/content/test/browser_removeTabsToTheEnd.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + // Add two new tabs after the original tab. Pin the first one. + let originalTab = gBrowser.selectedTab; + let newTab1 = gBrowser.addTab(); + let newTab2 = gBrowser.addTab(); + gBrowser.pinTab(newTab1); + + // Check that there is only one closable tab from originalTab to the end + is(gBrowser.getTabsToTheEndFrom(originalTab).length, 1, + "One unpinned tab to the right"); + + // Remove tabs to the end + gBrowser.removeTabsToTheEndFrom(originalTab); + is(gBrowser.tabs.length, 2, "Length is 2"); + is(gBrowser.tabs[1], originalTab, "Starting tab is not removed"); + is(gBrowser.tabs[0], newTab1, "Pinned tab is not removed"); + + // Remove pinned tab + gBrowser.removeTab(newTab1); +} diff --git a/browser/base/content/test/browser_sanitize-download-history.js b/browser/base/content/test/browser_sanitize-download-history.js new file mode 100644 index 000000000..186b02167 --- /dev/null +++ b/browser/base/content/test/browser_sanitize-download-history.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +function test() +{ + ////////////////////////////////////////////////////////////////////////////// + //// Tests (defined locally for scope's sake) + + function test_checkedAndDisabledAtStart(aWin) + { + let doc = aWin.document; + let downloads = doc.getElementById("downloads-checkbox"); + let history = doc.getElementById("history-checkbox"); + + ok(history.checked, "history checkbox is checked"); + ok(downloads.disabled, "downloads checkbox is disabled"); + ok(downloads.checked, "downloads checkbox is checked"); + } + + function test_checkedAndDisabledOnHistoryToggle(aWin) + { + let doc = aWin.document; + let downloads = doc.getElementById("downloads-checkbox"); + let history = doc.getElementById("history-checkbox"); + + EventUtils.synthesizeMouse(history, 0, 0, {}, aWin); + ok(!history.checked, "history checkbox is not checked"); + ok(downloads.disabled, "downloads checkbox is disabled"); + ok(downloads.checked, "downloads checkbox is checked"); + } + + function test_checkedAfterAddingDownload(aWin) + { + let doc = aWin.document; + let downloads = doc.getElementById("downloads-checkbox"); + let history = doc.getElementById("history-checkbox"); + + // Add download to DB + let file = Cc["@mozilla.org/file/directory_service;1"]. + getService(Ci.nsIProperties).get("TmpD", Ci.nsIFile); + file.append("sanitize-dm-test.file"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666); + let testPath = Services.io.newFileURI(file).spec; + let data = { + name: "381603.patch", + source: "https://bugzilla.mozilla.org/attachment.cgi?id=266520", + target: testPath, + startTime: 1180493839859230, + endTime: 1180493839859239, + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "a1bcD23eF4g5" + }; + let db = Cc["@mozilla.org/download-manager;1"]. + getService(Ci.nsIDownloadManager).DBConnection; + let stmt = db.createStatement( + "INSERT INTO moz_downloads (name, source, target, startTime, endTime, " + + "state, currBytes, maxBytes, preferredAction, autoResume, guid) " + + "VALUES (:name, :source, :target, :startTime, :endTime, :state, " + + ":currBytes, :maxBytes, :preferredAction, :autoResume, :guid)"); + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.finalize(); + } + + // Toggle history to get everything to update + EventUtils.synthesizeMouse(history, 0, 0, {}, aWin); + EventUtils.synthesizeMouse(history, 0, 0, {}, aWin); + + ok(!history.checked, "history checkbox is not checked"); + ok(!downloads.disabled, "downloads checkbox is not disabled"); + ok(downloads.checked, "downloads checkbox is checked"); + } + + function test_checkedAndDisabledWithHistoryChecked(aWin) + { + let doc = aWin.document; + let downloads = doc.getElementById("downloads-checkbox"); + let history = doc.getElementById("history-checkbox"); + + EventUtils.synthesizeMouse(history, 0, 0, {}, aWin); + ok(history.checked, "history checkbox is checked"); + ok(downloads.disabled, "downloads checkbox is disabled"); + ok(downloads.checked, "downloads checkbox is checked"); + } + + let tests = [ + test_checkedAndDisabledAtStart, + test_checkedAndDisabledOnHistoryToggle, + test_checkedAfterAddingDownload, + test_checkedAndDisabledWithHistoryChecked, + ]; + + ////////////////////////////////////////////////////////////////////////////// + //// Run the tests + + let dm = Cc["@mozilla.org/download-manager;1"]. + getService(Ci.nsIDownloadManager); + let db = dm.DBConnection; + + // Empty any old downloads + db.executeSimpleSQL("DELETE FROM moz_downloads"); + + // Close the UI if necessary + let win = Services.ww.getWindowByName("Sanitize", null); + if (win && (win instanceof Ci.nsIDOMWindow)) + win.close(); + + // Start the test when the sanitize window loads + Services.ww.registerNotification(function (aSubject, aTopic, aData) { + Services.ww.unregisterNotification(arguments.callee); + aSubject.QueryInterface(Ci.nsIDOMEventTarget) + .addEventListener("DOMContentLoaded", doTest, false); + }); + + // Let the methods that run onload finish before we test + let doTest = function() setTimeout(function() { + let win = Services.ww.getWindowByName("Sanitize", null) + .QueryInterface(Ci.nsIDOMWindow); + + for (let i = 0; i < tests.length; i++) + tests[i](win); + + win.close(); + finish(); + }, 0); + + // Show the UI + Services.ww.openWindow(window, + "chrome://browser/content/sanitize.xul", + "Sanitize", + "chrome,titlebar,centerscreen", + null); + + waitForExplicitFinish(); +} diff --git a/browser/base/content/test/browser_sanitize-passwordDisabledHosts.js b/browser/base/content/test/browser_sanitize-passwordDisabledHosts.js new file mode 100644 index 000000000..06cf2467e --- /dev/null +++ b/browser/base/content/test/browser_sanitize-passwordDisabledHosts.js @@ -0,0 +1,41 @@ +// Bug 474792 - Clear "Never remember passwords for this site" when +// clearing site-specific settings in Clear Recent History dialog + +let tempScope = {}; +Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tempScope); +let Sanitizer = tempScope.Sanitizer; + +function test() { + + var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); + + // Add a disabled host + pwmgr.setLoginSavingEnabled("http://example.com", false); + + // Sanity check + is(pwmgr.getLoginSavingEnabled("http://example.com"), false, + "example.com should be disabled for password saving since we haven't cleared that yet."); + + // Set up the sanitizer to just clear siteSettings + let s = new Sanitizer(); + s.ignoreTimespan = false; + s.prefDomain = "privacy.cpd."; + var itemPrefs = gPrefService.getBranch(s.prefDomain); + itemPrefs.setBoolPref("history", false); + itemPrefs.setBoolPref("downloads", false); + itemPrefs.setBoolPref("cache", false); + itemPrefs.setBoolPref("cookies", false); + itemPrefs.setBoolPref("formdata", false); + itemPrefs.setBoolPref("offlineApps", false); + itemPrefs.setBoolPref("passwords", false); + itemPrefs.setBoolPref("sessions", false); + itemPrefs.setBoolPref("siteSettings", true); + + // Clear it + s.sanitize(); + + // Make sure it's gone + is(pwmgr.getLoginSavingEnabled("http://example.com"), true, + "example.com should be enabled for password saving again now that we've cleared."); +} diff --git a/browser/base/content/test/browser_sanitize-sitepermissions.js b/browser/base/content/test/browser_sanitize-sitepermissions.js new file mode 100644 index 000000000..13c0e2068 --- /dev/null +++ b/browser/base/content/test/browser_sanitize-sitepermissions.js @@ -0,0 +1,37 @@ +// Bug 380852 - Delete permission manager entries in Clear Recent History + +let tempScope = {}; +Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tempScope); +let Sanitizer = tempScope.Sanitizer; + +function test() { + + // Add a permission entry + var pm = Services.perms; + pm.add(makeURI("http://example.com"), "testing", pm.ALLOW_ACTION); + + // Sanity check + ok(pm.enumerator.hasMoreElements(), "Permission manager should have elements, since we just added one"); + + // Set up the sanitizer to just clear siteSettings + let s = new Sanitizer(); + s.ignoreTimespan = false; + s.prefDomain = "privacy.cpd."; + var itemPrefs = gPrefService.getBranch(s.prefDomain); + itemPrefs.setBoolPref("history", false); + itemPrefs.setBoolPref("downloads", false); + itemPrefs.setBoolPref("cache", false); + itemPrefs.setBoolPref("cookies", false); + itemPrefs.setBoolPref("formdata", false); + itemPrefs.setBoolPref("offlineApps", false); + itemPrefs.setBoolPref("passwords", false); + itemPrefs.setBoolPref("sessions", false); + itemPrefs.setBoolPref("siteSettings", true); + + // Clear it + s.sanitize(); + + // Make sure it's gone + ok(!pm.enumerator.hasMoreElements(), "Permission manager shouldn't have entries after Sanitizing"); +} diff --git a/browser/base/content/test/browser_sanitize-timespans.js b/browser/base/content/test/browser_sanitize-timespans.js new file mode 100644 index 000000000..6c6fb059b --- /dev/null +++ b/browser/base/content/test/browser_sanitize-timespans.js @@ -0,0 +1,810 @@ +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// Bug 453440 - Test the timespan-based logic of the sanitizer code +var now_uSec = Date.now() * 1000; + +const dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); + +const kUsecPerMin = 60 * 1000000; + +let tempScope = {}; +Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tempScope); +let Sanitizer = tempScope.Sanitizer; + +let FormHistory = (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory; + +function promiseFormHistoryRemoved() { + let deferred = Promise.defer(); + Services.obs.addObserver(function onfh() { + Services.obs.removeObserver(onfh, "satchel-storage-changed", false); + deferred.resolve(); + }, "satchel-storage-changed", false); + return deferred.promise; +} + +function test() { + waitForExplicitFinish(); + + Task.spawn(function() { + setupDownloads(); + yield setupFormHistory(); + yield setupHistory(); + yield onHistoryReady(); + }).then(finish); +} + +function countEntries(name, message, check) { + let deferred = Promise.defer(); + + var obj = {}; + if (name !== null) + obj.fieldname = name; + + let count; + FormHistory.count(obj, { handleResult: function (result) count = result, + handleError: function (error) { + do_throw("Error occurred searching form history: " + error); + deferred.reject(error) + }, + handleCompletion: function (reason) { + if (!reason) { + check(count, message); + deferred.resolve(); + } + }, + }); + + return deferred.promise; +} + +function onHistoryReady() { + var hoursSinceMidnight = new Date().getHours(); + var minutesSinceMidnight = hoursSinceMidnight * 60 + new Date().getMinutes(); + + // Should test cookies here, but nsICookieManager/nsICookieService + // doesn't let us fake creation times. bug 463127 + + let s = new Sanitizer(); + s.ignoreTimespan = false; + s.prefDomain = "privacy.cpd."; + var itemPrefs = gPrefService.getBranch(s.prefDomain); + itemPrefs.setBoolPref("history", true); + itemPrefs.setBoolPref("downloads", true); + itemPrefs.setBoolPref("cache", false); + itemPrefs.setBoolPref("cookies", false); + itemPrefs.setBoolPref("formdata", true); + itemPrefs.setBoolPref("offlineApps", false); + itemPrefs.setBoolPref("passwords", false); + itemPrefs.setBoolPref("sessions", false); + itemPrefs.setBoolPref("siteSettings", false); + + // Clear 10 minutes ago + s.range = [now_uSec - 10*60*1000000, now_uSec]; + s.sanitize(); + s.range = null; + + yield promiseFormHistoryRemoved(); + + ok(!(yield promiseIsURIVisited(makeURI("http://10minutes.com"))), + "Pretend visit to 10minutes.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://1hour.com"))), + "Pretend visit to 1hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))), + "Pretend visit to 1hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))), + "Pretend visit to 2hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (minutesSinceMidnight > 10) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + + let checkZero = function(num, message) { is(num, 0, message); } + let checkOne = function(num, message) { is(num, 1, message); } + + yield countEntries("10minutes", "10minutes form entry should be deleted", checkZero); + yield countEntries("1hour", "1hour form entry should still exist", checkOne); + yield countEntries("1hour10minutes", "1hour10minutes form entry should still exist", checkOne); + yield countEntries("2hour", "2hour form entry should still exist", checkOne); + yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne); + yield countEntries("4hour", "4hour form entry should still exist", checkOne); + yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne); + if (minutesSinceMidnight > 10) + yield countEntries("today", "today form entry should still exist", checkOne); + yield countEntries("b4today", "b4today form entry should still exist", checkOne); + + ok(!downloadExists(5555555), "10 minute download should now be deleted"); + ok(downloadExists(5555551), "<1 hour download should still be present"); + ok(downloadExists(5555556), "1 hour 10 minute download should still be present"); + ok(downloadExists(5555550), "Year old download should still be present"); + ok(downloadExists(5555552), "<2 hour old download should still be present"); + ok(downloadExists(5555557), "2 hour 10 minute download should still be present"); + ok(downloadExists(5555553), "<4 hour old download should still be present"); + ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); + + if (minutesSinceMidnight > 10) + ok(downloadExists(5555554), "'Today' download should still be present"); + + // Clear 1 hour + Sanitizer.prefs.setIntPref("timeSpan", 1); + s.sanitize(); + + yield promiseFormHistoryRemoved(); + + ok(!(yield promiseIsURIVisited(makeURI("http://1hour.com"))), + "Pretend visit to 1hour.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))), + "Pretend visit to 1hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))), + "Pretend visit to 2hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (hoursSinceMidnight > 1) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + + yield countEntries("1hour", "1hour form entry should be deleted", checkZero); + yield countEntries("1hour10minutes", "1hour10minutes form entry should still exist", checkOne); + yield countEntries("2hour", "2hour form entry should still exist", checkOne); + yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne); + yield countEntries("4hour", "4hour form entry should still exist", checkOne); + yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne); + if (hoursSinceMidnight > 1) + yield countEntries("today", "today form entry should still exist", checkOne); + yield countEntries("b4today", "b4today form entry should still exist", checkOne); + + ok(!downloadExists(5555551), "<1 hour download should now be deleted"); + ok(downloadExists(5555556), "1 hour 10 minute download should still be present"); + ok(downloadExists(5555550), "Year old download should still be present"); + ok(downloadExists(5555552), "<2 hour old download should still be present"); + ok(downloadExists(5555557), "2 hour 10 minute download should still be present"); + ok(downloadExists(5555553), "<4 hour old download should still be present"); + ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); + + if (hoursSinceMidnight > 1) + ok(downloadExists(5555554), "'Today' download should still be present"); + + // Clear 1 hour 10 minutes + s.range = [now_uSec - 70*60*1000000, now_uSec]; + s.sanitize(); + s.range = null; + + yield promiseFormHistoryRemoved(); + + ok(!(yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))), + "Pretend visit to 1hour10minutes.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://2hour.com"))), + "Pretend visit to 2hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (minutesSinceMidnight > 70) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + + yield countEntries("1hour10minutes", "1hour10minutes form entry should be deleted", checkZero); + yield countEntries("2hour", "2hour form entry should still exist", checkOne); + yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne); + yield countEntries("4hour", "4hour form entry should still exist", checkOne); + yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne); + if (minutesSinceMidnight > 70) + yield countEntries("today", "today form entry should still exist", checkOne); + yield countEntries("b4today", "b4today form entry should still exist", checkOne); + + ok(!downloadExists(5555556), "1 hour 10 minute old download should now be deleted"); + ok(downloadExists(5555550), "Year old download should still be present"); + ok(downloadExists(5555552), "<2 hour old download should still be present"); + ok(downloadExists(5555557), "2 hour 10 minute download should still be present"); + ok(downloadExists(5555553), "<4 hour old download should still be present"); + ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); + if (minutesSinceMidnight > 70) + ok(downloadExists(5555554), "'Today' download should still be present"); + + // Clear 2 hours + Sanitizer.prefs.setIntPref("timeSpan", 2); + s.sanitize(); + + yield promiseFormHistoryRemoved(); + + ok(!(yield promiseIsURIVisited(makeURI("http://2hour.com"))), + "Pretend visit to 2hour.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (hoursSinceMidnight > 2) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + + yield countEntries("2hour", "2hour form entry should be deleted", checkZero); + yield countEntries("2hour10minutes", "2hour10minutes form entry should still exist", checkOne); + yield countEntries("4hour", "4hour form entry should still exist", checkOne); + yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne); + if (hoursSinceMidnight > 2) + yield countEntries("today", "today form entry should still exist", checkOne); + yield countEntries("b4today", "b4today form entry should still exist", checkOne); + + ok(!downloadExists(5555552), "<2 hour old download should now be deleted"); + ok(downloadExists(5555550), "Year old download should still be present"); + ok(downloadExists(5555557), "2 hour 10 minute download should still be present"); + ok(downloadExists(5555553), "<4 hour old download should still be present"); + ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); + if (hoursSinceMidnight > 2) + ok(downloadExists(5555554), "'Today' download should still be present"); + + // Clear 2 hours 10 minutes + s.range = [now_uSec - 130*60*1000000, now_uSec]; + s.sanitize(); + s.range = null; + + yield promiseFormHistoryRemoved(); + + ok(!(yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))), + "Pretend visit to 2hour10minutes.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should should still exist"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (minutesSinceMidnight > 130) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + + yield countEntries("2hour10minutes", "2hour10minutes form entry should be deleted", checkZero); + yield countEntries("4hour", "4hour form entry should still exist", checkOne); + yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne); + if (minutesSinceMidnight > 130) + yield countEntries("today", "today form entry should still exist", checkOne); + yield countEntries("b4today", "b4today form entry should still exist", checkOne); + + ok(!downloadExists(5555557), "2 hour 10 minute old download should now be deleted"); + ok(downloadExists(5555553), "<4 hour old download should still be present"); + ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); + ok(downloadExists(5555550), "Year old download should still be present"); + if (minutesSinceMidnight > 130) + ok(downloadExists(5555554), "'Today' download should still be present"); + + // Clear 4 hours + Sanitizer.prefs.setIntPref("timeSpan", 3); + s.sanitize(); + + yield promiseFormHistoryRemoved(); + + ok(!(yield promiseIsURIVisited(makeURI("http://4hour.com"))), + "Pretend visit to 4hour.com should now be deleted"); + ok((yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should should still exist"); + if (hoursSinceMidnight > 4) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + + yield countEntries("4hour", "4hour form entry should be deleted", checkZero); + yield countEntries("4hour10minutes", "4hour10minutes form entry should still exist", checkOne); + if (hoursSinceMidnight > 4) + yield countEntries("today", "today form entry should still exist", checkOne); + yield countEntries("b4today", "b4today form entry should still exist", checkOne); + + ok(!downloadExists(5555553), "<4 hour old download should now be deleted"); + ok(downloadExists(5555558), "4 hour 10 minute download should still be present"); + ok(downloadExists(5555550), "Year old download should still be present"); + if (hoursSinceMidnight > 4) + ok(downloadExists(5555554), "'Today' download should still be present"); + + // Clear 4 hours 10 minutes + s.range = [now_uSec - 250*60*1000000, now_uSec]; + s.sanitize(); + s.range = null; + + yield promiseFormHistoryRemoved(); + + ok(!(yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))), + "Pretend visit to 4hour10minutes.com should now be deleted"); + if (minutesSinceMidnight > 250) { + ok((yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should still exist"); + } + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + + yield countEntries("4hour10minutes", "4hour10minutes form entry should be deleted", checkZero); + if (minutesSinceMidnight > 250) + yield countEntries("today", "today form entry should still exist", checkOne); + yield countEntries("b4today", "b4today form entry should still exist", checkOne); + + ok(!downloadExists(5555558), "4 hour 10 minute download should now be deleted"); + ok(downloadExists(5555550), "Year old download should still be present"); + if (minutesSinceMidnight > 250) + ok(downloadExists(5555554), "'Today' download should still be present"); + + // Clear Today + Sanitizer.prefs.setIntPref("timeSpan", 4); + s.sanitize(); + + yield promiseFormHistoryRemoved(); + + // Be careful. If we add our objectss just before midnight, and sanitize + // runs immediately after, they won't be expired. This is expected, but + // we should not test in that case. We cannot just test for opposite + // condition because we could cross midnight just one moment after we + // cache our time, then we would have an even worse random failure. + var today = isToday(new Date(now_uSec/1000)); + if (today) { + ok(!(yield promiseIsURIVisited(makeURI("http://today.com"))), + "Pretend visit to today.com should now be deleted"); + + yield countEntries("today", "today form entry should be deleted", checkZero); + ok(!downloadExists(5555554), "'Today' download should now be deleted"); + } + + ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should still exist"); + yield countEntries("b4today", "b4today form entry should still exist", checkOne); + ok(downloadExists(5555550), "Year old download should still be present"); + + // Choose everything + Sanitizer.prefs.setIntPref("timeSpan", 0); + s.sanitize(); + + yield promiseFormHistoryRemoved(); + + ok(!(yield promiseIsURIVisited(makeURI("http://before-today.com"))), + "Pretend visit to before-today.com should now be deleted"); + + yield countEntries("b4today", "b4today form entry should be deleted", checkZero); + + ok(!downloadExists(5555550), "Year old download should now be deleted"); +} + +function setupHistory() { + let deferred = Promise.defer(); + + let places = []; + + function addPlace(aURI, aTitle, aVisitDate) { + places.push({ + uri: aURI, + title: aTitle, + visits: [{ + visitDate: aVisitDate, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK + }] + }); + } + + addPlace(makeURI("http://10minutes.com/"), "10 minutes ago", now_uSec - 10 * kUsecPerMin); + addPlace(makeURI("http://1hour.com/"), "Less than 1 hour ago", now_uSec - 45 * kUsecPerMin); + addPlace(makeURI("http://1hour10minutes.com/"), "1 hour 10 minutes ago", now_uSec - 70 * kUsecPerMin); + addPlace(makeURI("http://2hour.com/"), "Less than 2 hours ago", now_uSec - 90 * kUsecPerMin); + addPlace(makeURI("http://2hour10minutes.com/"), "2 hours 10 minutes ago", now_uSec - 130 * kUsecPerMin); + addPlace(makeURI("http://4hour.com/"), "Less than 4 hours ago", now_uSec - 180 * kUsecPerMin); + addPlace(makeURI("http://4hour10minutes.com/"), "4 hours 10 minutesago", now_uSec - 250 * kUsecPerMin); + + let today = new Date(); + today.setHours(0); + today.setMinutes(0); + today.setSeconds(1); + addPlace(makeURI("http://today.com/"), "Today", today.getTime() * 1000); + + let lastYear = new Date(); + lastYear.setFullYear(lastYear.getFullYear() - 1); + addPlace(makeURI("http://before-today.com/"), "Before Today", lastYear.getTime() * 1000); + + PlacesUtils.asyncHistory.updatePlaces(places, { + handleError: function () ok(false, "Unexpected error in adding visit."), + handleResult: function () { }, + handleCompletion: function () deferred.resolve() + }); + + return deferred.promise; +} + +function setupFormHistory() { + + function searchEntries(terms, params) { + let deferred = Promise.defer(); + + let results = []; + FormHistory.search(terms, params, { handleResult: function (result) results.push(result), + handleError: function (error) { + do_throw("Error occurred searching form history: " + error); + deferred.reject(error); + }, + handleCompletion: function (reason) { deferred.resolve(results); } + }); + return deferred.promise; + } + + function update(changes) + { + let deferred = Promise.defer(); + FormHistory.update(changes, { handleError: function (error) { + do_throw("Error occurred searching form history: " + error); + deferred.reject(error); + }, + handleCompletion: function (reason) { deferred.resolve(); } + }); + return deferred.promise; + } + + // Make sure we've got a clean DB to start with, then add the entries we'll be testing. + yield update( + [{ + op: "remove" + }, + { + op : "add", + fieldname : "10minutes", + value : "10m" + }, { + op : "add", + fieldname : "1hour", + value : "1h" + }, { + op : "add", + fieldname : "1hour10minutes", + value : "1h10m" + }, { + op : "add", + fieldname : "2hour", + value : "2h" + }, { + op : "add", + fieldname : "2hour10minutes", + value : "2h10m" + }, { + op : "add", + fieldname : "4hour", + value : "4h" + }, { + op : "add", + fieldname : "4hour10minutes", + value : "4h10m" + }, { + op : "add", + fieldname : "today", + value : "1d" + }, { + op : "add", + fieldname : "b4today", + value : "1y" + }]); + + // Artifically age the entries to the proper vintage. + let timestamp = now_uSec - 10 * kUsecPerMin; + let results = yield searchEntries(["guid"], { fieldname: "10minutes" }); + yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid }); + + timestamp = now_uSec - 45 * kUsecPerMin; + results = yield searchEntries(["guid"], { fieldname: "1hour" }); + yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid }); + + timestamp = now_uSec - 70 * kUsecPerMin; + results = yield searchEntries(["guid"], { fieldname: "1hour10minutes" }); + yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid }); + + timestamp = now_uSec - 90 * kUsecPerMin; + results = yield searchEntries(["guid"], { fieldname: "2hour" }); + yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid }); + + timestamp = now_uSec - 130 * kUsecPerMin; + results = yield searchEntries(["guid"], { fieldname: "2hour10minutes" }); + yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid }); + + timestamp = now_uSec - 180 * kUsecPerMin; + results = yield searchEntries(["guid"], { fieldname: "4hour" }); + yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid }); + + timestamp = now_uSec - 250 * kUsecPerMin; + results = yield searchEntries(["guid"], { fieldname: "4hour10minutes" }); + yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid }); + + let today = new Date(); + today.setHours(0); + today.setMinutes(0); + today.setSeconds(1); + timestamp = today.getTime() * 1000; + results = yield searchEntries(["guid"], { fieldname: "today" }); + yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid }); + + let lastYear = new Date(); + lastYear.setFullYear(lastYear.getFullYear() - 1); + timestamp = lastYear.getTime() * 1000; + results = yield searchEntries(["guid"], { fieldname: "b4today" }); + yield update({ op: "update", firstUsed: timestamp, guid: results[0].guid }); + + var checks = 0; + let checkOne = function(num, message) { is(num, 1, message); checks++; } + + // Sanity check. + yield countEntries("10minutes", "Checking for 10minutes form history entry creation", checkOne); + yield countEntries("1hour", "Checking for 1hour form history entry creation", checkOne); + yield countEntries("1hour10minutes", "Checking for 1hour10minutes form history entry creation", checkOne); + yield countEntries("2hour", "Checking for 2hour form history entry creation", checkOne); + yield countEntries("2hour10minutes", "Checking for 2hour10minutes form history entry creation", checkOne); + yield countEntries("4hour", "Checking for 4hour form history entry creation", checkOne); + yield countEntries("4hour10minutes", "Checking for 4hour10minutes form history entry creation", checkOne); + yield countEntries("today", "Checking for today form history entry creation", checkOne); + yield countEntries("b4today", "Checking for b4today form history entry creation", checkOne); + is(checks, 9, "9 checks made"); +} + +function setupDownloads() { + + // Add 10-minutes download to DB + let data = { + id: "5555555", + name: "fakefile-10-minutes", + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169", + target: "fakefile-10-minutes", + startTime: now_uSec - 10 * kUsecPerMin, // 10 minutes ago, in uSec + endTime: now_uSec - 11 * kUsecPerMin, // 1 minute later + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "a1bcD23eF4g5" + }; + + let db = dm.DBConnection; + let stmt = db.createStatement( + "INSERT INTO moz_downloads (id, name, source, target, startTime, endTime, " + + "state, currBytes, maxBytes, preferredAction, autoResume, guid) " + + "VALUES (:id, :name, :source, :target, :startTime, :endTime, :state, " + + ":currBytes, :maxBytes, :preferredAction, :autoResume, :guid)"); + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.reset(); + } + + // Add within-1-hour download to DB + data = { + id: "5555551", + name: "fakefile-1-hour", + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440", + target: "fakefile-1-hour", + startTime: now_uSec - 45 * kUsecPerMin, // 45 minutes ago, in uSec + endTime: now_uSec - 44 * kUsecPerMin, // 1 minute later + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "1bcD23eF4g5a" + }; + + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.reset(); + } + + // Add 1-hour-10-minutes download to DB + data = { + id: "5555556", + name: "fakefile-1-hour-10-minutes", + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169", + target: "fakefile-1-hour-10-minutes", + startTime: now_uSec - 70 * kUsecPerMin, // 70 minutes ago, in uSec + endTime: now_uSec - 71 * kUsecPerMin, // 1 minute later + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "a1cbD23e4Fg5" + }; + + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.reset(); + } + + // Add within-2-hour download + data = { + id: "5555552", + name: "fakefile-2-hour", + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440", + target: "fakefile-2-hour", + startTime: now_uSec - 90 * kUsecPerMin, // 90 minutes ago, in uSec + endTime: now_uSec - 89 * kUsecPerMin, // 1 minute later + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "b1aDc23eFg54" + }; + + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.reset(); + } + + // Add 2-hour-10-minutes download + data = { + id: "5555557", + name: "fakefile-2-hour-10-minutes", + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169", + target: "fakefile-2-hour-10-minutes", + startTime: now_uSec - 130 * kUsecPerMin, // 130 minutes ago, in uSec + endTime: now_uSec - 131 * kUsecPerMin, // 1 minute later + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "z1bcD23eF4g5" + }; + + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.reset(); + } + + // Add within-4-hour download + data = { + id: "5555553", + name: "fakefile-4-hour", + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440", + target: "fakefile-4-hour", + startTime: now_uSec - 180 * kUsecPerMin, // 180 minutes ago, in uSec + endTime: now_uSec - 179 * kUsecPerMin, // 1 minute later + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "zzzcD23eF4g5" + }; + + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.reset(); + } + + // Add 4-hour-10-minutes download + data = { + id: "5555558", + name: "fakefile-4-hour-10-minutes", + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169", + target: "fakefile-4-hour-10-minutes", + startTime: now_uSec - 250 * kUsecPerMin, // 250 minutes ago, in uSec + endTime: now_uSec - 251 * kUsecPerMin, // 1 minute later + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "z1bzz23eF4gz" + }; + + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.reset(); + } + + // Add "today" download + let today = new Date(); + today.setHours(0); + today.setMinutes(0); + today.setSeconds(1); + + data = { + id: "5555554", + name: "fakefile-today", + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440", + target: "fakefile-today", + startTime: today.getTime() * 1000, // 12:00:30am this morning, in uSec + endTime: (today.getTime() + 1000) * 1000, // 1 second later + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "ffffD23eF4g5" + }; + + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.reset(); + } + + // Add "before today" download + let lastYear = new Date(); + lastYear.setFullYear(lastYear.getFullYear() - 1); + data = { + id: "5555550", + name: "fakefile-old", + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440", + target: "fakefile-old", + startTime: lastYear.getTime() * 1000, // 1 year ago, in uSec + endTime: (lastYear.getTime() + 1000) * 1000, // 1 second later + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "ggggg23eF4g5" + }; + + try { + for (let prop in data) + stmt.params[prop] = data[prop]; + stmt.execute(); + } + finally { + stmt.finalize(); + } + + // Confirm everything worked + ok(downloadExists(5555550), "Pretend download for everything case should exist"); + ok(downloadExists(5555555), "Pretend download for 10-minutes case should exist"); + ok(downloadExists(5555551), "Pretend download for 1-hour case should exist"); + ok(downloadExists(5555556), "Pretend download for 1-hour-10-minutes case should exist"); + ok(downloadExists(5555552), "Pretend download for 2-hour case should exist"); + ok(downloadExists(5555557), "Pretend download for 2-hour-10-minutes case should exist"); + ok(downloadExists(5555553), "Pretend download for 4-hour case should exist"); + ok(downloadExists(5555558), "Pretend download for 4-hour-10-minutes case should exist"); + ok(downloadExists(5555554), "Pretend download for Today case should exist"); +} + +/** + * Checks to see if the downloads with the specified id exists. + * + * @param aID + * The ids of the downloads to check. + */ +function downloadExists(aID) +{ + let db = dm.DBConnection; + let stmt = db.createStatement( + "SELECT * " + + "FROM moz_downloads " + + "WHERE id = :id" + ); + stmt.params.id = aID; + var rows = stmt.executeStep(); + stmt.finalize(); + return rows; +} + +function isToday(aDate) { + return aDate.getDate() == new Date().getDate(); +} diff --git a/browser/base/content/test/browser_sanitizeDialog.js b/browser/base/content/test/browser_sanitizeDialog.js new file mode 100644 index 000000000..02f32e3bf --- /dev/null +++ b/browser/base/content/test/browser_sanitizeDialog.js @@ -0,0 +1,1099 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests the sanitize dialog (a.k.a. the clear recent history dialog). + * See bug 480169. + * + * The purpose of this test is not to fully flex the sanitize timespan code; + * browser/base/content/test/browser_sanitize-timespans.js does that. This + * test checks the UI of the dialog and makes sure it's correctly connected to + * the sanitize timespan code. + * + * Some of this code, especially the history creation parts, was taken from + * browser/base/content/test/browser_sanitize-timespans.js. + */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); + +let tempScope = {}; +Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tempScope); +let Sanitizer = tempScope.Sanitizer; + +const dm = Cc["@mozilla.org/download-manager;1"]. + getService(Ci.nsIDownloadManager); + +const kUsecPerMin = 60 * 1000000; + +let formEntries; + +// Add tests here. Each is a function that's called by doNextTest(). +var gAllTests = [ + + /** + * Initializes the dialog to its default state. + */ + function () { + let wh = new WindowHelper(); + wh.onload = function () { + // Select "Last Hour" + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + // Hide details + if (!this.getItemList().collapsed) + this.toggleDetails(); + this.acceptDialog(); + }; + wh.open(); + }, + + /** + * Cancels the dialog, makes sure history not cleared. + */ + function () { + // Add history (within the past hour) + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("http://" + i + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); + uris.push(pURI); + } + + addVisits(places, function() { + let wh = new WindowHelper(); + wh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + this.checkPrefCheckbox("history", false); + this.checkDetails(false); + + // Show details + this.toggleDetails(); + this.checkDetails(true); + + // Hide details + this.toggleDetails(); + this.checkDetails(false); + this.cancelDialog(); + }; + wh.onunload = function () { + yield promiseHistoryClearedState(uris, false); + yield blankSlate(); + yield promiseHistoryClearedState(uris, true); + }; + wh.open(); + }); + }, + + /** + * Ensures that the combined history-downloads checkbox clears both history + * visits and downloads when checked; the dialog respects simple timespan. + */ + function () { + // Add history (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("http://" + i + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); + uris.push(pURI); + } + // Add history (over an hour ago). + let olderURIs = []; + for (let i = 0; i < 5; i++) { + pURI = makeURI("http://" + (61 + i) + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(61 + i)}); + olderURIs.push(pURI); + } + + addVisits(places, function() { + // Add downloads (within the past hour). + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + downloadIDs.push(addDownloadWithMinutesAgo(i)); + } + // Add downloads (over an hour ago). + let olderDownloadIDs = []; + for (let i = 0; i < 5; i++) { + olderDownloadIDs.push(addDownloadWithMinutesAgo(61 + i)); + } + let totalHistoryVisits = uris.length + olderURIs.length; + + let wh = new WindowHelper(); + wh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + this.checkPrefCheckbox("history", true); + this.acceptDialog(); + + intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_HOUR, + "timeSpan pref should be hour after accepting dialog with " + + "hour selected"); + boolPrefIs("cpd.history", true, + "history pref should be true after accepting dialog with " + + "history checkbox checked"); + boolPrefIs("cpd.downloads", true, + "downloads pref should be true after accepting dialog with " + + "history checkbox checked"); + }; + wh.onunload = function () { + // History visits and downloads within one hour should be cleared. + yield promiseHistoryClearedState(uris, true); + ensureDownloadsClearedState(downloadIDs, true); + + // Visits and downloads > 1 hour should still exist. + yield promiseHistoryClearedState(olderURIs, false); + ensureDownloadsClearedState(olderDownloadIDs, false); + + // OK, done, cleanup after ourselves. + yield blankSlate(); + yield promiseHistoryClearedState(olderURIs, true); + ensureDownloadsClearedState(olderDownloadIDs, true); + }; + wh.open(); + }); + }, + + /** + * Add form history entries for the next test. + */ + function () { + formEntries = []; + + let iter = function() { + for (let i = 0; i < 5; i++) { + formEntries.push(addFormEntryWithMinutesAgo(iter, i)); + yield; + } + doNextTest(); + }(); + + iter.next(); + }, + + /** + * Ensures that the combined history-downloads checkbox removes neither + * history visits nor downloads when not checked. + */ + function () { + // Add history, downloads, form entries (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 5; i++) { + pURI = makeURI("http://" + i + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); + uris.push(pURI); + } + + addVisits(places, function() { + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + downloadIDs.push(addDownloadWithMinutesAgo(i)); + } + + let wh = new WindowHelper(); + wh.onload = function () { + is(this.isWarningPanelVisible(), false, + "Warning panel should be hidden after previously accepting dialog " + + "with a predefined timespan"); + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + + // Remove only form entries, leave history (including downloads). + this.checkPrefCheckbox("history", false); + this.checkPrefCheckbox("formdata", true); + this.acceptDialog(); + + intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_HOUR, + "timeSpan pref should be hour after accepting dialog with " + + "hour selected"); + boolPrefIs("cpd.history", false, + "history pref should be false after accepting dialog with " + + "history checkbox unchecked"); + boolPrefIs("cpd.downloads", false, + "downloads pref should be false after accepting dialog with " + + "history checkbox unchecked"); + }; + wh.onunload = function () { + // Of the three only form entries should be cleared. + yield promiseHistoryClearedState(uris, false); + ensureDownloadsClearedState(downloadIDs, false); + + formEntries.forEach(function (entry) { + let exists = yield formNameExists(entry); + is(exists, false, "form entry " + entry + " should no longer exist"); + }); + + // OK, done, cleanup after ourselves. + yield blankSlate(); + yield promiseHistoryClearedState(uris, true); + ensureDownloadsClearedState(downloadIDs, true); + }; + wh.open(); + }); + }, + + /** + * Ensures that the "Everything" duration option works. + */ + function () { + // Add history. + let uris = []; + let places = []; + let pURI; + // within past hour, within past two hours, within past four hours and + // outside past four hours + [10, 70, 130, 250].forEach(function(aValue) { + pURI = makeURI("http://" + aValue + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)}); + uris.push(pURI); + }); + addVisits(places, function() { + let wh = new WindowHelper(); + wh.onload = function () { + is(this.isWarningPanelVisible(), false, + "Warning panel should be hidden after previously accepting dialog " + + "with a predefined timespan"); + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + this.checkPrefCheckbox("history", true); + this.checkDetails(true); + + // Hide details + this.toggleDetails(); + this.checkDetails(false); + + // Show details + this.toggleDetails(); + this.checkDetails(true); + + this.acceptDialog(); + + intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_EVERYTHING, + "timeSpan pref should be everything after accepting dialog " + + "with everything selected"); + }; + wh.onunload = function () { + yield promiseHistoryClearedState(uris, true); + }; + wh.open(); + }); + }, + + /** + * Ensures that the "Everything" warning is visible on dialog open after + * the previous test. + */ + function () { + // Add history. + let uris = []; + let places = []; + let pURI; + // within past hour, within past two hours, within past four hours and + // outside past four hours + [10, 70, 130, 250].forEach(function(aValue) { + pURI = makeURI("http://" + aValue + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)}); + uris.push(pURI); + }); + addVisits(places, function() { + let wh = new WindowHelper(); + wh.onload = function () { + is(this.isWarningPanelVisible(), true, + "Warning panel should be visible after previously accepting dialog " + + "with clearing everything"); + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + this.checkPrefCheckbox("history", true); + this.acceptDialog(); + + intPrefIs("sanitize.timeSpan", Sanitizer.TIMESPAN_EVERYTHING, + "timeSpan pref should be everything after accepting dialog " + + "with everything selected"); + }; + wh.onunload = function () { + yield promiseHistoryClearedState(uris, true); + }; + wh.open(); + }); + }, + + /** + * Add form history entry for the next test. + */ + function () { + let iter = function() { + formEntries = [ addFormEntryWithMinutesAgo(iter, 10) ]; + yield; + doNextTest(); + }(); + + iter.next(); + }, + + /** + * The next three tests checks that when a certain history item cannot be + * cleared then the checkbox should be both disabled and unchecked. + * In addition, we ensure that this behavior does not modify the preferences. + */ + function () { + // Add history. + let pURI = makeURI("http://" + 10 + "-minutes-ago.com/"); + addVisits({uri: pURI, visitDate: visitTimeForMinutesAgo(10)}, function() { + let uris = [ pURI ]; + + let wh = new WindowHelper(); + wh.onload = function() { + // Check that the relevant checkboxes are enabled + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='privacy.cpd.formdata']"); + ok(cb.length == 1 && !cb[0].disabled, "There is formdata, checkbox to " + + "clear formdata should be enabled."); + + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='privacy.cpd.history']"); + ok(cb.length == 1 && !cb[0].disabled, "There is history, checkbox to " + + "clear history should be enabled."); + + this.checkAllCheckboxes(); + this.acceptDialog(); + }; + wh.onunload = function () { + yield promiseHistoryClearedState(uris, true); + + let exists = yield formNameExists(formEntries[0]); + is(exists, false, "form entry " + formEntries[0] + " should no longer exist"); + }; + wh.open(); + }); + }, + function () { + let wh = new WindowHelper(); + wh.onload = function() { + boolPrefIs("cpd.history", true, + "history pref should be true after accepting dialog with " + + "history checkbox checked"); + boolPrefIs("cpd.formdata", true, + "formdata pref should be true after accepting dialog with " + + "formdata checkbox checked"); + + + // Even though the formdata pref is true, because there is no history + // left to clear, the checkbox will be disabled. + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='privacy.cpd.formdata']"); + ok(cb.length == 1 && cb[0].disabled && !cb[0].checked, + "There is no formdata history, checkbox should be disabled and be " + + "cleared to reduce user confusion (bug 497664)."); + + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='privacy.cpd.history']"); + ok(cb.length == 1 && !cb[0].disabled && cb[0].checked, + "There is no history, but history checkbox should always be enabled " + + "and will be checked from previous preference."); + + this.acceptDialog(); + } + wh.open(); + }, + + /** + * Add form history entry for the next test. + */ + function () { + let iter = function() { + formEntries = [ addFormEntryWithMinutesAgo(iter, 10) ]; + yield; + doNextTest(); + }(); + + iter.next(); + }, + + function () { + let wh = new WindowHelper(); + wh.onload = function() { + boolPrefIs("cpd.formdata", true, + "formdata pref should persist previous value after accepting " + + "dialog where you could not clear formdata."); + + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='privacy.cpd.formdata']"); + ok(cb.length == 1 && !cb[0].disabled && cb[0].checked, + "There exists formEntries so the checkbox should be in sync with " + + "the pref."); + + this.acceptDialog(); + }; + wh.onunload = function () { + let exists = yield formNameExists(formEntries[0]); + is(exists, false, "form entry " + formEntries[0] + " should no longer exist"); + }; + wh.open(); + }, + + + /** + * These next six tests together ensure that toggling details persists + * across dialog openings. + */ + function () { + let wh = new WindowHelper(); + wh.onload = function () { + // Check all items and select "Everything" + this.checkAllCheckboxes(); + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + + // Hide details + this.toggleDetails(); + this.checkDetails(false); + this.acceptDialog(); + }; + wh.open(); + }, + function () { + let wh = new WindowHelper(); + wh.onload = function () { + // Details should remain closed because all items are checked. + this.checkDetails(false); + + // Uncheck history. + this.checkPrefCheckbox("history", false); + this.acceptDialog(); + }; + wh.open(); + }, + function () { + let wh = new WindowHelper(); + wh.onload = function () { + // Details should be open because not all items are checked. + this.checkDetails(true); + + // Modify the Site Preferences item state (bug 527820) + this.checkAllCheckboxes(); + this.checkPrefCheckbox("siteSettings", false); + this.acceptDialog(); + }; + wh.open(); + }, + function () { + let wh = new WindowHelper(); + wh.onload = function () { + // Details should be open because not all items are checked. + this.checkDetails(true); + + // Hide details + this.toggleDetails(); + this.checkDetails(false); + this.cancelDialog(); + }; + wh.open(); + }, + function () { + let wh = new WindowHelper(); + wh.onload = function () { + // Details should be open because not all items are checked. + this.checkDetails(true); + + // Select another duration + this.selectDuration(Sanitizer.TIMESPAN_HOUR); + // Hide details + this.toggleDetails(); + this.checkDetails(false); + this.acceptDialog(); + }; + wh.open(); + }, + function () { + let wh = new WindowHelper(); + wh.onload = function () { + // Details should not be open because "Last Hour" is selected + this.checkDetails(false); + + this.cancelDialog(); + }; + wh.open(); + }, + function () { + let wh = new WindowHelper(); + wh.onload = function () { + // Details should have remained closed + this.checkDetails(false); + + // Show details + this.toggleDetails(); + this.checkDetails(true); + this.cancelDialog(); + }; + wh.open(); + }, + function () { + // Test for offline cache deletion + + // Prepare stuff, we will work with www.example.com + var URL = "http://www.example.com"; + + var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + var URI = ios.newURI(URL, null, null); + + var sm = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + var principal = sm.getNoAppCodebasePrincipal(URI); + + // Give www.example.com privileges to store offline data + var pm = Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager); + pm.addFromPrincipal(principal, "offline-app", Ci.nsIPermissionManager.ALLOW_ACTION); + pm.addFromPrincipal(principal, "offline-app", Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN); + + // Store something to the offline cache + const nsICache = Components.interfaces.nsICache; + var cs = Components.classes["@mozilla.org/network/cache-service;1"] + .getService(Components.interfaces.nsICacheService); + var session = cs.createSession(URL + "/manifest", nsICache.STORE_OFFLINE, nsICache.STREAM_BASED); + + // Open the dialog + let wh = new WindowHelper(); + wh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + // Show details + this.toggleDetails(); + // Clear only offlineApps + this.uncheckAllCheckboxes(); + this.checkPrefCheckbox("offlineApps", true); + this.acceptDialog(); + + // Check if the cache has been deleted + var size = -1; + var visitor = { + visitDevice: function (deviceID, deviceInfo) + { + if (deviceID == "offline") + size = deviceInfo.totalSize; + + // Do not enumerate entries + return false; + }, + + visitEntry: function (deviceID, entryInfo) + { + // Do not enumerate entries. + return false; + } + }; + cs.visitEntries(visitor); + is(size, 0, "offline application cache entries evicted"); + }; + + var cacheListener = { + onCacheEntryAvailable: function (entry, access, status) { + is(status, Cr.NS_OK); + var stream = entry.openOutputStream(0); + var content = "content"; + stream.write(content, content.length); + stream.close(); + entry.close(); + wh.open(); + } + }; + + session.asyncOpenCacheEntry(URL, nsICache.ACCESS_READ_WRITE, cacheListener); + }, + function () { + // Test for offline apps permission deletion + + // Prepare stuff, we will work with www.example.com + var URL = "http://www.example.com"; + + var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService); + var URI = ios.newURI(URL, null, null); + + var sm = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + var principal = sm.getNoAppCodebasePrincipal(URI); + + // Open the dialog + let wh = new WindowHelper(); + wh.onload = function () { + this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + // Show details + this.toggleDetails(); + // Clear only offlineApps + this.uncheckAllCheckboxes(); + this.checkPrefCheckbox("siteSettings", true); + this.acceptDialog(); + + // Check all has been deleted (privileges, data, cache) + var pm = Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager); + is(pm.testPermissionFromPrincipal(principal, "offline-app"), 0, "offline-app permissions removed"); + }; + wh.open(); + } +]; + +// Used as the download database ID for a new download. Incremented for each +// new download. See addDownloadWithMinutesAgo(). +var gDownloadId = 5555551; + +// Index in gAllTests of the test currently being run. Incremented for each +// test run. See doNextTest(). +var gCurrTest = 0; + +var now_uSec = Date.now() * 1000; + +/////////////////////////////////////////////////////////////////////////////// + +/** + * This wraps the dialog and provides some convenience methods for interacting + * with it. + * + * @param aWin + * The dialog's nsIDOMWindow + */ +function WindowHelper(aWin) { + this.win = aWin; +} + +WindowHelper.prototype = { + /** + * "Presses" the dialog's OK button. + */ + acceptDialog: function () { + is(this.win.document.documentElement.getButton("accept").disabled, false, + "Dialog's OK button should not be disabled"); + this.win.document.documentElement.acceptDialog(); + }, + + /** + * "Presses" the dialog's Cancel button. + */ + cancelDialog: function () { + this.win.document.documentElement.cancelDialog(); + }, + + /** + * Ensures that the details progressive disclosure button and the item list + * hidden by it match up. Also makes sure the height of the dialog is + * sufficient for the item list and warning panel. + * + * @param aShouldBeShown + * True if you expect the details to be shown and false if hidden + */ + checkDetails: function (aShouldBeShown) { + let button = this.getDetailsButton(); + let list = this.getItemList(); + let hidden = list.hidden || list.collapsed; + is(hidden, !aShouldBeShown, + "Details should be " + (aShouldBeShown ? "shown" : "hidden") + + " but were actually " + (hidden ? "hidden" : "shown")); + let dir = hidden ? "down" : "up"; + is(button.className, "expander-" + dir, + "Details button should be " + dir + " because item list is " + + (hidden ? "" : "not ") + "hidden"); + let height = 0; + if (!hidden) { + ok(list.boxObject.height > 30, "listbox has sufficient size") + height += list.boxObject.height; + } + if (this.isWarningPanelVisible()) + height += this.getWarningPanel().boxObject.height; + ok(height < this.win.innerHeight, + "Window should be tall enough to fit warning panel and item list"); + }, + + /** + * (Un)checks a history scope checkbox (browser & download history, + * form history, etc.). + * + * @param aPrefName + * The final portion of the checkbox's privacy.cpd.* preference name + * @param aCheckState + * True if the checkbox should be checked, false otherwise + */ + checkPrefCheckbox: function (aPrefName, aCheckState) { + var pref = "privacy.cpd." + aPrefName; + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='" + pref + "']"); + is(cb.length, 1, "found checkbox for " + pref + " preference"); + if (cb[0].checked != aCheckState) + cb[0].click(); + }, + + /** + * Makes sure all the checkboxes are checked. + */ + _checkAllCheckboxesCustom: function (check) { + var cb = this.win.document.querySelectorAll("#itemList > [preference]"); + ok(cb.length > 1, "found checkboxes for preferences"); + for (var i = 0; i < cb.length; ++i) { + var pref = this.win.document.getElementById(cb[i].getAttribute("preference")); + if (!!pref.value ^ check) + cb[i].click(); + } + }, + + checkAllCheckboxes: function () { + this._checkAllCheckboxesCustom(true); + }, + + uncheckAllCheckboxes: function () { + this._checkAllCheckboxesCustom(false); + }, + + /** + * @return The details progressive disclosure button + */ + getDetailsButton: function () { + return this.win.document.getElementById("detailsExpander"); + }, + + /** + * @return The dialog's duration dropdown + */ + getDurationDropdown: function () { + return this.win.document.getElementById("sanitizeDurationChoice"); + }, + + /** + * @return The item list hidden by the details progressive disclosure button + */ + getItemList: function () { + return this.win.document.getElementById("itemList"); + }, + + /** + * @return The clear-everything warning box + */ + getWarningPanel: function () { + return this.win.document.getElementById("sanitizeEverythingWarningBox"); + }, + + /** + * @return True if the "Everything" warning panel is visible (as opposed to + * the tree) + */ + isWarningPanelVisible: function () { + return !this.getWarningPanel().hidden; + }, + + /** + * Opens the clear recent history dialog. Before calling this, set + * this.onload to a function to execute onload. It should close the dialog + * when done so that the tests may continue. Set this.onunload to a function + * to execute onunload. this.onunload is optional. If it returns true, the + * caller is expected to call waitForAsyncUpdates at some point; if false is + * returned, waitForAsyncUpdates is called automatically. + */ + open: function () { + let wh = this; + + function windowObserver(aSubject, aTopic, aData) { + if (aTopic != "domwindowopened") + return; + + Services.ww.unregisterNotification(windowObserver); + + var loaded = false; + let win = aSubject.QueryInterface(Ci.nsIDOMWindow); + + win.addEventListener("load", function onload(event) { + win.removeEventListener("load", onload, false); + + if (win.name !== "SanitizeDialog") + return; + + wh.win = win; + loaded = true; + + executeSoon(function () { + // Some exceptions that reach here don't reach the test harness, but + // ok()/is() do... + try { + wh.onload(); + } + catch (exc) { + win.close(); + ok(false, "Unexpected exception: " + exc + "\n" + exc.stack); + finish(); + } + }); + }, false); + + win.addEventListener("unload", function onunload(event) { + if (win.name !== "SanitizeDialog") { + win.removeEventListener("unload", onunload, false); + return; + } + + // Why is unload fired before load? + if (!loaded) + return; + + win.removeEventListener("unload", onunload, false); + wh.win = win; + + executeSoon(function () { + // Some exceptions that reach here don't reach the test harness, but + // ok()/is() do... + try { + if (wh.onunload) { + Task.spawn(wh.onunload).then(function() { + waitForAsyncUpdates(doNextTest); + }); + } else { + waitForAsyncUpdates(doNextTest); + } + } + catch (exc) { + win.close(); + ok(false, "Unexpected exception: " + exc + "\n" + exc.stack); + finish(); + } + }); + }, false); + } + Services.ww.registerNotification(windowObserver); + Services.ww.openWindow(null, + "chrome://browser/content/sanitize.xul", + "SanitizeDialog", + "chrome,titlebar,dialog,centerscreen,modal", + null); + }, + + /** + * Selects a duration in the duration dropdown. + * + * @param aDurVal + * One of the Sanitizer.TIMESPAN_* values + */ + selectDuration: function (aDurVal) { + this.getDurationDropdown().value = aDurVal; + if (aDurVal === Sanitizer.TIMESPAN_EVERYTHING) { + is(this.isWarningPanelVisible(), true, + "Warning panel should be visible for TIMESPAN_EVERYTHING"); + } + else { + is(this.isWarningPanelVisible(), false, + "Warning panel should not be visible for non-TIMESPAN_EVERYTHING"); + } + }, + + /** + * Toggles the details progressive disclosure button. + */ + toggleDetails: function () { + this.getDetailsButton().click(); + } +}; + +/** + * Adds a download to history. + * + * @param aMinutesAgo + * The download will be downloaded this many minutes ago + */ +function addDownloadWithMinutesAgo(aMinutesAgo) { + let name = "fakefile-" + aMinutesAgo + "-minutes-ago"; + let data = { + id: gDownloadId, + name: name, + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169", + target: name, + startTime: now_uSec - (aMinutesAgo * kUsecPerMin), + endTime: now_uSec - ((aMinutesAgo + 1) * kUsecPerMin), + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0 + }; + + let db = dm.DBConnection; + let stmt = db.createStatement( + "INSERT INTO moz_downloads (id, name, source, target, startTime, endTime, " + + "state, currBytes, maxBytes, preferredAction, autoResume) " + + "VALUES (:id, :name, :source, :target, :startTime, :endTime, :state, " + + ":currBytes, :maxBytes, :preferredAction, :autoResume)"); + try { + for (let prop in data) { + stmt.params[prop] = data[prop]; + } + stmt.execute(); + } + finally { + stmt.reset(); + } + + is(downloadExists(gDownloadId), true, + "Sanity check: download " + gDownloadId + + " should exist after creating it"); + + return gDownloadId++; +} + +/** + * Adds a form entry to history. + * + * @param aMinutesAgo + * The entry will be added this many minutes ago + */ +function addFormEntryWithMinutesAgo(then, aMinutesAgo) { + let name = aMinutesAgo + "-minutes-ago"; + + // Artifically age the entry to the proper vintage. + let timestamp = now_uSec - (aMinutesAgo * kUsecPerMin); + + FormHistory.update({ op: "add", fieldname: name, value: "dummy", firstUsed: timestamp }, + { handleError: function (error) { + do_throw("Error occurred updating form history: " + error); + }, + handleCompletion: function (reason) { then.next(); } + }); + return name; +} + +/** + * Checks if a form entry exists. + */ +function formNameExists(name) +{ + let deferred = Promise.defer(); + + let count = 0; + FormHistory.count({ fieldname: name }, + { handleResult: function (result) count = result, + handleError: function (error) { + do_throw("Error occurred searching form history: " + error); + deferred.reject(error); + }, + handleCompletion: function (reason) { + if (!reason) deferred.resolve(count); + } + }); + + return deferred.promise; +} + +/** + * Removes all history visits, downloads, and form entries. + */ +function blankSlate() { + PlacesUtils.bhistory.removeAllPages(); + dm.cleanUp(); + + let deferred = Promise.defer(); + FormHistory.update({ op: "remove" }, + { handleError: function (error) { + do_throw("Error occurred updating form history: " + error); + deferred.reject(error); + }, + handleCompletion: function (reason) { if (!reason) deferred.resolve(); } + }); + return deferred.promise; +} + +/** + * Ensures that the given pref is the expected value. + * + * @param aPrefName + * The pref's sub-branch under the privacy branch + * @param aExpectedVal + * The pref's expected value + * @param aMsg + * Passed to is() + */ +function boolPrefIs(aPrefName, aExpectedVal, aMsg) { + is(gPrefService.getBoolPref("privacy." + aPrefName), aExpectedVal, aMsg); +} + +/** + * Checks to see if the download with the specified ID exists. + * + * @param aID + * The ID of the download to check + * @return True if the download exists, false otherwise + */ +function downloadExists(aID) +{ + let db = dm.DBConnection; + let stmt = db.createStatement( + "SELECT * " + + "FROM moz_downloads " + + "WHERE id = :id" + ); + stmt.params.id = aID; + let rows = stmt.executeStep(); + stmt.finalize(); + return !!rows; +} + +/** + * Runs the next test in the gAllTests array. If all tests have been run, + * finishes the entire suite. + */ +function doNextTest() { + if (gAllTests.length <= gCurrTest) { + blankSlate(); + waitForAsyncUpdates(finish); + } + else { + let ct = gCurrTest; + gCurrTest++; + gAllTests[ct](); + } +} + +/** + * Ensures that the specified downloads are either cleared or not. + * + * @param aDownloadIDs + * Array of download database IDs + * @param aShouldBeCleared + * True if each download should be cleared, false otherwise + */ +function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) { + let niceStr = aShouldBeCleared ? "no longer" : "still"; + aDownloadIDs.forEach(function (id) { + is(downloadExists(id), !aShouldBeCleared, + "download " + id + " should " + niceStr + " exist"); + }); +} + +/** + * Ensures that the given pref is the expected value. + * + * @param aPrefName + * The pref's sub-branch under the privacy branch + * @param aExpectedVal + * The pref's expected value + * @param aMsg + * Passed to is() + */ +function intPrefIs(aPrefName, aExpectedVal, aMsg) { + is(gPrefService.getIntPref("privacy." + aPrefName), aExpectedVal, aMsg); +} + +/** + * Creates a visit time. + * + * @param aMinutesAgo + * The visit will be visited this many minutes ago + */ +function visitTimeForMinutesAgo(aMinutesAgo) { + return now_uSec - aMinutesAgo * kUsecPerMin; +} + +/////////////////////////////////////////////////////////////////////////////// + +function test() { + requestLongerTimeout(2); + waitForExplicitFinish(); + blankSlate(); + // Kick off all the tests in the gAllTests array. + waitForAsyncUpdates(doNextTest); +} diff --git a/browser/base/content/test/browser_sanitizeDialog_treeView.js b/browser/base/content/test/browser_sanitizeDialog_treeView.js new file mode 100644 index 000000000..10e726100 --- /dev/null +++ b/browser/base/content/test/browser_sanitizeDialog_treeView.js @@ -0,0 +1,632 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests the sanitize dialog (a.k.a. the clear recent history dialog). + * See bug 480169. + * + * The purpose of this test is not to fully flex the sanitize timespan code; + * browser/base/content/test/browser_sanitize-timespans.js does that. This + * test checks the UI of the dialog and makes sure it's correctly connected to + * the sanitize timespan code. + * + * Some of this code, especially the history creation parts, was taken from + * browser/base/content/test/browser_sanitize-timespans.js. + */ + +Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader). + loadSubScript("chrome://browser/content/sanitize.js"); + +const dm = Cc["@mozilla.org/download-manager;1"]. + getService(Ci.nsIDownloadManager); +const formhist = Cc["@mozilla.org/satchel/form-history;1"]. + getService(Ci.nsIFormHistory2); + +// Add tests here. Each is a function that's called by doNextTest(). +var gAllTests = [ + + /** + * Moves the grippy around, makes sure it works OK. + */ + function () { + // Add history (within the past hour) to get some rows in the tree. + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("http://" + i + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); + uris.push(pURI); + } + + addVisits(places, function() { + // Open the dialog and do our tests. + openWindow(function (aWin) { + let wh = new WindowHelper(aWin); + wh.selectDuration(Sanitizer.TIMESPAN_HOUR); + wh.checkGrippy("Grippy should be at last row after selecting HOUR " + + "duration", + wh.getRowCount() - 1); + + // Move the grippy around. + let row = wh.getGrippyRow(); + while (row !== 0) { + row--; + wh.moveGrippyBy(-1); + wh.checkGrippy("Grippy should be moved up one row", row); + } + wh.moveGrippyBy(-1); + wh.checkGrippy("Grippy should remain at first row after trying to move " + + "it up", + 0); + while (row !== wh.getRowCount() - 1) { + row++; + wh.moveGrippyBy(1); + wh.checkGrippy("Grippy should be moved down one row", row); + } + wh.moveGrippyBy(1); + wh.checkGrippy("Grippy should remain at last row after trying to move " + + "it down", + wh.getRowCount() - 1); + + // Cancel the dialog, make sure history visits are not cleared. + wh.checkPrefCheckbox("history", false); + + wh.cancelDialog(); + yield promiseHistoryClearedState(uris, false); + + // OK, done, cleanup after ourselves. + blankSlate(); + yield promiseHistoryClearedState(uris, true); + }); + }); + }, + + /** + * Ensures that the combined history-downloads checkbox clears both history + * visits and downloads when checked; the dialog respects simple timespan. + */ + function () { + // Add history (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 30; i++) { + pURI = makeURI("http://" + i + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); + uris.push(pURI); + } + // Add history (over an hour ago). + let olderURIs = []; + for (let i = 0; i < 5; i++) { + pURI = makeURI("http://" + (60 + i) + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(60 + i)}); + olderURIs.push(pURI); + } + + addVisits(places, function() { + // Add downloads (within the past hour). + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + downloadIDs.push(addDownloadWithMinutesAgo(i)); + } + // Add downloads (over an hour ago). + let olderDownloadIDs = []; + for (let i = 0; i < 5; i++) { + olderDownloadIDs.push(addDownloadWithMinutesAgo(61 + i)); + } + let totalHistoryVisits = uris.length + olderURIs.length; + + // Open the dialog and do our tests. + openWindow(function (aWin) { + let wh = new WindowHelper(aWin); + wh.selectDuration(Sanitizer.TIMESPAN_HOUR); + wh.checkGrippy("Grippy should be at proper row after selecting HOUR " + + "duration", + uris.length); + + // Accept the dialog, make sure history visits and downloads within one + // hour are cleared. + wh.checkPrefCheckbox("history", true); + wh.acceptDialog(); + yield promiseHistoryClearedState(uris, true); + ensureDownloadsClearedState(downloadIDs, true); + + // Make sure visits and downloads > 1 hour still exist. + yield promiseHistoryClearedState(olderURIs, false); + ensureDownloadsClearedState(olderDownloadIDs, false); + + // OK, done, cleanup after ourselves. + blankSlate(); + yield promiseHistoryClearedState(olderURIs, true); + ensureDownloadsClearedState(olderDownloadIDs, true); + }); + }); + }, + + /** + * Ensures that the combined history-downloads checkbox removes neither + * history visits nor downloads when not checked. + */ + function () { + // Add history, downloads, form entries (within the past hour). + let uris = []; + let places = []; + let pURI; + for (let i = 0; i < 5; i++) { + pURI = makeURI("http://" + i + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(i)}); + uris.push(pURI); + } + + addVisits(places, function() { + let downloadIDs = []; + for (let i = 0; i < 5; i++) { + downloadIDs.push(addDownloadWithMinutesAgo(i)); + } + let formEntries = []; + for (let i = 0; i < 5; i++) { + formEntries.push(addFormEntryWithMinutesAgo(i)); + } + + // Open the dialog and do our tests. + openWindow(function (aWin) { + let wh = new WindowHelper(aWin); + wh.selectDuration(Sanitizer.TIMESPAN_HOUR); + wh.checkGrippy("Grippy should be at last row after selecting HOUR " + + "duration", + wh.getRowCount() - 1); + + // Remove only form entries, leave history (including downloads). + wh.checkPrefCheckbox("history", false); + wh.checkPrefCheckbox("formdata", true); + wh.acceptDialog(); + + // Of the three only form entries should be cleared. + yield promiseHistoryClearedState(uris, false); + ensureDownloadsClearedState(downloadIDs, false); + ensureFormEntriesClearedState(formEntries, true); + + // OK, done, cleanup after ourselves. + blankSlate(); + yield promiseHistoryClearedState(uris, true); + ensureDownloadsClearedState(downloadIDs, true); + }); + }); + }, + + /** + * Ensures that the "Everything" duration option works. + */ + function () { + // Add history. + let uris = []; + let places = []; + let pURI; + // within past hour, within past two hours, within past four hours and + // outside past four hours + [10, 70, 130, 250].forEach(function(aValue) { + pURI = makeURI("http://" + aValue + "-minutes-ago.com/"); + places.push({uri: pURI, visitDate: visitTimeForMinutesAgo(aValue)}); + uris.push(pURI); + }); + addVisits(places, function() { + + // Open the dialog and do our tests. + openWindow(function (aWin) { + let wh = new WindowHelper(aWin); + wh.selectDuration(Sanitizer.TIMESPAN_EVERYTHING); + wh.checkPrefCheckbox("history", true); + wh.acceptDialog(); + yield promiseHistoryClearedState(uris, true); + }); + }); + } +]; + +// Used as the download database ID for a new download. Incremented for each +// new download. See addDownloadWithMinutesAgo(). +var gDownloadId = 5555551; + +// Index in gAllTests of the test currently being run. Incremented for each +// test run. See doNextTest(). +var gCurrTest = 0; + +var now_uSec = Date.now() * 1000; + +/////////////////////////////////////////////////////////////////////////////// + +/** + * This wraps the dialog and provides some convenience methods for interacting + * with it. + * + * A warning: Before you call any function that uses the tree (or any function + * that calls a function that uses the tree), you must set a non-everything + * duration by calling selectDuration(). The dialog does not initialize the + * tree if it does not yet need to be shown. + * + * @param aWin + * The dialog's nsIDOMWindow + */ +function WindowHelper(aWin) { + this.win = aWin; +} + +WindowHelper.prototype = { + /** + * "Presses" the dialog's OK button. + */ + acceptDialog: function () { + is(this.win.document.documentElement.getButton("accept").disabled, false, + "Dialog's OK button should not be disabled"); + this.win.document.documentElement.acceptDialog(); + }, + + /** + * "Presses" the dialog's Cancel button. + */ + cancelDialog: function () { + this.win.document.documentElement.cancelDialog(); + }, + + /** + * Ensures that the grippy row is in the right place, tree selection is OK, + * and that the grippy's visible. + * + * @param aMsg + * Passed to is() when checking grippy location + * @param aExpectedRow + * The row that the grippy should be at + */ + checkGrippy: function (aMsg, aExpectedRow) { + is(this.getGrippyRow(), aExpectedRow, aMsg); + this.checkTreeSelection(); + this.ensureGrippyIsVisible(); + }, + + /** + * (Un)checks a history scope checkbox (browser & download history, + * form history, etc.). + * + * @param aPrefName + * The final portion of the checkbox's privacy.cpd.* preference name + * @param aCheckState + * True if the checkbox should be checked, false otherwise + */ + checkPrefCheckbox: function (aPrefName, aCheckState) { + var pref = "privacy.cpd." + aPrefName; + var cb = this.win.document.querySelectorAll( + "#itemList > [preference='" + pref + "']"); + is(cb.length, 1, "found checkbox for " + pref + " preference"); + if (cb[0].checked != aCheckState) + cb[0].click(); + }, + + /** + * Ensures that the tree selection is appropriate to the grippy row. (A + * single, contiguous selection should exist from the first row all the way + * to the grippy.) + */ + checkTreeSelection: function () { + let grippyRow = this.getGrippyRow(); + let sel = this.getTree().view.selection; + if (grippyRow === 0) { + is(sel.getRangeCount(), 0, + "Grippy row is 0, so no tree selection should exist"); + } + else { + is(sel.getRangeCount(), 1, + "Grippy row > 0, so only one tree selection range should exist"); + let min = {}; + let max = {}; + sel.getRangeAt(0, min, max); + is(min.value, 0, "Tree selection should start at first row"); + is(max.value, grippyRow - 1, + "Tree selection should end at row before grippy"); + } + }, + + /** + * The grippy should always be visible when it's moved directly. This method + * ensures that. + */ + ensureGrippyIsVisible: function () { + let tbo = this.getTree().treeBoxObject; + let firstVis = tbo.getFirstVisibleRow(); + let lastVis = tbo.getLastVisibleRow(); + let grippyRow = this.getGrippyRow(); + ok(firstVis <= grippyRow && grippyRow <= lastVis, + "Grippy row should be visible; this inequality should be true: " + + firstVis + " <= " + grippyRow + " <= " + lastVis); + }, + + /** + * @return The dialog's duration dropdown + */ + getDurationDropdown: function () { + return this.win.document.getElementById("sanitizeDurationChoice"); + }, + + /** + * @return The grippy row index + */ + getGrippyRow: function () { + return this.win.gContiguousSelectionTreeHelper.getGrippyRow(); + }, + + /** + * @return The tree's row count (includes the grippy row) + */ + getRowCount: function () { + return this.getTree().view.rowCount; + }, + + /** + * @return The tree + */ + getTree: function () { + return this.win.gContiguousSelectionTreeHelper.tree; + }, + + /** + * @return True if the "Everything" warning panel is visible (as opposed to + * the tree) + */ + isWarningPanelVisible: function () { + return this.win.document.getElementById("durationDeck").selectedIndex == 1; + }, + + /** + * @return True if the tree is visible (as opposed to the warning panel) + */ + isTreeVisible: function () { + return this.win.document.getElementById("durationDeck").selectedIndex == 0; + }, + + /** + * Moves the grippy one row at a time in the direction and magnitude specified. + * If aDelta < 0, moves the grippy up; if aDelta > 0, moves it down. + * + * @param aDelta + * The amount and direction to move + */ + moveGrippyBy: function (aDelta) { + if (aDelta === 0) + return; + let key = aDelta < 0 ? "UP" : "DOWN"; + let abs = Math.abs(aDelta); + let treechildren = this.getTree().treeBoxObject.treeBody; + treechildren.focus(); + for (let i = 0; i < abs; i++) { + EventUtils.sendKey(key); + } + }, + + /** + * Selects a duration in the duration dropdown. + * + * @param aDurVal + * One of the Sanitizer.TIMESPAN_* values + */ + selectDuration: function (aDurVal) { + this.getDurationDropdown().value = aDurVal; + if (aDurVal === Sanitizer.TIMESPAN_EVERYTHING) { + is(this.isTreeVisible(), false, + "Tree should not be visible for TIMESPAN_EVERYTHING"); + is(this.isWarningPanelVisible(), true, + "Warning panel should be visible for TIMESPAN_EVERYTHING"); + } + else { + is(this.isTreeVisible(), true, + "Tree should be visible for non-TIMESPAN_EVERYTHING"); + is(this.isWarningPanelVisible(), false, + "Warning panel should not be visible for non-TIMESPAN_EVERYTHING"); + } + } +}; + +/** + * Adds a download to history. + * + * @param aMinutesAgo + * The download will be downloaded this many minutes ago + */ +function addDownloadWithMinutesAgo(aMinutesAgo) { + let name = "fakefile-" + aMinutesAgo + "-minutes-ago"; + let data = { + id: gDownloadId, + name: name, + source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169", + target: name, + startTime: now_uSec - (aMinutesAgo * 60 * 1000000), + endTime: now_uSec - ((aMinutesAgo + 1) *60 * 1000000), + state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED, + currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0, + guid: "a1bcD23eF4g5" + }; + + let db = dm.DBConnection; + let stmt = db.createStatement( + "INSERT INTO moz_downloads (id, name, source, target, startTime, endTime, " + + "state, currBytes, maxBytes, preferredAction, autoResume, guid) " + + "VALUES (:id, :name, :source, :target, :startTime, :endTime, :state, " + + ":currBytes, :maxBytes, :preferredAction, :autoResume, :guid)"); + try { + for (let prop in data) { + stmt.params[prop] = data[prop]; + } + stmt.execute(); + } + finally { + stmt.reset(); + } + + is(downloadExists(gDownloadId), true, + "Sanity check: download " + gDownloadId + + " should exist after creating it"); + + return gDownloadId++; +} + +/** + * Adds a form entry to history. + * + * @param aMinutesAgo + * The entry will be added this many minutes ago + */ +function addFormEntryWithMinutesAgo(aMinutesAgo) { + let name = aMinutesAgo + "-minutes-ago"; + formhist.addEntry(name, "dummy"); + + // Artifically age the entry to the proper vintage. + let db = formhist.DBConnection; + let timestamp = now_uSec - (aMinutesAgo * 60 * 1000000); + db.executeSimpleSQL("UPDATE moz_formhistory SET firstUsed = " + + timestamp + " WHERE fieldname = '" + name + "'"); + + is(formhist.nameExists(name), true, + "Sanity check: form entry " + name + " should exist after creating it"); + return name; +} + +/** + * Removes all history visits, downloads, and form entries. + */ +function blankSlate() { + PlacesUtils.bhistory.removeAllPages(); + dm.cleanUp(); + formhist.removeAllEntries(); +} + +/** + * Checks to see if the download with the specified ID exists. + * + * @param aID + * The ID of the download to check + * @return True if the download exists, false otherwise + */ +function downloadExists(aID) +{ + let db = dm.DBConnection; + let stmt = db.createStatement( + "SELECT * " + + "FROM moz_downloads " + + "WHERE id = :id" + ); + stmt.params.id = aID; + let rows = stmt.executeStep(); + stmt.finalize(); + return !!rows; +} + +/** + * Runs the next test in the gAllTests array. If all tests have been run, + * finishes the entire suite. + */ +function doNextTest() { + if (gAllTests.length <= gCurrTest) { + blankSlate(); + waitForAsyncUpdates(finish); + } + else { + let ct = gCurrTest; + gCurrTest++; + gAllTests[ct](); + } +} + +/** + * Ensures that the specified downloads are either cleared or not. + * + * @param aDownloadIDs + * Array of download database IDs + * @param aShouldBeCleared + * True if each download should be cleared, false otherwise + */ +function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) { + let niceStr = aShouldBeCleared ? "no longer" : "still"; + aDownloadIDs.forEach(function (id) { + is(downloadExists(id), !aShouldBeCleared, + "download " + id + " should " + niceStr + " exist"); + }); +} + +/** + * Ensures that the specified form entries are either cleared or not. + * + * @param aFormEntries + * Array of form entry names + * @param aShouldBeCleared + * True if each form entry should be cleared, false otherwise + */ +function ensureFormEntriesClearedState(aFormEntries, aShouldBeCleared) { + let niceStr = aShouldBeCleared ? "no longer" : "still"; + aFormEntries.forEach(function (entry) { + is(formhist.nameExists(entry), !aShouldBeCleared, + "form entry " + entry + " should " + niceStr + " exist"); + }); +} + +/** + * Opens the sanitize dialog and runs a callback once it's finished loading. + * + * @param aOnloadCallback + * A function that will be called once the dialog has loaded + */ +function openWindow(aOnloadCallback) { + function windowObserver(aSubject, aTopic, aData) { + if (aTopic != "domwindowopened") + return; + + Services.ww.unregisterNotification(windowObserver); + let win = aSubject.QueryInterface(Ci.nsIDOMWindow); + win.addEventListener("load", function onload(event) { + win.removeEventListener("load", onload, false); + executeSoon(function () { + // Some exceptions that reach here don't reach the test harness, but + // ok()/is() do... + try { + Task.spawn(function() { + aOnloadCallback(win); + }).then(function() { + waitForAsyncUpdates(doNextTest); + }); + } + catch (exc) { + win.close(); + ok(false, "Unexpected exception: " + exc + "\n" + exc.stack); + finish(); + } + }); + }, false); + } + Services.ww.registerNotification(windowObserver); + Services.ww.openWindow(null, + "chrome://browser/content/sanitize.xul", + "Sanitize", + "chrome,titlebar,dialog,centerscreen,modal", + null); +} + +/** + * Creates a visit time. + * + * @param aMinutesAgo + * The visit will be visited this many minutes ago + */ +function visitTimeForMinutesAgo(aMinutesAgo) { + return now_uSec - (aMinutesAgo * 60 * 1000000); +} + +/////////////////////////////////////////////////////////////////////////////// + +function test() { + blankSlate(); + waitForExplicitFinish(); + // Kick off all the tests in the gAllTests array. + waitForAsyncUpdates(doNextTest); +} diff --git a/browser/base/content/test/browser_save_link-perwindowpb.js b/browser/base/content/test/browser_save_link-perwindowpb.js new file mode 100644 index 000000000..c44344761 --- /dev/null +++ b/browser/base/content/test/browser_save_link-perwindowpb.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +let tempScope = {}; +Cu.import("resource://gre/modules/NetUtil.jsm", tempScope); +let NetUtil = tempScope.NetUtil; + +// Trigger a save of a link in public mode, then trigger an identical save +// in private mode and ensure that the second request is differentiated from +// the first by checking that cookies set by the first response are not sent +// during the second request. +function triggerSave(aWindow, aCallback) { + var fileName; + let testBrowser = aWindow.gBrowser.selectedBrowser; + // This page sets a cookie if and only if a cookie does not exist yet + testBrowser.loadURI("http://mochi.test:8888/browser/browser/base/content/test/bug792517-2.html"); + testBrowser.addEventListener("pageshow", function pageShown(event) { + if (event.target.location == "about:blank") + return; + testBrowser.removeEventListener("pageshow", pageShown, false); + + executeSoon(function () { + aWindow.document.addEventListener("popupshown", function(e) contextMenuOpened(aWindow, e), false); + + var link = testBrowser.contentDocument.getElementById("fff"); + EventUtils.synthesizeMouseAtCenter(link, + { type: "contextmenu", button: 2 }, + testBrowser.contentWindow); + }); + }, false); + + function contextMenuOpened(aWindow, event) { + event.currentTarget.removeEventListener("popupshown", contextMenuOpened, false); + + // Create the folder the link will be saved into. + var destDir = createTemporarySaveDirectory(); + var destFile = destDir.clone(); + + MockFilePicker.displayDirectory = destDir; + MockFilePicker.showCallback = function(fp) { + fileName = fp.defaultString; + destFile.append (fileName); + MockFilePicker.returnFiles = [destFile]; + MockFilePicker.filterIndex = 1; // kSaveAsType_URL + }; + + mockTransferCallback = function(downloadSuccess) { + onTransferComplete(aWindow, downloadSuccess, destDir); + destDir.remove(true); + ok(!destDir.exists(), "Destination dir should be removed"); + ok(!destFile.exists(), "Destination file should be removed"); + mockTransferCallback = function(){}; + } + + // Select "Save Link As" option from context menu + var saveLinkCommand = aWindow.document.getElementById("context-savelink"); + saveLinkCommand.doCommand(); + + event.target.hidePopup(); + } + + function onTransferComplete(aWindow, downloadSuccess, destDir) { + ok(downloadSuccess, "Link should have been downloaded successfully"); + aWindow.gBrowser.removeCurrentTab(); + + executeSoon(function() aCallback()); + } +} + +function test() { + waitForExplicitFinish(); + + var windowsToClose = []; + var gNumSet = 0; + function testOnWindow(options, callback) { + var win = OpenBrowserWindow(options); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad, false); + windowsToClose.push(win); + executeSoon(function() callback(win)); + }, false); + } + + mockTransferRegisterer.register(); + + registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + windowsToClose.forEach(function(win) { + win.close(); + }); + Services.obs.removeObserver(observer, "http-on-modify-request"); + Services.obs.removeObserver(observer, "http-on-examine-response"); + }); + + function observer(subject, topic, state) { + if (topic == "http-on-modify-request") { + onModifyRequest(subject); + } else if (topic == "http-on-examine-response") { + onExamineResponse(subject); + } + } + + function onExamineResponse(subject) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (channel.URI.spec != "http://mochi.test:8888/browser/browser/base/content/test/bug792517.sjs") { + return; + } + try { + let cookies = channel.getResponseHeader("set-cookie"); + // From browser/base/content/test/bug792715.sjs, we receive a Set-Cookie + // header with foopy=1 when there are no cookies for that domain. + is(cookies, "foopy=1", "Cookie should be foopy=1"); + gNumSet += 1; + } catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { } + } + + function onModifyRequest(subject) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + if (channel.URI.spec != "http://mochi.test:8888/browser/browser/base/content/test/bug792517.sjs") { + return; + } + try { + let cookies = channel.getRequestHeader("cookie"); + // From browser/base/content/test/bug792715.sjs, we should never send a + // cookie because we are making only 2 requests: one in public mode, and + // one in private mode. + throw "We should never send a cookie in this test"; + } catch (ex if ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { } + } + + Services.obs.addObserver(observer, "http-on-modify-request", false); + Services.obs.addObserver(observer, "http-on-examine-response", false); + + testOnWindow(undefined, function(win) { + // The first save from a regular window sets a cookie. + triggerSave(win, function() { + is(gNumSet, 1, "1 cookie should be set"); + + // The second save from a private window also sets a cookie. + testOnWindow({private: true}, function(win) { + triggerSave(win, function() { + is(gNumSet, 2, "2 cookies should be set"); + finish(); + }); + }); + }); + }); +} + +Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this); + +function createTemporarySaveDirectory() { + var saveDir = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755); + return saveDir; +} diff --git a/browser/base/content/test/browser_save_private_link_perwindowpb.js b/browser/base/content/test/browser_save_private_link_perwindowpb.js new file mode 100644 index 000000000..7365ab1b0 --- /dev/null +++ b/browser/base/content/test/browser_save_private_link_perwindowpb.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +function test() { + // initialization + waitForExplicitFinish(); + let windowsToClose = []; + let testURI = "http://mochi.test:8888/browser/browser/base/content/test/bug792517.html"; + let fileName; + let MockFilePicker = SpecialPowers.MockFilePicker; + let cache = Cc["@mozilla.org/network/cache-service;1"] + .getService(Ci.nsICacheService); + + function checkDiskCacheFor(filename) { + let visitor = { + visitDevice: function(deviceID, deviceInfo) { + if (deviceID == "disk") + info(deviceID + " device contains " + deviceInfo.entryCount + " entries"); + return deviceID == "disk"; + }, + + visitEntry: function(deviceID, entryInfo) { + info(entryInfo.key); + is(entryInfo.key.contains(filename), false, "web content present in disk cache"); + } + }; + cache.visitEntries(visitor); + } + + function contextMenuOpened(aWindow, event) { + cache.evictEntries(Ci.nsICache.STORE_ANYWHERE); + + event.currentTarget.removeEventListener("popupshown", contextMenuOpened); + + // Create the folder the image will be saved into. + var destDir = createTemporarySaveDirectory(); + var destFile = destDir.clone(); + + MockFilePicker.displayDirectory = destDir; + MockFilePicker.showCallback = function(fp) { + fileName = fp.defaultString; + destFile.append (fileName); + MockFilePicker.returnFiles = [destFile]; + MockFilePicker.filterIndex = 1; // kSaveAsType_URL + }; + + mockTransferCallback = onTransferComplete; + mockTransferRegisterer.register(); + + registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + destDir.remove(true); + }); + + // Select "Save Image As" option from context menu + var saveVideoCommand = aWindow.document.getElementById("context-saveimage"); + saveVideoCommand.doCommand(); + + event.target.hidePopup(); + } + + function onTransferComplete(downloadSuccess) { + ok(downloadSuccess, "Image file should have been downloaded successfully"); + + // Give the request a chance to finish and create a cache entry + executeSoon(function() { + checkDiskCacheFor(fileName); + finish(); + }); + } + + function createTemporarySaveDirectory() { + var saveDir = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755); + return saveDir; + } + + function doTest(aIsPrivateMode, aWindow, aCallback) { + aWindow.gBrowser.addEventListener("pageshow", function pageShown(event) { + // If data: -url PAC file isn't loaded soon enough, we may get about:privatebrowsing loaded + if (event.target.location == "about:blank" || + event.target.location == "about:privatebrowsing") { + aWindow.gBrowser.selectedBrowser.loadURI(testURI); + return; + } + aWindow.gBrowser.removeEventListener("pageshow", pageShown); + + executeSoon(function () { + aWindow.document.addEventListener("popupshown", + function(e) contextMenuOpened(aWindow, e), false); + var img = aWindow.gBrowser.selectedBrowser.contentDocument.getElementById("img"); + EventUtils.synthesizeMouseAtCenter(img, + { type: "contextmenu", button: 2 }, + aWindow.gBrowser.contentWindow); + }); + }); + } + + function testOnWindow(aOptions, aCallback) { + whenNewWindowLoaded(aOptions, function(aWin) { + windowsToClose.push(aWin); + // execute should only be called when need, like when you are opening + // web pages on the test. If calling executeSoon() is not necesary, then + // call whenNewWindowLoaded() instead of testOnWindow() on your test. + executeSoon(function() aCallback(aWin)); + }); + }; + + // this function is called after calling finish() on the test. + registerCleanupFunction(function() { + windowsToClose.forEach(function(aWin) { + aWin.close(); + }); + }); + + MockFilePicker.init(window); + // then test when on private mode + testOnWindow({private: true}, function(aWin) { + doTest(true, aWin, finish); + }); +} + +Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this); diff --git a/browser/base/content/test/browser_save_video.js b/browser/base/content/test/browser_save_video.js new file mode 100644 index 000000000..fa00ea37b --- /dev/null +++ b/browser/base/content/test/browser_save_video.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +/** + * TestCase for bug 564387 + * <https://bugzilla.mozilla.org/show_bug.cgi?id=564387> + */ +function test() { + waitForExplicitFinish(); + var fileName; + + gBrowser.loadURI("http://mochi.test:8888/browser/browser/base/content/test/bug564387.html"); + + registerCleanupFunction(function () { + gBrowser.addTab(); + gBrowser.removeCurrentTab(); + }); + + gBrowser.addEventListener("pageshow", function pageShown(event) { + if (event.target.location == "about:blank") + return; + gBrowser.removeEventListener("pageshow", pageShown); + + executeSoon(function () { + document.addEventListener("popupshown", contextMenuOpened); + + var video1 = gBrowser.contentDocument.getElementById("video1"); + EventUtils.synthesizeMouseAtCenter(video1, + { type: "contextmenu", button: 2 }, + gBrowser.contentWindow); + }); + }); + + function contextMenuOpened(event) { + event.currentTarget.removeEventListener("popupshown", contextMenuOpened); + + // Create the folder the video will be saved into. + var destDir = createTemporarySaveDirectory(); + var destFile = destDir.clone(); + + MockFilePicker.displayDirectory = destDir; + MockFilePicker.showCallback = function(fp) { + fileName = fp.defaultString; + destFile.append (fileName); + MockFilePicker.returnFiles = [destFile]; + MockFilePicker.filterIndex = 1; // kSaveAsType_URL + }; + + mockTransferCallback = onTransferComplete; + mockTransferRegisterer.register(); + + registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + destDir.remove(true); + }); + + // Select "Save Video As" option from context menu + var saveVideoCommand = document.getElementById("context-savevideo"); + saveVideoCommand.doCommand(); + + event.target.hidePopup(); + } + + function onTransferComplete(downloadSuccess) { + ok(downloadSuccess, "Video file should have been downloaded successfully"); + + is(fileName, "Bug564387-expectedName.ogv", + "Video file name is correctly retrieved from Content-Disposition http header"); + + finish(); + } +} + +Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this); + +function createTemporarySaveDirectory() { + var saveDir = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + if (!saveDir.exists()) + saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755); + return saveDir; +} diff --git a/browser/base/content/test/browser_scope.js b/browser/base/content/test/browser_scope.js new file mode 100644 index 000000000..e4edac1e0 --- /dev/null +++ b/browser/base/content/test/browser_scope.js @@ -0,0 +1,4 @@ +function test() { + ok(!!gBrowser, "gBrowser exists"); + is(gBrowser, getBrowser(), "both ways of getting tabbrowser work"); +} diff --git a/browser/base/content/test/browser_selectTabAtIndex.js b/browser/base/content/test/browser_selectTabAtIndex.js new file mode 100644 index 000000000..e9a32184e --- /dev/null +++ b/browser/base/content/test/browser_selectTabAtIndex.js @@ -0,0 +1,19 @@ +function test() { + for (let i = 0; i < 9; i++) + gBrowser.addTab(); + + var isLinux = navigator.platform.indexOf("Linux") == 0; + for (let i = 9; i >= 1; i--) { + EventUtils.synthesizeKey(i.toString(), { altKey: isLinux, accelKey: !isLinux }); + + is(gBrowser.tabContainer.selectedIndex, (i == 9 ? gBrowser.tabs.length : i) - 1, + (isLinux ? "Alt" : "Accel") + "+" + i + " selects expected tab"); + } + + gBrowser.selectTabAtIndex(-3); + is(gBrowser.tabContainer.selectedIndex, gBrowser.tabs.length - 3, + "gBrowser.selectTabAtIndex(-3) selects expected tab"); + + for (let i = 0; i < 9; i++) + gBrowser.removeCurrentTab(); +} diff --git a/browser/base/content/test/browser_tabDrop.js b/browser/base/content/test/browser_tabDrop.js new file mode 100644 index 000000000..c159270f3 --- /dev/null +++ b/browser/base/content/test/browser_tabDrop.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + let newTab = gBrowser.selectedTab = gBrowser.addTab("about:blank", {skipAnimation: true}); + registerCleanupFunction(function () { + gBrowser.removeTab(newTab); + }); + + let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + let ChromeUtils = {}; + scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils); + + let tabContainer = gBrowser.tabContainer; + var receivedDropCount = 0; + function dropListener() { + receivedDropCount++; + if (receivedDropCount == triggeredDropCount) { + is(openedTabs, validDropCount, "correct number of tabs were opened"); + executeSoon(finish); + } + } + tabContainer.addEventListener("drop", dropListener, false); + registerCleanupFunction(function () { + tabContainer.removeEventListener("drop", dropListener, false); + }); + + var openedTabs = 0; + function tabOpenListener(e) { + openedTabs++; + let tab = e.target; + executeSoon(function () { + gBrowser.removeTab(tab); + }); + } + + tabContainer.addEventListener("TabOpen", tabOpenListener, false); + registerCleanupFunction(function () { + tabContainer.removeEventListener("TabOpen", tabOpenListener, false); + }); + + var triggeredDropCount = 0; + var validDropCount = 0; + function drop(text, valid) { + triggeredDropCount++; + if (valid) + validDropCount++; + executeSoon(function () { + // A drop type of "link" onto an existing tab would normally trigger a + // load in that same tab, but tabbrowser code in _getDragTargetTab treats + // drops on the outer edges of a tab differently (loading a new tab + // instead). The events created by synthesizeDrop have all of their + // coordinates set to 0 (screenX/screenY), so they're treated as drops + // on the outer edge of the tab, thus they open new tabs. + ChromeUtils.synthesizeDrop(newTab, newTab, [[{type: "text/plain", data: text}]], "link", window); + }); + } + + // Begin and end with valid drops to make sure we wait for all drops before + // ending the test + drop("mochi.test/first", true); + drop("javascript:'bad'"); + drop("jAvascript:'bad'"); + drop("search this", true); + drop("mochi.test/second", true); + drop("data:text/html,bad"); + drop("mochi.test/third", true); +} diff --git a/browser/base/content/test/browser_tabMatchesInAwesomebar_perwindowpb.js b/browser/base/content/test/browser_tabMatchesInAwesomebar_perwindowpb.js new file mode 100644 index 000000000..ef3344842 --- /dev/null +++ b/browser/base/content/test/browser_tabMatchesInAwesomebar_perwindowpb.js @@ -0,0 +1,249 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim:set ts=2 sw=2 sts=2 et: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 TEST_URL_BASES = [ + "http://example.org/browser/browser/base/content/test/dummy_page.html#tabmatch", + "http://example.org/browser/browser/base/content/test/moz.png#tabmatch" +]; + +var gController = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + +var gTabWaitCount = 0; +var gTabCounter = 0; + +var gTestSteps = [ + function() { + info("Running step 1"); + for (let i = 0; i < 10; i++) { + let tab = gBrowser.addTab(); + loadTab(tab, TEST_URL_BASES[0] + (++gTabCounter)); + } + }, + function() { + info("Running step 2"); + gBrowser.selectTabAtIndex(1); + gBrowser.removeCurrentTab(); + gBrowser.selectTabAtIndex(1); + gBrowser.removeCurrentTab(); + for (let i = 1; i < gBrowser.tabs.length; i++) + loadTab(gBrowser.tabs[i], TEST_URL_BASES[1] + (++gTabCounter)); + }, + function() { + info("Running step 3"); + for (let i = 1; i < gBrowser.tabs.length; i++) + loadTab(gBrowser.tabs[i], TEST_URL_BASES[0] + gTabCounter); + }, + function() { + info("Running step 4 - ensure we don't register subframes as open pages"); + let tab = gBrowser.addTab(); + tab.linkedBrowser.addEventListener("load", function () { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + // Start the sub-document load. + executeSoon(function () { + tab.linkedBrowser.addEventListener("load", function (e) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + ensure_opentabs_match_db(nextStep); + }, true); + tab.linkedBrowser.contentDocument.querySelector("iframe").src = "http://test2.example.org/"; + }); + }, true); + tab.linkedBrowser.loadURI('data:text/html,<body><iframe src=""></iframe></body>'); + }, + function() { + info("Running step 5 - remove tab immediately"); + let tab = gBrowser.addTab("about:logo"); + gBrowser.removeTab(tab); + ensure_opentabs_match_db(nextStep); + }, + function() { + info("Running step 6 - check swapBrowsersAndCloseOther preserves registered switch-to-tab result"); + let tabToKeep = gBrowser.addTab(); + let tab = gBrowser.addTab(); + tab.linkedBrowser.addEventListener("load", function () { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + gBrowser.swapBrowsersAndCloseOther(tabToKeep, tab); + ensure_opentabs_match_db(function () { + gBrowser.removeTab(tabToKeep); + ensure_opentabs_match_db(nextStep); + }); + }, true); + tab.linkedBrowser.loadURI("about:mozilla"); + }, + function() { + info("Running step 7 - close all tabs"); + + Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand"); + + gBrowser.addTab("about:blank", {skipAnimation: true}); + while (gBrowser.tabs.length > 1) { + info("Removing tab: " + gBrowser.tabs[0].linkedBrowser.currentURI.spec); + gBrowser.selectTabAtIndex(0); + gBrowser.removeCurrentTab(); + } + ensure_opentabs_match_db(nextStep); + } +]; + + + +function test() { + waitForExplicitFinish(); + nextStep(); +} + +function loadTab(tab, url) { + // Because adding visits is async, we will not be notified immediately. + let visited = false; + let loaded = false; + + function maybeCheckResults() { + if (visited && loaded && --gTabWaitCount == 0) { + ensure_opentabs_match_db(nextStep); + } + } + + tab.linkedBrowser.addEventListener("load", function () { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + loaded = true; + maybeCheckResults(); + }, true); + + if (!visited) { + Services.obs.addObserver( + function (aSubject, aTopic, aData) { + if (url != aSubject.QueryInterface(Ci.nsIURI).spec) + return; + Services.obs.removeObserver(arguments.callee, aTopic); + visited = true; + maybeCheckResults(); + }, + "uri-visit-saved", + false + ); + } + + gTabWaitCount++; + info("Loading page: " + url); + tab.linkedBrowser.loadURI(url); +} + +function waitForRestoredTab(tab) { + gTabWaitCount++; + + tab.linkedBrowser.addEventListener("load", function () { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + if (--gTabWaitCount == 0) { + ensure_opentabs_match_db(nextStep); + } + }, true); +} + + +function nextStep() { + if (gTestSteps.length == 0) { + while (gBrowser.tabs.length > 1) { + gBrowser.selectTabAtIndex(1); + gBrowser.removeCurrentTab(); + } + + waitForClearHistory(finish); + + return; + } + + var stepFunc = gTestSteps.shift(); + stepFunc(); +} + +function ensure_opentabs_match_db(aCallback) { + var tabs = {}; + + var winEnum = Services.wm.getEnumerator("navigator:browser"); + while (winEnum.hasMoreElements()) { + let browserWin = winEnum.getNext(); + // skip closed-but-not-destroyed windows + if (browserWin.closed) + continue; + + for (let i = 0; i < browserWin.gBrowser.tabContainer.childElementCount; i++) { + let browser = browserWin.gBrowser.getBrowserAtIndex(i); + let url = browser.currentURI.spec; + if (browserWin.isBlankPageURL(url)) + continue; + if (!(url in tabs)) + tabs[url] = 1; + else + tabs[url]++; + } + } + + checkAutocompleteResults(tabs, aCallback); +} + +/** + * Clears history invoking callback when done. + */ +function waitForClearHistory(aCallback) { + const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished"; + let observer = { + observe: function(aSubject, aTopic, aData) { + Services.obs.removeObserver(this, TOPIC_EXPIRATION_FINISHED); + aCallback(); + } + }; + Services.obs.addObserver(observer, TOPIC_EXPIRATION_FINISHED, false); + + PlacesUtils.bhistory.removeAllPages(); +} + +function checkAutocompleteResults(aExpected, aCallback) +{ + gController.input = { + timeout: 10, + textValue: "", + searches: ["history"], + searchParam: "enable-actions", + popupOpen: false, + minResultsForPopup: 0, + invalidate: function() {}, + disableAutoComplete: false, + completeDefaultIndex: false, + get popup() { return this; }, + onSearchBegin: function() {}, + onSearchComplete: function () + { + info("Found " + gController.matchCount + " matches."); + // Check to see the expected uris and titles match up (in any order) + for (let i = 0; i < gController.matchCount; i++) { + let uri = gController.getValueAt(i).replace(/^moz-action:[^,]+,/i, ""); + + info("Search for '" + uri + "' in open tabs."); + let expected = uri in aExpected; + ok(expected, uri + " was found in autocomplete, was " + (expected ? "" : "not ") + "expected"); + // Remove the found entry from expected results. + delete aExpected[uri]; + } + + // Make sure there is no reported open page that is not open. + for (let entry in aExpected) { + ok(false, "'" + entry + "' should be found in autocomplete"); + } + + executeSoon(aCallback); + }, + setSelectedIndex: function() {}, + get searchCount() { return this.searches.length; }, + getSearchAt: function(aIndex) this.searches[aIndex], + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIAutoCompleteInput, + Ci.nsIAutoCompletePopup, + ]) + }; + + info("Searching open pages."); + gController.startSearch(Services.prefs.getCharPref("browser.urlbar.restrict.openpage")); +} diff --git a/browser/base/content/test/browser_tab_drag_drop_perwindow.js b/browser/base/content/test/browser_tab_drag_drop_perwindow.js new file mode 100644 index 000000000..f787ae860 --- /dev/null +++ b/browser/base/content/test/browser_tab_drag_drop_perwindow.js @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + //initialization + waitForExplicitFinish(); + + let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. + getService(Ci.mozIJSSubScriptLoader); + let ChromeUtils = {}; + scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils); + + function testOnWindow(aIsPrivate, aCallback) { + whenNewWindowLoaded({private: aIsPrivate}, function(win) { + executeSoon(function() aCallback(win)); + }); + } + + testOnWindow(false, function(aNormalWindow) { + testOnWindow(true, function(aPrivateWindow) { + // Open a tab in each window + let normalTab = aNormalWindow.gBrowser.addTab("about:blank", {skipAnimation: true}); + let privateTab = aPrivateWindow.gBrowser.addTab("about:blank", {skipAnimation: true}); + + let effect = ChromeUtils.synthesizeDrop(normalTab, privateTab, + [[{type: TAB_DROP_TYPE, data: normalTab}]], + null, aNormalWindow, aPrivateWindow); + is(effect, "none", "Should not be able to drag a normal tab to a private window"); + + effect = ChromeUtils.synthesizeDrop(privateTab, normalTab, + [[{type: TAB_DROP_TYPE, data: privateTab}]], + null, aPrivateWindow, aNormalWindow); + is(effect, "none", "Should not be able to drag a private tab to a normal window"); + + aNormalWindow.gBrowser.swapBrowsersAndCloseOther(normalTab, privateTab); + is(aNormalWindow.gBrowser.tabs.length, 2, "Prevent moving a normal tab to a private tabbrowser"); + is(aPrivateWindow.gBrowser.tabs.length, 2, "Prevent accepting a normal tab in a private tabbrowser"); + + aPrivateWindow.gBrowser.swapBrowsersAndCloseOther(privateTab, normalTab); + is(aPrivateWindow.gBrowser.tabs.length, 2, "Prevent moving a private tab to a normal tabbrowser"); + is(aNormalWindow.gBrowser.tabs.length, 2, "Prevent accepting a private tab in a normal tabbrowser"); + + aNormalWindow.close(); + aPrivateWindow.close(); + finish(); + }); + }); +} + diff --git a/browser/base/content/test/browser_tab_dragdrop.js b/browser/base/content/test/browser_tab_dragdrop.js new file mode 100644 index 000000000..04e4acdd7 --- /dev/null +++ b/browser/base/content/test/browser_tab_dragdrop.js @@ -0,0 +1,118 @@ +function test() +{ + var embed = '<embed type="application/x-test" allowscriptaccess="always" allowfullscreen="true" wmode="window" width="640" height="480"></embed>' + + waitForExplicitFinish(); + + // create a few tabs + var tabs = [ + gBrowser.tabs[0], + gBrowser.addTab("about:blank", {skipAnimation: true}), + gBrowser.addTab("about:blank", {skipAnimation: true}), + gBrowser.addTab("about:blank", {skipAnimation: true}), + gBrowser.addTab("about:blank", {skipAnimation: true}) + ]; + + function setLocation(i, url) { + gBrowser.getBrowserForTab(tabs[i]).contentWindow.location = url; + } + function moveTabTo(a, b) { + gBrowser.swapBrowsersAndCloseOther(gBrowser.tabs[b], gBrowser.tabs[a]); + } + function clickTest(doc, win) { + var clicks = doc.defaultView.clicks; + EventUtils.synthesizeMouseAtCenter(doc.body, {}, win); + is(doc.defaultView.clicks, clicks+1, "adding 1 more click on BODY"); + } + function test1() { + moveTabTo(2, 3); // now: 0 1 2 4 + is(gBrowser.tabs[1], tabs[1], "tab1"); + is(gBrowser.tabs[2], tabs[3], "tab3"); + + var plugin = gBrowser.getBrowserForTab(tabs[4]).docShell.contentViewer.DOMDocument.wrappedJSObject.body.firstChild; + var tab4_plugin_object = plugin.getObjectValue(); + + gBrowser.selectedTab = gBrowser.tabs[2]; + moveTabTo(3, 2); // now: 0 1 4 + gBrowser.selectedTab = tabs[4]; + var doc = gBrowser.getBrowserForTab(gBrowser.tabs[2]).docShell.contentViewer.DOMDocument.wrappedJSObject; + plugin = doc.body.firstChild; + ok(plugin && plugin.checkObjectValue(tab4_plugin_object), "same plugin instance"); + is(gBrowser.tabs[1], tabs[1], "tab1"); + is(gBrowser.tabs[2], tabs[3], "tab4"); + is(doc.defaultView.clicks, 0, "no click on BODY so far"); + clickTest(doc, window); + + moveTabTo(2, 1); // now: 0 4 + is(gBrowser.tabs[1], tabs[1], "tab1"); + doc = gBrowser.getBrowserForTab(gBrowser.tabs[1]).docShell.contentViewer.DOMDocument.wrappedJSObject; + plugin = doc.body.firstChild; + ok(plugin && plugin.checkObjectValue(tab4_plugin_object), "same plugin instance"); + clickTest(doc, window); + + // Load a new document (about:blank) in tab4, then detach that tab into a new window. + // In the new window, navigate back to the original document and click on its <body>, + // verify that its onclick was called. + var t = tabs[1]; + var b = gBrowser.getBrowserForTab(t); + gBrowser.selectedTab = t; + b.addEventListener("load", function() { + b.removeEventListener("load", arguments.callee, true); + + executeSoon(function () { + var win = gBrowser.replaceTabWithWindow(t); + whenDelayedStartupFinished(win, function () { + // Verify that the original window now only has the initial tab left in it. + is(gBrowser.tabs[0], tabs[0], "tab0"); + is(gBrowser.getBrowserForTab(gBrowser.tabs[0]).contentWindow.location, "about:blank", "tab0 uri"); + + executeSoon(function () { + win.gBrowser.addEventListener("pageshow", function () { + win.gBrowser.removeEventListener("pageshow", arguments.callee, false); + executeSoon(function () { + t = win.gBrowser.tabs[0]; + b = win.gBrowser.getBrowserForTab(t); + var doc = b.docShell.contentViewer.DOMDocument.wrappedJSObject; + clickTest(doc, win); + win.close(); + finish(); + }); + }, false); + win.gBrowser.goBack(); + }); + }); + }); + }, true); + b.loadURI("about:blank"); + + } + + var loads = 0; + function waitForLoad(event, tab, listenerContainer) { + var b = gBrowser.getBrowserForTab(gBrowser.tabs[tab]); + if (b.contentDocument != event.target) { + return; + } + gBrowser.getBrowserForTab(gBrowser.tabs[tab]).removeEventListener("load", listenerContainer.listener, true); + ++loads; + if (loads == tabs.length - 1) { + executeSoon(test1); + } + } + + function fn(f, arg) { + var listenerContainer = { listener: null } + listenerContainer.listener = function (event) { return f(event, arg, listenerContainer); }; + return listenerContainer.listener; + } + for (var i = 1; i < tabs.length; ++i) { + gBrowser.getBrowserForTab(tabs[i]).addEventListener("load", fn(waitForLoad,i), true); + } + + setLocation(1, "data:text/html;charset=utf-8,<title>tab1</title><body>tab1<iframe>"); + setLocation(2, "data:text/plain;charset=utf-8,tab2"); + setLocation(3, "data:text/html;charset=utf-8,<title>tab3</title><body>tab3<iframe>"); + setLocation(4, "data:text/html;charset=utf-8,<body onload='clicks=0' onclick='++clicks'>"+embed); + gBrowser.selectedTab = tabs[3]; + +} diff --git a/browser/base/content/test/browser_tab_dragdrop2.js b/browser/base/content/test/browser_tab_dragdrop2.js new file mode 100644 index 000000000..e2f293e85 --- /dev/null +++ b/browser/base/content/test/browser_tab_dragdrop2.js @@ -0,0 +1,52 @@ +function test() +{ + waitForExplicitFinish(); + + var level1 = false; + var level2 = false; + function test1() { + // Load the following URI (which runs some child popup tests) in a new window (B), + // then add a blank tab to B and call replaceTabWithWindow to detach the URI tab + // into yet a new window (C), then close B. + // Now run the tests again and then close C. + // The test results does not matter, all this is just to exercise some code to + // catch assertions or crashes. + var chromeroot = getRootDirectory(gTestPath); + var uri = chromeroot + "browser_tab_dragdrop2_frame1.xul"; + let window_B = openDialog(location, "_blank", "chrome,all,dialog=no,left=200,top=200,width=200,height=200", uri); + window_B.addEventListener("load", function(aEvent) { + window_B.removeEventListener("load", arguments.callee, false); + if (level1) return; level1=true; + executeSoon(function () { + window_B.gBrowser.addEventListener("load", function(aEvent) { + window_B.removeEventListener("load", arguments.callee, true); + if (level2) return; level2=true; + is(window_B.gBrowser.getBrowserForTab(window_B.gBrowser.tabs[0]).contentWindow.location, uri, "sanity check"); + //alert("1:"+window_B.gBrowser.getBrowserForTab(window_B.gBrowser.tabs[0]).contentWindow.location); + var windowB_tab2 = window_B.gBrowser.addTab("about:blank", {skipAnimation: true}); + setTimeout(function () { + //alert("2:"+window_B.gBrowser.getBrowserForTab(window_B.gBrowser.tabs[0]).contentWindow.location); + window_B.gBrowser.addEventListener("pagehide", function(aEvent) { + window_B.gBrowser.removeEventListener("pagehide", arguments.callee, true); + executeSoon(function () { + // alert("closing window_B which has "+ window_B.gBrowser.tabs.length+" tabs\n"+ + // window_B.gBrowser.getBrowserForTab(window_B.gBrowser.tabs[0]).contentWindow.location); + window_B.close(); + + var doc = window_C.gBrowser.getBrowserForTab(window_C.gBrowser.tabs[0]) + .docShell.contentViewer.DOMDocument; + var calls = doc.defaultView.test_panels(); + window_C.close(); + finish(); + }); + }, true); + window_B.gBrowser.selectedTab = window_B.gBrowser.tabs[0]; + var window_C = window_B.gBrowser.replaceTabWithWindow(window_B.gBrowser.tabs[0]); + }, 1000); // 1 second to allow the tests to create the popups + }, true); + }); + }, false); + } + + test1(); +} diff --git a/browser/base/content/test/browser_tab_dragdrop2_frame1.xul b/browser/base/content/test/browser_tab_dragdrop2_frame1.xul new file mode 100644 index 000000000..ee3e62646 --- /dev/null +++ b/browser/base/content/test/browser_tab_dragdrop2_frame1.xul @@ -0,0 +1,167 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- + XUL Widget Test for panels + --> +<window title="Titlebar" width="200" height="200" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<tree id="tree" seltype="single" width="100" height="100"> + <treecols> + <treecol flex="1"/> + <treecol flex="1"/> + </treecols> + <treechildren id="treechildren"> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + <treeitem><treerow><treecell label="One"/><treecell label="Two"/></treerow></treeitem> + </treechildren> +</tree> + + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/> + + <!-- test code goes here --> + <script type="application/javascript"><![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var currentTest = null; + +var i = 0; +var my_debug = false; +function test_panels() +{ + checkTreeCoords(); + + addEventListener("popupshown", popupShown, false); + addEventListener("popuphidden", nextTest, false); + return nextTest(); +} + +function nextTest() +{ + ok(true,"popuphidden " + i) + if (i == tests.length) { + return i; + } + + currentTest = tests[i]; + var panel = createPanel(currentTest.attrs); + currentTest.test(panel); + return i; +} + +var waitSteps = 0; +function popupShown(event) +{ + var panel = event.target; + if (waitSteps > 0 && navigator.platform.indexOf("Linux") >= 0 && + panel.boxObject.screenY == 210) { + waitSteps--; + setTimeout(popupShown, 10, event); + return; + } + ++i; + + currentTest.result(currentTest.testname + " ", panel); + panel.hidePopup(); +} + +function createPanel(attrs) +{ + var panel = document.createElement("panel"); + for (var a in attrs) { + panel.setAttribute(a, attrs[a]); + } + + var button = document.createElement("button"); + panel.appendChild(button); + button.label = "OK"; + button.width = 120; + button.height = 40; + button.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;"); + panel.setAttribute("style", "-moz-appearance: none; border: 0; margin: 0;"); + return document.documentElement.appendChild(panel); +} + +function checkTreeCoords() +{ + var tree = $("tree"); + var treechildren = $("treechildren"); + tree.currentIndex = 0; + tree.treeBoxObject.scrollToRow(0); + synthesizeMouse(treechildren, 10, tree.treeBoxObject.rowHeight + 2, { }); + + tree.treeBoxObject.scrollToRow(2); + synthesizeMouse(treechildren, 10, tree.treeBoxObject.rowHeight + 2, { }); +} + +var tests = [ + { + testname: "normal panel", + attrs: { }, + test: function(panel) { + panel.openPopupAtScreen(200, 210); + }, + result: function(testname, panel) { + if (my_debug) alert(testname); + var panelrect = panel.getBoundingClientRect(); + } + }, + { + // only noautohide panels support titlebars, so one shouldn't be shown here + testname: "autohide panel with titlebar", + attrs: { titlebar: "normal" }, + test: function(panel) { + panel.openPopupAtScreen(200, 210); + }, + result: function(testname, panel) { + if (my_debug) alert(testname); + var panelrect = panel.getBoundingClientRect(); + } + }, + { + testname: "noautohide panel with titlebar", + attrs: { noautohide: true, titlebar: "normal" }, + test: function(panel) { + waitSteps = 25; + panel.openPopupAtScreen(200, 210); + }, + result: function(testname, panel) { + if (my_debug) alert(testname); + var panelrect = panel.getBoundingClientRect(); + + var gotMouseEvent = false; + function mouseMoved(event) + { + gotMouseEvent = true; + } + + panel.addEventListener("mousemove", mouseMoved, true); + synthesizeMouse(panel, 10, 10, { type: "mousemove" }); + panel.removeEventListener("mousemove", mouseMoved, true); + + var tree = $("tree"); + tree.currentIndex = 0; + panel.appendChild(tree); + checkTreeCoords(); + } + } +]; + +SimpleTest.waitForFocus(test_panels); + +]]> +</script> + +</window> diff --git a/browser/base/content/test/browser_tabfocus.js b/browser/base/content/test/browser_tabfocus.js new file mode 100644 index 000000000..289c83c89 --- /dev/null +++ b/browser/base/content/test/browser_tabfocus.js @@ -0,0 +1,278 @@ +/* + * This test checks that focus is adjusted properly when switching tabs. + */ + +let testPage1 = "data:text/html,<html id='tab1'><body><button id='button1'>Tab 1</button></body></html>"; +let testPage2 = "data:text/html,<html id='tab2'><body><button id='button2'>Tab 2</button></body></html>"; +let testPage3 = "data:text/html,<html id='tab3'><body><button id='button3'>Tab 3</button></body></html>"; + +function test() { + waitForExplicitFinish(); + + var tab1 = gBrowser.addTab(); + var browser1 = gBrowser.getBrowserForTab(tab1); + + var tab2 = gBrowser.addTab(); + var browser2 = gBrowser.getBrowserForTab(tab2); + + gURLBar.focus(); + + var loadCount = 0; + function check() + { + // wait for both tabs to load + if (++loadCount != 2) + return; + + browser1.removeEventListener("load", check, true); + browser2.removeEventListener("load", check, true); + executeSoon(_run_focus_tests); + } + + function _run_focus_tests() { + window.focus(); + + _browser_tabfocus_test_lastfocus = gURLBar; + _browser_tabfocus_test_lastfocuswindow = window; + + window.addEventListener("focus", _browser_tabfocus_test_eventOccured, true); + window.addEventListener("blur", _browser_tabfocus_test_eventOccured, true); + + // make sure that the focus initially starts out blank + var fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + var focusedWindow = {}; + is(fm.getFocusedElementForWindow(browser1.contentWindow, false, focusedWindow), null, "initial focus in tab 1"); + is(focusedWindow.value, browser1.contentWindow, "initial frame focus in tab 1"); + is(fm.getFocusedElementForWindow(browser2.contentWindow, false, focusedWindow), null, "initial focus in tab 2"); + is(focusedWindow.value, browser2.contentWindow, "initial frame focus in tab 2"); + + expectFocusShift(function () gBrowser.selectedTab = tab2, + browser2.contentWindow, null, true, + "focusedElement after tab change, focus in new tab"); + + // switching tabs when nothing in the new tab is focused + // should focus the browser + expectFocusShift(function () gBrowser.selectedTab = tab1, + browser1.contentWindow, null, true, + "focusedElement after tab change, focus in new tab"); + + // focusing a button in the current tab should focus it + var button1 = browser1.contentDocument.getElementById("button1"); + expectFocusShift(function () button1.focus(), + browser1.contentWindow, button1, true, + "focusedWindow after focus in focused tab"); + + // focusing a button in a background tab should not change the actual + // focus, but should set the focus that would be in that background tab to + // that button. + var button2 = browser2.contentDocument.getElementById("button2"); + button2.focus(); + + expectFocusShift(function () button2.focus(), + browser1.contentWindow, button1, false, + "focusedWindow after focus in unfocused tab"); + is(fm.getFocusedElementForWindow(browser2.contentWindow, false, {}), button2, "focus in unfocused tab"); + + // switching tabs should now make the button in the other tab focused + expectFocusShift(function () gBrowser.selectedTab = tab2, + browser2.contentWindow, button2, true, + "focusedWindow after tab change"); + + // blurring an element in a background tab should not change the active + // focus, but should clear the focus in that tab. + expectFocusShift(function () button1.blur(), + browser2.contentWindow, button2, false, + "focusedWindow after blur in unfocused tab"); + is(fm.getFocusedElementForWindow(browser1.contentWindow, false, {}), null, "blur in unfocused tab"); + + // When focus is in the tab bar, it should be retained there + expectFocusShift(function () gBrowser.selectedTab.focus(), + window, gBrowser.selectedTab, true, + "focusing tab element"); + expectFocusShift(function () gBrowser.selectedTab = tab1, + window, tab1, true, + "tab change when selected tab element was focused"); + expectFocusShift(function () gBrowser.selectedTab = tab2, + window, tab2, true, + "tab change when selected tab element was focused"); + expectFocusShift(function () gBrowser.selectedTab.blur(), + window, null, true, + "blurring tab element"); + + // focusing the url field should switch active focus away from the browser but + // not clear what would be the focus in the browser + button1.focus(); + expectFocusShift(function () gURLBar.focus(), + window, gURLBar.inputField, true, + "focusedWindow after url field focused"); + is(fm.getFocusedElementForWindow(browser2.contentWindow, false, {}), button2, "url field focused, button in browser"); + expectFocusShift(function () gURLBar.blur(), + window, null, true, + "blurring url field"); + + // when a chrome element is focused, switching tabs to a tab with a button + // with the current focus should focus the button + expectFocusShift(function () gBrowser.selectedTab = tab1, + browser1.contentWindow, button1, true, + "focusedWindow after tab change, focus in url field, button focused in new tab"); + is(fm.getFocusedElementForWindow(browser2.contentWindow, false, {}), button2, "after switch tab, focus in unfocused tab"); + + // blurring an element in the current tab should clear the active focus + expectFocusShift(function () button1.blur(), + browser1.contentWindow, null, true, + "focusedWindow after blur in focused tab"); + + // blurring an non-focused url field should have no effect + expectFocusShift(function () gURLBar.blur(), + browser1.contentWindow, null, false, + "focusedWindow after blur in unfocused url field"); + + // switch focus to a tab with a currently focused element + expectFocusShift(function () gBrowser.selectedTab = tab2, + browser2.contentWindow, button2, true, + "focusedWindow after switch from unfocused to focused tab"); + + // clearing focus on the chrome window should switch the focus to the + // chrome window + expectFocusShift(function () fm.clearFocus(window), + window, null, true, + "focusedWindow after switch to chrome with no focused element"); + + // switch focus to another tab when neither have an active focus + expectFocusShift(function () gBrowser.selectedTab = tab1, + browser1.contentWindow, null, true, + "focusedWindow after tab switch from no focus to no focus"); + + gURLBar.focus(); + _browser_tabfocus_test_events = ""; + _browser_tabfocus_test_lastfocus = gURLBar; + _browser_tabfocus_test_lastfocuswindow = window; + + expectFocusShift(function () EventUtils.synthesizeKey("VK_F6", { }), + browser1.contentWindow, browser1.contentDocument.documentElement, + true, "switch document forward with f6"); + EventUtils.synthesizeKey("VK_F6", { }); + is(fm.focusedWindow, window, "switch document forward again with f6"); + + browser1.style.MozUserFocus = "ignore"; + browser1.clientWidth; + EventUtils.synthesizeKey("VK_F6", { }); + is(fm.focusedWindow, window, "switch document forward again with f6 when browser non-focusable"); + + window.removeEventListener("focus", _browser_tabfocus_test_eventOccured, true); + window.removeEventListener("blur", _browser_tabfocus_test_eventOccured, true); + + // next, check whether navigating forward, focusing the urlbar and then + // navigating back maintains the focus in the urlbar. + browser1.addEventListener("pageshow", _browser_tabfocus_navigation_test_eventOccured, true); + button1.focus(); + browser1.contentWindow.location = testPage3; + } + + browser1.addEventListener("load", check, true); + browser2.addEventListener("load", check, true); + browser1.contentWindow.location = testPage1; + browser2.contentWindow.location = testPage2; +} + +var _browser_tabfocus_test_lastfocus; +var _browser_tabfocus_test_lastfocuswindow = null; +var _browser_tabfocus_test_events = ""; + +function _browser_tabfocus_test_eventOccured(event) +{ + var id; + if (event.target instanceof Window) + id = event.originalTarget.document.documentElement.id + "-window"; + else if (event.target instanceof Document) + id = event.originalTarget.documentElement.id + "-document"; + else if (event.target.id == "urlbar" && event.originalTarget.localName == "input") + id = "urlbar"; + else + id = event.originalTarget.id; + + if (_browser_tabfocus_test_events) + _browser_tabfocus_test_events += " "; + _browser_tabfocus_test_events += event.type + ": " + id; +} + +function _browser_tabfocus_navigation_test_eventOccured(event) +{ + if (event.target instanceof Document) { + var contentwin = event.target.defaultView; + if (contentwin.location.toString().indexOf("3") > 0) { + // just moved forward, so focus the urlbar and go back + gURLBar.focus(); + setTimeout(function () contentwin.history.back(), 0); + } + else if (contentwin.location.toString().indexOf("2") > 0) { + event.currentTarget.removeEventListener("pageshow", _browser_tabfocus_navigation_test_eventOccured, true); + is(window.document.activeElement, gURLBar.inputField, "urlbar still focused after navigating back"); + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); + finish(); + } + } +} + +function getId(element) +{ + return (element.localName == "input") ? "urlbar" : element.id; +} + +function expectFocusShift(callback, expectedWindow, expectedElement, focusChanged, testid) +{ + var expectedEvents = ""; + if (focusChanged) { + if (_browser_tabfocus_test_lastfocus) + expectedEvents += "blur: " + getId(_browser_tabfocus_test_lastfocus); + + if (_browser_tabfocus_test_lastfocuswindow && + _browser_tabfocus_test_lastfocuswindow != expectedWindow) { + if (expectedEvents) + expectedEvents += " "; + var windowid = _browser_tabfocus_test_lastfocuswindow.document.documentElement.id; + expectedEvents += "blur: " + windowid + "-document " + + "blur: " + windowid + "-window"; + } + + if (expectedWindow && _browser_tabfocus_test_lastfocuswindow != expectedWindow) { + if (expectedEvents) + expectedEvents += " "; + var windowid = expectedWindow.document.documentElement.id; + expectedEvents += "focus: " + windowid + "-document " + + "focus: " + windowid + "-window"; + } + + if (expectedElement && expectedElement != expectedElement.ownerDocument.documentElement) { + if (expectedEvents) + expectedEvents += " "; + expectedEvents += "focus: " + getId(expectedElement); + } + + _browser_tabfocus_test_lastfocus = expectedElement; + _browser_tabfocus_test_lastfocuswindow = expectedWindow; + } + + callback(); + + is(_browser_tabfocus_test_events, expectedEvents, testid + " events"); + _browser_tabfocus_test_events = ""; + + var fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + + var focusedElement = fm.focusedElement; + is(focusedElement ? getId(focusedElement) : "none", + expectedElement ? getId(expectedElement) : "none", testid + " focusedElement"); + is(fm.focusedWindow, expectedWindow, testid + " focusedWindow"); + var focusedWindow = {}; + is(fm.getFocusedElementForWindow(expectedWindow, false, focusedWindow), + expectedElement, testid + " getFocusedElementForWindow"); + is(focusedWindow.value, expectedWindow, testid + " getFocusedElementForWindow frame"); + is(expectedWindow.document.hasFocus(), true, testid + " hasFocus"); + var expectedActive = expectedElement; + if (!expectedActive) + expectedActive = expectedWindow.document instanceof XULDocument ? + expectedWindow.document.documentElement : expectedWindow.document.body; + is(expectedWindow.document.activeElement, expectedActive, testid + " activeElement"); +} diff --git a/browser/base/content/test/browser_tabopen_reflows.js b/browser/base/content/test/browser_tabopen_reflows.js new file mode 100644 index 000000000..d28c6304a --- /dev/null +++ b/browser/base/content/test/browser_tabopen_reflows.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +XPCOMUtils.defineLazyGetter(this, "docShell", () => { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); +}); + +const EXPECTED_REFLOWS = [ + // tabbrowser.adjustTabstrip() call after tabopen animation has finished + "adjustTabstrip@chrome://browser/content/tabbrowser.xml|" + + "_handleNewTab@chrome://browser/content/tabbrowser.xml|" + + "onxbltransitionend@chrome://browser/content/tabbrowser.xml|", + + // switching focus in updateCurrentBrowser() causes reflows + "updateCurrentBrowser@chrome://browser/content/tabbrowser.xml|" + + "onselect@chrome://browser/content/browser.xul|", + + // switching focus in openLinkIn() causes reflows + "openLinkIn@chrome://browser/content/utilityOverlay.js|" + + "openUILinkIn@chrome://browser/content/utilityOverlay.js|" + + "BrowserOpenTab@chrome://browser/content/browser.js|", + + // accessing element.scrollPosition in _fillTrailingGap() flushes layout + "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml|" + + "_fillTrailingGap@chrome://browser/content/tabbrowser.xml|" + + "_handleNewTab@chrome://browser/content/tabbrowser.xml|" + + "onxbltransitionend@chrome://browser/content/tabbrowser.xml|", + + // The TabView iframe causes reflows in the parent document. + "iQClass_height@chrome://browser/content/tabview.js|" + + "GroupItem_getContentBounds@chrome://browser/content/tabview.js|" + + "GroupItem_shouldStack@chrome://browser/content/tabview.js|" + + "GroupItem_arrange@chrome://browser/content/tabview.js|" + + "GroupItem_add@chrome://browser/content/tabview.js|" + + "GroupItems_newTab@chrome://browser/content/tabview.js|" + + "TabItem__reconnect@chrome://browser/content/tabview.js|" + + "TabItem@chrome://browser/content/tabview.js|" + + "TabItems_link@chrome://browser/content/tabview.js|" + + "@chrome://browser/content/tabview.js|" + + "addTab@chrome://browser/content/tabbrowser.xml|", + + // SessionStore.getWindowDimensions() + "ssi_getWindowDimension@resource:///modules/sessionstore/SessionStore.jsm|" + + "@resource:///modules/sessionstore/SessionStore.jsm|" + + "ssi_updateWindowFeatures@resource:///modules/sessionstore/SessionStore.jsm|" + + "ssi_collectWindowData@resource:///modules/sessionstore/SessionStore.jsm|" + + "@resource:///modules/sessionstore/SessionStore.jsm|" + + "ssi_forEachBrowserWindow@resource:///modules/sessionstore/SessionStore.jsm|" + + "ssi_getCurrentState@resource:///modules/sessionstore/SessionStore.jsm|" + + "ssi_saveState@resource:///modules/sessionstore/SessionStore.jsm|" + + "ssi_onTimerCallback@resource:///modules/sessionstore/SessionStore.jsm|" + + "ssi_observe@resource:///modules/sessionstore/SessionStore.jsm|", + + // tabPreviews.capture() + "tabPreviews_capture@chrome://browser/content/browser.js|" + + "tabPreviews_handleEvent/<@chrome://browser/content/browser.js|" +]; + +const PREF_PRELOAD = "browser.newtab.preload"; + +/* + * This test ensures that there are no unexpected + * uninterruptible reflows when opening new tabs. + */ +function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref(PREF_PRELOAD, false); + registerCleanupFunction(() => Services.prefs.clearUserPref(PREF_PRELOAD)); + + // Add a reflow observer and open a new tab. + docShell.addWeakReflowObserver(observer); + BrowserOpenTab(); + + // Wait until the tabopen animation has finished. + waitForTransitionEnd(function () { + // Remove reflow observer and clean up. + docShell.removeWeakReflowObserver(observer); + gBrowser.removeCurrentTab(); + + finish(); + }); +} + +let observer = { + reflow: function (start, end) { + // Gather information about the current code path. + let path = (new Error().stack).split("\n").slice(1).map(line => { + return line.replace(/:\d+$/, ""); + }).join("|"); + + // Stack trace is empty. Reflow was triggered by native code. + if (path === "") { + return; + } + + // Check if this is an expected reflow. + for (let stack of EXPECTED_REFLOWS) { + if (path.startsWith(stack)) { + ok(true, "expected uninterruptible reflow '" + stack + "'"); + return; + } + } + + ok(false, "unexpected uninterruptible reflow '" + path + "'"); + }, + + reflowInterruptible: function (start, end) { + // We're not interested in interruptible reflows. + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver, + Ci.nsISupportsWeakReference]) +}; + +function waitForTransitionEnd(callback) { + let tab = gBrowser.selectedTab; + tab.addEventListener("transitionend", function onEnd(event) { + if (event.propertyName === "max-width") { + tab.removeEventListener("transitionend", onEnd); + executeSoon(callback); + } + }); +} diff --git a/browser/base/content/test/browser_tabs_isActive.js b/browser/base/content/test/browser_tabs_isActive.js new file mode 100644 index 000000000..57d86e821 --- /dev/null +++ b/browser/base/content/test/browser_tabs_isActive.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + test_tab("about:blank"); + test_tab("about:license"); +} + +function test_tab(url) { + let originalTab = gBrowser.selectedTab; + let newTab = gBrowser.addTab(url, {skipAnimation: true}); + is(tabIsActive(newTab), false, "newly added " + url + " tab is not active"); + is(tabIsActive(originalTab), true, "original tab is active initially"); + + gBrowser.selectedTab = newTab; + is(tabIsActive(newTab), true, "newly added " + url + " tab is active after selection"); + is(tabIsActive(originalTab), false, "original tab is not active while unselected"); + + gBrowser.selectedTab = originalTab; + is(tabIsActive(newTab), false, "newly added " + url + " tab is not active after switch back"); + is(tabIsActive(originalTab), true, "original tab is active again after switch back"); + + gBrowser.removeTab(newTab); +} + +function tabIsActive(tab) { + let browser = tab.linkedBrowser; + return browser.docShell.isActive; +} diff --git a/browser/base/content/test/browser_tabs_owner.js b/browser/base/content/test/browser_tabs_owner.js new file mode 100644 index 000000000..d432eab24 --- /dev/null +++ b/browser/base/content/test/browser_tabs_owner.js @@ -0,0 +1,32 @@ +function test() { + gBrowser.addTab(); + gBrowser.addTab(); + gBrowser.addTab(); + + var tabs = gBrowser.tabs; + var owner; + + is(tabs.length, 4, "4 tabs are open"); + + owner = gBrowser.selectedTab = tabs[2]; + BrowserOpenTab(); + is(gBrowser.selectedTab, tabs[4], "newly opened tab is selected"); + gBrowser.removeCurrentTab(); + is(gBrowser.selectedTab, owner, "owner is selected"); + + owner = gBrowser.selectedTab; + BrowserOpenTab(); + gBrowser.selectedTab = tabs[1]; + gBrowser.selectedTab = tabs[4]; + gBrowser.removeCurrentTab(); + isnot(gBrowser.selectedTab, owner, "selecting a different tab clears the owner relation"); + + owner = gBrowser.selectedTab; + BrowserOpenTab(); + gBrowser.moveTabTo(gBrowser.selectedTab, 0); + gBrowser.removeCurrentTab(); + is(gBrowser.selectedTab, owner, "owner relatitionship persists when tab is moved"); + + while (tabs.length > 1) + gBrowser.removeCurrentTab(); +} diff --git a/browser/base/content/test/browser_typeAheadFind.js b/browser/base/content/test/browser_typeAheadFind.js new file mode 100644 index 000000000..507a63b26 --- /dev/null +++ b/browser/base/content/test/browser_typeAheadFind.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let testWindow = null; + +function test() { + waitForExplicitFinish(); + + testWindow = OpenBrowserWindow(); + whenDelayedStartupFinished(testWindow, function () { + let selectedBrowser = testWindow.gBrowser.selectedBrowser; + selectedBrowser.addEventListener("load", function onLoad() { + selectedBrowser.removeEventListener("load", onLoad, true); + ok(true, "load listener called"); + waitForFocus(onFocus, testWindow.content); + }, true); + testWindow.gBrowser.loadURI("data:text/html,<h1>A Page</h1>"); + }); +} + +function onFocus() { + ok(!testWindow.gFindBarInitialized, "find bar is not initialized"); + EventUtils.synthesizeKey("/", {}, testWindow); + ok(testWindow.gFindBarInitialized, "find bar is now initialized"); + testWindow.close(); + finish(); +} diff --git a/browser/base/content/test/browser_unloaddialogs.js b/browser/base/content/test/browser_unloaddialogs.js new file mode 100644 index 000000000..b8dca5447 --- /dev/null +++ b/browser/base/content/test/browser_unloaddialogs.js @@ -0,0 +1,134 @@ +function notify(event) +{ + if (event.target.location == "about:blank") + return; + + var eventname = event.type; + if (eventname == "pagehide") + details.pagehides++; + else if (eventname == "beforeunload") + details.beforeunloads++; + else if (eventname == "unload") + details.unloads++; +} + +var details; + +var gUseFrame = false; + +const windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator); + +const TEST_BASE_URL = "data:text/html,<script>" + + "function note(event) { try { alert(event.type); } catch(ex) { return; } throw 'alert appeared'; }" + + "</script>" + + "<body onpagehide='note(event)' onbeforeunload='alert(event.type);' onunload='note(event)'>"; + +const TEST_URL = TEST_BASE_URL + "Test</body>"; +const TEST_FRAME_URL = TEST_BASE_URL + "Frames</body>"; + +function test() +{ + waitForExplicitFinish(); + windowMediator.addListener(promptListener); + runTest(); +} + +function runTest() +{ + details = { + testNumber : 0, + beforeunloads : 0, + pagehides : 0, + unloads : 0, + prompts : 0 + }; + + var tab = gBrowser.addTab(TEST_URL); + gBrowser.selectedTab = tab; + tab.linkedBrowser.addEventListener("pageshow", shown, true); + + tab.linkedBrowser.addEventListener("pagehide", notify, true); + tab.linkedBrowser.addEventListener("beforeunload", notify, true); + tab.linkedBrowser.addEventListener("unload", notify, true); +} + +function shown(event) +{ + if (details.testNumber == 0) { + var browser; + var iframe; + if (gUseFrame) { + iframe = event.target.createElement("iframe"); + iframe.src = TEST_FRAME_URL; + event.target.documentElement.appendChild(iframe); + browser = iframe.contentWindow; + } + else { + browser = gBrowser.selectedTab.linkedBrowser; + details.testNumber = 1; // Move onto to the next step immediately + } + } + + if (details.testNumber == 1) { + // Test going to another page + executeSoon(function () { + const urlToLoad = "data:text/html,<body>Another Page</body>"; + if (gUseFrame) { + event.target.location = urlToLoad; + } + else { + gBrowser.selectedBrowser.loadURI(urlToLoad); + } + }); + } + else if (details.testNumber == 2) { + is(details.pagehides, 1, "pagehides after next page") + is(details.beforeunloads, 1, "beforeunloads after next page") + is(details.unloads, 1, "unloads after next page") + is(details.prompts, 1, "prompts after next page") + + executeSoon(function () gUseFrame ? gBrowser.goBack() : event.target.defaultView.back()); + } + else if (details.testNumber == 3) { + is(details.pagehides, 2, "pagehides after back") + is(details.beforeunloads, 2, "beforeunloads after back") + // No cache, so frame is unloaded + is(details.unloads, gUseFrame ? 2 : 1, "unloads after back") + is(details.prompts, 1, "prompts after back") + + // Test closing the tab + gBrowser.selectedBrowser.removeEventListener("pageshow", shown, true); + gBrowser.removeTab(gBrowser.selectedTab); + + // When the frame is present, there is are two beforeunload and prompts, + // one for the frame and the other for the parent. + is(details.pagehides, 3, "pagehides after close") + is(details.beforeunloads, gUseFrame ? 4 : 3, "beforeunloads after close") + is(details.unloads, gUseFrame ? 3 : 2, "unloads after close") + is(details.prompts, gUseFrame ? 3 : 2, "prompts after close") + + // Now run the test again using a child frame. + if (gUseFrame) { + windowMediator.removeListener(promptListener); + finish(); + } + else { + gUseFrame = true; + runTest(); + } + + return; + } + + details.testNumber++; +} + +var promptListener = { + onWindowTitleChange: function () {}, + onOpenWindow: function (win) { + details.prompts++; + let domWin = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow); + executeSoon(function () { domWin.close() }); + }, + onCloseWindow: function () {}, +}; diff --git a/browser/base/content/test/browser_urlHighlight.js b/browser/base/content/test/browser_urlHighlight.js new file mode 100644 index 000000000..3ab312738 --- /dev/null +++ b/browser/base/content/test/browser_urlHighlight.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function testVal(aExpected) { + gURLBar.value = aExpected.replace(/[<>]/g, ""); + + let selectionController = gURLBar.editor.selectionController; + let selection = selectionController.getSelection(selectionController.SELECTION_URLSECONDARY); + let value = gURLBar.editor.rootElement.textContent; + let result = ""; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i).toString(); + let pos = value.indexOf(range); + result += value.substring(0, pos) + "<" + range + ">"; + value = value.substring(pos + range.length); + } + result += value; + is(result, aExpected); +} + +function test() { + const prefname = "browser.urlbar.formatting.enabled"; + + registerCleanupFunction(function () { + Services.prefs.clearUserPref(prefname); + URLBarSetURI(); + }); + + Services.prefs.setBoolPref(prefname, true); + + gURLBar.focus(); + + testVal("https://mozilla.org"); + + gBrowser.selectedBrowser.focus(); + + testVal("<https://>mozilla.org"); + testVal("<https://>mözilla.org"); + testVal("<https://>mozilla.imaginatory"); + + testVal("<https://www.>mozilla.org"); + testVal("<https://sub.>mozilla.org"); + testVal("<https://sub1.sub2.sub3.>mozilla.org"); + testVal("<www.>mozilla.org"); + testVal("<sub.>mozilla.org"); + testVal("<sub1.sub2.sub3.>mozilla.org"); + + testVal("<http://ftp.>mozilla.org"); + testVal("<ftp://ftp.>mozilla.org"); + + testVal("<https://sub.>mozilla.org"); + testVal("<https://sub1.sub2.sub3.>mozilla.org"); + testVal("<https://user:pass@sub1.sub2.sub3.>mozilla.org"); + testVal("<https://user:pass@>mozilla.org"); + + testVal("<https://>mozilla.org</file.ext>"); + testVal("<https://>mozilla.org</sub/file.ext>"); + testVal("<https://>mozilla.org</sub/file.ext?foo>"); + testVal("<https://>mozilla.org</sub/file.ext?foo&bar>"); + testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>"); + testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>"); + + testVal("<https://sub.>mozilla.org<:666/file.ext>"); + testVal("<sub.>mozilla.org<:666/file.ext>"); + testVal("localhost<:666/file.ext>"); + + let IPs = ["192.168.1.1", + "[::]", + "[::1]", + "[1::]", + "[::]", + "[::1]", + "[1::]", + "[1:2:3:4:5:6:7::]", + "[::1:2:3:4:5:6:7]", + "[1:2:a:B:c:D:e:F]", + "[1::8]", + "[1:2::8]", + "[fe80::222:19ff:fe11:8c76]", + "[0000:0123:4567:89AB:CDEF:abcd:ef00:0000]", + "[::192.168.1.1]", + "[1::0.0.0.0]", + "[1:2::255.255.255.255]", + "[1:2:3::255.255.255.255]", + "[1:2:3:4::255.255.255.255]", + "[1:2:3:4:5::255.255.255.255]", + "[1:2:3:4:5:6:255.255.255.255]"]; + IPs.forEach(function (IP) { + testVal(IP); + testVal(IP + "</file.ext>"); + testVal(IP + "<:666/file.ext>"); + testVal("<https://>" + IP); + testVal("<https://>" + IP + "</file.ext>"); + testVal("<https://user:pass@>" + IP + "<:666/file.ext>"); + testVal("<http://user:pass@>" + IP + "<:666/file.ext>"); + }); + + testVal("mailto:admin@mozilla.org"); + testVal("gopher://mozilla.org/"); + testVal("about:config"); + testVal("jar:http://mozilla.org/example.jar!/"); + testVal("view-source:http://mozilla.org/"); + testVal("foo9://mozilla.org/"); + testVal("foo+://mozilla.org/"); + testVal("foo.://mozilla.org/"); + testVal("foo-://mozilla.org/"); + + Services.prefs.setBoolPref(prefname, false); + + testVal("https://mozilla.org"); +} diff --git a/browser/base/content/test/browser_urlbarAutoFillTrimURLs.js b/browser/base/content/test/browser_urlbarAutoFillTrimURLs.js new file mode 100644 index 000000000..3219898a9 --- /dev/null +++ b/browser/base/content/test/browser_urlbarAutoFillTrimURLs.js @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This test ensures that autoFilled values are not trimmed, unless the user +// selects from the autocomplete popup. + +function test() { + waitForExplicitFinish(); + + const PREF_TRIMURL = "browser.urlbar.trimURLs"; + const PREF_AUTOFILL = "browser.urlbar.autoFill"; + + registerCleanupFunction(function () { + Services.prefs.clearUserPref(PREF_TRIMURL); + Services.prefs.clearUserPref(PREF_AUTOFILL); + gURLBar.handleRevert(); + }); + Services.prefs.setBoolPref(PREF_TRIMURL, true); + Services.prefs.setBoolPref(PREF_AUTOFILL, true); + + // Adding a tab would hit switch-to-tab, so it's safer to just add a visit. + let callback = { + handleError: function () {}, + handleResult: function () {}, + handleCompletion: continue_test + }; + let history = Cc["@mozilla.org/browser/history;1"] + .getService(Ci.mozIAsyncHistory); + history.updatePlaces({ uri: NetUtil.newURI("http://www.autofilltrimurl.com/") + , visits: [ { transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED + , visitDate: Date.now() * 1000 + } ] + }, callback); +} + +function continue_test() { + function test_autoFill(aTyped, aExpected, aCallback) { + gURLBar.inputField.value = aTyped.substr(0, aTyped.length - 1); + gURLBar.focus(); + gURLBar.selectionStart = aTyped.length - 1; + gURLBar.selectionEnd = aTyped.length - 1; + + EventUtils.synthesizeKey(aTyped.substr(-1), {}); + is(gURLBar.value, aExpected, "trim was applied correctly"); + + aCallback(); + } + + test_autoFill("http://", "http://", function () { + test_autoFill("http://a", "http://autofilltrimurl.com/", function () { + test_autoFill("http://www.autofilltrimurl.com", "http://www.autofilltrimurl.com/", function () { + // Now ensure selecting from the popup correctly trims. + waitForSearchComplete(function () { + EventUtils.synthesizeKey("VK_DOWN", {}); + is(gURLBar.value, "www.autofilltrimurl.com", "trim was applied correctly"); + gURLBar.closePopup(); + waitForClearHistory(finish); + }); + }); + }); + }); +} + +function waitForClearHistory(aCallback) { + Services.obs.addObserver(function observeCH(aSubject, aTopic, aData) { + Services.obs.removeObserver(observeCH, PlacesUtils.TOPIC_EXPIRATION_FINISHED); + aCallback(); + }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); + PlacesUtils.bhistory.removeAllPages(); +} + +function waitForSearchComplete(aCallback) { + info("Waiting for onSearchComplete"); + let onSearchComplete = gURLBar.onSearchComplete; + registerCleanupFunction(function () { + gURLBar.onSearchComplete = onSearchComplete; + }); + gURLBar.onSearchComplete = function () { + ok(gURLBar.popupOpen, "The autocomplete popup is correctly open"); + is(gURLBar.controller.matchCount, 1, "Found the expected number of matches") + onSearchComplete.apply(gURLBar); + aCallback(); + } +} diff --git a/browser/base/content/test/browser_urlbarCopying.js b/browser/base/content/test/browser_urlbarCopying.js new file mode 100644 index 000000000..7f0a511f8 --- /dev/null +++ b/browser/base/content/test/browser_urlbarCopying.js @@ -0,0 +1,204 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const trimPref = "browser.urlbar.trimURLs"; +const phishyUserPassPref = "network.http.phishy-userpass-length"; + +function test() { + + let tab = gBrowser.selectedTab = gBrowser.addTab(); + + registerCleanupFunction(function () { + gBrowser.removeTab(tab); + Services.prefs.clearUserPref(trimPref); + Services.prefs.clearUserPref(phishyUserPassPref); + URLBarSetURI(); + }); + + Services.prefs.setBoolPref(trimPref, true); + Services.prefs.setIntPref(phishyUserPassPref, 32); // avoid prompting about phishing + + waitForExplicitFinish(); + + nextTest(); +} + +var tests = [ + // pageproxystate="invalid" + { + setURL: "http://example.com/", + expectedURL: "example.com", + copyExpected: "example.com" + }, + { + copyVal: "<e>xample.com", + copyExpected: "e" + }, + + // pageproxystate="valid" from this point on (due to the load) + { + loadURL: "http://example.com/", + expectedURL: "example.com", + copyExpected: "http://example.com/" + }, + { + copyVal: "<example.co>m", + copyExpected: "example.co" + }, + { + copyVal: "e<x>ample.com", + copyExpected: "x" + }, + { + copyVal: "<e>xample.com", + copyExpected: "e" + }, + + { + loadURL: "http://example.com/foo", + expectedURL: "example.com/foo", + copyExpected: "http://example.com/foo" + }, + { + copyVal: "<example.com>/foo", + copyExpected: "http://example.com" + }, + { + copyVal: "<example>.com/foo", + copyExpected: "example" + }, + + // Test that userPass is stripped out + { + loadURL: "http://user:pass@mochi.test:8888/browser/browser/base/content/test/authenticate.sjs?user=user&pass=pass", + expectedURL: "mochi.test:8888/browser/browser/base/content/test/authenticate.sjs?user=user&pass=pass", + copyExpected: "http://mochi.test:8888/browser/browser/base/content/test/authenticate.sjs?user=user&pass=pass" + }, + + // Test escaping + { + loadURL: "http://example.com/()%C3%A9", + expectedURL: "example.com/()\xe9", + copyExpected: "http://example.com/%28%29%C3%A9" + }, + { + copyVal: "<example.com/(>)\xe9", + copyExpected: "http://example.com/(" + }, + { + copyVal: "e<xample.com/(>)\xe9", + copyExpected: "xample.com/(" + }, + + { + loadURL: "http://example.com/%C3%A9%C3%A9", + expectedURL: "example.com/\xe9\xe9", + copyExpected: "http://example.com/%C3%A9%C3%A9" + }, + { + copyVal: "e<xample.com/\xe9>\xe9", + copyExpected: "xample.com/\xe9" + }, + { + copyVal: "<example.com/\xe9>\xe9", + copyExpected: "http://example.com/\xe9" + }, + + { + loadURL: "http://example.com/?%C3%B7%C3%B7", + expectedURL: "example.com/?\xf7\xf7", + copyExpected: "http://example.com/?%C3%B7%C3%B7" + }, + { + copyVal: "e<xample.com/?\xf7>\xf7", + copyExpected: "xample.com/?\xf7" + }, + { + copyVal: "<example.com/?\xf7>\xf7", + copyExpected: "http://example.com/?\xf7" + }, + + // data: and javsacript: URIs shouldn't be encoded + { + loadURL: "javascript:('%C3%A9')", + expectedURL: "javascript:('\xe9')", + copyExpected: "javascript:('\xe9')" + }, + { + copyVal: "<javascript:(>'\xe9')", + copyExpected: "javascript:(" + }, + + { + loadURL: "data:text/html,(%C3%A9)", + expectedURL: "data:text/html,(\xe9)", + copyExpected: "data:text/html,(\xe9)" + }, + { + copyVal: "<data:text/html,(>\xe9)", + copyExpected: "data:text/html,(" + }, + { + copyVal: "data:<text/html,(\xe9>)", + copyExpected: "text/html,(\xe9" + } +]; + +function nextTest() { + let test = tests.shift(); + if (tests.length == 0) + runTest(test, finish); + else + runTest(test, nextTest); +} + +function runTest(test, cb) { + function doCheck() { + if (test.setURL || test.loadURL) { + gURLBar.valueIsTyped = !!test.setURL; + is(gURLBar.value, test.expectedURL, "url bar value set"); + } + + testCopy(test.copyVal, test.copyExpected, cb); + } + + if (test.loadURL) { + loadURL(test.loadURL, doCheck); + } else { + if (test.setURL) + gURLBar.value = test.setURL; + doCheck(); + } +} + +function testCopy(copyVal, targetValue, cb) { + info("Expecting copy of: " + targetValue); + waitForClipboard(targetValue, function () { + gURLBar.focus(); + if (copyVal) { + let startBracket = copyVal.indexOf("<"); + let endBracket = copyVal.indexOf(">"); + if (startBracket == -1 || endBracket == -1 || + startBracket > endBracket || + copyVal.replace("<", "").replace(">", "") != gURLBar.value) { + ok(false, "invalid copyVal: " + copyVal); + } + gURLBar.selectionStart = startBracket; + gURLBar.selectionEnd = endBracket - 1; + } else { + gURLBar.select(); + } + + goDoCommand("cmd_copy"); + }, cb, cb); +} + +function loadURL(aURL, aCB) { + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + is(gBrowser.currentURI.spec, aURL, "loaded expected URL"); + aCB(); + }, true); + + gBrowser.loadURI(aURL); +} diff --git a/browser/base/content/test/browser_urlbarEnter.js b/browser/base/content/test/browser_urlbarEnter.js new file mode 100644 index 000000000..defea1396 --- /dev/null +++ b/browser/base/content/test/browser_urlbarEnter.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_VALUE = "example.com/\xF7?\xF7"; +const START_VALUE = "example.com/%C3%B7?%C3%B7"; + +function test() { + waitForExplicitFinish(); + runNextTest(); +} + +function locationBarEnter(aEvent, aClosure) { + executeSoon(function() { + gURLBar.focus(); + EventUtils.synthesizeKey("VK_RETURN", aEvent); + addPageShowListener(aClosure); + }); +} + +function runNextTest() { + let test = gTests.shift(); + if (!test) { + finish(); + return; + } + + info("Running test: " + test.desc); + let tab = gBrowser.selectedTab = gBrowser.addTab(START_VALUE); + addPageShowListener(function() { + locationBarEnter(test.event, function() { + test.check(tab); + + // Clean up + while (gBrowser.tabs.length > 1) + gBrowser.removeTab(gBrowser.selectedTab) + runNextTest(); + }); + }); +} + +let gTests = [ + { desc: "Simple return keypress", + event: {}, + check: checkCurrent + }, + + { desc: "Alt+Return keypress", + event: { altKey: true }, + check: checkNewTab, + }, +] + +function checkCurrent(aTab) { + is(gURLBar.value, TEST_VALUE, "Urlbar should preserve the value on return keypress"); + is(gBrowser.selectedTab, aTab, "New URL was loaded in the current tab"); +} + +function checkNewTab(aTab) { + is(gURLBar.value, TEST_VALUE, "Urlbar should preserve the value on return keypress"); + isnot(gBrowser.selectedTab, aTab, "New URL was loaded in a new tab"); +} + +function addPageShowListener(aFunc) { + gBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false); + aFunc(); + }); +} + diff --git a/browser/base/content/test/browser_urlbarRevert.js b/browser/base/content/test/browser_urlbarRevert.js new file mode 100644 index 000000000..2bd596efc --- /dev/null +++ b/browser/base/content/test/browser_urlbarRevert.js @@ -0,0 +1,29 @@ +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab("http://example.com"); + gBrowser.selectedTab = tab; + + onLoad(function () { + let originalValue = gURLBar.value; + + gBrowser.userTypedValue = "foobar"; + gBrowser.selectedTab = gBrowser.tabs[0]; + gBrowser.selectedTab = tab; + is(gURLBar.value, "foobar", "location bar displays typed value"); + + gURLBar.focus(); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + is(gURLBar.value, originalValue, "ESC reverted the location bar value"); + + gBrowser.removeTab(tab); + finish(); + }); +} + +function onLoad(callback) { + gBrowser.selectedBrowser.addEventListener("pageshow", function loadListener() { + gBrowser.selectedBrowser.removeEventListener("pageshow", loadListener, false); + executeSoon(callback); + }); +} diff --git a/browser/base/content/test/browser_urlbarStop.js b/browser/base/content/test/browser_urlbarStop.js new file mode 100644 index 000000000..2d9a87a71 --- /dev/null +++ b/browser/base/content/test/browser_urlbarStop.js @@ -0,0 +1,40 @@ +const goodURL = "http://mochi.test:8888/"; +const badURL = "http://mochi.test:8888/whatever.html"; + +function test() { + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(goodURL); + gBrowser.selectedBrowser.addEventListener("load", onload, true); +} + +function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + + is(gURLBar.value, gURLBar.trimValue(goodURL), "location bar reflects loaded page"); + + typeAndSubmit(badURL); + is(gURLBar.value, gURLBar.trimValue(badURL), "location bar reflects loading page"); + + gBrowser.contentWindow.stop(); + is(gURLBar.value, gURLBar.trimValue(goodURL), "location bar reflects loaded page after stop()"); + gBrowser.removeCurrentTab(); + + gBrowser.selectedTab = gBrowser.addTab("about:blank"); + is(gURLBar.value, "", "location bar is empty"); + + typeAndSubmit(badURL); + is(gURLBar.value, gURLBar.trimValue(badURL), "location bar reflects loading page"); + + gBrowser.contentWindow.stop(); + is(gURLBar.value, gURLBar.trimValue(badURL), "location bar reflects stopped page in an empty tab"); + gBrowser.removeCurrentTab(); + + finish(); +} + +function typeAndSubmit(value) { + gBrowser.userTypedValue = value; + URLBarSetURI(); + gURLBar.handleCommand(); +} diff --git a/browser/base/content/test/browser_urlbarTrimURLs.js b/browser/base/content/test/browser_urlbarTrimURLs.js new file mode 100644 index 000000000..095ccc4a6 --- /dev/null +++ b/browser/base/content/test/browser_urlbarTrimURLs.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function testVal(originalValue, targetValue) { + gURLBar.value = originalValue; + gURLBar.valueIsTyped = false; + is(gURLBar.value, targetValue || originalValue, "url bar value set"); +} + +function test() { + const prefname = "browser.urlbar.trimURLs"; + + gBrowser.selectedTab = gBrowser.addTab(); + + registerCleanupFunction(function () { + gBrowser.removeCurrentTab(); + Services.prefs.clearUserPref(prefname); + URLBarSetURI(); + }); + + Services.prefs.setBoolPref(prefname, true); + + testVal("http://mozilla.org/", "mozilla.org"); + testVal("https://mozilla.org/", "https://mozilla.org"); + testVal("http://mözilla.org/", "mözilla.org"); + testVal("http://mozilla.imaginatory/", "mozilla.imaginatory"); + testVal("http://www.mozilla.org/", "www.mozilla.org"); + testVal("http://sub.mozilla.org/", "sub.mozilla.org"); + testVal("http://sub1.sub2.sub3.mozilla.org/", "sub1.sub2.sub3.mozilla.org"); + testVal("http://mozilla.org/file.ext", "mozilla.org/file.ext"); + testVal("http://mozilla.org/sub/", "mozilla.org/sub/"); + + testVal("http://ftp.mozilla.org/", "http://ftp.mozilla.org"); + testVal("http://ftp1.mozilla.org/", "http://ftp1.mozilla.org"); + testVal("http://ftp42.mozilla.org/", "http://ftp42.mozilla.org"); + testVal("http://ftpx.mozilla.org/", "ftpx.mozilla.org"); + testVal("ftp://ftp.mozilla.org/", "ftp://ftp.mozilla.org"); + testVal("ftp://ftp1.mozilla.org/", "ftp://ftp1.mozilla.org"); + testVal("ftp://ftp42.mozilla.org/", "ftp://ftp42.mozilla.org"); + testVal("ftp://ftpx.mozilla.org/", "ftp://ftpx.mozilla.org"); + + testVal("https://user:pass@mozilla.org/", "https://user:pass@mozilla.org"); + testVal("http://user:pass@mozilla.org/", "http://user:pass@mozilla.org"); + testVal("http://sub.mozilla.org:666/", "sub.mozilla.org:666"); + + testVal("https://[fe80::222:19ff:fe11:8c76]/file.ext"); + testVal("http://[fe80::222:19ff:fe11:8c76]/", "[fe80::222:19ff:fe11:8c76]"); + testVal("https://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext"); + testVal("http://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext"); + + testVal("mailto:admin@mozilla.org"); + testVal("gopher://mozilla.org/"); + testVal("about:config"); + testVal("jar:http://mozilla.org/example.jar!/"); + testVal("view-source:http://mozilla.org/"); + + Services.prefs.setBoolPref(prefname, false); + + testVal("http://mozilla.org/"); + + Services.prefs.setBoolPref(prefname, true); + + waitForExplicitFinish(); + + gBrowser.selectedBrowser.addEventListener("load", function () { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + is(gBrowser.currentURI.spec, "http://example.com/", "expected page should have loaded"); + + testCopy("example.com", "http://example.com/", function () { + SetPageProxyState("invalid"); + gURLBar.valueIsTyped = true; + testCopy("example.com", "example.com", finish); + }); + }, true); + + gBrowser.loadURI("http://example.com/"); +} + +function testCopy(originalValue, targetValue, cb) { + waitForClipboard(targetValue, function () { + is(gURLBar.value, originalValue, "url bar copy value set"); + + gURLBar.focus(); + gURLBar.select(); + goDoCommand("cmd_copy"); + }, cb, cb); +} diff --git a/browser/base/content/test/browser_urlbar_search_healthreport.js b/browser/base/content/test/browser_urlbar_search_healthreport.js new file mode 100644 index 000000000..4315a9864 --- /dev/null +++ b/browser/base/content/test/browser_urlbar_search_healthreport.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function test() { + waitForExplicitFinish(); + try { + let cm = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager); + cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider"); + } catch (ex) { + // Health Report disabled, or no SearchesProvider. + ok(true, "Firefox Health Report is not enabled."); + finish(); + return; + } + + let reporter = Cc["@mozilla.org/datareporting/service;1"] + .getService() + .wrappedJSObject + .healthReporter; + ok(reporter, "Health Reporter available."); + reporter.onInit().then(function onInit() { + let provider = reporter.getProvider("org.mozilla.searches"); + ok(provider, "Searches provider is available."); + let m = provider.getMeasurement("counts", 2); + + m.getValues().then(function onData(data) { + let now = new Date(); + let oldCount = 0; + + // This will to be need changed if default search engine is not Google. + let field = "google.urlbar"; + + if (data.days.hasDay(now)) { + let day = data.days.getDay(now); + if (day.has(field)) { + oldCount = day.get(field); + } + } + + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + + gURLBar.value = "firefox health report"; + gURLBar.handleCommand(); + + executeSoon(() => executeSoon(() => { + gBrowser.removeTab(tab); + + m.getValues().then(function onData(data) { + ok(data.days.hasDay(now), "FHR has data for today."); + let day = data.days.getDay(now); + ok(day.has(field), "FHR has url bar count for today."); + + let newCount = day.get(field); + + is(newCount, oldCount + 1, "Exactly one search has been recorded."); + finish(); + }); + })); + }); + }); +} + diff --git a/browser/base/content/test/browser_utilityOverlay.js b/browser/base/content/test/browser_utilityOverlay.js new file mode 100644 index 000000000..a3d909eb7 --- /dev/null +++ b/browser/base/content/test/browser_utilityOverlay.js @@ -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/. */ + +const gTests = [ + test_getTopWin, + test_getBoolPref, + test_openNewTabWith, + test_openUILink +]; + +function test () { + waitForExplicitFinish(); + executeSoon(runNextTest); +} + +function runNextTest() { + if (gTests.length) { + let testFun = gTests.shift(); + info("Running " + testFun.name); + testFun() + } + else { + finish(); + } +} + +function test_getTopWin() { + is(getTopWin(), window, "got top window"); + runNextTest(); +} + + +function test_getBoolPref() { + is(getBoolPref("browser.search.openintab", false), false, "getBoolPref"); + is(getBoolPref("this.pref.doesnt.exist", true), true, "getBoolPref fallback"); + is(getBoolPref("this.pref.doesnt.exist", false), false, "getBoolPref fallback #2"); + runNextTest(); +} + +function test_openNewTabWith() { + openNewTabWith("http://example.com/"); + let tab = gBrowser.selectedTab = gBrowser.tabs[1]; + tab.linkedBrowser.addEventListener("load", function onLoad(event) { + tab.linkedBrowser.removeEventListener("load", onLoad, true); + is(tab.linkedBrowser.currentURI.spec, "http://example.com/", "example.com loaded"); + gBrowser.removeCurrentTab(); + runNextTest(); + }, true); +} + +function test_openUILink() { + let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank"); + tab.linkedBrowser.addEventListener("load", function onLoad(event) { + tab.linkedBrowser.removeEventListener("load", onLoad, true); + is(tab.linkedBrowser.currentURI.spec, "http://example.org/", "example.org loaded"); + gBrowser.removeCurrentTab(); + runNextTest(); + }, true); + + openUILink("http://example.org/"); // defaults to "current" +} diff --git a/browser/base/content/test/browser_visibleFindSelection.js b/browser/base/content/test/browser_visibleFindSelection.js new file mode 100644 index 000000000..b46104ad4 --- /dev/null +++ b/browser/base/content/test/browser_visibleFindSelection.js @@ -0,0 +1,39 @@ + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + tab.linkedBrowser.addEventListener("load", function(aEvent) { + tab.linkedBrowser.removeEventListener("load", arguments.callee, true); + + ok(true, "Load listener called"); + waitForFocus(onFocus, content); + }, true); + + content.location = "data:text/html,<div style='position: absolute; left: 2200px; background: green; width: 200px; height: 200px;'>div</div><div style='position: absolute; left: 0px; background: red; width: 200px; height: 200px;'><span id='s'>div</span></div>"; +} + +function onFocus() { + EventUtils.synthesizeKey("f", { accelKey: true }); + ok(gFindBarInitialized, "find bar is now initialized"); + + EventUtils.synthesizeKey("d", {}); + EventUtils.synthesizeKey("i", {}); + EventUtils.synthesizeKey("v", {}); + // finds the div in the green box + + EventUtils.synthesizeKey("g", { accelKey: true }); + // finds the div in the red box + + var rect = content.document.getElementById("s").getBoundingClientRect(); + ok(rect.left >= 0, "scroll should include find result"); + + // clear the find bar + EventUtils.synthesizeKey("a", { accelKey: true }); + EventUtils.synthesizeKey("VK_DELETE", { }); + + gFindBar.close(); + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/browser/base/content/test/browser_visibleTabs.js b/browser/base/content/test/browser_visibleTabs.js new file mode 100644 index 000000000..d02bb64cb --- /dev/null +++ b/browser/base/content/test/browser_visibleTabs.js @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + // There should be one tab when we start the test + let [origTab] = gBrowser.visibleTabs; + + // Add a tab that will get pinned + let pinned = gBrowser.addTab(); + gBrowser.pinTab(pinned); + + let testTab = gBrowser.addTab(); + + let visible = gBrowser.visibleTabs; + is(visible.length, 3, "3 tabs should be open"); + is(visible[0], pinned, "the pinned tab is first"); + is(visible[1], origTab, "original tab is next"); + is(visible[2], testTab, "last created tab is last"); + + // Only show the test tab (but also get pinned and selected) + is(gBrowser.selectedTab, origTab, "sanity check that we're on the original tab"); + gBrowser.showOnlyTheseTabs([testTab]); + is(gBrowser.visibleTabs.length, 3, "all 3 tabs are still visible"); + + // Select the test tab and only show that (and pinned) + gBrowser.selectedTab = testTab; + gBrowser.showOnlyTheseTabs([testTab]); + + visible = gBrowser.visibleTabs; + is(visible.length, 2, "2 tabs should be visible including the pinned"); + is(visible[0], pinned, "first is pinned"); + is(visible[1], testTab, "next is the test tab"); + is(gBrowser.tabs.length, 3, "3 tabs should still be open"); + + gBrowser.selectTabAtIndex(0); + is(gBrowser.selectedTab, pinned, "first tab is pinned"); + gBrowser.selectTabAtIndex(1); + is(gBrowser.selectedTab, testTab, "second tab is the test tab"); + gBrowser.selectTabAtIndex(2); + is(gBrowser.selectedTab, testTab, "no third tab, so no change"); + gBrowser.selectTabAtIndex(0); + is(gBrowser.selectedTab, pinned, "switch back to the pinned"); + gBrowser.selectTabAtIndex(2); + is(gBrowser.selectedTab, pinned, "no third tab, so no change"); + gBrowser.selectTabAtIndex(-1); + is(gBrowser.selectedTab, testTab, "last tab is the test tab"); + + gBrowser.tabContainer.advanceSelectedTab(1, true); + is(gBrowser.selectedTab, pinned, "wrapped around the end to pinned"); + gBrowser.tabContainer.advanceSelectedTab(1, true); + is(gBrowser.selectedTab, testTab, "next to test tab"); + gBrowser.tabContainer.advanceSelectedTab(1, true); + is(gBrowser.selectedTab, pinned, "next to pinned again"); + + gBrowser.tabContainer.advanceSelectedTab(-1, true); + is(gBrowser.selectedTab, testTab, "going backwards to last tab"); + gBrowser.tabContainer.advanceSelectedTab(-1, true); + is(gBrowser.selectedTab, pinned, "next to pinned"); + gBrowser.tabContainer.advanceSelectedTab(-1, true); + is(gBrowser.selectedTab, testTab, "next to test tab again"); + + // Try showing all tabs + gBrowser.showOnlyTheseTabs(Array.slice(gBrowser.tabs)); + is(gBrowser.visibleTabs.length, 3, "all 3 tabs are visible again"); + + // Select the pinned tab and show the testTab to make sure selection updates + gBrowser.selectedTab = pinned; + gBrowser.showOnlyTheseTabs([testTab]); + is(gBrowser.tabs[1], origTab, "make sure origTab is in the middle"); + is(origTab.hidden, true, "make sure it's hidden"); + gBrowser.removeTab(pinned); + is(gBrowser.selectedTab, testTab, "making sure origTab was skipped"); + is(gBrowser.visibleTabs.length, 1, "only testTab is there"); + + // Only show one of the non-pinned tabs (but testTab is selected) + gBrowser.showOnlyTheseTabs([origTab]); + is(gBrowser.visibleTabs.length, 2, "got 2 tabs"); + + // Now really only show one of the tabs + gBrowser.showOnlyTheseTabs([testTab]); + visible = gBrowser.visibleTabs; + is(visible.length, 1, "only the original tab is visible"); + is(visible[0], testTab, "it's the original tab"); + is(gBrowser.tabs.length, 2, "still have 2 open tabs"); + + // Close the last visible tab and make sure we still get a visible tab + gBrowser.removeTab(testTab); + is(gBrowser.visibleTabs.length, 1, "only orig is left and visible"); + is(gBrowser.tabs.length, 1, "sanity check that it matches"); + is(gBrowser.selectedTab, origTab, "got the orig tab"); + is(origTab.hidden, false, "and it's not hidden -- visible!"); +} diff --git a/browser/base/content/test/browser_visibleTabs_bookmarkAllPages.js b/browser/base/content/test/browser_visibleTabs_bookmarkAllPages.js new file mode 100644 index 000000000..0d9a12067 --- /dev/null +++ b/browser/base/content/test/browser_visibleTabs_bookmarkAllPages.js @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let tabOne = gBrowser.addTab("about:blank"); + let tabTwo = gBrowser.addTab("http://mochi.test:8888/"); + gBrowser.selectedTab = tabTwo; + + let browser = gBrowser.getBrowserForTab(tabTwo); + let onLoad = function() { + browser.removeEventListener("load", onLoad, true); + + gBrowser.showOnlyTheseTabs([tabTwo]); + + is(gBrowser.visibleTabs.length, 1, "Only one tab is visible"); + + let uris = PlacesCommandHook.uniqueCurrentPages; + is(uris.length, 1, "Only one uri is returned"); + + is(uris[0].spec, tabTwo.linkedBrowser.currentURI.spec, "It's the correct URI"); + + gBrowser.removeTab(tabOne); + gBrowser.removeTab(tabTwo); + Array.forEach(gBrowser.tabs, function(tab) { + gBrowser.showTab(tab); + }); + + finish(); + } + browser.addEventListener("load", onLoad, true); +} diff --git a/browser/base/content/test/browser_visibleTabs_bookmarkAllTabs.js b/browser/base/content/test/browser_visibleTabs_bookmarkAllTabs.js new file mode 100644 index 000000000..09d790b94 --- /dev/null +++ b/browser/base/content/test/browser_visibleTabs_bookmarkAllTabs.js @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + // There should be one tab when we start the test + let [origTab] = gBrowser.visibleTabs; + is(gBrowser.visibleTabs.length, 1, "1 tab should be open"); + is(Disabled(), true, "Bookmark All Tabs should be disabled"); + + // Add a tab + let testTab1 = gBrowser.addTab(); + is(gBrowser.visibleTabs.length, 2, "2 tabs should be open"); + is(Disabled(), true, "Bookmark All Tabs should be disabled since there are two tabs with the same address"); + + let testTab2 = gBrowser.addTab("about:mozilla"); + is(gBrowser.visibleTabs.length, 3, "3 tabs should be open"); + // Wait for tab load, the code checks for currentURI. + testTab2.linkedBrowser.addEventListener("load", function () { + testTab2.linkedBrowser.removeEventListener("load", arguments.callee, true); + is(Disabled(), false, "Bookmark All Tabs should be enabled since there are two tabs with different addresses"); + + // Hide the original tab + gBrowser.selectedTab = testTab2; + gBrowser.showOnlyTheseTabs([testTab2]); + is(gBrowser.visibleTabs.length, 1, "1 tab should be visible"); + is(Disabled(), true, "Bookmark All Tabs should be disabled as there is only one visible tab"); + + // Add a tab that will get pinned + let pinned = gBrowser.addTab(); + is(gBrowser.visibleTabs.length, 2, "2 tabs should be visible now"); + is(Disabled(), false, "Bookmark All Tabs should be available as there are two visible tabs"); + gBrowser.pinTab(pinned); + is(Hidden(), false, "Bookmark All Tabs should be visible on a normal tab"); + is(Disabled(), true, "Bookmark All Tabs should not be available since one tab is pinned"); + gBrowser.selectedTab = pinned; + is(Hidden(), true, "Bookmark All Tabs should be hidden on a pinned tab"); + + // Show all tabs + let allTabs = [tab for each (tab in gBrowser.tabs)]; + gBrowser.showOnlyTheseTabs(allTabs); + + // reset the environment + gBrowser.removeTab(testTab2); + gBrowser.removeTab(testTab1); + gBrowser.removeTab(pinned); + is(gBrowser.visibleTabs.length, 1, "only orig is left and visible"); + is(gBrowser.tabs.length, 1, "sanity check that it matches"); + is(Disabled(), true, "Bookmark All Tabs should be hidden"); + is(gBrowser.selectedTab, origTab, "got the orig tab"); + is(origTab.hidden, false, "and it's not hidden -- visible!"); + finish(); + }, true); +} + +function Disabled() { + updateTabContextMenu(); + return document.getElementById("Browser:BookmarkAllTabs").getAttribute("disabled") == "true"; +} + +function Hidden() { + updateTabContextMenu(); + return document.getElementById("context_bookmarkAllTabs").hidden; +} diff --git a/browser/base/content/test/browser_visibleTabs_contextMenu.js b/browser/base/content/test/browser_visibleTabs_contextMenu.js new file mode 100644 index 000000000..0539c8106 --- /dev/null +++ b/browser/base/content/test/browser_visibleTabs_contextMenu.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + // There should be one tab when we start the test + let [origTab] = gBrowser.visibleTabs; + is(gBrowser.visibleTabs.length, 1, "there is one visible tab"); + let testTab = gBrowser.addTab(); + is(gBrowser.visibleTabs.length, 2, "there are now two visible tabs"); + + // Check the context menu with two tabs + updateTabContextMenu(origTab); + is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled"); + is(document.getElementById("context_reloadAllTabs").disabled, false, "Reload All Tabs is enabled"); + + // Hide the original tab. + gBrowser.selectedTab = testTab; + gBrowser.showOnlyTheseTabs([testTab]); + is(gBrowser.visibleTabs.length, 1, "now there is only one visible tab"); + + // Check the context menu with one tab. + updateTabContextMenu(testTab); + is(document.getElementById("context_closeTab").disabled, false, "Close Tab is enabled when more than one tab exists"); + is(document.getElementById("context_reloadAllTabs").disabled, true, "Reload All Tabs is disabled"); + + // Add a tab that will get pinned + // So now there's one pinned tab, one visible unpinned tab, and one hidden tab + let pinned = gBrowser.addTab(); + gBrowser.pinTab(pinned); + is(gBrowser.visibleTabs.length, 2, "now there are two visible tabs"); + + // Check the context menu on the unpinned visible tab + updateTabContextMenu(testTab); + is(document.getElementById("context_closeOtherTabs").disabled, true, "Close Other Tabs is disabled"); + is(document.getElementById("context_closeTabsToTheEnd").disabled, true, "Close Tabs To The End is disabled"); + + // Show all tabs + let allTabs = [tab for each (tab in gBrowser.tabs)]; + gBrowser.showOnlyTheseTabs(allTabs); + + // Check the context menu now + updateTabContextMenu(testTab); + is(document.getElementById("context_closeOtherTabs").disabled, false, "Close Other Tabs is enabled"); + is(document.getElementById("context_closeTabsToTheEnd").disabled, true, "Close Tabs To The End is disabled"); + + // Check the context menu of the original tab + // Close Tabs To The End should now be enabled + updateTabContextMenu(origTab); + is(document.getElementById("context_closeTabsToTheEnd").disabled, false, "Close Tabs To The End is enabled"); + + gBrowser.removeTab(testTab); + gBrowser.removeTab(pinned); +} diff --git a/browser/base/content/test/browser_visibleTabs_tabPreview.js b/browser/base/content/test/browser_visibleTabs_tabPreview.js new file mode 100644 index 000000000..9491690cb --- /dev/null +++ b/browser/base/content/test/browser_visibleTabs_tabPreview.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + gPrefService.setBoolPref("browser.ctrlTab.previews", true); + + let [origTab] = gBrowser.visibleTabs; + let tabOne = gBrowser.addTab(); + let tabTwo = gBrowser.addTab(); + + // test the ctrlTab.tabList + pressCtrlTab(); + ok(ctrlTab.tabList.length, 3, "Show 3 tabs in tab preview"); + releaseCtrl(); + + gBrowser.showOnlyTheseTabs([origTab]); + pressCtrlTab(); + ok(ctrlTab.tabList.length, 1, "Show 1 tab in tab preview"); + ok(!ctrlTab.isOpen, "With 1 tab open, Ctrl+Tab doesn't open the preview panel"); + + gBrowser.showOnlyTheseTabs([origTab, tabOne, tabTwo]); + pressCtrlTab(); + ok(ctrlTab.isOpen, "With 3 tabs open, Ctrl+Tab does open the preview panel"); + releaseCtrl(); + + // cleanup + gBrowser.removeTab(tabOne); + gBrowser.removeTab(tabTwo); + + if (gPrefService.prefHasUserValue("browser.ctrlTab.previews")) + gPrefService.clearUserPref("browser.ctrlTab.previews"); +} + +function pressCtrlTab(aShiftKey) { + EventUtils.synthesizeKey("VK_TAB", { ctrlKey: true, shiftKey: !!aShiftKey }); +} + +function releaseCtrl() { + EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }); +} diff --git a/browser/base/content/test/browser_wyciwyg_urlbarCopying.js b/browser/base/content/test/browser_wyciwyg_urlbarCopying.js new file mode 100644 index 000000000..f908e5254 --- /dev/null +++ b/browser/base/content/test/browser_wyciwyg_urlbarCopying.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + let url = "http://mochi.test:8888/browser/browser/base/content/test/test_wyciwyg_copying.html"; + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + tab.linkedBrowser.addEventListener("pageshow", function () { + let btn = content.document.getElementById("btn"); + executeSoon(function () { + EventUtils.synthesizeMouseAtCenter(btn, {}, content); + let currentURL = gBrowser.currentURI.spec; + ok(/^wyciwyg:\/\//i.test(currentURL), currentURL + " is a wyciwyg URI"); + + executeSoon(function () { + testURLBarCopy(url, endTest); + }); + }); + }, false); + + function endTest() { + while (gBrowser.tabs.length > 1) + gBrowser.removeCurrentTab(); + finish(); + } + + function testURLBarCopy(targetValue, cb) { + info("Expecting copy of: " + targetValue); + waitForClipboard(targetValue, function () { + gURLBar.focus(); + gURLBar.select(); + + goDoCommand("cmd_copy"); + }, cb, cb); + } +} + + diff --git a/browser/base/content/test/browser_zbug569342.js b/browser/base/content/test/browser_zbug569342.js new file mode 100644 index 000000000..8ca6674ce --- /dev/null +++ b/browser/base/content/test/browser_zbug569342.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var gTab = null; + +function load(url, cb) { + gTab = gBrowser.addTab(url); + gBrowser.addEventListener("load", function (event) { + if (event.target.location != url) + return; + + gBrowser.removeEventListener("load", arguments.callee, true); + // Trigger onLocationChange by switching tabs. + gBrowser.selectedTab = gTab; + cb(); + }, true); +} + +function test() { + waitForExplicitFinish(); + + ok(gFindBar.hidden, "Find bar should not be visible by default"); + + // Open the Find bar before we navigate to pages that shouldn't have it. + EventUtils.synthesizeKey("f", { accelKey: true }); + ok(!gFindBar.hidden, "Find bar should be visible"); + + nextTest(); +} + +let urls = [ + "about:config", + "about:addons", + "about:permissions" +]; + +function nextTest() { + let url = urls.shift(); + if (url) { + testFindDisabled(url, nextTest); + } else { + // Make sure the find bar is re-enabled after disabled page is closed. + testFindEnabled("about:blank", finish); + } +} + +function testFindDisabled(url, cb) { + load(url, function() { + ok(gFindBar.hidden, "Find bar should not be visible"); + EventUtils.synthesizeKey("/", {}, gTab.linkedBrowser.contentWindow); + ok(gFindBar.hidden, "Find bar should not be visible"); + EventUtils.synthesizeKey("f", { accelKey: true }); + ok(gFindBar.hidden, "Find bar should not be visible"); + ok(document.getElementById("cmd_find").getAttribute("disabled"), + "Find command should be disabled"); + + gBrowser.removeTab(gTab); + cb(); + }); +} + +function testFindEnabled(url, cb) { + load(url, function() { + ok(!document.getElementById("cmd_find").getAttribute("disabled"), + "Find command should not be disabled"); + + ok(!gFindBar.hidden, "Find bar should be visible again"); + + // Give focus to the Find bar and then close it. + EventUtils.synthesizeKey("f", { accelKey: true }); + EventUtils.synthesizeKey("VK_ESCAPE", { }); + ok(gFindBar.hidden, "Find bar should now be hidden"); + + gBrowser.removeTab(gTab); + cb(); + }); +} diff --git a/browser/base/content/test/bug364677-data.xml b/browser/base/content/test/bug364677-data.xml new file mode 100644 index 000000000..b48915c05 --- /dev/null +++ b/browser/base/content/test/bug364677-data.xml @@ -0,0 +1,5 @@ +<rss version="2.0"> + <channel> + <title>t</title> + </channel> +</rss> diff --git a/browser/base/content/test/bug364677-data.xml^headers^ b/browser/base/content/test/bug364677-data.xml^headers^ new file mode 100644 index 000000000..f203c6368 --- /dev/null +++ b/browser/base/content/test/bug364677-data.xml^headers^ @@ -0,0 +1 @@ +Content-Type: text/xml diff --git a/browser/base/content/test/bug395533-data.txt b/browser/base/content/test/bug395533-data.txt new file mode 100644 index 000000000..e0ed39850 --- /dev/null +++ b/browser/base/content/test/bug395533-data.txt @@ -0,0 +1,6 @@ +<rss version="2.0"> + <channel> + <link>http://example.org/</link> + <title>t</title> + </channel> +</rss> diff --git a/browser/base/content/test/bug564387.html b/browser/base/content/test/bug564387.html new file mode 100644 index 000000000..51ba4d04b --- /dev/null +++ b/browser/base/content/test/bug564387.html @@ -0,0 +1,11 @@ +<html> + <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=564387 --> + <head> + <title> Bug 564387 test</title> + </head> + <body> + Testing for Mozilla Bug: 564387 + <br> + <video src="bug564387_video1.ogv" id="video1"> </video> + </body> +</html> diff --git a/browser/base/content/test/bug564387_video1.ogv b/browser/base/content/test/bug564387_video1.ogv Binary files differnew file mode 100644 index 000000000..093158432 --- /dev/null +++ b/browser/base/content/test/bug564387_video1.ogv diff --git a/browser/base/content/test/bug564387_video1.ogv^headers^ b/browser/base/content/test/bug564387_video1.ogv^headers^ new file mode 100644 index 000000000..f880d0ac3 --- /dev/null +++ b/browser/base/content/test/bug564387_video1.ogv^headers^ @@ -0,0 +1,3 @@ +Content-Disposition: filename="Bug564387-expectedName.ogv" +Content-Type: video/ogg + diff --git a/browser/base/content/test/bug592338.html b/browser/base/content/test/bug592338.html new file mode 100644 index 000000000..159b21a76 --- /dev/null +++ b/browser/base/content/test/bug592338.html @@ -0,0 +1,24 @@ +<html> +<head> +<script type="text/javascript"> +var theme = { + id: "test", + name: "Test Background", + headerURL: "http://example.com/firefox/personas/01/header.jpg", + footerURL: "http://example.com/firefox/personas/01/footer.jpg", + textcolor: "#fff", + accentcolor: "#6b6b6b" +}; + +function setTheme(node) { + node.setAttribute("data-browsertheme", JSON.stringify(theme)); + var event = document.createEvent("Events"); + event.initEvent("InstallBrowserTheme", true, false); + node.dispatchEvent(event); +} +</script> +</head> +<body> +<a id="theme-install" href="#" onclick="setTheme(this)">Install</a> +</body> +</html> diff --git a/browser/base/content/test/bug792517-2.html b/browser/base/content/test/bug792517-2.html new file mode 100644 index 000000000..bfc24d817 --- /dev/null +++ b/browser/base/content/test/bug792517-2.html @@ -0,0 +1,5 @@ +<html> +<body> +<a href="bug792517.sjs" id="fff">this is a link</a> +</body> +</html> diff --git a/browser/base/content/test/bug792517.html b/browser/base/content/test/bug792517.html new file mode 100644 index 000000000..e7c040bf1 --- /dev/null +++ b/browser/base/content/test/bug792517.html @@ -0,0 +1,5 @@ +<html> +<body> +<img src="moz.png" id="img"> +</body> +</html> diff --git a/browser/base/content/test/bug792517.sjs b/browser/base/content/test/bug792517.sjs new file mode 100644 index 000000000..91e5aa23f --- /dev/null +++ b/browser/base/content/test/bug792517.sjs @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function handleRequest(aRequest, aResponse) { + aResponse.setStatusLine(aRequest.httpVersion, 200); + if (aRequest.hasHeader('Cookie')) { + aResponse.write("cookie-present"); + } else { + aResponse.setHeader("Set-Cookie", "foopy=1"); + aResponse.write("cookie-not-present"); + } +} diff --git a/browser/base/content/test/bug839103.css b/browser/base/content/test/bug839103.css new file mode 100644 index 000000000..611907d3d --- /dev/null +++ b/browser/base/content/test/bug839103.css @@ -0,0 +1 @@ +* {} diff --git a/browser/base/content/test/ctxmenu-image.png b/browser/base/content/test/ctxmenu-image.png Binary files differnew file mode 100644 index 000000000..4c3be5084 --- /dev/null +++ b/browser/base/content/test/ctxmenu-image.png diff --git a/browser/base/content/test/disablechrome.html b/browser/base/content/test/disablechrome.html new file mode 100644 index 000000000..7879e1ce9 --- /dev/null +++ b/browser/base/content/test/disablechrome.html @@ -0,0 +1,4 @@ +<html> +<body> +</body> +</html> diff --git a/browser/base/content/test/discovery.html b/browser/base/content/test/discovery.html new file mode 100644 index 000000000..1679e6545 --- /dev/null +++ b/browser/base/content/test/discovery.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> + <head id="linkparent"> + <title>Autodiscovery Test</title> + </head> + <body> + </body> +</html> diff --git a/browser/base/content/test/domplate_test.js b/browser/base/content/test/domplate_test.js new file mode 100644 index 000000000..75f2b25e2 --- /dev/null +++ b/browser/base/content/test/domplate_test.js @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let doc; +let div; +let plate; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource:///modules/domplate.jsm"); + +function createDocument() +{ + doc.body.innerHTML = '<div id="first">no</div>'; + doc.title = "Domplate Test"; + setupDomplateTests(); +} + +function setupDomplateTests() +{ + ok(domplate, "domplate is defined"); + plate = domplate({tag: domplate.DIV("Hello!")}); + ok(plate, "template is defined"); + div = doc.getElementById("first"); + ok(div, "we have our div"); + plate.tag.replace({}, div, template); + is(div.innerText, "Hello!", "Is the div's innerText replaced?"); + finishUp(); +} + +function finishUp() +{ + gBrowser.removeCurrentTab(); + finish(); +} + +function test() +{ + waitForExplicitFinish(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function() { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + doc = content.document; + waitForFocus(createDocument, content); + }, true); + + content.location = "data:text/html,basic domplate tests"; +} + diff --git a/browser/base/content/test/download_page.html b/browser/base/content/test/download_page.html new file mode 100644 index 000000000..541f6f88b --- /dev/null +++ b/browser/base/content/test/download_page.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=676619 +--> + <head> + <title>Test for the download attribute</title> + + </head> + <body> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=676619">Bug 676619</a> + <br/> + <ul> + <li><a href="data:text/plain,Hey What are you looking for?" + download="test.txt" id="link1">Download "test.txt"</a></li> + <li><a href="video.ogg" + download id="link2">Download "video.ogg"</a></li> + <li><a href="video.ogg" + download="just some video" id="link3">Download "just some video"</a></li> + <li><a href="data:text/plain,test" + download="with-target.txt" id="link4">Download "with-target.txt"</a></li> + <li><a href="javascript:1+2" + download="javascript.txt" id="link5">Download "javascript.txt"</a></li> + </ul> + <script> + var li = document.createElement('li'); + var a = document.createElement('a'); + + a.href = window.URL.createObjectURL(new Blob(["just text"])) ; + a.download = "test.blob"; + a.id = "link6"; + a.textContent = 'Download "test.blob"'; + + li.appendChild(a); + document.getElementsByTagName('ul')[0].appendChild(li); + + window.addEventListener("beforeunload", function (evt) { + document.getElementById("unload-flag").textContent = "Fail"; + }); + </script> + <ul> + <li><a href="http://example.com/" + download="example.com" id="link7" target="_blank">Download "example.com"</a></li> + <ul> + <div id="unload-flag">Okay</div> + </body> +</html> diff --git a/browser/base/content/test/dummy_page.html b/browser/base/content/test/dummy_page.html new file mode 100644 index 000000000..578567564 --- /dev/null +++ b/browser/base/content/test/dummy_page.html @@ -0,0 +1,8 @@ +<html> +<head> +<title>Dummy test page</title> +</head> +<body> +<p>Dummy test page</p> +</body> +</html> diff --git a/browser/base/content/test/feed_discovery.html b/browser/base/content/test/feed_discovery.html new file mode 100644 index 000000000..f7a30091c --- /dev/null +++ b/browser/base/content/test/feed_discovery.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=377611 +--> + <head> + <title>Test for feed discovery</title> + + <!-- Straight up standard --> + <link rel="alternate" type="application/atom+xml" title="1" href="/1.atom" /> + <link rel="alternate" type="application/rss+xml" title="2" href="/2.rss" /> + <link rel="feed" title="3" href="/3.xml" /> + + <!-- rel is a space-separated list --> + <link rel=" alternate " type="application/atom+xml" title="4" href="/4.atom" /> + <link rel="foo alternate" type="application/atom+xml" title="5" href="/5.atom" /> + <link rel="alternate foo" type="application/atom+xml" title="6" href="/6.atom" /> + <link rel="foo alternate foo" type="application/atom+xml" title="7" href="/7.atom" /> + <link rel="meat feed cake" title="8" href="/8.atom" /> + + <!-- rel is case-insensitive --> + <link rel="ALTERNate" type="application/atom+xml" title="9" href="/9.atom" /> + <link rel="fEEd" title="10" href="/10.atom" /> + + <!-- type can have leading and trailing whitespace --> + <link rel="alternate" type=" application/atom+xml " title="11" href="/11.atom" /> + + <!-- type is case-insensitive --> + <link rel="alternate" type="aPPliCAtion/ATom+xML" title="12" href="/12.atom" /> + + <!-- "feed stylesheet" is a feed, though "alternate stylesheet" isn't --> + <link rel="feed stylesheet" title="13" href="/13.atom" /> + + <!-- hyphens or letters around rel not allowed --> + <link rel="disabled-alternate" type="application/atom+xml" title="Bogus1" href="/Bogus1" /> + <link rel="alternates" type="application/atom+xml" title="Bogus2" href="/Bogus2" /> + <link rel=" alternate-like" type="application/atom+xml" title="Bogus3" href="/Bogus3" /> + + <!-- don't tolerate text/xml if title includes 'rss' not as a word --> + <link rel="alternate" type="text/xml" title="Bogus4 scissorsshaped" href="/Bogus4" /> + + <!-- don't tolerate application/xml if title includes 'rss' not as a word --> + <link rel="alternate" type="application/xml" title="Bogus5 scissorsshaped" href="/Bogus5" /> + + <!-- don't tolerate application/rdf+xml if title includes 'rss' not as a word --> + <link rel="alternate" type="application/rdf+xml" title="Bogus6 scissorsshaped" href="/Bogus6" /> + + <!-- don't tolerate random types --> + <link rel="alternate" type="text/plain" title="Bogus7 rss" href="/Bogus7" /> + + <!-- don't find Atom by title --> + <link rel="foopy" type="application/atom+xml" title="Bogus8 Atom and RSS" href="/Bogus8" /> + + <!-- don't find application/rss+xml by title --> + <link rel="goats" type="application/rss+xml" title="Bogus9 RSS and Atom" href="/Bogus9" /> + + <!-- don't find application/rdf+xml by title --> + <link rel="alternate" type="application/rdf+xml" title="Bogus10 RSS and Atom" href="/Bogus10" /> + + <!-- don't find application/xml by title --> + <link rel="alternate" type="application/xml" title="Bogus11 RSS and Atom" href="/Bogus11" /> + + <!-- don't find text/xml by title --> + <link rel="alternate" type="text/xml" title="Bogus12 RSS and Atom" href="/Bogus12" /> + + <!-- alternate and stylesheet isn't a feed --> + <link rel="alternate stylesheet" type="application/rss+xml" title="Bogus13 RSS" href="/Bogus13" /> + </head> + <body> + <script type="text/javascript"> + window.onload = function() { + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + + var tests = new Array(); + + var currentWindow = + window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindow); + var browser = currentWindow.gBrowser.selectedBrowser; + + var discovered = browser.feeds; + tests.push({ check: discovered.length > 0, + message: "some feeds should be discovered" }); + + var feeds = []; + + for (var aFeed of discovered) { + feeds[aFeed.href] = true; + } + + for (var aLink of document.getElementsByTagName("link")) { + // ignore real stylesheets, and anything without an href property + if (aLink.type != "text/css" && aLink.href) { + if (/bogus/i.test(aLink.title)) { + tests.push({ check: !feeds[aLink.href], + message: "don't discover " + aLink.href }); + } else { + tests.push({ check: feeds[aLink.href], + message: "should discover " + aLink.href }); + } + } + } + window.arguments[0].tests = tests; + window.close(); + } + </script> + </body> +</html> + diff --git a/browser/base/content/test/feed_tab.html b/browser/base/content/test/feed_tab.html new file mode 100644 index 000000000..50903f48b --- /dev/null +++ b/browser/base/content/test/feed_tab.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=458579 +--> + <head> + <title>Test for page info feeds tab</title> + + <!-- Straight up standard --> + <link rel="alternate" type="application/atom+xml" title="1" href="/1.atom" /> + <link rel="alternate" type="application/rss+xml" title="2" href="/2.rss" /> + <link rel="feed" title="3" href="/3.xml" /> + + </head> + <body> + </body> +</html> diff --git a/browser/base/content/test/file_bug550565_favicon.ico b/browser/base/content/test/file_bug550565_favicon.ico Binary files differnew file mode 100644 index 000000000..d44438903 --- /dev/null +++ b/browser/base/content/test/file_bug550565_favicon.ico diff --git a/browser/base/content/test/file_bug550565_popup.html b/browser/base/content/test/file_bug550565_popup.html new file mode 100644 index 000000000..b4cddf971 --- /dev/null +++ b/browser/base/content/test/file_bug550565_popup.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test file for bug 550565.</title> + + <!--Set a favicon; that's the whole point of this file.--> + <link rel="icon" href="file_bug550565_favicon.ico"> +</head> +<body> + Test file for bug 550565. +</body> +</html> diff --git a/browser/base/content/test/file_bug822367_1.html b/browser/base/content/test/file_bug822367_1.html new file mode 100644 index 000000000..0bb5011b8 --- /dev/null +++ b/browser/base/content/test/file_bug822367_1.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test 1 for Mixed Content Blocker User Override - Mixed Script +https://bugzilla.mozilla.org/show_bug.cgi?id=822367 +--> +<head> + <meta charset="utf-8"> + <title>Test 1 for Bug 822367</title> +</head> +<body> + <div id="testContent"> + <p id="p1"></p> + </div> + <script src="http://example.com/browser/browser/base/content/test/file_bug822367_1.js"> + </script> +</body> +</html> diff --git a/browser/base/content/test/file_bug822367_1.js b/browser/base/content/test/file_bug822367_1.js new file mode 100644 index 000000000..175de363b --- /dev/null +++ b/browser/base/content/test/file_bug822367_1.js @@ -0,0 +1 @@ +document.getElementById('p1').innerHTML="hello"; diff --git a/browser/base/content/test/file_bug822367_2.html b/browser/base/content/test/file_bug822367_2.html new file mode 100644 index 000000000..fe56ee213 --- /dev/null +++ b/browser/base/content/test/file_bug822367_2.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test 2 for Mixed Content Blocker User Override - Mixed Display +https://bugzilla.mozilla.org/show_bug.cgi?id=822367 +--> +<head> + <meta charset="utf-8"> + <title>Test 2 for Bug 822367 - Mixed Display</title> +</head> +<body> + <div id="testContent"> + <img src="http://example.com/tests/image/test/mochitest/blue.png"> + </div> +</body> +</html> diff --git a/browser/base/content/test/file_bug822367_3.html b/browser/base/content/test/file_bug822367_3.html new file mode 100644 index 000000000..646e61206 --- /dev/null +++ b/browser/base/content/test/file_bug822367_3.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test 3 for Mixed Content Blocker User Override - Mixed Script and Display +https://bugzilla.mozilla.org/show_bug.cgi?id=822367 +--> +<head> + <meta charset="utf-8"> + <title>Test 3 for Bug 822367</title> + <script> + function foo() { + var x = document.createElement('p'); + x.setAttribute("id", "p2"); + x.innerHTML = "bye"; + document.getElementById("testContent").appendChild(x); + } + </script> +</head> +<body> + <div id="testContent"> + <p id="p1"></p> + <img src="http://example.com/tests/image/test/mochitest/blue.png" onload="foo()"> + </div> + <script src="http://example.com/browser/browser/base/content/test/file_bug822367_1.js"> + </script> +</body> +</html> diff --git a/browser/base/content/test/file_bug822367_4.html b/browser/base/content/test/file_bug822367_4.html new file mode 100644 index 000000000..490692af9 --- /dev/null +++ b/browser/base/content/test/file_bug822367_4.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test 4 for Mixed Content Blocker User Override - Mixed Script and Display +https://bugzilla.mozilla.org/show_bug.cgi?id=822367 +--> +<head> + <meta charset="utf-8"> + <title>Test 4 for Bug 822367</title> +</head> +<body> + <div id="testContent"> + <p id="p1"></p> + </div> + <script src="http://example.com/browser/browser/base/content/test/file_bug822367_4.js"> + </script> +</body> +</html> diff --git a/browser/base/content/test/file_bug822367_4.js b/browser/base/content/test/file_bug822367_4.js new file mode 100644 index 000000000..70462f05f --- /dev/null +++ b/browser/base/content/test/file_bug822367_4.js @@ -0,0 +1 @@ +document.location = "https://example.com/browser/browser/base/content/test/file_bug822367_4B.html"; diff --git a/browser/base/content/test/file_bug822367_4B.html b/browser/base/content/test/file_bug822367_4B.html new file mode 100644 index 000000000..05e11d2d0 --- /dev/null +++ b/browser/base/content/test/file_bug822367_4B.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test 4B for Mixed Content Blocker User Override - Location Changed +https://bugzilla.mozilla.org/show_bug.cgi?id=822367 +--> +<head> + <meta charset="utf-8"> + <title>Test 4B Location Change for Bug 822367</title> +</head> +<body> + <div id="testContent"> + <p id="p1"></p> + </div> + <script src="http://example.com/browser/browser/base/content/test/file_bug822367_1.js"> + </script> +</body> +</html> diff --git a/browser/base/content/test/file_bug822367_5.html b/browser/base/content/test/file_bug822367_5.html new file mode 100644 index 000000000..e45408761 --- /dev/null +++ b/browser/base/content/test/file_bug822367_5.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test 5 for Mixed Content Blocker User Override - Mixed Script in document.open() +https://bugzilla.mozilla.org/show_bug.cgi?id=822367 +--> +<head> + <meta charset="utf-8"> + <title>Test 5 for Bug 822367</title> + <script> + function createDoc() + { + var doc=document.open("text/html","replace"); + doc.write('<!DOCTYPE html><html><body><p id="p1">This is some content</p><script src="http://example.com/browser/browser/base/content/test/file_bug822367_1.js">\<\/script\>\<\/body>\<\/html>'); + doc.close(); + } + </script> +</head> +<body> + <div id="testContent"> + <img src="https://example.com/tests/image/test/mochitest/blue.png" onload="createDoc()"> + </div> +</body> +</html> diff --git a/browser/base/content/test/file_bug822367_6.html b/browser/base/content/test/file_bug822367_6.html new file mode 100644 index 000000000..c158772f5 --- /dev/null +++ b/browser/base/content/test/file_bug822367_6.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<!-- +Test 6 for Mixed Content Blocker User Override - Mixed Script in document.open() within an iframe +https://bugzilla.mozilla.org/show_bug.cgi?id=822367 +--> +<head> + <meta charset="utf-8"> + <title>Test 6 for Bug 822367</title> +</head> +<body> + <div id="testContent"> + <iframe name="f1" id="f1" src="https://example.com/browser/browser/base/content/test/file_bug822367_5.html"></iframe> + </div> +</body> +</html> diff --git a/browser/base/content/test/file_bug902156.js b/browser/base/content/test/file_bug902156.js new file mode 100644 index 000000000..806667204 --- /dev/null +++ b/browser/base/content/test/file_bug902156.js @@ -0,0 +1,5 @@ +/* + * Once the mixed content blocker is disabled for the page, this scripts loads + * and updates the text inside the div container. + */ +document.getElementById("mctestdiv").innerHTML = "Mixed Content Blocker disabled"; diff --git a/browser/base/content/test/file_bug902156_1.html b/browser/base/content/test/file_bug902156_1.html new file mode 100644 index 000000000..04d525817 --- /dev/null +++ b/browser/base/content/test/file_bug902156_1.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test 1 for Bug 902156 - See file browser_bug902156.js for description. + https://bugzilla.mozilla.org/show_bug.cgi?id=902156 +--> +<head> + <meta charset="utf-8"> + <title>Test 1 for Bug 902156</title> +</head> +<body> + <div id="mctestdiv">Mixed Content Blocker enabled</div> + <script src="http://test1.example.com/browser/browser/base/content/test/file_bug902156.js" ></script> +</body> +</html> diff --git a/browser/base/content/test/file_bug902156_2.html b/browser/base/content/test/file_bug902156_2.html new file mode 100644 index 000000000..396142520 --- /dev/null +++ b/browser/base/content/test/file_bug902156_2.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test 2 for Bug 902156 - See file browser_bug902156.js for description. + https://bugzilla.mozilla.org/show_bug.cgi?id=902156 +--> +<head> + <meta charset="utf-8"> + <title>Test 2 for Bug 902156</title> +</head> +<body> + <div id="mctestdiv">Mixed Content Blocker enabled</div> + <a href="https://test2.example.com/browser/browser/base/content/test/file_bug902156_1.html" + id="mctestlink" target="_top">Go to http site</a> + <script src="http://test2.example.com/browser/browser/base/content/test/file_bug902156.js" ></script> +</body> +</html> diff --git a/browser/base/content/test/file_bug902156_3.html b/browser/base/content/test/file_bug902156_3.html new file mode 100644 index 000000000..23d1b11ba --- /dev/null +++ b/browser/base/content/test/file_bug902156_3.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test 3 for Bug 902156 - See file browser_bug902156.js for description. + https://bugzilla.mozilla.org/show_bug.cgi?id=902156 +--> +<head> + <meta charset="utf-8"> + <title>Test 3 for Bug 902156</title> +</head> +<body> + <div id="mctestdiv">Mixed Content Blocker enabled</div> + <script src="http://test1.example.com/browser/browser/base/content/test/file_bug902156.js" ></script> +</body> +</html> diff --git a/browser/base/content/test/file_fullscreen-window-open.html b/browser/base/content/test/file_fullscreen-window-open.html new file mode 100644 index 000000000..1584f4c98 --- /dev/null +++ b/browser/base/content/test/file_fullscreen-window-open.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test for window.open() when browser is in fullscreen</title> + </head> + <body> + <script> + window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, true); + + document.getElementById("test").addEventListener("click", onClick, true); + }, true); + + function onClick(aEvent) { + aEvent.preventDefault(); + + var dataStr = aEvent.target.getAttribute("data-test-param"); + var data = JSON.parse(dataStr); + window.open(data.uri, data.title, data.option); + } + </script> + <a id="test" href="" data-test-param="">Test</a> + </body> +</html> diff --git a/browser/base/content/test/gZipOfflineChild.cacheManifest b/browser/base/content/test/gZipOfflineChild.cacheManifest new file mode 100644 index 000000000..ae0545d12 --- /dev/null +++ b/browser/base/content/test/gZipOfflineChild.cacheManifest @@ -0,0 +1,2 @@ +CACHE MANIFEST +gZipOfflineChild.html diff --git a/browser/base/content/test/gZipOfflineChild.cacheManifest^headers^ b/browser/base/content/test/gZipOfflineChild.cacheManifest^headers^ new file mode 100644 index 000000000..257f2eb60 --- /dev/null +++ b/browser/base/content/test/gZipOfflineChild.cacheManifest^headers^ @@ -0,0 +1 @@ +Content-Type: text/cache-manifest diff --git a/browser/base/content/test/gZipOfflineChild.html b/browser/base/content/test/gZipOfflineChild.html Binary files differnew file mode 100644 index 000000000..bd2d62ee0 --- /dev/null +++ b/browser/base/content/test/gZipOfflineChild.html diff --git a/browser/base/content/test/gZipOfflineChild.html^headers^ b/browser/base/content/test/gZipOfflineChild.html^headers^ new file mode 100644 index 000000000..4204d8601 --- /dev/null +++ b/browser/base/content/test/gZipOfflineChild.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html +Content-Encoding: gzip diff --git a/browser/base/content/test/gZipOfflineChild_uncompressed.html b/browser/base/content/test/gZipOfflineChild_uncompressed.html new file mode 100644 index 000000000..4ab8f8d5e --- /dev/null +++ b/browser/base/content/test/gZipOfflineChild_uncompressed.html @@ -0,0 +1,21 @@ +<html manifest="gZipOfflineChild.cacheManifest"> +<head> + <!-- This file is gzipped to create gZipOfflineChild.html --> +<title></title> +<script type="text/javascript"> + +function finish(success) { + window.parent.postMessage(success, "*"); +} + +applicationCache.oncached = function() { finish("oncache"); } +applicationCache.onnoupdate = function() { finish("onupdate"); } +applicationCache.onerror = function() { finish("onerror"); } + +</script> +</head> + +<body> +<h1>Child</h1> +</body> +</html> diff --git a/browser/base/content/test/head.js b/browser/base/content/test/head.js new file mode 100644 index 000000000..fbc28f41e --- /dev/null +++ b/browser/base/content/test/head.js @@ -0,0 +1,400 @@ +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/commonjs/sdk/core/promise.js"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +function whenDelayedStartupFinished(aWindow, aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + executeSoon(aCallback); + } + }, "browser-delayed-startup-finished", false); +} + +function findChromeWindowByURI(aURI) { + let windows = Services.wm.getEnumerator(null); + while (windows.hasMoreElements()) { + let win = windows.getNext(); + if (win.location.href == aURI) + return win; + } + return null; +} + +function updateTabContextMenu(tab) { + let menu = document.getElementById("tabContextMenu"); + if (!tab) + tab = gBrowser.selectedTab; + var evt = new Event(""); + tab.dispatchEvent(evt); + menu.openPopup(tab, "end_after", 0, 0, true, false, evt); + is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab"); + menu.hidePopup(); +} + +function findToolbarCustomizationWindow(aBrowserWin) { + if (!aBrowserWin) + aBrowserWin = window; + + let iframe = aBrowserWin.document.getElementById("customizeToolbarSheetIFrame"); + let win = iframe && iframe.contentWindow; + if (win) + return win; + + win = findChromeWindowByURI("chrome://global/content/customizeToolbar.xul"); + if (win && win.opener == aBrowserWin) + return win; + + throw Error("Failed to find the customization window"); +} + +function openToolbarCustomizationUI(aCallback, aBrowserWin) { + if (!aBrowserWin) + aBrowserWin = window; + + aBrowserWin.document.getElementById("cmd_CustomizeToolbars").doCommand(); + + aBrowserWin.gNavToolbox.addEventListener("beforecustomization", function UI_loaded() { + aBrowserWin.gNavToolbox.removeEventListener("beforecustomization", UI_loaded); + + let win = findToolbarCustomizationWindow(aBrowserWin); + waitForFocus(function () { + aCallback(win); + }, win); + }); +} + +function closeToolbarCustomizationUI(aCallback, aBrowserWin) { + let win = findToolbarCustomizationWindow(aBrowserWin); + + win.addEventListener("unload", function unloaded() { + win.removeEventListener("unload", unloaded); + executeSoon(aCallback); + }); + + let button = win.document.getElementById("donebutton"); + button.focus(); + button.doCommand(); +} + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + if (condition()) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} + +function getTestPlugin(aName) { + var pluginName = aName || "Test Plug-in"; + var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); + var tags = ph.getPluginTags(); + + // Find the test plugin + for (var i = 0; i < tags.length; i++) { + if (tags[i].name == pluginName) + return tags[i]; + } + ok(false, "Unable to find plugin"); + return null; +} + +// after a test is done using the plugin doorhanger, we should just clear +// any permissions that may have crept in +function clearAllPluginPermissions() { + let perms = Services.perms.enumerator; + while (perms.hasMoreElements()) { + let perm = perms.getNext(); + if (perm.type.startsWith('plugin')) { + Services.perms.remove(perm.host, perm.type); + } + } +} + +function updateBlocklist(aCallback) { + var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"] + .getService(Ci.nsITimerCallback); + var observer = function() { + Services.obs.removeObserver(observer, "blocklist-updated"); + SimpleTest.executeSoon(aCallback); + }; + Services.obs.addObserver(observer, "blocklist-updated", false); + blocklistNotifier.notify(null); +} + +var _originalTestBlocklistURL = null; +function setAndUpdateBlocklist(aURL, aCallback) { + if (!_originalTestBlocklistURL) + _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url"); + Services.prefs.setCharPref("extensions.blocklist.url", aURL); + updateBlocklist(aCallback); +} + +function resetBlocklist() { + Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL); +} + +function whenNewWindowLoaded(aOptions, aCallback) { + let win = OpenBrowserWindow(aOptions); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad, false); + aCallback(win); + }, false); +} + +/** + * Waits for all pending async statements on the default connection, before + * proceeding with aCallback. + * + * @param aCallback + * Function to be called when done. + * @param aScope + * Scope for the callback. + * @param aArguments + * Arguments array for the callback. + * + * @note The result is achieved by asynchronously executing a query requiring + * a write lock. Since all statements on the same connection are + * serialized, the end of this write operation means that all writes are + * complete. Note that WAL makes so that writers don't block readers, but + * this is a problem only across different connections. + */ +function waitForAsyncUpdates(aCallback, aScope, aArguments) { + let scope = aScope || this; + let args = aArguments || []; + let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection; + let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); + begin.executeAsync(); + begin.finalize(); + + let commit = db.createAsyncStatement("COMMIT"); + commit.executeAsync({ + handleResult: function() {}, + handleError: function() {}, + handleCompletion: function(aReason) { + aCallback.apply(scope, args); + } + }); + commit.finalize(); +} + +/** + * Asynchronously check a url is visited. + + * @param aURI The URI. + * @param aExpectedValue The expected value. + * @return {Promise} + * @resolves When the check has been added successfully. + * @rejects JavaScript exception. + */ +function promiseIsURIVisited(aURI, aExpectedValue) { + let deferred = Promise.defer(); + PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { + deferred.resolve(aIsVisited); + }); + + return deferred.promise; +} + +function whenNewTabLoaded(aWindow, aCallback) { + aWindow.BrowserOpenTab(); + + let browser = aWindow.gBrowser.selectedBrowser; + if (browser.contentDocument.readyState === "complete") { + aCallback(); + return; + } + + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + aCallback(); + }, true); +} + +function addVisits(aPlaceInfo, aCallback) { + let places = []; + if (aPlaceInfo instanceof Ci.nsIURI) { + places.push({ uri: aPlaceInfo }); + } else if (Array.isArray(aPlaceInfo)) { + places = places.concat(aPlaceInfo); + } else { + places.push(aPlaceInfo); + } + + // Create mozIVisitInfo for each entry. + let now = Date.now(); + for (let i = 0; i < places.length; i++) { + if (!places[i].title) { + places[i].title = "test visit for " + places[i].uri.spec; + } + places[i].visits = [{ + transitionType: places[i].transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK + : places[i].transition, + visitDate: places[i].visitDate || (now++) * 1000, + referrerURI: places[i].referrer + }]; + } + + PlacesUtils.asyncHistory.updatePlaces( + places, + { + handleError: function AAV_handleError() { + throw("Unexpected error in adding visit."); + }, + handleResult: function () {}, + handleCompletion: function UP_handleCompletion() { + if (aCallback) + aCallback(); + } + } + ); +} + +/** + * Ensures that the specified URIs are either cleared or not. + * + * @param aURIs + * Array of page URIs + * @param aShouldBeCleared + * True if each visit to the URI should be cleared, false otherwise + */ +function promiseHistoryClearedState(aURIs, aShouldBeCleared) { + let deferred = Promise.defer(); + let callbackCount = 0; + let niceStr = aShouldBeCleared ? "no longer" : "still"; + function callbackDone() { + if (++callbackCount == aURIs.length) + deferred.resolve(); + } + aURIs.forEach(function (aURI) { + PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { + is(aIsVisited, !aShouldBeCleared, + "history visit " + aURI.spec + " should " + niceStr + " exist"); + callbackDone(); + }); + }); + + return deferred.promise; +} + +let FullZoomHelper = { + + selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(tab) { + if (!tab) + throw new Error("tab must be given."); + if (gBrowser.selectedTab == tab) + return Promise.resolve(); + gBrowser.selectedTab = tab; + return this.waitForLocationChange(); + }, + + removeTabAndWaitForLocationChange: function removeTabAndWaitForLocationChange(tab) { + tab = tab || gBrowser.selectedTab; + let selected = gBrowser.selectedTab == tab; + gBrowser.removeTab(tab); + if (selected) + return this.waitForLocationChange(); + return Promise.resolve(); + }, + + waitForLocationChange: function waitForLocationChange() { + let deferred = Promise.defer(); + Services.obs.addObserver(function obs() { + Services.obs.removeObserver(obs, "browser-fullZoom:locationChange"); + deferred.resolve(); + }, "browser-fullZoom:locationChange", false); + return deferred.promise; + }, + + load: function load(tab, url) { + let deferred = Promise.defer(); + let didLoad = false; + let didZoom = false; + + tab.linkedBrowser.addEventListener("load", function (event) { + event.currentTarget.removeEventListener("load", arguments.callee, true); + didLoad = true; + if (didZoom) + deferred.resolve(); + }, true); + + this.waitForLocationChange().then(function () { + didZoom = true; + if (didLoad) + deferred.resolve(); + }); + + tab.linkedBrowser.loadURI(url); + + return deferred.promise; + }, + + zoomTest: function zoomTest(tab, val, msg) { + is(ZoomManager.getZoomForBrowser(tab.linkedBrowser), val, msg); + }, + + enlarge: function enlarge() { + let deferred = Promise.defer(); + FullZoom.enlarge(function () deferred.resolve()); + return deferred.promise; + }, + + reduce: function reduce() { + let deferred = Promise.defer(); + FullZoom.reduce(function () deferred.resolve()); + return deferred.promise; + }, + + reset: function reset() { + let deferred = Promise.defer(); + FullZoom.reset(function () deferred.resolve()); + return deferred.promise; + }, + + BACK: 0, + FORWARD: 1, + navigate: function navigate(direction) { + let deferred = Promise.defer(); + let didPs = false; + let didZoom = false; + + gBrowser.addEventListener("pageshow", function (event) { + gBrowser.removeEventListener("pageshow", arguments.callee, true); + didPs = true; + if (didZoom) + deferred.resolve(); + }, true); + + if (direction == this.BACK) + gBrowser.goBack(); + else if (direction == this.FORWARD) + gBrowser.goForward(); + + this.waitForLocationChange().then(function () { + didZoom = true; + if (didPs) + deferred.resolve(); + }); + return deferred.promise; + }, + + failAndContinue: function failAndContinue(func) { + return function (err) { + ok(false, err); + func(); + }; + }, +}; diff --git a/browser/base/content/test/head_plain.js b/browser/base/content/test/head_plain.js new file mode 100644 index 000000000..62f9afb2e --- /dev/null +++ b/browser/base/content/test/head_plain.js @@ -0,0 +1,15 @@ + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + if (condition()) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} diff --git a/browser/base/content/test/healthreport_testRemoteCommands.html b/browser/base/content/test/healthreport_testRemoteCommands.html new file mode 100644 index 000000000..1cd6de841 --- /dev/null +++ b/browser/base/content/test/healthreport_testRemoteCommands.html @@ -0,0 +1,128 @@ +<html>
+ <head>
+ <meta charset="utf-8">
+
+<script>
+
+function init() {
+ window.addEventListener("message", function process(e) {doTest(e)}, false);
+ doTest();
+}
+
+function checkSubmissionValue(payload, expectedValue) {
+ return payload.enabled == expectedValue;
+}
+
+function validatePayload(payload) {
+ payload = JSON.parse(payload);
+
+ // xxxmpc - this is some pretty low-bar validation, but we have plenty of tests of that API elsewhere
+ if (!payload.thisPingDate)
+ return false;
+
+ return true;
+}
+
+var tests = [
+{
+ info: "Checking initial value is enabled",
+ event: "RequestCurrentPrefs",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, true);
+ },
+},
+{
+ info: "Verifying disabling works",
+ event: "DisableDataSubmission",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, false);
+ },
+},
+{
+ info: "Verifying we're still disabled",
+ event: "RequestCurrentPrefs",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, false);
+ },
+},
+{
+ info: "Verifying we can get a payload while submission is disabled",
+ event: "RequestCurrentPayload",
+ payloadType: "payload",
+ validateResponse: function(payload) {
+ return validatePayload(payload);
+ },
+},
+{
+ info: "Verifying enabling works",
+ event: "EnableDataSubmission",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, true);
+ },
+},
+{
+ info: "Verifying we're still re-enabled",
+ event: "RequestCurrentPrefs",
+ payloadType: "prefs",
+ validateResponse: function(payload) {
+ return checkSubmissionValue(payload, true);
+ },
+},
+{
+ info: "Verifying we can get a payload after re-enabling",
+ event: "RequestCurrentPayload",
+ payloadType: "payload",
+ validateResponse: function(payload) {
+ return validatePayload(payload);
+ },
+},
+];
+
+var currentTest = -1;
+function doTest(evt) {
+ if (evt) {
+ if (currentTest < 0 || !evt.data.content)
+ return; // not yet testing
+
+ var test = tests[currentTest];
+ if (evt.data.type != test.payloadType)
+ return; // skip unrequested events
+
+ var error = JSON.stringify(evt.data.content);
+ var pass = false;
+ try {
+ pass = test.validateResponse(evt.data.content)
+ } catch (e) {}
+ reportResult(test.info, pass, error);
+ }
+ // start the next test if there are any left
+ if (tests[++currentTest])
+ sendToBrowser(tests[currentTest].event);
+ else
+ reportFinished();
+}
+
+function reportResult(info, pass, error) {
+ var data = {type: "testResult", info: info, pass: pass, error: error};
+ window.parent.postMessage(data, "*");
+}
+
+function reportFinished(cmd) {
+ var data = {type: "testsComplete", count: tests.length};
+ window.parent.postMessage(data, "*");
+}
+
+function sendToBrowser(type) {
+ var event = new CustomEvent("RemoteHealthReportCommand", {detail: {command: type}, bubbles: true});
+ document.dispatchEvent(event);
+}
+
+</script>
+ </head>
+ <body onload="init()">
+ </body>
+</html>
diff --git a/browser/base/content/test/moz.build b/browser/base/content/test/moz.build new file mode 100644 index 000000000..27399e075 --- /dev/null +++ b/browser/base/content/test/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += ['newtab', 'social'] + diff --git a/browser/base/content/test/moz.png b/browser/base/content/test/moz.png Binary files differnew file mode 100644 index 000000000..769c63634 --- /dev/null +++ b/browser/base/content/test/moz.png diff --git a/browser/base/content/test/newtab/Makefile.in b/browser/base/content/test/newtab/Makefile.in new file mode 100644 index 000000000..6ac460d14 --- /dev/null +++ b/browser/base/content/test/newtab/Makefile.in @@ -0,0 +1,38 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = @relativesrcdir@ + +include $(DEPTH)/config/autoconf.mk + +MOCHITEST_BROWSER_FILES = \ + browser_newtab_block.js \ + browser_newtab_disable.js \ + browser_newtab_drag_drop.js \ + browser_newtab_drag_drop_ext.js \ + browser_newtab_drop_preview.js \ + browser_newtab_focus.js \ + browser_newtab_reset.js \ + browser_newtab_tabsync.js \ + browser_newtab_undo.js \ + browser_newtab_unpin.js \ + browser_newtab_bug721442.js \ + browser_newtab_bug722273.js \ + browser_newtab_bug723102.js \ + browser_newtab_bug723121.js \ + browser_newtab_bug725996.js \ + browser_newtab_bug734043.js \ + browser_newtab_bug735987.js \ + browser_newtab_bug752841.js \ + browser_newtab_bug765628.js \ + browser_newtab_bug876313.js \ + browser_newtab_perwindow_private_browsing.js \ + head.js \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/browser/base/content/test/newtab/browser_newtab_block.js b/browser/base/content/test/newtab/browser_newtab_block.js new file mode 100644 index 000000000..bcb3d7baf --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_block.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that blocking/removing sites from the grid works + * as expected. Pinned tabs should not be moved. Gaps will be re-filled + * if more sites are available. + */ +function runTests() { + // we remove sites and expect the gaps to be filled as long as there still + // are some sites available + yield setLinks("0,1,2,3,4,5,6,7,8,9"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8"); + + yield blockCell(4); + checkGrid("0,1,2,3,5,6,7,8,9"); + + yield blockCell(4); + checkGrid("0,1,2,3,6,7,8,9,"); + + yield blockCell(4); + checkGrid("0,1,2,3,7,8,9,,"); + + // we removed a pinned site + yield restore(); + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(",1"); + + yield addNewTabPageTab(); + checkGrid("0,1p,2,3,4,5,6,7,8"); + + yield blockCell(1); + checkGrid("0,2,3,4,5,6,7,8,"); + + // we remove the last site on the grid (which is pinned) and expect the gap + // to be re-filled and the new site to be unpinned + yield restore(); + yield setLinks("0,1,2,3,4,5,6,7,8,9"); + setPinnedLinks(",,,,,,,,8"); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8p"); + + yield blockCell(8); + checkGrid("0,1,2,3,4,5,6,7,9"); + + // we remove the first site on the grid with the last one pinned. all cells + // but the last one should shift to the left and a new site fades in + yield restore(); + yield setLinks("0,1,2,3,4,5,6,7,8,9"); + setPinnedLinks(",,,,,,,,8"); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8p"); + + yield blockCell(0); + checkGrid("1,2,3,4,5,6,7,9,8p"); +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug721442.js b/browser/base/content/test/newtab/browser_newtab_bug721442.js new file mode 100644 index 000000000..597aed251 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug721442.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function runTests() { + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks([ + {url: "http://example.com/#7", title: ""}, + {url: "http://example.com/#8", title: "title"}, + {url: "http://example.com/#9", title: "http://example.com/#9"} + ]); + + yield addNewTabPageTab(); + checkGrid("7p,8p,9p,0,1,2,3,4,5"); + + checkTooltip(0, "http://example.com/#7", "1st tooltip is correct"); + checkTooltip(1, "title\nhttp://example.com/#8", "2nd tooltip is correct"); + checkTooltip(2, "http://example.com/#9", "3rd tooltip is correct"); +} + +function checkTooltip(aIndex, aExpected, aMessage) { + let link = getCell(aIndex).node.querySelector(".newtab-link"); + is(link.getAttribute("title"), aExpected, aMessage); +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug722273.js b/browser/base/content/test/newtab/browser_newtab_bug722273.js new file mode 100644 index 000000000..bc561b321 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug722273.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const NOW = Date.now() * 1000; +const URL = "http://fake-site.com/"; + +let tmp = {}; +Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tmp); + +let {Sanitizer} = tmp; + +function runTests() { + sanitizeHistory(); + yield addFakeVisits(); + yield addNewTabPageTab(); + + is(getCell(0).site.url, URL, "first site is our fake site"); + + whenPagesUpdated(); + yield sanitizeHistory(); + + ok(!getCell(0).site, "the fake site is gone"); +} + +function addFakeVisits() { + let visits = []; + for (let i = 59; i > 0; i--) { + visits.push({ + visitDate: NOW - i * 60 * 1000000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK + }); + } + let place = { + uri: makeURI(URL), + title: "fake site", + visits: visits + }; + PlacesUtils.asyncHistory.updatePlaces(place, { + handleError: function () ok(false, "couldn't add visit"), + handleResult: function () {}, + handleCompletion: function () { + NewTabUtils.links.populateCache(function () { + NewTabUtils.allPages.update(); + TestRunner.next(); + }, true); + } + }); +} + +function sanitizeHistory() { + let s = new Sanitizer(); + s.prefDomain = "privacy.cpd."; + + let prefs = gPrefService.getBranch(s.prefDomain); + prefs.setBoolPref("history", true); + prefs.setBoolPref("downloads", false); + prefs.setBoolPref("cache", false); + prefs.setBoolPref("cookies", false); + prefs.setBoolPref("formdata", false); + prefs.setBoolPref("offlineApps", false); + prefs.setBoolPref("passwords", false); + prefs.setBoolPref("sessions", false); + prefs.setBoolPref("siteSettings", false); + + s.sanitize(); +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug723102.js b/browser/base/content/test/newtab/browser_newtab_bug723102.js new file mode 100644 index 000000000..aa04b1150 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug723102.js @@ -0,0 +1,19 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function runTests() { + // create a new tab page and hide it. + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + let firstTab = gBrowser.selectedTab; + + yield addNewTabPageTab(); + gBrowser.removeTab(firstTab); + + ok(NewTabUtils.allPages.enabled, "page is enabled"); + NewTabUtils.allPages.enabled = false; + ok(getGrid().node.hasAttribute("page-disabled"), "page is disabled"); + NewTabUtils.allPages.enabled = true; +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug723121.js b/browser/base/content/test/newtab/browser_newtab_bug723121.js new file mode 100644 index 000000000..5ad8e7ca0 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug723121.js @@ -0,0 +1,30 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function runTests() { + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + checkGridLocked(false, "grid is unlocked"); + + let cell = getCell(0).node; + let site = getCell(0).site.node; + let link = site.querySelector(".newtab-link"); + + sendDragEvent("dragstart", link); + checkGridLocked(true, "grid is now locked"); + + sendDragEvent("dragend", link); + checkGridLocked(false, "grid isn't locked anymore"); + + sendDragEvent("dragstart", cell); + checkGridLocked(false, "grid isn't locked - dragstart was ignored"); + + sendDragEvent("dragstart", site); + checkGridLocked(false, "grid isn't locked - dragstart was ignored"); +} + +function checkGridLocked(aLocked, aMessage) { + is(getGrid().node.hasAttribute("locked"), aLocked, aMessage); +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug725996.js b/browser/base/content/test/newtab/browser_newtab_bug725996.js new file mode 100644 index 000000000..4d3ef7d5e --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug725996.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function runTests() { + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8"); + + let cell = getCell(0).node; + + sendDragEvent("drop", cell, "http://example.com/#99\nblank"); + is(NewTabUtils.pinnedLinks.links[0].url, "http://example.com/#99", + "first cell is pinned and contains the dropped site"); + + yield whenPagesUpdated(); + checkGrid("99p,0,1,2,3,4,5,6,7"); + + sendDragEvent("drop", cell, ""); + is(NewTabUtils.pinnedLinks.links[0].url, "http://example.com/#99", + "first cell is still pinned with the site we dropped before"); +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug734043.js b/browser/base/content/test/newtab/browser_newtab_bug734043.js new file mode 100644 index 000000000..dff7a14b4 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug734043.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function runTests() { + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8"); + + let receivedError = false; + let block = getContentDocument().querySelector(".newtab-control-block"); + + function onError() { + receivedError = true; + } + + let cw = getContentWindow(); + cw.addEventListener("error", onError); + + for (let i = 0; i < 3; i++) + EventUtils.synthesizeMouseAtCenter(block, {}, cw); + + yield whenPagesUpdated(); + ok(!receivedError, "we got here without any errors"); + cw.removeEventListener("error", onError); +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug735987.js b/browser/base/content/test/newtab/browser_newtab_bug735987.js new file mode 100644 index 000000000..8dda601b9 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug735987.js @@ -0,0 +1,26 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function runTests() { + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8"); + + yield simulateDrop(1); + checkGrid("0,99p,1,2,3,4,5,6,7"); + + yield blockCell(1); + checkGrid("0,1,2,3,4,5,6,7,8"); + + yield simulateDrop(1); + checkGrid("0,99p,1,2,3,4,5,6,7"); + + NewTabUtils.blockedLinks.resetCache(); + yield addNewTabPageTab(); + checkGrid("0,99p,1,2,3,4,5,6,7"); + + yield blockCell(1); + checkGrid("0,1,2,3,4,5,6,7,8"); +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug752841.js b/browser/base/content/test/newtab/browser_newtab_bug752841.js new file mode 100644 index 000000000..91c347b0c --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug752841.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_NEWTAB_ROWS = "browser.newtabpage.rows"; +const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns"; + +function runTests() { + let testValues = [ + {row: 0, column: 0}, + {row: -1, column: -1}, + {row: -1, column: 0}, + {row: 0, column: -1}, + {row: 2, column: 4}, + {row: 2, column: 5}, + ]; + + // Expected length of grid + let expectedValues = [1, 1, 1, 1, 8, 10]; + + // Values before setting new pref values (9 is the default value -> 3 x 3) + let previousValues = [9, 1, 1, 1, 1, 8]; + + let existingTab, existingTabGridLength, newTab, newTabGridLength; + yield addNewTabPageTab(); + existingTab = gBrowser.selectedTab; + + for (let i = 0; i < expectedValues.length; i++) { + gBrowser.selectedTab = existingTab; + existingTabGridLength = getGrid().cells.length; + is(existingTabGridLength, previousValues[i], + "Grid length of existing page before update is correctly."); + + Services.prefs.setIntPref(PREF_NEWTAB_ROWS, testValues[i].row); + Services.prefs.setIntPref(PREF_NEWTAB_COLUMNS, testValues[i].column); + + existingTabGridLength = getGrid().cells.length; + is(existingTabGridLength, expectedValues[i], + "Existing page grid is updated correctly."); + + yield addNewTabPageTab(); + newTab = gBrowser.selectedTab; + newTabGridLength = getGrid().cells.length; + is(newTabGridLength, expectedValues[i], + "New page grid is updated correctly."); + + gBrowser.removeTab(newTab); + } + + gBrowser.removeTab(existingTab); + + Services.prefs.clearUserPref(PREF_NEWTAB_ROWS); + Services.prefs.clearUserPref(PREF_NEWTAB_COLUMNS); +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug765628.js b/browser/base/content/test/newtab/browser_newtab_bug765628.js new file mode 100644 index 000000000..6b93c8e6d --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug765628.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const BAD_DRAG_DATA = "javascript:alert('h4ck0rz');\nbad stuff"; +const GOOD_DRAG_DATA = "http://example.com/#99\nsite 99"; + +function runTests() { + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8"); + + sendDropEvent(0, BAD_DRAG_DATA); + sendDropEvent(1, GOOD_DRAG_DATA); + + yield whenPagesUpdated(); + checkGrid("0,99p,1,2,3,4,5,6,7"); +} + +function sendDropEvent(aCellIndex, aDragData) { + let ifaceReq = getContentWindow().QueryInterface(Ci.nsIInterfaceRequestor); + let windowUtils = ifaceReq.getInterface(Ci.nsIDOMWindowUtils); + + let event = createDragEvent("drop", aDragData); + windowUtils.dispatchDOMEventViaPresShell(getCell(aCellIndex).node, event, true); +} diff --git a/browser/base/content/test/newtab/browser_newtab_bug876313.js b/browser/base/content/test/newtab/browser_newtab_bug876313.js new file mode 100644 index 000000000..ed9e8fbb3 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_bug876313.js @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * This test makes sure that the changes made by unpinning + * a site are actually written to NewTabUtils' storage. + */ +function runTests() { + // Second cell is pinned with page #99. + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(",99"); + + yield addNewTabPageTab(); + checkGrid("0,99p,1,2,3,4,5,6,7"); + + // Unpin the second cell's site. + yield unpinCell(1); + checkGrid("0,1,2,3,4,5,6,7,8"); + + // Clear the pinned cache to force NewTabUtils to read the pref again. + NewTabUtils.pinnedLinks.resetCache(); + NewTabUtils.allPages.update(); + checkGrid("0,1,2,3,4,5,6,7,8"); +} diff --git a/browser/base/content/test/newtab/browser_newtab_disable.js b/browser/base/content/test/newtab/browser_newtab_disable.js new file mode 100644 index 000000000..57aa59761 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_disable.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that the 'New Tab Page' feature can be disabled if the + * decides not to use it. + */ +function runTests() { + // create a new tab page and hide it. + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + let gridNode = getGrid().node; + + ok(!gridNode.hasAttribute("page-disabled"), "page is not disabled"); + + NewTabUtils.allPages.enabled = false; + ok(gridNode.hasAttribute("page-disabled"), "page is disabled"); + + let oldGridNode = gridNode; + + // create a second new tage page and make sure it's disabled. enable it + // again and check if the former page gets enabled as well. + yield addNewTabPageTab(); + ok(gridNode.hasAttribute("page-disabled"), "page is disabled"); + + // check that no sites have been rendered + is(0, getContentDocument().querySelectorAll(".site").length, "no sites have been rendered"); + + NewTabUtils.allPages.enabled = true; + ok(!gridNode.hasAttribute("page-disabled"), "page is not disabled"); + ok(!oldGridNode.hasAttribute("page-disabled"), "old page is not disabled"); +} diff --git a/browser/base/content/test/newtab/browser_newtab_drag_drop.js b/browser/base/content/test/newtab/browser_newtab_drag_drop.js new file mode 100644 index 000000000..1c64ddf72 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_drag_drop.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that dragging and dropping sites works as expected. + * Sites contained in the grid need to shift around to indicate the result + * of the drag-and-drop operation. If the grid is full and we're dragging + * a new site into it another one gets pushed out. + */ +function runTests() { + requestLongerTimeout(2); + + // test a simple drag-and-drop scenario + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8"); + + yield simulateDrop(1, 0); + checkGrid("1,0p,2,3,4,5,6,7,8"); + + // drag a cell to its current cell and make sure it's not pinned afterwards + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8"); + + yield simulateDrop(0, 0); + checkGrid("0,1,2,3,4,5,6,7,8"); + + // ensure that pinned pages aren't moved if that's not necessary + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(",1,2"); + + yield addNewTabPageTab(); + checkGrid("0,1p,2p,3,4,5,6,7,8"); + + yield simulateDrop(3, 0); + checkGrid("3,1p,2p,0p,4,5,6,7,8"); + + // pinned sites should always be moved around as blocks. if a pinned site is + // moved around, neighboring pinned are affected as well + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks("0,1"); + + yield addNewTabPageTab(); + checkGrid("0p,1p,2,3,4,5,6,7,8"); + + yield simulateDrop(0, 2); + checkGrid("2p,0p,1p,3,4,5,6,7,8"); + + // pinned sites should not be pushed out of the grid (unless there are only + // pinned ones left on the grid) + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(",,,,,,,7,8"); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7p,8p"); + + yield simulateDrop(8, 2); + checkGrid("0,1,3,4,5,6,7p,8p,2p"); + + // make sure that pinned sites are re-positioned correctly + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks("0,1,2,,,5"); + + yield addNewTabPageTab(); + checkGrid("0p,1p,2p,3,4,5p,6,7,8"); + + yield simulateDrop(4, 0); + checkGrid("3,1p,2p,4,0p,5p,6,7,8"); +} diff --git a/browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js b/browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js new file mode 100644 index 000000000..527ea2cc7 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_drag_drop_ext.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that dragging and dropping sites works as expected. + * Sites contained in the grid need to shift around to indicate the result + * of the drag-and-drop operation. If the grid is full and we're dragging + * a new site into it another one gets pushed out. + * This is a continuation of browser_newtab_drag_drop.js + * to decrease test run time, focusing on external sites. + */ +function runTests() { + // drag a new site onto the very first cell + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(",,,,,,,7,8"); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7p,8p"); + + yield simulateDrop(0); + checkGrid("99p,0,1,2,3,4,5,7p,8p"); + + // drag a new site onto the grid and make sure that pinned cells don't get + // pushed out + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(",,,,,,,7,8"); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7p,8p"); + + yield simulateDrop(7); + checkGrid("0,1,2,3,4,5,7p,99p,8p"); + + // drag a new site beneath a pinned cell and make sure the pinned cell is + // not moved + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(",,,,,,,,8"); + + yield addNewTabPageTab(); + checkGrid("0,1,2,3,4,5,6,7,8p"); + + yield simulateDrop(7); + checkGrid("0,1,2,3,4,5,6,99p,8p"); + + // drag a new site onto a block of pinned sites and make sure they're shifted + // around accordingly + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks("0,1,2,,,,,,"); + + yield addNewTabPageTab(); + checkGrid("0p,1p,2p"); + + yield simulateDrop(1); + checkGrid("0p,99p,1p,2p,3,4,5,6,7"); +}
\ No newline at end of file diff --git a/browser/base/content/test/newtab/browser_newtab_drop_preview.js b/browser/base/content/test/newtab/browser_newtab_drop_preview.js new file mode 100644 index 000000000..61c163d9d --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_drop_preview.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests ensure that the drop preview correctly arranges sites when + * dragging them around. + */ +function runTests() { + // the first three sites are pinned - make sure they're re-arranged correctly + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks("0,1,2,,,5"); + + yield addNewTabPageTab(); + checkGrid("0p,1p,2p,3,4,5p,6,7,8"); + + let cw = getContentWindow(); + cw.gDrag._draggedSite = getCell(0).site; + let sites = cw.gDropPreview.rearrange(getCell(4)); + cw.gDrag._draggedSite = null; + + checkGrid("3,1p,2p,4,0p,5p,6,7,8", sites); +} diff --git a/browser/base/content/test/newtab/browser_newtab_focus.js b/browser/base/content/test/newtab/browser_newtab_focus.js new file mode 100644 index 000000000..e841d3537 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_focus.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that focusing the 'New Tage Page' works as expected. + */ +function runTests() { + // Handle the OSX full keyboard access setting + Services.prefs.setIntPref("accessibility.tabfocus", 7); + + // Focus count in new tab page. + // 28 = 9 * 3 + 1 = 9 sites and 1 toggle button, each site has a link, a pin + // and a remove button. + let FOCUS_COUNT = 28; + + // Create a new tab page. + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + gURLBar.focus(); + + // Count the focus with the enabled page. + yield countFocus(FOCUS_COUNT); + + // Disable page and count the focus with the disabled page. + NewTabUtils.allPages.enabled = false; + yield countFocus(1); + + Services.prefs.clearUserPref("accessibility.tabfocus"); + NewTabUtils.allPages.enabled = true; +} + +/** + * Focus the urlbar and count how many focus stops to return again to the urlbar. + */ +function countFocus(aExpectedCount) { + let focusCount = 0; + let contentDoc = getContentDocument(); + + window.addEventListener("focus", function onFocus() { + let focusedElement = document.commandDispatcher.focusedElement; + if (focusedElement && focusedElement.classList.contains("urlbar-input")) { + window.removeEventListener("focus", onFocus, true); + is(focusCount, aExpectedCount, "Validate focus count in the new tab page."); + executeSoon(TestRunner.next); + } else { + if (focusedElement && focusedElement.ownerDocument == contentDoc && + focusedElement instanceof HTMLElement) { + focusCount++; + } + document.commandDispatcher.advanceFocus(); + } + }, true); + + document.commandDispatcher.advanceFocus(); +} diff --git a/browser/base/content/test/newtab/browser_newtab_perwindow_private_browsing.js b/browser/base/content/test/newtab/browser_newtab_perwindow_private_browsing.js new file mode 100644 index 000000000..68717a304 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_perwindow_private_browsing.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests ensure that all changes made to the new tab page in private + * browsing mode are discarded after switching back to normal mode again. + * The private browsing mode should start with the current grid shown in normal + * mode. + */ + +function runTests() { + // prepare the grid + yield testOnWindow(undefined); + yield setLinks("0,1,2,3,4,5,6,7,8,9"); + + yield addNewTabPageTab(); + pinCell(0); + checkGrid("0p,1,2,3,4,5,6,7,8"); + + // open private window + yield testOnWindow({private: true}); + + yield addNewTabPageTab(); + checkGrid("0p,1,2,3,4,5,6,7,8"); + + // modify the grid while we're in pb mode + yield blockCell(1); + checkGrid("0p,2,3,4,5,6,7,8"); + + yield unpinCell(0); + checkGrid("0,2,3,4,5,6,7,8"); + + // open normal window + yield testOnWindow(undefined); + + // check that the grid is the same as before entering pb mode + yield addNewTabPageTab(); + checkGrid("0,2,3,4,5,6,7,8") +} + +var windowsToClose = []; +function testOnWindow(options) { + var win = OpenBrowserWindow(options); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad, false); + windowsToClose.push(win); + gWindow = win; + whenDelayedStartupFinished(win, TestRunner.next); + }, false); +} + +function whenDelayedStartupFinished(win, callback) { + const topic = "browser-delayed-startup-finished"; + Services.obs.addObserver(function onStartup(subject) { + if (win == subject) { + Services.obs.removeObserver(onStartup, topic); + executeSoon(callback); + } + }, topic, false); +} + +registerCleanupFunction(function () { + gWindow = window; + windowsToClose.forEach(function(win) { + win.close(); + }); +}); + diff --git a/browser/base/content/test/newtab/browser_newtab_reset.js b/browser/base/content/test/newtab/browser_newtab_reset.js new file mode 100644 index 000000000..3503fbb8d --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_reset.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that resetting the 'New Tage Page' works as expected. + */ +function runTests() { + // Disabled until bug 716543 is fixed. + return; + + // create a new tab page and check its modified state after blocking a site + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + let resetButton = getContentDocument().getElementById("toolbar-button-reset"); + + checkGrid("0,1,2,3,4,5,6,7,8"); + ok(!resetButton.hasAttribute("modified"), "page is not modified"); + + yield blockCell(4); + checkGrid("0,1,2,3,5,6,7,8,"); + ok(resetButton.hasAttribute("modified"), "page is modified"); + + yield getContentWindow().gToolbar.reset(TestRunner.next); + checkGrid("0,1,2,3,4,5,6,7,8"); + ok(!resetButton.hasAttribute("modified"), "page is not modified"); +} diff --git a/browser/base/content/test/newtab/browser_newtab_tabsync.js b/browser/base/content/test/newtab/browser_newtab_tabsync.js new file mode 100644 index 000000000..2ffd11b30 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_tabsync.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that all changes that are made to a specific + * 'New Tab Page' are synchronized with all other open 'New Tab Pages' + * automatically. All about:newtab pages should always be in the same + * state. + */ +function runTests() { + // Disabled until bug 716543 is fixed. + return; + + yield setLinks("0,1,2,3,4,5,6,7,8,9"); + setPinnedLinks(",1"); + + yield addNewTabPageTab(); + checkGrid("0,1p,2,3,4,5,6,7,8"); + + let resetButton = getContentDocument().getElementById("toolbar-button-reset"); + ok(!resetButton.hasAttribute("modified"), "page is not modified"); + + let oldSites = getGrid().sites; + let oldResetButton = resetButton; + + // create the new tab page + yield addNewTabPageTab(); + checkGrid("0,1p,2,3,4,5,6,7,8"); + + resetButton = getContentDocument().getElementById("toolbar-button-reset"); + ok(!resetButton.hasAttribute("modified"), "page is not modified"); + + // unpin a cell + yield unpinCell(1); + checkGrid("0,1,2,3,4,5,6,7,8"); + checkGrid("0,1,2,3,4,5,6,7,8", oldSites); + + // remove a cell + yield blockCell(1); + checkGrid("0,2,3,4,5,6,7,8,9"); + checkGrid("0,2,3,4,5,6,7,8,9", oldSites); + ok(resetButton.hasAttribute("modified"), "page is modified"); + ok(oldResetButton.hasAttribute("modified"), "page is modified"); + + // insert a new cell by dragging + yield simulateDrop(1); + checkGrid("0,99p,2,3,4,5,6,7,8"); + checkGrid("0,99p,2,3,4,5,6,7,8", oldSites); + + // drag a cell around + yield simulateDrop(1, 2); + checkGrid("0,2p,99p,3,4,5,6,7,8"); + checkGrid("0,2p,99p,3,4,5,6,7,8", oldSites); + + // reset the new tab page + yield getContentWindow().gToolbar.reset(TestRunner.next); + checkGrid("0,1,2,3,4,5,6,7,8"); + checkGrid("0,1,2,3,4,5,6,7,8", oldSites); + ok(!resetButton.hasAttribute("modified"), "page is not modified"); + ok(!oldResetButton.hasAttribute("modified"), "page is not modified"); +} diff --git a/browser/base/content/test/newtab/browser_newtab_undo.js b/browser/base/content/test/newtab/browser_newtab_undo.js new file mode 100644 index 000000000..bc0eb3df2 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_undo.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that the undo dialog works as expected. + */ +function runTests() { + // remove unpinned sites and undo it + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks("5"); + + yield addNewTabPageTab(); + checkGrid("5p,0,1,2,3,4,6,7,8"); + + yield blockCell(4); + yield blockCell(4); + checkGrid("5p,0,1,2,6,7,8"); + + yield undo(); + checkGrid("5p,0,1,2,4,6,7,8"); + + // now remove a pinned site and undo it + yield blockCell(0); + checkGrid("0,1,2,4,6,7,8"); + + yield undo(); + checkGrid("5p,0,1,2,4,6,7,8"); + + // remove a site and restore all + yield blockCell(1); + checkGrid("5p,1,2,4,6,7,8"); + + yield undoAll(); + checkGrid("5p,0,1,2,3,4,6,7,8"); +} + +function undo() { + let cw = getContentWindow(); + let target = cw.document.getElementById("newtab-undo-button"); + EventUtils.synthesizeMouseAtCenter(target, {}, cw); + whenPagesUpdated(); +} + +function undoAll() { + let cw = getContentWindow(); + let target = cw.document.getElementById("newtab-undo-restore-button"); + EventUtils.synthesizeMouseAtCenter(target, {}, cw); + whenPagesUpdated(); +}
\ No newline at end of file diff --git a/browser/base/content/test/newtab/browser_newtab_unpin.js b/browser/base/content/test/newtab/browser_newtab_unpin.js new file mode 100644 index 000000000..6d2d45b1e --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_unpin.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that when a site gets unpinned it is either moved to + * its actual place in the grid or removed in case it's not on the grid anymore. + */ +function runTests() { + // we have a pinned link that didn't change its position since it was pinned. + // nothing should happend when we unpin it. + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(",1"); + + yield addNewTabPageTab(); + checkGrid("0,1p,2,3,4,5,6,7,8"); + + yield unpinCell(1); + checkGrid("0,1,2,3,4,5,6,7,8"); + + // we have a pinned link that is not anymore in the list of the most-visited + // links. this should disappear, the remaining links adjust their positions + // and a new link will appear at the end of the grid. + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(",99"); + + yield addNewTabPageTab(); + checkGrid("0,99p,1,2,3,4,5,6,7"); + + yield unpinCell(1); + checkGrid("0,1,2,3,4,5,6,7,8"); + + // we have a pinned link that changed its position since it was pinned. it + // should be moved to its new position after being unpinned. + yield setLinks("0,1,2,3,4,5,6,7"); + setPinnedLinks(",1,,,,,,,0"); + + yield addNewTabPageTab(); + checkGrid("2,1p,3,4,5,6,7,,0p"); + + yield unpinCell(1); + checkGrid("1,2,3,4,5,6,7,,0p"); + + yield unpinCell(8); + checkGrid("0,1,2,3,4,5,6,7,"); + + // we have pinned link that changed its position since it was pinned. the + // link will disappear from the grid because it's now a much lower priority + yield setLinks("0,1,2,3,4,5,6,7,8,9"); + setPinnedLinks("9"); + + yield addNewTabPageTab(); + checkGrid("9p,0,1,2,3,4,5,6,7"); + + yield unpinCell(0); + checkGrid("0,1,2,3,4,5,6,7,8"); +} diff --git a/browser/base/content/test/newtab/head.js b/browser/base/content/test/newtab/head.js new file mode 100644 index 000000000..b14d20f27 --- /dev/null +++ b/browser/base/content/test/newtab/head.js @@ -0,0 +1,390 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled"; + +Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true); + +let tmp = {}; +Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp); +Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tmp); + +let {NewTabUtils, Sanitizer} = tmp; + +let uri = Services.io.newURI("about:newtab", null, null); +let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri); + +let gWindow = window; + +registerCleanupFunction(function () { + while (gWindow.gBrowser.tabs.length > 1) + gWindow.gBrowser.removeTab(gWindow.gBrowser.tabs[1]); + + Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED); +}); + +/** + * Provide the default test function to start our test runner. + */ +function test() { + TestRunner.run(); +} + +/** + * The test runner that controls the execution flow of our tests. + */ +let TestRunner = { + /** + * Starts the test runner. + */ + run: function () { + waitForExplicitFinish(); + + this._iter = runTests(); + this.next(); + }, + + /** + * Runs the next available test or finishes if there's no test left. + */ + next: function () { + try { + TestRunner._iter.next(); + } catch (e if e instanceof StopIteration) { + TestRunner.finish(); + } + }, + + /** + * Finishes all tests and cleans up. + */ + finish: function () { + function cleanupAndFinish() { + clearHistory(function () { + whenPagesUpdated(finish); + NewTabUtils.restore(); + }); + } + + let callbacks = NewTabUtils.links._populateCallbacks; + let numCallbacks = callbacks.length; + + if (numCallbacks) + callbacks.splice(0, numCallbacks, cleanupAndFinish); + else + cleanupAndFinish(); + } +}; + +/** + * Returns the selected tab's content window. + * @return The content window. + */ +function getContentWindow() { + return gWindow.gBrowser.selectedBrowser.contentWindow; +} + +/** + * Returns the selected tab's content document. + * @return The content document. + */ +function getContentDocument() { + return gWindow.gBrowser.selectedBrowser.contentDocument; +} + +/** + * Returns the newtab grid of the selected tab. + * @return The newtab grid. + */ +function getGrid() { + return getContentWindow().gGrid; +} + +/** + * Returns the cell at the given index of the selected tab's newtab grid. + * @param aIndex The cell index. + * @return The newtab cell. + */ +function getCell(aIndex) { + return getGrid().cells[aIndex]; +} + +/** + * Allows to provide a list of links that is used to construct the grid. + * @param aLinksPattern the pattern (see below) + * + * Example: setLinks("1,2,3") + * Result: [{url: "http://example.com/#1", title: "site#1"}, + * {url: "http://example.com/#2", title: "site#2"} + * {url: "http://example.com/#3", title: "site#3"}] + */ +function setLinks(aLinks) { + let links = aLinks; + + if (typeof links == "string") { + links = aLinks.split(/\s*,\s*/).map(function (id) { + return {url: "http://example.com/#" + id, title: "site#" + id}; + }); + } + + // Call populateCache() once to make sure that all link fetching that is + // currently in progress has ended. We clear the history, fill it with the + // given entries and call populateCache() now again to make sure the cache + // has the desired contents. + NewTabUtils.links.populateCache(function () { + clearHistory(function () { + fillHistory(links, function () { + NewTabUtils.links.populateCache(function () { + NewTabUtils.allPages.update(); + TestRunner.next(); + }, true); + }); + }); + }); +} + +function clearHistory(aCallback) { + Services.obs.addObserver(function observe(aSubject, aTopic, aData) { + Services.obs.removeObserver(observe, aTopic); + executeSoon(aCallback); + }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); + + PlacesUtils.history.removeAllPages(); +} + +function fillHistory(aLinks, aCallback) { + let numLinks = aLinks.length; + let transitionLink = Ci.nsINavHistoryService.TRANSITION_LINK; + + for (let link of aLinks.reverse()) { + let place = { + uri: makeURI(link.url), + title: link.title, + visits: [{visitDate: Date.now() * 1000, transitionType: transitionLink}] + }; + + PlacesUtils.asyncHistory.updatePlaces(place, { + handleError: function () ok(false, "couldn't add visit to history"), + handleResult: function () {}, + handleCompletion: function () { + if (--numLinks == 0) + aCallback(); + } + }); + } +} + +/** + * Allows to specify the list of pinned links (that have a fixed position in + * the grid. + * @param aLinksPattern the pattern (see below) + * + * Example: setPinnedLinks("3,,1") + * Result: 'http://example.com/#3' is pinned in the first cell. 'http://example.com/#1' is + * pinned in the third cell. + */ +function setPinnedLinks(aLinks) { + let links = aLinks; + + if (typeof links == "string") { + links = aLinks.split(/\s*,\s*/).map(function (id) { + if (id) + return {url: "http://example.com/#" + id, title: "site#" + id}; + }); + } + + let string = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(links); + Services.prefs.setComplexValue("browser.newtabpage.pinned", + Ci.nsISupportsString, string); + + NewTabUtils.pinnedLinks.resetCache(); + NewTabUtils.allPages.update(); +} + +/** + * Restore the grid state. + */ +function restore() { + whenPagesUpdated(); + NewTabUtils.restore(); +} + +/** + * Creates a new tab containing 'about:newtab'. + */ +function addNewTabPageTab() { + let tab = gWindow.gBrowser.selectedTab = gWindow.gBrowser.addTab("about:newtab"); + let browser = tab.linkedBrowser; + + function whenNewTabLoaded() { + if (NewTabUtils.allPages.enabled) { + // Continue when the link cache has been populated. + NewTabUtils.links.populateCache(function () { + executeSoon(TestRunner.next); + }); + } else { + // It's important that we call next() asynchronously. + // 'yield addNewTabPageTab()' would fail if next() is called + // synchronously because the iterator is already executing. + executeSoon(TestRunner.next); + } + } + + // The new tab page might have been preloaded in the background. + if (browser.contentDocument.readyState == "complete") { + whenNewTabLoaded(); + return; + } + + // Wait for the new tab page to be loaded. + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + whenNewTabLoaded(); + }, true); +} + +/** + * Compares the current grid arrangement with the given pattern. + * @param the pattern (see below) + * @param the array of sites to compare with (optional) + * + * Example: checkGrid("3p,2,,1p") + * Result: We expect the first cell to contain the pinned site 'http://example.com/#3'. + * The second cell contains 'http://example.com/#2'. The third cell is empty. + * The fourth cell contains the pinned site 'http://example.com/#4'. + */ +function checkGrid(aSitesPattern, aSites) { + let length = aSitesPattern.split(",").length; + let sites = (aSites || getGrid().sites).slice(0, length); + let current = sites.map(function (aSite) { + if (!aSite) + return ""; + + let pinned = aSite.isPinned(); + let pinButton = aSite.node.querySelector(".newtab-control-pin"); + let hasPinnedAttr = pinButton.hasAttribute("pinned"); + + if (pinned != hasPinnedAttr) + ok(false, "invalid state (site.isPinned() != site[pinned])"); + + return aSite.url.replace(/^http:\/\/example\.com\/#(\d+)$/, "$1") + (pinned ? "p" : ""); + }); + + is(current, aSitesPattern, "grid status = " + aSitesPattern); +} + +/** + * Blocks a site from the grid. + * @param aIndex The cell index. + */ +function blockCell(aIndex) { + whenPagesUpdated(); + getCell(aIndex).site.block(); +} + +/** + * Pins a site on a given position. + * @param aIndex The cell index. + * @param aPinIndex The index the defines where the site should be pinned. + */ +function pinCell(aIndex, aPinIndex) { + getCell(aIndex).site.pin(aPinIndex); +} + +/** + * Unpins the given cell's site. + * @param aIndex The cell index. + */ +function unpinCell(aIndex) { + whenPagesUpdated(); + getCell(aIndex).site.unpin(); +} + +/** + * Simulates a drop and drop operation. + * @param aDropIndex The cell index of the drop target. + * @param aDragIndex The cell index containing the dragged site (optional). + */ +function simulateDrop(aDropIndex, aDragIndex) { + let draggedSite; + let {gDrag: drag, gDrop: drop} = getContentWindow(); + let event = createDragEvent("drop", "http://example.com/#99\nblank"); + + if (typeof aDragIndex != "undefined") + draggedSite = getCell(aDragIndex).site; + + if (draggedSite) + drag.start(draggedSite, event); + + whenPagesUpdated(); + drop.drop(getCell(aDropIndex), event); + + if (draggedSite) + drag.end(draggedSite); +} + +/** + * Sends a custom drag event to a given DOM element. + * @param aEventType The drag event's type. + * @param aTarget The DOM element that the event is dispatched to. + * @param aData The event's drag data (optional). + */ +function sendDragEvent(aEventType, aTarget, aData) { + let event = createDragEvent(aEventType, aData); + let ifaceReq = getContentWindow().QueryInterface(Ci.nsIInterfaceRequestor); + let windowUtils = ifaceReq.getInterface(Ci.nsIDOMWindowUtils); + windowUtils.dispatchDOMEventViaPresShell(aTarget, event, true); +} + +/** + * Creates a custom drag event. + * @param aEventType The drag event's type. + * @param aData The event's drag data (optional). + * @return The drag event. + */ +function createDragEvent(aEventType, aData) { + let dataTransfer = { + mozUserCancelled: false, + setData: function () null, + setDragImage: function () null, + getData: function () aData, + + types: { + contains: function (aType) aType == "text/x-moz-url" + }, + + mozGetDataAt: function (aType, aIndex) { + if (aIndex || aType != "text/x-moz-url") + return null; + + return aData; + } + }; + + let event = getContentDocument().createEvent("DragEvents"); + event.initDragEvent(aEventType, true, true, getContentWindow(), 0, 0, 0, 0, 0, + false, false, false, false, 0, null, dataTransfer); + + return event; +} + +/** + * Resumes testing when all pages have been updated. + */ +function whenPagesUpdated(aCallback) { + let page = { + update: function () { + NewTabUtils.allPages.unregister(this); + executeSoon(aCallback || TestRunner.next); + } + }; + + NewTabUtils.allPages.register(page); + registerCleanupFunction(function () { + NewTabUtils.allPages.unregister(page); + }); +} diff --git a/browser/base/content/test/newtab/moz.build b/browser/base/content/test/newtab/moz.build new file mode 100644 index 000000000..895d11993 --- /dev/null +++ b/browser/base/content/test/newtab/moz.build @@ -0,0 +1,6 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + diff --git a/browser/base/content/test/offlineChild.cacheManifest b/browser/base/content/test/offlineChild.cacheManifest new file mode 100644 index 000000000..091fe7194 --- /dev/null +++ b/browser/base/content/test/offlineChild.cacheManifest @@ -0,0 +1,2 @@ +CACHE MANIFEST +offlineChild.html diff --git a/browser/base/content/test/offlineChild.cacheManifest^headers^ b/browser/base/content/test/offlineChild.cacheManifest^headers^ new file mode 100644 index 000000000..257f2eb60 --- /dev/null +++ b/browser/base/content/test/offlineChild.cacheManifest^headers^ @@ -0,0 +1 @@ +Content-Type: text/cache-manifest diff --git a/browser/base/content/test/offlineChild.html b/browser/base/content/test/offlineChild.html new file mode 100644 index 000000000..43f225b3b --- /dev/null +++ b/browser/base/content/test/offlineChild.html @@ -0,0 +1,20 @@ +<html manifest="offlineChild.cacheManifest"> +<head> +<title></title> +<script type="text/javascript"> + +function finish(success) { + window.parent.postMessage(success ? "success" : "failure", "*"); +} + +applicationCache.oncached = function() { finish(true); } +applicationCache.onnoupdate = function() { finish(true); } +applicationCache.onerror = function() { finish(false); } + +</script> +</head> + +<body> +<h1>Child</h1> +</body> +</html> diff --git a/browser/base/content/test/offlineChild2.cacheManifest b/browser/base/content/test/offlineChild2.cacheManifest new file mode 100644 index 000000000..19efe54fe --- /dev/null +++ b/browser/base/content/test/offlineChild2.cacheManifest @@ -0,0 +1,2 @@ +CACHE MANIFEST +offlineChild2.html diff --git a/browser/base/content/test/offlineChild2.cacheManifest^headers^ b/browser/base/content/test/offlineChild2.cacheManifest^headers^ new file mode 100644 index 000000000..257f2eb60 --- /dev/null +++ b/browser/base/content/test/offlineChild2.cacheManifest^headers^ @@ -0,0 +1 @@ +Content-Type: text/cache-manifest diff --git a/browser/base/content/test/offlineChild2.html b/browser/base/content/test/offlineChild2.html new file mode 100644 index 000000000..ac762e759 --- /dev/null +++ b/browser/base/content/test/offlineChild2.html @@ -0,0 +1,20 @@ +<html manifest="offlineChild2.cacheManifest"> +<head> +<title></title> +<script type="text/javascript"> + +function finish(success) { + window.parent.postMessage(success ? "success" : "failure", "*"); +} + +applicationCache.oncached = function() { finish(true); } +applicationCache.onnoupdate = function() { finish(true); } +applicationCache.onerror = function() { finish(false); } + +</script> +</head> + +<body> +<h1>Child</h1> +</body> +</html> diff --git a/browser/base/content/test/offlineEvent.cacheManifest b/browser/base/content/test/offlineEvent.cacheManifest new file mode 100644 index 000000000..091fe7194 --- /dev/null +++ b/browser/base/content/test/offlineEvent.cacheManifest @@ -0,0 +1,2 @@ +CACHE MANIFEST +offlineChild.html diff --git a/browser/base/content/test/offlineEvent.cacheManifest^headers^ b/browser/base/content/test/offlineEvent.cacheManifest^headers^ new file mode 100644 index 000000000..257f2eb60 --- /dev/null +++ b/browser/base/content/test/offlineEvent.cacheManifest^headers^ @@ -0,0 +1 @@ +Content-Type: text/cache-manifest diff --git a/browser/base/content/test/offlineEvent.html b/browser/base/content/test/offlineEvent.html new file mode 100644 index 000000000..f6e2494e2 --- /dev/null +++ b/browser/base/content/test/offlineEvent.html @@ -0,0 +1,9 @@ +<html manifest="offlineEvent.cacheManifest"> +<head> +<title></title> +</head> + +<body> +<h1>Child</h1> +</body> +</html> diff --git a/browser/base/content/test/offlineQuotaNotification.cacheManifest b/browser/base/content/test/offlineQuotaNotification.cacheManifest new file mode 100644 index 000000000..2e210abd2 --- /dev/null +++ b/browser/base/content/test/offlineQuotaNotification.cacheManifest @@ -0,0 +1,7 @@ +CACHE MANIFEST +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +# store a "large" file so an "over quota warning" will be issued - any file +# larger than 1kb and in '_BROWSER_FILES' should be right... +title_test.svg diff --git a/browser/base/content/test/offlineQuotaNotification.html b/browser/base/content/test/offlineQuotaNotification.html new file mode 100644 index 000000000..b1b91bf9e --- /dev/null +++ b/browser/base/content/test/offlineQuotaNotification.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html manifest="offlineQuotaNotification.cacheManifest"> +<head> + <meta charset="utf-8"> + <title>Test offline app quota notification</title> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +</head> +</html> diff --git a/browser/base/content/test/page_style_sample.html b/browser/base/content/test/page_style_sample.html new file mode 100644 index 000000000..56637a58d --- /dev/null +++ b/browser/base/content/test/page_style_sample.html @@ -0,0 +1,40 @@ +<html> + <head> + <title>Test for page style menu</title> + <!-- data-state values: + 0: should not appear in the page style menu + 0-todo: should not appear in the page style menu, but does + 1: should appear in the page style menu + 2: should appear in the page style menu as the selected stylesheet --> + <link data-state="1" href="404.css" title="1" rel="alternate stylesheet"> + <link data-state="0" title="2" rel="alternate stylesheet"> + <link data-state="0" href="404.css" rel="alternate stylesheet"> + <link data-state="0" href="404.css" title="" rel="alternate stylesheet"> + <link data-state="1" href="404.css" title="3" rel="stylesheet alternate"> + <link data-state="1" href="404.css" title="4" rel=" alternate stylesheet "> + <link data-state="1" href="404.css" title="5" rel="alternate stylesheet"> + <link data-state="2" href="404.css" title="6" rel="stylesheet"> + <link data-state="1" href="404.css" title="7" rel="foo stylesheet"> + <link data-state="0" href="404.css" title="8" rel="alternate"> + <link data-state="1" href="404.css" title="9" rel="alternate STYLEsheet"> + <link data-state="1" href="404.css" title="10" rel="alternate stylesheet" media=""> + <link data-state="1" href="404.css" title="11" rel="alternate stylesheet" media="all"> + <link data-state="1" href="404.css" title="12" rel="alternate stylesheet" media="ALL "> + <link data-state="1" href="404.css" title="13" rel="alternate stylesheet" media="screen"> + <link data-state="1" href="404.css" title="14" rel="alternate stylesheet" media=" Screen"> + <link data-state="0" href="404.css" title="15" rel="alternate stylesheet" media="screen foo"> + <link data-state="0" href="404.css" title="16" rel="alternate stylesheet" media="all screen"> + <link data-state="0" href="404.css" title="17" rel="alternate stylesheet" media="foo bar"> + <link data-state="1" href="404.css" title="18" rel="alternate stylesheet" media="all,screen"> + <link data-state="1" href="404.css" title="19" rel="alternate stylesheet" media="all, screen"> + <link data-state="0" href="404.css" title="20" rel="alternate stylesheet" media="all screen"> + <link data-state="0" href="404.css" title="21" rel="alternate stylesheet" media="foo"> + <link data-state="0" href="404.css" title="22" rel="alternate stylesheet" media="allscreen"> + <link data-state="0" href="404.css" title="23" rel="alternate stylesheet" media="_all"> + <link data-state="0" href="404.css" title="24" rel="alternate stylesheet" media="not screen"> + <link data-state="1" href="404.css" title="25" rel="alternate stylesheet" media="only screen"> + <link data-state="1" href="404.css" title="26" rel="alternate stylesheet" media="screen and (min-device-width: 1px)"> + <link data-state="0" href="404.css" title="27" rel="alternate stylesheet" media="screen and (max-device-width: 1px)"> + </head> + <body></body> +</html> diff --git a/browser/base/content/test/pluginCrashCommentAndURL.html b/browser/base/content/test/pluginCrashCommentAndURL.html new file mode 100644 index 000000000..711a19ed3 --- /dev/null +++ b/browser/base/content/test/pluginCrashCommentAndURL.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <script type="text/javascript"> + function crash() { + var plugin = document.getElementById("plugin"); + var argStr = decodeURIComponent(window.location.search.substr(1)); + if (argStr) { + var args = JSON.parse(argStr); + for (var key in args) + plugin.setAttribute(key, args[key]); + } + try { + plugin.crash(); + } + catch (err) {} + } + </script> + </head> + <body onload="crash();"> + <embed id="plugin" type="application/x-test" + width="400" height="400" + drawmode="solid" color="FF00FFFF"> + </embed> + </body> +</html> diff --git a/browser/base/content/test/plugin_add_dynamically.html b/browser/base/content/test/plugin_add_dynamically.html new file mode 100644 index 000000000..3fdaf110c --- /dev/null +++ b/browser/base/content/test/plugin_add_dynamically.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> +<script> +function addPlugin(type="application/x-test") { + var embed = document.createElement("embed"); + embed.style.width = "200px"; + embed.style.height = "200px"; + embed.setAttribute("type", type); + return document.body.appendChild(embed); +} +</script> +</body> +</html> diff --git a/browser/base/content/test/plugin_alternate_content.html b/browser/base/content/test/plugin_alternate_content.html new file mode 100644 index 000000000..f8acc833c --- /dev/null +++ b/browser/base/content/test/plugin_alternate_content.html @@ -0,0 +1,9 @@ +<!-- bug 739575 --> +<html> +<head><meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> +</head> +<body> +<object id="test" type="application/x-test" style="height: 200px; width:200px"> +<p><a href="about:blank">you should not see this link when plugins are click-to-play</a></p> +</object> +</body></html> diff --git a/browser/base/content/test/plugin_both.html b/browser/base/content/test/plugin_both.html new file mode 100644 index 000000000..2f3d2efe8 --- /dev/null +++ b/browser/base/content/test/plugin_both.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head>
+<meta charset="utf-8">
+</head> +<body> +<embed id="unknown" style="width: 100px; height: 100px" type="application/x-unknown"> +<embed id="test" style="width: 100px; height: 100px" type="application/x-test"> +</body> +</html> diff --git a/browser/base/content/test/plugin_both2.html b/browser/base/content/test/plugin_both2.html new file mode 100644 index 000000000..ba605d6e8 --- /dev/null +++ b/browser/base/content/test/plugin_both2.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head>
+<meta charset="utf-8">
+</head> +<body> +<embed id="test" style="width: 100px; height: 100px" type="application/x-test"> +<embed id="unknown" style="width: 100px; height: 100px" type="application/x-unknown"> +</body> +</html> diff --git a/browser/base/content/test/plugin_bug744745.html b/browser/base/content/test/plugin_bug744745.html new file mode 100644 index 000000000..d0691c9c0 --- /dev/null +++ b/browser/base/content/test/plugin_bug744745.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head><meta charset="utf-8"/></head> +<body> +<style> +.x { + opacity: 0 !important; +} +</style> +<object id="test" class="x" type="application/x-test" width=200 height=200></object> +</body> +</html> diff --git a/browser/base/content/test/plugin_bug749455.html b/browser/base/content/test/plugin_bug749455.html new file mode 100644 index 000000000..831dc82f7 --- /dev/null +++ b/browser/base/content/test/plugin_bug749455.html @@ -0,0 +1,8 @@ +<!-- bug 749455 --> +<html> +<head><meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> +</head> +<body> +<embed src="plugin_bug749455.html" type="application/x-test" width="100px" height="100px"></embed> +</body> +</html> diff --git a/browser/base/content/test/plugin_bug752516.html b/browser/base/content/test/plugin_bug752516.html new file mode 100644 index 000000000..6121e7068 --- /dev/null +++ b/browser/base/content/test/plugin_bug752516.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"/> + <style type="text/css"> + div { + padding: 2%; + position: absolute; + top: 0; bottom: 0; + left: 0; right: 0; + text-align: center; + border: 4px solid red; + } + </style> +</head> +<body> + <div id="container"> + <object id="test" type="application/x-test" width="159" height="91"></object> + </div> + <div id="overlay"> + <h1>overlay</h1> + </div> +</body> +</html> diff --git a/browser/base/content/test/plugin_bug787619.html b/browser/base/content/test/plugin_bug787619.html new file mode 100644 index 000000000..cb91116f0 --- /dev/null +++ b/browser/base/content/test/plugin_bug787619.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head><meta charset="utf-8"/></head> +<body> + <a id="wrapper"> + <embed id="plugin" style="width: 200px; height: 200px" type="application/x-test"> + </a> +</body> +</html> diff --git a/browser/base/content/test/plugin_bug797677.html b/browser/base/content/test/plugin_bug797677.html new file mode 100644 index 000000000..1545f3647 --- /dev/null +++ b/browser/base/content/test/plugin_bug797677.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html> +<head><meta charset="utf-8"/></head> +<body><embed id="plugin" type="9000"></embed></body> +</html> diff --git a/browser/base/content/test/plugin_bug820497.html b/browser/base/content/test/plugin_bug820497.html new file mode 100644 index 000000000..4884e9dbe --- /dev/null +++ b/browser/base/content/test/plugin_bug820497.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head><meta charset="utf-8"/></head> +<body> +<object id="test" type="application/x-test" width=200 height=200></object> +<script> + function addSecondPlugin() { + var object = document.createElement("object"); + object.type = "application/x-second-test"; + object.width = 200; + object.height = 200; + object.id = "secondtest"; + document.body.appendChild(object); + } +</script> +</body> +</html> diff --git a/browser/base/content/test/plugin_clickToPlayAllow.html b/browser/base/content/test/plugin_clickToPlayAllow.html new file mode 100644 index 000000000..3f5df1984 --- /dev/null +++ b/browser/base/content/test/plugin_clickToPlayAllow.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head>
+<meta charset="utf-8">
+</head> +<body> +<embed id="test" style="width: 200px; height: 200px" type="application/x-test"> +</body> +</html> diff --git a/browser/base/content/test/plugin_clickToPlayDeny.html b/browser/base/content/test/plugin_clickToPlayDeny.html new file mode 100644 index 000000000..3f5df1984 --- /dev/null +++ b/browser/base/content/test/plugin_clickToPlayDeny.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head>
+<meta charset="utf-8">
+</head> +<body> +<embed id="test" style="width: 200px; height: 200px" type="application/x-test"> +</body> +</html> diff --git a/browser/base/content/test/plugin_data_url.html b/browser/base/content/test/plugin_data_url.html new file mode 100644 index 000000000..77e101144 --- /dev/null +++ b/browser/base/content/test/plugin_data_url.html @@ -0,0 +1,11 @@ +<html> +<body> + <a id="data-link-1" href='data:text/html,<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/>'> + data: with one plugin + </a><br /> + <a id="data-link-2" href='data:text/html,<embed id="test1" style="width: 200px; height: 200px" type="application/x-test"/><embed id="test2" style="width: 200px; height: 200px" type="application/x-second-test"/>'> + data: with two plugins + </a><br /> + <object id="test" style="width: 200px; height: 200px" type="application/x-test"></object> +</body> +</html> diff --git a/browser/base/content/test/plugin_hidden_to_visible.html b/browser/base/content/test/plugin_hidden_to_visible.html new file mode 100644 index 000000000..e8d92c68c --- /dev/null +++ b/browser/base/content/test/plugin_hidden_to_visible.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + <div id="container" style="display: none"> + <object id="plugin" type="application/x-test" style="width: 200px; height: 200px;"></object> + </div> +</body> +</html> diff --git a/browser/base/content/test/plugin_test.html b/browser/base/content/test/plugin_test.html new file mode 100644 index 000000000..3f5df1984 --- /dev/null +++ b/browser/base/content/test/plugin_test.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head>
+<meta charset="utf-8">
+</head> +<body> +<embed id="test" style="width: 200px; height: 200px" type="application/x-test"> +</body> +</html> diff --git a/browser/base/content/test/plugin_test2.html b/browser/base/content/test/plugin_test2.html new file mode 100644 index 000000000..95614c930 --- /dev/null +++ b/browser/base/content/test/plugin_test2.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> +<embed id="test1" style="width: 200px; height: 200px" type="application/x-test"> +<embed id="test2" style="width: 200px; height: 200px" type="application/x-test"> +</body> +</html> diff --git a/browser/base/content/test/plugin_test3.html b/browser/base/content/test/plugin_test3.html new file mode 100644 index 000000000..af14c0024 --- /dev/null +++ b/browser/base/content/test/plugin_test3.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head>
+<meta charset="utf-8">
+</head> +<body> +<embed id="test" style="width: 0px; height: 0px" type="application/x-test"> +</body> +</html> diff --git a/browser/base/content/test/plugin_two_types.html b/browser/base/content/test/plugin_two_types.html new file mode 100644 index 000000000..2359d2ec1 --- /dev/null +++ b/browser/base/content/test/plugin_two_types.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head><meta charset="utf-8"/></head> +<body> +<embed id="test" style="width: 200px; height: 200px" type="application/x-test"/> +<embed id="secondtestA" style="width: 200px; height: 200px" type="application/x-second-test"/> +<embed id="secondtestB" style="width: 200px; height: 200px" type="application/x-second-test"/> +</body> +</html> diff --git a/browser/base/content/test/plugin_unknown.html b/browser/base/content/test/plugin_unknown.html new file mode 100644 index 000000000..a35674549 --- /dev/null +++ b/browser/base/content/test/plugin_unknown.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head>
+<meta charset="utf-8">
+</head> +<body> +<embed id="unknown" style="width: 100px; height: 100px" type="application/x-unknown"> +</body> +</html> diff --git a/browser/base/content/test/print_postdata.sjs b/browser/base/content/test/print_postdata.sjs new file mode 100644 index 000000000..4175a2480 --- /dev/null +++ b/browser/base/content/test/print_postdata.sjs @@ -0,0 +1,22 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + if (request.method == "GET") { + response.write(request.queryString); + } else { + var body = new BinaryInputStream(request.bodyInputStream); + + var avail; + var bytes = []; + + while ((avail = body.available()) > 0) + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + + var data = String.fromCharCode.apply(null, bytes); + response.bodyOutputStream.write(data, data.length); + } +} diff --git a/browser/base/content/test/privateBrowsingMode.js b/browser/base/content/test/privateBrowsingMode.js new file mode 100644 index 000000000..a624d5281 --- /dev/null +++ b/browser/base/content/test/privateBrowsingMode.js @@ -0,0 +1,3 @@ +// This file is only present in per-window private browsing buikds. +var perWindowPrivateBrowsing = true; + diff --git a/browser/base/content/test/redirect_bug623155.sjs b/browser/base/content/test/redirect_bug623155.sjs new file mode 100644 index 000000000..64c6f143b --- /dev/null +++ b/browser/base/content/test/redirect_bug623155.sjs @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host. + +function handleRequest(aRequest, aResponse) { + // Set HTTP Status + aResponse.setStatusLine(aRequest.httpVersion, 301, "Moved Permanently"); + + // Set redirect URI, mirroring the hash value. + let hash = (/\#.+/.test(aRequest.path))? + "#" + aRequest.path.split("#")[1]: + ""; + aResponse.setHeader("Location", REDIRECT_TO + hash); +} diff --git a/browser/base/content/test/social/Makefile.in b/browser/base/content/test/social/Makefile.in new file mode 100644 index 000000000..1aabbcdca --- /dev/null +++ b/browser/base/content/test/social/Makefile.in @@ -0,0 +1,47 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = @relativesrcdir@ + +include $(DEPTH)/config/autoconf.mk + +MOCHITEST_BROWSER_FILES = \ + head.js \ + blocklist.xml \ + browser_blocklist.js \ + browser_defaults.js \ + browser_addons.js \ + browser_chat_tearoff.js \ + browser_social_activation.js \ + browser_social_perwindowPB.js \ + browser_social_toolbar.js \ + browser_social_markButton.js \ + browser_social_sidebar.js \ + browser_social_flyout.js \ + browser_social_mozSocial_API.js \ + browser_social_isVisible.js \ + browser_social_chatwindow.js \ + browser_social_chatwindow_resize.js \ + browser_social_chatwindowfocus.js \ + browser_social_multiprovider.js \ + browser_social_errorPage.js \ + browser_social_window.js \ + social_activate.html \ + social_activate_iframe.html \ + browser_share.js \ + social_panel.html \ + social_mark_image.png \ + social_sidebar.html \ + social_chat.html \ + social_flyout.html \ + social_window.html \ + social_worker.js \ + share.html \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/browser/base/content/test/social/blocklist.xml b/browser/base/content/test/social/blocklist.xml new file mode 100644 index 000000000..2e3665c36 --- /dev/null +++ b/browser/base/content/test/social/blocklist.xml @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem blockID="s1" id="test1.example.com@services.mozilla.org"></emItem> + </emItems> +</blocklist> diff --git a/browser/base/content/test/social/browser_addons.js b/browser/base/content/test/social/browser_addons.js new file mode 100644 index 000000000..1e3b336bd --- /dev/null +++ b/browser/base/content/test/social/browser_addons.js @@ -0,0 +1,327 @@ + + +let AddonManager = Cu.import("resource://gre/modules/AddonManager.jsm", {}).AddonManager; +let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +const ADDON_TYPE_SERVICE = "service"; +const ID_SUFFIX = "@services.mozilla.org"; +const STRING_TYPE_NAME = "type.%ID%.name"; +const XPINSTALL_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; + +let manifest = { // builtin provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" +}; +let manifest2 = { // used for testing install + name: "provider 2", + origin: "https://test1.example.com", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://test1.example.com/browser/browser/base/content/test/moz.png", + version: 1 +}; + +function test() { + waitForExplicitFinish(); + + let prefname = getManifestPrefname(manifest); + setBuiltinManifestPref(prefname, manifest); + // ensure that manifest2 is NOT showing as builtin + is(SocialService.getOriginActivationType(manifest.origin), "builtin", "manifest is builtin"); + is(SocialService.getOriginActivationType(manifest2.origin), "foreign", "manifest2 is not builtin"); + + Services.prefs.setBoolPref("social.remote-install.enabled", true); + runSocialTests(tests, undefined, undefined, function () { + Services.prefs.clearUserPref("social.remote-install.enabled"); + // clear our builtin pref + ok(!Services.prefs.prefHasUserValue(prefname), "manifest is not in user-prefs"); + resetBuiltinManifestPref(prefname); + // just in case the tests failed, clear these here as well + Services.prefs.clearUserPref("social.whitelist"); + Services.prefs.clearUserPref("social.directories"); + finish(); + }); +} + +function installListener(next, aManifest) { + let expectEvent = "onInstalling"; + let prefname = getManifestPrefname(aManifest); + // wait for the actual removal to call next + SocialService.registerProviderListener(function providerListener(topic, data) { + if (topic == "provider-removed") { + SocialService.unregisterProviderListener(providerListener); + executeSoon(next); + } + }); + + return { + onInstalling: function(addon) { + is(expectEvent, "onInstalling", "install started"); + is(addon.manifest.origin, aManifest.origin, "provider about to be installed"); + ok(!Services.prefs.prefHasUserValue(prefname), "manifest is not in user-prefs"); + expectEvent = "onInstalled"; + }, + onInstalled: function(addon) { + is(addon.manifest.origin, aManifest.origin, "provider installed"); + ok(addon.installDate.getTime() > 0, "addon has installDate"); + ok(addon.updateDate.getTime() > 0, "addon has updateDate"); + ok(Services.prefs.prefHasUserValue(prefname), "manifest is in user-prefs"); + expectEvent = "onUninstalling"; + }, + onUninstalling: function(addon) { + is(expectEvent, "onUninstalling", "uninstall started"); + is(addon.manifest.origin, aManifest.origin, "provider about to be uninstalled"); + ok(Services.prefs.prefHasUserValue(prefname), "manifest is in user-prefs"); + expectEvent = "onUninstalled"; + }, + onUninstalled: function(addon) { + is(expectEvent, "onUninstalled", "provider has been uninstalled"); + is(addon.manifest.origin, aManifest.origin, "provider uninstalled"); + ok(!Services.prefs.prefHasUserValue(prefname), "manifest is not in user-prefs"); + AddonManager.removeAddonListener(this); + } + }; +} + +var tests = { + testAddonEnableToggle: function(next) { + let expectEvent; + let prefname = getManifestPrefname(manifest); + let listener = { + onEnabled: function(addon) { + is(expectEvent, "onEnabled", "provider onEnabled"); + ok(!addon.userDisabled, "provider enabled"); + executeSoon(function() { + expectEvent = "onDisabling"; + addon.userDisabled = true; + }); + }, + onEnabling: function(addon) { + is(expectEvent, "onEnabling", "provider onEnabling"); + expectEvent = "onEnabled"; + }, + onDisabled: function(addon) { + is(expectEvent, "onDisabled", "provider onDisabled"); + ok(addon.userDisabled, "provider disabled"); + AddonManager.removeAddonListener(listener); + // clear the provider user-level pref + Services.prefs.clearUserPref(prefname); + executeSoon(next); + }, + onDisabling: function(addon) { + is(expectEvent, "onDisabling", "provider onDisabling"); + expectEvent = "onDisabled"; + } + }; + AddonManager.addAddonListener(listener); + + // we're only testing enable disable, so we quickly set the user-level pref + // for this provider and test enable/disable toggling + setManifestPref(prefname, manifest); + ok(Services.prefs.prefHasUserValue(prefname), "manifest is in user-prefs"); + AddonManager.getAddonsByTypes([ADDON_TYPE_SERVICE], function(addons) { + for (let addon of addons) { + if (addon.userDisabled) { + expectEvent = "onEnabling"; + addon.userDisabled = false; + // only test with one addon + return; + } + } + ok(false, "no addons toggled"); + next(); + }); + }, + testProviderEnableToggle: function(next) { + // enable and disabel a provider from the SocialService interface, check + // that the addon manager is updated + + let expectEvent; + let prefname = getManifestPrefname(manifest); + + let listener = { + onEnabled: function(addon) { + is(expectEvent, "onEnabled", "provider onEnabled"); + is(addon.manifest.origin, manifest.origin, "provider enabled"); + ok(!addon.userDisabled, "provider !userDisabled"); + }, + onEnabling: function(addon) { + is(expectEvent, "onEnabling", "provider onEnabling"); + is(addon.manifest.origin, manifest.origin, "provider about to be enabled"); + expectEvent = "onEnabled"; + }, + onDisabled: function(addon) { + is(expectEvent, "onDisabled", "provider onDisabled"); + is(addon.manifest.origin, manifest.origin, "provider disabled"); + ok(addon.userDisabled, "provider userDisabled"); + }, + onDisabling: function(addon) { + is(expectEvent, "onDisabling", "provider onDisabling"); + is(addon.manifest.origin, manifest.origin, "provider about to be disabled"); + expectEvent = "onDisabled"; + } + }; + AddonManager.addAddonListener(listener); + + expectEvent = "onEnabling"; + setManifestPref(prefname, manifest); + SocialService.addBuiltinProvider(manifest.origin, function(provider) { + expectEvent = "onDisabling"; + SocialService.removeProvider(provider.origin, function() { + AddonManager.removeAddonListener(listener); + Services.prefs.clearUserPref(prefname); + next(); + }); + }); + }, + testForeignInstall: function(next) { + AddonManager.addAddonListener(installListener(next, manifest2)); + + // we expect the addon install dialog to appear, we need to accept the + // install from the dialog. + info("Waiting for install dialog"); + let panel = document.getElementById("servicesInstall-notification"); + PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() { + PopupNotifications.panel.removeEventListener("popupshown", onpopupshown); + info("servicesInstall-notification panel opened"); + panel.button.click(); + }) + + let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + let doc = tab.linkedBrowser.contentDocument; + let installFrom = doc.nodePrincipal.origin; + Services.prefs.setCharPref("social.whitelist", ""); + is(SocialService.getOriginActivationType(installFrom), "foreign", "testing foriegn install"); + Social.installProvider(doc, manifest2, function(addonManifest) { + Services.prefs.clearUserPref("social.whitelist"); + SocialService.addBuiltinProvider(addonManifest.origin, function(provider) { + Social.uninstallProvider(addonManifest.origin); + gBrowser.removeTab(tab); + }); + }); + }); + }, + testBuiltinInstallWithoutManifest: function(next) { + // send installProvider null for the manifest + AddonManager.addAddonListener(installListener(next, manifest)); + + let prefname = getManifestPrefname(manifest); + let activationURL = manifest.origin + "/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + let doc = tab.linkedBrowser.contentDocument; + let installFrom = doc.nodePrincipal.origin; + is(SocialService.getOriginActivationType(installFrom), "builtin", "testing builtin install"); + ok(!Services.prefs.prefHasUserValue(prefname), "manifest is not in user-prefs"); + Social.installProvider(doc, null, function(addonManifest) { + ok(Services.prefs.prefHasUserValue(prefname), "manifest is in user-prefs"); + SocialService.addBuiltinProvider(addonManifest.origin, function(provider) { + Social.uninstallProvider(addonManifest.origin); + gBrowser.removeTab(tab); + }); + }); + }); + }, + testBuiltinInstall: function(next) { + // send installProvider a json object for the manifest + AddonManager.addAddonListener(installListener(next, manifest)); + + let prefname = getManifestPrefname(manifest); + let activationURL = manifest.origin + "/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + let doc = tab.linkedBrowser.contentDocument; + let installFrom = doc.nodePrincipal.origin; + is(SocialService.getOriginActivationType(installFrom), "builtin", "testing builtin install"); + ok(!Services.prefs.prefHasUserValue(prefname), "manifest is not in user-prefs"); + Social.installProvider(doc, manifest, function(addonManifest) { + ok(Services.prefs.prefHasUserValue(prefname), "manifest is in user-prefs"); + SocialService.addBuiltinProvider(addonManifest.origin, function(provider) { + Social.uninstallProvider(addonManifest.origin); + gBrowser.removeTab(tab); + }); + }); + }); + }, + testWhitelistInstall: function(next) { + AddonManager.addAddonListener(installListener(next, manifest2)); + + let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + let doc = tab.linkedBrowser.contentDocument; + let installFrom = doc.nodePrincipal.origin; + Services.prefs.setCharPref("social.whitelist", installFrom); + is(SocialService.getOriginActivationType(installFrom), "whitelist", "testing whitelist install"); + Social.installProvider(doc, manifest2, function(addonManifest) { + Services.prefs.clearUserPref("social.whitelist"); + SocialService.addBuiltinProvider(addonManifest.origin, function(provider) { + Social.uninstallProvider(addonManifest.origin); + gBrowser.removeTab(tab); + }); + }); + }); + }, + testDirectoryInstall: function(next) { + AddonManager.addAddonListener(installListener(next, manifest2)); + + let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + let doc = tab.linkedBrowser.contentDocument; + let installFrom = doc.nodePrincipal.origin; + Services.prefs.setCharPref("social.directories", installFrom); + is(SocialService.getOriginActivationType(installFrom), "directory", "testing directory install"); + Social.installProvider(doc, manifest2, function(addonManifest) { + Services.prefs.clearUserPref("social.directories"); + SocialService.addBuiltinProvider(addonManifest.origin, function(provider) { + Social.uninstallProvider(addonManifest.origin); + gBrowser.removeTab(tab); + }); + }); + }); + }, + testUpgradeProviderFromWorker: function(next) { + // add the provider, change the pref, add it again. The provider at that + // point should be upgraded + let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + let doc = tab.linkedBrowser.contentDocument; + let installFrom = doc.nodePrincipal.origin; + Services.prefs.setCharPref("social.whitelist", installFrom); + Social.installProvider(doc, manifest2, function(addonManifest) { + SocialService.addBuiltinProvider(addonManifest.origin, function(provider) { + is(provider.manifest.version, 1, "manifest version is 1"); + Social.enabled = true; + + // watch for the provider-update and test the new version + SocialService.registerProviderListener(function providerListener(topic, data) { + if (topic != "provider-update") + return; + SocialService.unregisterProviderListener(providerListener); + Services.prefs.clearUserPref("social.whitelist"); + let provider = Social._getProviderFromOrigin(addonManifest.origin); + is(provider.manifest.version, 2, "manifest version is 2"); + Social.uninstallProvider(addonManifest.origin, function() { + gBrowser.removeTab(tab); + next(); + }); + }); + + let port = provider.getWorkerPort(); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-sidebar-message": + ok(true, "got the sidebar message from provider 1"); + port.postMessage({topic: "worker.update", data: true}); + break; + } + }; + port.postMessage({topic: "test-init"}); + + }); + }); + }); + } +} diff --git a/browser/base/content/test/social/browser_blocklist.js b/browser/base/content/test/social/browser_blocklist.js new file mode 100644 index 000000000..6f61b1ac2 --- /dev/null +++ b/browser/base/content/test/social/browser_blocklist.js @@ -0,0 +1,179 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// a place for miscellaneous social tests + +let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul"; +let blocklistURL = "http://example.org/browser/browser/base/content/test/social/blocklist.xml"; + +let manifest = { // normal provider + name: "provider ok", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" +}; +let manifest_bad = { // normal provider + name: "provider blocked", + origin: "https://test1.example.com", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://test1.example.com/browser/browser/base/content/test/moz.png" +}; + +function test() { + waitForExplicitFinish(); + + runSocialTests(tests, undefined, undefined, function () { + resetBlocklist(finish); //restore to original pref + }); +} + +var tests = { + testSimpleBlocklist: function(next) { + // this really just tests adding and clearing our blocklist for later tests + setAndUpdateBlocklist(blocklistURL, function() { + ok(Services.blocklist.isAddonBlocklisted("test1.example.com@services.mozilla.org", "0", "0", "0"), "blocking 'blocked'"); + ok(!Services.blocklist.isAddonBlocklisted("example.com@services.mozilla.org", "0", "0", "0"), "not blocking 'good'"); + resetBlocklist(function() { + ok(!Services.blocklist.isAddonBlocklisted("test1.example.com@services.mozilla.org", "0", "0", "0"), "blocklist cleared"); + next(); + }); + }); + }, + testAddingNonBlockedProvider: function(next) { + function finish(isgood) { + ok(isgood, "adding non-blocked provider ok"); + Services.prefs.clearUserPref("social.manifest.good"); + resetBlocklist(next); + } + setManifestPref("social.manifest.good", manifest); + setAndUpdateBlocklist(blocklistURL, function() { + try { + SocialService.addProvider(manifest, function(provider) { + try { + SocialService.removeProvider(provider.origin, function() { + ok(true, "added and removed provider"); + finish(true); + }); + } catch(e) { + ok(false, "SocialService.removeProvider threw exception: " + e); + finish(false); + } + }); + } catch(e) { + ok(false, "SocialService.addProvider threw exception: " + e); + finish(false); + } + }); + }, + testAddingBlockedProvider: function(next) { + function finish(good) { + ok(good, "Unable to add blocklisted provider"); + Services.prefs.clearUserPref("social.manifest.blocked"); + resetBlocklist(next); + } + setManifestPref("social.manifest.blocked", manifest_bad); + setAndUpdateBlocklist(blocklistURL, function() { + try { + SocialService.addProvider(manifest_bad, function(provider) { + ok(false, "SocialService.addProvider should throw blocklist exception"); + finish(false); + }); + } catch(e) { + ok(true, "SocialService.addProvider should throw blocklist exception: " + e); + finish(true); + } + }); + }, + testInstallingBlockedProvider: function(next) { + function finish(good) { + ok(good, "Unable to add blocklisted provider"); + Services.prefs.clearUserPref("social.whitelist"); + resetBlocklist(next); + } + let activationURL = manifest_bad.origin + "/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + let doc = tab.linkedBrowser.contentDocument; + let installFrom = doc.nodePrincipal.origin; + // whitelist to avoid the 3rd party install dialog, we only want to test + // the blocklist inside installProvider. + Services.prefs.setCharPref("social.whitelist", installFrom); + setAndUpdateBlocklist(blocklistURL, function() { + try { + // expecting an exception when attempting to install a hard blocked + // provider + Social.installProvider(doc, manifest_bad, function(addonManifest) { + gBrowser.removeTab(tab); + finish(false); + }); + } catch(e) { + gBrowser.removeTab(tab); + finish(true); + } + }); + }); + }, + testBlockingExistingProvider: function(next) { + let windowWasClosed = false; + function finish() { + waitForCondition(function() windowWasClosed, function() { + Services.wm.removeListener(listener); + next(); + }, "blocklist dialog was closed"); + } + + let listener = { + _window: null, + onOpenWindow: function(aXULWindow) { + Services.wm.removeListener(this); + this._window = aXULWindow; + let domwindow = aXULWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + + domwindow.addEventListener("unload", function _unload() { + domwindow.removeEventListener("unload", _unload, false); + windowWasClosed = true; + }, false); + info("dialog opened, waiting for focus"); + waitForFocus(function() { + is(domwindow.document.location.href, URI_EXTENSION_BLOCKLIST_DIALOG, "dialog opened and focused"); + executeSoon(function() { + domwindow.close(); + }); + }, domwindow); + }, + onCloseWindow: function(aXULWindow) { }, + onWindowTitleChange: function(aXULWindow, aNewTitle) { } + }; + + Services.wm.addListener(listener); + + setManifestPref("social.manifest.blocked", manifest_bad); + try { + SocialService.addProvider(manifest_bad, function(provider) { + // the act of blocking should cause a 'provider-removed' notification + // from SocialService. + SocialService.registerProviderListener(function providerListener(topic) { + if (topic != "provider-removed") + return; + SocialService.unregisterProviderListener(providerListener); + SocialService.getProvider(provider.origin, function(p) { + ok(p==null, "blocklisted provider removed"); + Services.prefs.clearUserPref("social.manifest.blocked"); + resetBlocklist(finish); + }); + }); + // no callback - the act of updating should cause the listener above + // to fire. + setAndUpdateBlocklist(blocklistURL); + }); + } catch(e) { + ok(false, "unable to add provider " + e); + finish(); + } + } +} diff --git a/browser/base/content/test/social/browser_chat_tearoff.js b/browser/base/content/test/social/browser_chat_tearoff.js new file mode 100644 index 000000000..7ee8acfab --- /dev/null +++ b/browser/base/content/test/social/browser_chat_tearoff.js @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + requestLongerTimeout(2); // only debug builds seem to need more time... + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" + }; + + let postSubTest = function(cb) { + let chats = document.getElementById("pinnedchats"); + ok(chats.children.length == 0, "no chatty children left behind"); + cb(); + }; + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, postSubTest, function() { + finishcb(); + }); + }); +} + +var tests = { + testTearoffChat: function(next) { + let chats = document.getElementById("pinnedchats"); + let chatTitle; + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-sidebar-message": + port.postMessage({topic: "test-chatbox-open"}); + break; + case "got-chatbox-visibility": + // chatbox is open, lets detach. The new chat window will be caught in + // the window watcher below + let doc = chats.selectedChat.contentDocument; + // This message is (sometimes!) received a second time + // before we start our tests from the onCloseWindow + // callback. + if (doc.location == "about:blank") + return; + chatTitle = doc.title; + ok(chats.selectedChat.getAttribute("label") == chatTitle, + "the new chatbox should show the title of the chat window"); + let div = doc.createElement("div"); + div.setAttribute("id", "testdiv"); + div.setAttribute("test", "1"); + doc.body.appendChild(div); + let swap = document.getAnonymousElementByAttribute(chats.selectedChat, "anonid", "swap"); + swap.click(); + break; + case "got-chatbox-message": + ok(true, "got chatbox message"); + ok(e.data.result == "ok", "got chatbox windowRef result: "+e.data.result); + chats.selectedChat.toggle(); + break; + } + } + + Services.wm.addListener({ + onWindowTitleChange: function() {}, + onCloseWindow: function(xulwindow) {}, + onOpenWindow: function(xulwindow) { + var domwindow = xulwindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindow); + Services.wm.removeListener(this); + // wait for load to ensure the window is ready for us to test + domwindow.addEventListener("load", function _load() { + domwindow.removeEventListener("load", _load, false); + let doc = domwindow.document; + is(doc.documentElement.getAttribute("windowtype"), "Social:Chat", "Social:Chat window opened"); + is(doc.location.href, "chrome://browser/content/chatWindow.xul", "Should have seen the right window open"); + // window is loaded, but the docswap does not happen until after load, + // and we have no event to wait on, so we'll wait for document state + // to be ready + let chatbox = doc.getElementById("chatter"); + waitForCondition(function() { + return chats.selectedChat == null && + chatbox.contentDocument && + chatbox.contentDocument.readyState == "complete"; + },function() { + ok(chatbox.getAttribute("label") == chatTitle, + "detached window should show the title of the chat window"); + let testdiv = chatbox.contentDocument.getElementById("testdiv"); + is(testdiv.getAttribute("test"), "1", "docshell should have been swapped"); + testdiv.setAttribute("test", "2"); + // swap the window back to the chatbar + let swap = doc.getAnonymousElementByAttribute(chatbox, "anonid", "swap"); + swap.click(); + }, domwindow); + }, false); + domwindow.addEventListener("unload", function _close() { + domwindow.removeEventListener("unload", _close, false); + info("window has been closed"); + waitForCondition(function() { + return chats.selectedChat && chats.selectedChat.contentDocument && + chats.selectedChat.contentDocument.readyState == "complete"; + },function () { + ok(chats.selectedChat, "should have a chatbox in our window again"); + ok(chats.selectedChat.getAttribute("label") == chatTitle, + "the new chatbox should show the title of the chat window again"); + let testdiv = chats.selectedChat.contentDocument.getElementById("testdiv"); + is(testdiv.getAttribute("test"), "2", "docshell should have been swapped"); + chats.selectedChat.close(); + next(); + }); + }, false); + } + }); + + port.postMessage({topic: "test-init", data: { id: 1 }}); + } +}
\ No newline at end of file diff --git a/browser/base/content/test/social/browser_defaults.js b/browser/base/content/test/social/browser_defaults.js new file mode 100644 index 000000000..653509a98 --- /dev/null +++ b/browser/base/content/test/social/browser_defaults.js @@ -0,0 +1,14 @@ + +let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +// this test ensures that any builtin providers have the builtin flag that we +// need to help with "install" of a builtin. +function test() { + let manifestPrefs = Services.prefs.getDefaultBranch("social.manifest."); + let prefs = manifestPrefs.getChildList("", []); + ok(prefs.length > 0, "we have builtin providers"); + for (let pref of prefs) { + let manifest = JSON.parse(manifestPrefs.getComplexValue(pref, Ci.nsISupportsString).data); + ok(manifest.builtin, "manifest is builtin " + manifest.origin); + } +} diff --git a/browser/base/content/test/social/browser_share.js b/browser/base/content/test/social/browser_share.js new file mode 100644 index 000000000..146bb6fca --- /dev/null +++ b/browser/base/content/test/social/browser_share.js @@ -0,0 +1,140 @@ + +let baseURL = "https://example.com/browser/browser/base/content/test/social/"; + +function test() { + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png", + shareURL: "https://example.com/browser/browser/base/content/test/social/share.html" + }; + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, undefined, finishcb); + }); +} + +let corpus = [ + { + url: baseURL+"opengraph/opengraph.html", + options: { + // og:title + title: ">This is my title<", + // og:description + description: "A test corpus file for open graph tags we care about", + //medium: this.getPageMedium(), + //source: this.getSourceURL(), + // og:url + url: "https://www.mozilla.org/", + //shortUrl: this.getShortURL(), + // og:image + previews:["https://www.mozilla.org/favicon.png"], + // og:site_name + siteName: ">My simple test page<" + } + }, + { + // tests that og:url doesn't override the page url if it is bad + url: baseURL+"opengraph/og_invalid_url.html", + options: { + description: "A test corpus file for open graph tags passing a bad url", + url: baseURL+"opengraph/og_invalid_url.html", + previews: [], + siteName: "Evil chrome delivering website" + } + }, + { + url: baseURL+"opengraph/shorturl_link.html", + options: { + previews: ["http://example.com/1234/56789.jpg"], + url: "http://www.example.com/photos/56789/", + shortUrl: "http://imshort/p/abcde" + } + }, + { + url: baseURL+"opengraph/shorturl_linkrel.html", + options: { + previews: ["http://example.com/1234/56789.jpg"], + url: "http://www.example.com/photos/56789/", + shortUrl: "http://imshort/p/abcde" + } + }, + { + url: baseURL+"opengraph/shortlink_linkrel.html", + options: { + previews: ["http://example.com/1234/56789.jpg"], + url: "http://www.example.com/photos/56789/", + shortUrl: "http://imshort/p/abcde" + } + } +]; + +function loadURLInTab(url, callback) { + info("Loading tab with "+url); + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + tab.linkedBrowser.addEventListener("load", function listener() { + is(tab.linkedBrowser.currentURI.spec, url, "tab loaded") + tab.linkedBrowser.removeEventListener("load", listener, true); + callback(tab); + }, true); +} + +function hasoptions(testOptions, options) { + let msg; + for (let option in testOptions) { + let data = testOptions[option]; + info("data: "+JSON.stringify(data)); + let message_data = options[option]; + info("message_data: "+JSON.stringify(message_data)); + if (Array.isArray(data)) { + // the message may have more array elements than we are testing for, this + // is ok since some of those are hard to test. So we just test that + // anything in our test data IS in the message. + ok(Array.every(data, function(item) { return message_data.indexOf(item) >= 0 }), "option "+option); + } else { + is(message_data, data, "option "+option); + } + } +} + +var tests = { + testSharePage: function(next) { + let panel = document.getElementById("social-flyout-panel"); + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + let testTab; + let testIndex = 0; + let testData = corpus[testIndex++]; + + function runOneTest() { + loadURLInTab(testData.url, function(tab) { + testTab = tab; + SocialShare.sharePage(); + }); + } + + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-sidebar-message": + // open a tab with share data, then open the share panel + runOneTest(); + break; + case "got-share-data-message": + gBrowser.removeTab(testTab); + hasoptions(testData.options, e.data.result); + testData = corpus[testIndex++]; + if (testData) { + runOneTest(); + } else { + next(); + } + break; + } + } + port.postMessage({topic: "test-init"}); + } +} diff --git a/browser/base/content/test/social/browser_social_activation.js b/browser/base/content/test/social/browser_social_activation.js new file mode 100644 index 000000000..7d7b499a7 --- /dev/null +++ b/browser/base/content/test/social/browser_social_activation.js @@ -0,0 +1,348 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +let tabsToRemove = []; + +function postTestCleanup(callback) { + Social.provider = null; + // any tabs opened by the test. + for (let tab of tabsToRemove) + gBrowser.removeTab(tab); + tabsToRemove = []; + // theses tests use the notification panel but don't bother waiting for it + // to fully open - the end result is that the panel might stay open + SocialUI.activationPanel.hidePopup(); + + Services.prefs.clearUserPref("social.whitelist"); + + // all providers may have had their manifests added. + for (let manifest of gProviders) + Services.prefs.clearUserPref("social.manifest." + manifest.origin); + + // all the providers may have been added. + let providers = gProviders.slice(0) + function removeProviders() { + if (providers.length < 1) { + executeSoon(function() { + is(Social.providers.length, 0, "all providers removed"); + callback(); + }); + return; + } + + let provider = providers.pop(); + try { + SocialService.removeProvider(provider.origin, removeProviders); + } catch(ex) { + removeProviders(); + } + } + removeProviders(); +} + +function addBuiltinManifest(manifest) { + let prefname = getManifestPrefname(manifest); + setBuiltinManifestPref(prefname, manifest); + return prefname; +} + +function addTab(url, callback) { + let tab = gBrowser.selectedTab = gBrowser.addTab(url, {skipAnimation: true}); + tab.linkedBrowser.addEventListener("load", function tabLoad(event) { + tab.linkedBrowser.removeEventListener("load", tabLoad, true); + tabsToRemove.push(tab); + executeSoon(function() {callback(tab)}); + }, true); +} + +function sendActivationEvent(tab, callback, nullManifest) { + // hack Social.lastEventReceived so we don't hit the "too many events" check. + Social.lastEventReceived = 0; + let doc = tab.linkedBrowser.contentDocument; + // if our test has a frame, use it + if (doc.defaultView.frames[0]) + doc = doc.defaultView.frames[0].document; + let button = doc.getElementById(nullManifest ? "activation-old" : "activation"); + EventUtils.synthesizeMouseAtCenter(button, {}, doc.defaultView); + executeSoon(callback); +} + +function activateProvider(domain, callback, nullManifest) { + let activationURL = domain+"/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + sendActivationEvent(tab, callback, nullManifest); + }); +} + +function activateIFrameProvider(domain, callback) { + let activationURL = domain+"/browser/browser/base/content/test/social/social_activate_iframe.html" + addTab(activationURL, function(tab) { + sendActivationEvent(tab, callback, false); + }); +} + +function waitForProviderLoad(cb) { + Services.obs.addObserver(function providerSet(subject, topic, data) { + Services.obs.removeObserver(providerSet, "social:provider-set"); + info("social:provider-set observer was notified"); + waitForCondition(function() { + let sbrowser = document.getElementById("social-sidebar-browser"); + return Social.provider && + Social.provider.profile && + Social.provider.profile.displayName && + sbrowser.docShellIsActive; + }, function() { + // executeSoon to let the browser UI observers run first + executeSoon(cb); + }, + "waitForProviderLoad: provider profile was not set"); + }, "social:provider-set", false); +} + + +function getAddonItemInList(aId, aList) { + var item = aList.firstChild; + while (item) { + if ("mAddon" in item && item.mAddon.id == aId) { + aList.ensureElementIsVisible(item); + return item; + } + item = item.nextSibling; + } + return null; +} + +function clickAddonRemoveButton(tab, aCallback) { + AddonManager.getAddonsByTypes(["service"], function(aAddons) { + let addon = aAddons[0]; + + let doc = tab.linkedBrowser.contentDocument; + let list = doc.getElementById("addon-list"); + + let item = getAddonItemInList(addon.id, list); + isnot(item, null, "Should have found the add-on in the list"); + + var button = doc.getAnonymousElementByAttribute(item, "anonid", "remove-btn"); + isnot(button, null, "Should have a remove button"); + ok(!button.disabled, "Button should not be disabled"); + + EventUtils.synthesizeMouseAtCenter(button, { }, doc.defaultView); + + // Force XBL to apply + item.clientTop; + + is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling"); + + executeSoon(function() { aCallback(addon); }); + }); +} + +function activateOneProvider(manifest, finishActivation, aCallback) { + activateProvider(manifest.origin, function() { + waitForProviderLoad(function() { + ok(!SocialUI.activationPanel.hidden, "activation panel is showing"); + is(Social.provider.origin, manifest.origin, "new provider is active"); + checkSocialUI(); + + if (finishActivation) + document.getElementById("social-activation-button").click(); + else + document.getElementById("social-undoactivation-button").click(); + + executeSoon(aCallback); + }); + }); +} + +let gTestDomains = ["https://example.com", "https://test1.example.com", "https://test2.example.com"]; +let gProviders = [ + { + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html?provider1", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js#no-profile,no-recommend", + iconURL: "chrome://branding/content/icon48.png" + }, + { + name: "provider 2", + origin: "https://test1.example.com", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider2", + workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js#no-profile,no-recommend", + iconURL: "chrome://branding/content/icon64.png" + }, + { + name: "provider 3", + origin: "https://test2.example.com", + sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider2", + workerURL: "https://test2.example.com/browser/browser/base/content/test/social/social_worker.js#no-profile,no-recommend", + iconURL: "chrome://branding/content/about-logo.png" + } +]; + + +function test() { + waitForExplicitFinish(); + runSocialTests(tests, undefined, postTestCleanup); +} + +var tests = { + testActivationWrongOrigin: function(next) { + // At this stage none of our providers exist, so we expect failure. + Services.prefs.setBoolPref("social.remote-install.enabled", false); + activateProvider(gTestDomains[0], function() { + is(SocialUI.enabled, false, "SocialUI is not enabled"); + ok(SocialUI.activationPanel.hidden, "activation panel still hidden"); + checkSocialUI(); + Services.prefs.clearUserPref("social.remote-install.enabled"); + next(); + }); + }, + + testIFrameActivation: function(next) { + Services.prefs.setCharPref("social.whitelist", gTestDomains.join(",")); + activateIFrameProvider(gTestDomains[0], function() { + is(SocialUI.enabled, false, "SocialUI is not enabled"); + ok(!Social.provider, "provider is not installed"); + ok(SocialUI.activationPanel.hidden, "activation panel still hidden"); + checkSocialUI(); + Services.prefs.clearUserPref("social.whitelist"); + next(); + }); + }, + + testActivationFirstProvider: function(next) { + Services.prefs.setCharPref("social.whitelist", gTestDomains.join(",")); + // first up we add a manifest entry for a single provider. + activateOneProvider(gProviders[0], false, function() { + // we deactivated leaving no providers left, so Social is disabled. + ok(!Social.provider, "should be no provider left after disabling"); + checkSocialUI(); + Services.prefs.clearUserPref("social.whitelist"); + next(); + }); + }, + + testActivationBuiltin: function(next) { + let prefname = addBuiltinManifest(gProviders[0]); + is(SocialService.getOriginActivationType(gTestDomains[0]), "builtin", "manifest is builtin"); + // first up we add a manifest entry for a single provider. + activateOneProvider(gProviders[0], false, function() { + // we deactivated leaving no providers left, so Social is disabled. + ok(!Social.provider, "should be no provider left after disabling"); + checkSocialUI(); + resetBuiltinManifestPref(prefname); + next(); + }); + }, + + testActivationMultipleProvider: function(next) { + // The trick with this test is to make sure that Social.providers[1] is + // the current provider when doing the undo - this makes sure that the + // Social code doesn't fallback to Social.providers[0], which it will + // do in some cases (but those cases do not include what this test does) + // first enable the 2 providers + Services.prefs.setCharPref("social.whitelist", gTestDomains.join(",")); + SocialService.addProvider(gProviders[0], function() { + SocialService.addProvider(gProviders[1], function() { + Social.provider = Social.providers[1]; + checkSocialUI(); + // activate the last provider. + let prefname = addBuiltinManifest(gProviders[2]); + activateOneProvider(gProviders[2], false, function() { + // we deactivated - the first provider should be enabled. + is(Social.provider.origin, Social.providers[1].origin, "original provider should have been reactivated"); + checkSocialUI(); + Services.prefs.clearUserPref("social.whitelist"); + resetBuiltinManifestPref(prefname); + next(); + }); + }); + }); + }, + + testRemoveNonCurrentProvider: function(next) { + Services.prefs.setCharPref("social.whitelist", gTestDomains.join(",")); + SocialService.addProvider(gProviders[0], function() { + SocialService.addProvider(gProviders[1], function() { + Social.provider = Social.providers[1]; + checkSocialUI(); + // activate the last provider. + let prefname = addBuiltinManifest(gProviders[2]); + activateProvider(gTestDomains[2], function() { + waitForProviderLoad(function() { + ok(!SocialUI.activationPanel.hidden, "activation panel is showing"); + is(Social.provider.origin, gTestDomains[2], "new provider is active"); + checkSocialUI(); + // A bit contrived, but set a new provider current while the + // activation ui is up. + Social.provider = Social.providers[1]; + // hit "undo" + document.getElementById("social-undoactivation-button").click(); + executeSoon(function() { + // we deactivated - the same provider should be enabled. + is(Social.provider.origin, Social.providers[1].origin, "original provider still be active"); + checkSocialUI(); + Services.prefs.clearUserPref("social.whitelist"); + resetBuiltinManifestPref(prefname); + next(); + }); + }); + }); + }); + }); + }, + + testAddonManagerDoubleInstall: function(next) { + Services.prefs.setCharPref("social.whitelist", gTestDomains.join(",")); + // Create a new tab and load about:addons + let blanktab = gBrowser.addTab(); + gBrowser.selectedTab = blanktab; + BrowserOpenAddonsMgr('addons://list/service'); + + is(blanktab, gBrowser.selectedTab, "Current tab should be blank tab"); + + gBrowser.selectedBrowser.addEventListener("load", function tabLoad() { + gBrowser.selectedBrowser.removeEventListener("load", tabLoad, true); + let browser = blanktab.linkedBrowser; + is(browser.currentURI.spec, "about:addons", "about:addons should load into blank tab."); + + let prefname = addBuiltinManifest(gProviders[0]); + activateOneProvider(gProviders[0], true, function() { + gBrowser.removeTab(gBrowser.selectedTab); + tabsToRemove.pop(); + // uninstall the provider + clickAddonRemoveButton(blanktab, function(addon) { + checkSocialUI(); + activateOneProvider(gProviders[0], true, function() { + + // after closing the addons tab, verify provider is still installed + gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() { + gBrowser.tabContainer.removeEventListener("TabClose", onTabClose); + AddonManager.getAddonsByTypes(["service"], function(aAddons) { + is(aAddons.length, 1, "there can be only one"); + Services.prefs.clearUserPref("social.whitelist"); + resetBuiltinManifestPref(prefname); + next(); + }); + }); + + // verify only one provider in list + AddonManager.getAddonsByTypes(["service"], function(aAddons) { + is(aAddons.length, 1, "there can be only one"); + + let doc = blanktab.linkedBrowser.contentDocument; + let list = doc.getElementById("addon-list"); + is(list.childNodes.length, 1, "only one addon is displayed"); + + gBrowser.removeTab(blanktab); + }); + + }); + }); + }); + }, true); + } +} diff --git a/browser/base/content/test/social/browser_social_chatwindow.js b/browser/base/content/test/social/browser_social_chatwindow.js new file mode 100644 index 000000000..9fb47d3f9 --- /dev/null +++ b/browser/base/content/test/social/browser_social_chatwindow.js @@ -0,0 +1,472 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + requestLongerTimeout(2); // only debug builds seem to need more time... + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" + }; + let oldwidth = window.outerWidth; // we futz with these, so we restore them + let oldleft = window.screenX; + window.moveTo(0, window.screenY) + let postSubTest = function(cb) { + let chats = document.getElementById("pinnedchats"); + ok(chats.children.length == 0, "no chatty children left behind"); + cb(); + }; + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, postSubTest, function() { + window.moveTo(oldleft, window.screenY) + window.resizeTo(oldwidth, window.outerHeight); + finishcb(); + }); + }); +} + +var tests = { + testOpenCloseChat: function(next) { + let chats = document.getElementById("pinnedchats"); + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-sidebar-message": + port.postMessage({topic: "test-chatbox-open"}); + break; + case "got-chatbox-visibility": + if (e.data.result == "hidden") { + ok(true, "chatbox got minimized"); + chats.selectedChat.toggle(); + } else if (e.data.result == "shown") { + ok(true, "chatbox got shown"); + // close it now + let content = chats.selectedChat.content; + content.addEventListener("unload", function chatUnload() { + content.removeEventListener("unload", chatUnload, true); + ok(true, "got chatbox unload on close"); + port.close(); + next(); + }, true); + chats.selectedChat.close(); + } + break; + case "got-chatbox-message": + ok(true, "got chatbox message"); + ok(e.data.result == "ok", "got chatbox windowRef result: "+e.data.result); + chats.selectedChat.toggle(); + break; + } + } + port.postMessage({topic: "test-init", data: { id: 1 }}); + }, + testOpenMinimized: function(next) { + // In this case the sidebar opens a chat (without specifying minimized). + // We then minimize it and have the sidebar reopen the chat (again without + // minimized). On that second call the chat should open and no longer + // be minimized. + let chats = document.getElementById("pinnedchats"); + let port = Social.provider.getWorkerPort(); + let seen_opened = false; + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "test-init-done": + port.postMessage({topic: "test-chatbox-open"}); + break; + case "chatbox-opened": + is(e.data.result, "ok", "the sidebar says it got a chatbox"); + if (!seen_opened) { + // first time we got the opened message, so minimize the chat then + // re-request the same chat to be opened - we should get the + // message again and the chat should be restored. + ok(!chats.selectedChat.minimized, "chat not initially minimized") + chats.selectedChat.minimized = true + seen_opened = true; + port.postMessage({topic: "test-chatbox-open"}); + } else { + // This is the second time we've seen this message - there should + // be exactly 1 chat open and it should no longer be minimized. + let chats = document.getElementById("pinnedchats"); + ok(!chats.selectedChat.minimized, "chat no longer minimized") + chats.selectedChat.close(); + is(chats.selectedChat, null, "should only have been one chat open"); + port.close(); + next(); + } + } + } + port.postMessage({topic: "test-init", data: { id: 1 }}); + }, + testManyChats: function(next) { + // open enough chats to overflow the window, then check + // if the menupopup is visible + let port = Social.provider.getWorkerPort(); + let chats = document.getElementById("pinnedchats"); + ok(port, "provider has a port"); + ok(chats.menupopup.parentNode.collapsed, "popup nub collapsed at start"); + port.postMessage({topic: "test-init"}); + // we should *never* find a test box that needs more than this to cause + // an overflow! + let maxToOpen = 20; + let numOpened = 0; + let maybeOpenAnother = function() { + if (numOpened++ >= maxToOpen) { + ok(false, "We didn't find a collapsed chat after " + maxToOpen + "chats!"); + closeAllChats(); + next(); + } + port.postMessage({topic: "test-chatbox-open", data: { id: numOpened }}); + } + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-chatbox-message": + if (!chats.menupopup.parentNode.collapsed) { + maybeOpenAnother(); + break; + } + ok(true, "popup nub became visible"); + // close our chats now + while (chats.selectedChat) { + chats.selectedChat.close(); + } + ok(!chats.selectedChat, "chats are all closed"); + port.close(); + next(); + break; + } + } + maybeOpenAnother(); + }, + testWorkerChatWindow: function(next) { + const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html"; + let chats = document.getElementById("pinnedchats"); + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.postMessage({topic: "test-init"}); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-chatbox-message": + ok(true, "got a chat window opened"); + ok(chats.selectedChat, "chatbox from worker opened"); + while (chats.selectedChat) { + chats.selectedChat.close(); + } + ok(!chats.selectedChat, "chats are all closed"); + gURLsNotRemembered.push(chatUrl); + port.close(); + next(); + break; + } + } + ok(!chats.selectedChat, "chats are all closed"); + port.postMessage({topic: "test-worker-chat", data: chatUrl}); + }, + testCloseSelf: function(next) { + let chats = document.getElementById("pinnedchats"); + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "test-init-done": + port.postMessage({topic: "test-chatbox-open"}); + break; + case "got-chatbox-visibility": + is(e.data.result, "shown", "chatbox shown"); + port.close(); // don't want any more visibility messages. + let chat = chats.selectedChat; + ok(chat.parentNode, "chat has a parent node before it is closed"); + // ask it to close itself. + let doc = chat.contentDocument; + let evt = doc.createEvent("CustomEvent"); + evt.initCustomEvent("socialTest-CloseSelf", true, true, {}); + doc.documentElement.dispatchEvent(evt); + ok(!chat.parentNode, "chat is now closed"); + port.close(); + next(); + break; + } + } + port.postMessage({topic: "test-init", data: { id: 1 }}); + }, + testSameChatCallbacks: function(next) { + let chats = document.getElementById("pinnedchats"); + let port = Social.provider.getWorkerPort(); + let seen_opened = false; + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "test-init-done": + port.postMessage({topic: "test-chatbox-open"}); + break; + case "chatbox-opened": + is(e.data.result, "ok", "the sidebar says it got a chatbox"); + if (seen_opened) { + // This is the second time we've seen this message - there should + // be exactly 1 chat open. + let chats = document.getElementById("pinnedchats"); + chats.selectedChat.close(); + is(chats.selectedChat, null, "should only have been one chat open"); + port.close(); + next(); + } else { + // first time we got the opened message, so re-request the same + // chat to be opened - we should get the message again. + seen_opened = true; + port.postMessage({topic: "test-chatbox-open"}); + } + } + } + port.postMessage({topic: "test-init", data: { id: 1 }}); + }, + + // check removeAll does the right thing + testRemoveAll: function(next, mode) { + let port = Social.provider.getWorkerPort(); + port.postMessage({topic: "test-init"}); + get3ChatsForCollapsing(mode || "normal", function() { + let chatbar = window.SocialChatBar.chatbar; + chatbar.removeAll(); + // should be no evidence of any chats left. + is(chatbar.childNodes.length, 0, "should be no chats left"); + checkPopup(); + is(chatbar.selectedChat, null, "nothing should be selected"); + is(chatbar.chatboxForURL.size, 0, "chatboxForURL map should be empty"); + port.close(); + next(); + }); + }, + + testRemoveAllMinimized: function(next) { + this.testRemoveAll(next, "minimized"); + }, + + // Check what happens when you close the only visible chat. + testCloseOnlyVisible: function(next) { + let chatbar = window.SocialChatBar.chatbar; + let chatWidth = undefined; + let num = 0; + is(chatbar.childNodes.length, 0, "chatbar starting empty"); + is(chatbar.menupopup.childNodes.length, 0, "popup starting empty"); + + makeChat("normal", "first chat", function() { + // got the first one. + checkPopup(); + ok(chatbar.menupopup.parentNode.collapsed, "menu selection isn't visible"); + // we kinda cheat here and get the width of the first chat, assuming + // that all future chats will have the same width when open. + chatWidth = chatbar.calcTotalWidthOf(chatbar.selectedChat); + let desired = chatWidth * 1.5; + resizeWindowToChatAreaWidth(desired, function(sizedOk) { + ok(sizedOk, "can't do any tests without this width"); + checkPopup(); + makeChat("normal", "second chat", function() { + is(chatbar.childNodes.length, 2, "now have 2 chats"); + let first = chatbar.childNodes[0]; + let second = chatbar.childNodes[1]; + is(chatbar.selectedChat, first, "first chat is selected"); + ok(second.collapsed, "second chat is currently collapsed"); + // closing the first chat will leave enough room for the second + // chat to appear, and thus become selected. + chatbar.selectedChat.close(); + is(chatbar.selectedChat, second, "second chat is selected"); + closeAllChats(); + next(); + }); + }); + }); + }, + + testShowWhenCollapsed: function(next) { + let port = Social.provider.getWorkerPort(); + port.postMessage({topic: "test-init"}); + get3ChatsForCollapsing("normal", function(first, second, third) { + let chatbar = window.SocialChatBar.chatbar; + chatbar.showChat(first); + ok(!first.collapsed, "first should no longer be collapsed"); + ok(second.collapsed || third.collapsed, false, "one of the others should be collapsed"); + closeAllChats(); + port.close(); + next(); + }); + }, + + testActivity: function(next) { + let port = Social.provider.getWorkerPort(); + port.postMessage({topic: "test-init"}); + get3ChatsForCollapsing("normal", function(first, second, third) { + let chatbar = window.SocialChatBar.chatbar; + is(chatbar.selectedChat, third, "third chat should be selected"); + ok(!chatbar.selectedChat.hasAttribute("activity"), "third chat should have no activity"); + // send an activity message to the second. + ok(!second.hasAttribute("activity"), "second chat should have no activity"); + let chat2 = second.content; + let evt = chat2.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent("socialChatActivity", true, true, {}); + chat2.contentDocument.documentElement.dispatchEvent(evt); + // second should have activity. + ok(second.hasAttribute("activity"), "second chat should now have activity"); + // select the second - it should lose "activity" + chatbar.selectedChat = second; + ok(!second.hasAttribute("activity"), "second chat should no longer have activity"); + // Now try the first - it is collapsed, so the 'nub' also gets activity attr. + ok(!first.hasAttribute("activity"), "first chat should have no activity"); + let chat1 = first.content; + let evt = chat1.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent("socialChatActivity", true, true, {}); + chat1.contentDocument.documentElement.dispatchEvent(evt); + ok(first.hasAttribute("activity"), "first chat should now have activity"); + ok(chatbar.nub.hasAttribute("activity"), "nub should also have activity"); + // first is collapsed, so use openChat to get it. + chatbar.openChat(Social.provider, first.getAttribute("src")); + ok(!first.hasAttribute("activity"), "first chat should no longer have activity"); + // The nub should lose the activity flag here too + todo(!chatbar.nub.hasAttribute("activity"), "Bug 806266 - nub should no longer have activity"); + // TODO: tests for bug 806266 should arrange to have 2 chats collapsed + // then open them checking the nub is updated correctly. + // Now we will go and change the embedded browser in the second chat and + // ensure the activity magic still works (ie, check that the unload for + // the browser didn't cause our event handlers to be removed.) + ok(!second.hasAttribute("activity"), "second chat should have no activity"); + let subiframe = chat2.contentDocument.getElementById("iframe"); + subiframe.contentWindow.addEventListener("unload", function subunload() { + subiframe.contentWindow.removeEventListener("unload", subunload); + // ensure all other unload listeners have fired. + executeSoon(function() { + let evt = chat2.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent("socialChatActivity", true, true, {}); + chat2.contentDocument.documentElement.dispatchEvent(evt); + ok(second.hasAttribute("activity"), "second chat still has activity after unloading sub-iframe"); + closeAllChats(); + port.close(); + next(); + }) + }) + subiframe.setAttribute("src", "data:text/plain:new location for iframe"); + }); + }, + + testOnlyOneCallback: function(next) { + let chats = document.getElementById("pinnedchats"); + let port = Social.provider.getWorkerPort(); + let numOpened = 0; + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "test-init-done": + port.postMessage({topic: "test-chatbox-open"}); + break; + case "chatbox-opened": + numOpened += 1; + port.postMessage({topic: "ping"}); + break; + case "pong": + executeSoon(function() { + is(numOpened, 1, "only got one open message"); + chats.removeAll(); + port.close(); + next(); + }); + } + } + port.postMessage({topic: "test-init", data: { id: 1 }}); + }, + + testSecondTopLevelWindow: function(next) { + // Bug 817782 - check chats work in new top-level windows. + const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html"; + let port = Social.provider.getWorkerPort(); + let secondWindow; + port.onmessage = function(e) { + if (e.data.topic == "test-init-done") { + secondWindow = OpenBrowserWindow(); + secondWindow.addEventListener("load", function loadListener() { + secondWindow.removeEventListener("load", loadListener); + port.postMessage({topic: "test-worker-chat", data: chatUrl}); + }); + } else if (e.data.topic == "got-chatbox-message") { + // the chat was created - let's make sure it was created in the second window. + is(secondWindow.SocialChatBar.chatbar.childElementCount, 1); + secondWindow.close(); + next(); + } + } + port.postMessage({topic: "test-init"}); + }, + + testChatWindowChooser: function(next) { + // Tests that when a worker creates a chat, it is opened in the correct + // window. + const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html"; + let chatId = 1; + let port = Social.provider.getWorkerPort(); + port.postMessage({topic: "test-init"}); + + function openChat(callback) { + port.onmessage = function(e) { + if (e.data.topic == "got-chatbox-message") + callback(); + } + let url = chatUrl + "?" + (chatId++); + port.postMessage({topic: "test-worker-chat", data: url}); + } + + // open a chat (it will open in the main window) + ok(!window.SocialChatBar.hasChats, "first window should start with no chats"); + openChat(function() { + ok(window.SocialChatBar.hasChats, "first window has the chat"); + // create a second window - this will be the "most recent" and will + // therefore be the window that hosts the new chat (see bug 835111) + let secondWindow = OpenBrowserWindow(); + secondWindow.addEventListener("load", function loadListener() { + secondWindow.removeEventListener("load", loadListener); + ok(!secondWindow.SocialChatBar.hasChats, "second window has no chats"); + openChat(function() { + ok(secondWindow.SocialChatBar.hasChats, "second window now has chats"); + is(window.SocialChatBar.chatbar.childElementCount, 1, "first window still has 1 chat"); + window.SocialChatBar.chatbar.removeAll(); + // now open another chat - it should still open in the second. + openChat(function() { + ok(!window.SocialChatBar.hasChats, "first window has no chats"); + ok(secondWindow.SocialChatBar.hasChats, "second window has a chat"); + secondWindow.close(); + next(); + }); + }); + }) + }); + }, + + // XXX - note this must be the last test until we restore the login state + // between tests... + testCloseOnLogout: function(next) { + const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html"; + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.postMessage({topic: "test-init"}); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-chatbox-message": + ok(true, "got a chat window opened"); + port.postMessage({topic: "test-logout"}); + port.close(); + waitForCondition(function() document.getElementById("pinnedchats").firstChild == null, + next, + "chat windows didn't close"); + break; + } + } + port.postMessage({topic: "test-worker-chat", data: chatUrl}); + }, +} diff --git a/browser/base/content/test/social/browser_social_chatwindow_resize.js b/browser/base/content/test/social/browser_social_chatwindow_resize.js new file mode 100644 index 000000000..c6bd72078 --- /dev/null +++ b/browser/base/content/test/social/browser_social_chatwindow_resize.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + requestLongerTimeout(2); // only debug builds seem to need more time... + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png", + // added for test purposes + chatURL: "https://example.com/browser/browser/base/content/test/social/social_chat.html" + }; + let oldwidth = window.outerWidth; // we futz with these, so we restore them + let oldleft = window.screenX; + window.moveTo(0, window.screenY) + let postSubTest = function(cb) { + let chats = document.getElementById("pinnedchats"); + ok(chats.children.length == 0, "no chatty children left behind"); + cb(); + }; + + runSocialTestWithProvider(manifest, function (finishcb) { + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.postMessage({topic: "test-init"}); + // we require a logged in user for chats, wait for that + waitForCondition(function() { + let sbrowser = document.getElementById("social-sidebar-browser"); + return Social.provider && + Social.provider.profile && + Social.provider.profile.displayName && + sbrowser.docShellIsActive; + }, function() { + // executeSoon to let the browser UI observers run first + runSocialTests(tests, undefined, postSubTest, function() { + window.moveTo(oldleft, window.screenY) + window.resizeTo(oldwidth, window.outerHeight); + port.close(); + finishcb(); + }); + }, + "waitForProviderLoad: provider profile was not set"); + }); +} + +var tests = { + + // resize and collapse testing. + testBrowserResize: function(next, mode) { + let chats = document.getElementById("pinnedchats"); + get3ChatsForCollapsing(mode || "normal", function(first, second, third) { + let chatWidth = chats.getTotalChildWidth(first); + ok(chatWidth, "have a chatwidth"); + let popupWidth = getPopupWidth(); + ok(popupWidth, "have a popupwidth"); + info("starting resize tests - each chat's width is " + chatWidth + + " and the popup width is " + popupWidth); + // Note that due to a difference between "device", "app" and "css" pixels + // we allow use 2 pixels as the minimum size difference. + resizeAndCheckWidths(first, second, third, [ + [chatWidth-2, 1, "to < 1 chat width - only last should be visible."], + [chatWidth+2, 1, "2 pixels more then one fully exposed (not counting popup) - still only 1."], + [chatWidth+popupWidth+2, 1, "2 pixels more than one fully exposed (including popup) - still only 1."], + [chatWidth*2-2, 1, "second not showing by 2 pixels (not counting popup) - only 1 exposed."], + [chatWidth*2+popupWidth-2, 1, "second not showing by 2 pixelx (including popup) - only 1 exposed."], + [chatWidth*2+popupWidth+2, 2, "big enough to fit 2 - nub remains visible as first is still hidden"], + [chatWidth*3+popupWidth-2, 2, "one smaller than the size necessary to display all three - first still hidden"], + [chatWidth*3+popupWidth+2, 3, "big enough to fit all - all exposed (which removes the nub)"], + [chatWidth*3+2, 3, "now the nub is hidden we can resize back down to chatWidth*3 before overflow."], + [chatWidth*3-2, 2, "2 pixels less and the first is again collapsed (and the nub re-appears)"], + [chatWidth*2+popupWidth+2, 2, "back down to just big enough to fit 2"], + [chatWidth*2+popupWidth-2, 1, "back down to just not enough to fit 2"], + [chatWidth*3+popupWidth+2, 3, "now a large jump to make all 3 visible (ie, affects 2)"], + [chatWidth*1.5, 1, "and a large jump back down to 1 visible (ie, affects 2)"], + ], function() { + closeAllChats(); + next(); + }); + }); + }, + + testBrowserResizeMinimized: function(next) { + this.testBrowserResize(next); + } +} diff --git a/browser/base/content/test/social/browser_social_chatwindowfocus.js b/browser/base/content/test/social/browser_social_chatwindowfocus.js new file mode 100644 index 000000000..96824326b --- /dev/null +++ b/browser/base/content/test/social/browser_social_chatwindowfocus.js @@ -0,0 +1,360 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Is the currently opened tab focused? +function isTabFocused() { + let tabb = gBrowser.getBrowserForTab(gBrowser.selectedTab); + return Services.focus.focusedWindow == tabb.contentWindow; +} + +function isChatFocused(chat) { + return SocialChatBar.chatbar._isChatFocused(chat); +} + +function openChatViaUser() { + let sidebarDoc = document.getElementById("social-sidebar-browser").contentDocument; + let button = sidebarDoc.getElementById("chat-opener"); + // Note we must use synthesizeMouseAtCenter() rather than calling + // .click() directly as this causes nsIDOMWindowUtils.isHandlingUserInput + // to be true. + EventUtils.synthesizeMouseAtCenter(button, {}, sidebarDoc.defaultView); +} + +function openChatViaSidebarMessage(port, data, callback) { + port.onmessage = function (e) { + if (e.data.topic == "chatbox-opened") + callback(); + } + port.postMessage({topic: "test-chatbox-open", data: data}); +} + +function openChatViaWorkerMessage(port, data, callback) { + // sadly there is no message coming back to tell us when the chat has + // been opened, so we wait until one appears. + let chatbar = SocialChatBar.chatbar; + let numExpected = chatbar.childElementCount + 1; + port.postMessage({topic: "test-worker-chat", data: data}); + waitForCondition(function() chatbar.childElementCount == numExpected, + function() { + // so the child has been added, but we don't know if it + // has been intialized - re-request it and the callback + // means it's done. Minimized, same as the worker. + SocialChatBar.openChat(Social.provider, + data, + function() { + callback(); + }, + "minimized"); + }, + "No new chat appeared"); +} + + +let isSidebarLoaded = false; + +function startTestAndWaitForSidebar(callback) { + let doneCallback; + let port = Social.provider.getWorkerPort(); + function maybeCallback() { + if (!doneCallback) + callback(port); + doneCallback = true; + } + port.onmessage = function(e) { + let topic = e.data.topic; + switch (topic) { + case "got-sidebar-message": + // if sidebar loaded too fast, we need a backup ping + case "got-isVisible-response": + isSidebarLoaded = true; + maybeCallback(); + break; + case "test-init-done": + if (isSidebarLoaded) + maybeCallback(); + else + port.postMessage({topic: "test-isVisible"}); + break; + } + } + port.postMessage({topic: "test-init"}); +} + +let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" +}; + +function test() { + waitForExplicitFinish(); + + // Note that (probably) due to bug 604289, if a tab is focused but the + // focused element is null, our chat windows can "steal" focus. This is + // avoided if we explicitly focus an element in the tab. + // So we load a page with an <input> field and focus that before testing. + let url = "data:text/html;charset=utf-8," + encodeURI('<input id="theinput">'); + let tab = gBrowser.selectedTab = gBrowser.addTab(url, {skipAnimation: true}); + tab.linkedBrowser.addEventListener("load", function tabLoad(event) { + tab.linkedBrowser.removeEventListener("load", tabLoad, true); + // before every test we focus the input field. + let preSubTest = function(cb) { + // XXX - when bug 604289 is fixed it should be possible to just do: + // tab.linkedBrowser.contentWindow.focus() + // but instead we must do: + tab.linkedBrowser.contentDocument.getElementById("theinput").focus(); + waitForCondition(function() isTabFocused(), cb, "tab should have focus"); + } + let postSubTest = function(cb) { + window.SocialChatBar.chatbar.removeAll(); + cb(); + } + // and run the tests. + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, preSubTest, postSubTest, function () { + finishcb(); + }); + }); + }, true); + registerCleanupFunction(function() { + gBrowser.removeTab(tab); + }); + +} + +var tests = { + // In this test the worker asks the sidebar to open a chat. As that means + // we aren't handling user-input we will not focus the chatbar. + // Then we do it again - should still not be focused. + // Then we perform a user-initiated request - it should get focus. + testNoFocusWhenViaWorker: function(next) { + startTestAndWaitForSidebar(function(port) { + openChatViaSidebarMessage(port, {stealFocus: 1}, function() { + ok(true, "got chatbox message"); + is(SocialChatBar.chatbar.childElementCount, 1, "exactly 1 chat open"); + ok(isTabFocused(), "tab should still be focused"); + // re-request the same chat via a message. + openChatViaSidebarMessage(port, {stealFocus: 1}, function() { + is(SocialChatBar.chatbar.childElementCount, 1, "still exactly 1 chat open"); + ok(isTabFocused(), "tab should still be focused"); + // re-request the same chat via user event. + openChatViaUser(); + waitForCondition(function() isChatFocused(SocialChatBar.chatbar.selectedChat), + function() { + is(SocialChatBar.chatbar.childElementCount, 1, "still exactly 1 chat open"); + is(SocialChatBar.chatbar.selectedChat, SocialChatBar.chatbar.firstElementChild, "chat should be selected"); + next(); + }, "chat should be focused"); + }); + }); + }); + }, + + // In this test we arrange for the sidebar to open the chat via a simulated + // click. This should cause the new chat to be opened and focused. + testFocusWhenViaUser: function(next) { + startTestAndWaitForSidebar(function(port) { + openChatViaUser(); + ok(SocialChatBar.chatbar.firstElementChild, "chat opened"); + waitForCondition(function() isChatFocused(SocialChatBar.chatbar.selectedChat), + function() { + is(SocialChatBar.chatbar.selectedChat, SocialChatBar.chatbar.firstElementChild, "chat is selected"); + next(); + }, "chat should be focused"); + }); + }, + + // Open a chat via the worker - it will open and not have focus. + // Then open the same chat via a sidebar message - it will be restored but + // should still not have grabbed focus. + testNoFocusOnAutoRestore: function(next) { + const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html?id=1"; + let chatbar = SocialChatBar.chatbar; + startTestAndWaitForSidebar(function(port) { + openChatViaWorkerMessage(port, chatUrl, function() { + is(chatbar.childElementCount, 1, "exactly 1 chat open"); + // bug 865086 opening minimized still sets the window as selected + todo(chatbar.selectedChat != chatbar.firstElementChild, "chat is not selected"); + ok(isTabFocused(), "tab should be focused"); + openChatViaSidebarMessage(port, {stealFocus: 1, id: 1}, function() { + is(chatbar.childElementCount, 1, "still 1 chat open"); + ok(!chatbar.firstElementChild.minimized, "chat no longer minimized"); + // bug 865086 because we marked it selected on open, it still is + todo(chatbar.selectedChat != chatbar.firstElementChild, "chat is not selected"); + ok(isTabFocused(), "tab should still be focused"); + next(); + }); + }); + }); + }, + + // Here we open a chat, which will not be focused. Then we minimize it and + // restore it via a titlebar clock - it should get focus at that point. + testFocusOnExplicitRestore: function(next) { + startTestAndWaitForSidebar(function(port) { + openChatViaSidebarMessage(port, {stealFocus: 1}, function() { + ok(true, "got chatbox message"); + ok(isTabFocused(), "tab should still be focused"); + let chatbox = SocialChatBar.chatbar.firstElementChild; + ok(chatbox, "chat opened"); + chatbox.minimized = true; + ok(isTabFocused(), "tab should still be focused"); + // pretend we clicked on the titlebar + chatbox.onTitlebarClick({button: 0}); + waitForCondition(function() isChatFocused(SocialChatBar.chatbar.selectedChat), + function() { + ok(!chatbox.minimized, "chat should have been restored"); + ok(isChatFocused(chatbox), "chat should be focused"); + is(chatbox, SocialChatBar.chatbar.selectedChat, "chat is marked selected"); + next(); + }, "chat should have focus"); + }); + }); + }, + + // Open 2 chats and give 1 focus. Minimize the focused one - the second + // should get focus. + testMinimizeFocused: function(next) { + let chatbar = SocialChatBar.chatbar; + startTestAndWaitForSidebar(function(port) { + openChatViaSidebarMessage(port, {stealFocus: 1, id: 1}, function() { + let chat1 = chatbar.firstElementChild; + openChatViaSidebarMessage(port, {stealFocus: 1, id: 2}, function() { + is(chatbar.childElementCount, 2, "exactly 2 chats open"); + let chat2 = chat1.nextElementSibling || chat1.previousElementSibling; + chatbar.selectedChat = chat1; + chatbar.focus(); + waitForCondition(function() isChatFocused(chat1), + function() { + is(chat1, SocialChatBar.chatbar.selectedChat, "chat1 is marked selected"); + isnot(chat2, SocialChatBar.chatbar.selectedChat, "chat2 is not marked selected"); + chat1.minimized = true; + waitForCondition(function() isChatFocused(chat2), + function() { + // minimizing the chat with focus should give it to another. + isnot(chat1, SocialChatBar.chatbar.selectedChat, "chat1 is not marked selected"); + is(chat2, SocialChatBar.chatbar.selectedChat, "chat2 is marked selected"); + next(); + }, "chat2 should have focus"); + }, "chat1 should have focus"); + }); + }); + }); + }, + + // Open 2 chats, select (but not focus) one, then re-request it be + // opened via a message. Focus should not move. + testReopenNonFocused: function(next) { + let chatbar = SocialChatBar.chatbar; + startTestAndWaitForSidebar(function(port) { + openChatViaSidebarMessage(port, {id: 1}, function() { + let chat1 = chatbar.firstElementChild; + openChatViaSidebarMessage(port, {id: 2}, function() { + let chat2 = chat1.nextElementSibling || chat1.previousElementSibling; + chatbar.selectedChat = chat2; + // tab still has focus + ok(isTabFocused(), "tab should still be focused"); + // re-request the first. + openChatViaSidebarMessage(port, {id: 1}, function() { + is(chatbar.selectedChat, chat1, "chat1 now selected"); + ok(isTabFocused(), "tab should still be focused"); + next(); + }); + }); + }); + }); + }, + + // Open 2 chats, select and focus the second. Pressing the TAB key should + // cause focus to move between all elements in our chat window before moving + // to the next chat window. + testTab: function(next) { + function sendTabAndWaitForFocus(chat, eltid, callback) { + // ideally we would use the 'focus' event here, but that doesn't work + // as expected for the iframe - the iframe itself never gets the focus + // event (apparently the sub-document etc does.) + // So just poll for the correct element getting focus... + let doc = chat.contentDocument; + EventUtils.sendKey("tab"); + waitForCondition(function() { + let elt = eltid ? doc.getElementById(eltid) : doc.documentElement; + return doc.activeElement == elt; + }, callback, "element " + eltid + " never got focus"); + } + + let chatbar = SocialChatBar.chatbar; + startTestAndWaitForSidebar(function(port) { + openChatViaSidebarMessage(port, {id: 1}, function() { + let chat1 = chatbar.firstElementChild; + openChatViaSidebarMessage(port, {id: 2}, function() { + let chat2 = chat1.nextElementSibling || chat1.previousElementSibling; + chatbar.selectedChat = chat2; + chatbar.focus(); + waitForCondition(function() isChatFocused(chatbar.selectedChat), + function() { + // Our chats have 3 focusable elements, so it takes 4 TABs to move + // to the new chat. + sendTabAndWaitForFocus(chat2, "input1", function() { + is(chat2.contentDocument.activeElement.getAttribute("id"), "input1", + "first input field has focus"); + ok(isChatFocused(chat2), "new chat still focused after first tab"); + sendTabAndWaitForFocus(chat2, "input2", function() { + ok(isChatFocused(chat2), "new chat still focused after tab"); + is(chat2.contentDocument.activeElement.getAttribute("id"), "input2", + "second input field has focus"); + sendTabAndWaitForFocus(chat2, "iframe", function() { + ok(isChatFocused(chat2), "new chat still focused after tab"); + is(chat2.contentDocument.activeElement.getAttribute("id"), "iframe", + "iframe has focus"); + // this tab now should move to the next chat, but focus the + // document element itself (hence the null eltid) + sendTabAndWaitForFocus(chat1, null, function() { + ok(isChatFocused(chat1), "first chat is focused"); + next(); + }); + }); + }); + }); + }, "chat should have focus"); + }); + }); + }); + }, + + // Open a chat and focus an element other than the first. Move focus to some + // other item (the tab itself in this case), then focus the chatbar - the + // same element that was previously focused should still have focus. + testFocusedElement: function(next) { + let chatbar = SocialChatBar.chatbar; + startTestAndWaitForSidebar(function(port) { + openChatViaUser(); + let chat = chatbar.firstElementChild; + // need to wait for the content to load before we can focus it. + chat.addEventListener("DOMContentLoaded", function DOMContentLoaded() { + chat.removeEventListener("DOMContentLoaded", DOMContentLoaded); + chat.contentDocument.getElementById("input2").focus(); + waitForCondition(function() isChatFocused(chat), + function() { + is(chat.contentDocument.activeElement.getAttribute("id"), "input2", + "correct input field has focus"); + // set focus to the tab. + let tabb = gBrowser.getBrowserForTab(gBrowser.selectedTab); + Services.focus.moveFocus(tabb.contentWindow, null, Services.focus.MOVEFOCUS_ROOT, 0); + waitForCondition(function() isTabFocused(), + function() { + chatbar.focus(); + waitForCondition(function() isChatFocused(chat), + function() { + is(chat.contentDocument.activeElement.getAttribute("id"), "input2", + "correct input field still has focus"); + next(); + }, "chat took focus"); + }, "tab has focus"); + }, "chat took focus"); + }); + }); + }, +}; diff --git a/browser/base/content/test/social/browser_social_errorPage.js b/browser/base/content/test/social/browser_social_errorPage.js new file mode 100644 index 000000000..325d5c80e --- /dev/null +++ b/browser/base/content/test/social/browser_social_errorPage.js @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 gc() { + Cu.forceGC(); + let wu = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils); + wu.garbageCollect(); +} + +// Support for going on and offline. +// (via browser/base/content/test/browser_bookmark_titles.js) +let origProxyType = Services.prefs.getIntPref('network.proxy.type'); + +function goOffline() { + // Simulate a network outage with offline mode. (Localhost is still + // accessible in offline mode, so disable the test proxy as well.) + if (!Services.io.offline) + BrowserOffline.toggleOfflineStatus(); + Services.prefs.setIntPref('network.proxy.type', 0); + // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache. + Services.cache.evictEntries(Components.interfaces.nsICache.STORE_ANYWHERE); +} + +function goOnline(callback) { + Services.prefs.setIntPref('network.proxy.type', origProxyType); + if (Services.io.offline) + BrowserOffline.toggleOfflineStatus(); + if (callback) + callback(); +} + +function openPanel(url, panelCallback, loadCallback) { + // open a flyout + SocialFlyout.open(url, 0, panelCallback); + SocialFlyout.panel.firstChild.addEventListener("load", function panelLoad() { + SocialFlyout.panel.firstChild.removeEventListener("load", panelLoad, true); + loadCallback(); + }, true); +} + +function openChat(url, panelCallback, loadCallback) { + // open a chat window + SocialChatBar.openChat(Social.provider, url, panelCallback); + SocialChatBar.chatbar.firstChild.addEventListener("DOMContentLoaded", function panelLoad() { + SocialChatBar.chatbar.firstChild.removeEventListener("DOMContentLoaded", panelLoad, true); + loadCallback(); + }, true); +} + +function onSidebarLoad(callback) { + let sbrowser = document.getElementById("social-sidebar-browser"); + sbrowser.addEventListener("load", function load() { + sbrowser.removeEventListener("load", load, true); + callback(); + }, true); +} + +function ensureWorkerLoaded(provider, callback) { + // once the worker responds to a ping we know it must be up. + let port = provider.getWorkerPort(); + port.onmessage = function(msg) { + if (msg.data.topic == "pong") { + port.close(); + callback(); + } + } + port.postMessage({topic: "ping"}) +} + +let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" +}; + +function test() { + waitForExplicitFinish(); + // we don't want the sidebar to auto-load in these tests.. + Services.prefs.setBoolPref("social.sidebar.open", false); + registerCleanupFunction(function() { + Services.prefs.clearUserPref("social.sidebar.open"); + }); + + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, goOnline, finishcb); + }); +} + +var tests = { + testSidebar: function(next) { + let sbrowser = document.getElementById("social-sidebar-browser"); + onSidebarLoad(function() { + ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page"); + gc(); + // Add a new load listener, then find and click the "try again" button. + onSidebarLoad(function() { + // should still be on the error page. + ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "is still on social error page"); + // go online and try again - this should work. + goOnline(); + onSidebarLoad(function() { + // should now be on the correct page. + is(sbrowser.contentDocument.location.href, manifest.sidebarURL, "is now on social sidebar page"); + next(); + }); + sbrowser.contentDocument.getElementById("btnTryAgain").click(); + }); + sbrowser.contentDocument.getElementById("btnTryAgain").click(); + }); + // we want the worker to be fully loaded before going offline, otherwise + // it might fail due to going offline. + ensureWorkerLoaded(Social.provider, function() { + // go offline then attempt to load the sidebar - it should fail. + goOffline(); + Services.prefs.setBoolPref("social.sidebar.open", true); + }); + }, + + testFlyout: function(next) { + let panelCallbackCount = 0; + let panel = document.getElementById("social-flyout-panel"); + // go offline and open a flyout. + goOffline(); + openPanel( + "https://example.com/browser/browser/base/content/test/social/social_panel.html", + function() { // the panel api callback + panelCallbackCount++; + }, + function() { // the "load" callback. + executeSoon(function() { + todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads."); + ok(panel.firstChild.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page"); + // Bug 832943 - the listeners previously stopped working after a GC, so + // force a GC now and try again. + gc(); + openPanel( + "https://example.com/browser/browser/base/content/test/social/social_panel.html", + function() { // the panel api callback + panelCallbackCount++; + }, + function() { // the "load" callback. + executeSoon(function() { + todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads."); + ok(panel.firstChild.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page"); + gc(); + SocialFlyout.unload(); + next(); + }); + } + ); + }); + } + ); + }, + + testChatWindow: function(next) { + let panelCallbackCount = 0; + // go offline and open a flyout. + goOffline(); + openChat( + "https://example.com/browser/browser/base/content/test/social/social_chat.html", + function() { // the panel api callback + panelCallbackCount++; + }, + function() { // the "load" callback. + executeSoon(function() { + todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads."); + let chat = SocialChatBar.chatbar.selectedChat; + waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0, + function() { + chat.close(); + next(); + }, + "error page didn't appear"); + }); + } + ); + } +} diff --git a/browser/base/content/test/social/browser_social_flyout.js b/browser/base/content/test/social/browser_social_flyout.js new file mode 100644 index 000000000..b8642e338 --- /dev/null +++ b/browser/base/content/test/social/browser_social_flyout.js @@ -0,0 +1,164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" + }; + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, undefined, finishcb); + }); +} + +var tests = { + testOpenCloseFlyout: function(next) { + let panel = document.getElementById("social-flyout-panel"); + panel.addEventListener("popupshowing", function onShowing() { + panel.removeEventListener("popupshowing", onShowing); + is(panel.firstChild.contentDocument.readyState, "complete", "panel is loaded prior to showing"); + }); + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-sidebar-message": + port.postMessage({topic: "test-flyout-open"}); + break; + case "got-flyout-visibility": + if (e.data.result == "hidden") { + ok(true, "flyout visibility is 'hidden'"); + is(panel.state, "closed", "panel really is closed"); + port.close(); + next(); + } else if (e.data.result == "shown") { + ok(true, "flyout visibility is 'shown"); + port.postMessage({topic: "test-flyout-close"}); + } + break; + case "got-flyout-message": + ok(e.data.result == "ok", "got flyout message"); + break; + } + } + port.postMessage({topic: "test-init"}); + }, + + testResizeFlyout: function(next) { + let panel = document.getElementById("social-flyout-panel"); + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "test-init-done": + port.postMessage({topic: "test-flyout-open"}); + break; + case "got-flyout-visibility": + if (e.data.result != "shown") + return; + // The width of the flyout should be 400px initially + let iframe = panel.firstChild; + let body = iframe.contentDocument.body; + let cs = iframe.contentWindow.getComputedStyle(body); + + is(cs.width, "400px", "should be 400px wide"); + is(iframe.boxObject.width, 400, "iframe should now be 400px wide"); + is(cs.height, "400px", "should be 400px high"); + is(iframe.boxObject.height, 400, "iframe should now be 400px high"); + + iframe.contentWindow.addEventListener("resize", function _doneHandler() { + iframe.contentWindow.removeEventListener("resize", _doneHandler, false); + cs = iframe.contentWindow.getComputedStyle(body); + + is(cs.width, "500px", "should now be 500px wide"); + is(iframe.boxObject.width, 500, "iframe should now be 500px wide"); + is(cs.height, "500px", "should now be 500px high"); + is(iframe.boxObject.height, 500, "iframe should now be 500px high"); + panel.hidePopup(); + port.close(); + next(); + }, false); + SocialFlyout.dispatchPanelEvent("socialTest-MakeWider"); + break; + } + } + port.postMessage({topic: "test-init"}); + }, + + testCloseSelf: function(next) { + // window.close is affected by the pref dom.allow_scripts_to_close_windows, + // which defaults to false, but is set to true by the test harness. + // so temporarily set it back. + const ALLOW_SCRIPTS_TO_CLOSE_PREF = "dom.allow_scripts_to_close_windows"; + // note clearUserPref doesn't do what we expect, as the test harness itself + // changes the pref value - so clearUserPref resets it to false rather than + // the true setup by the test harness. + let oldAllowScriptsToClose = Services.prefs.getBoolPref(ALLOW_SCRIPTS_TO_CLOSE_PREF); + Services.prefs.setBoolPref(ALLOW_SCRIPTS_TO_CLOSE_PREF, false); + let panel = document.getElementById("social-flyout-panel"); + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "test-init-done": + port.postMessage({topic: "test-flyout-open"}); + break; + case "got-flyout-visibility": + let iframe = panel.firstChild; + iframe.contentDocument.addEventListener("SocialTest-DoneCloseSelf", function _doneHandler() { + iframe.contentDocument.removeEventListener("SocialTest-DoneCloseSelf", _doneHandler, false); + is(panel.state, "closed", "flyout should have closed itself"); + Services.prefs.setBoolPref(ALLOW_SCRIPTS_TO_CLOSE_PREF, oldAllowScriptsToClose); + next(); + }, false); + is(panel.state, "open", "flyout should be open"); + port.close(); // so we don't get the -visibility message as it hides... + SocialFlyout.dispatchPanelEvent("socialTest-CloseSelf"); + break; + } + } + port.postMessage({topic: "test-init"}); + }, + + testCloseOnLinkTraversal: function(next) { + + function onTabOpen(event) { + gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true); + waitForCondition(function() { return panel.state == "closed" }, function() { + gBrowser.removeTab(event.target); + next(); + }, "panel should close after tab open"); + } + + let panel = document.getElementById("social-flyout-panel"); + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "test-init-done": + port.postMessage({topic: "test-flyout-open"}); + break; + case "got-flyout-visibility": + if (e.data.result == "shown") { + // click on our test link + is(panel.state, "open", "flyout should be open"); + gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true); + let iframe = panel.firstChild; + iframe.contentDocument.getElementById('traversal').click(); + } + break; + } + } + port.postMessage({topic: "test-init"}); + } +} diff --git a/browser/base/content/test/social/browser_social_isVisible.js b/browser/base/content/test/social/browser_social_isVisible.js new file mode 100644 index 000000000..6ae6b9d1f --- /dev/null +++ b/browser/base/content/test/social/browser_social_isVisible.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" + }; + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, undefined, finishcb); + }); +} + +var tests = { + testSidebarMessage: function(next) { + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.postMessage({topic: "test-init"}); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-sidebar-message": + // The sidebar message will always come first, since it loads by default + ok(true, "got sidebar message"); + port.close(); + next(); + break; + } + }; + }, + testIsVisible: function(next) { + let port = Social.provider.getWorkerPort(); + port.postMessage({topic: "test-init"}); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-isVisible-response": + is(e.data.result, true, "Sidebar should be visible by default"); + Social.toggleSidebar(); + port.close(); + next(); + } + }; + port.postMessage({topic: "test-isVisible"}); + }, + testIsNotVisible: function(next) { + let port = Social.provider.getWorkerPort(); + port.postMessage({topic: "test-init"}); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-isVisible-response": + is(e.data.result, false, "Sidebar should be hidden"); + Services.prefs.clearUserPref("social.sidebar.open"); + port.close(); + next(); + } + }; + port.postMessage({topic: "test-isVisible"}); + } +} diff --git a/browser/base/content/test/social/browser_social_markButton.js b/browser/base/content/test/social/browser_social_markButton.js new file mode 100644 index 000000000..eb7b6dead --- /dev/null +++ b/browser/base/content/test/social/browser_social_markButton.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 prefName = "social.enabled", + gFinishCB; + +function test() { + waitForExplicitFinish(); + + // Need to load a http/https/ftp/ftps page for the social mark button to appear + let tab = gBrowser.selectedTab = gBrowser.addTab("https://test1.example.com", {skipAnimation: true}); + tab.linkedBrowser.addEventListener("load", function tabLoad(event) { + tab.linkedBrowser.removeEventListener("load", tabLoad, true); + executeSoon(tabLoaded); + }, true); + + registerCleanupFunction(function() { + Services.prefs.clearUserPref(prefName); + gBrowser.removeTab(tab); + }); +} + +function tabLoaded() { + ok(Social, "Social module loaded"); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" + }; + runSocialTestWithProvider(manifest, function (finishcb) { + gFinishCB = finishcb; + testInitial(); + }); +} + +function testInitial(finishcb) { + ok(Social.provider, "Social provider is active"); + ok(Social.provider.enabled, "Social provider is enabled"); + let port = Social.provider.getWorkerPort(); + ok(port, "Social provider has a port to its FrameWorker"); + port.close(); + + let markButton = SocialMark.button; + ok(markButton, "mark button exists"); + + // ensure the worker initialization and handshakes are all done and we + // have a profile and the worker has sent a page-mark-config msg. + waitForCondition(function() Social.provider.pageMarkInfo != null, function() { + is(markButton.hasAttribute("marked"), false, "SocialMark button should not have 'marked' attribute before mark button is clicked"); + // Check the strings from our worker actually ended up on the button. + is(markButton.getAttribute("tooltiptext"), "Mark this page", "check tooltip text is correct"); + // Check the relative URL was resolved correctly (note this image has offsets of zero...) + is(markButton.style.listStyleImage, 'url("https://example.com/browser/browser/base/content/test/social/social_mark_image.png")', "check image url is correct"); + + // Test the mark button command handler + SocialMark.togglePageMark(function() { + is(markButton.hasAttribute("marked"), true, "mark button should have 'marked' attribute after mark button is clicked"); + is(markButton.getAttribute("tooltiptext"), "Unmark this page", "check tooltip text is correct"); + // Check the URL and offsets were applied correctly + is(markButton.style.listStyleImage, 'url("https://example.com/browser/browser/base/content/test/social/social_mark_image.png")', "check image url is correct"); + SocialMark.togglePageMark(function() { + is(markButton.hasAttribute("marked"), false, "mark button should not be marked"); + executeSoon(function() { + testStillMarkedIn2Tabs(); + }); + }); + }); + }, "provider didn't provide page-mark-config"); +} + +function testStillMarkedIn2Tabs() { + let toMark = "http://test2.example.com"; + let markUri = Services.io.newURI(toMark, null, null); + let markButton = SocialMark.button; + let initialTab = gBrowser.selectedTab; + info("initialTab has loaded " + gBrowser.currentURI.spec); + is(markButton.hasAttribute("marked"), false, "SocialMark button should not have 'marked' for the initial tab"); + + addTab(toMark, function(tab1) { + addTab(toMark, function(tab2) { + // should start without either page being marked. + is(markButton.hasAttribute("marked"), false, "SocialMark button should not have 'marked' before we've done anything"); + Social.isURIMarked(markUri, function(marked) { + ok(!marked, "page is unmarked in annotations"); + markButton.click(); + waitForCondition(function() markButton.hasAttribute("marked"), function() { + Social.isURIMarked(markUri, function(marked) { + ok(marked, "page is marked in annotations"); + // and switching to the first tab (with the same URL) should still reflect marked. + selectBrowserTab(tab1, function() { + is(markButton.hasAttribute("marked"), true, "SocialMark button should reflect the marked state"); + // wait for tabselect + selectBrowserTab(initialTab, function() { + waitForCondition(function() !markButton.hasAttribute("marked"), function() { + gBrowser.selectedTab = tab1; + + SocialMark.togglePageMark(function() { + Social.isURIMarked(gBrowser.currentURI, function(marked) { + ok(!marked, "page is unmarked in annotations"); + is(markButton.hasAttribute("marked"), false, "mark button should not be marked"); + gBrowser.removeTab(tab1); + gBrowser.removeTab(tab2); + executeSoon(testStillMarkedAfterReopen); + }); + }); + }, "button has been unmarked"); + }); + }); + }); + }, "button has been marked"); + }); + }); + }); +} + +function testStillMarkedAfterReopen() { + let toMark = "http://test2.example.com"; + let markButton = SocialMark.button; + + is(markButton.hasAttribute("marked"), false, "Reopen: SocialMark button should not have 'marked' for the initial tab"); + addTab(toMark, function(tab) { + SocialMark.togglePageMark(function() { + is(markButton.hasAttribute("marked"), true, "SocialMark button should reflect the marked state"); + gBrowser.removeTab(tab); + // should be on the initial unmarked tab now. + waitForCondition(function() !markButton.hasAttribute("marked"), function() { + // now open the same URL - should be back to Marked. + addTab(toMark, function(tab) { + is(markButton.hasAttribute("marked"), true, "New tab to previously marked URL should reflect marked state"); + SocialMark.togglePageMark(function() { + gBrowser.removeTab(tab); + executeSoon(testOnlyMarkCertainUrlsTabSwitch); + }); + }); + }, "button is now unmarked"); + }); + }); +} + +function testOnlyMarkCertainUrlsTabSwitch() { + let toMark = "http://test2.example.com"; + let notSharable = "about:blank"; + let markButton = SocialMark.button; + addTab(toMark, function(tab) { + ok(!markButton.hidden, "SocialMark button not hidden for http url"); + addTab(notSharable, function(tab2) { + ok(markButton.disabled, "SocialMark button disabled for about:blank"); + selectBrowserTab(tab, function() { + ok(!markButton.disabled, "SocialMark button re-shown when switching back to http: url"); + selectBrowserTab(tab2, function() { + ok(markButton.disabled, "SocialMark button re-hidden when switching back to about:blank"); + gBrowser.removeTab(tab); + gBrowser.removeTab(tab2); + executeSoon(testOnlyMarkCertainUrlsSameTab); + }); + }); + }); + }); +} + +function testOnlyMarkCertainUrlsSameTab() { + let toMark = "http://test2.example.com"; + let notSharable = "about:blank"; + let markButton = SocialMark.button; + addTab(toMark, function(tab) { + ok(!markButton.disabled, "SocialMark button not disabled for http url"); + loadIntoTab(tab, notSharable, function() { + ok(markButton.disabled, "SocialMark button disabled for about:blank"); + loadIntoTab(tab, toMark, function() { + ok(!markButton.disabled, "SocialMark button re-enabled http url"); + gBrowser.removeTab(tab); + executeSoon(testDisable); + }); + }); + }); +} + +function testDisable() { + let markButton = SocialMark.button; + Services.prefs.setBoolPref(prefName, false); + is(markButton.hidden, true, "SocialMark button should be hidden when pref is disabled"); + gFinishCB(); +} diff --git a/browser/base/content/test/social/browser_social_mozSocial_API.js b/browser/base/content/test/social/browser_social_mozSocial_API.js new file mode 100644 index 000000000..5a1e08b31 --- /dev/null +++ b/browser/base/content/test/social/browser_social_mozSocial_API.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" + }; + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, undefined, finishcb); + }); +} + +var tests = { + testStatusIcons: function(next) { + let iconsReady = false; + let gotSidebarMessage = false; + + function checkNext() { + if (iconsReady && gotSidebarMessage) + triggerIconPanel(); + } + + function triggerIconPanel() { + waitForCondition(function() { + let mButton = document.getElementById("social-mark-button"); + let pButton = document.getElementById("social-provider-button"); + // wait for a new button to be inserted inbetween the provider and mark + // button + return pButton.nextSibling != mButton; + }, function() { + // Click the button to trigger its contentPanel + let statusIcon = document.getElementById("social-provider-button").nextSibling; + EventUtils.synthesizeMouseAtCenter(statusIcon, {}); + }, "Status icon didn't become non-hidden"); + } + + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "test-init-done": + iconsReady = true; + checkNext(); + break; + case "got-panel-message": + ok(true, "got panel message"); + // Check the panel isn't in our history. + gURLsNotRemembered.push(e.data.location); + break; + case "got-social-panel-visibility": + if (e.data.result == "shown") { + ok(true, "panel shown"); + let panel = document.getElementById("social-notification-panel"); + panel.hidePopup(); + } else if (e.data.result == "hidden") { + ok(true, "panel hidden"); + port.close(); + next(); + } + break; + case "got-sidebar-message": + // The sidebar message will always come first, since it loads by default + ok(true, "got sidebar message"); + gotSidebarMessage = true; + // load a status panel + port.postMessage({topic: "test-ambient-notification"}); + checkNext(); + break; + } + } + port.postMessage({topic: "test-init"}); + } +} diff --git a/browser/base/content/test/social/browser_social_multiprovider.js b/browser/base/content/test/social/browser_social_multiprovider.js new file mode 100644 index 000000000..7ec7f0e2a --- /dev/null +++ b/browser/base/content/test/social/browser_social_multiprovider.js @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function test() { + waitForExplicitFinish(); + + runSocialTestWithProvider(gProviders, function (finishcb) { + runSocialTests(tests, undefined, undefined, finishcb); + }); +} + +let gProviders = [ + { + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html?provider1", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "chrome://branding/content/icon48.png" + }, + { + name: "provider 2", + origin: "https://test1.example.com", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider2", + workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "chrome://branding/content/icon48.png" + } +]; + +var tests = { + testProviderSwitch: function(next) { + function checkProviderMenu(selectedProvider) { + let menu = document.getElementById("social-statusarea-popup"); + let menuProviders = menu.querySelectorAll(".social-provider-menuitem"); + is(menuProviders.length, gProviders.length, "correct number of providers listed in the menu"); + // Find the selectedProvider's menu item + let el = menu.getElementsByAttribute("origin", selectedProvider.origin); + is(el.length, 1, "selected provider menu item exists"); + is(el[0].getAttribute("checked"), "true", "selected provider menu item is checked"); + } + + checkProviderMenu(gProviders[0]); + + // Now wait for the initial provider profile to be set + waitForProviderLoad(function() { + checkUIStateMatchesProvider(gProviders[0]); + + // Now activate "provider 2" + observeProviderSet(function () { + waitForProviderLoad(function() { + checkUIStateMatchesProvider(gProviders[1]); + // disable social, click on the provider menuitem to switch providers + Social.enabled = false; + let menu = document.getElementById("social-statusarea-popup"); + let el = menu.getElementsByAttribute("origin", gProviders[0].origin); + is(el.length, 1, "selected provider menu item exists"); + el[0].click(); + waitForProviderLoad(function() { + checkUIStateMatchesProvider(gProviders[0]); + next(); + }); + }); + }); + Social.activateFromOrigin("https://test1.example.com"); + }); + } +} + +function checkUIStateMatchesProvider(provider) { + let profileData = getExpectedProfileData(provider); + // The toolbar + let loginStatus = document.getElementsByClassName("social-statusarea-loggedInStatus"); + for (let label of loginStatus) { + is(label.value, profileData.userName, "username name matches provider profile"); + } + // Sidebar + is(document.getElementById("social-sidebar-browser").getAttribute("src"), provider.sidebarURL, "side bar URL is set"); +} + +function getExpectedProfileData(provider) { + // This data is defined in social_worker.js + if (provider.origin == "https://test1.example.com") { + return { + displayName: "Test1 User", + userName: "tester" + }; + } + + return { + displayName: "Kuma Lisa", + userName: "trickster" + }; +} + +function observeProviderSet(cb) { + Services.obs.addObserver(function providerSet(subject, topic, data) { + Services.obs.removeObserver(providerSet, "social:provider-set"); + info("social:provider-set observer was notified"); + // executeSoon to let the browser UI observers run first + executeSoon(cb); + }, "social:provider-set", false); +} + +function waitForProviderLoad(cb) { + waitForCondition(function() { + let sbrowser = document.getElementById("social-sidebar-browser"); + return Social.provider.profile && + Social.provider.profile.displayName && + sbrowser.docShellIsActive; + }, cb, "waitForProviderLoad: provider profile was not set"); +} diff --git a/browser/base/content/test/social/browser_social_perwindowPB.js b/browser/base/content/test/social/browser_social_perwindowPB.js new file mode 100644 index 000000000..60fc28529 --- /dev/null +++ b/browser/base/content/test/social/browser_social_perwindowPB.js @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 openTab(win, url, callback) { + let newTab = win.gBrowser.addTab(url); + let tabBrowser = win.gBrowser.getBrowserForTab(newTab); + tabBrowser.addEventListener("load", function tabLoadListener() { + tabBrowser.removeEventListener("load", tabLoadListener, true); + win.gBrowser.selectedTab = newTab; + callback(newTab); + }, true) +} + +// Tests for per-window private browsing. +function openPBWindow(callback) { + let w = OpenBrowserWindow({private: true}); + w.addEventListener("load", function loadListener() { + w.removeEventListener("load", loadListener); + openTab(w, "http://example.com", function() { + callback(w); + }); + }); +} + +function postAndReceive(port, postTopic, receiveTopic, callback) { + port.onmessage = function(e) { + if (e.data.topic == receiveTopic) + callback(); + } + port.postMessage({topic: postTopic}); +} + +function test() { + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/social/moz.png" + }; + runSocialTestWithProvider(manifest, function (finishcb) { + openTab(window, "http://example.com", function(newTab) { + runSocialTests(tests, undefined, undefined, function() { + window.gBrowser.removeTab(newTab); + finishcb(); + }); + }); + }); +} + +var tests = { + testPrivateBrowsing: function(next) { + let port = Social.provider.getWorkerPort(); + ok(port, "provider has a port"); + postAndReceive(port, "test-init", "test-init-done", function() { + // social features should all be enabled in the existing window. + info("checking main window ui"); + ok(window.SocialUI.enabled, "social is enabled in normal window"); + checkSocialUI(window); + // open a new private-window + openPBWindow(function(pbwin) { + // The provider should remain alive. + postAndReceive(port, "ping", "pong", function() { + // the new window should have no social features at all. + info("checking private window ui"); + ok(!pbwin.SocialUI.enabled, "social is disabled in a PB window"); + checkSocialUI(pbwin); + // but they should all remain enabled in the initial window + info("checking main window ui"); + ok(window.SocialUI.enabled, "social is still enabled in normal window"); + checkSocialUI(window); + // that's all folks... + pbwin.close(); + next(); + }) + }); + }); + }, +} diff --git a/browser/base/content/test/social/browser_social_sidebar.js b/browser/base/content/test/social/browser_social_sidebar.js new file mode 100644 index 000000000..b9c471899 --- /dev/null +++ b/browser/base/content/test/social/browser_social_sidebar.js @@ -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/. */ + +function test() { + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" + }; + runSocialTestWithProvider(manifest, doTest); +} + +function doTest(finishcb) { + ok(SocialSidebar.canShow, "social sidebar should be able to be shown"); + ok(SocialSidebar.opened, "social sidebar should be open by default"); + + let command = document.getElementById("Social:ToggleSidebar"); + let sidebar = document.getElementById("social-sidebar-box"); + let browser = sidebar.firstChild; + + function checkShown(shouldBeShown) { + is(command.getAttribute("checked"), shouldBeShown ? "true" : "false", + "toggle command should be " + (shouldBeShown ? "checked" : "unchecked")); + is(sidebar.hidden, !shouldBeShown, + "sidebar should be " + (shouldBeShown ? "visible" : "hidden")); + // The sidebar.open pref only reflects the actual state of the sidebar + // when social is enabled. + if (Social.enabled) + is(Services.prefs.getBoolPref("social.sidebar.open"), shouldBeShown, + "sidebar open pref should be " + shouldBeShown); + if (shouldBeShown) { + is(browser.getAttribute('src'), Social.provider.sidebarURL, "sidebar url should be set"); + // We don't currently check docShellIsActive as this is only set + // after load event fires, and the tests below explicitly wait for this + // anyway. + } + else { + ok(!browser.docShellIsActive, "sidebar should have an inactive docshell"); + // sidebar will only be immediately unloaded (and thus set to + // about:blank) when canShow is false. + if (SocialSidebar.canShow) { + // should not have unloaded so will still be the provider URL. + is(browser.getAttribute('src'), Social.provider.sidebarURL, "sidebar url should be set"); + } else { + // should have been an immediate unload. + is(browser.getAttribute('src'), "about:blank", "sidebar url should be blank"); + } + } + } + + // First check the the sidebar is initially visible, and loaded + ok(!command.hidden, "toggle command should be visible"); + checkShown(true); + + browser.addEventListener("socialFrameHide", function sidebarhide() { + browser.removeEventListener("socialFrameHide", sidebarhide); + + checkShown(false); + + browser.addEventListener("socialFrameShow", function sidebarshow() { + browser.removeEventListener("socialFrameShow", sidebarshow); + + checkShown(true); + + // Set Social.enabled = false and check everything is as expected. + Social.enabled = false; + checkShown(false); + + Social.enabled = true; + checkShown(true); + + // And an edge-case - disable social and reset the provider. + Social.provider = null; + Social.enabled = false; + checkShown(false); + + // Finish the test + finishcb(); + }); + + // Toggle it back on + info("Toggling sidebar back on"); + Social.toggleSidebar(); + }); + + // use port messaging to ensure that the sidebar is both loaded and + // ready before we run other tests + let port = Social.provider.getWorkerPort(); + port.postMessage({topic: "test-init"}); + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "got-sidebar-message": + ok(true, "sidebar is loaded and ready"); + Social.toggleSidebar(); + } + }; +} + +// XXX test sidebar in popup diff --git a/browser/base/content/test/social/browser_social_toolbar.js b/browser/base/content/test/social/browser_social_toolbar.js new file mode 100644 index 000000000..2a648ade2 --- /dev/null +++ b/browser/base/content/test/social/browser_social_toolbar.js @@ -0,0 +1,195 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" +}; + +function test() { + waitForExplicitFinish(); + + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, undefined, finishcb); + }); +} + +var tests = { + testProfileNone: function(next, useNull) { + let profile = useNull ? null : {}; + Social.provider.updateUserProfile(profile); + // check dom values + let portrait = document.getElementsByClassName("social-statusarea-user-portrait")[0].getAttribute("src"); + // this is the default image for the profile area when not logged in. + ok(!portrait, "portrait is empty"); + let userDetailsBroadcaster = document.getElementById("socialBroadcaster_userDetails"); + let notLoggedInStatusValue = userDetailsBroadcaster.getAttribute("notLoggedInLabel"); + let userButton = document.getElementsByClassName("social-statusarea-loggedInStatus")[0]; + ok(!userButton.hidden, "username is visible"); + is(userButton.getAttribute("label"), notLoggedInStatusValue, "label reflects not being logged in"); + next(); + }, + testProfileNull: function(next) { + this.testProfileNone(next, true); + }, + testProfileSet: function(next) { + let statusIcon = document.getElementById("social-provider-button").style.listStyleImage; + is(statusIcon, "url(\"" + manifest.iconURL + "\")", "manifest iconURL is showing"); + let profile = { + portrait: "https://example.com/portrait.jpg", + userName: "trickster", + displayName: "Kuma Lisa", + profileURL: "http://example.com/Kuma_Lisa", + iconURL: "https://example.com/browser/browser/base/content/test/social/moz.png" + } + Social.provider.updateUserProfile(profile); + // check dom values + statusIcon = document.getElementById("social-provider-button").style.listStyleImage; + is(statusIcon, "url(\"" + profile.iconURL + "\")", "profile iconURL is showing"); + let portrait = document.getElementsByClassName("social-statusarea-user-portrait")[0].getAttribute("src"); + is(profile.portrait, portrait, "portrait is set"); + let userButton = document.getElementsByClassName("social-statusarea-loggedInStatus")[0]; + ok(!userButton.hidden, "username is visible"); + is(userButton.value, profile.userName, "username is set"); + next(); + }, + testNoAmbientNotificationsIsNoKeyboardMenu: function(next) { + // The menu bar isn't as easy to instrument on Mac. + if (navigator.platform.contains("Mac")) { + info("Skipping checking the menubar on Mac OS"); + next(); + return; + } + + // Test that keyboard accessible menuitem doesn't exist when no ambient icons specified. + let toolsPopup = document.getElementById("menu_ToolsPopup"); + toolsPopup.addEventListener("popupshown", function ontoolspopupshownNoAmbient() { + toolsPopup.removeEventListener("popupshown", ontoolspopupshownNoAmbient); + let socialToggleMore = document.getElementById("menu_socialAmbientMenu"); + ok(socialToggleMore, "Keyboard accessible social menu should exist"); + is(socialToggleMore.querySelectorAll("menuitem").length, 6, "The minimum number of menuitems is two when there are no ambient notifications."); + is(socialToggleMore.hidden, false, "Menu should be visible since we show some non-ambient notifications in the menu."); + toolsPopup.hidePopup(); + next(); + }, false); + document.getElementById("menu_ToolsPopup").openPopup(); + }, + testAmbientNotifications: function(next) { + let ambience = { + name: "testIcon", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png", + contentPanel: "about:blank", + counter: 42, + label: "Test Ambient 1 \u2046", + menuURL: "https://example.com/testAmbient1" + }; + let ambience2 = { + name: "testIcon2", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png", + contentPanel: "about:blank", + counter: 0, + label: "Test Ambient 2", + menuURL: "https://example.com/testAmbient2" + }; + let ambience3 = { + name: "testIcon3", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png", + contentPanel: "about:blank", + counter: 0, + label: "Test Ambient 3", + menuURL: "https://example.com/testAmbient3" + }; + let ambience4 = { + name: "testIcon4", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png", + contentPanel: "about:blank", + counter: 0, + label: "Test Ambient 4", + menuURL: "https://example.com/testAmbient4" + }; + Social.provider.setAmbientNotification(ambience); + + // for Bug 813834. Check preference whether stored data is correct. + is(JSON.parse(Services.prefs.getComplexValue("social.cached.ambientNotificationIcons", Ci.nsISupportsString).data).data.testIcon.label, "Test Ambient 1 \u2046", "label is stored into preference correctly"); + + Social.provider.setAmbientNotification(ambience2); + Social.provider.setAmbientNotification(ambience3); + + try { + Social.provider.setAmbientNotification(ambience4); + } catch(e) {} + let numIcons = Object.keys(Social.provider.ambientNotificationIcons).length; + ok(numIcons == 3, "prevent adding more than 3 ambient notification icons"); + + let statusIcon = document.getElementById("social-provider-button").nextSibling; + waitForCondition(function() { + statusIcon = document.getElementById("social-provider-button").nextSibling; + return !!statusIcon; + }, function () { + let badge = statusIcon.getAttribute("badge"); + is(badge, "42", "status value is correct"); + // If there is a counter, the aria-label should reflect it. + is(statusIcon.getAttribute("aria-label"), "Test Ambient 1 \u2046 (42)"); + + ambience.counter = 0; + Social.provider.setAmbientNotification(ambience); + badge = statusIcon.getAttribute("badge"); + is(badge, "", "status value is correct"); + // If there is no counter, the aria-label should be the same as the label + is(statusIcon.getAttribute("aria-label"), "Test Ambient 1 \u2046"); + + // The menu bar isn't as easy to instrument on Mac. + if (navigator.platform.contains("Mac")) { + next(); + return; + } + + // Test that keyboard accessible menuitem was added. + let toolsPopup = document.getElementById("menu_ToolsPopup"); + toolsPopup.addEventListener("popupshown", function ontoolspopupshownAmbient() { + toolsPopup.removeEventListener("popupshown", ontoolspopupshownAmbient); + let socialToggleMore = document.getElementById("menu_socialAmbientMenu"); + ok(socialToggleMore, "Keyboard accessible social menu should exist"); + is(socialToggleMore.querySelectorAll("menuitem").length, 9, "The number of menuitems is minimum plus three ambient notification menuitems."); + is(socialToggleMore.hidden, false, "Menu is visible when ambient notifications have label & menuURL"); + let menuitem = socialToggleMore.querySelector(".ambient-menuitem"); + is(menuitem.getAttribute("label"), "Test Ambient 1 \u2046", "Keyboard accessible ambient menuitem should have specified label"); + toolsPopup.hidePopup(); + next(); + }, false); + document.getElementById("menu_ToolsPopup").openPopup(); + }, "statusIcon was never found"); + }, + testProfileUnset: function(next) { + Social.provider.updateUserProfile({}); + // check dom values + let ambientIcons = document.querySelectorAll("#social-toolbar-item > box"); + for (let ambientIcon of ambientIcons) { + ok(ambientIcon.collapsed, "ambient icon (" + ambientIcon.id + ") is collapsed"); + } + + next(); + }, + testMenuitemsExist: function(next) { + let toggleSidebarMenuitems = document.getElementsByClassName("social-toggle-sidebar-menuitem"); + is(toggleSidebarMenuitems.length, 2, "Toggle Sidebar menuitems exist"); + let toggleDesktopNotificationsMenuitems = document.getElementsByClassName("social-toggle-notifications-menuitem"); + is(toggleDesktopNotificationsMenuitems.length, 2, "Toggle notifications menuitems exist"); + let toggleSocialMenuitems = document.getElementsByClassName("social-toggle-menuitem"); + is(toggleSocialMenuitems.length, 2, "Toggle Social menuitems exist"); + next(); + }, + testToggleNotifications: function(next) { + let enabled = Services.prefs.getBoolPref("social.toast-notifications.enabled"); + let cmd = document.getElementById("Social:ToggleNotifications"); + is(cmd.getAttribute("checked"), enabled ? "true" : "false"); + enabled = !enabled; + Services.prefs.setBoolPref("social.toast-notifications.enabled", enabled); + is(cmd.getAttribute("checked"), enabled ? "true" : "false"); + Services.prefs.clearUserPref("social.toast-notifications.enabled"); + next(); + }, +} diff --git a/browser/base/content/test/social/browser_social_window.js b/browser/base/content/test/social/browser_social_window.js new file mode 100644 index 000000000..f6a0afab0 --- /dev/null +++ b/browser/base/content/test/social/browser_social_window.js @@ -0,0 +1,145 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Test the top-level window UI for social. + +// This function should "reset" Social such that the next time Social.init() +// is called (eg, when a new window is opened), it re-performs all +// initialization. +function resetSocial() { + Social.initialized = false; + Social._provider = null; + Social.providers = []; + // *sob* - listeners keep getting added... + let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + SocialService._providerListeners.clear(); +} + +let createdWindows = []; + +function openWindowAndWaitForInit(callback) { + // this notification tells us SocialUI.init() has been run... + let topic = "browser-delayed-startup-finished"; + let w = OpenBrowserWindow(); + createdWindows.push(w); + Services.obs.addObserver(function providerSet(subject, topic, data) { + Services.obs.removeObserver(providerSet, topic); + info(topic + " observer was notified - continuing test"); + // executeSoon to let the browser UI observers run first + executeSoon(function() {callback(w)}); + }, topic, false); +} + +function postTestCleanup(cb) { + for (let w of createdWindows) + w.close(); + createdWindows = []; + Services.prefs.clearUserPref("social.enabled"); + cb(); +} + +let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js", + iconURL: "https://example.com/browser/browser/base/content/test/social/moz.png" +}; + +function test() { + waitForExplicitFinish(); + runSocialTests(tests, undefined, postTestCleanup); +} + +let tests = { + // check when social is totally disabled at startup (ie, no providers) + testInactiveStartup: function(cbnext) { + is(Social.providers.length, 0, "needs zero providers to start this test."); + resetSocial(); + openWindowAndWaitForInit(function(w1) { + checkSocialUI(w1); + // Now social is (re-)initialized, open a secondary window and check that. + openWindowAndWaitForInit(function(w2) { + checkSocialUI(w2); + checkSocialUI(w1); + cbnext(); + }); + }); + }, + + // Check when providers exist and social is turned on at startup. + testEnabledStartup: function(cbnext) { + runSocialTestWithProvider(manifest, function (finishcb) { + resetSocial(); + openWindowAndWaitForInit(function(w1) { + ok(Social.enabled, "social is enabled"); + checkSocialUI(w1); + // now init is complete, open a second window + openWindowAndWaitForInit(function(w2) { + checkSocialUI(w2); + checkSocialUI(w1); + // disable social and re-check + Services.prefs.setBoolPref("social.enabled", false); + executeSoon(function() { // let all the UI observers run... + ok(!Social.enabled, "social is disabled"); + checkSocialUI(w2); + checkSocialUI(w1); + finishcb(); + }); + }); + }); + }, cbnext); + }, + + // Check when providers exist but social is turned off at startup. + testDisabledStartup: function(cbnext) { + runSocialTestWithProvider(manifest, function (finishcb) { + Services.prefs.setBoolPref("social.enabled", false); + resetSocial(); + openWindowAndWaitForInit(function(w1) { + ok(!Social.enabled, "social is disabled"); + checkSocialUI(w1); + // now init is complete, open a second window + openWindowAndWaitForInit(function(w2) { + checkSocialUI(w2); + checkSocialUI(w1); + // enable social and re-check + Services.prefs.setBoolPref("social.enabled", true); + executeSoon(function() { // let all the UI observers run... + ok(Social.enabled, "social is enabled"); + checkSocialUI(w2); + checkSocialUI(w1); + finishcb(); + }); + }); + }); + }, cbnext); + }, + + // Check when the last provider is removed. + testRemoveProvider: function(cbnext) { + runSocialTestWithProvider(manifest, function (finishcb) { + openWindowAndWaitForInit(function(w1) { + checkSocialUI(w1); + // now init is complete, open a second window + openWindowAndWaitForInit(function(w2) { + checkSocialUI(w2); + // remove the current provider. + let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + SocialService.removeProvider(manifest.origin, function() { + ok(!Social.enabled, "social is disabled"); + is(Social.providers.length, 0, "no providers"); + checkSocialUI(w2); + checkSocialUI(w1); + // *sob* - runSocialTestWithProvider's cleanup fails when it can't + // remove the provider, so re-add it. + SocialService.addProvider(manifest, function() { + finishcb(); + }); + }); + }); + }); + }, cbnext); + }, +} diff --git a/browser/base/content/test/social/head.js b/browser/base/content/test/social/head.js new file mode 100644 index 000000000..1e5d8412c --- /dev/null +++ b/browser/base/content/test/social/head.js @@ -0,0 +1,517 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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, "Promise", + "resource://gre/modules/commonjs/sdk/core/promise.js"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 30) { + ok(false, errorMsg); + moveOn(); + } + if (condition()) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} + +// Check that a specified (string) URL hasn't been "remembered" (ie, is not +// in history, will not appear in about:newtab or auto-complete, etc.) +function promiseSocialUrlNotRemembered(url) { + let deferred = Promise.defer(); + let uri = Services.io.newURI(url, null, null); + PlacesUtils.asyncHistory.isURIVisited(uri, function(aURI, aIsVisited) { + ok(!aIsVisited, "social URL " + url + " should not be in global history"); + deferred.resolve(); + }); + return deferred.promise; +} + +let gURLsNotRemembered = []; + + +function checkProviderPrefsEmpty(isError) { + let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest."); + let prefs = MANIFEST_PREFS.getChildList("", []); + let c = 0; + for (let pref of prefs) { + if (MANIFEST_PREFS.prefHasUserValue(pref)) { + info("provider [" + pref + "] manifest left installed from previous test"); + c++; + } + } + is(c, 0, "all provider prefs uninstalled from previous test"); + is(Social.providers.length, 0, "all providers uninstalled from previous test " + Social.providers.length); +} + +function defaultFinishChecks() { + checkProviderPrefsEmpty(true); + finish(); +} + +function runSocialTestWithProvider(manifest, callback, finishcallback) { + let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + + let manifests = Array.isArray(manifest) ? manifest : [manifest]; + + // Check that none of the provider's content ends up in history. + function finishCleanUp() { + for (let i = 0; i < manifests.length; i++) { + let m = manifests[i]; + for (let what of ['sidebarURL', 'workerURL', 'iconURL']) { + if (m[what]) { + yield promiseSocialUrlNotRemembered(m[what]); + } + }; + } + for (let i = 0; i < gURLsNotRemembered.length; i++) { + yield promiseSocialUrlNotRemembered(gURLsNotRemembered[i]); + } + gURLsNotRemembered = []; + } + + info("runSocialTestWithProvider: " + manifests.toSource()); + + let finishCount = 0; + function finishIfDone(callFinish) { + finishCount++; + if (finishCount == manifests.length) + Task.spawn(finishCleanUp).then(finishcallback || defaultFinishChecks); + } + function removeAddedProviders(cleanup) { + manifests.forEach(function (m) { + // If we're "cleaning up", don't call finish when done. + let callback = cleanup ? function () {} : finishIfDone; + // Similarly, if we're cleaning up, catch exceptions from removeProvider + let removeProvider = SocialService.removeProvider.bind(SocialService); + if (cleanup) { + removeProvider = function (origin, cb) { + try { + SocialService.removeProvider(origin, cb); + } catch (ex) { + // Ignore "provider doesn't exist" errors. + if (ex.message.indexOf("SocialService.removeProvider: no provider with origin") == 0) + return; + info("Failed to clean up provider " + origin + ": " + ex); + } + } + } + removeProvider(m.origin, callback); + }); + } + function finishSocialTest(cleanup) { + // disable social before removing the providers to avoid providers + // being activated immediately before we get around to removing it. + Services.prefs.clearUserPref("social.enabled"); + removeAddedProviders(cleanup); + } + + let providersAdded = 0; + let firstProvider; + + manifests.forEach(function (m) { + SocialService.addProvider(m, function(provider) { + + providersAdded++; + info("runSocialTestWithProvider: provider added"); + + // we want to set the first specified provider as the UI's provider + if (provider.origin == manifests[0].origin) { + firstProvider = provider; + } + + // If we've added all the providers we need, call the callback to start + // the tests (and give it a callback it can call to finish them) + if (providersAdded == manifests.length) { + // Set the UI's provider (which enables the feature) + Social.provider = firstProvider; + + registerCleanupFunction(function () { + finishSocialTest(true); + }); + callback(finishSocialTest); + } + }); + }); +} + +function runSocialTests(tests, cbPreTest, cbPostTest, cbFinish) { + let testIter = Iterator(tests); + let providersAtStart = Social.providers.length; + info("runSocialTests: start test run with " + providersAtStart + " providers"); + + if (cbPreTest === undefined) { + cbPreTest = function(cb) {cb()}; + } + if (cbPostTest === undefined) { + cbPostTest = function(cb) {cb()}; + } + + function runNextTest() { + let name, func; + try { + [name, func] = testIter.next(); + } catch (err if err instanceof StopIteration) { + // out of items: + (cbFinish || defaultFinishChecks)(); + is(providersAtStart, Social.providers.length, + "runSocialTests: finish test run with " + Social.providers.length + " providers"); + return; + } + // We run on a timeout as the frameworker also makes use of timeouts, so + // this helps keep the debug messages sane. + executeSoon(function() { + function cleanupAndRunNextTest() { + info("sub-test " + name + " complete"); + cbPostTest(runNextTest); + } + cbPreTest(function() { + is(providersAtStart, Social.providers.length, "pre-test: no new providers left enabled"); + info("sub-test " + name + " starting"); + try { + func.call(tests, cleanupAndRunNextTest); + } catch (ex) { + ok(false, "sub-test " + name + " failed: " + ex.toString() +"\n"+ex.stack); + cleanupAndRunNextTest(); + } + }) + }); + } + runNextTest(); +} + +// A fairly large hammer which checks all aspects of the SocialUI for +// internal consistency. +function checkSocialUI(win) { + win = win || window; + let doc = win.document; + let provider = Social.provider; + let enabled = win.SocialUI.enabled; + let active = Social.providers.length > 0 && !win.SocialUI._chromeless && + !PrivateBrowsingUtils.isWindowPrivate(win); + + function isbool(a, b, msg) { + is(!!a, !!b, msg); + } + isbool(win.SocialSidebar.canShow, enabled, "social sidebar active?"); + if (enabled) + isbool(win.SocialSidebar.opened, enabled, "social sidebar open?"); + isbool(win.SocialChatBar.isAvailable, enabled && Social.haveLoggedInUser(), "chatbar available?"); + isbool(!win.SocialChatBar.chatbar.hidden, enabled && Social.haveLoggedInUser(), "chatbar visible?"); + + let markVisible = enabled && provider.pageMarkInfo; + let canMark = markVisible && win.SocialMark.canMarkPage(win.gBrowser.currentURI); + isbool(!win.SocialMark.button.hidden, markVisible, "SocialMark button visible?"); + isbool(!win.SocialMark.button.disabled, canMark, "SocialMark button enabled?"); + isbool(!doc.getElementById("social-toolbar-item").hidden, active, "toolbar items visible?"); + if (active) { + if (!enabled) { + ok(!win.SocialToolbar.button.style.listStyleImage, "toolbar button is default icon"); + } else { + is(win.SocialToolbar.button.style.listStyleImage, 'url("' + Social.defaultProvider.iconURL + '")', "toolbar button has provider icon"); + } + } + // the menus should always have the provider name + if (provider) { + for (let id of ["menu_socialSidebar", "menu_socialAmbientMenu"]) + is(document.getElementById(id).getAttribute("label"), Social.provider.name, "element has the provider name"); + } + + // and for good measure, check all the social commands. + isbool(!doc.getElementById("Social:Toggle").hidden, active, "Social:Toggle visible?"); + isbool(!doc.getElementById("Social:ToggleNotifications").hidden, enabled, "Social:ToggleNotifications visible?"); + isbool(!doc.getElementById("Social:FocusChat").hidden, enabled && Social.haveLoggedInUser(), "Social:FocusChat visible?"); + isbool(doc.getElementById("Social:FocusChat").getAttribute("disabled"), enabled ? "false" : "true", "Social:FocusChat disabled?"); + is(doc.getElementById("Social:TogglePageMark").getAttribute("disabled"), canMark ? "false" : "true", "Social:TogglePageMark enabled?"); + + // broadcasters. + isbool(!doc.getElementById("socialActiveBroadcaster").hidden, active, "socialActiveBroadcaster hidden?"); +} + +// blocklist testing +function updateBlocklist(aCallback) { + var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"] + .getService(Ci.nsITimerCallback); + var observer = function() { + Services.obs.removeObserver(observer, "blocklist-updated"); + if (aCallback) + executeSoon(aCallback); + }; + Services.obs.addObserver(observer, "blocklist-updated", false); + blocklistNotifier.notify(null); +} + +function setAndUpdateBlocklist(aURL, aCallback) { + Services.prefs.setCharPref("extensions.blocklist.url", aURL); + updateBlocklist(aCallback); +} + +function resetBlocklist(aCallback) { + Services.prefs.clearUserPref("extensions.blocklist.url"); + updateBlocklist(aCallback); +} + +function setManifestPref(name, manifest) { + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(manifest); + Services.prefs.setComplexValue(name, Ci.nsISupportsString, string); +} + +function getManifestPrefname(aManifest) { + // is same as the generated name in SocialServiceInternal.getManifestPrefname + let originUri = Services.io.newURI(aManifest.origin, null, null); + return "social.manifest." + originUri.hostPort.replace('.','-'); +} + +function setBuiltinManifestPref(name, manifest) { + // we set this as a default pref, it must not be a user pref + manifest.builtin = true; + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(manifest); + Services.prefs.getDefaultBranch(null).setComplexValue(name, Ci.nsISupportsString, string); + // verify this is set on the default branch + let stored = Services.prefs.getComplexValue(name, Ci.nsISupportsString).data; + is(stored, string.data, "manifest '"+name+"' stored in default prefs"); + // don't dirty our manifest, we'll need it without this flag later + delete manifest.builtin; + // verify we DO NOT have a user-level pref + ok(!Services.prefs.prefHasUserValue(name), "manifest '"+name+"' is not in user-prefs"); +} + +function resetBuiltinManifestPref(name) { + Services.prefs.getDefaultBranch(null).deleteBranch(name); + is(Services.prefs.getDefaultBranch(null).getPrefType(name), + Services.prefs.PREF_INVALID, "default manifest removed"); +} + +function addTab(url, callback) { + let tab = gBrowser.selectedTab = gBrowser.addTab(url, {skipAnimation: true}); + tab.linkedBrowser.addEventListener("load", function tabLoad(event) { + tab.linkedBrowser.removeEventListener("load", tabLoad, true); + executeSoon(function() {callback(tab)}); + }, true); +} + +function selectBrowserTab(tab, callback) { + if (gBrowser.selectedTab == tab) { + executeSoon(function() {callback(tab)}); + return; + } + gBrowser.tabContainer.addEventListener("TabSelect", function onTabSelect() { + gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect, false); + is(gBrowser.selectedTab, tab, "browser tab is selected"); + executeSoon(function() {callback(tab)}); + }); + gBrowser.selectedTab = tab; +} + +function loadIntoTab(tab, url, callback) { + tab.linkedBrowser.addEventListener("load", function tabLoad(event) { + tab.linkedBrowser.removeEventListener("load", tabLoad, true); + executeSoon(function() {callback(tab)}); + }, true); + tab.linkedBrowser.loadURI(url); +} + + +// chat test help functions + +// And lots of helpers for the resize tests. +function get3ChatsForCollapsing(mode, cb) { + // We make one chat, then measure its size. We then resize the browser to + // ensure a second can be created fully visible but a third can not - then + // create the other 2. first will will be collapsed, second fully visible + // and the third also visible and the "selected" one. + // To make our life easier we don't go via the worker and ports so we get + // more control over creation *and* to make the code much simpler. We + // assume the worker/port stuff is individually tested above. + let chatbar = window.SocialChatBar.chatbar; + let chatWidth = undefined; + let num = 0; + is(chatbar.childNodes.length, 0, "chatbar starting empty"); + is(chatbar.menupopup.childNodes.length, 0, "popup starting empty"); + + makeChat(mode, "first chat", function() { + // got the first one. + checkPopup(); + ok(chatbar.menupopup.parentNode.collapsed, "menu selection isn't visible"); + // we kinda cheat here and get the width of the first chat, assuming + // that all future chats will have the same width when open. + chatWidth = chatbar.calcTotalWidthOf(chatbar.selectedChat); + let desired = chatWidth * 2.5; + resizeWindowToChatAreaWidth(desired, function(sizedOk) { + ok(sizedOk, "can't do any tests without this width"); + checkPopup(); + makeChat(mode, "second chat", function() { + is(chatbar.childNodes.length, 2, "now have 2 chats"); + checkPopup(); + // and create the third. + makeChat(mode, "third chat", function() { + is(chatbar.childNodes.length, 3, "now have 3 chats"); + checkPopup(); + // XXX - this is a hacky implementation detail around the order of + // the chats. Ideally things would be a little more sane wrt the + // other in which the children were created. + let second = chatbar.childNodes[2]; + let first = chatbar.childNodes[1]; + let third = chatbar.childNodes[0]; + ok(first.collapsed && !second.collapsed && !third.collapsed, "collapsed state as promised"); + is(chatbar.selectedChat, third, "third is selected as promised") + info("have 3 chats for collapse testing - starting actual test..."); + cb(first, second, third); + }, mode); + }, mode); + }); + }, mode); +} + +function makeChat(mode, uniqueid, cb) { + info("making a chat window '" + uniqueid +"'"); + const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html"; + let provider = Social.provider; + let isOpened = window.SocialChatBar.openChat(provider, chatUrl + "?id=" + uniqueid, function(chat) { + info("chat window has opened"); + // we can't callback immediately or we might close the chat during + // this event which upsets the implementation - it is only 1/2 way through + // handling the load event. + chat.document.title = uniqueid; + executeSoon(cb); + }, mode); + if (!isOpened) { + ok(false, "unable to open chat window, no provider? more failures to come"); + executeSoon(cb); + } +} + +function checkPopup() { + // popup only showing if any collapsed popup children. + let chatbar = window.SocialChatBar.chatbar; + let numCollapsed = 0; + for (let chat of chatbar.childNodes) { + if (chat.collapsed) { + numCollapsed += 1; + // and it have a menuitem weakmap + is(chatbar.menuitemMap.get(chat).nodeName, "menuitem", "collapsed chat has a menu item"); + } else { + ok(!chatbar.menuitemMap.has(chat), "open chat has no menu item"); + } + } + is(chatbar.menupopup.parentNode.collapsed, numCollapsed == 0, "popup matches child collapsed state"); + is(chatbar.menupopup.childNodes.length, numCollapsed, "popup has correct count of children"); + // todo - check each individual elt is what we expect? +} +// Resize the main window so the chat area's boxObject is |desired| wide. +// Does a callback passing |true| if the window is now big enough or false +// if we couldn't resize large enough to satisfy the test requirement. +function resizeWindowToChatAreaWidth(desired, cb, count = 0) { + let current = window.SocialChatBar.chatbar.getBoundingClientRect().width; + let delta = desired - current; + info(count + ": resizing window so chat area is " + desired + " wide, currently it is " + + current + ". Screen avail is " + window.screen.availWidth + + ", current outer width is " + window.outerWidth); + + // WTF? Sometimes we will get fractional values due to the - err - magic + // of DevPointsPerCSSPixel etc, so we allow a couple of pixels difference. + let widthDeltaCloseEnough = function(d) { + return Math.abs(d) < 2; + } + + // attempting to resize by (0,0), unsurprisingly, doesn't cause a resize + // event - so just callback saying all is well. + if (widthDeltaCloseEnough(delta)) { + info(count + ": skipping this as screen width is close enough"); + executeSoon(function() { + cb(true); + }); + return; + } + // On lo-res screens we may already be maxed out but still smaller than the + // requested size, so asking to resize up also will not cause a resize event. + // So just callback now saying the test must be skipped. + if (window.screen.availWidth - window.outerWidth < delta) { + info(count + ": skipping this as screen available width is less than necessary"); + executeSoon(function() { + cb(false); + }); + return; + } + function resize_handler(event) { + // for whatever reason, sometimes we get called twice for different event + // phases, only handle one of them. + if (event.eventPhase != event.AT_TARGET) + return; + // we did resize - but did we get far enough to be able to continue? + let newSize = window.SocialChatBar.chatbar.getBoundingClientRect().width; + let sizedOk = widthDeltaCloseEnough(newSize - desired); + if (!sizedOk) + return; + window.removeEventListener("resize", resize_handler); + info(count + ": resized window width is " + newSize); + executeSoon(function() { + cb(sizedOk); + }); + } + // Otherwise we request resize and expect a resize event + window.addEventListener("resize", resize_handler); + window.resizeBy(delta, 0); +} + +function resizeAndCheckWidths(first, second, third, checks, cb) { + if (checks.length == 0) { + cb(); // nothing more to check! + return; + } + let count = checks.length; + let [width, numExpectedVisible, why] = checks.shift(); + info("<< Check " + count + ": " + why); + info(count + ": " + "resizing window to " + width + ", expect " + numExpectedVisible + " visible items"); + resizeWindowToChatAreaWidth(width, function(sizedOk) { + checkPopup(); + ok(sizedOk, count+": window resized correctly"); + function collapsedObserver(r, m) { + if ([first, second, third].filter(function(item) !item.collapsed).length == numExpectedVisible) { + if (m) { + m.disconnect(); + } + ok(true, count + ": " + "correct number of chats visible"); + info(">> Check " + count); + resizeAndCheckWidths(first, second, third, checks, cb); + return true; + } + return false; + } + if (!collapsedObserver()) { + let m = new MutationObserver(collapsedObserver); + m.observe(first, {attributes: true }); + m.observe(second, {attributes: true }); + m.observe(third, {attributes: true }); + } + }, count); +} + +function getPopupWidth() { + let popup = window.SocialChatBar.chatbar.menupopup; + ok(!popup.parentNode.collapsed, "asking for popup width when it is visible"); + let cs = document.defaultView.getComputedStyle(popup.parentNode); + let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight); + return popup.parentNode.getBoundingClientRect().width + margins; +} + +function closeAllChats() { + let chatbar = window.SocialChatBar.chatbar; + chatbar.removeAll(); +} + diff --git a/browser/base/content/test/social/moz.build b/browser/base/content/test/social/moz.build new file mode 100644 index 000000000..83fd82cd8 --- /dev/null +++ b/browser/base/content/test/social/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += ['opengraph'] diff --git a/browser/base/content/test/social/moz.png b/browser/base/content/test/social/moz.png Binary files differnew file mode 100644 index 000000000..769c63634 --- /dev/null +++ b/browser/base/content/test/social/moz.png diff --git a/browser/base/content/test/social/opengraph/Makefile.in b/browser/base/content/test/social/opengraph/Makefile.in new file mode 100644 index 000000000..d0d5a1890 --- /dev/null +++ b/browser/base/content/test/social/opengraph/Makefile.in @@ -0,0 +1,21 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = @DEPTH@ +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = @relativesrcdir@ + +include $(DEPTH)/config/autoconf.mk + +MOCHITEST_BROWSER_FILES := \ + opengraph.html \ + og_invalid_url.html \ + shortlink_linkrel.html \ + shorturl_link.html \ + shorturl_linkrel.html \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/browser/base/content/test/social/opengraph/moz.build b/browser/base/content/test/social/opengraph/moz.build new file mode 100644 index 000000000..89251dc39 --- /dev/null +++ b/browser/base/content/test/social/opengraph/moz.build @@ -0,0 +1,4 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. diff --git a/browser/base/content/test/social/opengraph/og_invalid_url.html b/browser/base/content/test/social/opengraph/og_invalid_url.html new file mode 100644 index 000000000..ad1dae2be --- /dev/null +++ b/browser/base/content/test/social/opengraph/og_invalid_url.html @@ -0,0 +1,11 @@ +<html xmlns:og="http://ogp.me/ns#"> +<head> + <meta property="og:url" content="chrome://browser/content/aboutDialog.xul"/> + <meta property="og:site_name" content="Evil chrome delivering website"/> + <meta property="og:description" + content="A test corpus file for open graph tags passing a bad url"/> +</head> +<body> + Open Graph Test Page +</body> +</html> diff --git a/browser/base/content/test/social/opengraph/opengraph.html b/browser/base/content/test/social/opengraph/opengraph.html new file mode 100644 index 000000000..50b7703b8 --- /dev/null +++ b/browser/base/content/test/social/opengraph/opengraph.html @@ -0,0 +1,13 @@ +<html xmlns:og="http://ogp.me/ns#"> +<head> + <meta property="og:title" content=">This is my title<"/> + <meta property="og:url" content="https://www.mozilla.org"/> + <meta property="og:image" content="https://www.mozilla.org/favicon.png"/> + <meta property="og:site_name" content=">My simple test page<"/> + <meta property="og:description" + content="A test corpus file for open graph tags we care about"/> +</head> +<body> + Open Graph Test Page +</body> +</html> diff --git a/browser/base/content/test/social/opengraph/shortlink_linkrel.html b/browser/base/content/test/social/opengraph/shortlink_linkrel.html new file mode 100644 index 000000000..54c40c376 --- /dev/null +++ b/browser/base/content/test/social/opengraph/shortlink_linkrel.html @@ -0,0 +1,10 @@ +<html> +<head> + <link rel="image_src" href="http://example.com/1234/56789.jpg" id="image-src" /> + <link id="canonicalurl" rel="canonical" href="http://www.example.com/photos/56789/" /> + <link rel="shortlink" href="http://imshort/p/abcde" /> +</head> +<body> + link[rel='shortlink'] +</body> +</html> diff --git a/browser/base/content/test/social/opengraph/shorturl_link.html b/browser/base/content/test/social/opengraph/shorturl_link.html new file mode 100644 index 000000000..667122cea --- /dev/null +++ b/browser/base/content/test/social/opengraph/shorturl_link.html @@ -0,0 +1,10 @@ +<html> +<head> + <link rel="image_src" href="http://example.com/1234/56789.jpg" id="image-src" /> + <link id="canonicalurl" rel="canonical" href="http://www.example.com/photos/56789/" /> + <link id="shorturl" rev="canonical" type="text/html" href="http://imshort/p/abcde" /> +</head> +<body> + link id="shorturl" +</body> +</html> diff --git a/browser/base/content/test/social/opengraph/shorturl_linkrel.html b/browser/base/content/test/social/opengraph/shorturl_linkrel.html new file mode 100644 index 000000000..36533528e --- /dev/null +++ b/browser/base/content/test/social/opengraph/shorturl_linkrel.html @@ -0,0 +1,25 @@ +<html> +<head> + <title>Test Image</title> + + <meta name="description" content="Iron man in a tutu" /> + <meta name="title" content="Test Image" /> + + <meta name="medium" content="image" /> + <link rel="image_src" href="http://example.com/1234/56789.jpg" id="image-src" /> + <link id="canonicalurl" rel="canonical" href="http://www.example.com/photos/56789/" /> + <link id="shorturl" href="http://imshort/p/abcde" /> + + <meta property="og:title" content="TestImage" /> + <meta property="og:type" content="photos:photo" /> + <meta property="og:url" content="http://www.example.com/photos/56789/" /> + <meta property="og:site_name" content="My Photo Site" /> + <meta property="og:description" content="Iron man in a tutu" /> + <meta property="og:image" content="http://example.com/1234/56789.jpg" /> + <meta property="og:image:width" content="480" /> + <meta property="og:image:height" content="640" /> +</head> +<body> + link[rel='shorturl'] +</body> +</html> diff --git a/browser/base/content/test/social/share.html b/browser/base/content/test/social/share.html new file mode 100644 index 000000000..6a4dc49b6 --- /dev/null +++ b/browser/base/content/test/social/share.html @@ -0,0 +1,18 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + var shareData; + addEventListener("OpenGraphData", function(e) { + shareData = JSON.parse(e.detail); + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "share-data-message", result: shareData}); + // share windows self-close + window.close(); + }) + </script> + </head> + <body> + <p>This is a test social share window.</p> + </body> +</html> diff --git a/browser/base/content/test/social/social_activate.html b/browser/base/content/test/social/social_activate.html new file mode 100644 index 000000000..b008aad33 --- /dev/null +++ b/browser/base/content/test/social/social_activate.html @@ -0,0 +1,45 @@ +<html> +<head> + <title>Activation test</title> +</head> +<script> +// icons from http://findicons.com/icon/158311/firefox?id=356182 by ipapun +var data = { + // currently required + "name": "Demo Social Service", + "iconURL": "chrome://branding/content/icon16.png", + "icon32URL": "chrome://branding/content/favicon32.png", + "icon64URL": "chrome://branding/content/icon64.png", + + // at least one of these must be defined + "sidebarURL": "/browser/browser/base/content/test/social/social_sidebar.html", + "workerURL": "/browser/browser/base/content/test/social/social_worker.js", + + // should be available for display purposes + "description": "A short paragraph about this provider", + "author": "Shane Caraveo, Mozilla", + + // optional + "version": 1 +} + +function activate(node) { + node.setAttribute("data-service", JSON.stringify(data)); + var event = new CustomEvent("ActivateSocialFeature"); + node.dispatchEvent(event); +} + +function oldActivate(node) { + var event = new CustomEvent("ActivateSocialFeature"); + document.dispatchEvent(event); +} +</script> +<body> + +nothing to see here + +<button id="activation-old" onclick="oldActivate(this)">Activate The Demo Provider</button> +<button id="activation" onclick="activate(this)">Activate The Demo Provider</button> + +</body> +</html> diff --git a/browser/base/content/test/social/social_activate_iframe.html b/browser/base/content/test/social/social_activate_iframe.html new file mode 100644 index 000000000..f8462ec80 --- /dev/null +++ b/browser/base/content/test/social/social_activate_iframe.html @@ -0,0 +1,11 @@ +<html> +<head> + <title>Activation iframe test</title> +</head> + +<body> + +<iframe src="social_activate.html"/> + +</body> +</html> diff --git a/browser/base/content/test/social/social_chat.html b/browser/base/content/test/social/social_chat.html new file mode 100644 index 000000000..ba507592e --- /dev/null +++ b/browser/base/content/test/social/social_chat.html @@ -0,0 +1,32 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + function pingWorker() { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "chatbox-message", result: "ok"}); + } + window.addEventListener("socialFrameShow", function(e) { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "chatbox-visibility", result: "shown"}); + }, false); + window.addEventListener("socialFrameHide", function(e) { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "chatbox-visibility", result: "hidden"}); + }, false); + window.addEventListener("socialTest-CloseSelf", function(e) { + window.close(); + }, false); + </script> + <title>test chat window</title> + </head> + <body onload="pingWorker();"> + <p>This is a test social chat window.</p> + <!-- a couple of input fields to help with focus testing --> + <input id="input1"/> + <input id="input2"/> + + <!-- an iframe here so this one page generates multiple load events --> + <iframe id="iframe" src="data:text/plain:this is an iframe"></iframe> + </body> +</html> diff --git a/browser/base/content/test/social/social_flyout.html b/browser/base/content/test/social/social_flyout.html new file mode 100644 index 000000000..ff426783a --- /dev/null +++ b/browser/base/content/test/social/social_flyout.html @@ -0,0 +1,37 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + function pingWorker() { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "flyout-message", result: "ok"}); + } + window.addEventListener("socialFrameShow", function(e) { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "flyout-visibility", result: "shown"}); + }, false); + window.addEventListener("socialFrameHide", function(e) { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "flyout-visibility", result: "hidden"}); + }, false); + window.addEventListener("socialTest-MakeWider", function(e) { + document.body.setAttribute("style", "width: 500px; height: 500px; margin: 0; overflow: hidden;"); + document.body.offsetWidth; // force a layout flush + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent("SocialTest-DoneMakeWider", true, true, {}); + document.documentElement.dispatchEvent(evt); + }, false); + window.addEventListener("socialTest-CloseSelf", function(e) { + window.close(); + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent("SocialTest-DoneCloseSelf", true, true, {}); + document.documentElement.dispatchEvent(evt); + }, false); + </script> + </head> + <body style="width: 400px; height: 400px; margin: 0; overflow: hidden;" onload="pingWorker();"> + <p>This is a test social flyout panel.</p> + <a id="traversal" href="http://mochi.test">test link</a> + </body> +</html> + diff --git a/browser/base/content/test/social/social_mark_image.png b/browser/base/content/test/social/social_mark_image.png Binary files differnew file mode 100644 index 000000000..fa1f8fb0e --- /dev/null +++ b/browser/base/content/test/social/social_mark_image.png diff --git a/browser/base/content/test/social/social_panel.html b/browser/base/content/test/social/social_panel.html new file mode 100644 index 000000000..ada53d9ed --- /dev/null +++ b/browser/base/content/test/social/social_panel.html @@ -0,0 +1,24 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + function pingWorker() { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "panel-message", + result: "ok", + location: window.location.href}); + } + window.addEventListener("socialFrameShow", function(e) { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "status-panel-visibility", result: "shown"}); + }, false); + window.addEventListener("socialFrameHide", function(e) { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "status-panel-visibility", result: "hidden"}); + }, false); + </script> + </head> + <body onload="pingWorker();"> + <p>This is a test social panel.</p> + </body> +</html> diff --git a/browser/base/content/test/social/social_sidebar.html b/browser/base/content/test/social/social_sidebar.html new file mode 100644 index 000000000..dc66b5d31 --- /dev/null +++ b/browser/base/content/test/social/social_sidebar.html @@ -0,0 +1,47 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + var testwindow; + function pingWorker() { + var port = navigator.mozSocial.getWorker().port; + port.onmessage = function(e) { + var topic = e.data.topic; + switch (topic) { + case "test-flyout-open": + navigator.mozSocial.openPanel("social_flyout.html"); + break; + case "test-flyout-close": + navigator.mozSocial.closePanel(); + break; + case "test-chatbox-open": + var url = "social_chat.html"; + var data = e.data.data; + if (data && data.id) { + url = url + "?id="+data.id; + } + navigator.mozSocial.openChatWindow(url, function(chatwin) { + // Note that the following .focus() call should *not* arrange + // to steal focus - see browser_social_chatwindowfocus.js + if (data && data.stealFocus && chatwin) { + chatwin.focus(); + } + port.postMessage({topic: "chatbox-opened", + result: chatwin ? "ok" : "failed"}); + }); + break; + case "test-isVisible": + port.postMessage({topic: "test-isVisible-response", + result: navigator.mozSocial.isVisible}); + break; + } + } + port.postMessage({topic: "sidebar-message", result: "ok"}); + } + </script> + </head> + <body onload="pingWorker();"> + <p>This is a test social sidebar.</p> + <button id="chat-opener" onclick="navigator.mozSocial.openChatWindow('./social_chat.html');"/> + </body> +</html> diff --git a/browser/base/content/test/social/social_window.html b/browser/base/content/test/social/social_window.html new file mode 100644 index 000000000..1e95ca0ee --- /dev/null +++ b/browser/base/content/test/social/social_window.html @@ -0,0 +1,17 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + function pingWorker() { + var port = navigator.mozSocial.getWorker().port; + port.postMessage({topic: "service-window-message", + location: window.location.href, + result: "ok" + }); + } + </script> + </head> + <body onload="pingWorker();"> + <p>This is a test social service window.</p> + </body> +</html> diff --git a/browser/base/content/test/social/social_worker.js b/browser/base/content/test/social/social_worker.js new file mode 100644 index 000000000..1c1fd43e2 --- /dev/null +++ b/browser/base/content/test/social/social_worker.js @@ -0,0 +1,149 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 testPort, sidebarPort, apiPort; + +onconnect = function(e) { + let port = e.ports[0]; + port.onmessage = function onMessage(event) { + let topic = event.data.topic; + switch (topic) { + case "test-init": + testPort = port; + port.postMessage({topic: "test-init-done"}); + break; + case "ping": + port.postMessage({topic: "pong"}); + break; + case "test-logout": + apiPort.postMessage({topic: "social.user-profile", data: {}}); + break; + case "sidebar-message": + sidebarPort = port; + if (testPort && event.data.result == "ok") + testPort.postMessage({topic:"got-sidebar-message"}); + break; + case "service-window-message": + testPort.postMessage({topic:"got-service-window-message", + location: event.data.location}); + break; + case "service-window-closed-message": + testPort.postMessage({topic:"got-service-window-closed-message"}); + break; + case "test-service-window": + sidebarPort.postMessage({topic:"test-service-window"}); + break; + case "test-service-window-twice": + sidebarPort.postMessage({topic:"test-service-window-twice"}); + break; + case "test-service-window-twice-result": + testPort.postMessage({topic: "test-service-window-twice-result", result: event.data.result }) + break; + case "test-close-service-window": + sidebarPort.postMessage({topic:"test-close-service-window"}); + break; + case "panel-message": + if (testPort && event.data.result == "ok") + testPort.postMessage({topic:"got-panel-message", + location: event.data.location + }); + break; + case "status-panel-visibility": + testPort.postMessage({topic:"got-social-panel-visibility", result: event.data.result }); + break; + case "test-chatbox-open": + sidebarPort.postMessage(event.data); + break; + case "chatbox-opened": + testPort.postMessage(event.data); + break; + case "chatbox-message": + testPort.postMessage({topic:"got-chatbox-message", result: event.data.result}); + break; + case "chatbox-visibility": + testPort.postMessage({topic:"got-chatbox-visibility", result: event.data.result}); + break; + case "test-flyout-open": + sidebarPort.postMessage({topic:"test-flyout-open"}); + break; + case "flyout-message": + testPort.postMessage({topic:"got-flyout-message", result: event.data.result}); + break; + case "flyout-visibility": + testPort.postMessage({topic:"got-flyout-visibility", result: event.data.result}); + break; + case "test-flyout-close": + sidebarPort.postMessage({topic:"test-flyout-close"}); + break; + case "test-worker-chat": + apiPort.postMessage({topic: "social.request-chat", data: event.data.data }); + break; + case "social.initialize": + // This is the workerAPI port, respond and set up a notification icon. + // For multiprovider tests, we support acting like different providers + // based on the domain we load from. + apiPort = port; + let profile; + if (location.href.indexOf("https://test1.example.com") == 0) { + profile = { + portrait: "https://test1.example.com/portrait.jpg", + userName: "tester", + displayName: "Test1 User", + }; + } else { + profile = { + portrait: "https://example.com/portrait.jpg", + userName: "trickster", + displayName: "Kuma Lisa", + profileURL: "http://en.wikipedia.org/wiki/Kuma_Lisa" + }; + } + port.postMessage({topic: "social.user-profile", data: profile}); + port.postMessage({ + topic: "social.page-mark-config", + data: { + images: { + // this one is relative to test we handle relative ones. + marked: "/browser/browser/base/content/test/social/social_mark_image.png", + // absolute to check we handle them too. + unmarked: "https://example.com/browser/browser/base/content/test/social/social_mark_image.png" + }, + messages: { + unmarkedTooltip: "Mark this page", + markedTooltip: "Unmark this page", + unmarkedLabel: "Mark", + markedLabel: "Unmark", + } + } + }); + break; + case "test-ambient-notification": + let icon = { + name: "testIcon", + iconURL: "chrome://browser/skin/Info.png", + contentPanel: "https://example.com/browser/browser/base/content/test/social/social_panel.html", + counter: 1 + }; + apiPort.postMessage({topic: "social.ambient-notification", data: icon}); + break; + case "test-isVisible": + sidebarPort.postMessage({topic: "test-isVisible"}); + break; + case "test-isVisible-response": + testPort.postMessage({topic: "got-isVisible-response", result: event.data.result}); + break; + case "share-data-message": + if (testPort) + testPort.postMessage({topic:"got-share-data-message", result: event.data.result}); + break; + case "worker.update": + apiPort.postMessage({topic: 'social.manifest-get'}); + break; + case "social.manifest": + event.data.data.version = 2; + apiPort.postMessage({topic: 'social.manifest-set', data: event.data.data}); + break; + } + } +} diff --git a/browser/base/content/test/subtst_contextmenu.html b/browser/base/content/test/subtst_contextmenu.html new file mode 100644 index 000000000..9f8687205 --- /dev/null +++ b/browser/base/content/test/subtst_contextmenu.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Subtest for browser context menu</title> +</head> +<body> +Browser context menu subtest. + +<div id="test-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div> +<a id="test-link" href="http://mozilla.com">Click the monkey!</a> +<a id="test-mailto" href="mailto:codemonkey@mozilla.com">Mail the monkey!</a><br> +<input id="test-input"><br> +<img id="test-image" src="ctxmenu-image.png"> +<canvas id="test-canvas" width="100" height="100" style="background-color: blue"></canvas> +<video controls id="test-video-ok" src="video.ogg" width="100" height="100" style="background-color: green"></video> +<video id="test-audio-in-video" src="audio.ogg" width="100" height="100" style="background-color: red"></video> +<video controls id="test-video-bad" src="bogus.duh" width="100" height="100" style="background-color: orange"></video> +<video controls id="test-video-bad2" width="100" height="100" style="background-color: yellow"> + <source src="bogus.duh" type="video/durrrr;"> +</video> +<iframe id="test-iframe" width="98" height="98" style="border: 1px solid black"></iframe> +<iframe id="test-video-in-iframe" src="video.ogg" width="98" height="98" style="border: 1px solid black"></iframe> +<iframe id="test-image-in-iframe" src="ctxmenu-image.png" width="98" height="98" style="border: 1px solid black"></iframe> +<textarea id="test-textarea">chssseesbbbie</textarea> <!-- a weird word which generates only one suggestion --> +<div id="test-contenteditable" contenteditable="true">chssseefsbbbie</div> <!-- a more weird word which generates no suggestions --> +<input id="test-input-spellcheck" type="text" spellcheck="true" autofocus value="prodkjfgigrty"> <!-- this one also generates one suggestion --> +<div id="test-contenteditable-spellcheck-false" contenteditable="true" spellcheck="false">test</div> <!-- No Check Spelling menu item --> +<div id="test-dom-full-screen">DOM full screen FTW</div> +<div contextmenu="myMenu"> + <p id="test-pagemenu" hopeless="true">I've got a context menu!</p> + <menu id="myMenu" type="context"> + <menuitem label="Plain item" onclick="document.getElementById('test-pagemenu').removeAttribute('hopeless');"></menuitem> + <menuitem label="Disabled item" disabled></menuitem> + <menuitem> Item w/ textContent</menuitem> + <menu> + <menuitem type="checkbox" label="Checkbox" checked></menuitem> + </menu> + <menu> + <menuitem type="radio" label="Radio1" checked></menuitem> + <menuitem type="radio" label="Radio2"></menuitem> + <menuitem type="radio" label="Radio3"></menuitem> + </menu> + <menu> + <menuitem label="Item w/ icon" icon="favicon.ico"></menuitem> + <menuitem label="Item w/ bad icon" icon="data://www.mozilla.org/favicon.ico"></menuitem> + </menu> + <menu label="Submenu"> + <menuitem type="radio" label="Radio1" radiogroup="rg"></menuitem> + <menuitem type="radio" label="Radio2" checked radiogroup="rg"></menuitem> + <menuitem type="radio" label="Radio3" radiogroup="rg"></menuitem> + <menu> + <menuitem type="checkbox" label="Checkbox"></menuitem> + </menu> + </menu> + <menu hidden> + <menuitem label="Bogus item"></menuitem> + </menu> + <menu> + </menu> + <menuitem label="Hidden item" hidden></menuitem> + <menuitem></menuitem> + </menu> +</div> +<div id="test-select-text">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</div> +<div id="test-select-text-link">http://mozilla.com</div> +<a id="test-image-link" href="#"><img src="ctxmenu-image.png"></a> +<input id="test-select-input-text" type="text" value="input"> +<input id="test-select-input-text-type-password" type="password" value="password"> +<embed id="test-plugin" style="width: 200px; height: 200px;" type="application/x-test"></embed> +</body> +</html> diff --git a/browser/base/content/test/test_bug364677.html b/browser/base/content/test/test_bug364677.html new file mode 100644 index 000000000..67b9729d1 --- /dev/null +++ b/browser/base/content/test/test_bug364677.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=364677 +--> +<head> + <title>Test for Bug 364677</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=364677">Mozilla Bug 364677</a> +<p id="display"><iframe id="testFrame" src="bug364677-data.xml"></iframe></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 364677 **/ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + is(SpecialPowers.wrap($("testFrame")).contentDocument.documentElement.id, "feedHandler", + "Feed served as text/xml without a channel/link should have been sniffed"); +}); +addLoadEvent(SimpleTest.finish); +</script> +</pre> +</body> +</html> + diff --git a/browser/base/content/test/test_bug395533.html b/browser/base/content/test/test_bug395533.html new file mode 100644 index 000000000..013c8789f --- /dev/null +++ b/browser/base/content/test/test_bug395533.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=395533 +--> +<head> + <title>Test for Bug 395533</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=395533">Mozilla Bug 395533</a> +<p id="display"><iframe id="testFrame" src="bug395533-data.txt"></iframe></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 395533 **/ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Need privs because the feed seems to have an about:feeds principal or some + // such. It's not same-origin with us in any case. + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + is($("testFrame").contentDocument.documentElement.id, "", + "Text got sniffed as a feed?"); +}); +addLoadEvent(SimpleTest.finish); + + + + +</script> +</pre> +</body> +</html> + diff --git a/browser/base/content/test/test_bug435035.html b/browser/base/content/test/test_bug435035.html new file mode 100644 index 000000000..a3d353514 --- /dev/null +++ b/browser/base/content/test/test_bug435035.html @@ -0,0 +1 @@ +<img src="http://example.com/browser/browser/base/content/test/moz.png"> diff --git a/browser/base/content/test/test_bug452451.html b/browser/base/content/test/test_bug452451.html new file mode 100644 index 000000000..633bd5fa2 --- /dev/null +++ b/browser/base/content/test/test_bug452451.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=452451 +--> +<head> + <title>Test for Bug 452451</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=452451">Mozilla Bug 452451</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 452451 **/ + + netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect"); + const prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + + ok(prefs.getBoolPref("javascript.options.relimit"), + "relimit should be enabled by default"); + + /** + * Following tests are inspired from: + * js/src/tests/js1_5/extensions/regress-330569.js + */ + + var s; + const expected = 'InternalError: regular expression too complex'; + + s = '<!DOCTYPE HTML PUBLIC>' + + '<html>\n' + + '<head>\n' + + '<meta http-equiv="content-type" content="text/html">\n' + + '<title></title>\n'+ + '</head>\n' + + '<body>\n' + + '<!-- hello -->\n' + + '<script language="JavaScript">\n' + + 'var s = document. body. innerHTML;\n' + + 'var d = s. replace (/<!--(.*|\n)*-->/, "");\n' + + '<\/script>\n' + + '<\/body>\n' + + '<\/html>\n'; + + try { + /<!--(.*|\n)*-->/.exec(s); + } + catch(ex) { + actual = ex; + } + + is(actual, expected, "reg exp too complex error should have been thrown"); + + function testre( re, n ) + { + var txt = ''; + for (var i= 0; i <= n; ++i) { + txt += ','; + re.test(txt); + } + } + + try { + testre( /(?:,*)*x/, 22 ); + } + catch(ex) { + actual = ex; + } + + is(actual, expected, "reg exp too complex error should have been thrown"); + + try { + testre( /(?:,|,)*x/, 22 ); + } + catch(ex) { + actual = ex; + } + + is(actual, expected, "reg exp too complex error should have been thrown"); + + try { + testre( /(?:,|,|,|,|,)*x/, 10 ); + } + catch(ex) { + actual = ex; + } + + is(actual, expected, "reg exp too complex error should have been thrown"); +</script> +</pre> +</body> +</html> diff --git a/browser/base/content/test/test_bug462673.html b/browser/base/content/test/test_bug462673.html new file mode 100644 index 000000000..d864990e4 --- /dev/null +++ b/browser/base/content/test/test_bug462673.html @@ -0,0 +1,18 @@ +<html> +<head> +<script> +var w; +function openIt() { + w = window.open("", "window2"); +} +function closeIt() { + if (w) { + w.close(); + w = null; + } +} +</script> +</head> +<body onload="openIt();" onunload="closeIt();"> +</body> +</html> diff --git a/browser/base/content/test/test_bug628179.html b/browser/base/content/test/test_bug628179.html new file mode 100644 index 000000000..d35e17a7c --- /dev/null +++ b/browser/base/content/test/test_bug628179.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test for closing the Find bar in subdocuments</title> + </head> + <body> + <iframe id=iframe src="http://example.com/" width=320 height=240></iframe> + </body> +</html> + diff --git a/browser/base/content/test/test_bug839103.html b/browser/base/content/test/test_bug839103.html new file mode 100644 index 000000000..3639d4bda --- /dev/null +++ b/browser/base/content/test/test_bug839103.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <title>Document for Bug 839103</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style></style> +</head> +<body> +</body> +</html> diff --git a/browser/base/content/test/test_contextmenu.html b/browser/base/content/test/test_contextmenu.html new file mode 100644 index 000000000..15555fe31 --- /dev/null +++ b/browser/base/content/test/test_contextmenu.html @@ -0,0 +1,1088 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tests for browser context menu</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Browser context menu tests. +<p id="display"></p> + +<div id="content"> +</div> + +<pre id="test"> +<script> var perWindowPrivateBrowsing = false; </script> +<script type="text/javascript" src="privateBrowsingMode.js"></script> +<script class="testbody" type="text/javascript"> + +/** Test for Login Manager: multiple login autocomplete. **/ + +SpecialPowers.Cu.import("resource://gre/modules/InlineSpellChecker.jsm", window); +SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", window); + +const Ci = SpecialPowers.Ci; + +function openContextMenuFor(element, shiftkey, waitForSpellCheck) { + // Context menu should be closed before we open it again. + is(SpecialPowers.wrap(contextMenu).state, "closed", "checking if popup is closed"); + + if (lastElement) + lastElement.blur(); + element.focus(); + + // Some elements need time to focus and spellcheck before any tests are + // run on them. + function actuallyOpenContextMenuFor() { + lastElement = element; + var eventDetails = { type : "contextmenu", button : 2, shiftKey : shiftkey }; + synthesizeMouse(element, 2, 2, eventDetails, element.ownerDocument.defaultView); + } + + if (waitForSpellCheck) + onSpellCheck(element, actuallyOpenContextMenuFor); + else + actuallyOpenContextMenuFor(); +} + +function closeContextMenu() { + contextMenu.hidePopup(); +} + +function executeCopyCommand(command, expectedValue) +{ + // Just execute the command directly rather than simulating a context menu + // press to avoid having to deal with its asynchronous nature + SpecialPowers.wrap(subwindow).controllers.getControllerForCommand(command).doCommand(command); + + // The easiest way to check the clipboard is to paste the contents into a + // textbox + input.focus(); + input.value = ""; + SpecialPowers.wrap(input).controllers.getControllerForCommand("cmd_paste").doCommand("cmd_paste"); + is(input.value, expectedValue, "paste for command " + command); +} + +function invokeItemAction(generatedItemId) +{ + var item = contextMenu.getElementsByAttribute("generateditemid", + generatedItemId)[0]; + ok(item, "Got generated XUL menu item"); + item.doCommand(); + ok(!pagemenu.hasAttribute("hopeless"), "attribute got removed"); +} + +function selectText(element) { + // Clear any previous selections before selecting new element. + subwindow.getSelection().removeAllRanges(); + + var div = subwindow.document.createRange(); + div.setStartBefore(element); + div.setEndAfter(element); + subwindow.getSelection().addRange(div); +} + +function selectInputText(element) { + // Clear any previous selections before selecting new element. + subwindow.getSelection().removeAllRanges(); + + element.select(); +} + +function getVisibleMenuItems(aMenu, aData) { + var items = []; + var accessKeys = {}; + for (var i = 0; i < aMenu.childNodes.length; i++) { + var item = aMenu.childNodes[i]; + if (item.hidden) + continue; + + var key = item.accessKey; + if (key) + key = key.toLowerCase(); + + var isGenerated = item.hasAttribute("generateditemid"); + + if (item.nodeName == "menuitem") { + var isSpellSuggestion = item.className == "spell-suggestion"; + if (isSpellSuggestion) { + is(item.id, "", "child menuitem #" + i + " is a spelling suggestion"); + } else if (isGenerated) { + is(item.id, "", "child menuitem #" + i + " is a generated item"); + } else { + ok(item.id, "child menuitem #" + i + " has an ID"); + } + var label = item.getAttribute("label"); + ok(label.length, "menuitem " + item.id + " has a label"); + if (isSpellSuggestion) { + is(key, "", "Spell suggestions shouldn't have an access key"); + items.push("*" + label); + } else if (isGenerated) { + items.push("+" + label); + } else if (item.id.indexOf("spell-check-dictionary-") != 0 && + item.id != "spell-no-suggestions" && + item.id != "spell-add-dictionaries-main") { + ok(key, "menuitem " + item.id + " has an access key"); + if (accessKeys[key]) + ok(false, "menuitem " + item.id + " has same accesskey as " + accessKeys[key]); + else + accessKeys[key] = item.id; + } + if (!isSpellSuggestion && !isGenerated) { + items.push(item.id); + } + if (isGenerated) { + var p = {}; + p.type = item.getAttribute("type"); + p.icon = item.getAttribute("image"); + p.checked = item.hasAttribute("checked"); + p.disabled = item.hasAttribute("disabled"); + items.push(p); + } else { + items.push(!item.disabled); + } + } else if (item.nodeName == "menuseparator") { + ok(true, "--- seperator id is " + item.id); + items.push("---"); + items.push(null); + } else if (item.nodeName == "menu") { + if (isGenerated) { + item.id = "generated-submenu-" + aData.generatedSubmenuId++; + } + ok(item.id, "child menu #" + i + " has an ID"); + if (!isGenerated) { + ok(key, "menu has an access key"); + if (accessKeys[key]) + ok(false, "menu " + item.id + " has same accesskey as " + accessKeys[key]); + else + accessKeys[key] = item.id; + } + items.push(item.id); + items.push(!item.disabled); + // Add a dummy item to that the indexes in checkMenu are the same + // for expectedItems and actualItems. + items.push([]); + items.push(null); + } else { + ok(false, "child #" + i + " of menu ID " + aMenu.id + + " has an unknown type (" + item.nodeName + ")"); + } + } + return items; +} + +function checkContextMenu(expectedItems) { + is(contextMenu.state, "open", "checking if popup is open"); + var data = { generatedSubmenuId: 1 }; + checkMenu(contextMenu, expectedItems, data); +} + +/* + * checkMenu - checks to see if the specified <menupopup> contains the + * expected items and state. + * expectedItems is a array of (1) item IDs and (2) a boolean specifying if + * the item is enabled or not (or null to ignore it). Submenus can be checked + * by providing a nested array entry after the expected <menu> ID. + * For example: ["blah", true, // item enabled + * "submenu", null, // submenu + * ["sub1", true, // submenu contents + * "sub2", false], null, // submenu contents + * "lol", false] // item disabled + * + */ +function checkMenu(menu, expectedItems, data) { + var actualItems = getVisibleMenuItems(menu, data); + //ok(false, "Items are: " + actualItems); + for (var i = 0; i < expectedItems.length; i+=2) { + var actualItem = actualItems[i]; + var actualEnabled = actualItems[i + 1]; + var expectedItem = expectedItems[i]; + var expectedEnabled = expectedItems[i + 1]; + if (expectedItem instanceof Array) { + ok(true, "Checking submenu..."); + var menuID = expectedItems[i - 2]; // The last item was the menu ID. + var submenu = menu.getElementsByAttribute("id", menuID)[0]; + ok(submenu, "got a submenu element of id='" + menuID + "'"); + if (submenu) { + is(submenu.nodeName, "menu", "submenu element of id='" + menuID + + "' has expected nodeName"); + checkMenu(submenu.menupopup, expectedItem, data); + } + } else { + is(actualItem, expectedItem, + "checking item #" + i/2 + " (" + expectedItem + ") name"); + + if (typeof expectedEnabled == "object" && expectedEnabled != null || + typeof actualEnabled == "object" && actualEnabled != null) { + + ok(!(actualEnabled == null), "actualEnabled is not null"); + ok(!(expectedEnabled == null), "expectedEnabled is not null"); + is(typeof actualEnabled, typeof expectedEnabled, "checking types"); + + if (typeof actualEnabled != typeof expectedEnabled || + actualEnabled == null || expectedEnabled == null) + continue; + + is(actualEnabled.type, expectedEnabled.type, + "checking item #" + i/2 + " (" + expectedItem + ") type attr value"); + var icon = actualEnabled.icon; + if (icon) { + var tmp = ""; + var j = icon.length - 1; + while (j && icon[j] != "/") { + tmp = icon[j--] + tmp; + } + icon = tmp; + } + is(icon, expectedEnabled.icon, + "checking item #" + i/2 + " (" + expectedItem + ") icon attr value"); + is(actualEnabled.checked, expectedEnabled.checked, + "checking item #" + i/2 + " (" + expectedItem + ") has checked attr"); + is(actualEnabled.disabled, expectedEnabled.disabled, + "checking item #" + i/2 + " (" + expectedItem + ") has disabled attr"); + } else if (expectedEnabled != null) + is(actualEnabled, expectedEnabled, + "checking item #" + i/2 + " (" + expectedItem + ") enabled state"); + } + } + // Could find unexpected extra items at the end... + is(actualItems.length, expectedItems.length, "checking expected number of menu entries"); +} + +/* + * runTest + * + * Called by a popupshowing event handler. Each test checks for expected menu + * contents, closes the popup, and finally triggers the popup on a new element + * (thus kicking off another cycle). + * + */ +function runTest(testNum) { + // Seems we need to enable this again, or sendKeyEvent() complaints. + ok(true, "Starting test #" + testNum); + + var inspectItems = []; + if (SpecialPowers.getBoolPref("devtools.inspector.enabled")) { + inspectItems = ["---", null, + "context-inspect", true]; + } + + switch (testNum) { + case 1: + // Invoke context menu for next test. + openContextMenuFor(text); + break; + + case 2: + // Context menu for plain text + plainTextItems = ["context-back", false, + "context-forward", false, + "context-reload", true, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "---", null, + "context-viewsource", true, + "context-viewinfo", true + ].concat(inspectItems); + checkContextMenu(plainTextItems); + closeContextMenu(); + openContextMenuFor(link); // Invoke context menu for next test. + break; + + case 3: + // Context menu for text link + if (perWindowPrivateBrowsing) { + checkContextMenu(["context-openlinkintab", true, + "context-openlink", true, + "context-openlinkprivate", true, + "---", null, + "context-bookmarklink", true, + "context-savelink", true, + "context-copylink", true + ].concat(inspectItems)); + } else { + checkContextMenu(["context-openlinkintab", true, + "context-openlink", true, + "---", null, + "context-bookmarklink", true, + "context-savelink", true, + "context-copylink", true + ].concat(inspectItems)); + } + closeContextMenu(); + openContextMenuFor(mailto); // Invoke context menu for next test. + break; + + case 4: + // Context menu for text mailto-link + checkContextMenu(["context-copyemail", true].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(input); // Invoke context menu for next test. + break; + + case 5: + // Context menu for text input field + checkContextMenu(["context-undo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", false, + "---", null, + "spell-check-enabled", true + ].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(img); // Invoke context menu for next test. + break; + + case 6: + // Context menu for an image + checkContextMenu(["context-viewimage", true, + "context-copyimage-contents", true, + "context-copyimage", true, + "---", null, + "context-saveimage", true, + "context-sendimage", true, + "context-setDesktopBackground", true, + "context-viewimageinfo", true + ].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(canvas); // Invoke context menu for next test. + break; + + case 7: + // Context menu for a canvas + checkContextMenu(["context-viewimage", true, + "context-saveimage", true, + "context-bookmarkpage", true, + "context-selectall", true + ].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(video_ok); // Invoke context menu for next test. + break; + + case 8: + // Context menu for a video (with a VALID media source) + checkContextMenu(["context-media-play", true, + "context-media-mute", true, + "context-media-playbackrate", null, + ["context-media-playbackrate-050x", true, + "context-media-playbackrate-100x", true, + "context-media-playbackrate-150x", true, + "context-media-playbackrate-200x", true], null, + "context-media-hidecontrols", true, + "context-video-showstats", true, + "context-video-fullscreen", true, + "---", null, + "context-viewvideo", true, + "context-copyvideourl", true, + "---", null, + "context-savevideo", true, + "context-video-saveimage", true, + "context-sendvideo", true + ].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(audio_in_video); // Invoke context menu for next test. + break; + + case 9: + // Context menu for a video (with an audio-only file) + checkContextMenu(["context-media-play", true, + "context-media-mute", true, + "context-media-playbackrate", null, + ["context-media-playbackrate-050x", true, + "context-media-playbackrate-100x", true, + "context-media-playbackrate-150x", true, + "context-media-playbackrate-200x", true], null, + "context-media-showcontrols", true, + "---", null, + "context-copyaudiourl", true, + "---", null, + "context-saveaudio", true, + "context-sendaudio", true + ].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(video_bad); // Invoke context menu for next test. + break; + + case 10: + // Context menu for a video (with an INVALID media source) + checkContextMenu(["context-media-play", false, + "context-media-mute", false, + "context-media-playbackrate", null, + ["context-media-playbackrate-050x", false, + "context-media-playbackrate-100x", false, + "context-media-playbackrate-150x", false, + "context-media-playbackrate-200x", false], null, + "context-media-hidecontrols", false, + "context-video-showstats", false, + "context-video-fullscreen", false, + "---", null, + "context-viewvideo", true, + "context-copyvideourl", true, + "---", null, + "context-savevideo", true, + "context-video-saveimage", false, + "context-sendvideo", true + ].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(video_bad2); // Invoke context menu for next test. + break; + + case 11: + // Context menu for a video (with an INVALID media source) + checkContextMenu(["context-media-play", false, + "context-media-mute", false, + "context-media-playbackrate", null, + ["context-media-playbackrate-050x", false, + "context-media-playbackrate-100x", false, + "context-media-playbackrate-150x", false, + "context-media-playbackrate-200x", false], null, + "context-media-hidecontrols", false, + "context-video-showstats", false, + "context-video-fullscreen", false, + "---", null, + "context-viewvideo", false, + "context-copyvideourl", false, + "---", null, + "context-savevideo", false, + "context-video-saveimage", false, + "context-sendvideo", false + ].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(iframe); // Invoke context menu for next test. + break; + + case 12: + // Context menu for an iframe + checkContextMenu(["context-back", false, + "context-forward", false, + "context-reload", true, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "frame", null, + ["context-showonlythisframe", true, + "context-openframeintab", true, + "context-openframe", true, + "---", null, + "context-reloadframe", true, + "---", null, + "context-bookmarkframe", true, + "context-saveframe", true, + "---", null, + "context-printframe", true, + "---", null, + "context-viewframesource", true, + "context-viewframeinfo", true], null, + "---", null, + "context-viewsource", true, + "context-viewinfo", true + ].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(video_in_iframe); // Invoke context menu for next test. + break; + + case 13: + // Context menu for a video in an iframe + checkContextMenu(["context-media-play", true, + "context-media-mute", true, + "context-media-playbackrate", null, + ["context-media-playbackrate-050x", true, + "context-media-playbackrate-100x", true, + "context-media-playbackrate-150x", true, + "context-media-playbackrate-200x", true], null, + "context-media-hidecontrols", true, + "context-video-showstats", true, + "context-video-fullscreen", true, + "---", null, + "context-viewvideo", true, + "context-copyvideourl", true, + "---", null, + "context-savevideo", true, + "context-video-saveimage", true, + "context-sendvideo", true, + "frame", null, + ["context-showonlythisframe", true, + "context-openframeintab", true, + "context-openframe", true, + "---", null, + "context-reloadframe", true, + "---", null, + "context-bookmarkframe", true, + "context-saveframe", true, + "---", null, + "context-printframe", true, + "---", null, + "context-viewframeinfo", true], null].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(image_in_iframe); // Invoke context menu for next test. + break; + + case 14: + // Context menu for an image in an iframe + checkContextMenu(["context-viewimage", true, + "context-copyimage-contents", true, + "context-copyimage", true, + "---", null, + "context-saveimage", true, + "context-sendimage", true, + "context-setDesktopBackground", true, + "context-viewimageinfo", true, + "frame", null, + ["context-showonlythisframe", true, + "context-openframeintab", true, + "context-openframe", true, + "---", null, + "context-reloadframe", true, + "---", null, + "context-bookmarkframe", true, + "context-saveframe", true, + "---", null, + "context-printframe", true, + "---", null, + "context-viewframeinfo", true], null].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(textarea, false, true); // Invoke context menu for next test, but wait for the spellcheck. + break; + + case 15: + // Context menu for textarea + checkContextMenu(["*chubbiness", true, // spelling suggestion + "spell-add-to-dictionary", true, + "---", null, + "context-undo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", true, + "---", null, + "spell-check-enabled", true, + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null + ].concat(inspectItems)); + contextMenu.ownerDocument.getElementById("spell-add-to-dictionary").doCommand(); // Add to dictionary + closeContextMenu(); + openContextMenuFor(text); // Invoke context menu for next test. + break; + + case 16: + // Re-check context menu for plain text to make sure it hasn't changed + checkContextMenu(plainTextItems); + closeContextMenu(); + openContextMenuFor(textarea, false, true); // Invoke context menu for next test. + break; + + case 17: + // Context menu for textarea after a word has been added + // to the dictionary + checkContextMenu(["spell-undo-add-to-dictionary", true, + "---", null, + "context-undo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", true, + "---", null, + "spell-check-enabled", true, + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null + ].concat(inspectItems)); + contextMenu.ownerDocument.getElementById("spell-undo-add-to-dictionary").doCommand(); // Undo add to dictionary + closeContextMenu(); + openContextMenuFor(contenteditable, false, true); + break; + + case 18: + // Context menu for contenteditable + checkContextMenu(["spell-no-suggestions", false, + "spell-add-to-dictionary", true, + "---", null, + "context-undo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", true, + "---", null, + "spell-check-enabled", true, + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null + ].concat(inspectItems)); + + closeContextMenu(); + openContextMenuFor(inputspell, false, true); // Invoke context menu for next test. + break; + + case 19: + // Context menu for spell-check input + checkContextMenu(["*prodigality", true, // spelling suggestion + "spell-add-to-dictionary", true, + "---", null, + "context-undo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", true, + "---", null, + "spell-check-enabled", true, + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null + ].concat(inspectItems)); + + closeContextMenu(); + openContextMenuFor(inputspellfalse, false, true); // Invoke context menu for next test. + break; + + case 20: + // Context menu for text input field with spellcheck=false + checkContextMenu(["context-undo", false, + "---", null, + "context-cut", false, + "context-copy", false, + "context-paste", null, // ignore clipboard state + "context-delete", false, + "---", null, + "context-selectall", true, + "---", null, + "spell-add-dictionaries-main", true, + ].concat(inspectItems)); + + closeContextMenu(); + openContextMenuFor(link); // Invoke context menu for next test. + break; + + case 21: + executeCopyCommand("cmd_copyLink", "http://mozilla.com/"); + closeContextMenu(); + openContextMenuFor(pagemenu); // Invoke context menu for next test. + break; + + case 22: + // Context menu for element with assigned content context menu + checkContextMenu(["+Plain item", {type: "", icon: "", checked: false, disabled: false}, + "+Disabled item", {type: "", icon: "", checked: false, disabled: true}, + "+Item w/ textContent", {type: "", icon: "", checked: false, disabled: false}, + "---", null, + "+Checkbox", {type: "checkbox", icon: "", checked: true, disabled: false}, + "---", null, + "+Radio1", {type: "checkbox", icon: "", checked: true, disabled: false}, + "+Radio2", {type: "checkbox", icon: "", checked: false, disabled: false}, + "+Radio3", {type: "checkbox", icon: "", checked: false, disabled: false}, + "---", null, + "+Item w/ icon", {type: "", icon: "favicon.ico", checked: false, disabled: false}, + "+Item w/ bad icon", {type: "", icon: "", checked: false, disabled: false}, + "---", null, + "generated-submenu-1", true, + ["+Radio1", {type: "checkbox", icon: "", checked: false, disabled: false}, + "+Radio2", {type: "checkbox", icon: "", checked: true, disabled: false}, + "+Radio3", {type: "checkbox", icon: "", checked: false, disabled: false}, + "---", null, + "+Checkbox", {type: "checkbox", icon: "", checked: false, disabled: false}], null, + "---", null, + "context-back", false, + "context-forward", false, + "context-reload", true, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "---", null, + "context-viewsource", true, + "context-viewinfo", true + ].concat(inspectItems)); + + invokeItemAction("0"); + closeContextMenu(); + + // run mozRequestFullScreen on the element we're testing + var full_screen_element = subwindow.document.getElementById("test-dom-full-screen"); + var openDomFullScreen = function() { + subwindow.removeEventListener("mozfullscreenchange", openDomFullScreen, false); + openContextMenuFor(dom_full_screen, true); // Invoke context menu for next test. + } + subwindow.addEventListener("mozfullscreenchange", openDomFullScreen, false); + SpecialPowers.setBoolPref("full-screen-api.approval-required", false); + SpecialPowers.setBoolPref("full-screen-api.allow-trusted-requests-only", false); + full_screen_element.mozRequestFullScreen(); + break; + + case 23: + // Context menu for DOM Fullscreen mode (NOTE: this is *NOT* on an img) + checkContextMenu(["context-leave-dom-fullscreen", true, + "---", null, + "context-back", false, + "context-forward", false, + "context-reload", true, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "---", null, + "context-viewsource", true, + "context-viewinfo", true + ].concat(inspectItems)); + closeContextMenu(); + var full_screen_element = subwindow.document.getElementById("test-dom-full-screen"); + var openPagemenu = function() { + subwindow.removeEventListener("mozfullscreenchange", openPagemenu, false); + SpecialPowers.clearUserPref("full-screen-api.approval-required"); + SpecialPowers.clearUserPref("full-screen-api.allow-trusted-requests-only"); + openContextMenuFor(pagemenu, true); // Invoke context menu for next test. + } + subwindow.addEventListener("mozfullscreenchange", openPagemenu, false); + subwindow.document.mozCancelFullScreen(); + break; + + case 24: + // Context menu for element with assigned content context menu + // The shift key should bypass content context menu processing + checkContextMenu(["context-back", false, + "context-forward", false, + "context-reload", true, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "---", null, + "context-viewsource", true, + "context-viewinfo", true + ].concat(inspectItems)); + closeContextMenu(); + selectText(selecttext); // Select text prior to opening context menu. + openContextMenuFor(selecttext); // Invoke context menu for next test. + return; + + case 25: + // Context menu for selected text + if (SpecialPowers.Services.appinfo.OS == "Darwin") { + // This test is only enabled on Mac due to bug 736399. + checkContextMenu(["context-copy", true, + "context-selectall", true, + "---", null, + "context-searchselect", true, + "context-viewpartialsource-selection", true + ].concat(inspectItems)); + } + closeContextMenu(); + selectText(selecttextlink); // Select text prior to opening context menu. + openContextMenuFor(selecttextlink); // Invoke context menu for next test. + return; + + case 26: + // Context menu for selected text which matches valid URL pattern + if (SpecialPowers.Services.appinfo.OS == "Darwin") { + // This test is only enabled on Mac due to bug 736399. + if (perWindowPrivateBrowsing) { + checkContextMenu(["context-openlinkincurrent", true, + "context-openlinkintab", true, + "context-openlink", true, + "context-openlinkprivate", true, + "---", null, + "context-bookmarklink", true, + "context-savelink", true, + "context-copy", true, + "context-selectall", true, + "---", null, + "context-searchselect", true, + "context-viewpartialsource-selection", true + ].concat(inspectItems)); + } else { + checkContextMenu(["context-openlinkincurrent", true, + "context-openlinkintab", true, + "context-openlink", true, + "---", null, + "context-bookmarklink", true, + "context-savelink", true, + "context-copy", true, + "context-selectall", true, + "---", null, + "context-searchselect", true, + "context-viewpartialsource-selection", true + ].concat(inspectItems)); + } + } + closeContextMenu(); + // clear the selection because following tests don't expect any selection + subwindow.getSelection().removeAllRanges(); + + openContextMenuFor(imagelink) + break; + + case 27: + // Context menu for image link + if (perWindowPrivateBrowsing) { + checkContextMenu(["context-openlinkintab", true, + "context-openlink", true, + "context-openlinkprivate", true, + "---", null, + "context-bookmarklink", true, + "context-savelink", true, + "context-copylink", true, + "---", null, + "context-viewimage", true, + "context-copyimage-contents", true, + "context-copyimage", true, + "---", null, + "context-saveimage", true, + "context-sendimage", true, + "context-setDesktopBackground", true, + "context-viewimageinfo", true + ].concat(inspectItems)); + } else { + checkContextMenu(["context-openlinkintab", true, + "context-openlink", true, + "---", null, + "context-bookmarklink", true, + "context-savelink", true, + "context-copylink", true, + "---", null, + "context-viewimage", true, + "context-copyimage-contents", true, + "context-copyimage", true, + "---", null, + "context-saveimage", true, + "context-sendimage", true, + "context-setDesktopBackground", true, + "context-viewimageinfo", true + ].concat(inspectItems)); + } + closeContextMenu(); + selectInputText(select_inputtext); // Select text prior to opening context menu. + openContextMenuFor(select_inputtext); // Invoke context menu for next test. + return; + + case 28: + // Context menu for selected text in input + checkContextMenu(["context-undo", false, + "---", null, + "context-cut", true, + "context-copy", true, + "context-paste", null, // ignore clipboard state + "context-delete", true, + "---", null, + "context-selectall", true, + "context-searchselect",true, + "---", null, + "spell-check-enabled", true + ].concat(inspectItems)); + closeContextMenu(); + selectInputText(select_inputtext_password); // Select text prior to opening context menu. + openContextMenuFor(select_inputtext_password); // Invoke context menu for next test. + return; + + case 29: + // Context menu for selected text in input[type="password"] + checkContextMenu(["context-undo", false, + "---", null, + "context-cut", true, + "context-copy", true, + "context-paste", null, // ignore clipboard state + "context-delete", true, + "---", null, + "context-selectall", true, + "---", null, + "spell-check-enabled", true, + //spell checker is shown on input[type="password"] on this testcase + "spell-dictionaries", true, + ["spell-check-dictionary-en-US", true, + "---", null, + "spell-add-dictionaries", true], null + ].concat(inspectItems)); + closeContextMenu(); + subwindow.getSelection().removeAllRanges(); + openContextMenuFor(plugin); + return; + + case 30: + // Context menu for click-to-play blocked plugin + checkContextMenu(["context-ctp-play", true, + "context-ctp-hide", true, + "---", null, + "context-back", false, + "context-forward", false, + "context-reload", true, + "---", null, + "context-bookmarkpage", true, + "context-savepage", true, + "---", null, + "context-viewbgimage", false, + "context-selectall", true, + "---", null, + "context-viewsource", true, + "context-viewinfo", true + ].concat(inspectItems)); + closeContextMenu(); + SpecialPowers.clearUserPref("plugins.click_to_play"); + var ph = SpecialPowers.Cc["@mozilla.org/plugin/host;1"] + .getService(SpecialPowers.Ci.nsIPluginHost); + var tags = ph.getPluginTags(); + for (var tag of tags) { + if (tag.name == "Test Plug-in") { + tag.enabledState = SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED; + } + } + + // finish test + subwindow.close(); + SimpleTest.finish(); + return; + + /* + * Other things that would be nice to test: + * - spelling / misspelled word (in text input?) + * - check state of disabled items + * - test execution of menu items (maybe as a separate test?) + */ + + default: + ok(false, "Unexpected invocation of test #" + testNum); + subwindow.close(); + SimpleTest.finish(); + return; + } + +} + + +var testNum = 1; +var subwindow, chromeWin, contextMenu, lastElement; +var text, link, mailto, input, img, canvas, video_ok, video_bad, video_bad2, + iframe, video_in_iframe, image_in_iframe, textarea, contenteditable, + inputspell, pagemenu, dom_full_screen, plainTextItems, audio_in_video, + selecttext, selecttextlink, imagelink, select_inputtext, select_inputtext_password, + plugin, inputspellfalse; + +function startTest() { + chromeWin = SpecialPowers.wrap(subwindow) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow) + .QueryInterface(Ci.nsIDOMChromeWindow); + contextMenu = chromeWin.document.getElementById("contentAreaContextMenu"); + ok(contextMenu, "Got context menu XUL"); + + if (chromeWin.document.getElementById("Browser:Stop").getAttribute("disabled") != "true") { + todo(false, "Wait for subwindow to load... (This should usually happen once.)"); + SimpleTest.executeSoon(startTest); + return; + } + + subwindow.allowFullscreen = true; + lastElement = null; + + text = subwindow.document.getElementById("test-text"); + link = subwindow.document.getElementById("test-link"); + imagelink = subwindow.document.getElementById("test-image-link"); + mailto = subwindow.document.getElementById("test-mailto"); + input = subwindow.document.getElementById("test-input"); + img = subwindow.document.getElementById("test-image"); + canvas = subwindow.document.getElementById("test-canvas"); + video_ok = subwindow.document.getElementById("test-video-ok"); + audio_in_video = subwindow.document.getElementById("test-audio-in-video"); + video_bad = subwindow.document.getElementById("test-video-bad"); + video_bad2 = subwindow.document.getElementById("test-video-bad2"); + iframe = subwindow.document.getElementById("test-iframe"); + video_in_iframe = subwindow.document.getElementById("test-video-in-iframe").contentDocument.getElementsByTagName("video")[0]; + video_in_iframe.pause(); + image_in_iframe = subwindow.document.getElementById("test-image-in-iframe").contentDocument.getElementsByTagName("img")[0]; + textarea = subwindow.document.getElementById("test-textarea"); + contenteditable = subwindow.document.getElementById("test-contenteditable"); + contenteditable.focus(); // content editable needs to be focused to enable spellcheck + inputspell = subwindow.document.getElementById("test-input-spellcheck"); + inputspellfalse = subwindow.document.getElementById("test-contenteditable-spellcheck-false"); + pagemenu = subwindow.document.getElementById("test-pagemenu"); + dom_full_screen = subwindow.document.getElementById("test-dom-full-screen"); + selecttext = subwindow.document.getElementById("test-select-text"); + selecttextlink = subwindow.document.getElementById("test-select-text-link"); + select_inputtext = subwindow.document.getElementById("test-select-input-text"); + select_inputtext_password = subwindow.document.getElementById("test-select-input-text-type-password"); + plugin = subwindow.document.getElementById("test-plugin"); + + contextMenu.addEventListener("popupshown", function() { runTest(++testNum); }, false); + runTest(1); +} + +// We open this in a separate window, because the Mochitests run inside a frame. +// The frame causes an extra menu item, and prevents running the test +// standalone (ie, clicking the test name in the Mochitest window) to see +// success/failure messages. +var painted = false, loaded = false; + +function waitForEvents(event) +{ + if (event.type == "MozAfterPaint") + painted = true; + else if (event.type == "load") + loaded = true; + if (painted && loaded) { + subwindow.removeEventListener("MozAfterPaint", waitForEvents, false); + subwindow.onload = null; + startTest(); + } +} + +const isOSXMtnLion = navigator.userAgent.indexOf("Mac OS X 10.8") != -1; + +if (isOSXMtnLion) { + todo(false, "Mountain Lion doesn't like this test (bug 792304)"); +} else { + SpecialPowers.setBoolPref("plugins.click_to_play", true); + var ph = SpecialPowers.Cc["@mozilla.org/plugin/host;1"] + .getService(SpecialPowers.Ci.nsIPluginHost); + var tags = ph.getPluginTags(); + for (var tag of tags) { + if (tag.name == "Test Plug-in") { + tag.enabledState = SpecialPowers.Ci.nsIPluginTag.STATE_CLICKTOPLAY; + } + } + + var subwindow = window.open("./subtst_contextmenu.html", "contextmenu-subtext", "width=600,height=800"); + subwindow.addEventListener("MozAfterPaint", waitForEvents, false); + subwindow.onload = waitForEvents; + + SimpleTest.waitForExplicitFinish(); +} +</script> +</pre> +</body> +</html> diff --git a/browser/base/content/test/test_feed_discovery.html b/browser/base/content/test/test_feed_discovery.html new file mode 100644 index 000000000..31d716385 --- /dev/null +++ b/browser/base/content/test/test_feed_discovery.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=377611 +--> +<head> + <title>Test for feed discovery</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377611">Mozilla Bug 377611</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 377611 **/ + +var rv = { tests: null }; +var testCheckInterval = null; + +function startTest() { + var url = window.location.href.replace(/test_feed_discovery\.html/, + 'feed_discovery.html'); + SpecialPowers.openDialog(window, [url, '', 'dialog=no,width=10,height=10', rv]); + testCheckInterval = window.setInterval(tryIfTestIsFinished, 500); +} + +function tryIfTestIsFinished() { + if (rv.tests) { + window.clearInterval(testCheckInterval); + checkTest(); + } +} + +function checkTest() { + for (var i = 0; i < rv.tests.length; ++ i) { + var test = rv.tests[i]; + ok(test.check, test.message); + } + SimpleTest.finish(); +} + +window.onload = startTest; + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> + diff --git a/browser/base/content/test/test_offlineNotification.html b/browser/base/content/test/test_offlineNotification.html new file mode 100644 index 000000000..8c603f150 --- /dev/null +++ b/browser/base/content/test/test_offlineNotification.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=462856 +--> +<head> + <title>Test offline app notification</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="loaded()"> +<p id="display"> +<!-- Load the test frame twice from the same domain, + to make sure we get notifications for both --> +<iframe name="testFrame" src="offlineChild.html"></iframe> +<iframe name="testFrame2" src="offlineChild2.html"></iframe> +<!-- Load from another domain to make sure we get a second allow/deny + notification --> +<iframe name="testFrame3" src="http://example.com/tests/browser/base/content/test/offlineChild.html"></iframe> + +<iframe id="eventsTestFrame" src="offlineEvent.html"></iframe> + +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +const Cc = SpecialPowers.Cc; + +var numFinished = 0; + +window.addEventListener("message", function(event) { + is(event.data, "success", "Child was successfully cached."); + + if (++numFinished == 3) { + // Clean up after ourself + var pm = Cc["@mozilla.org/permissionmanager;1"]. + getService(SpecialPowers.Ci.nsIPermissionManager); + var ioService = Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + var uri1 = ioService.newURI(frames.testFrame.location, null, null); + var uri2 = ioService.newURI(frames.testFrame3.location, null, null); + + var principal1 = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager) + .getNoAppCodebasePrincipal(uri1); + var principal2 = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager) + .getNoAppCodebasePrincipal(uri2); + + pm.removeFromPrincipal(principal1, "offline-app"); + pm.removeFromPrincipal(principal2, "offline-app"); + + SimpleTest.finish(); + } + }, false); + +var count = 0; +var expectedEvent = ""; +function eventHandler(evt) { + ++count; + is(evt.type, expectedEvent, "Wrong event!"); +} + +function testEventHandling() { + var events = [ "checking", + "error", + "noupdate", + "downloading", + "progress", + "updateready", + "cached", + "obsolete"]; + var w = document.getElementById("eventsTestFrame").contentWindow; + var e; + for (var i = 0; i < events.length; ++i) { + count = 0; + expectedEvent = events[i]; + e = w.document.createEvent("event"); + e.initEvent(expectedEvent, true, true); + w.applicationCache["on" + expectedEvent] = eventHandler; + w.applicationCache.addEventListener(expectedEvent, eventHandler, true); + w.applicationCache.dispatchEvent(e); + is(count, 2, "Wrong number events!"); + w.applicationCache["on" + expectedEvent] = null; + w.applicationCache.removeEventListener(expectedEvent, eventHandler, true); + w.applicationCache.dispatchEvent(e); + is(count, 2, "Wrong number events!"); + } + + // Test some random event. + count = 0; + expectedEvent = "foo"; + e = w.document.createEvent("event"); + e.initEvent(expectedEvent, true, true); + w.applicationCache.addEventListener(expectedEvent, eventHandler, true); + w.applicationCache.dispatchEvent(e); + is(count, 1, "Wrong number events!"); + w.applicationCache.removeEventListener(expectedEvent, eventHandler, true); + w.applicationCache.dispatchEvent(e); + is(count, 1, "Wrong number events!"); +} + +function loaded() { + testEventHandling(); + + // Click the notification panel's "Allow" button. This should kick + // off updates, which will eventually lead to getting messages from + // the children. + var wm = SpecialPowers.Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(SpecialPowers.Ci.nsIWindowMediator); + var win = wm.getMostRecentWindow("navigator:browser"); + var panel = win.PopupNotifications.panel; + is(panel.childElementCount, 2, "2 notifications being displayed"); + panel.firstElementChild.button.click(); + + // should have dismissed one of the notifications. + is(panel.childElementCount, 1, "1 notification now being displayed"); + panel.firstElementChild.button.click(); +} + +</script> +</pre> +</body> +</html> diff --git a/browser/base/content/test/test_offline_gzip.html b/browser/base/content/test/test_offline_gzip.html new file mode 100644 index 000000000..09713da92 --- /dev/null +++ b/browser/base/content/test/test_offline_gzip.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=501422 + +When content which was transported over the network with +Content-Type: gzip is added to the offline +cache, it can be fetched from the cache successfully. +--> +<head> + <title>Test gzipped offline resources</title> + <script type="text/javascript" + src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="loaded()"> +<p id="display"> +<iframe name="testFrame" src="gZipOfflineChild.html"></iframe> + +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var cacheCount = 0; +var intervalID = 0; + +window.addEventListener("message", handleMessageEvents, false); +SimpleTest.waitForExplicitFinish(); + +function finishTest() { + // Clean up after ourselves. + var Cc = SpecialPowers.Cc; + var pm = Cc["@mozilla.org/permissionmanager;1"]. + getService(SpecialPowers.Ci.nsIPermissionManager); + + var uri = Cc["@mozilla.org/network/io-service;1"].getService(SpecialPowers.Ci.nsIIOService) + .newURI(window.frames[0].location, null, null); + var principal = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(SpecialPowers.Ci.nsIScriptSecurityManager) + .getNoAppCodebasePrincipal(uri); + + pm.removeFromPrincipal(principal, "offline-app"); + + window.removeEventListener("message", handleMessageEvents, false); + + SimpleTest.finish(); +} + +//// +// Handle "message" events which are posted from the iframe upon +// offline cache events. +// +function handleMessageEvents(event) { + cacheCount++; + switch (cacheCount) { + case 1: + // This is the initial caching off offline data. + is(event.data, "oncache", "Child was successfully cached."); + // Reload the frame; this will generate an error message + // in the case of bug 501422. + frames.testFrame.window.location.reload(); + // Use setInterval to repeatedly call a function which + // checks that one of two things has occurred: either + // the offline cache is udpated (which means our iframe + // successfully reloaded), or the string "error" appears + // in the iframe, as in the case of bug 501422. + intervalID = setInterval(function() { + // Sometimes document.body may not exist, and trying to access + // it will throw an exception, so handle this case. + try { + var bodyInnerHTML = frames.testFrame.document.body.innerHTML; + } + catch (e) { + var bodyInnerHTML = ""; + } + if (cacheCount == 2 || bodyInnerHTML.contains("error")) { + clearInterval(intervalID); + is(cacheCount, 2, "frame not reloaded successfully"); + if (cacheCount != 2) { + finishTest(); + } + } + }, 100); + break; + case 2: + is(event.data, "onupdate", "Child was successfully updated."); + finishTest(); + break; + default: + // how'd we get here? + ok(false, "cacheCount not 1 or 2"); + } +} + +function loaded() { + // Click the notification panel's "Allow" button. This should kick + // off updates, which will eventually lead to getting messages from + // the iframe. + var wm = SpecialPowers.Cc["@mozilla.org/appshell/window-mediator;1"]. + getService(SpecialPowers.Ci.nsIWindowMediator); + var win = wm.getMostRecentWindow("navigator:browser"); + var panel = win.PopupNotifications.panel; + panel.firstElementChild.button.click(); +} + +</script> +</pre> +</body> +</html> diff --git a/browser/base/content/test/test_wyciwyg_copying.html b/browser/base/content/test/test_wyciwyg_copying.html new file mode 100644 index 000000000..3a8c3a150 --- /dev/null +++ b/browser/base/content/test/test_wyciwyg_copying.html @@ -0,0 +1,13 @@ +<html> +<body> +<script> + function go() { + var w = window.open(); + w.document.open(); + w.document.write("<html><body>test document</body></html>"); + w.document.close(); + } +</script> +<button id="btn" onclick="go();">test</button> +</body> +</html> diff --git a/browser/base/content/test/title_test.svg b/browser/base/content/test/title_test.svg new file mode 100644 index 000000000..6ab5b2f5c --- /dev/null +++ b/browser/base/content/test/title_test.svg @@ -0,0 +1,59 @@ +<svg width="640px" height="480px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0"> + <title>This is a root SVG element's title</title> + <foreignObject> + <html xmlns="http://www.w3.org/1999/xhtml"> + <body> + <svg xmlns="http://www.w3.org/2000/svg" id="svg1"> + <title>This is a non-root SVG element title</title> + </svg> + </body> + </html> + </foreignObject> + <text id="text1" x="10px" y="32px" font-size="24px"> + This contains only <title> + <title> + + + This is a title + + </title> + </text> + <text id="text2" x="10px" y="96px" font-size="24px"> + This contains only <desc> + <desc>This is a desc</desc> + </text> + <text id="text3" x="10px" y="128px" font-size="24px"> + This contains nothing. + </text> + <a id="link1" xlink:href="#"> + This link contains <title> + <title> + This is a title + </title> + <text id="text4" x="10px" y="192px" font-size="24px"> + </text> + </a> + <a id="link2" xlink:href="#"> + <text x="10px" y="192px" font-size="24px"> + This text contains <title> + <title> + This is a title + </title> + </text> + </a> + <a id="link3" xlink:href="#" xlink:title="This is an xlink:title attribute"> + <text x="10px" y="224px" font-size="24px"> + This link contains <title> & xlink:title attr. + <title>This is a title</title> + </text> + </a> + <a id="link4" xlink:href="#" xlink:title="This is an xlink:title attribute"> + <text x="10px" y="256px" font-size="24px"> + This link contains xlink:title attr. + </text> + </a> + <text id="text5" x="10px" y="160px" font-size="24px" + xlink:title="This is an xlink:title attribute but it isn't on a link" > + This contains nothing. + </text> +</svg> diff --git a/browser/base/content/test/video.ogg b/browser/base/content/test/video.ogg Binary files differnew file mode 100644 index 000000000..ac7ece351 --- /dev/null +++ b/browser/base/content/test/video.ogg diff --git a/browser/base/content/test/zoom_test.html b/browser/base/content/test/zoom_test.html new file mode 100644 index 000000000..bf80490ca --- /dev/null +++ b/browser/base/content/test/zoom_test.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=416661 +--> + <head> + <title>Test for zoom setting</title> + + </head> + <body> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=416661">Bug 416661</a> + <p>Site specific zoom settings should not apply to image documents.</p> + </body> +</html> diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml new file mode 100644 index 000000000..1cec6d20f --- /dev/null +++ b/browser/base/content/urlbarBindings.xml @@ -0,0 +1,2198 @@ +<?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; + 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 flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; + // 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; + // If the value wasn't typed, we know that we decoded the value as + // UTF-8 (see losslessDecodeURI) + if (!this.valueIsTyped) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_URI_IS_UTF8; + 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 }; + if (!this.valueIsTyped) + params.isUTF8 = true; + 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(); + } + ]]></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.contains("/")) { + 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_USE_UTF8); + } 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="_numNoActionsKeys"><![CDATA[ + 0 + ]]></field> + + <method name="_clearNoActions"> + <parameter name="aURL"/> + <body><![CDATA[ + this._numNoActionsKeys = 0; + 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._numNoActionsKeys++; + this.popup.setAttribute("noactions", "true"); + this.removeAttribute("actiontype"); + } + ]]></handler> + + <handler event="keyup"><![CDATA[ + if ((event.keyCode === KeyEvent.DOM_VK_ALT || + event.keyCode === KeyEvent.DOM_VK_SHIFT) && + this._numNoActionsKeys > 0) { + this._numNoActionsKeys--; + if (this._numNoActionsKeys == 0) + this._clearNoActions(); + } + ]]></handler> + + <handler event="blur"><![CDATA[ + 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[ + 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; + + 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" + 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 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="identity-request-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:vbox anonid="identity-deck"> + <xul:vbox flex="1" pack="center"> <!-- 1: add an email --> + <html:input type="email" anonid="email" required="required" size="30"/> + <xul:description anonid="newidentitydesc"/> + <xul:spacer flex="1"/> + <xul:label class="text-link custom-link small-margin" anonid="chooseemail" hidden="true"/> + </xul:vbox> + <xul:vbox flex="1" hidden="true"> <!-- 2: choose an email --> + <xul:description anonid="chooseidentitydesc"/> + <xul:radiogroup anonid="identities"> + </xul:radiogroup> + <xul:label class="text-link custom-link" anonid="newemail"/> + </xul:vbox> + </xul:vbox> + <xul:hbox class="popup-notification-button-container" + pack="end" align="center"> + <xul:label anonid="tos" class="text-link" hidden="true"/> + <xul:label anonid="privacypolicy" class="text-link" hidden="true"/> + <xul:spacer flex="1"/> + <xul:image anonid="throbber" src="chrome://browser/skin/tabbrowser/loading.png" + style="visibility:hidden" width="16" height="16"/> + <xul:button anonid="button" + type="menu-button" + class="popup-notification-menubutton" + 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" + 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 popup-notification-closebutton tabbable" + xbl:inherits="oncommand=closebuttoncommand" + tooltiptext="&closeNotification.tooltip;"/> + </xul:vbox> + </content> + <implementation> + <constructor><![CDATA[ + // this.notification.options.identity is used to pass identity-specific info to the binding + let origin = this.identity.origin + + // Populate text + this.emailField.placeholder = gNavigatorBundle. + getString("identity.newIdentity.email.placeholder"); + this.newIdentityDesc.textContent = gNavigatorBundle.getFormattedString( + "identity.newIdentity.description", [origin]); + this.chooseIdentityDesc.textContent = gNavigatorBundle.getFormattedString( + "identity.chooseIdentity.description", [origin]); + + // Show optional terms of service and privacy policy links + this._populateLink(this.identity.termsOfService, "tos", "identity.termsOfService"); + this._populateLink(this.identity.privacyPolicy, "privacypolicy", "identity.privacyPolicy"); + + // Populate the list of identities to choose from. The origin is used to provide + // better suggestions. + let identities = this.SignInToWebsiteUX.getIdentitiesForSite(origin); + + this._populateIdentityList(identities); + + if (typeof this.step == "undefined") { + // First opening of this notification + // Show the add email pane (0) if there are no existing identities otherwise show the list + this.step = "result" in identities && identities.result.length ? 1 : 0; + } else { + // Already opened so restore previous state + if (this.identity.typedEmail) { + this.emailField.value = this.identity.typedEmail; + } + if (this.identity.selected) { + // If the user already chose an identity then update the UI to reflect that + this.onIdentitySelected(); + } + // Update the view for the step + this.step = this.step; + } + + // Fire notification with the chosen identity when main button is clicked + this.button.addEventListener("command", this._onButtonCommand.bind(this), true); + + // Do the same if enter is pressed in the email field + this.emailField.addEventListener("keypress", function emailFieldKeypress(aEvent) { + if (aEvent.keyCode != aEvent.DOM_VK_RETURN) + return; + this._onButtonCommand(aEvent); + }.bind(this)); + + this.addEmailLink.value = gNavigatorBundle.getString("identity.newIdentity.label"); + this.addEmailLink.accessKey = gNavigatorBundle.getString("identity.newIdentity.accessKey"); + this.addEmailLink.addEventListener("click", function addEmailClick(evt) { + this.step = 0; + }.bind(this)); + + this.chooseEmailLink.value = gNavigatorBundle.getString("identity.chooseIdentity.label"); + this.chooseEmailLink.hidden = !("result" in identities && identities.result.length); + this.chooseEmailLink.addEventListener("click", function chooseEmailClick(evt) { + this.step = 1; + }.bind(this)); + + this.emailField.addEventListener("blur", function onEmailBlur() { + this.identity.typedEmail = this.emailField.value; + }.bind(this)); + ]]></constructor> + + <field name="SignInToWebsiteUX" readonly="true"> + let sitw = {}; + Components.utils.import("resource:///modules/SignInToWebsite.jsm", sitw); + sitw.SignInToWebsiteUX; + </field> + + <field name="newIdentityDesc" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "newidentitydesc"); + </field> + + <field name="chooseIdentityDesc" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "chooseidentitydesc"); + </field> + + <field name="identityList" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "identities"); + </field> + + <field name="emailField" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "email"); + </field> + + <field name="addEmailLink" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "newemail"); + </field> + + <field name="chooseEmailLink" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "chooseemail"); + </field> + + <field name="throbber" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "throbber"); + </field> + + <field name="identity" readonly="true"> + this.notification.options.identity; + </field> + + <!-- persist the state on the identity object so we can re-create the + notification state upon re-opening --> + <property name="step"> + <getter> + return this.identity.step; + </getter> + <setter><![CDATA[ + let deck = document.getAnonymousElementByAttribute(this, "anonid", "identity-deck"); + for (let i = 0; i < deck.children.length; i++) { + deck.children[i].hidden = (val != i); + } + this.identity.step = val; + switch (val) { + case 0: + this.emailField.focus(); + break; + }]]> + </setter> + </property> + + <method name="onIdentitySelected"> + <body><![CDATA[ + this.throbber.style.visibility = "visible"; + this.button.disabled = true; + this.emailField.value = this.identity.selected + this.emailField.disabled = true; + this.identityList.disabled = true; + ]]></body> + </method> + + <method name="_populateLink"> + <parameter name="aURL"/> + <parameter name="aLinkId"/> + <parameter name="aStringId"/> + <body><![CDATA[ + if (aURL) { + // Show optional link to aURL + let link = document.getAnonymousElementByAttribute(this, "anonid", aLinkId); + link.value = gNavigatorBundle.getString(aStringId); + link.href = aURL; + link.hidden = false; + } + ]]></body> + </method> + + <method name="_populateIdentityList"> + <parameter name="aIdentities"/> + <body><![CDATA[ + let foundLastUsed = false; + let lastUsed = this.identity.selected || aIdentities.lastUsed; + for (let id in aIdentities.result) { + let label = aIdentities.result[id]; + let opt = this.identityList.appendItem(label); + if (label == lastUsed) { + this.identityList.selectedItem = opt; + foundLastUsed = true; + } + } + if (!foundLastUsed) { + this.identityList.selectedIndex = -1; + } + ]]></body> + </method> + + <method name="_onButtonCommand"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.target != aEvent.currentTarget) + return; + let chosenId; + switch (this.step) { + case 0: + aEvent.stopPropagation(); + if (!this.emailField.validity.valid) { + this.emailField.focus(); + return; + } + chosenId = this.emailField.value; + break; + case 1: + aEvent.stopPropagation(); + let selectedItem = this.identityList.selectedItem + chosenId = selectedItem ? selectedItem.label : null; + if (!chosenId) + return; + break; + default: + throw new Error("Unknown case"); + return; + } + // Actually select the identity + this.SignInToWebsiteUX.selectIdentity(this.identity.rpId, chosenId); + this.identity.selected = chosenId; + this.onIdentitySelected(); + ]]></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 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" + 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://support.mozilla.org/kb/how-do-i-enable-add-sync"; + } + return "https://services.mozilla.com/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) { + 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/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js new file mode 100644 index 000000000..2271d9793 --- /dev/null +++ b/browser/base/content/utilityOverlay.js @@ -0,0 +1,686 @@ +# -*- 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" 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"; +} + +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; + // Currently, this parameter works only for where=="tab" or "current" + var aIsUTF8 = params.isUTF8; + var aInitiatingDoc = params.initiatingDoc; + var aIsPrivate = params.private; + + 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") { + 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); + 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; + if (aDisallowInheritPrincipal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; + if (aIsUTF8) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_URI_IS_UTF8; + 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, + isUTF8: aIsUTF8}); + 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) +{ + if (Services.prefs.getBoolPref("browser.preferences.inContent")) { + openUILinkIn("about:preferences", "tab"); + } else { + 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; + } + + 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"); +} + +#ifdef MOZ_SERVICES_HEALTHREPORT +/** + * Opens the troubleshooting information (about:support) page for this version + * of the application. + */ +function openHealthReport() +{ + openUILinkIn("about:healthreport", "tab"); +} +#endif + +/** + * Opens the feedback page for this version of the application. + */ +function openFeedbackPage() +{ + openUILinkIn("http://forum.palemoon.org", "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/browser/base/content/viewSourceOverlay.xul b/browser/base/content/viewSourceOverlay.xul new file mode 100644 index 000000000..8b40ddfd2 --- /dev/null +++ b/browser/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/browser/base/content/web-panels.js b/browser/base/content/web-panels.js new file mode 100644 index 000000000..b784d0a1e --- /dev/null +++ b/browser/base/content/web-panels.js @@ -0,0 +1,103 @@ +/* -*- 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/browser/base/content/web-panels.xul b/browser/base/content/web-panels.xul new file mode 100644 index 000000000..8be808cca --- /dev/null +++ b/browser/base/content/web-panels.xul @@ -0,0 +1,71 @@ +<?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 % palemoonDTD SYSTEM "chrome://browser/locale/palemoon.dtd" > +%palemoonDTD; +<!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();"/> + </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/browser/base/content/win6BrowserOverlay.xul b/browser/base/content/win6BrowserOverlay.xul new file mode 100644 index 000000000..a69e3f6bd --- /dev/null +++ b/browser/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> diff --git a/browser/base/jar.mn b/browser/base/jar.mn new file mode 100644 index 000000000..f178a74c2 --- /dev/null +++ b/browser/base/jar.mn @@ -0,0 +1,143 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +browser.jar: +% content browser %content/browser/ contentaccessible=yes +#ifdef XP_MACOSX +% overlay chrome://mozapps/content/downloads/downloads.xul chrome://browser/content/downloadManagerOverlay.xul +% overlay chrome://global/content/console.xul chrome://browser/content/jsConsoleOverlay.xul +% overlay chrome://mozapps/content/update/updates.xul chrome://browser/content/softwareUpdateOverlay.xul +#endif +#ifdef XP_WIN +% overlay chrome://browser/content/browser.xul chrome://browser/content/win6BrowserOverlay.xul os=WINNT osversion>=6 +#endif +% overlay chrome://global/content/viewSource.xul chrome://browser/content/viewSourceOverlay.xul +% overlay chrome://global/content/viewPartialSource.xul chrome://browser/content/viewSourceOverlay.xul +% style chrome://global/content/customizeToolbar.xul chrome://browser/content/browser.css +% style chrome://global/content/customizeToolbar.xul chrome://browser/skin/ +* content/browser/aboutDialog.xul (content/aboutDialog.xul) +* content/browser/aboutDialog.js (content/aboutDialog.js) + content/browser/aboutDialog.css (content/aboutDialog.css) + content/browser/aboutRobots.xhtml (content/aboutRobots.xhtml) + content/browser/abouthome/aboutHome.xhtml (content/abouthome/aboutHome.xhtml) + content/browser/abouthome/aboutHome.js (content/abouthome/aboutHome.js) +* content/browser/abouthome/aboutHome.css (content/abouthome/aboutHome.css) + content/browser/abouthome/noise.png (content/abouthome/noise.png) + content/browser/abouthome/snippet1.png (content/abouthome/snippet1.png) + content/browser/abouthome/snippet2.png (content/abouthome/snippet2.png) + content/browser/abouthome/downloads.png (content/abouthome/downloads.png) + content/browser/abouthome/bookmarks.png (content/abouthome/bookmarks.png) + content/browser/abouthome/history.png (content/abouthome/history.png) + content/browser/abouthome/apps.png (content/abouthome/apps.png) + content/browser/abouthome/addons.png (content/abouthome/addons.png) + content/browser/abouthome/sync.png (content/abouthome/sync.png) + content/browser/abouthome/settings.png (content/abouthome/settings.png) + content/browser/abouthome/restore.png (content/abouthome/restore.png) + content/browser/abouthome/restore-large.png (content/abouthome/restore-large.png) + content/browser/abouthome/mozilla.png (content/abouthome/mozilla.png) + content/browser/abouthome/snippet1@2x.png (content/abouthome/snippet1@2x.png) + content/browser/abouthome/snippet2@2x.png (content/abouthome/snippet2@2x.png) + content/browser/abouthome/downloads@2x.png (content/abouthome/downloads@2x.png) + content/browser/abouthome/bookmarks@2x.png (content/abouthome/bookmarks@2x.png) + content/browser/abouthome/history@2x.png (content/abouthome/history@2x.png) + content/browser/abouthome/apps@2x.png (content/abouthome/apps@2x.png) + content/browser/abouthome/addons@2x.png (content/abouthome/addons@2x.png) + content/browser/abouthome/sync@2x.png (content/abouthome/sync@2x.png) + content/browser/abouthome/settings@2x.png (content/abouthome/settings@2x.png) + content/browser/abouthome/restore@2x.png (content/abouthome/restore@2x.png) + content/browser/abouthome/restore-large@2x.png (content/abouthome/restore-large@2x.png) + content/browser/abouthome/mozilla@2x.png (content/abouthome/mozilla@2x.png) +#ifdef MOZ_SERVICES_HEALTHREPORT + content/browser/abouthealthreport/abouthealth.xhtml (content/abouthealthreport/abouthealth.xhtml) + content/browser/abouthealthreport/abouthealth.js (content/abouthealthreport/abouthealth.js) + content/browser/abouthealthreport/abouthealth.css (content/abouthealthreport/abouthealth.css) +#endif + content/browser/aboutRobots-icon.png (content/aboutRobots-icon.png) + content/browser/aboutRobots-widget-left.png (content/aboutRobots-widget-left.png) + content/browser/aboutSocialError.xhtml (content/aboutSocialError.xhtml) +* content/browser/browser.css (content/browser.css) +* content/browser/browser-title.css (content/browser-title.css) +* content/browser/browser.js (content/browser.js) +* content/browser/browser.xul (content/browser.xul) +* content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml) +* content/browser/chatWindow.xul (content/chatWindow.xul) + content/browser/content.js (content/content.js) + content/browser/imagedocument.png (content/imagedocument.png) +* content/browser/padlock.xul (content/padlock.xul) +* content/browser/padlock.js (content/padlock.js) +* content/browser/padlock.css (content/padlock.css) + content/browser/padlock_mod_ev.png (content/padlock_mod_ev.png) + content/browser/padlock_mod_https.png (content/padlock_mod_https.png) + content/browser/padlock_mod_low.png (content/padlock_mod_low.png) + content/browser/padlock_mod_broken.png (content/padlock_mod_broken.png) + content/browser/padlock_classic_ev.png (content/padlock_classic_ev.png) + content/browser/padlock_classic_https.png (content/padlock_classic_https.png) + content/browser/padlock_classic_broken.png (content/padlock_classic_broken.png) + content/browser/newtab/newTab.xul (content/newtab/newTab.xul) +* content/browser/newtab/newTab.js (content/newtab/newTab.js) + content/browser/newtab/newTab.css (content/newtab/newTab.css) +* content/browser/pageinfo/pageInfo.xul (content/pageinfo/pageInfo.xul) + content/browser/pageinfo/pageInfo.js (content/pageinfo/pageInfo.js) + content/browser/pageinfo/pageInfo.css (content/pageinfo/pageInfo.css) + content/browser/pageinfo/pageInfo.xml (content/pageinfo/pageInfo.xml) + content/browser/pageinfo/feeds.js (content/pageinfo/feeds.js) + content/browser/pageinfo/feeds.xml (content/pageinfo/feeds.xml) + content/browser/pageinfo/permissions.js (content/pageinfo/permissions.js) + content/browser/pageinfo/security.js (content/pageinfo/security.js) +#ifdef MOZ_SERVICES_SYNC + content/browser/sync/aboutSyncTabs.xul (content/sync/aboutSyncTabs.xul) + content/browser/sync/aboutSyncTabs.js (content/sync/aboutSyncTabs.js) + content/browser/sync/aboutSyncTabs.css (content/sync/aboutSyncTabs.css) + content/browser/sync/aboutSyncTabs-bindings.xml (content/sync/aboutSyncTabs-bindings.xml) +* content/browser/sync/setup.xul (content/sync/setup.xul) + content/browser/sync/addDevice.js (content/sync/addDevice.js) + content/browser/sync/addDevice.xul (content/sync/addDevice.xul) +* content/browser/sync/setup.js (content/sync/setup.js) + content/browser/sync/genericChange.xul (content/sync/genericChange.xul) + content/browser/sync/genericChange.js (content/sync/genericChange.js) + content/browser/sync/key.xhtml (content/sync/key.xhtml) + content/browser/sync/notification.xml (content/sync/notification.xml) + content/browser/sync/quota.xul (content/sync/quota.xul) + content/browser/sync/quota.js (content/sync/quota.js) + content/browser/sync/utils.js (content/sync/utils.js) + content/browser/sync/progress.js (content/sync/progress.js) + content/browser/sync/progress.xhtml (content/sync/progress.xhtml) +#endif + content/browser/openLocation.js (content/openLocation.js) + content/browser/openLocation.xul (content/openLocation.xul) +* content/browser/safeMode.css (content/safeMode.css) +* content/browser/safeMode.js (content/safeMode.js) +* content/browser/safeMode.xul (content/safeMode.xul) +* content/browser/sanitize.js (content/sanitize.js) +* content/browser/sanitize.xul (content/sanitize.xul) +* content/browser/sanitizeDialog.js (content/sanitizeDialog.js) + content/browser/sanitizeDialog.css (content/sanitizeDialog.css) + content/browser/tabbrowser.css (content/tabbrowser.css) +* content/browser/tabbrowser.xml (content/tabbrowser.xml) +* content/browser/urlbarBindings.xml (content/urlbarBindings.xml) +* content/browser/utilityOverlay.js (content/utilityOverlay.js) + content/browser/web-panels.js (content/web-panels.js) +* content/browser/web-panels.xul (content/web-panels.xul) +* content/browser/baseMenuOverlay.xul (content/baseMenuOverlay.xul) +* content/browser/nsContextMenu.js (content/nsContextMenu.js) +# XXX: We should exclude this one as well (bug 71895) +* content/browser/hiddenWindow.xul (content/hiddenWindow.xul) +#ifdef XP_MACOSX +* content/browser/macBrowserOverlay.xul (content/macBrowserOverlay.xul) +* content/browser/downloadManagerOverlay.xul (content/downloadManagerOverlay.xul) +* content/browser/jsConsoleOverlay.xul (content/jsConsoleOverlay.xul) +* content/browser/softwareUpdateOverlay.xul (content/softwareUpdateOverlay.xul) +#endif +* content/browser/viewSourceOverlay.xul (content/viewSourceOverlay.xul) +#ifdef XP_WIN + content/browser/win6BrowserOverlay.xul (content/win6BrowserOverlay.xul) +#endif + content/browser/socialchat.xml (content/socialchat.xml) +# the following files are browser-specific overrides +* content/browser/license.html (/toolkit/content/license.html) +% override chrome://global/content/license.html chrome://browser/content/license.html +#ifdef MOZ_SAFE_BROWSING + content/browser/report-phishing-overlay.xul (content/report-phishing-overlay.xul) + content/browser/blockedSite.xhtml (content/blockedSite.xhtml) +% overlay chrome://browser/content/browser.xul chrome://browser/content/report-phishing-overlay.xul +#endif diff --git a/browser/base/moz.build b/browser/base/moz.build new file mode 100644 index 000000000..d13541370 --- /dev/null +++ b/browser/base/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +TEST_DIRS += ['content/test'] |