summaryrefslogtreecommitdiffstats
path: root/mobile/android/chrome/content/browser.js
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/chrome/content/browser.js')
-rw-r--r--mobile/android/chrome/content/browser.js6999
1 files changed, 6999 insertions, 0 deletions
diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js
new file mode 100644
index 000000000..b00e1af15
--- /dev/null
+++ b/mobile/android/chrome/content/browser.js
@@ -0,0 +1,6999 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/AsyncPrefs.jsm");
+Cu.import("resource://gre/modules/DelayedInit.jsm");
+
+if (AppConstants.ACCESSIBILITY) {
+ XPCOMUtils.defineLazyModuleGetter(this, "AccessFu",
+ "resource://gre/modules/accessibility/AccessFu.jsm");
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "SpatialNavigation",
+ "resource://gre/modules/SpatialNavigation.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadNotifications",
+ "resource://gre/modules/DownloadNotifications.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JNI",
+ "resource://gre/modules/JNI.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
+ "resource://gre/modules/UITelemetry.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
+ "resource://gre/modules/UserAgentOverrides.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
+ "resource://gre/modules/LoginManagerContent.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
+ "resource://gre/modules/LoginManagerParent.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
+ "resource://gre/modules/SafeBrowsing.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+ "resource://gre/modules/BrowserUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
+ "resource://gre/modules/Sanitizer.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
+ "resource://gre/modules/HelperApps.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
+ "resource://gre/modules/SSLExceptions.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
+ "resource://gre/modules/FormHistory.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+XPCOMUtils.defineLazyServiceGetter(this, "Profiler",
+ "@mozilla.org/tools/profiler;1",
+ "nsIProfiler");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
+ "resource://gre/modules/SimpleServiceDiscovery.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
+ "resource://gre/modules/CharsetMenu.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetErrorHelper",
+ "resource://gre/modules/NetErrorHelper.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
+ "resource://gre/modules/PermissionsUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+ "resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences",
+ "resource://gre/modules/SharedPreferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Notifications",
+ "resource://gre/modules/Notifications.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "WebsiteMetadata", "resource://gre/modules/WebsiteMetadata.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "FontEnumerator",
+ "@mozilla.org/gfx/fontenumerator;1",
+ "nsIFontEnumerator");
+
+var lazilyLoadedBrowserScripts = [
+ ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
+ ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
+ ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
+ ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
+ ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
+ ["Linkifier", "chrome://browser/content/Linkify.js"],
+ ["CastingApps", "chrome://browser/content/CastingApps.js"],
+ ["RemoteDebugger", "chrome://browser/content/RemoteDebugger.js"],
+];
+if (!AppConstants.RELEASE_OR_BETA) {
+ lazilyLoadedBrowserScripts.push(
+ ["WebcompatReporter", "chrome://browser/content/WebcompatReporter.js"]);
+}
+
+lazilyLoadedBrowserScripts.forEach(function (aScript) {
+ let [name, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+});
+
+var lazilyLoadedObserverScripts = [
+ ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
+ ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
+ ["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
+ ["PermissionsHelper", ["Permissions:Check", "Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
+ ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
+ ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
+ ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
+ ["Reader", ["Reader:AddToCache", "Reader:RemoveFromCache"], "chrome://browser/content/Reader.js"],
+ ["PrintHelper", ["Print:PDF"], "chrome://browser/content/PrintHelper.js"],
+];
+
+lazilyLoadedObserverScripts.push(
+["ActionBarHandler", ["TextSelection:Get", "TextSelection:Action", "TextSelection:End"],
+ "chrome://browser/content/ActionBarHandler.js"]
+);
+
+if (AppConstants.MOZ_WEBRTC) {
+ lazilyLoadedObserverScripts.push(
+ ["WebrtcUI", ["getUserMedia:request",
+ "PeerConnection:request",
+ "recording-device-events",
+ "VideoCapture:Paused",
+ "VideoCapture:Resumed"], "chrome://browser/content/WebrtcUI.js"])
+}
+
+lazilyLoadedObserverScripts.forEach(function (aScript) {
+ let [name, notifications, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+ let observer = (s, t, d) => {
+ Services.obs.removeObserver(observer, t);
+ Services.obs.addObserver(window[name], t, false);
+ window[name].observe(s, t, d); // Explicitly notify new observer
+ };
+ notifications.forEach((notification) => {
+ Services.obs.addObserver(observer, notification, false);
+ });
+});
+
+// Lazily-loaded browser scripts that use message listeners.
+[
+ ["Reader", [
+ ["Reader:AddToCache", false],
+ ["Reader:RemoveFromCache", false],
+ ["Reader:ArticleGet", false],
+ ["Reader:DropdownClosed", true], // 'true' allows us to survive mid-air cycle-collection.
+ ["Reader:DropdownOpened", false],
+ ["Reader:FaviconRequest", false],
+ ["Reader:ToolbarHidden", false],
+ ["Reader:SystemUIVisibility", false],
+ ["Reader:UpdateReaderButton", false],
+ ], "chrome://browser/content/Reader.js"],
+].forEach(aScript => {
+ let [name, messages, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+
+ let mm = window.getGroupMessageManager("browsers");
+ let listener = (message) => {
+ mm.removeMessageListener(message.name, listener);
+ let listenAfterClose = false;
+ for (let [name, laClose] of messages) {
+ if (message.name === name) {
+ listenAfterClose = laClose;
+ break;
+ }
+ }
+
+ mm.addMessageListener(message.name, window[name], listenAfterClose);
+ window[name].receiveMessage(message);
+ };
+
+ messages.forEach((message) => {
+ let [name, listenAfterClose] = message;
+ mm.addMessageListener(name, listener, listenAfterClose);
+ });
+});
+
+// Lazily-loaded JS modules that use observer notifications
+[
+ ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
+ "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
+].forEach(module => {
+ let [name, notifications, resource] = module;
+ XPCOMUtils.defineLazyModuleGetter(this, name, resource);
+ let observer = (s, t, d) => {
+ Services.obs.removeObserver(observer, t);
+ Services.obs.addObserver(this[name], t, false);
+ this[name].observe(s, t, d); // Explicitly notify new observer
+ };
+ notifications.forEach(notification => {
+ Services.obs.addObserver(observer, notification, false);
+ });
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
+ "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+ "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
+ "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
+
+XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
+ "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
+
+if (AppConstants.MOZ_WEBRTC) {
+ XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
+ "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService");
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+ "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
+
+// Define the "dump" function as a binding of the Log.d function so it specifies
+// the "debug" priority and a log tag.
+function dump(msg) {
+ Log.d("Browser", msg);
+}
+
+const kStateActive = 0x00000001; // :active pseudoclass for elements
+
+const kXLinkNamespace = "http://www.w3.org/1999/xlink";
+
+function fuzzyEquals(a, b) {
+ return (Math.abs(a - b) < 1e-6);
+}
+
+XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
+ let ContentAreaUtils = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
+ return ContentAreaUtils;
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Point", "resource://gre/modules/Geometry.jsm");
+
+function resolveGeckoURI(aURI) {
+ if (!aURI)
+ throw "Can't resolve an empty uri";
+
+ if (aURI.startsWith("chrome://")) {
+ let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
+ return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
+ } else if (aURI.startsWith("resource://")) {
+ let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ return handler.resolveURI(Services.io.newURI(aURI, null, null));
+ }
+ return aURI;
+}
+
+/**
+ * Cache of commonly used string bundles.
+ */
+var Strings = {
+ init: function () {
+ XPCOMUtils.defineLazyGetter(Strings, "brand", () => Services.strings.createBundle("chrome://branding/locale/brand.properties"));
+ XPCOMUtils.defineLazyGetter(Strings, "browser", () => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
+ XPCOMUtils.defineLazyGetter(Strings, "reader", () => Services.strings.createBundle("chrome://global/locale/aboutReader.properties"));
+ },
+
+ flush: function () {
+ Services.strings.flushBundles();
+ this.init();
+ },
+};
+
+Strings.init();
+
+const kFormHelperModeDisabled = 0;
+const kFormHelperModeEnabled = 1;
+const kFormHelperModeDynamic = 2; // disabled on tablets
+const kMaxHistoryListSize = 50;
+
+function InitLater(fn, object, name) {
+ return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */);
+}
+
+var BrowserApp = {
+ _tabs: [],
+ _selectedTab: null,
+
+ get isTablet() {
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ delete this.isTablet;
+ return this.isTablet = sysInfo.get("tablet");
+ },
+
+ get isOnLowMemoryPlatform() {
+ let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
+ delete this.isOnLowMemoryPlatform;
+ return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
+ },
+
+ deck: null,
+
+ startup: function startup() {
+ window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
+ dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
+ Services.obs.notifyObservers(this.browser, "BrowserChrome:Ready", null);
+
+ this.deck = document.getElementById("browsers");
+
+ BrowserEventHandler.init();
+
+ ViewportHandler.init();
+
+ Services.androidBridge.browserApp = this;
+
+ Services.obs.addObserver(this, "Locale:OS", false);
+ Services.obs.addObserver(this, "Locale:Changed", false);
+ Services.obs.addObserver(this, "Tab:Load", false);
+ Services.obs.addObserver(this, "Tab:Selected", false);
+ Services.obs.addObserver(this, "Tab:Closed", false);
+ Services.obs.addObserver(this, "Session:Back", false);
+ Services.obs.addObserver(this, "Session:Forward", false);
+ Services.obs.addObserver(this, "Session:Navigate", false);
+ Services.obs.addObserver(this, "Session:Reload", false);
+ Services.obs.addObserver(this, "Session:Stop", false);
+ Services.obs.addObserver(this, "SaveAs:PDF", false);
+ Services.obs.addObserver(this, "Browser:Quit", false);
+ Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
+ Services.obs.addObserver(this, "Sanitize:ClearData", false);
+ Services.obs.addObserver(this, "FullScreen:Exit", false);
+ Services.obs.addObserver(this, "Passwords:Init", false);
+ Services.obs.addObserver(this, "FormHistory:Init", false);
+ Services.obs.addObserver(this, "android-get-pref", false);
+ Services.obs.addObserver(this, "android-set-pref", false);
+ Services.obs.addObserver(this, "gather-telemetry", false);
+ Services.obs.addObserver(this, "keyword-search", false);
+ Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
+ Services.obs.addObserver(this, "Fonts:Reload", false);
+ Services.obs.addObserver(this, "Vibration:Request", false);
+
+ Messaging.addListener(this.getHistory.bind(this), "Session:GetHistory");
+
+ window.addEventListener("fullscreen", function() {
+ Messaging.sendRequest({
+ type: window.fullScreen ? "ToggleChrome:Hide" : "ToggleChrome:Show"
+ });
+ }, false);
+
+ window.addEventListener("fullscreenchange", (e) => {
+ // This event gets fired on the document and its entire ancestor chain
+ // of documents. When enabling fullscreen, it is fired on the top-level
+ // document first and goes down; when disabling the order is reversed
+ // (per spec). This means the last event on enabling will be for the innermost
+ // document, which will have fullscreenElement set correctly.
+ let doc = e.target;
+ Messaging.sendRequest({
+ type: doc.fullscreenElement ? "DOMFullScreen:Start" : "DOMFullScreen:Stop",
+ rootElement: doc.fullscreenElement == doc.documentElement
+ });
+
+ if (this.fullscreenTransitionTab) {
+ // Tab selection has changed during a fullscreen transition, handle it now.
+ let tab = this.fullscreenTransitionTab;
+ this.fullscreenTransitionTab = null;
+ this._handleTabSelected(tab);
+ }
+ }, false);
+
+ NativeWindow.init();
+ FormAssistant.init();
+ IndexedDB.init();
+ XPInstallObserver.init();
+ CharacterEncoding.init();
+ ActivityObserver.init();
+ RemoteDebugger.init();
+ UserAgentOverrides.init();
+ DesktopUserAgent.init();
+ Distribution.init();
+ Tabs.init();
+ SearchEngines.init();
+ Experiments.init();
+
+ // XXX maybe we don't do this if the launch was kicked off from external
+ Services.io.offline = false;
+
+ // Broadcast a UIReady message so add-ons know we are finished with startup
+ let event = document.createEvent("Events");
+ event.initEvent("UIReady", true, false);
+ window.dispatchEvent(event);
+
+ if (this._startupStatus) {
+ this.onAppUpdated();
+ }
+
+ if (!ParentalControls.isAllowed(ParentalControls.INSTALL_EXTENSION)) {
+ // Disable extension installs
+ Services.prefs.setIntPref("extensions.enabledScopes", 1);
+ Services.prefs.setIntPref("extensions.autoDisableScopes", 1);
+ Services.prefs.setBoolPref("xpinstall.enabled", false);
+ } else if (ParentalControls.parentalControlsEnabled) {
+ Services.prefs.clearUserPref("extensions.enabledScopes");
+ Services.prefs.clearUserPref("extensions.autoDisableScopes");
+ Services.prefs.setBoolPref("xpinstall.enabled", true);
+ }
+
+ if (ParentalControls.parentalControlsEnabled) {
+ let isBlockListEnabled = ParentalControls.isAllowed(ParentalControls.BLOCK_LIST);
+ Services.prefs.setBoolPref("browser.safebrowsing.forbiddenURIs.enabled", isBlockListEnabled);
+ Services.prefs.setBoolPref("browser.safebrowsing.allowOverride", !isBlockListEnabled);
+
+ let isTelemetryEnabled = ParentalControls.isAllowed(ParentalControls.TELEMETRY);
+ Services.prefs.setBoolPref("toolkit.telemetry.enabled", isTelemetryEnabled);
+
+ let isHealthReportEnabled = ParentalControls.isAllowed(ParentalControls.HEALTH_REPORT);
+ SharedPreferences.forApp().setBoolPref("android.not_a_preference.healthreport.uploadEnabled", isHealthReportEnabled);
+ }
+
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ if (sysInfo.get("version") < 16) {
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setBoolPref("media.autoplay.enabled", false);
+ }
+
+ InitLater(() => {
+ // The order that context menu items are added is important
+ // Make sure the "Open in App" context menu item appears at the bottom of the list
+ this.initContextMenu();
+ ExternalApps.init();
+ }, NativeWindow, "contextmenus");
+
+ if (AppConstants.ACCESSIBILITY) {
+ InitLater(() => AccessFu.attach(window), window, "AccessFu");
+ }
+
+ // Don't delay loading content.js because when we restore reader mode tabs,
+ // we require the reader mode scripts in content.js right away.
+ let mm = window.getGroupMessageManager("browsers");
+ mm.loadFrameScript("chrome://browser/content/content.js", true);
+
+ // We can't delay registering WebChannel listeners: if the first page is
+ // about:accounts, which can happen when starting the Firefox Account flow
+ // from the first run experience, or via the Firefox Account Status
+ // Activity, we can and do miss messages from the fxa-content-server.
+ // However, we never allow suitably restricted profiles from listening to
+ // fxa-content-server messages.
+ if (ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
+ console.log("browser.js: loading Firefox Accounts WebChannel");
+ Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
+ EnsureFxAccountsWebChannel();
+ } else {
+ console.log("browser.js: not loading Firefox Accounts WebChannel; this profile cannot connect to Firefox Accounts.");
+ }
+
+ // Notify Java that Gecko has loaded.
+ Messaging.sendRequest({ type: "Gecko:Ready" });
+
+ this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
+ BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
+
+ InitLater(() => Cu.import("resource://gre/modules/NotificationDB.jsm"));
+ InitLater(() => Cu.import("resource://gre/modules/PresentationDeviceInfoManager.jsm"));
+
+ InitLater(() => Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""));
+ InitLater(() => Messaging.sendRequest({ type: "Gecko:DelayedStartup" }));
+
+ if (!AppConstants.RELEASE_OR_BETA) {
+ InitLater(() => WebcompatReporter.init());
+ }
+
+ // Collect telemetry data.
+ // We do this at startup because we want to move away from "gather-telemetry" (bug 1127907)
+ InitLater(() => {
+ Telemetry.addData("FENNEC_TRACKING_PROTECTION_STATE", parseInt(BrowserApp.getTrackingProtectionState()));
+ Telemetry.addData("ZOOMED_VIEW_ENABLED", Services.prefs.getBoolPref("ui.zoomedview.enabled"));
+ });
+
+ InitLater(() => LightWeightThemeWebInstaller.init());
+ InitLater(() => SpatialNavigation.init(BrowserApp.deck, null), window, "SpatialNavigation");
+ InitLater(() => CastingApps.init(), window, "CastingApps");
+ InitLater(() => Services.search.init(), Services, "search");
+ InitLater(() => DownloadNotifications.init(), window, "DownloadNotifications");
+
+ // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
+ InitLater(() => SafeBrowsing.init(), window, "SafeBrowsing");
+
+ InitLater(() => Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager));
+ InitLater(() => LoginManagerParent.init(), window, "LoginManagerParent");
+
+ }, false);
+
+ // Pass caret StateChanged events to ActionBarHandler.
+ window.addEventListener("mozcaretstatechanged", e => {
+ ActionBarHandler.caretStateChangedHandler(e);
+ }, /* useCapture = */ true, /* wantsUntrusted = */ false);
+ },
+
+ get _startupStatus() {
+ delete this._startupStatus;
+
+ let savedMilestone = null;
+ try {
+ savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
+ } catch (e) {
+ }
+ let ourMilestone = AppConstants.MOZ_APP_VERSION;
+ this._startupStatus = "";
+ if (ourMilestone != savedMilestone) {
+ Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone);
+ this._startupStatus = savedMilestone ? "upgrade" : "new";
+ }
+
+ return this._startupStatus;
+ },
+
+ /**
+ * Pass this a locale string, such as "fr" or "es_ES".
+ */
+ setLocale: function (locale) {
+ console.log("browser.js: requesting locale set: " + locale);
+ Messaging.sendRequest({ type: "Locale:Set", locale: locale });
+ },
+
+ initContextMenu: function () {
+ // We pass a thunk in place of a raw label string. This allows the
+ // context menu to automatically accommodate locale changes without
+ // having to be rebuilt.
+ let stringGetter = name => () => Strings.browser.GetStringFromName(name);
+
+ // TODO: These should eventually move into more appropriate classes
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.openInNewTab"),
+ NativeWindow.contextmenus.linkOpenableNonPrivateContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
+ UITelemetry.addEvent("loadurl.1", "contextmenu", null);
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
+ let tab = BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
+
+ let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened");
+ let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
+ let buttonLabel = Strings.browser.GetStringFromName("newtabpopup.switch");
+
+ Snackbars.show(label, Snackbars.LENGTH_LONG, {
+ action: {
+ label: buttonLabel,
+ callback: () => { BrowserApp.selectTab(tab); },
+ }
+ });
+ });
+
+ let showOpenInPrivateTab = true;
+ if ("@mozilla.org/parental-controls-service;1" in Cc) {
+ let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
+ showOpenInPrivateTab = pc.isAllowed(Ci.nsIParentalControlsService.PRIVATE_BROWSING);
+ }
+
+ if (showOpenInPrivateTab) {
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.openInPrivateTab"),
+ NativeWindow.contextmenus.linkOpenableContext,
+ function (aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
+ UITelemetry.addEvent("loadurl.1", "contextmenu", null);
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
+ let tab = BrowserApp.addTab(url, {selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true});
+
+ let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened");
+ let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
+ let buttonLabel = Strings.browser.GetStringFromName("newtabpopup.switch");
+ Snackbars.show(label, Snackbars.LENGTH_LONG, {
+ action: {
+ label: buttonLabel,
+ callback: () => { BrowserApp.selectTab(tab); },
+ }
+ });
+ });
+ }
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyLink"),
+ NativeWindow.contextmenus.linkCopyableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyEmailAddress"),
+ NativeWindow.contextmenus.emailLinkContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ let emailAddr = NativeWindow.contextmenus._stripScheme(url);
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyPhoneNumber"),
+ NativeWindow.contextmenus.phoneNumberLinkContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareLink"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.linkShareableContext),
+ showAsActions: function(aElement) {
+ return {
+ title: aElement.textContent.trim() || aElement.title.trim(),
+ uri: NativeWindow.contextmenus._getLinkURL(aElement),
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link");
+ }
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareEmailAddress"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.emailLinkContext),
+ showAsActions: function(aElement) {
+ let url = NativeWindow.contextmenus._getLinkURL(aElement);
+ let emailAddr = NativeWindow.contextmenus._stripScheme(url);
+ let title = aElement.textContent || aElement.title;
+ return {
+ title: title,
+ uri: emailAddr,
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email");
+ }
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.sharePhoneNumber"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.phoneNumberLinkContext),
+ showAsActions: function(aElement) {
+ let url = NativeWindow.contextmenus._getLinkURL(aElement);
+ let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
+ let title = aElement.textContent || aElement.title;
+ return {
+ title: title,
+ uri: phoneNumber,
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone");
+ }
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
+ NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.emailLinkContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ Messaging.sendRequest({
+ type: "Contact:Add",
+ email: url
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
+ NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.phoneNumberLinkContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ Messaging.sendRequest({
+ type: "Contact:Add",
+ phone: url
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.bookmarkLink"),
+ NativeWindow.contextmenus._disableRestricted("BOOKMARK", NativeWindow.contextmenus.linkBookmarkableContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
+ UITelemetry.addEvent("save.1", "contextmenu", null, "bookmark");
+
+ let url = NativeWindow.contextmenus._getLinkURL(aTarget);
+ let title = aTarget.textContent || aTarget.title || url;
+ Messaging.sendRequest({
+ type: "Bookmark:Insert",
+ url: url,
+ title: title
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.playMedia"),
+ NativeWindow.contextmenus.mediaContext("media-paused"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_play");
+ aTarget.play();
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.pauseMedia"),
+ NativeWindow.contextmenus.mediaContext("media-playing"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause");
+ aTarget.pause();
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.showControls2"),
+ NativeWindow.contextmenus.mediaContext("media-hidingcontrols"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media");
+ aTarget.setAttribute("controls", true);
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareMedia"),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
+ selector: NativeWindow.contextmenus._disableRestricted(
+ "SHARE", NativeWindow.contextmenus.videoContext()),
+ showAsActions: function(aElement) {
+ let url = (aElement.currentSrc || aElement.src);
+ let title = aElement.textContent || aElement.title;
+ return {
+ title: title,
+ uri: url,
+ type: "video/*",
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ callback: function(aTarget) {
+ // share.1 telemetry is handled in Java via PromptList
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media");
+ }
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.fullScreen"),
+ NativeWindow.contextmenus.videoContext("not-fullscreen"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen");
+ aTarget.requestFullscreen();
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.mute"),
+ NativeWindow.contextmenus.mediaContext("media-unmuted"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute");
+ aTarget.muted = true;
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.unmute"),
+ NativeWindow.contextmenus.mediaContext("media-muted"),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute");
+ aTarget.muted = false;
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.viewImage"),
+ NativeWindow.contextmenus.imageLocationCopyableContext,
+ function(aTarget) {
+ let url = aTarget.src;
+ ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
+
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_view_image");
+ UITelemetry.addEvent("loadurl.1", "contextmenu", null);
+ BrowserApp.selectedBrowser.loadURI(url);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.copyImageLocation"),
+ NativeWindow.contextmenus.imageLocationCopyableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image");
+
+ let url = aTarget.src;
+ NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
+ });
+
+ NativeWindow.contextmenus.add({
+ label: stringGetter("contextmenu.shareImage"),
+ selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.imageShareableContext),
+ order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
+ showAsActions: function(aTarget) {
+ let doc = aTarget.ownerDocument;
+ let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
+ .getImgCacheForDocument(doc);
+ let props = imageCache.findEntryProperties(aTarget.currentURI, doc);
+ let src = aTarget.src;
+ return {
+ title: src,
+ uri: src,
+ type: "image/*",
+ };
+ },
+ icon: "drawable://ic_menu_share",
+ menu: true,
+ callback: function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image");
+ }
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.saveImage"),
+ NativeWindow.contextmenus.imageSaveableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image");
+ UITelemetry.addEvent("save.1", "contextmenu", null, "image");
+
+ RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE).then(function(permissionGranted) {
+ if (!permissionGranted) {
+ return;
+ }
+
+ ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
+ false, true, aTarget.ownerDocument.documentURIObject,
+ aTarget.ownerDocument);
+ });
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.setImageAs"),
+ NativeWindow.contextmenus._disableRestricted("SET_IMAGE", NativeWindow.contextmenus.imageSaveableContext),
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");
+
+ let src = aTarget.src;
+ Messaging.sendRequest({
+ type: "Image:SetAs",
+ url: src
+ });
+ });
+
+ NativeWindow.contextmenus.add(
+ function(aTarget) {
+ if (aTarget instanceof HTMLVideoElement) {
+ // If a video element is zero width or height, its essentially
+ // an HTMLAudioElement.
+ if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 )
+ return Strings.browser.GetStringFromName("contextmenu.saveAudio");
+ return Strings.browser.GetStringFromName("contextmenu.saveVideo");
+ } else if (aTarget instanceof HTMLAudioElement) {
+ return Strings.browser.GetStringFromName("contextmenu.saveAudio");
+ }
+ return Strings.browser.GetStringFromName("contextmenu.saveVideo");
+ }, NativeWindow.contextmenus.mediaSaveableContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media");
+ UITelemetry.addEvent("save.1", "contextmenu", null, "media");
+
+ let url = aTarget.currentSrc || aTarget.src;
+ let filePickerTitleKey = (aTarget instanceof HTMLVideoElement &&
+ (aTarget.videoWidth != 0 && aTarget.videoHeight != 0))
+ ? "SaveVideoTitle" : "SaveAudioTitle";
+ // Skipped trying to pull MIME type out of cache for now
+ ContentAreaUtils.internalSave(url, null, null, null, null, false,
+ filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
+ aTarget.ownerDocument, true, null);
+ });
+
+ NativeWindow.contextmenus.add(stringGetter("contextmenu.showImage"),
+ NativeWindow.contextmenus.imageBlockingPolicyContext,
+ function(aTarget) {
+ UITelemetry.addEvent("action.1", "contextmenu", null, "web_show_image");
+ aTarget.setAttribute("data-ctv-show", "true");
+ aTarget.setAttribute("src", aTarget.getAttribute("data-ctv-src"));
+
+ // Shows a snackbar to unblock all images if browser.image_blocking.enabled is enabled.
+ let blockedImgs = aTarget.ownerDocument.querySelectorAll("[data-ctv-src]");
+ if (blockedImgs.length == 0) {
+ return;
+ }
+ let message = Strings.browser.GetStringFromName("imageblocking.downloadedImage");
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: Strings.browser.GetStringFromName("imageblocking.showAllImages"),
+ callback: () => {
+ UITelemetry.addEvent("action.1", "toast", null, "web_show_all_image");
+ for (let i = 0; i < blockedImgs.length; ++i) {
+ blockedImgs[i].setAttribute("data-ctv-show", "true");
+ blockedImgs[i].setAttribute("src", blockedImgs[i].getAttribute("data-ctv-src"));
+ }
+ },
+ }
+ });
+ });
+ },
+
+ onAppUpdated: function() {
+ // initialize the form history and passwords databases on upgrades
+ Services.obs.notifyObservers(null, "FormHistory:Init", "");
+ Services.obs.notifyObservers(null, "Passwords:Init", "");
+
+ if (this._startupStatus === "upgrade") {
+ this._migrateUI();
+ }
+ },
+
+ _migrateUI: function() {
+ const UI_VERSION = 3;
+ let currentUIVersion = 0;
+ try {
+ currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
+ } catch(ex) {}
+ if (currentUIVersion >= UI_VERSION) {
+ return;
+ }
+
+ if (currentUIVersion < 1) {
+ // Migrate user-set "plugins.click_to_play" pref. See bug 884694.
+ // Because the default value is true, a user-set pref means that the pref was set to false.
+ if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
+ Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ }
+
+ // Migrate the "privacy.donottrackheader.value" pref. See bug 1042135.
+ if (Services.prefs.prefHasUserValue("privacy.donottrackheader.value")) {
+ // Make sure the doNotTrack value conforms to the conversion from
+ // three-state to two-state. (This reverts a setting of "please track me"
+ // to the default "don't say anything").
+ if (Services.prefs.getBoolPref("privacy.donottrackheader.enabled") &&
+ (Services.prefs.getIntPref("privacy.donottrackheader.value") != 1)) {
+ Services.prefs.clearUserPref("privacy.donottrackheader.enabled");
+ }
+
+ // This pref has been removed, so always clear it.
+ Services.prefs.clearUserPref("privacy.donottrackheader.value");
+ }
+
+ // Set the search activity default pref on app upgrade if it has not been set already.
+ if (!Services.prefs.prefHasUserValue("searchActivity.default.migrated")) {
+ Services.prefs.setBoolPref("searchActivity.default.migrated", true);
+ SearchEngines.migrateSearchActivityDefaultPref();
+ }
+
+ Reader.migrateCache().catch(e => Cu.reportError("Error migrating Reader cache: " + e));
+
+ // We removed this pref from user visible settings, so we should reset it.
+ // Power users can go into about:config to re-enable this if they choose.
+ if (Services.prefs.prefHasUserValue("nglayout.debug.paint_flashing")) {
+ Services.prefs.clearUserPref("nglayout.debug.paint_flashing");
+ }
+ }
+
+ if (currentUIVersion < 2) {
+ let name;
+ if (Services.prefs.prefHasUserValue("browser.search.defaultenginename")) {
+ name = Services.prefs.getCharPref("browser.search.defaultenginename");
+ }
+ if (!name && Services.prefs.prefHasUserValue("browser.search.defaultenginename.US")) {
+ name = Services.prefs.getCharPref("browser.search.defaultenginename.US");
+ }
+ if (name) {
+ Services.search.init(() => {
+ let engine = Services.search.getEngineByName(name);
+ if (engine) {
+ Services.search.defaultEngine = engine;
+ Services.obs.notifyObservers(null, "default-search-engine-migrated", "");
+ }
+ });
+ }
+ }
+
+ if (currentUIVersion < 3) {
+ const kOldSafeBrowsingPref = "browser.safebrowsing.enabled";
+ // Default value is set to true, a user pref means that the pref was
+ // set to false.
+ if (Services.prefs.prefHasUserValue(kOldSafeBrowsingPref) &&
+ !Services.prefs.getBoolPref(kOldSafeBrowsingPref)) {
+ Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled",
+ false);
+ // Should just remove support for the pref entirely, even if it's
+ // only in about:config
+ Services.prefs.clearUserPref(kOldSafeBrowsingPref);
+ }
+ }
+
+ // Update the migration version.
+ Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
+ },
+
+ // This function returns false during periods where the browser displayed document is
+ // different from the browser content document, so user actions and some kinds of viewport
+ // updates should be ignored. This period starts when we start loading a new page or
+ // switch tabs, and ends when the new browser content document has been drawn and handed
+ // off to the compositor.
+ isBrowserContentDocumentDisplayed: function() {
+ try {
+ if (!Services.androidBridge.isContentDocumentDisplayed(window))
+ return false;
+ } catch (e) {
+ return false;
+ }
+
+ let tab = this.selectedTab;
+ if (!tab)
+ return false;
+ return tab.contentDocumentIsDisplayed;
+ },
+
+ contentDocumentChanged: function() {
+ window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true;
+ Services.androidBridge.contentDocumentChanged(window);
+ },
+
+ get tabs() {
+ return this._tabs;
+ },
+
+ set selectedTab(aTab) {
+ if (this._selectedTab == aTab)
+ return;
+
+ if (this._selectedTab) {
+ this._selectedTab.setActive(false);
+ }
+
+ this._selectedTab = aTab;
+ if (!aTab)
+ return;
+
+ aTab.setActive(true);
+ this.contentDocumentChanged();
+ this.deck.selectedPanel = aTab.browser;
+ // Focus the browser so that things like selection will be styled correctly.
+ aTab.browser.focus();
+ },
+
+ get selectedBrowser() {
+ if (this._selectedTab)
+ return this._selectedTab.browser;
+ return null;
+ },
+
+ getTabForId: function getTabForId(aId) {
+ let tabs = this._tabs;
+ for (let i=0; i < tabs.length; i++) {
+ if (tabs[i].id == aId)
+ return tabs[i];
+ }
+ return null;
+ },
+
+ getTabForBrowser: function getTabForBrowser(aBrowser) {
+ let tabs = this._tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser == aBrowser)
+ return tabs[i];
+ }
+ return null;
+ },
+
+ getTabForWindow: function getTabForWindow(aWindow) {
+ let tabs = this._tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser.contentWindow == aWindow)
+ return tabs[i];
+ }
+ return null;
+ },
+
+ getBrowserForWindow: function getBrowserForWindow(aWindow) {
+ let tabs = this._tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser.contentWindow == aWindow)
+ return tabs[i].browser;
+ }
+ return null;
+ },
+
+ getBrowserForDocument: function getBrowserForDocument(aDocument) {
+ let tabs = this._tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser.contentDocument == aDocument)
+ return tabs[i].browser;
+ }
+ return null;
+ },
+
+ loadURI: function loadURI(aURI, aBrowser, aParams) {
+ aBrowser = aBrowser || this.selectedBrowser;
+ if (!aBrowser)
+ return;
+
+ aParams = aParams || {};
+
+ let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null;
+ let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
+ let charset = "charset" in aParams ? aParams.charset : null;
+
+ let tab = this.getTabForBrowser(aBrowser);
+ if (tab) {
+ if ("userRequested" in aParams) tab.userRequested = aParams.userRequested;
+ tab.isSearch = ("isSearch" in aParams) ? aParams.isSearch : false;
+ }
+
+ try {
+ aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
+ } catch(e) {
+ if (tab) {
+ let message = {
+ type: "Content:LoadError",
+ tabID: tab.id
+ };
+ Messaging.sendRequest(message);
+ dump("Handled load error: " + e)
+ }
+ }
+ },
+
+ addTab: function addTab(aURI, aParams) {
+ aParams = aParams || {};
+
+ let newTab = new Tab(aURI, aParams);
+
+ if (typeof aParams.tabIndex == "number") {
+ this._tabs.splice(aParams.tabIndex, 0, newTab);
+ } else {
+ this._tabs.push(newTab);
+ }
+
+ let selected = "selected" in aParams ? aParams.selected : true;
+ if (selected)
+ this.selectedTab = newTab;
+
+ let pinned = "pinned" in aParams ? aParams.pinned : false;
+ if (pinned) {
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ ss.setTabValue(newTab, "appOrigin", aURI);
+ }
+
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabOpen", true, false, window, null);
+ newTab.browser.dispatchEvent(evt);
+
+ return newTab;
+ },
+
+ // Use this method to close a tab from JS. This method sends a message
+ // to Java to close the tab in the Java UI (we'll get a Tab:Closed message
+ // back from Java when that happens).
+ closeTab: function closeTab(aTab) {
+ if (!aTab) {
+ Cu.reportError("Error trying to close tab (tab doesn't exist)");
+ return;
+ }
+
+ let message = {
+ type: "Tab:Close",
+ tabID: aTab.id
+ };
+ Messaging.sendRequest(message);
+ },
+
+ // Calling this will update the state in BrowserApp after a tab has been
+ // closed in the Java UI.
+ _handleTabClosed: function _handleTabClosed(aTab, aShowUndoSnackbar) {
+ if (aTab == this.selectedTab)
+ this.selectedTab = null;
+
+ let tabIndex = this._tabs.indexOf(aTab);
+
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabClose", true, false, window, tabIndex);
+ aTab.browser.dispatchEvent(evt);
+
+ if (aShowUndoSnackbar) {
+ // Get a title for the undo close snackbar. Fall back to the URL if there is no title.
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ let closedTabData = ss.getClosedTabs(window)[0];
+
+ let message;
+ let title = closedTabData.entries[closedTabData.index - 1].title;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aTab.browser);
+
+ if (isPrivate) {
+ message = Strings.browser.GetStringFromName("privateClosedMessage.message");
+ } else if (title) {
+ message = Strings.browser.formatStringFromName("undoCloseToast.message", [title], 1);
+ } else {
+ message = Strings.browser.GetStringFromName("undoCloseToast.messageDefault");
+ }
+
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: Strings.browser.GetStringFromName("undoCloseToast.action2"),
+ callback: function() {
+ UITelemetry.addEvent("undo.1", "toast", null, "closetab");
+ ss.undoCloseTab(window, closedTabData);
+ }
+ }
+ });
+ }
+
+ aTab.destroy();
+ this._tabs.splice(tabIndex, 1);
+ },
+
+ // Use this method to select a tab from JS. This method sends a message
+ // to Java to select the tab in the Java UI (we'll get a Tab:Selected message
+ // back from Java when that happens).
+ selectTab: function selectTab(aTab) {
+ if (!aTab) {
+ Cu.reportError("Error trying to select tab (tab doesn't exist)");
+ return;
+ }
+
+ // There's nothing to do if the tab is already selected
+ if (aTab == this.selectedTab)
+ return;
+
+ let doc = this.selectedBrowser.contentDocument;
+ if (doc.fullscreenElement) {
+ // We'll finish the tab selection once the fullscreen transition has ended,
+ // remember the new tab for this.
+ this.fullscreenTransitionTab = aTab;
+ doc.exitFullscreen();
+ }
+
+ let message = {
+ type: "Tab:Select",
+ tabID: aTab.id
+ };
+ Messaging.sendRequest(message);
+ },
+
+ /**
+ * Gets an open tab with the given URL.
+ *
+ * @param aURL URL to look for
+ * @param aOptions Options for the search. Currently supports:
+ ** @option startsWith a Boolean indicating whether to search for a tab who's url starts with the
+ * requested url. Useful if you want to ignore hash codes on the end of a url. For instance
+ * to have about:downloads match about:downloads#123.
+ * @return the tab with the given URL, or null if no such tab exists
+ */
+ getTabWithURL: function getTabWithURL(aURL, aOptions) {
+ aOptions = aOptions || {};
+ let uri = Services.io.newURI(aURL, null, null);
+ for (let i = 0; i < this._tabs.length; ++i) {
+ let tab = this._tabs[i];
+ if (aOptions.startsWith) {
+ if (tab.browser.currentURI.spec.startsWith(aURL)) {
+ return tab;
+ }
+ } else {
+ if (tab.browser.currentURI.equals(uri)) {
+ return tab;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * If a tab with the given URL already exists, that tab is selected.
+ * Otherwise, a new tab is opened with the given URL.
+ *
+ * @param aURL URL to open
+ * @param aParam Options used if a tab is created
+ * @param aFlags Options for the search. Currently supports:
+ ** @option startsWith a Boolean indicating whether to search for a tab who's url starts with the
+ * requested url. Useful if you want to ignore hash codes on the end of a url. For instance
+ * to have about:downloads match about:downloads#123.
+ */
+ selectOrAddTab: function selectOrAddTab(aURL, aParams, aFlags) {
+ let tab = this.getTabWithURL(aURL, aFlags);
+ if (tab == null) {
+ tab = this.addTab(aURL, aParams);
+ } else {
+ this.selectTab(tab);
+ }
+
+ return tab;
+ },
+
+ // This method updates the state in BrowserApp after a tab has been selected
+ // in the Java UI.
+ _handleTabSelected: function _handleTabSelected(aTab) {
+ if (this.fullscreenTransitionTab) {
+ // Defer updating to "fullscreenchange" if tab selection happened during
+ // a fullscreen transition.
+ return;
+ }
+ this.selectedTab = aTab;
+
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabSelect", true, false, window, null);
+ aTab.browser.dispatchEvent(evt);
+ },
+
+ quit: function quit(aClear = { sanitize: {}, dontSaveSession: false }) {
+ // Notify all windows that an application quit has been requested.
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+ // Quit aborted.
+ if (cancelQuit.data) {
+ return;
+ }
+
+ Services.obs.notifyObservers(null, "quit-application-proceeding", null);
+
+ // Tell session store to forget about this window
+ if (aClear.dontSaveSession) {
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ ss.removeWindow(window);
+ }
+
+ BrowserApp.sanitize(aClear.sanitize, function() {
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eForceQuit);
+ }, true);
+ },
+
+ saveAsPDF: function saveAsPDF(aBrowser) {
+ RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE).then(function(permissionGranted) {
+ if (!permissionGranted) {
+ return;
+ }
+
+ Task.spawn(function* () {
+ let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
+ fileName = fileName.trim() + ".pdf";
+
+ let downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
+ let file = OS.Path.join(downloadsDir, fileName);
+
+ // Force this to have a unique name.
+ let openedFile = yield OS.File.openUnique(file, { humanReadable: true });
+ file = openedFile.path;
+ yield openedFile.file.close();
+
+ let download = yield Downloads.createDownload({
+ source: aBrowser.contentWindow,
+ target: file,
+ saver: "pdf",
+ startTime: Date.now(),
+ });
+
+ let list = yield Downloads.getList(download.source.isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC)
+ yield list.add(download);
+ yield download.start();
+ });
+ });
+ },
+
+ // These values come from pref_tracking_protection_entries in arrays.xml.
+ PREF_TRACKING_PROTECTION_ENABLED: "2",
+ PREF_TRACKING_PROTECTION_ENABLED_PB: "1",
+ PREF_TRACKING_PROTECTION_DISABLED: "0",
+
+ /**
+ * Returns the current state of the tracking protection pref.
+ * (0 = Disabled, 1 = Enabled in PB, 2 = Enabled)
+ */
+ getTrackingProtectionState: function() {
+ if (Services.prefs.getBoolPref("privacy.trackingprotection.enabled")) {
+ return this.PREF_TRACKING_PROTECTION_ENABLED;
+ }
+ if (Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled")) {
+ return this.PREF_TRACKING_PROTECTION_ENABLED_PB;
+ }
+ return this.PREF_TRACKING_PROTECTION_DISABLED;
+ },
+
+ sanitize: function (aItems, callback, aShutdown) {
+ let success = true;
+ var promises = [];
+
+ for (let key in aItems) {
+ if (!aItems[key])
+ continue;
+
+ key = key.replace("private.data.", "");
+
+ switch (key) {
+ case "cookies_sessions":
+ promises.push(Sanitizer.clearItem("cookies"));
+ promises.push(Sanitizer.clearItem("sessions"));
+ break;
+ default:
+ promises.push(Sanitizer.clearItem(key));
+ }
+ }
+
+ Promise.all(promises).then(function() {
+ Messaging.sendRequest({
+ type: "Sanitize:Finished",
+ success: true,
+ shutdown: aShutdown === true
+ });
+
+ if (callback) {
+ callback();
+ }
+ }).catch(function(err) {
+ Messaging.sendRequest({
+ type: "Sanitize:Finished",
+ error: err,
+ success: false,
+ shutdown: aShutdown === true
+ });
+
+ if (callback) {
+ callback();
+ }
+ })
+ },
+
+ getFocusedInput: function(aBrowser, aOnlyInputElements = false) {
+ if (!aBrowser)
+ return null;
+
+ let doc = aBrowser.contentDocument;
+ if (!doc)
+ return null;
+
+ let focused = doc.activeElement;
+ while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) {
+ doc = focused.contentDocument;
+ focused = doc.activeElement;
+ }
+
+ if (focused instanceof HTMLInputElement &&
+ (focused.mozIsTextField(false) || focused.type === "number")) {
+ return focused;
+ }
+
+ if (aOnlyInputElements)
+ return null;
+
+ if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) {
+
+ if (focused instanceof HTMLBodyElement) {
+ // we are putting focus into a contentEditable frame. scroll the frame into
+ // view instead of the contentEditable document contained within, because that
+ // results in a better user experience
+ focused = focused.ownerDocument.defaultView.frameElement;
+ }
+ return focused;
+ }
+ return null;
+ },
+
+ scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
+ let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
+ if (formHelperMode == kFormHelperModeDisabled)
+ return;
+
+ let dwu = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ if (!dwu) {
+ return;
+ }
+
+ let apzFlushDone = function() {
+ Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed", false);
+ dwu.zoomToFocusedInput();
+ };
+
+ let paintDone = function() {
+ window.removeEventListener("MozAfterPaint", paintDone, false);
+ if (dwu.flushApzRepaints()) {
+ Services.obs.addObserver(apzFlushDone, "apz-repaints-flushed", false);
+ } else {
+ apzFlushDone();
+ }
+ };
+
+ let gotResizeWindow = false;
+ let resizeWindow = function(e) {
+ gotResizeWindow = true;
+ aBrowser.contentWindow.removeEventListener("resize", resizeWindow, false);
+ if (dwu.isMozAfterPaintPending) {
+ window.addEventListener("MozAfterPaint", paintDone, false);
+ } else {
+ paintDone();
+ }
+ }
+
+ aBrowser.contentWindow.addEventListener("resize", resizeWindow, false);
+
+ // The "resize" event sometimes fails to fire, so set a timer to catch that case
+ // and unregister the event listener. See Bug 1253469
+ setTimeout(function(e) {
+ if (!gotResizeWindow) {
+ aBrowser.contentWindow.removeEventListener("resize", resizeWindow, false);
+ dwu.zoomToFocusedInput();
+ }
+ }, 500);
+ },
+
+ getUALocalePref: function () {
+ try {
+ return Services.prefs.getComplexValue("general.useragent.locale", Ci.nsIPrefLocalizedString).data;
+ } catch (e) {
+ try {
+ return Services.prefs.getCharPref("general.useragent.locale");
+ } catch (ee) {
+ return undefined;
+ }
+ }
+ },
+
+ getOSLocalePref: function () {
+ try {
+ return Services.prefs.getCharPref("intl.locale.os");
+ } catch (e) {
+ return undefined;
+ }
+ },
+
+ setLocalizedPref: function (pref, value) {
+ let pls = Cc["@mozilla.org/pref-localizedstring;1"]
+ .createInstance(Ci.nsIPrefLocalizedString);
+ pls.data = value;
+ Services.prefs.setComplexValue(pref, Ci.nsIPrefLocalizedString, pls);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let browser = this.selectedBrowser;
+
+ switch (aTopic) {
+
+ case "Session:Back":
+ browser.goBack();
+ break;
+
+ case "Session:Forward":
+ browser.goForward();
+ break;
+
+ case "Session:Navigate":
+ let index = JSON.parse(aData);
+ let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+ let historySize = webNav.sessionHistory.count;
+
+ if (index < 0) {
+ index = 0;
+ Log.e("Browser", "Negative index truncated to zero");
+ } else if (index >= historySize) {
+ Log.e("Browser", "Incorrect index " + index + " truncated to " + historySize - 1);
+ index = historySize - 1;
+ }
+
+ browser.gotoIndex(index);
+ break;
+
+ case "Session:Reload": {
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+
+ // Check to see if this is a message to enable/disable mixed content blocking.
+ if (aData) {
+ let data = JSON.parse(aData);
+
+ if (data.bypassCache) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY;
+ }
+
+ if (data.contentType === "tracking") {
+ // Convert document URI into the format used by
+ // nsChannelClassifier::ShouldEnableTrackingProtection
+ // (any scheme turned into https is correct)
+ let normalizedUrl = Services.io.newURI("https://" + browser.currentURI.hostPort, null, null);
+ if (data.allowContent) {
+ // Add the current host in the 'trackingprotection' consumer of
+ // the permission manager using a normalized URI. This effectively
+ // places this host on the tracking protection white list.
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
+ } else {
+ Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
+ Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
+ }
+ } else {
+ // Remove the current host from the 'trackingprotection' consumer
+ // of the permission manager. This effectively removes this host
+ // from the tracking protection white list (any list actually).
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
+ } else {
+ Services.perms.remove(normalizedUrl, "trackingprotection");
+ Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
+ }
+ }
+ }
+ }
+
+ // Try to use the session history to reload so that framesets are
+ // handled properly. If the window has no session history, fall back
+ // to using the web navigation's reload method.
+ let webNav = browser.webNavigation;
+ try {
+ let sh = webNav.sessionHistory;
+ if (sh)
+ webNav = sh.QueryInterface(Ci.nsIWebNavigation);
+ } catch (e) {}
+ webNav.reload(flags);
+ break;
+ }
+
+ case "Session:Stop":
+ browser.stop();
+ break;
+
+ case "Tab:Load": {
+ let data = JSON.parse(aData);
+ let url = data.url;
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
+ | Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+
+ // Pass LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL to prevent any loads from
+ // inheriting the currently loaded document's principal.
+ if (data.userEntered) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
+ }
+
+ let delayLoad = ("delayLoad" in data) ? data.delayLoad : false;
+ let params = {
+ selected: ("selected" in data) ? data.selected : !delayLoad,
+ parentId: ("parentId" in data) ? data.parentId : -1,
+ flags: flags,
+ tabID: data.tabID,
+ isPrivate: (data.isPrivate === true),
+ pinned: (data.pinned === true),
+ delayLoad: (delayLoad === true),
+ desktopMode: (data.desktopMode === true)
+ };
+
+ params.userRequested = url;
+
+ if (data.engine) {
+ let engine = Services.search.getEngineByName(data.engine);
+ if (engine) {
+ let submission = engine.getSubmission(url);
+ url = submission.uri.spec;
+ params.postData = submission.postData;
+ params.isSearch = true;
+ }
+ }
+
+ if (data.newTab) {
+ this.addTab(url, params);
+ } else {
+ if (data.tabId) {
+ // Use a specific browser instead of the selected browser, if it exists
+ let specificBrowser = this.getTabForId(data.tabId).browser;
+ if (specificBrowser)
+ browser = specificBrowser;
+ }
+ this.loadURI(url, browser, params);
+ }
+ break;
+ }
+
+ case "Tab:Selected":
+ this._handleTabSelected(this.getTabForId(parseInt(aData)));
+ break;
+
+ case "Tab:Closed": {
+ let data = JSON.parse(aData);
+ this._handleTabClosed(this.getTabForId(data.tabId), data.showUndoToast);
+ break;
+ }
+
+ case "keyword-search":
+ // This event refers to a search via the URL bar, not a bookmarks
+ // keyword search. Note that this code assumes that the user can only
+ // perform a keyword search on the selected tab.
+ this.selectedTab.isSearch = true;
+
+ // Don't store queries in private browsing mode.
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.selectedTab.browser);
+ let query = isPrivate ? "" : aData;
+
+ let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
+ Messaging.sendRequest({
+ type: "Search:Keyword",
+ identifier: engine.identifier,
+ name: engine.name,
+ query: query
+ });
+ break;
+
+ case "Browser:Quit":
+ // Add-ons like QuitNow and CleanQuit provide aData as an empty-string ("").
+ // Pass undefined to invoke the methods default parms.
+ this.quit(aData ? JSON.parse(aData) : undefined);
+ break;
+
+ case "SaveAs:PDF":
+ this.saveAsPDF(browser);
+ break;
+
+ case "ScrollTo:FocusedInput":
+ // these messages come from a change in the viewable area and not user interaction
+ // we allow scrolling to the selected input, but not zooming the page
+ this.scrollToFocusedInput(browser, false);
+ break;
+
+ case "Sanitize:ClearData":
+ this.sanitize(JSON.parse(aData));
+ break;
+
+ case "FullScreen:Exit":
+ browser.contentDocument.exitFullscreen();
+ break;
+
+ case "Passwords:Init": {
+ let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"].
+ getService(Ci.nsILoginManagerStorage);
+ storage.initialize();
+ Services.obs.removeObserver(this, "Passwords:Init");
+ break;
+ }
+
+ case "FormHistory:Init": {
+ // Force creation/upgrade of formhistory.sqlite
+ FormHistory.count({});
+ Services.obs.removeObserver(this, "FormHistory:Init");
+ break;
+ }
+
+ case "android-get-pref": {
+ // These pref names are not "real" pref names. They are used in the
+ // setting menu, and these are passed when initializing the setting
+ // menu. aSubject is a nsIWritableVariant to hold the pref value.
+ aSubject.QueryInterface(Ci.nsIWritableVariant);
+
+ switch (aData) {
+ // The plugin pref is actually two separate prefs, so
+ // we need to handle it differently
+ case "plugin.enable":
+ aSubject.setAsAString(PluginHelper.getPluginPreference());
+ break;
+
+ // Handle master password
+ case "privacy.masterpassword.enabled":
+ aSubject.setAsBool(MasterPassword.enabled);
+ break;
+
+ case "privacy.trackingprotection.state": {
+ aSubject.setAsAString(this.getTrackingProtectionState());
+ break;
+ }
+
+ // Crash reporter submit pref must be fetched from nsICrashReporter
+ // service.
+ case "datareporting.crashreporter.submitEnabled":
+ let crashReporterBuilt = "nsICrashReporter" in Ci &&
+ Services.appinfo instanceof Ci.nsICrashReporter;
+ if (crashReporterBuilt) {
+ aSubject.setAsBool(Services.appinfo.submitReports);
+ }
+ break;
+ }
+ break;
+ }
+
+ case "android-set-pref": {
+ // Pseudo-prefs. aSubject is an nsIWritableVariant that holds the pref
+ // value. Set to empty to signal the pref was handled.
+ aSubject.QueryInterface(Ci.nsIWritableVariant);
+ let value = aSubject.QueryInterface(Ci.nsIVariant);
+
+ switch (aData) {
+ // The plugin pref is actually two separate prefs, so we need to
+ // handle it differently.
+ case "plugin.enable":
+ PluginHelper.setPluginPreference(value);
+ aSubject.setAsEmpty();
+ break;
+
+ // MasterPassword pref is not real, we just need take action and leave
+ case "privacy.masterpassword.enabled":
+ if (MasterPassword.enabled) {
+ MasterPassword.removePassword(value);
+ } else {
+ MasterPassword.setPassword(value);
+ }
+ aSubject.setAsEmpty();
+ break;
+
+ // "privacy.trackingprotection.state" is not a "real" pref name, but
+ // it's used in the setting menu. By default
+ // "privacy.trackingprotection.pbmode.enabled" is true, and
+ // "privacy.trackingprotection.enabled" is false.
+ case "privacy.trackingprotection.state": {
+ switch (value) {
+ // Tracking protection disabled.
+ case this.PREF_TRACKING_PROTECTION_DISABLED:
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", false);
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
+ break;
+ // Tracking protection only in private browsing,
+ case this.PREF_TRACKING_PROTECTION_ENABLED_PB:
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
+ break;
+ // Tracking protection everywhere.
+ case this.PREF_TRACKING_PROTECTION_ENABLED:
+ Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
+ Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+ break;
+ }
+ aSubject.setAsEmpty();
+ break;
+ }
+
+ // Crash reporter preference is in a service; set and return.
+ case "datareporting.crashreporter.submitEnabled":
+ let crashReporterBuilt = "nsICrashReporter" in Ci &&
+ Services.appinfo instanceof Ci.nsICrashReporter;
+ if (crashReporterBuilt) {
+ Services.appinfo.submitReports = value;
+ aSubject.setAsEmpty();
+ }
+ break;
+ }
+ break;
+ }
+
+ case "sessionstore-state-purge-complete":
+ Messaging.sendRequest({ type: "Session:StatePurged" });
+ break;
+
+ case "gather-telemetry":
+ Messaging.sendRequest({ type: "Telemetry:Gather" });
+ break;
+
+ case "Locale:OS":
+ // We know the system locale. We use this for generating Accept-Language headers.
+ console.log("Locale:OS: " + aData);
+ let currentOSLocale = this.getOSLocalePref();
+ if (currentOSLocale == aData) {
+ break;
+ }
+
+ console.log("New OS locale.");
+
+ // Ensure that this choice is immediately persisted, because
+ // Gecko won't be told again if it forgets.
+ Services.prefs.setCharPref("intl.locale.os", aData);
+ Services.prefs.savePrefFile(null);
+
+ let appLocale = this.getUALocalePref();
+
+ this.computeAcceptLanguages(aData, appLocale);
+ break;
+
+ case "Locale:Changed":
+ if (aData) {
+ // The value provided to Locale:Changed should be a BCP47 language tag
+ // understood by Gecko -- for example, "es-ES" or "de".
+ console.log("Locale:Changed: " + aData);
+
+ // We always write a localized pref, even though sometimes the value is a char pref.
+ // (E.g., on desktop single-locale builds.)
+ this.setLocalizedPref("general.useragent.locale", aData);
+ } else {
+ // Resetting.
+ console.log("Switching to system locale.");
+ Services.prefs.clearUserPref("general.useragent.locale");
+ }
+
+ Services.prefs.setBoolPref("intl.locale.matchOS", !aData);
+
+ // Ensure that this choice is immediately persisted, because
+ // Gecko won't be told again if it forgets.
+ Services.prefs.savePrefFile(null);
+
+ // Blow away the string cache so that future lookups get the
+ // correct locale.
+ Strings.flush();
+
+ // Make sure we use the right Accept-Language header.
+ let osLocale;
+ try {
+ // This should never not be set at this point, but better safe than sorry.
+ osLocale = Services.prefs.getCharPref("intl.locale.os");
+ } catch (e) {
+ }
+
+ this.computeAcceptLanguages(osLocale, aData);
+ break;
+
+ case "Fonts:Reload":
+ FontEnumerator.updateFontList();
+ break;
+
+ case "Vibration:Request":
+ if (aSubject instanceof Navigator) {
+ let navigator = aSubject;
+ let buttons = [
+ {
+ label: Strings.browser.GetStringFromName("vibrationRequest.denyButton"),
+ callback: function() {
+ navigator.setVibrationPermission(false);
+ }
+ },
+ {
+ label: Strings.browser.GetStringFromName("vibrationRequest.allowButton"),
+ callback: function() {
+ navigator.setVibrationPermission(true);
+ },
+ positive: true
+ }
+ ];
+ let message = Strings.browser.GetStringFromName("vibrationRequest.message");
+ let options = {};
+ NativeWindow.doorhanger.show(message, "vibration-request", buttons,
+ BrowserApp.selectedTab.id, options, "VIBRATION");
+ }
+ break;
+
+ default:
+ dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
+ break;
+
+ }
+ },
+
+ /**
+ * Set intl.accept_languages accordingly.
+ *
+ * After Bug 881510 this will also accept a real Accept-Language choice as
+ * input; all Accept-Language logic lives here.
+ *
+ * osLocale should never be null, but this method is safe regardless.
+ * appLocale may explicitly be null.
+ */
+ computeAcceptLanguages(osLocale, appLocale) {
+ let defaultBranch = Services.prefs.getDefaultBranch(null);
+ let defaultAccept = defaultBranch.getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString).data;
+ console.log("Default intl.accept_languages = " + defaultAccept);
+
+ // A guard for potential breakage. Bug 438031.
+ // This should not be necessary, because we're reading from the default branch,
+ // but better safe than sorry.
+ if (defaultAccept && defaultAccept.startsWith("chrome://")) {
+ defaultAccept = null;
+ } else {
+ // Ensure lowercase everywhere so we can compare.
+ defaultAccept = defaultAccept.toLowerCase();
+ }
+
+ if (appLocale) {
+ appLocale = appLocale.toLowerCase();
+ }
+
+ if (osLocale) {
+ osLocale = osLocale.toLowerCase();
+ }
+
+ // Eliminate values if they're present in the default.
+ let chosen;
+ if (defaultAccept) {
+ // intl.accept_languages is a comma-separated list, with no q-value params. Those
+ // are added when the header is generated.
+ chosen = defaultAccept.split(",")
+ .map(String.trim)
+ .filter((x) => (x != appLocale && x != osLocale));
+ } else {
+ chosen = [];
+ }
+
+ if (osLocale) {
+ chosen.unshift(osLocale);
+ }
+
+ if (appLocale && appLocale != osLocale) {
+ chosen.unshift(appLocale);
+ }
+
+ let result = chosen.join(",");
+ console.log("Setting intl.accept_languages to " + result);
+ this.setLocalizedPref("intl.accept_languages", result);
+ },
+
+ // nsIAndroidBrowserApp
+ get selectedTab() {
+ return this._selectedTab;
+ },
+
+ // nsIAndroidBrowserApp
+ getBrowserTab: function(tabId) {
+ return this.getTabForId(tabId);
+ },
+
+ getUITelemetryObserver: function() {
+ return UITelemetry;
+ },
+
+ // This method will return a list of history items and toIndex based on the action provided from the fromIndex to toIndex,
+ // optionally selecting selIndex (if fromIndex <= selIndex <= toIndex)
+ getHistory: function(data) {
+ let action = data.action;
+ let webNav = BrowserApp.getTabForId(data.tabId).window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+ let historyIndex = webNav.sessionHistory.index;
+ let historySize = webNav.sessionHistory.count;
+ let canGoBack = webNav.canGoBack;
+ let canGoForward = webNav.canGoForward;
+ let listitems = [];
+ let fromIndex = 0;
+ let toIndex = historySize - 1;
+ let selIndex = historyIndex;
+
+ if (action == "BACK" && canGoBack) {
+ fromIndex = Math.max(historyIndex - kMaxHistoryListSize, 0);
+ toIndex = historyIndex;
+ selIndex = historyIndex;
+ } else if (action == "FORWARD" && canGoForward) {
+ fromIndex = historyIndex;
+ toIndex = Math.min(historySize - 1, historyIndex + kMaxHistoryListSize);
+ selIndex = historyIndex;
+ } else if (action == "ALL" && (canGoBack || canGoForward)){
+ fromIndex = historyIndex - kMaxHistoryListSize / 2;
+ toIndex = historyIndex + kMaxHistoryListSize / 2;
+ if (fromIndex < 0) {
+ toIndex -= fromIndex;
+ }
+
+ if (toIndex > historySize - 1) {
+ fromIndex -= toIndex - (historySize - 1);
+ toIndex = historySize - 1;
+ }
+
+ fromIndex = Math.max(fromIndex, 0);
+ selIndex = historyIndex;
+ } else {
+ // return empty list immediately.
+ return {
+ "historyItems": listitems,
+ "toIndex": toIndex
+ };
+ }
+
+ let browser = this.selectedBrowser;
+ let hist = browser.sessionHistory;
+ for (let i = toIndex; i >= fromIndex; i--) {
+ let entry = hist.getEntryAtIndex(i, false);
+ let item = {
+ title: entry.title || entry.URI.spec,
+ url: entry.URI.spec,
+ selected: (i == selIndex)
+ };
+ listitems.push(item);
+ }
+
+ return {
+ "historyItems": listitems,
+ "toIndex": toIndex
+ };
+ },
+};
+
+var NativeWindow = {
+ init: function() {
+ Services.obs.addObserver(this, "Menu:Clicked", false);
+ Services.obs.addObserver(this, "Doorhanger:Reply", false);
+ this.contextmenus.init();
+ },
+
+ loadDex: function(zipFile, implClass) {
+ Messaging.sendRequest({
+ type: "Dex:Load",
+ zipfile: zipFile,
+ impl: implClass || "Main"
+ });
+ },
+
+ unloadDex: function(zipFile) {
+ Messaging.sendRequest({
+ type: "Dex:Unload",
+ zipfile: zipFile
+ });
+ },
+
+ menu: {
+ _callbacks: [],
+ _menuId: 1,
+ toolsMenuID: -1,
+ add: function() {
+ let options;
+ if (arguments.length == 1) {
+ options = arguments[0];
+ } else if (arguments.length == 3) {
+ Log.w("Browser", "This menu addon API has been deprecated. Instead, use the options object API.");
+ options = {
+ name: arguments[0],
+ callback: arguments[2]
+ };
+ } else {
+ throw "Incorrect number of parameters";
+ }
+
+ options.type = "Menu:Add";
+ options.id = this._menuId;
+
+ Messaging.sendRequest(options);
+ this._callbacks[this._menuId] = options.callback;
+ this._menuId++;
+ return this._menuId - 1;
+ },
+
+ remove: function(aId) {
+ Messaging.sendRequest({ type: "Menu:Remove", id: aId });
+ },
+
+ update: function(aId, aOptions) {
+ if (!aOptions)
+ return;
+
+ Messaging.sendRequest({
+ type: "Menu:Update",
+ id: aId,
+ options: aOptions
+ });
+ }
+ },
+
+ doorhanger: {
+ _callbacks: {},
+ _callbacksId: 0,
+ _promptId: 0,
+
+ /**
+ * @param aOptions
+ * An options JavaScript object holding additional properties for the
+ * notification. The following properties are currently supported:
+ * persistence: An integer. The notification will not automatically
+ * dismiss for this many page loads. If persistence is set
+ * to -1, the doorhanger will never automatically dismiss.
+ * persistWhileVisible:
+ * A boolean. If true, a visible notification will always
+ * persist across location changes.
+ * timeout: A time in milliseconds. The notification will not
+ * automatically dismiss before this time.
+ *
+ * checkbox: A string to appear next to a checkbox under the notification
+ * message. The button callback functions will be called with
+ * the checked state as an argument.
+ *
+ * actionText: An object that specifies a clickable string, a type of action,
+ * and a bundle blob for the consumer to create a click action.
+ * { text: <text>,
+ * type: <type>,
+ * bundle: <blob-object> }
+ *
+ * @param aCategory
+ * Doorhanger type to display (e.g., LOGIN)
+ */
+ show: function(aMessage, aValue, aButtons, aTabID, aOptions, aCategory) {
+ if (aButtons == null) {
+ aButtons = [];
+ }
+
+ if (aButtons.length > 2) {
+ console.log("Doorhanger can have a maximum of two buttons!");
+ aButtons.length = 2;
+ }
+
+ aButtons.forEach((function(aButton) {
+ this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId };
+ aButton.callback = this._callbacksId;
+ this._callbacksId++;
+ }).bind(this));
+
+ this._promptId++;
+ let json = {
+ type: "Doorhanger:Add",
+ message: aMessage,
+ value: aValue,
+ buttons: aButtons,
+ // use the current tab if none is provided
+ tabID: aTabID || BrowserApp.selectedTab.id,
+ options: aOptions || {},
+ category: aCategory
+ };
+ Messaging.sendRequest(json);
+ },
+
+ hide: function(aValue, aTabID) {
+ Messaging.sendRequest({
+ type: "Doorhanger:Remove",
+ value: aValue,
+ tabID: aTabID
+ });
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "Menu:Clicked") {
+ if (this.menu._callbacks[aData])
+ this.menu._callbacks[aData]();
+ } else if (aTopic == "Doorhanger:Reply") {
+ let data = JSON.parse(aData);
+ let reply_id = data["callback"];
+
+ if (this.doorhanger._callbacks[reply_id]) {
+ // Pass the value of the optional checkbox to the callback
+ let checked = data["checked"];
+ this.doorhanger._callbacks[reply_id].cb(checked, data.inputs);
+
+ let prompt = this.doorhanger._callbacks[reply_id].prompt;
+ for (let id in this.doorhanger._callbacks) {
+ if (this.doorhanger._callbacks[id].prompt == prompt) {
+ delete this.doorhanger._callbacks[id];
+ }
+ }
+ }
+ }
+ },
+
+ contextmenus: {
+ items: {}, // a list of context menu items that we may show
+ DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items
+
+ init: function() {
+ // Accessing "NativeWindow.contextmenus" initializes context menus if needed.
+ BrowserApp.deck.addEventListener(
+ "contextmenu", (e) => NativeWindow.contextmenus.show(e), false);
+ },
+
+ add: function() {
+ let args;
+ if (arguments.length == 1) {
+ args = arguments[0];
+ } else if (arguments.length == 3) {
+ args = {
+ label : arguments[0],
+ selector: arguments[1],
+ callback: arguments[2]
+ };
+ } else {
+ throw "Incorrect number of parameters";
+ }
+
+ if (!args.label)
+ throw "Menu items must have a name";
+
+ let cmItem = new ContextMenuItem(args);
+ this.items[cmItem.id] = cmItem;
+ return cmItem.id;
+ },
+
+ remove: function(aId) {
+ delete this.items[aId];
+ },
+
+ // Although we do not use this ourselves anymore, add-ons may still
+ // need it as it has been documented, so we shouldn't remove it.
+ SelectorContext: function(aSelector) {
+ return {
+ matches: function(aElt) {
+ if (aElt.matches)
+ return aElt.matches(aSelector);
+ return false;
+ }
+ };
+ },
+
+ linkOpenableNonPrivateContext: {
+ matches: function linkOpenableNonPrivateContextMatches(aElement) {
+ let doc = aElement.ownerDocument;
+ if (!doc || PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView)) {
+ return false;
+ }
+
+ return NativeWindow.contextmenus.linkOpenableContext.matches(aElement);
+ }
+ },
+
+ linkOpenableContext: {
+ matches: function linkOpenableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontOpen = /^(javascript|mailto|news|snews|tel)$/;
+ return (scheme && !dontOpen.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ linkCopyableContext: {
+ matches: function linkCopyableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontCopy = /^(mailto|tel)$/;
+ return (scheme && !dontCopy.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ linkShareableContext: {
+ matches: function linkShareableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/;
+ return (scheme && !dontShare.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ linkBookmarkableContext: {
+ matches: function linkBookmarkableContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri) {
+ let scheme = uri.scheme;
+ let dontBookmark = /^(mailto|tel)$/;
+ return (scheme && !dontBookmark.test(scheme));
+ }
+ return false;
+ }
+ },
+
+ emailLinkContext: {
+ matches: function emailLinkContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri)
+ return uri.schemeIs("mailto");
+ return false;
+ }
+ },
+
+ phoneNumberLinkContext: {
+ matches: function phoneNumberLinkContextMatches(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri)
+ return uri.schemeIs("tel");
+ return false;
+ }
+ },
+
+ imageLocationCopyableContext: {
+ matches: function imageLinkCopyableContextMatches(aElement) {
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+ // The image is blocked by Tap-to-load Images
+ if (aElement.hasAttribute("data-ctv-src") && !aElement.hasAttribute("data-ctv-show")) {
+ return false;
+ }
+ }
+ return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI);
+ }
+ },
+
+ imageSaveableContext: {
+ matches: function imageSaveableContextMatches(aElement) {
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+ // The image is blocked by Tap-to-load Images
+ if (aElement.hasAttribute("data-ctv-src") && !aElement.hasAttribute("data-ctv-show")) {
+ return false;
+ }
+ }
+ if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) {
+ // The image must be loaded to allow saving
+ let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE));
+ }
+ return false;
+ }
+ },
+
+ imageShareableContext: {
+ matches: function imageShareableContextMatches(aElement) {
+ let imgSrc = '';
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+ imgSrc = aElement.src;
+ } else if (aElement instanceof Ci.nsIImageLoadingContent &&
+ aElement.currentURI &&
+ aElement.currentURI.spec) {
+ imgSrc = aElement.currentURI.spec;
+ }
+
+ // In order to share an image, we need to pass the image src over IPC via an Intent (in
+ // `ApplicationPackageManager.queryIntentActivities`). However, the transaction has a 1MB limit
+ // (shared by all transactions in progress) - otherwise we crash! (bug 1243305)
+ // https://developer.android.com/reference/android/os/TransactionTooLargeException.html
+ //
+ // The transaction limit is 1MB and we arbitrarily choose to cap this transaction at 1/4 of that = 250,000 bytes.
+ // In Java, a UTF-8 character is 1-4 bytes so, 250,000 bytes / 4 bytes/char = 62,500 char
+ let MAX_IMG_SRC_LEN = 62500;
+ let isTooLong = imgSrc.length >= MAX_IMG_SRC_LEN;
+ return !isTooLong && this.NativeWindow.contextmenus.imageSaveableContext.matches(aElement);
+ }.bind(this)
+ },
+
+ mediaSaveableContext: {
+ matches: function mediaSaveableContextMatches(aElement) {
+ return (aElement instanceof HTMLVideoElement ||
+ aElement instanceof HTMLAudioElement);
+ }
+ },
+
+ imageBlockingPolicyContext: {
+ matches: function imageBlockingPolicyContextMatches(aElement) {
+ if (aElement instanceof Ci.nsIDOMHTMLImageElement && aElement.getAttribute("data-ctv-src")) {
+ // Only show the menuitem if we are blocking the image
+ if (aElement.getAttribute("data-ctv-show") == "true") {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+ },
+
+ mediaContext: function(aMode) {
+ return {
+ matches: function(aElt) {
+ if (aElt instanceof Ci.nsIDOMHTMLMediaElement) {
+ let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE;
+ if (hasError)
+ return false;
+
+ let paused = aElt.paused || aElt.ended;
+ if (paused && aMode == "media-paused")
+ return true;
+ if (!paused && aMode == "media-playing")
+ return true;
+ let controls = aElt.controls;
+ if (!controls && aMode == "media-hidingcontrols")
+ return true;
+
+ let muted = aElt.muted;
+ if (muted && aMode == "media-muted")
+ return true;
+ else if (!muted && aMode == "media-unmuted")
+ return true;
+ }
+ return false;
+ }
+ };
+ },
+
+ videoContext: function(aMode) {
+ return {
+ matches: function(aElt) {
+ if (aElt instanceof HTMLVideoElement) {
+ if (!aMode) {
+ return true;
+ }
+ var isFullscreen = aElt.ownerDocument.fullscreenElement == aElt;
+ if (aMode == "not-fullscreen") {
+ return !isFullscreen;
+ }
+ if (aMode == "fullscreen") {
+ return isFullscreen;
+ }
+ }
+ return false;
+ }
+ };
+ },
+
+ /* Holds a WeakRef to the original target element this context menu was shown for.
+ * Most API's will have to walk up the tree from this node to find the correct element
+ * to act on
+ */
+ get _target() {
+ if (this._targetRef)
+ return this._targetRef.get();
+ return null;
+ },
+
+ set _target(aTarget) {
+ if (aTarget)
+ this._targetRef = Cu.getWeakReference(aTarget);
+ else this._targetRef = null;
+ },
+
+ get defaultContext() {
+ delete this.defaultContext;
+ return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default");
+ },
+
+ /* Gets menuitems for an arbitrary node
+ * Parameters:
+ * element - The element to look at. If this element has a contextmenu attribute, the
+ * corresponding contextmenu will be used.
+ */
+ _getHTMLContextMenuItemsForElement: function(element) {
+ let htmlMenu = element.contextMenu;
+ if (!htmlMenu) {
+ return [];
+ }
+
+ htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
+ htmlMenu.sendShowEvent();
+
+ return this._getHTMLContextMenuItemsForMenu(htmlMenu, element);
+ },
+
+ /* Add a menuitem for an HTML <menu> node
+ * Parameters:
+ * menu - The <menu> element to iterate through for menuitems
+ * target - The target element these context menu items are attached to
+ */
+ _getHTMLContextMenuItemsForMenu: function(menu, target) {
+ let items = [];
+ for (let i = 0; i < menu.childNodes.length; i++) {
+ let elt = menu.childNodes[i];
+ if (!elt.label)
+ continue;
+
+ items.push(new HTMLContextMenuItem(elt, target));
+ }
+
+ return items;
+ },
+
+ // Searches the current list of menuitems to show for any that match this id
+ _findMenuItem: function(aId) {
+ if (!this.menus) {
+ return null;
+ }
+
+ for (let context in this.menus) {
+ let menu = this.menus[context];
+ for (let i = 0; i < menu.length; i++) {
+ if (menu[i].id === aId) {
+ return menu[i];
+ }
+ }
+ }
+ return null;
+ },
+
+ // Returns true if there are any context menu items to show
+ _shouldShow: function() {
+ for (let context in this.menus) {
+ let menu = this.menus[context];
+ if (menu.length > 0) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this
+ * is an image inside an <a> tag, we may have a "link" context and an "image" one.
+ */
+ _getContextType: function(element) {
+ // For anchor nodes, we try to use the scheme to pick a string
+ if (element instanceof Ci.nsIDOMHTMLAnchorElement) {
+ let uri = this.makeURI(this._getLinkURL(element));
+ try {
+ return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme);
+ } catch(ex) { }
+ }
+
+ // Otherwise we try the nodeName
+ try {
+ return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase());
+ } catch(ex) { }
+
+ // Fallback to the default
+ return this.defaultContext;
+ },
+
+ // Adds context menu items added through the add-on api
+ _getNativeContextMenuItems: function(element, x, y) {
+ let res = [];
+ for (let itemId of Object.keys(this.items)) {
+ let item = this.items[itemId];
+
+ if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
+ res.push(item);
+ }
+ }
+
+ return res;
+ },
+
+ /* Checks if there are context menu items to show, and if it finds them
+ * sends a contextmenu event to content. We also send showing events to
+ * any html5 context menus we are about to show, and fire some local notifications
+ * for chrome consumers to do lazy menuitem construction
+ */
+ show: function(event) {
+ // Android Long-press / contextmenu event provides clientX/Y data. This is not provided
+ // by mochitest: test_browserElement_inproc_ContextmenuEvents.html.
+ if (!event.clientX || !event.clientY) {
+ return;
+ }
+
+ // If the event was already defaultPrevented by somebody (web content, or
+ // some other part of gecko), then don't do anything with it.
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ // Use the highlighted element for the context menu target. When accessibility is
+ // enabled, elements may not be highlighted so use the event target instead.
+ this._target = BrowserEventHandler._highlightElement || event.target;
+ if (!this._target) {
+ return;
+ }
+
+ // Try to build a list of contextmenu items. If successful, actually show the
+ // native context menu by passing the list to Java.
+ this._buildMenu(event.clientX, event.clientY);
+ if (this._shouldShow()) {
+ BrowserEventHandler._cancelTapHighlight();
+
+ // Consume / preventDefault the event, and show the contextmenu.
+ event.preventDefault();
+ this._innerShow(this._target, event.clientX, event.clientY);
+ this._target = null;
+
+ return;
+ }
+
+ // If no context-menu for long-press event, it may be meant to trigger text-selection.
+ this.menus = null;
+ Services.obs.notifyObservers(
+ {target: this._target, x: event.clientX, y: event.clientY}, "context-menu-not-shown", "");
+ },
+
+ // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
+ _getTitle: function(node) {
+ if (node.hasAttribute && node.hasAttribute("title")) {
+ return node.getAttribute("title");
+ }
+ return this._getUrl(node);
+ },
+
+ // Returns a url associated with a node
+ _getUrl: function(node) {
+ if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
+ (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
+ return this._getLinkURL(node);
+ } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) {
+ // The image is blocked by Tap-to-load Images
+ let originalURL = node.getAttribute("data-ctv-src");
+ if (originalURL) {
+ return originalURL;
+ }
+ return node.currentURI.spec;
+ } else if (node instanceof Ci.nsIDOMHTMLMediaElement) {
+ let srcUrl = node.currentSrc || node.src;
+ // If URL prepended with blob or mediasource, we'll remove it.
+ return srcUrl.replace(/^(?:blob|mediasource):/, '');
+ }
+
+ return "";
+ },
+
+ // Adds an array of menuitems to the current list of items to show, in the correct context
+ _addMenuItems: function(items, context) {
+ if (!this.menus[context]) {
+ this.menus[context] = [];
+ }
+ this.menus[context] = this.menus[context].concat(items);
+ },
+
+ /* Does the basic work of building a context menu to show. Will combine HTML and Native
+ * context menus items, as well as sorting menuitems into different menus based on context.
+ */
+ _buildMenu: function(x, y) {
+ // now walk up the tree and for each node look for any context menu items that apply
+ let element = this._target;
+
+ // this.menus holds a hashmap of "contexts" to menuitems associated with that context
+ // For instance, if the user taps an image inside a link, we'll have something like:
+ // {
+ // link: [ ContextMenuItem, ContextMenuItem ]
+ // image: [ ContextMenuItem, ContextMenuItem ]
+ // }
+ this.menus = {};
+
+ while (element) {
+ let context = this._getContextType(element);
+
+ // First check for any html5 context menus that might exist...
+ var items = this._getHTMLContextMenuItemsForElement(element);
+ if (items.length > 0) {
+ this._addMenuItems(items, context);
+ }
+
+ // then check for any context menu items registered in the ui.
+ items = this._getNativeContextMenuItems(element, x, y);
+ if (items.length > 0) {
+ this._addMenuItems(items, context);
+ }
+
+ // walk up the tree and find more items to show
+ element = element.parentNode;
+ }
+ },
+
+ // Walks the DOM tree to find a title from a node
+ _findTitle: function(node) {
+ let title = "";
+ while(node && !title) {
+ title = this._getTitle(node);
+ node = node.parentNode;
+ }
+ return title;
+ },
+
+ /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm
+ * If there is one menu, will return a flat array of menuitems. If there are multiple
+ * menus, will return an array with appropriate tabs/items inside it. i.e. :
+ * [
+ * { label: "link", items: [...] },
+ * { label: "image", items: [...] }
+ * ]
+ */
+ _reformatList: function(target) {
+ let contexts = Object.keys(this.menus);
+
+ if (contexts.length === 1) {
+ // If there's only one context, we'll only show a single flat single select list
+ return this._reformatMenuItems(target, this.menus[contexts[0]]);
+ }
+
+ // If there are multiple contexts, we'll only show a tabbed ui with multiple lists
+ return this._reformatListAsTabs(target, this.menus);
+ },
+
+ /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's
+ * addTabs method. i.e. :
+ * { link: [...], image: [...] } becomes
+ * [ { label: "link", items: [...] } ]
+ *
+ * Also reformats items and resolves any parmaeters that aren't known until display time
+ * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
+ */
+ _reformatListAsTabs: function(target, menus) {
+ let itemArray = [];
+
+ // Sort the keys so that "link" is always first
+ let contexts = Object.keys(this.menus);
+ contexts.sort((context1, context2) => {
+ if (context1 === this.defaultContext) {
+ return -1;
+ } else if (context2 === this.defaultContext) {
+ return 1;
+ }
+ return 0;
+ });
+
+ contexts.forEach(context => {
+ itemArray.push({
+ label: context,
+ items: this._reformatMenuItems(target, menus[context])
+ });
+ });
+
+ return itemArray;
+ },
+
+ /* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items
+ * and resolves any parmaeters that aren't known until display time
+ * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
+ */
+ _reformatMenuItems: function(target, menuitems) {
+ let itemArray = [];
+
+ for (let i = 0; i < menuitems.length; i++) {
+ let t = target;
+ while(t) {
+ if (menuitems[i].matches(t)) {
+ let val = menuitems[i].getValue(t);
+
+ // hidden menu items will return null from getValue
+ if (val) {
+ itemArray.push(val);
+ break;
+ }
+ }
+
+ t = t.parentNode;
+ }
+ }
+
+ return itemArray;
+ },
+
+ // Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt.
+ _innerShow: function(target, x, y) {
+ Haptic.performSimpleAction(Haptic.LongPress);
+
+ // spin through the tree looking for a title for this context menu
+ let title = this._findTitle(target);
+
+ for (let context in this.menus) {
+ let menu = this.menus[context];
+ menu.sort((a,b) => {
+ if (a.order === b.order) {
+ return 0;
+ }
+ return (a.order > b.order) ? 1 : -1;
+ });
+ }
+
+ let useTabs = Object.keys(this.menus).length > 1;
+ let prompt = new Prompt({
+ window: target.ownerDocument.defaultView,
+ title: useTabs ? undefined : title
+ });
+
+ let items = this._reformatList(target);
+ if (useTabs) {
+ prompt.addTabs({
+ id: "tabs",
+ items: items
+ });
+ } else {
+ prompt.setSingleChoiceItems(items);
+ }
+
+ prompt.show(this._promptDone.bind(this, target, x, y, items));
+ },
+
+ // Called when the contextmenu prompt is closed
+ _promptDone: function(target, x, y, items, data) {
+ if (data.button == -1) {
+ // Prompt was cancelled, or an ActionView was used.
+ return;
+ }
+
+ let selectedItemId;
+ if (data.tabs) {
+ let menu = items[data.tabs.tab];
+ selectedItemId = menu.items[data.tabs.item].id;
+ } else {
+ selectedItemId = items[data.list[0]].id
+ }
+
+ let selectedItem = this._findMenuItem(selectedItemId);
+ this.menus = null;
+
+ if (!selectedItem || !selectedItem.matches || !selectedItem.callback) {
+ return;
+ }
+
+ // for menuitems added using the native UI, pass the dom element that matched that item to the callback
+ while (target) {
+ if (selectedItem.matches(target, x, y)) {
+ selectedItem.callback(target, x, y);
+ break;
+ }
+ target = target.parentNode;
+ }
+ },
+
+ // XXX - These are stolen from Util.js, we should remove them if we bring it back
+ makeURLAbsolute: function makeURLAbsolute(base, url) {
+ // Note: makeURI() will throw if url is not a valid URI
+ return this.makeURI(url, null, this.makeURI(base)).spec;
+ },
+
+ makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
+ return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
+ },
+
+ _getLink: function(aElement) {
+ if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
+ ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
+ (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) ||
+ aElement instanceof Ci.nsIDOMHTMLLinkElement ||
+ aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) {
+ try {
+ let url = this._getLinkURL(aElement);
+ return Services.io.newURI(url, null, null);
+ } catch (e) {}
+ }
+ return null;
+ },
+
+ _disableRestricted: function _disableRestricted(restriction, selector) {
+ return {
+ matches: function _disableRestrictedMatches(aElement, aX, aY) {
+ if (!ParentalControls.isAllowed(ParentalControls[restriction])) {
+ return false;
+ }
+
+ return selector.matches(aElement, aX, aY);
+ }
+ };
+ },
+
+ _getLinkURL: function ch_getLinkURL(aLink) {
+ let href = aLink.href;
+ if (href)
+ return href;
+
+ href = aLink.getAttribute("href") ||
+ aLink.getAttributeNS(kXLinkNamespace, "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 this.makeURLAbsolute(aLink.baseURI, href);
+ },
+
+ _copyStringToDefaultClipboard: function(aString) {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(aString);
+ Snackbars.show(Strings.browser.GetStringFromName("selectionHelper.textCopied"), Snackbars.LENGTH_LONG);
+ },
+
+ _stripScheme: function(aString) {
+ let index = aString.indexOf(":");
+ return aString.slice(index + 1);
+ }
+ }
+};
+
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+ "resource://gre/modules/PageActions.jsm");
+
+// These alias to the old, deprecated NativeWindow interfaces
+[
+ ["pageactions", "resource://gre/modules/PageActions.jsm", "PageActions"],
+ ["toast", "resource://gre/modules/Snackbars.jsm", "Snackbars"]
+].forEach(item => {
+ let [name, script, exprt] = item;
+
+ XPCOMUtils.defineLazyGetter(NativeWindow, name, () => {
+ var err = Strings.browser.formatStringFromName("nativeWindow.deprecated", ["NativeWindow." + name, script], 2);
+ Cu.reportError(err);
+
+ let sandbox = {};
+ Cu.import(script, sandbox);
+ return sandbox[exprt];
+ });
+});
+
+var LightWeightThemeWebInstaller = {
+ init: function sh_init() {
+ let temp = {};
+ Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp);
+ let theme = new temp.LightweightThemeConsumer(document);
+ BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
+ BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
+ BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
+
+ if (ParentalControls.parentalControlsEnabled &&
+ !this._manager.currentTheme &&
+ ParentalControls.isAllowed(ParentalControls.DEFAULT_THEME)) {
+ // We are using the DEFAULT_THEME restriction to differentiate between restricted profiles & guest mode - Bug 1199596
+ this._installParentalControlsTheme();
+ }
+ },
+
+ 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 () {
+ let temp = {};
+ Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
+ delete this._manager;
+ return this._manager = temp.LightweightThemeManager;
+ },
+
+ _installParentalControlsTheme: function() {
+ let mgr = this._manager;
+ let parentalControlsTheme = {
+ "headerURL": "resource://android/assets/parental_controls_theme.png",
+ "name": "Parental Controls Theme",
+ "id": "parental-controls-theme@mozilla.org"
+ };
+
+ mgr.addBuiltInTheme(parentalControlsTheme);
+ mgr.themeChanged(parentalControlsTheme);
+ },
+
+ _installRequest: function (event) {
+ let node = event.target;
+ let data = this._getThemeFromNode(node);
+ if (!data)
+ return;
+
+ if (this._isAllowed(node)) {
+ this._install(data);
+ return;
+ }
+
+ let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton");
+ let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1);
+ let buttons = [{
+ label: allowButtonText,
+ callback: function () {
+ LightWeightThemeWebInstaller._install(data);
+ },
+ positive: true
+ }];
+
+ NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id);
+ },
+
+ _install: function (newLWTheme) {
+ this._manager.currentTheme = newLWTheme;
+ },
+
+ _previewWindow: null,
+ _preview: function (event) {
+ if (!this._isAllowed(event.target))
+ return;
+ let data = this._getThemeFromNode(event.target);
+ if (!data)
+ return;
+ this._resetPreview();
+
+ this._previewWindow = event.target.ownerDocument.defaultView;
+ this._previewWindow.addEventListener("pagehide", this, true);
+ BrowserApp.deck.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;
+ BrowserApp.deck.removeEventListener("TabSelect", this, false);
+
+ this._manager.resetPreview();
+ },
+
+ _isAllowed: function (node) {
+ // Make sure the whitelist has been imported to permissions
+ PermissionsUtils.importFromPrefs("xpinstall.", "install");
+
+ let pm = Services.perms;
+
+ let uri = node.ownerDocument.documentURIObject;
+ if (!uri.schemeIs("https")) {
+ return false;
+ }
+
+ return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
+ },
+
+ _getThemeFromNode: function (node) {
+ return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI);
+ }
+};
+
+var DesktopUserAgent = {
+ DESKTOP_UA: null,
+ TCO_DOMAIN: "t.co",
+ TCO_REPLACE: / Gecko.*/,
+
+ init: function ua_init() {
+ Services.obs.addObserver(this, "DesktopMode:Change", false);
+ UserAgentOverrides.addComplexOverride(this.onRequest.bind(this));
+
+ // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference
+ this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler).userAgent
+ .replace(/Android \d.+?; [a-zA-Z]+/, "X11; Linux x86_64")
+ .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
+ },
+
+ onRequest: function(channel, defaultUA) {
+ if (AppConstants.NIGHTLY_BUILD && this.TCO_DOMAIN == channel.URI.host) {
+ // Force the referrer
+ channel.referrer = channel.URI;
+
+ // Send a bot-like UA to t.co to get a real redirect. We strip off the
+ // "Gecko/x.y Firefox/x.y" part
+ return defaultUA.replace(this.TCO_REPLACE, "");
+ }
+
+ let channelWindow = this._getWindowForRequest(channel);
+ let tab = BrowserApp.getTabForWindow(channelWindow);
+ if (tab) {
+ return this.getUserAgentForTab(tab);
+ }
+
+ return null;
+ },
+
+ getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) {
+ let tab = BrowserApp.getTabForWindow(aWindow.top);
+ if (tab) {
+ return this.getUserAgentForTab(tab);
+ }
+
+ return null;
+ },
+
+ getUserAgentForTab: function ua_getUserAgentForTab(aTab) {
+ // Send desktop UA if "Request Desktop Site" is enabled.
+ if (aTab.desktopMode) {
+ return this.DESKTOP_UA;
+ }
+
+ return null;
+ },
+
+ _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) {
+ if (aRequest && aRequest.notificationCallbacks) {
+ try {
+ return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (ex) { }
+ }
+
+ if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) {
+ try {
+ return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
+ } catch (ex) { }
+ }
+
+ return null;
+ },
+
+ _getWindowForRequest: function ua_getWindowForRequest(aRequest) {
+ let loadContext = this._getRequestLoadContext(aRequest);
+ if (loadContext) {
+ try {
+ return loadContext.associatedWindow;
+ } catch (e) {
+ // loadContext.associatedWindow can throw when there's no window
+ }
+ }
+ return null;
+ },
+
+ observe: function ua_observe(aSubject, aTopic, aData) {
+ if (aTopic === "DesktopMode:Change") {
+ let args = JSON.parse(aData);
+ let tab = BrowserApp.getTabForId(args.tabId);
+ if (tab) {
+ tab.reloadWithMode(args.desktopMode);
+ }
+ }
+ }
+};
+
+
+function nsBrowserAccess() {
+}
+
+nsBrowserAccess.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]),
+
+ _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aFlags) {
+ let isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+ if (isExternal && aURI && aURI.schemeIs("chrome"))
+ return null;
+
+ let loadflags = isExternal ?
+ Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
+ if (isExternal) {
+ aWhere = Services.prefs.getIntPref("browser.link.open_external");
+ } else {
+ aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
+ }
+ }
+
+ Services.io.offline = false;
+
+ let referrer;
+ if (aOpener) {
+ try {
+ let location = aOpener.location;
+ referrer = Services.io.newURI(location, null, null);
+ } catch(e) { }
+ }
+
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ let pinned = false;
+
+ if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
+ pinned = true;
+ let spec = aURI.spec;
+ let tabs = BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ let appOrigin = ss.getTabValue(tabs[i], "appOrigin");
+ if (appOrigin == spec) {
+ let tab = tabs[i];
+ BrowserApp.selectTab(tab);
+ return tab.browser;
+ }
+ }
+ }
+
+ // If OPEN_SWITCHTAB was not handled above, we need to open a new tab,
+ // along with other OPEN_ values that create a new tab.
+ let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
+ aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
+ aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB);
+ let isPrivate = false;
+
+ if (newTab) {
+ let parentId = -1;
+ if (!isExternal && aOpener) {
+ let parent = BrowserApp.getTabForWindow(aOpener.top);
+ if (parent) {
+ parentId = parent.id;
+ isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
+ }
+ }
+
+ let openerWindow = (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener;
+ // BrowserApp.addTab calls loadURIWithFlags with the appropriate params
+ let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags,
+ referrerURI: referrer,
+ external: isExternal,
+ parentId: parentId,
+ opener: openerWindow,
+ selected: true,
+ isPrivate: isPrivate,
+ pinned: pinned });
+
+ return tab.browser;
+ }
+
+ // OPEN_CURRENTWINDOW and illegal values
+ let browser = BrowserApp.selectedBrowser;
+ if (aURI && browser) {
+ browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
+ }
+
+ return browser;
+ },
+
+ openURI: function browser_openURI(aURI, aOpener, aWhere, aFlags) {
+ let browser = this._getBrowser(aURI, aOpener, aWhere, aFlags);
+ return browser ? browser.contentWindow : null;
+ },
+
+ openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aFlags) {
+ let browser = this._getBrowser(aURI, null, aWhere, aFlags);
+ return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
+ },
+
+ isTabContentWindow: function(aWindow) {
+ return BrowserApp.getBrowserForWindow(aWindow) != null;
+ },
+
+ canClose() {
+ return BrowserUtils.canCloseWindow(window);
+ },
+};
+
+
+function Tab(aURL, aParams) {
+ this.filter = null;
+ this.browser = null;
+ this.id = 0;
+ this.lastTouchedAt = Date.now();
+ this._zoom = 1.0;
+ this._drawZoom = 1.0;
+ this._restoreZoom = false;
+ this.userScrollPos = { x: 0, y: 0 };
+ this.contentDocumentIsDisplayed = true;
+ this.pluginDoorhangerTimeout = null;
+ this.shouldShowPluginDoorhanger = true;
+ this.clickToPlayPluginsActivated = false;
+ this.desktopMode = false;
+ this.originalURI = null;
+ this.hasTouchListener = false;
+ this.playingAudio = false;
+
+ this.create(aURL, aParams);
+}
+
+/*
+ * Sanity limit for URIs passed to UI code.
+ *
+ * 2000 is the typical industry limit, largely due to older IE versions.
+ *
+ * We use 25000, so we'll allow almost any value through.
+ *
+ * Still, this truncation doesn't affect history, so this is only a practical
+ * concern in two ways: the truncated value is used when editing URIs, and as
+ * the key for favicon fetches.
+ */
+const MAX_URI_LENGTH = 25000;
+
+/*
+ * Similar restriction for titles. This is only a display concern.
+ */
+const MAX_TITLE_LENGTH = 255;
+
+/**
+ * Ensure that a string is of a sane length.
+ */
+function truncate(text, max) {
+ if (!text || !max) {
+ return text;
+ }
+
+ if (text.length <= max) {
+ return text;
+ }
+
+ return text.slice(0, max) + "…";
+}
+
+Tab.prototype = {
+ create: function(aURL, aParams) {
+ if (this.browser)
+ return;
+
+ aParams = aParams || {};
+
+ this.browser = document.createElement("browser");
+ this.browser.setAttribute("type", "content-targetable");
+ this.browser.setAttribute("messagemanagergroup", "browsers");
+
+ if (Preferences.get("browser.tabs.remote.force-enable", false)) {
+ this.browser.setAttribute("remote", "true");
+ }
+
+ this.browser.permanentKey = {};
+
+ // Check if we have a "parent" window which we need to set as our opener
+ if ("opener" in aParams) {
+ this.browser.presetOpenerWindow(aParams.opener);
+ }
+
+ // Make sure the previously selected panel remains selected. The selected panel of a deck is
+ // not stable when panels are added.
+ let selectedPanel = BrowserApp.deck.selectedPanel;
+ BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null);
+ BrowserApp.deck.selectedPanel = selectedPanel;
+
+ let attrs = {};
+ if (BrowserApp.manifestUrl) {
+ let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
+ let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl);
+ if (manifest) {
+ let app = manifest.QueryInterface(Ci.mozIApplication);
+ this.browser.docShell.frameType = Ci.nsIDocShell.FRAME_TYPE_APP;
+ attrs['appId'] = app.localId;
+ }
+ }
+
+ // Must be called after appendChild so the docShell has been created.
+ this.setActive(false);
+
+ let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate;
+ if (isPrivate) {
+ attrs['privateBrowsingId'] = 1;
+ }
+
+ this.browser.docShell.setOriginAttributes(attrs);
+
+ // Set the new docShell load flags based on network state.
+ if (Tabs.useCache) {
+ this.browser.docShell.defaultLoadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
+ }
+
+ this.browser.stop();
+
+ // Only set tab uri if uri is valid
+ let uri = null;
+ let title = aParams.title || aURL;
+ try {
+ uri = Services.io.newURI(aURL, null, null).spec;
+ } catch (e) {}
+
+ // When the tab is stubbed from Java, there's a window between the stub
+ // creation and the tab creation in Gecko where the stub could be removed
+ // or the selected tab can change (which is easiest to hit during startup).
+ // To prevent these races, we need to differentiate between tab stubs from
+ // Java and new tabs from Gecko.
+ let stub = false;
+
+ if (!aParams.zombifying) {
+ if ("tabID" in aParams) {
+ this.id = aParams.tabID;
+ stub = true;
+ } else {
+ let jenv = JNI.GetForThread();
+ let jTabs = JNI.LoadClass(jenv, "org.mozilla.gecko.Tabs", {
+ static_methods: [
+ { name: "getNextTabId", sig: "()I" }
+ ],
+ });
+ this.id = jTabs.getNextTabId();
+ JNI.UnloadClasses(jenv);
+ }
+
+ this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false;
+
+ let message = {
+ type: "Tab:Added",
+ tabID: this.id,
+ uri: truncate(uri, MAX_URI_LENGTH),
+ parentId: ("parentId" in aParams) ? aParams.parentId : -1,
+ tabIndex: ("tabIndex" in aParams) ? aParams.tabIndex : -1,
+ external: ("external" in aParams) ? aParams.external : false,
+ selected: ("selected" in aParams || aParams.cancelEditMode === true) ? aParams.selected : true,
+ cancelEditMode: aParams.cancelEditMode === true,
+ title: truncate(title, MAX_TITLE_LENGTH),
+ delayLoad: aParams.delayLoad || false,
+ desktopMode: this.desktopMode,
+ isPrivate: isPrivate,
+ stub: stub
+ };
+ Messaging.sendRequest(message);
+ }
+
+ let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
+ Ci.nsIWebProgress.NOTIFY_LOCATION |
+ Ci.nsIWebProgress.NOTIFY_SECURITY;
+ this.filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"].createInstance(Ci.nsIWebProgress);
+ this.filter.addProgressListener(this, flags)
+ this.browser.addProgressListener(this.filter, flags);
+ this.browser.sessionHistory.addSHistoryListener(this);
+
+ this.browser.addEventListener("DOMContentLoaded", this, true);
+ this.browser.addEventListener("DOMFormHasPassword", this, true);
+ this.browser.addEventListener("DOMInputPasswordAdded", this, true);
+ this.browser.addEventListener("DOMLinkAdded", this, true);
+ this.browser.addEventListener("DOMLinkChanged", this, true);
+ this.browser.addEventListener("DOMMetaAdded", this, false);
+ this.browser.addEventListener("DOMTitleChanged", this, true);
+ this.browser.addEventListener("DOMAudioPlaybackStarted", this, true);
+ this.browser.addEventListener("DOMAudioPlaybackStopped", this, true);
+ this.browser.addEventListener("DOMWindowClose", this, true);
+ this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
+ this.browser.addEventListener("DOMAutoComplete", this, true);
+ this.browser.addEventListener("blur", this, true);
+ this.browser.addEventListener("pageshow", this, true);
+ this.browser.addEventListener("MozApplicationManifest", this, true);
+ this.browser.addEventListener("TabPreZombify", this, true);
+
+ // Note that the XBL binding is untrusted
+ this.browser.addEventListener("PluginBindingAttached", this, true, true);
+ this.browser.addEventListener("VideoBindingAttached", this, true, true);
+ this.browser.addEventListener("VideoBindingCast", this, true, true);
+
+ Services.obs.addObserver(this, "before-first-paint", false);
+ Services.obs.addObserver(this, "media-playback", false);
+ Services.obs.addObserver(this, "media-playback-resumed", false);
+
+ // Always intialise new tabs with basic session store data to avoid
+ // problems with functions that always expect it to be present
+ this.browser.__SS_data = {
+ entries: [{
+ url: aURL,
+ title: truncate(title, MAX_TITLE_LENGTH)
+ }],
+ index: 1,
+ desktopMode: this.desktopMode,
+ isPrivate: isPrivate
+ };
+
+ if (aParams.delayLoad) {
+ // If this is a zombie tab, mark the browser for delay loading, which will
+ // restore the tab when selected using the session data added above
+ this.browser.__SS_restore = true;
+ } else {
+ let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
+ let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
+ let charset = "charset" in aParams ? aParams.charset : null;
+
+ // The search term the user entered to load the current URL
+ this.userRequested = "userRequested" in aParams ? aParams.userRequested : "";
+ this.isSearch = "isSearch" in aParams ? aParams.isSearch : false;
+
+ try {
+ this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData);
+ } catch(e) {
+ let message = {
+ type: "Content:LoadError",
+ tabID: this.id
+ };
+ Messaging.sendRequest(message);
+ dump("Handled load error: " + e);
+ }
+ }
+ },
+
+ /**
+ * Reloads the tab with the desktop mode setting.
+ */
+ reloadWithMode: function (aDesktopMode) {
+ // notify desktopmode for PIDOMWindow
+ let win = this.browser.contentWindow;
+ let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ dwi.setDesktopModeViewport(aDesktopMode);
+
+ // Set desktop mode for tab and send change to Java
+ if (this.desktopMode != aDesktopMode) {
+ this.desktopMode = aDesktopMode;
+ Messaging.sendRequest({
+ type: "DesktopMode:Changed",
+ desktopMode: aDesktopMode,
+ tabID: this.id
+ });
+ }
+
+ // Only reload the page for http/https schemes
+ let currentURI = this.browser.currentURI;
+ if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https"))
+ return;
+
+ let url = currentURI.spec;
+ // We need LOAD_FLAGS_BYPASS_CACHE here since we're changing the User-Agent
+ // string, and servers typically don't use the Vary: User-Agent header, so
+ // not doing this means that we'd get some of the previously cached content.
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
+ if (this.originalURI && !this.originalURI.equals(currentURI)) {
+ // We were redirected; reload the original URL
+ url = this.originalURI.spec;
+ }
+
+ this.browser.docShell.loadURI(url, flags, null, null, null);
+ },
+
+ destroy: function() {
+ if (!this.browser)
+ return;
+
+ this.browser.removeProgressListener(this.filter);
+ this.filter.removeProgressListener(this);
+ this.filter = null;
+ this.browser.sessionHistory.removeSHistoryListener(this);
+
+ this.browser.removeEventListener("DOMContentLoaded", this, true);
+ this.browser.removeEventListener("DOMFormHasPassword", this, true);
+ this.browser.removeEventListener("DOMInputPasswordAdded", this, true);
+ this.browser.removeEventListener("DOMLinkAdded", this, true);
+ this.browser.removeEventListener("DOMLinkChanged", this, true);
+ this.browser.removeEventListener("DOMMetaAdded", this, false);
+ this.browser.removeEventListener("DOMTitleChanged", this, true);
+ this.browser.removeEventListener("DOMAudioPlaybackStarted", this, true);
+ this.browser.removeEventListener("DOMAudioPlaybackStopped", this, true);
+ this.browser.removeEventListener("DOMWindowClose", this, true);
+ this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
+ this.browser.removeEventListener("DOMAutoComplete", this, true);
+ this.browser.removeEventListener("blur", this, true);
+ this.browser.removeEventListener("pageshow", this, true);
+ this.browser.removeEventListener("MozApplicationManifest", this, true);
+ this.browser.removeEventListener("TabPreZombify", this, true);
+
+ this.browser.removeEventListener("PluginBindingAttached", this, true, true);
+ this.browser.removeEventListener("VideoBindingAttached", this, true, true);
+ this.browser.removeEventListener("VideoBindingCast", this, true, true);
+
+ Services.obs.removeObserver(this, "before-first-paint");
+ Services.obs.removeObserver(this, "media-playback", false);
+ Services.obs.removeObserver(this, "media-playback-resumed", false);
+
+ // Make sure the previously selected panel remains selected. The selected panel of a deck is
+ // not stable when panels are removed.
+ let selectedPanel = BrowserApp.deck.selectedPanel;
+ BrowserApp.deck.removeChild(this.browser);
+ BrowserApp.deck.selectedPanel = selectedPanel;
+
+ this.browser = null;
+ },
+
+ // This should be called to update the browser when the tab gets selected/unselected
+ setActive: function setActive(aActive) {
+ if (!this.browser || !this.browser.docShell)
+ return;
+
+ this.lastTouchedAt = Date.now();
+
+ if (aActive) {
+ this.browser.setAttribute("type", "content-primary");
+ this.browser.focus();
+ this.browser.docShellIsActive = true;
+ Reader.updatePageAction(this);
+ ExternalApps.updatePageAction(this.browser.currentURI, this.browser.contentDocument);
+ } else {
+ this.browser.setAttribute("type", "content-targetable");
+ this.browser.docShellIsActive = false;
+ this.browser.blur();
+ }
+ },
+
+ getActive: function getActive() {
+ return this.browser.docShellIsActive;
+ },
+
+ // These constants are used to prioritize high quality metadata over low quality data, so that
+ // we can collect data as we find meta tags, and replace low quality metadata with higher quality
+ // matches. For instance a msApplicationTile icon is a better tile image than an og:image tag.
+ METADATA_GOOD_MATCH: 10,
+ METADATA_NORMAL_MATCH: 1,
+
+ addMetadata: function(type, value, quality = 1) {
+ if (!this.metatags) {
+ this.metatags = {
+ url: this.browser.currentURI.specIgnoringRef
+ };
+ }
+
+ if (type == "touchIconList") {
+ if (!this.metatags['touchIconList']) {
+ this.metatags['touchIconList'] = {};
+ }
+ this.metatags.touchIconList[quality] = value;
+ } else if (!this.metatags[type] || this.metatags[type + "_quality"] < quality) {
+ this.metatags[type] = value;
+ this.metatags[type + "_quality"] = quality;
+ }
+ },
+
+ sanitizeRelString: function(linkRel) {
+ // Sanitize the rel string
+ let list = [];
+ if (linkRel) {
+ list = linkRel.toLowerCase().split(/\s+/);
+ let hash = {};
+ list.forEach(function(value) { hash[value] = true; });
+ list = [];
+ for (let rel in hash)
+ list.push("[" + rel + "]");
+ }
+ return list;
+ },
+
+ makeFaviconMessage: function(eventTarget) {
+ // We want to get the largest icon size possible for our UI.
+ let maxSize = 0;
+
+ // We use the sizes attribute if available
+ // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
+ if (eventTarget.hasAttribute("sizes")) {
+ let sizes = eventTarget.getAttribute("sizes").toLowerCase();
+
+ if (sizes == "any") {
+ // Since Java expects an integer, use -1 to represent icons with sizes="any"
+ maxSize = -1;
+ } else {
+ let tokens = sizes.split(" ");
+ tokens.forEach(function(token) {
+ // TODO: check for invalid tokens
+ let [w, h] = token.split("x");
+ maxSize = Math.max(maxSize, Math.max(w, h));
+ });
+ }
+ }
+ return {
+ type: "Link:Favicon",
+ tabID: this.id,
+ href: resolveGeckoURI(eventTarget.href),
+ size: maxSize,
+ mime: eventTarget.getAttribute("type") || ""
+ };
+ },
+
+ makeFeedMessage: function(eventTarget, targetType) {
+ try {
+ // urlSecurityCeck will throw if things are not OK
+ ContentAreaUtils.urlSecurityCheck(eventTarget.href,
+ eventTarget.ownerDocument.nodePrincipal,
+ Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+
+ if (!this.browser.feeds)
+ this.browser.feeds = [];
+
+ this.browser.feeds.push({
+ href: eventTarget.href,
+ title: eventTarget.title,
+ type: targetType
+ });
+
+ return {
+ type: "Link:Feed",
+ tabID: this.id
+ };
+ } catch (e) {
+ return null;
+ }
+ },
+
+ sendOpenSearchMessage: function(eventTarget) {
+ let type = eventTarget.type && eventTarget.type.toLowerCase();
+ // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
+ type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
+
+ // Check that type matches opensearch.
+ let isOpenSearch = (type == "application/opensearchdescription+xml");
+ if (isOpenSearch && eventTarget.title && /^(?:https?|ftp):/i.test(eventTarget.href)) {
+ Services.search.init(() => {
+ let visibleEngines = Services.search.getVisibleEngines();
+ // NOTE: Engines are currently identified by name, but this can be changed
+ // when Engines are identified by URL (see bug 335102).
+ if (visibleEngines.some(function(e) {
+ return e.name == eventTarget.title;
+ })) {
+ // This engine is already present, do nothing.
+ return null;
+ }
+
+ if (this.browser.engines) {
+ // This engine has already been handled, do nothing.
+ if (this.browser.engines.some(function(e) {
+ return e.url == eventTarget.href;
+ })) {
+ return null;
+ }
+ } else {
+ this.browser.engines = [];
+ }
+
+ // Get favicon.
+ let iconURL = eventTarget.ownerDocument.documentURIObject.prePath + "/favicon.ico";
+
+ let newEngine = {
+ title: eventTarget.title,
+ url: eventTarget.href,
+ iconURL: iconURL
+ };
+
+ this.browser.engines.push(newEngine);
+
+ // Don't send a message to display engines if we've already handled an engine.
+ if (this.browser.engines.length > 1)
+ return null;
+
+ // Broadcast message that this tab contains search engines that should be visible.
+ Messaging.sendRequest({
+ type: "Link:OpenSearch",
+ tabID: this.id,
+ visible: true
+ });
+ });
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "DOMContentLoaded": {
+ let target = aEvent.originalTarget;
+
+ // ignore on frames and other documents
+ if (target != this.browser.contentDocument)
+ return;
+
+ // Sample the background color of the page and pass it along. (This is used to draw the
+ // checkerboard.) Right now we don't detect changes in the background color after this
+ // event fires; it's not clear that doing so is worth the effort.
+ var backgroundColor = null;
+ try {
+ let { contentDocument, contentWindow } = this.browser;
+ let computedStyle = contentWindow.getComputedStyle(contentDocument.body);
+ backgroundColor = computedStyle.backgroundColor;
+ } catch (e) {
+ // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds.
+ }
+
+ let docURI = target.documentURI;
+ let errorType = "";
+ if (docURI.startsWith("about:certerror")) {
+ errorType = "certerror";
+ }
+ else if (docURI.startsWith("about:blocked")) {
+ errorType = "blocked";
+ }
+ else if (docURI.startsWith("about:neterror")) {
+ let error = docURI.search(/e\=/);
+ let duffUrl = docURI.search(/\&u\=/);
+ let errorExtra = decodeURIComponent(docURI.slice(error + 2, duffUrl));
+ // Here is a list of errorExtra types (et_*)
+ // http://mxr.mozilla.org/mozilla-central/source/mobile/android/chrome/content/netError.xhtml#287
+ UITelemetry.addEvent("neterror.1", "content", null, errorExtra);
+ errorType = "neterror";
+ }
+
+ // 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.
+ if (docURI.startsWith("about:neterror")) {
+ NetErrorHelper.attachToBrowser(this.browser);
+ }
+
+ Messaging.sendRequest({
+ type: "DOMContentLoaded",
+ tabID: this.id,
+ bgColor: backgroundColor,
+ errorType: errorType,
+ metadata: this.metatags,
+ });
+
+ // Reset isSearch so that the userRequested term will be erased on next page load
+ this.metatags = null;
+
+ if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) {
+ this.browser.addEventListener("click", ErrorPageEventHandler, true);
+ let listener = function() {
+ this.browser.removeEventListener("click", ErrorPageEventHandler, true);
+ this.browser.removeEventListener("pagehide", listener, true);
+ }.bind(this);
+
+ this.browser.addEventListener("pagehide", listener, true);
+ }
+
+ if (AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_ANDROID_ACTIVITY_STREAM) {
+ WebsiteMetadata.parseAsynchronously(this.browser.contentDocument);
+ }
+
+ break;
+ }
+
+ case "DOMFormHasPassword": {
+ LoginManagerContent.onDOMFormHasPassword(aEvent,
+ this.browser.contentWindow);
+
+ // Send logins for this hostname to Java.
+ let hostname = aEvent.target.baseURIObject.prePath;
+ let foundLogins = Services.logins.findLogins({}, hostname, "", "");
+ if (foundLogins.length > 0) {
+ let displayHost = IdentityHandler.getEffectiveHost();
+ let title = { text: displayHost, resource: hostname };
+ let selectObj = { title: title, logins: foundLogins };
+ Messaging.sendRequest({ type: "Doorhanger:Logins", data: selectObj });
+ }
+ break;
+ }
+
+ case "DOMInputPasswordAdded": {
+ LoginManagerContent.onDOMInputPasswordAdded(aEvent,
+ this.browser.contentWindow);
+ }
+
+ case "DOMMetaAdded":
+ let target = aEvent.originalTarget;
+ let browser = BrowserApp.getBrowserForDocument(target.ownerDocument);
+
+ switch (target.name) {
+ case "msapplication-TileImage":
+ this.addMetadata("tileImage", browser.currentURI.resolve(target.content), this.METADATA_GOOD_MATCH);
+ break;
+ case "msapplication-TileColor":
+ this.addMetadata("tileColor", target.content, this.METADATA_GOOD_MATCH);
+ break;
+ }
+
+ break;
+
+ case "DOMLinkAdded":
+ case "DOMLinkChanged": {
+ let jsonMessage = null;
+ let target = aEvent.originalTarget;
+ if (!target.href || target.disabled)
+ return;
+
+ // Ignore on frames and other documents
+ if (target.ownerDocument != this.browser.contentDocument)
+ return;
+
+ // Sanitize rel link
+ let list = this.sanitizeRelString(target.rel);
+ if (list.indexOf("[icon]") != -1) {
+ jsonMessage = this.makeFaviconMessage(target);
+ } else if (list.indexOf("[apple-touch-icon]") != -1 ||
+ list.indexOf("[apple-touch-icon-precomposed]") != -1) {
+ jsonMessage = this.makeFaviconMessage(target);
+ jsonMessage['type'] = 'Link:Touchicon';
+ this.addMetadata("touchIconList", jsonMessage.href, jsonMessage.size);
+ } else if (list.indexOf("[alternate]") != -1 && aEvent.type == "DOMLinkAdded") {
+ let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
+ let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
+
+ if (!isFeed)
+ return;
+
+ jsonMessage = this.makeFeedMessage(target, type);
+ } else if (list.indexOf("[search]") != -1 && aEvent.type == "DOMLinkAdded") {
+ this.sendOpenSearchMessage(target);
+ }
+ if (!jsonMessage)
+ return;
+
+ Messaging.sendRequest(jsonMessage);
+ break;
+ }
+
+ case "DOMTitleChanged": {
+ if (!aEvent.isTrusted)
+ return;
+
+ // ignore on frames and other documents
+ if (aEvent.originalTarget != this.browser.contentDocument)
+ return;
+
+ Messaging.sendRequest({
+ type: "DOMTitleChanged",
+ tabID: this.id,
+ title: truncate(aEvent.target.title, MAX_TITLE_LENGTH)
+ });
+ break;
+ }
+
+ case "TabPreZombify": {
+ if (!this.playingAudio) {
+ return;
+ }
+ // Fall through to the DOMAudioPlayback events, so the
+ // audio playback indicator gets reset upon zombification.
+ }
+ case "DOMAudioPlaybackStarted":
+ case "DOMAudioPlaybackStopped": {
+ if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
+ !aEvent.isTrusted) {
+ return;
+ }
+
+ let browser = aEvent.originalTarget;
+ if (browser != this.browser) {
+ return;
+ }
+
+ this.playingAudio = aEvent.type === "DOMAudioPlaybackStarted";
+
+ Messaging.sendRequest({
+ type: "Tab:AudioPlayingChange",
+ tabID: this.id,
+ isAudioPlaying: this.playingAudio
+ });
+ return;
+ }
+
+ case "DOMWindowClose": {
+ if (!aEvent.isTrusted)
+ return;
+
+ // Find the relevant tab, and close it from Java
+ if (this.browser.contentWindow == aEvent.target) {
+ aEvent.preventDefault();
+
+ Messaging.sendRequest({
+ type: "Tab:Close",
+ tabID: this.id
+ });
+ }
+ break;
+ }
+
+ case "DOMWillOpenModalDialog": {
+ if (!aEvent.isTrusted)
+ return;
+
+ // We're about to open a modal dialog, make sure the opening
+ // tab is brought to the front.
+ let tab = BrowserApp.getTabForWindow(aEvent.target.top);
+ BrowserApp.selectTab(tab);
+ break;
+ }
+
+ case "DOMAutoComplete":
+ case "blur": {
+ LoginManagerContent.onUsernameInput(aEvent);
+ break;
+ }
+
+ case "PluginBindingAttached": {
+ PluginHelper.handlePluginBindingAttached(this, aEvent);
+ break;
+ }
+
+ case "VideoBindingAttached": {
+ CastingApps.handleVideoBindingAttached(this, aEvent);
+ break;
+ }
+
+ case "VideoBindingCast": {
+ CastingApps.handleVideoBindingCast(this, aEvent);
+ break;
+ }
+
+ case "MozApplicationManifest": {
+ OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView);
+ break;
+ }
+
+ case "pageshow": {
+ LoginManagerContent.onPageShow(aEvent, this.browser.contentWindow);
+
+ // The rest of this only handles pageshow for the top-level document.
+ if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
+ return;
+
+ let target = aEvent.originalTarget;
+ let docURI = target.documentURI;
+ if (!docURI.startsWith("about:neterror") && !this.isSearch) {
+ // If this wasn't an error page and the user isn't search, don't retain the typed entry
+ this.userRequested = "";
+ }
+
+ Messaging.sendRequest({
+ type: "Content:PageShow",
+ tabID: this.id,
+ userRequested: this.userRequested,
+ fromCache: Tabs.useCache
+ });
+
+ this.isSearch = false;
+
+ if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
+ if (!this._linkifier)
+ this._linkifier = new Linkifier();
+ this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
+ }
+
+ // Update page actions for helper apps.
+ let uri = this.browser.currentURI;
+ if (BrowserApp.selectedTab == this) {
+ if (ExternalApps.shouldCheckUri(uri)) {
+ ExternalApps.updatePageAction(uri, this.browser.contentDocument);
+ } else {
+ ExternalApps.clearPageAction();
+ }
+ }
+ }
+ }
+ },
+
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+ let contentWin = aWebProgress.DOMWindow;
+ if (contentWin != contentWin.top)
+ return;
+
+ // Filter optimization: Only really send NETWORK state changes to Java listener
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ if (AppConstants.NIGHTLY_BUILD && (aStateFlags & Ci.nsIWebProgressListener.STATE_START)) {
+ Profiler.AddMarker("Load start: " + aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec);
+ } else if (AppConstants.NIGHTLY_BUILD && (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && !aWebProgress.isLoadingDocument) {
+ Profiler.AddMarker("Load stop: " + aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec);
+ }
+
+ if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) {
+ // We may receive a document stop event while a document is still loading
+ // (such as when doing URI fixup). Don't notify Java UI in these cases.
+ return;
+ }
+
+ // Clear page-specific opensearch engines and feeds for a new request.
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) {
+ this.browser.engines = null;
+ this.browser.feeds = null;
+ }
+
+ // true if the page loaded successfully (i.e., no 404s or other errors)
+ let success = false;
+ let uri = "";
+ try {
+ // Remember original URI for UA changes on redirected pages
+ this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI;
+
+ if (this.originalURI != null)
+ uri = this.originalURI.spec;
+ } catch (e) { }
+ try {
+ success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded;
+ } catch (e) {
+ // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success
+ // status. Used for local files. See bug 948849.
+ success = aRequest.status == 0;
+ }
+
+ // Check to see if we restoring the content from a previous presentation (session)
+ // since there should be no real network activity
+ let restoring = (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) > 0;
+
+ let message = {
+ type: "Content:StateChange",
+ tabID: this.id,
+ uri: truncate(uri, MAX_URI_LENGTH),
+ state: aStateFlags,
+ restoring: restoring,
+ success: success
+ };
+ Messaging.sendRequest(message);
+ }
+ },
+
+ onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
+ let contentWin = aWebProgress.DOMWindow;
+
+ // Browser webapps may load content inside iframes that can not reach across the app/frame boundary
+ // i.e. even though the page is loaded in an iframe window.top != webapp
+ // Make cure this window is a top level tab before moving on.
+ if (BrowserApp.getBrowserForWindow(contentWin) == null)
+ return;
+
+ this._hostChanged = true;
+
+ let fixedURI = aLocationURI;
+ try {
+ fixedURI = URIFixup.createExposableURI(aLocationURI);
+ } catch (ex) { }
+
+ // In restricted profiles, we refuse to let you open various urls.
+ if (!ParentalControls.isAllowed(ParentalControls.BROWSE, fixedURI)) {
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+
+ this.browser.docShell.displayLoadError(Cr.NS_ERROR_UNKNOWN_PROTOCOL, fixedURI, null);
+ }
+
+ let contentType = contentWin.document.contentType;
+
+ // If fixedURI matches browser.lastURI, we assume this isn't a real location
+ // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883.
+ // Note that we have to ensure fixedURI is not the same as aLocationURI so we
+ // don't false-positive page reloads as spurious additions.
+ let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 ||
+ ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI));
+ this.browser.lastURI = fixedURI;
+
+ // Let the reader logic know about same document changes because we won't get a DOMContentLoaded
+ // or pageshow event, but we'll still want to update the reader view button to account for this change.
+ // This mirrors the desktop logic in TabsProgressListener.
+ if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
+ this.browser.messageManager.sendAsyncMessage("Reader:PushState", {isArticle: this.browser.isArticle});
+ }
+
+ // Reset state of click-to-play plugin notifications.
+ clearTimeout(this.pluginDoorhangerTimeout);
+ this.pluginDoorhangerTimeout = null;
+ this.shouldShowPluginDoorhanger = true;
+ this.clickToPlayPluginsActivated = false;
+
+ let documentURI = contentWin.document.documentURIObject.spec;
+
+ // If reader mode, get the base domain for the original url.
+ let strippedURI = this._stripAboutReaderURL(documentURI);
+
+ // Borrowed from desktop Firefox: http://hg.mozilla.org/mozilla-central/annotate/72835344333f/browser/base/content/urlbarBindings.xml#l236
+ let matchedURL = strippedURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
+ let baseDomain = "";
+ if (matchedURL) {
+ var domain = "";
+ [, , domain] = matchedURL;
+
+ 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 we are navigating to a new location with a different host,
+ // clear any URL origin that might have been pinned to this tab.
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ let appOrigin = ss.getTabValue(this, "appOrigin");
+ if (appOrigin) {
+ let originHost = "";
+ try {
+ originHost = Services.io.newURI(appOrigin, null, null).host;
+ } catch (e if (e.result == Cr.NS_ERROR_FAILURE)) {
+ // NS_ERROR_FAILURE can be thrown by nsIURI.host if the URI scheme does not possess a host - in this case
+ // we just act as if we have an empty host.
+ }
+ if (originHost != aLocationURI.host) {
+ // Note: going 'back' will not make this tab pinned again
+ ss.deleteTabValue(this, "appOrigin");
+ }
+ }
+
+ // Update the page actions URI for helper apps.
+ if (BrowserApp.selectedTab == this) {
+ ExternalApps.updatePageActionUri(fixedURI);
+ }
+
+ let webNav = contentWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
+
+ let message = {
+ type: "Content:LocationChange",
+ tabID: this.id,
+ uri: truncate(fixedURI.spec, MAX_URI_LENGTH),
+ userRequested: this.userRequested || "",
+ baseDomain: baseDomain,
+ contentType: (contentType ? contentType : ""),
+ sameDocument: sameDocument,
+
+ historyIndex: webNav.sessionHistory.index,
+ historySize: webNav.sessionHistory.count,
+ canGoBack: webNav.canGoBack,
+ canGoForward: webNav.canGoForward,
+ };
+
+ Messaging.sendRequest(message);
+
+ if (!sameDocument) {
+ // XXX This code assumes that this is the earliest hook we have at which
+ // browser.contentDocument is changed to the new document we're loading
+ this.contentDocumentIsDisplayed = false;
+ this.hasTouchListener = false;
+ Services.obs.notifyObservers(this.browser, "Session:NotifyLocationChange", null);
+ }
+ },
+
+ _stripAboutReaderURL: function (url) {
+ return ReaderMode.getOriginalUrl(url) || url;
+ },
+
+ // 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)
+ return;
+
+ this._state = aState;
+ this._hostChanged = false;
+
+ let identity = IdentityHandler.checkIdentity(aState, this.browser);
+
+ let message = {
+ type: "Content:SecurityChange",
+ tabID: this.id,
+ identity: identity
+ };
+
+ Messaging.sendRequest(message);
+ },
+
+ onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
+ // Note: aWebProgess and aRequest will be NULL since we are filtering webprogress
+ // notifications using nsBrowserStatusFilter.
+ },
+
+ onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
+ // Note: aWebProgess and aRequest will be NULL since we are filtering webprogress
+ // notifications using nsBrowserStatusFilter.
+ },
+
+ _getGeckoZoom: function() {
+ let res = {};
+ let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ cwu.getResolution(res);
+ let zoom = res.value * window.devicePixelRatio;
+ return zoom;
+ },
+
+ saveSessionZoom: function(aZoom) {
+ let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ cwu.setResolutionAndScaleTo(aZoom / window.devicePixelRatio);
+ },
+
+ restoredSessionZoom: function() {
+ let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+
+ if (this._restoreZoom && cwu.isResolutionSet) {
+ return this._getGeckoZoom();
+ }
+ return null;
+ },
+
+ _updateZoomFromHistoryEvent: function(aHistoryEventName) {
+ // Restore zoom only when moving in session history, not for new page loads.
+ this._restoreZoom = aHistoryEventName !== "New";
+ },
+
+ OnHistoryNewEntry: function(aUri) {
+ this._updateZoomFromHistoryEvent("New");
+ },
+
+ OnHistoryGoBack: function(aUri) {
+ this._updateZoomFromHistoryEvent("Back");
+ return true;
+ },
+
+ OnHistoryGoForward: function(aUri) {
+ this._updateZoomFromHistoryEvent("Forward");
+ return true;
+ },
+
+ OnHistoryReload: function(aUri, aFlags) {
+ // we don't do anything with this, so don't propagate it
+ // for now anyway
+ return true;
+ },
+
+ OnHistoryGotoIndex: function(aIndex, aUri) {
+ this._updateZoomFromHistoryEvent("Goto");
+ return true;
+ },
+
+ OnHistoryPurge: function(aNumEntries) {
+ this._updateZoomFromHistoryEvent("Purge");
+ return true;
+ },
+
+ OnHistoryReplaceEntry: function(aIndex) {
+ // we don't do anything with this, so don't propogate it
+ // for now anyway.
+ },
+
+ ShouldNotifyMediaPlaybackChange: function(activeState) {
+ // If the media is active, we would check it's duration, because we don't
+ // want to show the media control interface for the short sound which
+ // duration is smaller than the threshold. The basic unit is second.
+ // Note : the streaming format's duration is infinite.
+ if (activeState === "inactive") {
+ return true;
+ }
+
+ const mediaDurationThreshold = 1.0;
+
+ let audioElements = this.browser.contentDocument.getElementsByTagName("audio");
+ for each (let audio in audioElements) {
+ if (!audio.paused && audio.duration < mediaDurationThreshold) {
+ return false;
+ }
+ }
+
+ let videoElements = this.browser.contentDocument.getElementsByTagName("video");
+ for each (let video in videoElements) {
+ if (!video.paused && video.duration < mediaDurationThreshold) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "before-first-paint":
+ // Is it on the top level?
+ let contentDocument = aSubject;
+ if (contentDocument == this.browser.contentDocument) {
+ if (BrowserApp.selectedTab == this) {
+ BrowserApp.contentDocumentChanged();
+ }
+ this.contentDocumentIsDisplayed = true;
+
+ if (contentDocument instanceof Ci.nsIImageDocument) {
+ contentDocument.shrinkToFit();
+ }
+ }
+ break;
+
+ case "media-playback":
+ case "media-playback-resumed":
+ if (!aSubject) {
+ return;
+ }
+
+ let winId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (this.browser.outerWindowID != winId) {
+ return;
+ }
+
+ if (!this.ShouldNotifyMediaPlaybackChange(aData)) {
+ return;
+ }
+
+ let status;
+ if (aTopic == "media-playback") {
+ status = (aData === "inactive") ? "end" : "start";
+ } else if (aTopic == "media-playback-resumed") {
+ status = "resume";
+ }
+
+ Messaging.sendRequest({
+ type: "Tab:MediaPlaybackChange",
+ tabID: this.id,
+ status: status
+ });
+ break;
+ }
+ },
+
+ // nsIBrowserTab
+ get window() {
+ if (!this.browser)
+ return null;
+ return this.browser.contentWindow;
+ },
+
+ get scale() {
+ return this._zoom;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIWebProgressListener,
+ Ci.nsISHistoryListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIBrowserTab
+ ])
+};
+
+var BrowserEventHandler = {
+ init: function init() {
+ this._clickInZoomedView = false;
+ Services.obs.addObserver(this, "Gesture:SingleTap", false);
+ Services.obs.addObserver(this, "Gesture:ClickInZoomedView", false);
+
+ BrowserApp.deck.addEventListener("touchend", this, true);
+
+ BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false);
+ BrowserApp.deck.addEventListener("MozMouseHittest", this, true);
+ BrowserApp.deck.addEventListener("OpenMediaWithExternalApp", this, true);
+
+ InitLater(() => BrowserApp.deck.addEventListener("click", InputWidgetHelper, true));
+ InitLater(() => BrowserApp.deck.addEventListener("click", SelectHelper, true));
+
+ // ReaderViews support backPress listeners.
+ Messaging.addListener(() => {
+ return Reader.onBackPress(BrowserApp.selectedTab.id);
+ }, "Browser:OnBackPressed");
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case 'touchend':
+ if (this._inCluster) {
+ aEvent.preventDefault();
+ }
+ break;
+ case 'MozMouseHittest':
+ this._handleRetargetedTouchStart(aEvent);
+ break;
+ case 'OpenMediaWithExternalApp': {
+ let mediaSrc = aEvent.target.currentSrc || aEvent.target.src;
+ let uuid = uuidgen.generateUUID().toString();
+ Services.androidBridge.handleGeckoMessage({
+ type: "Video:Play",
+ uri: mediaSrc,
+ uuid: uuid
+ });
+ break;
+ }
+ }
+ },
+
+ _handleRetargetedTouchStart: function(aEvent) {
+ // we should only get this called just after a new touchstart with a single
+ // touch point.
+ if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.defaultPrevented) {
+ return;
+ }
+
+ let target = aEvent.target;
+ if (!target) {
+ return;
+ }
+
+ this._inCluster = aEvent.hitCluster;
+ if (this._inCluster) {
+ return; // No highlight for a cluster of links
+ }
+
+ let uri = this._getLinkURI(target);
+ if (uri) {
+ try {
+ Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+ } catch (e) {}
+ }
+ this._doTapHighlight(target);
+ },
+
+ _getLinkURI: function(aElement) {
+ if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
+ ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
+ (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) {
+ try {
+ return Services.io.newURI(aElement.href, null, null);
+ } catch (e) {}
+ }
+ return null;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ // the remaining events are all dependent on the browser content document being the
+ // same as the browser displayed document. if they are not the same, we should ignore
+ // the event.
+ if (BrowserApp.isBrowserContentDocumentDisplayed()) {
+ this.handleUserEvent(aTopic, aData);
+ }
+ },
+
+ handleUserEvent: function(aTopic, aData) {
+ switch (aTopic) {
+
+ case "Gesture:ClickInZoomedView":
+ this._clickInZoomedView = true;
+ break;
+
+ case "Gesture:SingleTap": {
+ let focusedElement = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser);
+ let data = JSON.parse(aData);
+ let {x, y} = data;
+
+ if (this._inCluster && this._clickInZoomedView != true) {
+ // If there is a focused element, the display of the zoomed view won't remove the focus.
+ // In this case, the form assistant linked to the focused element will never be closed.
+ // To avoid this situation, the focus is moved and the form assistant is closed.
+ if (focusedElement) {
+ try {
+ Services.focus.moveFocus(BrowserApp.selectedBrowser.contentWindow, null, Services.focus.MOVEFOCUS_ROOT, 0);
+ } catch(e) {
+ Cu.reportError(e);
+ }
+ Messaging.sendRequest({ type: "FormAssist:Hide" });
+ }
+ this._clusterClicked(x, y);
+ } else {
+ if (this._clickInZoomedView != true) {
+ this._closeZoomedView();
+ }
+ }
+ this._clickInZoomedView = false;
+ this._cancelTapHighlight();
+ break;
+ }
+
+ default:
+ dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
+ break;
+ }
+ },
+
+ _closeZoomedView: function() {
+ Messaging.sendRequest({
+ type: "Gesture:CloseZoomedView"
+ });
+ },
+
+ _clusterClicked: function(aX, aY) {
+ Messaging.sendRequest({
+ type: "Gesture:clusteredLinksClicked",
+ clickPosition: {
+ x: aX,
+ y: aY
+ }
+ });
+ },
+
+ _highlightElement: null,
+
+ _doTapHighlight: function _doTapHighlight(aElement) {
+ this._highlightElement = aElement;
+ },
+
+ _cancelTapHighlight: function _cancelTapHighlight() {
+ if (!this._highlightElement)
+ return;
+
+ this._highlightElement = null;
+ }
+};
+
+const ElementTouchHelper = {
+ getBoundingContentRect: function(aElement) {
+ if (!aElement)
+ return {x: 0, y: 0, w: 0, h: 0};
+
+ let document = aElement.ownerDocument;
+ while (document.defaultView.frameElement)
+ document = document.defaultView.frameElement.ownerDocument;
+
+ let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {};
+ cwu.getScrollXY(false, scrollX, scrollY);
+
+ let r = aElement.getBoundingClientRect();
+
+ // step out of iframes and frames, offsetting scroll values
+ for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) {
+ // adjust client coordinates' origin to be top left of iframe viewport
+ let rect = frame.frameElement.getBoundingClientRect();
+ let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
+ let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
+ scrollX.value += rect.left + parseInt(left);
+ scrollY.value += rect.top + parseInt(top);
+ }
+
+ return {x: r.left + scrollX.value,
+ y: r.top + scrollY.value,
+ w: r.width,
+ h: r.height };
+ }
+};
+
+var ErrorPageEventHandler = {
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "click": {
+ // Don't trust synthetic events
+ if (!aEvent.isTrusted)
+ return;
+
+ let target = aEvent.originalTarget;
+ let errorDoc = target.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 (errorDoc.documentURI.startsWith("about:certerror?e=nssBadCert")) {
+ let perm = errorDoc.getElementById("permanentExceptionButton");
+ let temp = errorDoc.getElementById("temporaryExceptionButton");
+ if (target == temp || target == perm) {
+ // Handle setting an cert exception and reloading the page
+ try {
+ // Add a new SSL exception for this URL
+ let uri = Services.io.newURI(errorDoc.location.href, null, null);
+ let sslExceptions = new SSLExceptions();
+
+ if (target == perm)
+ sslExceptions.addPermanentException(uri, errorDoc.defaultView);
+ else
+ sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
+ } catch (e) {
+ dump("Failed to set cert exception: " + e + "\n");
+ }
+ errorDoc.location.reload();
+ } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) {
+ errorDoc.location = "about:home";
+ }
+ } else if (errorDoc.documentURI.startsWith("about:blocked")) {
+ // The event came from a button on a malware/phishing block page
+ // First check whether it's malware, phishing or unwanted, so that we
+ // can use the right strings/links
+ let bucketName = "";
+ let sendTelemetry = false;
+ if (errorDoc.documentURI.includes("e=malwareBlocked")) {
+ sendTelemetry = true;
+ bucketName = "WARNING_MALWARE_PAGE_";
+ } else if (errorDoc.documentURI.includes("e=deceptiveBlocked")) {
+ sendTelemetry = true;
+ bucketName = "WARNING_PHISHING_PAGE_";
+ } else if (errorDoc.documentURI.includes("e=unwantedBlocked")) {
+ sendTelemetry = true;
+ bucketName = "WARNING_UNWANTED_PAGE_";
+ }
+ let nsISecTel = Ci.nsISecurityUITelemetry;
+ let isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView);
+ bucketName += isIframe ? "TOP_" : "FRAME_";
+
+ let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+
+ if (target == errorDoc.getElementById("getMeOutButton")) {
+ if (sendTelemetry) {
+ Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
+ }
+ errorDoc.location = "about:home";
+ } else if (target == errorDoc.getElementById("reportButton")) {
+ // We log even if malware/phishing info URL couldn't be found:
+ // the measurement is for how many users clicked the WHY BLOCKED button
+ if (sendTelemetry) {
+ Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "WHY_BLOCKED"]);
+ }
+
+ // This is the "Why is this site blocked" button. We redirect
+ // to the generic page describing phishing/malware protection.
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ BrowserApp.selectedBrowser.loadURI(url + "phishing-malware");
+ } else if (target == errorDoc.getElementById("ignoreWarningButton") &&
+ Services.prefs.getBoolPref("browser.safebrowsing.allowOverride")) {
+ if (sendTelemetry) {
+ Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "IGNORE_WARNING"]);
+ }
+
+ // Allow users to override and continue through to the site,
+ let webNav = BrowserApp.selectedBrowser.docShell.QueryInterface(Ci.nsIWebNavigation);
+ let location = BrowserApp.selectedBrowser.contentWindow.location;
+ webNav.loadURI(location, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, null, null, null);
+
+ // ....but add a notify bar as a reminder, so that they don't lose
+ // track after, e.g., tab switching.
+ NativeWindow.doorhanger.show(Strings.browser.GetStringFromName("safeBrowsingDoorhanger"), "safebrowsing-warning", [], BrowserApp.selectedTab.id);
+ }
+ }
+ break;
+ }
+ }
+ }
+};
+
+var FormAssistant = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
+
+ // Used to keep track of the element that corresponds to the current
+ // autocomplete suggestions
+ _currentInputElement: null,
+
+ // The value of the currently focused input
+ _currentInputValue: null,
+
+ // Whether we're in the middle of an autocomplete
+ _doingAutocomplete: false,
+
+ // Keep track of whether or not an invalid form has been submitted
+ _invalidSubmit: false,
+
+ init: function() {
+ Services.obs.addObserver(this, "FormAssist:AutoComplete", false);
+ Services.obs.addObserver(this, "FormAssist:Hidden", false);
+ Services.obs.addObserver(this, "FormAssist:Remove", false);
+ Services.obs.addObserver(this, "invalidformsubmit", false);
+ Services.obs.addObserver(this, "PanZoom:StateChange", false);
+
+ // We need to use a capturing listener for focus events
+ BrowserApp.deck.addEventListener("focus", this, true);
+ BrowserApp.deck.addEventListener("blur", this, true);
+ BrowserApp.deck.addEventListener("click", this, true);
+ BrowserApp.deck.addEventListener("input", this, false);
+ BrowserApp.deck.addEventListener("pageshow", this, false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "PanZoom:StateChange":
+ // If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing
+ if (aData == "TOUCHING" || aData == "WAITING_LISTENERS")
+ break;
+ if (aData == "NOTHING") {
+ // only look for input elements, not contentEditable or multiline text areas
+ let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
+ if (!focused)
+ break;
+
+ if (this._showValidationMessage(focused))
+ break;
+ let checkResultsClick = hasResults => {
+ if (!hasResults) {
+ this._hideFormAssistPopup();
+ }
+ };
+ this._showAutoCompleteSuggestions(focused, checkResultsClick);
+ } else {
+ // temporarily hide the form assist popup while we're panning or zooming the page
+ this._hideFormAssistPopup();
+ }
+ break;
+ case "FormAssist:AutoComplete":
+ if (!this._currentInputElement)
+ break;
+
+ let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
+
+ this._doingAutocomplete = true;
+
+ // If we have an active composition string, commit it before sending
+ // the autocomplete event with the text that will replace it.
+ try {
+ let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
+ if (imeEditor.composing)
+ imeEditor.forceCompositionEnd();
+ } catch (e) {}
+
+ editableElement.setUserInput(aData);
+ this._currentInputValue = aData;
+
+ let event = this._currentInputElement.ownerDocument.createEvent("Events");
+ event.initEvent("DOMAutoComplete", true, true);
+ this._currentInputElement.dispatchEvent(event);
+
+ this._doingAutocomplete = false;
+
+ break;
+
+ case "FormAssist:Hidden":
+ this._currentInputElement = null;
+ break;
+
+ case "FormAssist:Remove":
+ if (!this._currentInputElement) {
+ break;
+ }
+
+ FormHistory.update({
+ op: "remove",
+ fieldname: this._currentInputElement.name,
+ value: aData
+ });
+ break;
+ }
+ },
+
+ notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
+ if (!aInvalidElements.length)
+ return;
+
+ // Ignore this notificaiton if the current tab doesn't contain the invalid element
+ let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
+ if (BrowserApp.selectedBrowser.contentDocument !=
+ currentElement.ownerDocument.defaultView.top.document)
+ return;
+
+ this._invalidSubmit = true;
+
+ // Our focus listener will show the element's validation message
+ currentElement.focus();
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "focus": {
+ let currentElement = aEvent.target;
+
+ // Only show a validation message on focus.
+ this._showValidationMessage(currentElement);
+ break;
+ }
+
+ case "blur": {
+ this._currentInputValue = null;
+ break;
+ }
+
+ case "click": {
+ let currentElement = aEvent.target;
+
+ // Prioritize a form validation message over autocomplete suggestions
+ // when the element is first focused (a form validation message will
+ // only be available if an invalid form was submitted)
+ if (this._showValidationMessage(currentElement))
+ break;
+
+ let checkResultsClick = hasResults => {
+ if (!hasResults) {
+ this._hideFormAssistPopup();
+ }
+ };
+
+ this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
+ break;
+ }
+
+ case "input": {
+ let currentElement = aEvent.target;
+
+ // If this element isn't focused, we're already in middle of an
+ // autocomplete, or its value hasn't changed, don't show the
+ // autocomplete popup.
+ if (currentElement !== BrowserApp.getFocusedInput(BrowserApp.selectedBrowser) ||
+ this._doingAutocomplete ||
+ currentElement.value === this._currentInputValue) {
+ break;
+ }
+
+ this._currentInputValue = currentElement.value;
+
+ // Since we can only show one popup at a time, prioritze autocomplete
+ // suggestions over a form validation message
+ let checkResultsInput = hasResults => {
+ if (hasResults)
+ return;
+
+ if (this._showValidationMessage(currentElement))
+ return;
+
+ // If we're not showing autocomplete suggestions, hide the form assist popup
+ this._hideFormAssistPopup();
+ };
+
+ this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
+ break;
+ }
+
+ // Reset invalid submit state on each pageshow
+ case "pageshow": {
+ if (!this._invalidSubmit)
+ return;
+
+ let selectedBrowser = BrowserApp.selectedBrowser;
+ if (selectedBrowser) {
+ let selectedDocument = selectedBrowser.contentDocument;
+ let target = aEvent.originalTarget;
+ if (target == selectedDocument || target.ownerDocument == selectedDocument)
+ this._invalidSubmit = false;
+ }
+ break;
+ }
+ }
+ },
+
+ // We only want to show autocomplete suggestions for certain elements
+ _isAutoComplete: function _isAutoComplete(aElement) {
+ if (!(aElement instanceof HTMLInputElement) || aElement.readOnly || aElement.disabled ||
+ (aElement.getAttribute("type") == "password") ||
+ (aElement.hasAttribute("autocomplete") &&
+ aElement.getAttribute("autocomplete").toLowerCase() == "off"))
+ return false;
+
+ return true;
+ },
+
+ // Retrieves autocomplete suggestions for an element from the form autocomplete service.
+ // aCallback(array_of_suggestions) is called when results are available.
+ _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
+ // Cache the form autocomplete service for future use
+ if (!this._formAutoCompleteService) {
+ this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"]
+ .getService(Ci.nsIFormAutoComplete);
+ }
+
+ let resultsAvailable = function (results) {
+ let suggestions = [];
+ for (let i = 0; i < results.matchCount; i++) {
+ let value = results.getValueAt(i);
+
+ // Do not show the value if it is the current one in the input field
+ if (value == aSearchString)
+ continue;
+
+ // Supply a label and value, since they can differ for datalist suggestions
+ suggestions.push({ label: value, value: value });
+ }
+ aCallback(suggestions);
+ };
+
+ this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
+ aSearchString, aElement, null,
+ null, resultsAvailable);
+ },
+
+ /**
+ * (Copied from mobile/xul/chrome/content/forms.js)
+ * This function is similar to getListSuggestions from
+ * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
+ * used by the autocomplete.xml binding which is not in used in fennec
+ */
+ _getListSuggestions: function _getListSuggestions(aElement) {
+ if (!(aElement instanceof HTMLInputElement) || !aElement.list)
+ return [];
+
+ let suggestions = [];
+ let filter = !aElement.hasAttribute("mozNoFilter");
+ let lowerFieldValue = aElement.value.toLowerCase();
+
+ let options = aElement.list.options;
+ let length = options.length;
+ for (let i = 0; i < length; i++) {
+ let item = options.item(i);
+
+ let label = item.value;
+ if (item.label)
+ label = item.label;
+ else if (item.text)
+ label = item.text;
+
+ if (filter && !(label.toLowerCase().includes(lowerFieldValue)) )
+ continue;
+ suggestions.push({ label: label, value: item.value });
+ }
+
+ return suggestions;
+ },
+
+ // Retrieves autocomplete suggestions for an element from the form autocomplete service
+ // and sends the suggestions to the Java UI, along with element position data. As
+ // autocomplete queries are asynchronous, calls aCallback when done with a true
+ // argument if results were found and false if no results were found.
+ _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
+ if (!this._isAutoComplete(aElement)) {
+ aCallback(false);
+ return;
+ }
+ if (this._isDisabledElement(aElement)) {
+ aCallback(false);
+ return;
+ }
+
+ let isEmpty = (aElement.value.length === 0);
+
+ let resultsAvailable = autoCompleteSuggestions => {
+ // On desktop, we show datalist suggestions below autocomplete suggestions,
+ // without duplicates removed.
+ let listSuggestions = this._getListSuggestions(aElement);
+ let suggestions = autoCompleteSuggestions.concat(listSuggestions);
+
+ // Return false if there are no suggestions to show
+ if (!suggestions.length) {
+ aCallback(false);
+ return;
+ }
+
+ Messaging.sendRequest({
+ type: "FormAssist:AutoComplete",
+ suggestions: suggestions,
+ rect: ElementTouchHelper.getBoundingContentRect(aElement),
+ isEmpty: isEmpty,
+ });
+
+ // Keep track of input element so we can fill it in if the user
+ // selects an autocomplete suggestion
+ this._currentInputElement = aElement;
+ aCallback(true);
+ };
+
+ this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
+ },
+
+ // Only show a validation message if the user submitted an invalid form,
+ // there's a non-empty message string, and the element is the correct type
+ _isValidateable: function _isValidateable(aElement) {
+ if (!this._invalidSubmit ||
+ !aElement.validationMessage ||
+ !(aElement instanceof HTMLInputElement ||
+ aElement instanceof HTMLTextAreaElement ||
+ aElement instanceof HTMLSelectElement ||
+ aElement instanceof HTMLButtonElement))
+ return false;
+
+ return true;
+ },
+
+ // Sends a validation message and position data for an element to the Java UI.
+ // Returns true if there's a validation message to show, false otherwise.
+ _showValidationMessage: function _sendValidationMessage(aElement) {
+ if (!this._isValidateable(aElement))
+ return false;
+
+ Messaging.sendRequest({
+ type: "FormAssist:ValidationMessage",
+ validationMessage: aElement.validationMessage,
+ rect: ElementTouchHelper.getBoundingContentRect(aElement)
+ });
+
+ return true;
+ },
+
+ _hideFormAssistPopup: function _hideFormAssistPopup() {
+ Messaging.sendRequest({ type: "FormAssist:Hide" });
+ },
+
+ _isDisabledElement : function(aElement) {
+ let currentElement = aElement;
+ while (currentElement) {
+ if(currentElement.disabled)
+ return true;
+
+ currentElement = currentElement.parentElement;
+ }
+ return false;
+ }
+};
+
+var XPInstallObserver = {
+ init: function() {
+ Services.obs.addObserver(this, "addon-install-origin-blocked", false);
+ Services.obs.addObserver(this, "addon-install-disabled", false);
+ Services.obs.addObserver(this, "addon-install-blocked", false);
+ Services.obs.addObserver(this, "addon-install-started", false);
+ Services.obs.addObserver(this, "xpi-signature-changed", false);
+ Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
+
+ AddonManager.addInstallListener(this);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let installInfo, tab, host;
+ if (aSubject && aSubject instanceof Ci.amIWebInstallInfo) {
+ installInfo = aSubject;
+ tab = BrowserApp.getTabForBrowser(installInfo.browser);
+ if (installInfo.originatingURI) {
+ host = installInfo.originatingURI.host;
+ }
+ }
+
+ let strings = Strings.browser;
+ let brandShortName = Strings.brand.GetStringFromName("brandShortName");
+
+ switch (aTopic) {
+ case "addon-install-started":
+ Snackbars.show(strings.GetStringFromName("alertAddonsDownloading"), Snackbars.LENGTH_LONG);
+ break;
+ case "addon-install-disabled": {
+ if (!tab)
+ return;
+
+ let enabled = true;
+ try {
+ enabled = Services.prefs.getBoolPref("xpinstall.enabled");
+ } catch (e) {}
+
+ let buttons, message, callback;
+ if (!enabled) {
+ message = strings.GetStringFromName("xpinstallDisabledMessageLocked");
+ buttons = [strings.GetStringFromName("unsignedAddonsDisabled.dismiss")];
+ callback: (data) => {};
+ } else {
+ message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
+ buttons = [
+ strings.GetStringFromName("xpinstallDisabledButton"),
+ strings.GetStringFromName("unsignedAddonsDisabled.dismiss")
+ ];
+ callback: (data) => {
+ if (data.button === 1) {
+ Services.prefs.setBoolPref("xpinstall.enabled", true)
+ }
+ };
+ }
+
+ new Prompt({
+ title: Strings.browser.GetStringFromName("addonError.titleError"),
+ message: message,
+ buttons: buttons
+ }).show(callback);
+ break;
+ }
+ case "addon-install-blocked": {
+ if (!tab)
+ return;
+
+ let message;
+ if (host) {
+ // We have a host which asked for the install.
+ message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
+ } else {
+ // Without a host we address the add-on as the initiator of the install.
+ let addon = null;
+ if (installInfo.installs.length > 0) {
+ addon = installInfo.installs[0].name;
+ }
+ if (addon) {
+ // We have an addon name, show the regular message.
+ message = strings.formatStringFromName("xpinstallPromptWarningLocal", [brandShortName, addon], 2);
+ } else {
+ // We don't have an addon name, show an alternative message.
+ message = strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1);
+ }
+ }
+
+ let buttons = [
+ strings.GetStringFromName("xpinstallPromptAllowButton"),
+ strings.GetStringFromName("unsignedAddonsDisabled.dismiss")
+ ];
+ new Prompt({
+ title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
+ message: message,
+ buttons: buttons
+ }).show((data) => {
+ if (data.button === 0) {
+ // Kick off the install
+ installInfo.install();
+ }
+ });
+ break;
+ }
+ case "addon-install-origin-blocked": {
+ if (!tab)
+ return;
+
+ new Prompt({
+ title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
+ message: strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1),
+ buttons: [strings.GetStringFromName("unsignedAddonsDisabled.dismiss")]
+ }).show((data) => {});
+ break;
+ }
+ case "xpi-signature-changed": {
+ if (JSON.parse(aData).disabled.length) {
+ this._notifyUnsignedAddonsDisabled();
+ }
+ break;
+ }
+ case "browser-delayed-startup-finished": {
+ let disabledAddons = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+ for (let id of disabledAddons) {
+ if (AddonManager.getAddonByID(id).signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+ this._notifyUnsignedAddonsDisabled();
+ break;
+ }
+ }
+ break;
+ }
+ }
+ },
+
+ _notifyUnsignedAddonsDisabled: function() {
+ new Prompt({
+ window: window,
+ title: Strings.browser.GetStringFromName("unsignedAddonsDisabled.title"),
+ message: Strings.browser.GetStringFromName("unsignedAddonsDisabled.message"),
+ buttons: [
+ Strings.browser.GetStringFromName("unsignedAddonsDisabled.viewAddons"),
+ Strings.browser.GetStringFromName("unsignedAddonsDisabled.dismiss")
+ ]
+ }).show((data) => {
+ if (data.button === 0) {
+ // TODO: Open about:addons to show only unsigned add-ons?
+ BrowserApp.selectOrAddTab("about:addons", { parentId: BrowserApp.selectedTab.id });
+ }
+ });
+ },
+
+ onInstallEnded: function(aInstall, aAddon) {
+ // Don't create a notification for distribution add-ons.
+ if (Distribution.pendingAddonInstalls.has(aInstall)) {
+ Distribution.pendingAddonInstalls.delete(aInstall);
+ return;
+ }
+
+ let needsRestart = false;
+ if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
+ needsRestart = true;
+ else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
+ needsRestart = true;
+
+ if (needsRestart) {
+ this.showRestartPrompt();
+ } else {
+ // Display completion message for new installs or updates not done Automatically
+ if (!aInstall.existingAddon || !AddonManager.shouldAutoUpdate(aInstall.existingAddon)) {
+ let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart.message");
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart.action2"),
+ callback: () => {
+ UITelemetry.addEvent("show.1", "toast", null, "addons");
+ BrowserApp.selectOrAddTab("about:addons", { parentId: BrowserApp.selectedTab.id });
+ },
+ }
+ });
+ }
+ }
+ },
+
+ onInstallFailed: function(aInstall) {
+ this._showErrorMessage(aInstall);
+ },
+
+ onDownloadProgress: function(aInstall) {},
+
+ onDownloadFailed: function(aInstall) {
+ this._showErrorMessage(aInstall);
+ },
+
+ onDownloadCancelled: function(aInstall) {},
+
+ _showErrorMessage: function(aInstall) {
+ // Don't create a notification for distribution add-ons.
+ if (Distribution.pendingAddonInstalls.has(aInstall)) {
+ Cu.reportError("Error installing distribution add-on: " + aInstall.addon.id);
+ Distribution.pendingAddonInstalls.delete(aInstall);
+ return;
+ }
+
+ let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
+ if (!host) {
+ host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
+ }
+
+ let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
+ if (aInstall.error < 0) {
+ error += aInstall.error;
+ } else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ error += "Blocklisted";
+ } else {
+ error += "Incompatible";
+ }
+
+ let msg = Strings.browser.GetStringFromName(error);
+ // TODO: formatStringFromName
+ msg = msg.replace("#1", aInstall.name);
+ if (host) {
+ msg = msg.replace("#2", host);
+ }
+ msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName"));
+ msg = msg.replace("#4", Services.appinfo.version);
+
+ if (aInstall.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
+ new Prompt({
+ window: window,
+ title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
+ message: msg,
+ buttons: [Strings.browser.GetStringFromName("addonError.learnMore")]
+ }).show((data) => {
+ if (data.button === 0) {
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
+ BrowserApp.addTab(url, { parentId: BrowserApp.selectedTab.id });
+ }
+ });
+ } else {
+ Services.prompt.alert(null, Strings.browser.GetStringFromName("addonError.titleError"), msg);
+ }
+ },
+
+ showRestartPrompt: function() {
+ let buttons = [{
+ label: Strings.browser.GetStringFromName("notificationRestart.button"),
+ callback: function() {
+ // Notify all windows that an application quit has been requested
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+
+ // If nothing aborted, quit the app
+ if (cancelQuit.data == false) {
+ Services.obs.notifyObservers(null, "quit-application-proceeding", null);
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+ }
+ },
+ positive: true
+ }];
+
+ let message = Strings.browser.GetStringFromName("notificationRestart.normal");
+ NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
+ },
+
+ hideRestartPrompt: function() {
+ NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
+ }
+};
+
+var ViewportHandler = {
+ init: function init() {
+ Services.obs.addObserver(this, "Window:Resize", false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "Window:Resize" && aData) {
+ let scrollChange = JSON.parse(aData);
+ let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.setNextPaintSyncId(scrollChange.id);
+ }
+ }
+};
+
+/**
+ * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml
+ */
+var PopupBlockerObserver = {
+ onUpdatePageReport: function onUpdatePageReport(aEvent) {
+ let browser = BrowserApp.selectedBrowser;
+ if (aEvent.originalTarget != browser)
+ return;
+
+ if (!browser.pageReport)
+ return;
+
+ let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup");
+ if (result == Ci.nsIPermissionManager.DENY_ACTION)
+ return;
+
+ // 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 (!browser.pageReport.reported) {
+ if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
+ let brandShortName = Strings.brand.GetStringFromName("brandShortName");
+ let popupCount = browser.pageReport.length;
+
+ let strings = Strings.browser;
+ let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message"))
+ .replace("#1", brandShortName)
+ .replace("#2", popupCount);
+
+ let buttons = [
+ {
+ label: strings.GetStringFromName("popup.dontShow"),
+ callback: function(aChecked) {
+ if (aChecked)
+ PopupBlockerObserver.allowPopupsForSite(false);
+ }
+ },
+ {
+ label: strings.GetStringFromName("popup.show"),
+ callback: function(aChecked) {
+ // Set permission before opening popup windows
+ if (aChecked)
+ PopupBlockerObserver.allowPopupsForSite(true);
+
+ PopupBlockerObserver.showPopupsForSite();
+ },
+ positive: true
+ }
+ ];
+
+ let options = { checkbox: Strings.browser.GetStringFromName("popup.dontAskAgain") };
+ NativeWindow.doorhanger.show(message, "popup-blocked", buttons, null, options);
+ }
+ // Record the fact that we've reported this blocked popup, so we don't
+ // show it again.
+ browser.pageReport.reported = true;
+ }
+ },
+
+ allowPopupsForSite: function allowPopupsForSite(aAllow) {
+ let currentURI = BrowserApp.selectedBrowser.currentURI;
+ Services.perms.add(currentURI, "popup", aAllow
+ ? Ci.nsIPermissionManager.ALLOW_ACTION
+ : Ci.nsIPermissionManager.DENY_ACTION);
+ dump("Allowing popups for: " + currentURI);
+ },
+
+ showPopupsForSite: function showPopupsForSite() {
+ let uri = BrowserApp.selectedBrowser.currentURI;
+ let pageReport = BrowserApp.selectedBrowser.pageReport;
+ if (pageReport) {
+ for (let i = 0; i < pageReport.length; ++i) {
+ let popupURIspec = pageReport[i].popupWindowURIspec;
+
+ // 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;
+
+ let popupFeatures = pageReport[i].popupWindowFeatures;
+ let popupName = pageReport[i].popupWindowName;
+
+ let parent = BrowserApp.selectedTab;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
+ BrowserApp.addTab(popupURIspec, { parentId: parent.id, isPrivate: isPrivate });
+ }
+ }
+ }
+};
+
+
+var IndexedDB = {
+ _permissionsPrompt: "indexedDB-permissions-prompt",
+ _permissionsResponse: "indexedDB-permissions-response",
+
+ init: function IndexedDB_init() {
+ Services.obs.addObserver(this, this._permissionsPrompt, false);
+ },
+
+ observe: function IndexedDB_observe(subject, topic, data) {
+ if (topic != this._permissionsPrompt) {
+ throw new Error("Unexpected topic!");
+ }
+
+ let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
+
+ let browser = requestor.getInterface(Ci.nsIDOMNode);
+ let tab = BrowserApp.getTabForBrowser(browser);
+ if (!tab)
+ return;
+
+ let host = browser.currentURI.asciiHost;
+
+ let strings = Strings.browser;
+
+ let message, responseTopic;
+ if (topic == this._permissionsPrompt) {
+ message = strings.formatStringFromName("offlineApps.ask", [host], 1);
+ responseTopic = this._permissionsResponse;
+ }
+
+ const firstTimeoutDuration = 300000; // 5 minutes
+
+ let timeoutId;
+
+ let notificationID = responseTopic + host;
+ let observer = requestor.getInterface(Ci.nsIObserver);
+
+ // This will be set to the result of PopupNotifications.show() below, or to
+ // the result of PopupNotifications.getNotification() if this is a
+ // quotaCancel notification.
+ let notification;
+
+ function timeoutNotification() {
+ // Remove the notification.
+ NativeWindow.doorhanger.hide(notificationID, tab.id);
+
+ // 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);
+ }
+
+ let buttons = [
+ {
+ label: strings.GetStringFromName("offlineApps.dontAllow2"),
+ callback: function(aChecked) {
+ clearTimeout(timeoutId);
+ let action = aChecked ? Ci.nsIPermissionManager.DENY_ACTION : Ci.nsIPermissionManager.UNKNOWN_ACTION;
+ observer.observe(null, responseTopic, action);
+ }
+ },
+ {
+ label: strings.GetStringFromName("offlineApps.allow"),
+ callback: function() {
+ clearTimeout(timeoutId);
+ observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION);
+ },
+ positive: true
+ }];
+
+ let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
+ NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, 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);
+ }
+};
+
+var CharacterEncoding = {
+ _charsets: [],
+
+ init: function init() {
+ Services.obs.addObserver(this, "CharEncoding:Get", false);
+ Services.obs.addObserver(this, "CharEncoding:Set", false);
+ InitLater(() => this.sendState());
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "CharEncoding:Get":
+ this.getEncoding();
+ break;
+ case "CharEncoding:Set":
+ this.setEncoding(aData);
+ break;
+ }
+ },
+
+ sendState: function sendState() {
+ let showCharEncoding = "false";
+ try {
+ showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
+ } catch (e) { /* Optional */ }
+
+ Messaging.sendRequest({
+ type: "CharEncoding:State",
+ visible: showCharEncoding
+ });
+ },
+
+ getEncoding: function getEncoding() {
+ function infoToCharset(info) {
+ return { code: info.value, title: info.label };
+ }
+
+ if (!this._charsets.length) {
+ let data = CharsetMenu.getData();
+
+ // In the desktop UI, the pinned charsets are shown above the rest.
+ let pinnedCharsets = data.pinnedCharsets.map(infoToCharset);
+ let otherCharsets = data.otherCharsets.map(infoToCharset)
+
+ this._charsets = pinnedCharsets.concat(otherCharsets);
+ }
+
+ // Look for the index of the selected charset. Default to -1 if the
+ // doc charset isn't found in the list of available charsets.
+ let docCharset = BrowserApp.selectedBrowser.contentDocument.characterSet;
+ let selected = -1;
+ let charsetCount = this._charsets.length;
+
+ for (let i = 0; i < charsetCount; i++) {
+ if (this._charsets[i].code === docCharset) {
+ selected = i;
+ break;
+ }
+ }
+
+ Messaging.sendRequest({
+ type: "CharEncoding:Data",
+ charsets: this._charsets,
+ selected: selected
+ });
+ },
+
+ setEncoding: function setEncoding(aEncoding) {
+ let browser = BrowserApp.selectedBrowser;
+ browser.docShell.gatherCharsetMenuTelemetry();
+ browser.docShell.charset = aEncoding;
+ browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+ }
+};
+
+var IdentityHandler = {
+ // No trusted identity information. No site identity icon is shown.
+ IDENTITY_MODE_UNKNOWN: "unknown",
+
+ // Domain-Validation SSL CA-signed domain verification (DV).
+ IDENTITY_MODE_IDENTIFIED: "identified",
+
+ // Extended-Validation SSL CA-signed identity information (EV). A more rigorous validation process.
+ IDENTITY_MODE_VERIFIED: "verified",
+
+ // Part of the product's UI (built in about: pages)
+ IDENTITY_MODE_CHROMEUI: "chromeUI",
+
+ // The following mixed content modes are only used if "security.mixed_content.block_active_content"
+ // is enabled. Our Java frontend coalesces them into one indicator.
+
+ // No mixed content information. No mixed content icon is shown.
+ MIXED_MODE_UNKNOWN: "unknown",
+
+ // Blocked active mixed content.
+ MIXED_MODE_CONTENT_BLOCKED: "blocked",
+
+ // Loaded active mixed content.
+ MIXED_MODE_CONTENT_LOADED: "loaded",
+
+ // The following tracking content modes are only used if tracking protection
+ // is enabled. Our Java frontend coalesces them into one indicator.
+
+ // No tracking content information. No tracking content icon is shown.
+ TRACKING_MODE_UNKNOWN: "unknown",
+
+ // Blocked active tracking content. Shield icon is shown, with a popup option to load content.
+ TRACKING_MODE_CONTENT_BLOCKED: "tracking_content_blocked",
+
+ // Loaded active tracking content. Yellow triangle icon is shown.
+ TRACKING_MODE_CONTENT_LOADED: "tracking_content_loaded",
+
+ // Cache the most recent SSLStatus and Location seen in getIdentityStrings
+ _lastStatus : null,
+ _lastLocation : null,
+
+ /**
+ * Helper to parse out the important parts of _lastStatus (of the SSL cert in
+ * particular) for use in constructing identity UI strings
+ */
+ getIdentityData : function() {
+ let result = {};
+ let status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus);
+ let 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) {
+ let 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;
+ },
+
+ /**
+ * Determines the identity mode corresponding to the icon we show in the urlbar.
+ */
+ getIdentityMode: function getIdentityMode(aState, uri) {
+ if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
+ return this.IDENTITY_MODE_VERIFIED;
+ }
+
+ if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
+ return this.IDENTITY_MODE_IDENTIFIED;
+ }
+
+ // We also allow "about:" by allowing the selector to be empty (i.e. '(|.....|...|...)'
+ let whitelist = /^about:($|about|accounts|addons|buildconfig|cache|config|crashes|devices|downloads|fennec|firefox|feedback|healthreport|home|license|logins|logo|memory|mozilla|networking|plugins|privatebrowsing|rights|serviceworkers|support|telemetry|webrtc)($|\?)/i;
+ if (uri.schemeIs("about") && whitelist.test(uri.spec)) {
+ return this.IDENTITY_MODE_CHROMEUI;
+ }
+
+ return this.IDENTITY_MODE_UNKNOWN;
+ },
+
+ getMixedDisplayMode: function getMixedDisplayMode(aState) {
+ if (aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) {
+ return this.MIXED_MODE_CONTENT_LOADED;
+ }
+
+ if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT) {
+ return this.MIXED_MODE_CONTENT_BLOCKED;
+ }
+
+ return this.MIXED_MODE_UNKNOWN;
+ },
+
+ getMixedActiveMode: function getActiveDisplayMode(aState) {
+ // Only show an indicator for loaded mixed content if the pref to block it is enabled
+ if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
+ !Services.prefs.getBoolPref("security.mixed_content.block_active_content")) {
+ return this.MIXED_MODE_CONTENT_LOADED;
+ }
+
+ if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
+ return this.MIXED_MODE_CONTENT_BLOCKED;
+ }
+
+ return this.MIXED_MODE_UNKNOWN;
+ },
+
+ getTrackingMode: function getTrackingMode(aState, aBrowser) {
+ if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
+ this.shieldHistogramAdd(aBrowser, 2);
+ return this.TRACKING_MODE_CONTENT_BLOCKED;
+ }
+
+ // Only show an indicator for loaded tracking content if the pref to block it is enabled
+ let tpEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.enabled") ||
+ (Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled") &&
+ PrivateBrowsingUtils.isBrowserPrivate(aBrowser));
+
+ if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) && tpEnabled) {
+ this.shieldHistogramAdd(aBrowser, 1);
+ return this.TRACKING_MODE_CONTENT_LOADED;
+ }
+
+ this.shieldHistogramAdd(aBrowser, 0);
+ return this.TRACKING_MODE_UNKNOWN;
+ },
+
+ shieldHistogramAdd: function(browser, value) {
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ return;
+ }
+ Telemetry.addData("TRACKING_PROTECTION_SHIELD", value);
+ },
+
+ /**
+ * Determine the identity of the page being displayed by examining its SSL cert
+ * (if available). Return the data needed to update the UI.
+ */
+ checkIdentity: function checkIdentity(aState, aBrowser) {
+ this._lastStatus = aBrowser.securityUI
+ .QueryInterface(Components.interfaces.nsISSLStatusProvider)
+ .SSLStatus;
+
+ // 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)
+ let locationObj = {};
+ try {
+ let location = aBrowser.contentWindow.location;
+ locationObj.host = location.host;
+ locationObj.hostname = location.hostname;
+ locationObj.port = location.port;
+ locationObj.origin = location.origin;
+ } 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.
+ }
+ this._lastLocation = locationObj;
+
+ let uri = aBrowser.currentURI;
+ try {
+ uri = Services.uriFixup.createExposableURI(uri);
+ } catch (e) {}
+
+ let identityMode = this.getIdentityMode(aState, uri);
+ let mixedDisplay = this.getMixedDisplayMode(aState);
+ let mixedActive = this.getMixedActiveMode(aState);
+ let trackingMode = this.getTrackingMode(aState, aBrowser);
+ let result = {
+ origin: locationObj.origin,
+ mode: {
+ identity: identityMode,
+ mixed_display: mixedDisplay,
+ mixed_active: mixedActive,
+ tracking: trackingMode
+ }
+ };
+
+ // Don't show identity data for pages with an unknown identity or if any
+ // mixed content is loaded (mixed display content is loaded by default).
+ // We also return for CHROMEUI pages since they don't have any certificate
+ // information to load either. result.secure specifically refers to connection
+ // security, which is irrelevant for about: pages, as they're loaded locally.
+ if (identityMode == this.IDENTITY_MODE_UNKNOWN ||
+ identityMode == this.IDENTITY_MODE_CHROMEUI ||
+ aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
+ result.secure = false;
+ return result;
+ }
+
+ result.secure = true;
+
+ result.host = this.getEffectiveHost();
+
+ let iData = this.getIdentityData();
+ result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
+
+ // If the cert is identified, then we can populate the results with credentials
+ if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
+ result.owner = iData.subjectOrg;
+
+ // Build an appropriate supplemental block out of whatever location data we have
+ let supplemental = "";
+ if (iData.city) {
+ supplemental += iData.city + "\n";
+ }
+ if (iData.state && iData.country) {
+ supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2);
+ result.country = iData.country;
+ } else if (iData.state) { // State only
+ supplemental += iData.state;
+ } else if (iData.country) { // Country only
+ supplemental += iData.country;
+ result.country = iData.country;
+ }
+ result.supplemental = supplemental;
+
+ return result;
+ }
+
+ // Cache the override service the first time we need to check it
+ if (!this._overrideService)
+ this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
+
+ // 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, {}, {}))
+ result.verifier = Strings.browser.GetStringFromName("identity.identified.verified_by_you");
+
+ return result;
+ },
+
+ /**
+ * Attempt to provide proper IDN treatment for host names
+ */
+ getEffectiveHost: function getEffectiveHost() {
+ if (!this._IDNService)
+ this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
+ .getService(Ci.nsIIDNService);
+ try {
+ return this._IDNService.convertToDisplayIDN(this._uri.host, {});
+ } catch (e) {
+ // If something goes wrong (e.g. hostname is an IP address) just fail back
+ // to the full domain.
+ return this._lastLocation.hostname;
+ }
+ }
+};
+
+var SearchEngines = {
+ _contextMenuId: null,
+ PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
+ PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
+
+ // Shared preference key used for search activity default engine.
+ PREF_SEARCH_ACTIVITY_ENGINE_KEY: "search.engines.defaultname",
+
+ init: function init() {
+ Services.obs.addObserver(this, "SearchEngines:Add", false);
+ Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
+ Services.obs.addObserver(this, "SearchEngines:Remove", false);
+ Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
+ Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
+ Services.obs.addObserver(this, "browser-search-engine-modified", false);
+ },
+
+ // Fetch list of search engines. all ? All engines : Visible engines only.
+ _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
+ if (!Components.isSuccessCode(rv)) {
+ Cu.reportError("Could not initialize search service, bailing out.");
+ return;
+ }
+
+ let engineData = Services.search.getVisibleEngines({});
+
+ // Our Java UI assumes that the default engine is the first item in the array,
+ // so we need to make sure that's the case.
+ if (engineData[0] !== Services.search.defaultEngine) {
+ engineData = engineData.filter(engine => engine !== Services.search.defaultEngine);
+ engineData.unshift(Services.search.defaultEngine);
+ }
+
+ let searchEngines = engineData.map(function (engine) {
+ return {
+ name: engine.name,
+ identifier: engine.identifier,
+ iconURI: (engine.iconURI ? engine.iconURI.spec : null),
+ hidden: engine.hidden
+ };
+ });
+
+ let suggestTemplate = null;
+ let suggestEngine = null;
+
+ // Check to see if the default engine supports search suggestions. We only need to check
+ // the default engine because we only show suggestions for the default engine in the UI.
+ let engine = Services.search.defaultEngine;
+ if (engine.supportsResponseType("application/x-suggestions+json")) {
+ suggestEngine = engine.name;
+ suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
+ }
+
+ // By convention, the currently configured default engine is at position zero in searchEngines.
+ Messaging.sendRequest({
+ type: "SearchEngines:Data",
+ searchEngines: searchEngines,
+ suggest: {
+ engine: suggestEngine,
+ template: suggestTemplate,
+ enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
+ prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
+ }
+ });
+
+ // Send a speculative connection to the default engine.
+ Services.search.defaultEngine.speculativeConnect({window: window});
+ },
+
+ // Helper method to extract the engine name from a JSON. Simplifies the observe function.
+ _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
+ let data = JSON.parse(aData);
+ return Services.search.getEngineByName(data.engine);
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ let engine;
+ switch(aTopic) {
+ case "SearchEngines:Add":
+ this.displaySearchEnginesList(aData);
+ break;
+ case "SearchEngines:GetVisible":
+ Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
+ break;
+ case "SearchEngines:Remove":
+ // Make sure the engine isn't hidden before removing it, to make sure it's
+ // visible if the user later re-adds it (works around bug 341833)
+ engine = this._extractEngineFromJSON(aData);
+ engine.hidden = false;
+ Services.search.removeEngine(engine);
+ break;
+ case "SearchEngines:RestoreDefaults":
+ // Un-hides all default engines.
+ Services.search.restoreDefaultEngines();
+ break;
+ case "SearchEngines:SetDefault":
+ engine = this._extractEngineFromJSON(aData);
+ // Move the new default search engine to the top of the search engine list.
+ Services.search.moveEngine(engine, 0);
+ Services.search.defaultEngine = engine;
+ break;
+ case "browser-search-engine-modified":
+ if (aData == "engine-default") {
+ this._setSearchActivityDefaultPref(aSubject.QueryInterface(Ci.nsISearchEngine));
+ }
+ break;
+ default:
+ dump("Unexpected message type observed: " + aTopic);
+ break;
+ }
+ },
+
+ migrateSearchActivityDefaultPref: function migrateSearchActivityDefaultPref() {
+ Services.search.init(() => this._setSearchActivityDefaultPref(Services.search.defaultEngine));
+ },
+
+ // Updates the search activity pref when the default engine changes.
+ _setSearchActivityDefaultPref: function _setSearchActivityDefaultPref(engine) {
+ SharedPreferences.forApp().setCharPref(this.PREF_SEARCH_ACTIVITY_ENGINE_KEY, engine.name);
+ },
+
+ // Display context menu listing names of the search engines available to be added.
+ displaySearchEnginesList: function displaySearchEnginesList(aData) {
+ let data = JSON.parse(aData);
+ let tab = BrowserApp.getTabForId(data.tabId);
+
+ if (!tab)
+ return;
+
+ let browser = tab.browser;
+ let engines = browser.engines;
+
+ let p = new Prompt({
+ window: browser.contentWindow
+ }).setSingleChoiceItems(engines.map(function(e) {
+ return { label: e.title };
+ })).show((function(data) {
+ if (data.button == -1)
+ return;
+
+ this.addOpenSearchEngine(engines[data.button]);
+ engines.splice(data.button, 1);
+
+ if (engines.length < 1) {
+ // Broadcast message that there are no more add-able search engines.
+ let newEngineMessage = {
+ type: "Link:OpenSearch",
+ tabID: tab.id,
+ visible: false
+ };
+
+ Messaging.sendRequest(newEngineMessage);
+ }
+ }).bind(this));
+ },
+
+ addOpenSearchEngine: function addOpenSearchEngine(engine) {
+ Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
+ onSuccess: function() {
+ // Display a toast confirming addition of new search engine.
+ Snackbars.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), Snackbars.LENGTH_LONG);
+ },
+
+ onError: function(aCode) {
+ let errorMessage;
+ if (aCode == 2) {
+ // Engine is a duplicate.
+ errorMessage = "alertSearchEngineDuplicateToast";
+
+ } else {
+ // Unknown failure. Display general error message.
+ errorMessage = "alertSearchEngineErrorToast";
+ }
+
+ Snackbars.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), Snackbars.LENGTH_LONG);
+ }
+ });
+ },
+
+ /**
+ * Build and return an array of sorted form data / Query Parameters
+ * for an element in a submission form.
+ *
+ * @param element
+ * A valid submission element of a form.
+ */
+ _getSortedFormData: function(element) {
+ let formData = [];
+
+ for (let formElement of element.form.elements) {
+ if (!formElement.type) {
+ continue;
+ }
+
+ // Make this text field a generic search parameter.
+ if (element == formElement) {
+ formData.push({ name: formElement.name, value: "{searchTerms}" });
+ continue;
+ }
+
+ // Add other form elements as parameters.
+ switch (formElement.type.toLowerCase()) {
+ case "checkbox":
+ case "radio":
+ if (!formElement.checked) {
+ break;
+ }
+ case "text":
+ case "hidden":
+ case "textarea":
+ formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
+ break;
+
+ case "select-one": {
+ for (let option of formElement.options) {
+ if (option.selected) {
+ formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ // Return valid, pre-sorted queryParams.
+ return formData.filter(a => a.name && a.value).sort((a, b) => {
+ // nsIBrowserSearchService.hasEngineWithURL() ensures sort, but this helps.
+ if (a.name > b.name) {
+ return 1;
+ }
+ if (b.name > a.name) {
+ return -1;
+ }
+
+ if (a.value > b.value) {
+ return 1;
+ }
+ if (b.value > a.value) {
+ return -1;
+ }
+
+ return 0;
+ });
+ },
+
+ /**
+ * Check if any search engines already handle an EngineURL of type
+ * URLTYPE_SEARCH_HTML, matching this request-method, formURL, and queryParams.
+ */
+ visibleEngineExists: function(element) {
+ let formData = this._getSortedFormData(element);
+
+ let form = element.form;
+ let method = form.method.toUpperCase();
+
+ let charset = element.ownerDocument.characterSet;
+ let docURI = Services.io.newURI(element.ownerDocument.URL, charset, null);
+ let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
+
+ return Services.search.hasEngineWithURL(method, formURL, formData);
+ },
+
+ /**
+ * Adds a new search engine to the BrowserSearchService, based on its provided element. Prompts for an engine
+ * name, and appends a simple version-number in case of collision with an existing name.
+ *
+ * @return callback to handle success value. Currently used for ActionBarHandler.js and UI updates.
+ */
+ addEngine: function addEngine(aElement, resultCallback) {
+ let form = aElement.form;
+ let charset = aElement.ownerDocument.characterSet;
+ let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null);
+ let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
+ let method = form.method.toUpperCase();
+ let formData = this._getSortedFormData(aElement);
+
+ // prompt user for name of search engine
+ let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine3");
+ let title = { value: (aElement.ownerDocument.title || docURI.host) };
+ if (!Services.prompt.prompt(null, promptTitle, null, title, null, {})) {
+ if (resultCallback) {
+ resultCallback(false);
+ };
+ return;
+ }
+
+ // fetch the favicon for this page
+ let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
+ let mDBConn = Services.storage.openDatabase(dbFile);
+ let stmts = [];
+ stmts[0] = mDBConn.createStatement("SELECT favicon FROM history_with_favicons WHERE url = ?");
+ stmts[0].bindByIndex(0, docURI.spec);
+ let favicon = null;
+
+ Services.search.init(function addEngine_cb(rv) {
+ if (!Components.isSuccessCode(rv)) {
+ Cu.reportError("Could not initialize search service, bailing out.");
+ if (resultCallback) {
+ resultCallback(false);
+ };
+ return;
+ }
+
+ mDBConn.executeAsync(stmts, stmts.length, {
+ handleResult: function (results) {
+ let bytes = results.getNextRow().getResultByName("favicon");
+ if (bytes && bytes.length) {
+ favicon = "data:image/x-icon;base64," + btoa(String.fromCharCode.apply(null, bytes));
+ }
+ },
+ handleCompletion: function (reason) {
+ // if there's already an engine with this name, add a number to
+ // make the name unique (e.g., "Google" becomes "Google 2")
+ let name = title.value;
+ for (let i = 2; Services.search.getEngineByName(name); i++)
+ name = title.value + " " + i;
+
+ Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL);
+ Snackbars.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [name], 1), Snackbars.LENGTH_LONG);
+
+ let engine = Services.search.getEngineByName(name);
+ engine.wrappedJSObject._queryCharset = charset;
+ formData.forEach(param => { engine.addParam(param.name, param.value, null); });
+
+ if (resultCallback) {
+ return resultCallback(true);
+ };
+ }
+ });
+ });
+ }
+};
+
+var ActivityObserver = {
+ init: function ao_init() {
+ Services.obs.addObserver(this, "application-background", false);
+ Services.obs.addObserver(this, "application-foreground", false);
+ },
+
+ observe: function ao_observe(aSubject, aTopic, aData) {
+ let isForeground = false;
+ let tab = BrowserApp.selectedTab;
+
+ UITelemetry.addEvent("show.1", "system", null, aTopic);
+
+ switch (aTopic) {
+ case "application-background" :
+ let doc = (tab ? tab.browser.contentDocument : null);
+ if (doc && doc.fullscreenElement) {
+ doc.exitFullscreen();
+ }
+ isForeground = false;
+ break;
+ case "application-foreground" :
+ isForeground = true;
+ break;
+ }
+
+ if (tab && tab.getActive() != isForeground) {
+ tab.setActive(isForeground);
+ }
+ }
+};
+
+var Telemetry = {
+ addData: function addData(aHistogramId, aValue) {
+ let histogram = Services.telemetry.getHistogramById(aHistogramId);
+ histogram.add(aValue);
+ },
+};
+
+var Experiments = {
+ // Enable malware download protection (bug 936041)
+ MALWARE_DOWNLOAD_PROTECTION: "malware-download-protection",
+
+ // Try to load pages from disk cache when network is offline (bug 935190)
+ OFFLINE_CACHE: "offline-cache",
+
+ init() {
+ Messaging.sendRequestForResult({
+ type: "Experiments:GetActive"
+ }).then(experiments => {
+ let names = JSON.parse(experiments);
+ for (let name of names) {
+ switch (name) {
+ case this.MALWARE_DOWNLOAD_PROTECTION: {
+ // Apply experiment preferences on the default branch. This allows
+ // us to avoid migrating user prefs when experiments are enabled/disabled,
+ // and it also allows users to override these prefs in about:config.
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setBoolPref("browser.safebrowsing.downloads.enabled", true);
+ defaults.setBoolPref("browser.safebrowsing.downloads.remote.enabled", true);
+ continue;
+ }
+
+ case this.OFFLINE_CACHE: {
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setBoolPref("browser.tabs.useCache", true);
+ continue;
+ }
+ }
+ }
+ });
+ },
+
+ setOverride(name, isEnabled) {
+ Messaging.sendRequest({
+ type: "Experiments:SetOverride",
+ name: name,
+ isEnabled: isEnabled
+ });
+ },
+
+ clearOverride(name) {
+ Messaging.sendRequest({
+ type: "Experiments:ClearOverride",
+ name: name
+ });
+ }
+};
+
+var ExternalApps = {
+ _contextMenuId: null,
+
+ // extend _getLink to pickup html5 media links.
+ _getMediaLink: function(aElement) {
+ let uri = NativeWindow.contextmenus._getLink(aElement);
+ if (uri == null && aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement)) {
+ try {
+ let mediaSrc = aElement.currentSrc || aElement.src;
+ uri = ContentAreaUtils.makeURI(mediaSrc, null, null);
+ } catch (e) {}
+ }
+ return uri;
+ },
+
+ init: function helper_init() {
+ this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) {
+ let uri = null;
+ var node = aElement;
+ while (node && !uri) {
+ uri = ExternalApps._getMediaLink(node);
+ node = node.parentNode;
+ }
+ let apps = [];
+ if (uri)
+ apps = HelperApps.getAppsForUri(uri);
+
+ return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) :
+ Strings.browser.GetStringFromName("helperapps.openWithList2");
+ }, this.filter, this.openExternal);
+ },
+
+ filter: {
+ matches: function(aElement) {
+ let uri = ExternalApps._getMediaLink(aElement);
+ let apps = [];
+ if (uri) {
+ apps = HelperApps.getAppsForUri(uri);
+ }
+ return apps.length > 0;
+ }
+ },
+
+ openExternal: function(aElement) {
+ if (aElement.pause) {
+ aElement.pause();
+ }
+ let uri = ExternalApps._getMediaLink(aElement);
+ HelperApps.launchUri(uri);
+ },
+
+ shouldCheckUri: function(uri) {
+ if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
+ return false;
+ }
+
+ return true;
+ },
+
+ updatePageAction: function updatePageAction(uri, contentDocument) {
+ HelperApps.getAppsForUri(uri, { filterHttp: true }, (apps) => {
+ this.clearPageAction();
+ if (apps.length > 0)
+ this._setUriForPageAction(uri, apps, contentDocument);
+ });
+ },
+
+ updatePageActionUri: function updatePageActionUri(uri) {
+ this._pageActionUri = uri;
+ },
+
+ _getMediaContentElement(contentDocument) {
+ if (!contentDocument.contentType.startsWith("video/") &&
+ !contentDocument.contentType.startsWith("audio/")) {
+ return null;
+ }
+
+ let element = contentDocument.activeElement;
+
+ if (element instanceof HTMLBodyElement) {
+ element = element.firstChild;
+ }
+
+ if (element instanceof HTMLMediaElement) {
+ return element;
+ }
+
+ return null;
+ },
+
+ _setUriForPageAction: function setUriForPageAction(uri, apps, contentDocument) {
+ this.updatePageActionUri(uri);
+
+ // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
+ if (this._pageActionId != undefined)
+ return;
+
+ let mediaElement = this._getMediaContentElement(contentDocument);
+
+ this._pageActionId = PageActions.add({
+ title: Strings.browser.GetStringFromName("openInApp.pageAction"),
+ icon: "drawable://icon_openinapp",
+
+ clickCallback: () => {
+ UITelemetry.addEvent("launch.1", "pageaction", null, "helper");
+
+ let wasPlaying = mediaElement && !mediaElement.paused && !mediaElement.ended;
+ if (wasPlaying) {
+ mediaElement.pause();
+ }
+
+ if (apps.length > 1) {
+ // Use the HelperApps prompt here to filter out any Http handlers
+ HelperApps.prompt(apps, {
+ title: Strings.browser.GetStringFromName("openInApp.pageAction"),
+ buttons: [
+ Strings.browser.GetStringFromName("openInApp.ok"),
+ Strings.browser.GetStringFromName("openInApp.cancel")
+ ]
+ }, (result) => {
+ if (result.button != 0) {
+ if (wasPlaying) {
+ mediaElement.play();
+ }
+
+ return;
+ }
+ apps[result.icongrid0].launch(this._pageActionUri);
+ });
+ } else {
+ apps[0].launch(this._pageActionUri);
+ }
+ }
+ });
+ },
+
+ clearPageAction: function clearPageAction() {
+ if(!this._pageActionId)
+ return;
+
+ PageActions.remove(this._pageActionId);
+ delete this._pageActionId;
+ },
+};
+
+var Distribution = {
+ // File used to store campaign data
+ _file: null,
+
+ _preferencesJSON: null,
+
+ init: function dc_init() {
+ Services.obs.addObserver(this, "Distribution:Changed", false);
+ Services.obs.addObserver(this, "Distribution:Set", false);
+ Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
+ Services.obs.addObserver(this, "Campaign:Set", false);
+
+ // Look for file outside the APK:
+ // /data/data/org.mozilla.xxx/distribution.json
+ this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
+ this._file.append("distribution.json");
+ this.readJSON(this._file, this.update);
+ },
+
+ observe: function dc_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "Distribution:Changed":
+ // Re-init the search service.
+ try {
+ Services.search._asyncReInit();
+ } catch (e) {
+ console.log("Unable to reinit search service.");
+ }
+ // Fall through.
+
+ case "Distribution:Set":
+ if (aData) {
+ try {
+ this._preferencesJSON = JSON.parse(aData);
+ } catch (e) {
+ console.log("Invalid distribution JSON.");
+ }
+ }
+ // Reload the default prefs so we can observe "prefservice:after-app-defaults"
+ Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
+ this.installDistroAddons();
+ break;
+
+ case "prefservice:after-app-defaults":
+ this.getPrefs();
+ break;
+
+ case "Campaign:Set": {
+ // Update the prefs for this session
+ try {
+ this.update(JSON.parse(aData));
+ } catch (ex) {
+ Cu.reportError("Distribution: Could not parse JSON: " + ex);
+ return;
+ }
+
+ // Asynchronously copy the data to the file.
+ let array = new TextEncoder().encode(aData);
+ OS.File.writeAtomic(this._file.path, array, { tmpPath: this._file.path + ".tmp" });
+ break;
+ }
+ }
+ },
+
+ update: function dc_update(aData) {
+ // Force the distribution preferences on the default branch
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setCharPref("distribution.id", aData.id);
+ defaults.setCharPref("distribution.version", aData.version);
+ },
+
+ getPrefs: function dc_getPrefs() {
+ if (this._preferencesJSON) {
+ this.applyPrefs(this._preferencesJSON);
+ this._preferencesJSON = null;
+ return;
+ }
+
+ // Get the distribution directory, and bail if it doesn't exist.
+ let file = FileUtils.getDir("XREAppDist", [], false);
+ if (!file.exists())
+ return;
+
+ file.append("preferences.json");
+ this.readJSON(file, this.applyPrefs);
+ },
+
+ applyPrefs: function dc_applyPrefs(aData) {
+ // Check for required Global preferences
+ let global = aData["Global"];
+ if (!(global && global["id"] && global["version"] && global["about"])) {
+ Cu.reportError("Distribution: missing or incomplete Global preferences");
+ return;
+ }
+
+ // Force the distribution preferences on the default branch
+ let defaults = Services.prefs.getDefaultBranch(null);
+ defaults.setCharPref("distribution.id", global["id"]);
+ defaults.setCharPref("distribution.version", global["version"]);
+
+ let locale = BrowserApp.getUALocalePref();
+ let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ aboutString.data = global["about." + locale] || global["about"];
+ defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString);
+
+ let prefs = aData["Preferences"];
+ for (let key in prefs) {
+ try {
+ let value = prefs[key];
+ switch (typeof value) {
+ case "boolean":
+ defaults.setBoolPref(key, value);
+ break;
+ case "number":
+ defaults.setIntPref(key, value);
+ break;
+ case "string":
+ case "undefined":
+ defaults.setCharPref(key, value);
+ break;
+ }
+ } catch (e) { /* ignore bad prefs and move on */ }
+ }
+
+ // Apply a lightweight theme if necessary
+ if (prefs && prefs["lightweightThemes.selectedThemeID"]) {
+ Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
+ }
+
+ let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
+ let localizeablePrefs = aData["LocalizablePreferences"];
+ for (let key in localizeablePrefs) {
+ try {
+ let value = localizeablePrefs[key];
+ value = value.replace(/%LOCALE%/g, locale);
+ localizedString.data = "data:text/plain," + key + "=" + value;
+ defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
+ } catch (e) { /* ignore bad prefs and move on */ }
+ }
+
+ let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale];
+ for (let key in localizeablePrefsOverrides) {
+ try {
+ let value = localizeablePrefsOverrides[key];
+ localizedString.data = "data:text/plain," + key + "=" + value;
+ defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
+ } catch (e) { /* ignore bad prefs and move on */ }
+ }
+
+ Messaging.sendRequest({ type: "Distribution:Set:OK" });
+ },
+
+ // aFile is an nsIFile
+ // aCallback takes the parsed JSON object as a parameter
+ readJSON: function dc_readJSON(aFile, aCallback) {
+ Task.spawn(function() {
+ let bytes = yield OS.File.read(aFile.path);
+ let raw = new TextDecoder().decode(bytes) || "";
+
+ try {
+ aCallback(JSON.parse(raw));
+ } catch (e) {
+ Cu.reportError("Distribution: Could not parse JSON: " + e);
+ }
+ }).then(null, function onError(reason) {
+ if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
+ Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
+ }
+ });
+ },
+
+ // Track pending installs so we can avoid showing notifications for them.
+ pendingAddonInstalls: new Set(),
+
+ installDistroAddons: Task.async(function* () {
+ const PREF_ADDONS_INSTALLED = "distribution.addonsInstalled";
+ try {
+ let installed = Services.prefs.getBoolPref(PREF_ADDONS_INSTALLED);
+ if (installed) {
+ return;
+ }
+ } catch (e) {
+ Services.prefs.setBoolPref(PREF_ADDONS_INSTALLED, true);
+ }
+
+ let distroPath;
+ try {
+ distroPath = FileUtils.getDir("XREAppDist", ["extensions"]).path;
+
+ let info = yield OS.File.stat(distroPath);
+ if (!info.isDir) {
+ return;
+ }
+ } catch (e) {
+ return;
+ }
+
+ let it = new OS.File.DirectoryIterator(distroPath);
+ try {
+ yield it.forEach(entry => {
+ // Only support extensions that are zipped in .xpi files.
+ if (entry.isDir || !entry.name.endsWith(".xpi")) {
+ dump("Ignoring distribution add-on that isn't an XPI: " + entry.path);
+ return;
+ }
+
+ new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(new FileUtils.File(entry.path), resolve);
+ }).then(install => {
+ let id = entry.name.substring(0, entry.name.length - 4);
+ if (install.addon.id !== id) {
+ Cu.reportError("File entry " + entry.path + " contains an add-on with an incorrect ID");
+ return;
+ }
+ this.pendingAddonInstalls.add(install);
+ install.install();
+ }).catch(e => {
+ Cu.reportError("Error installing distribution add-on: " + entry.path + ": " + e);
+ });
+ });
+ } finally {
+ it.close();
+ }
+ })
+};
+
+var Tabs = {
+ _enableTabExpiration: false,
+ _useCache: false,
+ _domains: new Set(),
+
+ init: function() {
+ // On low-memory platforms, always allow tab expiration. On high-mem
+ // platforms, allow it to be turned on once we hit a low-mem situation.
+ if (BrowserApp.isOnLowMemoryPlatform) {
+ this._enableTabExpiration = true;
+ } else {
+ Services.obs.addObserver(this, "memory-pressure", false);
+ }
+
+ // Watch for opportunities to pre-connect to high probability targets.
+ Services.obs.addObserver(this, "Session:Prefetch", false);
+
+ // Track the network connection so we can efficiently use the cache
+ // for possible offline rendering.
+ Services.obs.addObserver(this, "network:link-status-changed", false);
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ this.useCache = !network.isLinkUp;
+
+ BrowserApp.deck.addEventListener("pageshow", this, false);
+ BrowserApp.deck.addEventListener("TabOpen", this, false);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "memory-pressure":
+ if (aData != "heap-minimize") {
+ // We received a low-memory related notification. This will enable
+ // expirations.
+ this._enableTabExpiration = true;
+ Services.obs.removeObserver(this, "memory-pressure");
+ } else {
+ // Use "heap-minimize" as a trigger to expire the most stale tab.
+ this.expireLruTab();
+ }
+ break;
+ case "Session:Prefetch":
+ if (aData) {
+ try {
+ let uri = Services.io.newURI(aData, null, null);
+ if (uri && !this._domains.has(uri.host)) {
+ Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
+ this._domains.add(uri.host);
+ }
+ } catch (e) {}
+ }
+ break;
+ case "network:link-status-changed":
+ if (["down", "unknown", "up"].indexOf(aData) == -1) {
+ return;
+ }
+ this.useCache = (aData === "down");
+ break;
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "pageshow":
+ // Clear the domain cache whenever a page is loaded into any browser.
+ this._domains.clear();
+
+ break;
+ case "TabOpen":
+ // Use opening a new tab as a trigger to expire the most stale tab.
+ this.expireLruTab();
+ break;
+ }
+ },
+
+ // Manage the most-recently-used list of tabs. Each tab has a timestamp
+ // associated with it that indicates when it was last touched.
+ expireLruTab: function() {
+ if (!this._enableTabExpiration) {
+ return false;
+ }
+ let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
+ if (expireTimeMs < 0) {
+ // This behaviour is disabled.
+ return false;
+ }
+ let tabs = BrowserApp.tabs;
+ let selected = BrowserApp.selectedTab;
+ let lruTab = null;
+ // Find the least recently used non-zombie tab.
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i] == selected ||
+ tabs[i].browser.__SS_restore ||
+ tabs[i].playingAudio) {
+ // This tab is selected, is already a zombie, or is currently playing
+ // audio, skip it.
+ continue;
+ }
+ if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
+ lruTab = tabs[i];
+ }
+ }
+ // If the tab was last touched more than browser.tabs.expireTime seconds ago,
+ // zombify it.
+ if (lruTab) {
+ if (Date.now() - lruTab.lastTouchedAt > expireTimeMs) {
+ MemoryObserver.zombify(lruTab);
+ return true;
+ }
+ }
+ return false;
+ },
+
+ get useCache() {
+ if (!Services.prefs.getBoolPref("browser.tabs.useCache")) {
+ return false;
+ }
+ return this._useCache;
+ },
+
+ set useCache(aUseCache) {
+ if (!Services.prefs.getBoolPref("browser.tabs.useCache")) {
+ return;
+ }
+
+ if (this._useCache == aUseCache) {
+ return;
+ }
+
+ BrowserApp.tabs.forEach(function(tab) {
+ if (tab.browser && tab.browser.docShell) {
+ if (aUseCache) {
+ tab.browser.docShell.defaultLoadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
+ } else {
+ tab.browser.docShell.defaultLoadFlags &= ~Ci.nsIRequest.LOAD_FROM_CACHE;
+ }
+ }
+ });
+ this._useCache = aUseCache;
+ },
+
+ // For debugging
+ dump: function(aPrefix) {
+ let tabs = BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore);
+ }
+ },
+};
+
+function ContextMenuItem(args) {
+ this.id = uuidgen.generateUUID().toString();
+ this.args = args;
+}
+
+ContextMenuItem.prototype = {
+ get order() {
+ return this.args.order || 0;
+ },
+
+ matches: function(elt, x, y) {
+ return this.args.selector.matches(elt, x, y);
+ },
+
+ callback: function(elt) {
+ this.args.callback(elt);
+ },
+
+ addVal: function(name, elt, defaultValue) {
+ if (!(name in this.args))
+ return defaultValue;
+
+ if (typeof this.args[name] == "function")
+ return this.args[name](elt);
+
+ return this.args[name];
+ },
+
+ getValue: function(elt) {
+ return {
+ id: this.id,
+ label: this.addVal("label", elt),
+ showAsActions: this.addVal("showAsActions", elt),
+ icon: this.addVal("icon", elt),
+ isGroup: this.addVal("isGroup", elt, false),
+ inGroup: this.addVal("inGroup", elt, false),
+ disabled: this.addVal("disabled", elt, false),
+ selected: this.addVal("selected", elt, false),
+ isParent: this.addVal("isParent", elt, false),
+ };
+ }
+}
+
+function HTMLContextMenuItem(elt, target) {
+ ContextMenuItem.call(this, { });
+
+ this.menuElementRef = Cu.getWeakReference(elt);
+ this.targetElementRef = Cu.getWeakReference(target);
+}
+
+HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
+ order: {
+ value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER
+ },
+
+ matches: {
+ value: function(target) {
+ let t = this.targetElementRef.get();
+ return t === target;
+ },
+ },
+
+ callback: {
+ value: function(target) {
+ let elt = this.menuElementRef.get();
+ if (!elt) {
+ return;
+ }
+
+ // If this is a menu item, show a new context menu with the submenu in it
+ if (elt instanceof Ci.nsIDOMHTMLMenuElement) {
+ try {
+ NativeWindow.contextmenus.menus = {};
+
+ let elt = this.menuElementRef.get();
+ let target = this.targetElementRef.get();
+ if (!elt) {
+ return;
+ }
+
+ var items = NativeWindow.contextmenus._getHTMLContextMenuItemsForMenu(elt, target);
+ // This menu will always only have one context, but we still make sure its the "right" one.
+ var context = NativeWindow.contextmenus._getContextType(target);
+ if (items.length > 0) {
+ NativeWindow.contextmenus._addMenuItems(items, context);
+ }
+
+ } catch(ex) {
+ Cu.reportError(ex);
+ }
+ } else {
+ // otherwise just click the menu item
+ elt.click();
+ }
+ },
+ },
+
+ getValue: {
+ value: function(target) {
+ let elt = this.menuElementRef.get();
+ if (!elt) {
+ return null;
+ }
+
+ if (elt.hasAttribute("hidden")) {
+ return null;
+ }
+
+ return {
+ id: this.id,
+ icon: elt.icon,
+ label: elt.label,
+ disabled: elt.disabled,
+ menu: elt instanceof Ci.nsIDOMHTMLMenuElement
+ };
+ }
+ },
+});
+