diff options
Diffstat (limited to 'mobile/android/chrome/content/browser.js')
-rw-r--r-- | mobile/android/chrome/content/browser.js | 6961 |
1 files changed, 0 insertions, 6961 deletions
diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js deleted file mode 100644 index 535f7e607..000000000 --- a/mobile/android/chrome/content/browser.js +++ /dev/null @@ -1,6961 +0,0 @@ -// -*- 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"); - -#ifdef MOZ_SAFE_BROWSING - XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", - "resource://gre/modules/SafeBrowsing.jsm"); -#endif - -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); - - // 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"); - -#ifdef MOZ_SAFE_BROWSING - // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008. - InitLater(() => SafeBrowsing.init(), window, "SafeBrowsing"); -#endif - - 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 isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView); - let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter); - - if (target == errorDoc.getElementById("getMeOutButton")) { - errorDoc.location = "about:home"; - } else if (target == errorDoc.getElementById("reportButton")) { - // 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")) { - // 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 - }; - } - }, -}); - |