// -*- 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"; let Cc = Components.classes; let Ci = Components.interfaces; let Cu = Components.utils; let 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/Payment.jsm'); Cu.import("resource://gre/modules/NotificationDB.jsm"); Cu.import("resource://gre/modules/SpatialNavigation.jsm"); if (AppConstants.ACCESSIBILITY) { Cu.import("resource://gre/modules/accessibility/AccessFu.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, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.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"); if (AppConstants.MOZ_SAFE_BROWSING) { XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", "resource://gre/modules/SafeBrowsing.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.defineLazyModuleGetter(this, "SimpleServiceDiscovery", "resource://gre/modules/SimpleServiceDiscovery.jsm"); if (AppConstants.NIGHTLY_BUILD) { XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils", "resource://shumway/ShumwayUtils.jsm"); } XPCOMUtils.defineLazyModuleGetter(this, "WebappManager", "resource://gre/modules/WebappManager.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, "SharedPreferences", "resource://gre/modules/SharedPreferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "GMPInstallManager", "resource://gre/modules/GMPInstallManager.jsm"); let 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"], ["ZoomHelper", "chrome://browser/content/ZoomHelper.js"], ["CastingApps", "chrome://browser/content/CastingApps.js"], ]; if (AppConstants.NIGHTLY_BUILD) { 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]; }); }); let 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:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"], ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"], ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"], ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"], ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"], ["Reader", ["Reader:Added", "Reader:Removed", "Gesture:DoubleTap"], "chrome://browser/content/Reader.js"], ]; if (AppConstants.MOZ_WEBRTC) { lazilyLoadedObserverScripts.push( ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "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:AddToList", "Reader:ArticleGet", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList", "Reader:Share", "Reader:ShowToast", "Reader:ToolbarVisibility", "Reader:SystemUIVisibility", "Reader:UpdateReaderButton", ], "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); mm.addMessageListener(message.name, window[name]); window[name].receiveMessage(message); }; messages.forEach((message) => { mm.addMessageListener(message, listener); }); }); // 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"); } const kStateActive = 0x00000001; // :active pseudoclass for elements const kXLinkNamespace = "http://www.w3.org/1999/xlink"; const kDefaultCSSViewportWidth = 980; const kDefaultCSSViewportHeight = 480; const kViewportRemeasureThrottle = 500; let Log = Cu.import("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. let dump = Log.d.bind(null, "Browser"); function doChangeMaxLineBoxWidth(aWidth) { gReflowPending = null; let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); let docShell = webNav.QueryInterface(Ci.nsIDocShell); let docViewer = docShell.contentViewer; let range = null; if (BrowserApp.selectedTab._mReflozPoint) { range = BrowserApp.selectedTab._mReflozPoint.range; } try { docViewer.pausePainting(); docViewer.changeMaxLineBoxWidth(aWidth); if (range) { ZoomHelper.zoomInAndSnapToRange(range); } else { // In this case, we actually didn't zoom into a specific range. It // probably happened from a page load reflow-on-zoom event, so we // need to make sure painting is re-enabled. BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); } } finally { docViewer.resumePainting(); } } function fuzzyEquals(a, b) { return (Math.abs(a - b) < 1e-6); } /** * Convert a font size to CSS pixels (px) from twentieiths-of-a-point * (twips). */ function convertFromTwipsToPx(aSize) { return aSize/240 * 16.0; } 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. */ let 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")); }, flush: function () { Services.strings.flushBundles(); this.init(); }, }; Strings.init(); const kFormHelperModeDisabled = 0; const kFormHelperModeEnabled = 1; const kFormHelperModeDynamic = 2; // disabled on tablets const kMaxHistoryListSize = 50; var BrowserApp = { _tabs: [], _selectedTab: null, _prefObservers: [], 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."); this.deck = document.getElementById("browsers"); this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() { try { BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false); Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); Messaging.sendRequest({ type: "Gecko:DelayedStartup" }); // Queue up some other performance-impacting initializations Services.tm.mainThread.dispatch(function() { Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); CastingApps.init(); DownloadNotifications.init(); // Delay this a minute because there's no rush setTimeout(() => { BrowserApp.gmpInstallManager = new GMPInstallManager(); BrowserApp.gmpInstallManager.simpleCheckAndInstall().then(null, () => {}); }, 1000 * 60); }, Ci.nsIThread.DISPATCH_NORMAL); if (AppConstants.MOZ_SAFE_BROWSING) { Services.tm.mainThread.dispatch(function() { // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008. SafeBrowsing.init(); }, Ci.nsIThread.DISPATCH_NORMAL); } if (AppConstants.NIGHTLY_BUILD) { WebcompatReporter.init(); Telemetry.addData("TRACKING_PROTECTION_ENABLED", Services.prefs.getBoolPref("privacy.trackingprotection.enabled")); } } catch(ex) { console.log(ex); } }, false); 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, "Preferences:Set", 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, "Viewport:Change", false); Services.obs.addObserver(this, "Viewport:Flush", false); Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false); Services.obs.addObserver(this, "Passwords:Init", false); Services.obs.addObserver(this, "FormHistory:Init", false); Services.obs.addObserver(this, "gather-telemetry", false); Services.obs.addObserver(this, "keyword-search", false); Services.obs.addObserver(this, "webapps-runtime-install", false); Services.obs.addObserver(this, "webapps-runtime-install-package", false); Services.obs.addObserver(this, "webapps-ask-install", false); Services.obs.addObserver(this, "webapps-ask-uninstall", false); Services.obs.addObserver(this, "webapps-launch", false); Services.obs.addObserver(this, "webapps-runtime-uninstall", false); Services.obs.addObserver(this, "Webapps:AutoInstall", false); Services.obs.addObserver(this, "Webapps:Load", false); Services.obs.addObserver(this, "Webapps:AutoUninstall", false); Services.obs.addObserver(this, "sessionstore-state-purge-complete", false); Messaging.addListener(this.getHistory.bind(this), "Session:GetHistory"); function showFullScreenWarning() { NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short"); } window.addEventListener("fullscreen", function() { Messaging.sendRequest({ type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide" }); }, false); window.addEventListener("mozfullscreenchange", function(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 mozFullScreenElement set correctly. let doc = e.target; Messaging.sendRequest({ type: doc.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop", rootElement: (doc.mozFullScreen && doc.mozFullScreenElement == doc.documentElement) }); if (doc.mozFullScreen) showFullScreenWarning(); }, false); // When a restricted key is pressed in DOM full-screen mode, we should display // the "Press ESC to exit" warning message. window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true); NativeWindow.init(); LightWeightThemeWebInstaller.init(); FormAssistant.init(); IndexedDB.init(); HealthReportStatusListener.init(); XPInstallObserver.init(); CharacterEncoding.init(); ActivityObserver.init(); // TODO: replace with Android implementation of WebappOSUtils.isLaunchable. Cu.import("resource://gre/modules/Webapps.jsm"); DOMApplicationRegistry.allAppsLaunchable = true; RemoteDebugger.init(); UserAgentOverrides.init(); DesktopUserAgent.init(); Distribution.init(); Tabs.init(); SearchEngines.init(); if (AppConstants.ACCESSIBILITY) { AccessFu.attach(window); } if (AppConstants.NIGHTLY_BUILD) { ShumwayUtils.init(); } let url = null; if ("arguments" in window) { if (window.arguments[0]) url = window.arguments[0]; if (window.arguments[1]) gScreenWidth = window.arguments[1]; if (window.arguments[2]) gScreenHeight = window.arguments[2]; } // 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(); // 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); } try { // Set the tiles click observer only if tiles reporting is enabled (that // is, a report URL is set in prefs). gTilesReportURL = Services.prefs.getCharPref("browser.tiles.reportURL"); Services.obs.addObserver(this, "Tiles:Click", false); } catch (e) { // Tiles reporting is disabled. } let mm = window.getGroupMessageManager("browsers"); mm.loadFrameScript("chrome://browser/content/content.js", true); // Notify Java that Gecko has loaded. Messaging.sendRequest({ type: "Gecko:Ready" }); }, 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 }); }, _initRuntime: function(status, url, callback) { let sandbox = {}; Services.scriptloader.loadSubScript("chrome://browser/content/WebappRT.js", sandbox); window.WebappRT = sandbox.WebappRT; WebappRT.init(status, url, callback); }, 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"); NativeWindow.toast.show(label, "long", { button: { icon: "drawable://switch_button_icon", label: buttonLabel, callback: () => { BrowserApp.selectTab(tab); }, } }); }); NativeWindow.contextmenus.add(stringGetter("contextmenu.openInPrivateTab"), NativeWindow.contextmenus.linkOpenableContext, function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_private_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"); NativeWindow.toast.show(label, "long", { button: { icon: "drawable://switch_button_icon", 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) { 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) { 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) { 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"); 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.SelectorContext("video")), 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) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media"); } }); NativeWindow.contextmenus.add(stringGetter("contextmenu.fullScreen"), NativeWindow.contextmenus.SelectorContext("video:not(:-moz-full-screen)"), function(aTarget) { UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen"); aTarget.mozRequestFullScreen(); }); 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.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.imageSaveableContext), 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.characterSet); 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"); 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"); 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); }); }, onAppUpdated: function() { // initialize the form history and passwords databases on upgrades Services.obs.notifyObservers(null, "FormHistory:Init", ""); Services.obs.notifyObservers(null, "Passwords:Init", ""); // 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 (this._startupStatus === "upgrade" && !Services.prefs.prefHasUserValue("searchActivity.default.migrated")) { Services.prefs.setBoolPref("searchActivity.default.migrated", true); SearchEngines.migrateSearchActivityDefaultPref(); } if (this._startupStatus === "upgrade") { Reader.migrateCache().catch(e => Cu.reportError("Error migrating Reader cache: " + e)); } }, // 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()) 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(); }, 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); aTab.setResolution(aTab._zoom, 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); }, _loadWebapp: function(aMessage) { // Entry point for WebApps. This is the point in which we know // the code is being used as a WebApp runtime. this._initRuntime(this._startupStatus, aMessage.url, aUrl => { this.manifestUrl = aMessage.url; this.addTab(aUrl, { title: aMessage.name }); }); }, // Calling this will update the state in BrowserApp after a tab has been // closed in the Java UI. _handleTabClosed: function _handleTabClosed(aTab, aShowUndoToast) { 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 (aShowUndoToast) { // Get a title for the undo close toast. 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; if (title) { message = Strings.browser.formatStringFromName("undoCloseToast.message", [title], 1); } else { message = Strings.browser.GetStringFromName("undoCloseToast.messageDefault"); } NativeWindow.toast.show(message, "short", { button: { icon: "drawable://undo_button_icon", 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 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) { 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 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. */ selectOrOpenTab: function selectOrOpenTab(aURL, aFlags) { let tab = this.getTabWithURL(aURL, aFlags); if (tab == null) { tab = this.addTab(aURL); } 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) { 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 }) { if (this.gmpInstallManager) { this.gmpInstallManager.uninit(); } // Figure out if there's at least one other browser window around. let lastBrowser = true; let e = Services.wm.getEnumerator("navigator:browser"); while (e.hasMoreElements() && lastBrowser) { let win = e.getNext(); if (!win.closed && win != window) lastBrowser = false; } if (lastBrowser) { // Let everyone know we are closing the last browser window let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null); if (closingCanceled.data) return; Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", 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() { window.QueryInterface(Ci.nsIDOMChromeWindow).minimize(); window.close(); }); }, saveAsPDF: function saveAsPDF(aBrowser) { 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(); }); }, notifyPrefObservers: function(aPref) { this._prefObservers[aPref].forEach(function(aRequestId) { this.getPreferences(aRequestId, [aPref], 1); }, this); }, handlePreferencesRequest: function handlePreferencesRequest(aRequestId, aPrefNames, aListen) { let prefs = []; for (let prefName of aPrefNames) { let pref = { name: prefName, type: "", value: null }; if (aListen) { if (this._prefObservers[prefName]) this._prefObservers[prefName].push(aRequestId); else this._prefObservers[prefName] = [ aRequestId ]; Services.prefs.addObserver(prefName, this, false); } // These pref names are not "real" pref names. // They are used in the setting menu, // and these are passed when initializing the setting menu. switch (prefName) { // The plugin pref is actually two separate prefs, so // we need to handle it differently case "plugin.enable": pref.type = "string";// Use a string type for java's ListPreference pref.value = PluginHelper.getPluginPreference(); prefs.push(pref); continue; // Handle master password case "privacy.masterpassword.enabled": pref.type = "bool"; pref.value = MasterPassword.enabled; prefs.push(pref); continue; // 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) { pref.type = "bool"; pref.value = Services.appinfo.submitReports; prefs.push(pref); } continue; } try { switch (Services.prefs.getPrefType(prefName)) { case Ci.nsIPrefBranch.PREF_BOOL: pref.type = "bool"; pref.value = Services.prefs.getBoolPref(prefName); break; case Ci.nsIPrefBranch.PREF_INT: pref.type = "int"; pref.value = Services.prefs.getIntPref(prefName); break; case Ci.nsIPrefBranch.PREF_STRING: default: pref.type = "string"; try { // Try in case it's a localized string (will throw an exception if not) pref.value = Services.prefs.getComplexValue(prefName, Ci.nsIPrefLocalizedString).data; } catch (e) { pref.value = Services.prefs.getCharPref(prefName); } break; } } catch (e) { dump("Error reading pref [" + prefName + "]: " + e); // preference does not exist; do not send it continue; } // Some Gecko preferences use integers or strings to reference // state instead of directly representing the value. // Since the Java UI uses the type to determine which ui elements // to show and how to handle them, we need to normalize these // preferences to the correct type. switch (prefName) { // (string) index for determining which multiple choice value to display. case "browser.chrome.titlebarMode": case "network.cookie.cookieBehavior": case "font.size.inflation.minTwips": case "home.sync.updateMode": pref.type = "string"; pref.value = pref.value.toString(); break; } prefs.push(pref); } Messaging.sendRequest({ type: "Preferences:Data", requestId: aRequestId, // opaque request identifier, can be any string/int/whatever preferences: prefs }); }, setPreferences: function setPreferences(aPref) { let json = JSON.parse(aPref); switch (json.name) { // The plugin pref is actually two separate prefs, so // we need to handle it differently case "plugin.enable": PluginHelper.setPluginPreference(json.value); return; // MasterPassword pref is not real, we just need take action and leave case "privacy.masterpassword.enabled": if (MasterPassword.enabled) MasterPassword.removePassword(json.value); else MasterPassword.setPassword(json.value); return; // Enabling or disabling suggestions will prevent future prompts case SearchEngines.PREF_SUGGEST_ENABLED: Services.prefs.setBoolPref(SearchEngines.PREF_SUGGEST_PROMPTED, true); 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 = json.value; } return; // When sending to Java, we normalized special preferences that use // integers and strings to represent booleans. Here, we convert them back // to their actual types so we can store them. case "browser.chrome.titlebarMode": case "network.cookie.cookieBehavior": case "font.size.inflation.minTwips": case "home.sync.updateMode": json.type = "int"; json.value = parseInt(json.value); break; } switch (json.type) { case "bool": Services.prefs.setBoolPref(json.name, json.value); break; case "int": Services.prefs.setIntPref(json.name, json.value); break; default: { let pref = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString); pref.data = json.value; Services.prefs.setComplexValue(json.name, Ci.nsISupportsString, pref); break; } } }, sanitize: function (aItems, callback) { let success = true; for (let key in aItems) { if (!aItems[key]) continue; key = key.replace("private.data.", ""); var promises = []; 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 }); if (callback) { callback(); } }).catch(function(err) { Messaging.sendRequest({ type: "Sanitize:Finished", error: err, success: false }); 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)) 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 focused = this.getFocusedInput(aBrowser); if (focused) { let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom"); if (formHelperMode == kFormHelperModeDynamic && this.isTablet) shouldZoom = false; // ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen ZoomHelper.zoomToElement(focused, -1, false, aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified); } }, 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_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; // Check to see if this is a message to enable/disable mixed content blocking. if (aData) { let data = JSON.parse(aData); if (data.contentType === "mixed") { if (data.allowContent) { // Set a flag to disable mixed content blocking. flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT; } else { // Set mixedContentChannel to null to re-enable mixed content blocking. let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell); docShell.mixedContentChannel = null; } } else if (data.contentType === "tracking") { if (data.allowContent) { // 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); // 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. 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). Services.perms.remove(browser.currentURI.host, "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_OWNER to prevent any loads from // inheriting the currently loaded document's principal. if (data.userEntered) { flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; } 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 "Preferences:Set": this.setPreferences(aData); 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.mozCancelFullScreen(); break; case "Viewport:Change": if (this.isBrowserContentDocumentDisplayed()) this.selectedTab.setViewport(JSON.parse(aData)); break; case "Viewport:Flush": this.contentDocumentChanged(); 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 "sessionstore-state-purge-complete": Messaging.sendRequest({ type: "Session:StatePurged" }); break; case "gather-telemetry": Messaging.sendRequest({ type: "Telemetry:Gather" }); break; case "Viewport:FixedMarginsChanged": gViewportMargins = JSON.parse(aData); this.selectedTab.updateViewportSize(gScreenWidth); break; case "nsPref:changed": this.notifyPrefObservers(aData); break; case "webapps-runtime-install": WebappManager.install(JSON.parse(aData), aSubject); break; case "webapps-runtime-install-package": WebappManager.installPackage(JSON.parse(aData), aSubject); break; case "webapps-ask-install": WebappManager.askInstall(JSON.parse(aData)); break; case "webapps-ask-uninstall": WebappManager.askUninstall(JSON.parse(aData)); break; case "webapps-launch": { WebappManager.launch(JSON.parse(aData)); break; } case "webapps-runtime-uninstall": { WebappManager.uninstall(JSON.parse(aData), aSubject); break; } case "Webapps:AutoInstall": WebappManager.autoInstall(JSON.parse(aData)); break; case "Webapps:Load": this._loadWebapp(JSON.parse(aData)); break; case "Webapps:AutoUninstall": WebappManager.autoUninstall(JSON.parse(aData)); 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 "Tiles:Click": // Set the click data for the given tab to be handled on the next page load. let data = JSON.parse(aData); let tab = this.getTabForId(data.tabId); tab.tilesData = data.payload; 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); }, get defaultBrowserWidth() { delete this.defaultBrowserWidth; let width = Services.prefs.getIntPref("browser.viewport.desktopWidth"); return this.defaultBrowserWidth = width; }, // nsIAndroidBrowserApp get selectedTab() { return this._selectedTab; }, // nsIAndroidBrowserApp getBrowserTab: function(tabId) { return this.getTabForId(tabId); }, getUITelemetryObserver: function() { return UITelemetry; }, getPreferences: function getPreferences(requestId, prefNames, count) { this.handlePreferencesRequest(requestId, prefNames, false); }, observePreferences: function observePreferences(requestId, prefNames, count) { this.handlePreferencesRequest(requestId, prefNames, true); }, removePreferenceObservers: function removePreferenceObservers(aRequestId) { let newPrefObservers = []; for (let prefName in this._prefObservers) { let requestIds = this._prefObservers[prefName]; // Remove the requestID from the preference handlers let i = requestIds.indexOf(aRequestId); if (i >= 0) { requestIds.splice(i, 1); } // If there are no more request IDs, remove the observer if (requestIds.length == 0) { Services.prefs.removeObserver(prefName, this); } else { newPrefObservers[prefName] = requestIds; } } this._prefObservers = newPrefObservers; }, // 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); Services.obs.addObserver(this, "Toast:Click", false); Services.obs.addObserver(this, "Toast:Hidden", 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 }); }, toast: { _callbacks: {}, show: function(aMessage, aDuration, aOptions) { let msg = { type: "Toast:Show", message: aMessage, duration: aDuration }; if (aOptions && aOptions.button) { msg.button = { id: uuidgen.generateUUID().toString(), }; // null is badly handled by the receiver, so try to avoid including nulls. if (aOptions.button.label) { msg.button.label = aOptions.button.label; } if (aOptions.button.icon) { // If the caller specified a button, make sure we convert any chrome urls // to jar:jar urls so that the frontend can show them msg.button.icon = resolveGeckoURI(aOptions.button.icon); }; this._callbacks[msg.button.id] = aOptions.button.callback; } Messaging.sendRequest(msg); } }, menu: { _callbacks: [], _menuId: 1, toolsMenuID: -1, add: function() { let options; if (arguments.length == 1) { options = arguments[0]; } else if (arguments.length == 3) { options = { name: arguments[0], icon: arguments[1], 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. */ show: function(aMessage, aValue, aButtons, aTabID, aOptions) { if (aButtons == null) { aButtons = []; } 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 || {} }; 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 == "Toast:Click") { if (this.toast._callbacks[aData]) { this.toast._callbacks[aData](); delete this.toast._callbacks[aData]; } } else if (aTopic == "Toast:Hidden") { if (this.toast._callbacks[aData]) delete this.toast._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() { BrowserApp.deck.addEventListener("contextmenu", this.show.bind(this), 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]; }, 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) { return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI); } }, imageSaveableContext: { matches: function imageSaveableContextMatches(aElement) { 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; } }, mediaSaveableContext: { matches: function mediaSaveableContextMatches(aElement) { return (aElement instanceof HTMLVideoElement || aElement instanceof HTMLAudioElement); } }, 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; } }; }, /* 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