mobile/android/chrome/content/browser.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 #filter substitution
     2 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 "use strict";
     8 let Cc = Components.classes;
     9 let Ci = Components.interfaces;
    10 let Cu = Components.utils;
    11 let Cr = Components.results;
    13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    14 Cu.import("resource://gre/modules/Services.jsm");
    15 Cu.import("resource://gre/modules/AddonManager.jsm");
    16 Cu.import("resource://gre/modules/FileUtils.jsm");
    17 Cu.import("resource://gre/modules/JNI.jsm");
    18 Cu.import('resource://gre/modules/Payment.jsm');
    19 Cu.import("resource://gre/modules/NotificationDB.jsm");
    20 Cu.import("resource://gre/modules/SpatialNavigation.jsm");
    21 Cu.import("resource://gre/modules/UITelemetry.jsm");
    23 #ifdef ACCESSIBILITY
    24 Cu.import("resource://gre/modules/accessibility/AccessFu.jsm");
    25 #endif
    27 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    28                                   "resource://gre/modules/PluralForm.jsm");
    30 XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava",
    31                                   "resource://gre/modules/Messaging.jsm");
    33 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
    34                                   "resource://gre/modules/devtools/dbg-server.jsm");
    36 XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
    37                                   "resource://gre/modules/UserAgentOverrides.jsm");
    39 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
    40                                   "resource://gre/modules/LoginManagerContent.jsm");
    42 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
    43 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
    45 #ifdef MOZ_SAFE_BROWSING
    46 XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
    47                                   "resource://gre/modules/SafeBrowsing.jsm");
    48 #endif
    50 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    51                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
    53 XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
    54                                   "resource://gre/modules/Sanitizer.jsm");
    56 XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
    57                                   "resource://gre/modules/Prompt.jsm");
    59 XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
    60                                   "resource://gre/modules/HelperApps.jsm");
    62 XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
    63                                   "resource://gre/modules/SSLExceptions.jsm");
    65 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
    66                                   "resource://gre/modules/FormHistory.jsm");
    68 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
    69                                    "@mozilla.org/uuid-generator;1",
    70                                    "nsIUUIDGenerator");
    72 XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
    73                                   "resource://gre/modules/SimpleServiceDiscovery.jsm");
    75 #ifdef NIGHTLY_BUILD
    76 XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
    77                                   "resource://shumway/ShumwayUtils.jsm");
    78 #endif
    80 #ifdef MOZ_ANDROID_SYNTHAPKS
    81 XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
    82                                   "resource://gre/modules/WebappManager.jsm");
    83 #endif
    85 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
    86                                   "resource://gre/modules/CharsetMenu.jsm");
    88 // Lazily-loaded browser scripts:
    89 [
    90   ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
    91   ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
    92   ["AboutReader", "chrome://browser/content/aboutReader.js"],
    93   ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
    94   ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
    95   ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
    96   ["Linkifier", "chrome://browser/content/Linkify.js"],
    97   ["ZoomHelper", "chrome://browser/content/ZoomHelper.js"],
    98   ["CastingApps", "chrome://browser/content/CastingApps.js"],
    99 ].forEach(function (aScript) {
   100   let [name, script] = aScript;
   101   XPCOMUtils.defineLazyGetter(window, name, function() {
   102     let sandbox = {};
   103     Services.scriptloader.loadSubScript(script, sandbox);
   104     return sandbox[name];
   105   });
   106 });
   108 [
   109 #ifdef MOZ_WEBRTC
   110   ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"],
   111 #endif
   112   ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
   113   ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
   114   ["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
   115   ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
   116   ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
   117   ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
   118   ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
   119 ].forEach(function (aScript) {
   120   let [name, notifications, script] = aScript;
   121   XPCOMUtils.defineLazyGetter(window, name, function() {
   122     let sandbox = {};
   123     Services.scriptloader.loadSubScript(script, sandbox);
   124     return sandbox[name];
   125   });
   126   notifications.forEach(function (aNotification) {
   127     Services.obs.addObserver(function(s, t, d) {
   128         window[name].observe(s, t, d)
   129     }, aNotification, false);
   130   });
   131 });
   133 // Lazily-loaded JS modules that use observer notifications
   134 [
   135   ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
   136             "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
   137 ].forEach(module => {
   138   let [name, notifications, resource] = module;
   139   XPCOMUtils.defineLazyModuleGetter(this, name, resource);
   140   notifications.forEach(notification => {
   141     Services.obs.addObserver((s,t,d) => {
   142       this[name].observe(s,t,d)
   143     }, notification, false);
   144   });
   145 });
   147 XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
   148   "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
   150 XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
   151   "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
   153 XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
   154   "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
   156 #ifdef MOZ_WEBRTC
   157 XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
   158   "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService");
   159 #endif
   161 const kStateActive = 0x00000001; // :active pseudoclass for elements
   163 const kXLinkNamespace = "http://www.w3.org/1999/xlink";
   165 const kDefaultCSSViewportWidth = 980;
   166 const kDefaultCSSViewportHeight = 480;
   168 const kViewportRemeasureThrottle = 500;
   170 const kDoNotTrackPrefState = Object.freeze({
   171   NO_PREF: "0",
   172   DISALLOW_TRACKING: "1",
   173   ALLOW_TRACKING: "2",
   174 });
   176 function dump(a) {
   177   Services.console.logStringMessage(a);
   178 }
   180 function doChangeMaxLineBoxWidth(aWidth) {
   181   gReflowPending = null;
   182   let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
   183   let docShell = webNav.QueryInterface(Ci.nsIDocShell);
   184   let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
   186   let range = null;
   187   if (BrowserApp.selectedTab._mReflozPoint) {
   188     range = BrowserApp.selectedTab._mReflozPoint.range;
   189   }
   191   try {
   192     docViewer.pausePainting();
   193     docViewer.changeMaxLineBoxWidth(aWidth);
   195     if (range) {
   196       ZoomHelper.zoomInAndSnapToRange(range);
   197     } else {
   198       // In this case, we actually didn't zoom into a specific range. It
   199       // probably happened from a page load reflow-on-zoom event, so we
   200       // need to make sure painting is re-enabled.
   201       BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
   202     }
   203   } finally {
   204     docViewer.resumePainting();
   205   }
   206 }
   208 function fuzzyEquals(a, b) {
   209   return (Math.abs(a - b) < 1e-6);
   210 }
   212 /**
   213  * Convert a font size to CSS pixels (px) from twentieiths-of-a-point
   214  * (twips).
   215  */
   216 function convertFromTwipsToPx(aSize) {
   217   return aSize/240 * 16.0;
   218 }
   220 #ifdef MOZ_CRASHREPORTER
   221 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
   222 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
   223   "@mozilla.org/xre/app-info;1", "nsICrashReporter");
   224 #endif
   226 XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
   227   let ContentAreaUtils = {};
   228   Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
   229   return ContentAreaUtils;
   230 });
   232 XPCOMUtils.defineLazyModuleGetter(this, "Rect",
   233                                   "resource://gre/modules/Geometry.jsm");
   235 function resolveGeckoURI(aURI) {
   236   if (!aURI)
   237     throw "Can't resolve an empty uri";
   239   if (aURI.startsWith("chrome://")) {
   240     let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
   241     return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
   242   } else if (aURI.startsWith("resource://")) {
   243     let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
   244     return handler.resolveURI(Services.io.newURI(aURI, null, null));
   245   }
   246   return aURI;
   247 }
   249 /**
   250  * Cache of commonly used string bundles.
   251  */
   252 var Strings = {};
   253 [
   254   ["brand",      "chrome://branding/locale/brand.properties"],
   255   ["browser",    "chrome://browser/locale/browser.properties"]
   256 ].forEach(function (aStringBundle) {
   257   let [name, bundle] = aStringBundle;
   258   XPCOMUtils.defineLazyGetter(Strings, name, function() {
   259     return Services.strings.createBundle(bundle);
   260   });
   261 });
   263 const kFormHelperModeDisabled = 0;
   264 const kFormHelperModeEnabled = 1;
   265 const kFormHelperModeDynamic = 2;   // disabled on tablets
   267 var BrowserApp = {
   268   _tabs: [],
   269   _selectedTab: null,
   270   _prefObservers: [],
   271   isGuest: false,
   273   get isTablet() {
   274     let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
   275     delete this.isTablet;
   276     return this.isTablet = sysInfo.get("tablet");
   277   },
   279   get isOnLowMemoryPlatform() {
   280     let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
   281     delete this.isOnLowMemoryPlatform;
   282     return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
   283   },
   285   deck: null,
   287   startup: function startup() {
   288     window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
   289     dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
   291     this.deck = document.getElementById("browsers");
   292     this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
   293       try {
   294         BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
   295         Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
   296         sendMessageToJava({ type: "Gecko:DelayedStartup" });
   297       } catch(ex) { console.log(ex); }
   298     }, false);
   300     BrowserEventHandler.init();
   301     ViewportHandler.init();
   303     Services.androidBridge.browserApp = this;
   305     Services.obs.addObserver(this, "Locale:Changed", false);
   306     Services.obs.addObserver(this, "Tab:Load", false);
   307     Services.obs.addObserver(this, "Tab:Selected", false);
   308     Services.obs.addObserver(this, "Tab:Closed", false);
   309     Services.obs.addObserver(this, "Session:Back", false);
   310     Services.obs.addObserver(this, "Session:ShowHistory", false);
   311     Services.obs.addObserver(this, "Session:Forward", false);
   312     Services.obs.addObserver(this, "Session:Reload", false);
   313     Services.obs.addObserver(this, "Session:Stop", false);
   314     Services.obs.addObserver(this, "SaveAs:PDF", false);
   315     Services.obs.addObserver(this, "Browser:Quit", false);
   316     Services.obs.addObserver(this, "Preferences:Set", false);
   317     Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
   318     Services.obs.addObserver(this, "Sanitize:ClearData", false);
   319     Services.obs.addObserver(this, "FullScreen:Exit", false);
   320     Services.obs.addObserver(this, "Viewport:Change", false);
   321     Services.obs.addObserver(this, "Viewport:Flush", false);
   322     Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false);
   323     Services.obs.addObserver(this, "Passwords:Init", false);
   324     Services.obs.addObserver(this, "FormHistory:Init", false);
   325     Services.obs.addObserver(this, "gather-telemetry", false);
   326     Services.obs.addObserver(this, "keyword-search", false);
   327 #ifdef MOZ_ANDROID_SYNTHAPKS
   328     Services.obs.addObserver(this, "webapps-runtime-install", false);
   329     Services.obs.addObserver(this, "webapps-runtime-install-package", false);
   330     Services.obs.addObserver(this, "webapps-ask-install", false);
   331     Services.obs.addObserver(this, "webapps-launch", false);
   332     Services.obs.addObserver(this, "webapps-uninstall", false);
   333     Services.obs.addObserver(this, "Webapps:AutoInstall", false);
   334     Services.obs.addObserver(this, "Webapps:Load", false);
   335     Services.obs.addObserver(this, "Webapps:AutoUninstall", false);
   336 #endif
   337     Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
   339     function showFullScreenWarning() {
   340       NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
   341     }
   343     window.addEventListener("fullscreen", function() {
   344       sendMessageToJava({
   345         type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide"
   346       });
   347     }, false);
   349     window.addEventListener("mozfullscreenchange", function() {
   350       sendMessageToJava({
   351         type: document.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop"
   352       });
   354       if (document.mozFullScreen)
   355         showFullScreenWarning();
   356     }, false);
   358     // When a restricted key is pressed in DOM full-screen mode, we should display
   359     // the "Press ESC to exit" warning message.
   360     window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true);
   362     NativeWindow.init();
   363     LightWeightThemeWebInstaller.init();
   364     Downloads.init();
   365     FormAssistant.init();
   366     IndexedDB.init();
   367     HealthReportStatusListener.init();
   368     XPInstallObserver.init();
   369     CharacterEncoding.init();
   370     ActivityObserver.init();
   371 #ifdef MOZ_ANDROID_SYNTHAPKS
   372     // TODO: replace with Android implementation of WebappOSUtils.isLaunchable.
   373     Cu.import("resource://gre/modules/Webapps.jsm");
   374     DOMApplicationRegistry.allAppsLaunchable = true;
   375 #else
   376     WebappsUI.init();
   377 #endif
   378     RemoteDebugger.init();
   379     Reader.init();
   380     UserAgentOverrides.init();
   381     DesktopUserAgent.init();
   382     CastingApps.init();
   383     Distribution.init();
   384     Tabs.init();
   385 #ifdef ACCESSIBILITY
   386     AccessFu.attach(window);
   387 #endif
   388 #ifdef NIGHTLY_BUILD
   389     ShumwayUtils.init();
   390 #endif
   392     // Init LoginManager
   393     Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
   395     let url = null;
   396     let pinned = false;
   397     if ("arguments" in window) {
   398       if (window.arguments[0])
   399         url = window.arguments[0];
   400       if (window.arguments[1])
   401         gScreenWidth = window.arguments[1];
   402       if (window.arguments[2])
   403         gScreenHeight = window.arguments[2];
   404       if (window.arguments[3])
   405         pinned = window.arguments[3];
   406       if (window.arguments[4])
   407         this.isGuest = window.arguments[4];
   408     }
   410     if (pinned) {
   411       this._initRuntime(this._startupStatus, url, aUrl => this.addTab(aUrl));
   412     } else {
   413       SearchEngines.init();
   414       this.initContextMenu();
   415     }
   416     // The order that context menu items are added is important
   417     // Make sure the "Open in App" context menu item appears at the bottom of the list
   418     ExternalApps.init();
   420     // XXX maybe we don't do this if the launch was kicked off from external
   421     Services.io.offline = false;
   423     // Broadcast a UIReady message so add-ons know we are finished with startup
   424     let event = document.createEvent("Events");
   425     event.initEvent("UIReady", true, false);
   426     window.dispatchEvent(event);
   428     if (this._startupStatus)
   429       this.onAppUpdated();
   431     // Store the low-precision buffer pref
   432     this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer");
   434     // notify java that gecko has loaded
   435     sendMessageToJava({ type: "Gecko:Ready" });
   437 #ifdef MOZ_SAFE_BROWSING
   438     // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
   439     setTimeout(function() { SafeBrowsing.init(); }, 5000);
   440 #endif
   441   },
   443   get _startupStatus() {
   444     delete this._startupStatus;
   446     let savedMilestone = null;
   447     try {
   448       savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
   449     } catch (e) {
   450     }
   451 #expand    let ourMilestone = "__MOZ_APP_VERSION__";
   452     this._startupStatus = "";
   453     if (ourMilestone != savedMilestone) {
   454       Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone);
   455       this._startupStatus = savedMilestone ? "upgrade" : "new";
   456     }
   458     return this._startupStatus;
   459   },
   461   /**
   462    * Pass this a locale string, such as "fr" or "es_ES".
   463    */
   464   setLocale: function (locale) {
   465     console.log("browser.js: requesting locale set: " + locale);
   466     sendMessageToJava({ type: "Locale:Set", locale: locale });
   467   },
   469   _initRuntime: function(status, url, callback) {
   470     let sandbox = {};
   471     Services.scriptloader.loadSubScript("chrome://browser/content/WebappRT.js", sandbox);
   472     window.WebappRT = sandbox.WebappRT;
   473     WebappRT.init(status, url, callback);
   474   },
   476   initContextMenu: function ba_initContextMenu() {
   477     // TODO: These should eventually move into more appropriate classes
   478     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"),
   479       NativeWindow.contextmenus.linkOpenableNonPrivateContext,
   480       function(aTarget) {
   481         UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
   482         UITelemetry.addEvent("loadurl.1", "contextmenu", null);
   484         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   485         ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
   486         BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
   488         let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened");
   489         let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
   490         NativeWindow.toast.show(label, "short");
   491       });
   493     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInPrivateTab"),
   494       NativeWindow.contextmenus.linkOpenableContext,
   495       function(aTarget) {
   496         UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_private_tab");
   497         UITelemetry.addEvent("loadurl.1", "contextmenu", null);
   499         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   500         ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
   501         BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true });
   503         let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened");
   504         let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
   505         NativeWindow.toast.show(label, "short");
   506       });
   508     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyLink"),
   509       NativeWindow.contextmenus.linkCopyableContext,
   510       function(aTarget) {
   511         UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link");
   513         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   514         NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
   515       });
   517     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyEmailAddress"),
   518       NativeWindow.contextmenus.emailLinkContext,
   519       function(aTarget) {
   520         UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email");
   522         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   523         let emailAddr = NativeWindow.contextmenus._stripScheme(url);
   524         NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr);
   525       });
   527     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyPhoneNumber"),
   528       NativeWindow.contextmenus.phoneNumberLinkContext,
   529       function(aTarget) {
   530         UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone");
   532         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   533         let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
   534         NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
   535       });
   537     NativeWindow.contextmenus.add({
   538       label: Strings.browser.GetStringFromName("contextmenu.shareLink"),
   539       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
   540       selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext),
   541       showAsActions: function(aElement) {
   542         return {
   543           title: aElement.textContent.trim() || aElement.title.trim(),
   544           uri: NativeWindow.contextmenus._getLinkURL(aElement),
   545         };
   546       },
   547       icon: "drawable://ic_menu_share",
   548       callback: function(aTarget) {
   549         UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link");
   550       }
   551     });
   553     NativeWindow.contextmenus.add({
   554       label: Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"),
   555       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
   556       selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
   557       showAsActions: function(aElement) {
   558         let url = NativeWindow.contextmenus._getLinkURL(aElement);
   559         let emailAddr = NativeWindow.contextmenus._stripScheme(url);
   560         let title = aElement.textContent || aElement.title;
   561         return {
   562           title: title,
   563           uri: emailAddr,
   564         };
   565       },
   566       icon: "drawable://ic_menu_share",
   567       callback: function(aTarget) {
   568         UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email");
   569       }
   570     });
   572     NativeWindow.contextmenus.add({
   573       label: Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"),
   574       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
   575       selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
   576       showAsActions: function(aElement) {
   577         let url = NativeWindow.contextmenus._getLinkURL(aElement);
   578         let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
   579         let title = aElement.textContent || aElement.title;
   580         return {
   581           title: title,
   582           uri: phoneNumber,
   583         };
   584       },
   585       icon: "drawable://ic_menu_share",
   586       callback: function(aTarget) {
   587         UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone");
   588       }
   589     });
   591     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
   592       NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
   593       function(aTarget) {
   594         UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");
   596         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   597         sendMessageToJava({
   598           type: "Contact:Add",
   599           email: url
   600         });
   601       });
   603     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
   604       NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
   605       function(aTarget) {
   606         UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");
   608         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   609         sendMessageToJava({
   610           type: "Contact:Add",
   611           phone: url
   612         });
   613       });
   615     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"),
   616       NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext),
   617       function(aTarget) {
   618         UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
   620         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   621         let title = aTarget.textContent || aTarget.title || url;
   622         sendMessageToJava({
   623           type: "Bookmark:Insert",
   624           url: url,
   625           title: title
   626         });
   627       });
   629     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.playMedia"),
   630       NativeWindow.contextmenus.mediaContext("media-paused"),
   631       function(aTarget) {
   632         UITelemetry.addEvent("action.1", "contextmenu", null, "web_play");
   633         aTarget.play();
   634       });
   636     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.pauseMedia"),
   637       NativeWindow.contextmenus.mediaContext("media-playing"),
   638       function(aTarget) {
   639         UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause");
   640         aTarget.pause();
   641       });
   643     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.showControls2"),
   644       NativeWindow.contextmenus.mediaContext("media-hidingcontrols"),
   645       function(aTarget) {
   646         UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media");
   647         aTarget.setAttribute("controls", true);
   648       });
   650     NativeWindow.contextmenus.add({
   651       label: Strings.browser.GetStringFromName("contextmenu.shareMedia"),
   652       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
   653       selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")),
   654       showAsActions: function(aElement) {
   655         let url = (aElement.currentSrc || aElement.src);
   656         let title = aElement.textContent || aElement.title;
   657         return {
   658           title: title,
   659           uri: url,
   660           type: "video/*",
   661         };
   662       },
   663       icon: "drawable://ic_menu_share",
   664       callback: function(aTarget) {
   665         UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media");
   666       }
   667     });
   669     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"),
   670       NativeWindow.contextmenus.SelectorContext("video:not(:-moz-full-screen)"),
   671       function(aTarget) {
   672         UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen");
   673         aTarget.mozRequestFullScreen();
   674       });
   676     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.mute"),
   677       NativeWindow.contextmenus.mediaContext("media-unmuted"),
   678       function(aTarget) {
   679         UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute");
   680         aTarget.muted = true;
   681       });
   683     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"),
   684       NativeWindow.contextmenus.mediaContext("media-muted"),
   685       function(aTarget) {
   686         UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute");
   687         aTarget.muted = false;
   688       });
   690     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyImageLocation"),
   691       NativeWindow.contextmenus.imageLocationCopyableContext,
   692       function(aTarget) {
   693         UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image");
   695         let url = aTarget.src;
   696         NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
   697       });
   699     NativeWindow.contextmenus.add({
   700       label: Strings.browser.GetStringFromName("contextmenu.shareImage"),
   701       selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
   702       order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
   703       showAsActions: function(aTarget) {
   704         let doc = aTarget.ownerDocument;
   705         let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
   706                                                          .getImgCacheForDocument(doc);
   707         let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet);
   708         let src = aTarget.src;
   709         return {
   710           title: src,
   711           uri: src,
   712           type: "image/*",
   713         };
   714       },
   715       icon: "drawable://ic_menu_share",
   716       menu: true,
   717       callback: function(aTarget) {
   718         UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image");
   719       }
   720     });
   722     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.saveImage"),
   723       NativeWindow.contextmenus.imageSaveableContext,
   724       function(aTarget) {
   725         UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image");
   727         ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
   728                                       false, true, aTarget.ownerDocument.documentURIObject,
   729                                       aTarget.ownerDocument);
   730       });
   732     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"),
   733       NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
   734       function(aTarget) {
   735         UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");
   737         let src = aTarget.src;
   738         sendMessageToJava({
   739           type: "Image:SetAs",
   740           url: src
   741         });
   742       });
   744     NativeWindow.contextmenus.add(
   745       function(aTarget) {
   746         if (aTarget instanceof HTMLVideoElement) {
   747           // If a video element is zero width or height, its essentially
   748           // an HTMLAudioElement.
   749           if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 )
   750             return Strings.browser.GetStringFromName("contextmenu.saveAudio");
   751           return Strings.browser.GetStringFromName("contextmenu.saveVideo");
   752         } else if (aTarget instanceof HTMLAudioElement) {
   753           return Strings.browser.GetStringFromName("contextmenu.saveAudio");
   754         }
   755         return Strings.browser.GetStringFromName("contextmenu.saveVideo");
   756       }, NativeWindow.contextmenus.mediaSaveableContext,
   757       function(aTarget) {
   758         UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media");
   760         let url = aTarget.currentSrc || aTarget.src;
   761         let filePickerTitleKey = (aTarget instanceof HTMLVideoElement &&
   762                                   (aTarget.videoWidth != 0 && aTarget.videoHeight != 0))
   763                                   ? "SaveVideoTitle" : "SaveAudioTitle";
   764         // Skipped trying to pull MIME type out of cache for now
   765         ContentAreaUtils.internalSave(url, null, null, null, null, false,
   766                                       filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
   767                                       aTarget.ownerDocument, true, null);
   768       });
   769   },
   771   onAppUpdated: function() {
   772     // initialize the form history and passwords databases on upgrades
   773     Services.obs.notifyObservers(null, "FormHistory:Init", "");
   774     Services.obs.notifyObservers(null, "Passwords:Init", "");
   776     // Migrate user-set "plugins.click_to_play" pref. See bug 884694.
   777     // Because the default value is true, a user-set pref means that the pref was set to false.
   778     if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
   779       Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
   780       Services.prefs.clearUserPref("plugins.click_to_play");
   781     }
   782   },
   784   shutdown: function shutdown() {
   785     NativeWindow.uninit();
   786     LightWeightThemeWebInstaller.uninit();
   787     FormAssistant.uninit();
   788     IndexedDB.uninit();
   789     ViewportHandler.uninit();
   790     XPInstallObserver.uninit();
   791     HealthReportStatusListener.uninit();
   792     CharacterEncoding.uninit();
   793     SearchEngines.uninit();
   794 #ifndef MOZ_ANDROID_SYNTHAPKS
   795     WebappsUI.uninit();
   796 #endif
   797     RemoteDebugger.uninit();
   798     Reader.uninit();
   799     UserAgentOverrides.uninit();
   800     DesktopUserAgent.uninit();
   801     ExternalApps.uninit();
   802     CastingApps.uninit();
   803     Distribution.uninit();
   804     Tabs.uninit();
   805   },
   807   // This function returns false during periods where the browser displayed document is
   808   // different from the browser content document, so user actions and some kinds of viewport
   809   // updates should be ignored. This period starts when we start loading a new page or
   810   // switch tabs, and ends when the new browser content document has been drawn and handed
   811   // off to the compositor.
   812   isBrowserContentDocumentDisplayed: function() {
   813     try {
   814       if (!Services.androidBridge.isContentDocumentDisplayed())
   815         return false;
   816     } catch (e) {
   817       return false;
   818     }
   820     let tab = this.selectedTab;
   821     if (!tab)
   822       return false;
   823     return tab.contentDocumentIsDisplayed;
   824   },
   826   contentDocumentChanged: function() {
   827     window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true;
   828     Services.androidBridge.contentDocumentChanged();
   829   },
   831   get tabs() {
   832     return this._tabs;
   833   },
   835   get selectedTab() {
   836     return this._selectedTab;
   837   },
   839   set selectedTab(aTab) {
   840     if (this._selectedTab == aTab)
   841       return;
   843     if (this._selectedTab) {
   844       this._selectedTab.setActive(false);
   845     }
   847     this._selectedTab = aTab;
   848     if (!aTab)
   849       return;
   851     aTab.setActive(true);
   852     aTab.setResolution(aTab._zoom, true);
   853     this.contentDocumentChanged();
   854     this.deck.selectedPanel = aTab.browser;
   855     // Focus the browser so that things like selection will be styled correctly.
   856     aTab.browser.focus();
   857   },
   859   get selectedBrowser() {
   860     if (this._selectedTab)
   861       return this._selectedTab.browser;
   862     return null;
   863   },
   865   getTabForId: function getTabForId(aId) {
   866     let tabs = this._tabs;
   867     for (let i=0; i < tabs.length; i++) {
   868        if (tabs[i].id == aId)
   869          return tabs[i];
   870     }
   871     return null;
   872   },
   874   getTabForBrowser: function getTabForBrowser(aBrowser) {
   875     let tabs = this._tabs;
   876     for (let i = 0; i < tabs.length; i++) {
   877       if (tabs[i].browser == aBrowser)
   878         return tabs[i];
   879     }
   880     return null;
   881   },
   883   getTabForWindow: function getTabForWindow(aWindow) {
   884     let tabs = this._tabs;
   885     for (let i = 0; i < tabs.length; i++) {
   886       if (tabs[i].browser.contentWindow == aWindow)
   887         return tabs[i];
   888     }
   889     return null;
   890   },
   892   getBrowserForWindow: function getBrowserForWindow(aWindow) {
   893     let tabs = this._tabs;
   894     for (let i = 0; i < tabs.length; i++) {
   895       if (tabs[i].browser.contentWindow == aWindow)
   896         return tabs[i].browser;
   897     }
   898     return null;
   899   },
   901   getBrowserForDocument: function getBrowserForDocument(aDocument) {
   902     let tabs = this._tabs;
   903     for (let i = 0; i < tabs.length; i++) {
   904       if (tabs[i].browser.contentDocument == aDocument)
   905         return tabs[i].browser;
   906     }
   907     return null;
   908   },
   910   loadURI: function loadURI(aURI, aBrowser, aParams) {
   911     aBrowser = aBrowser || this.selectedBrowser;
   912     if (!aBrowser)
   913       return;
   915     aParams = aParams || {};
   917     let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
   918     let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null;
   919     let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
   920     let charset = "charset" in aParams ? aParams.charset : null;
   922     let tab = this.getTabForBrowser(aBrowser);
   923     if (tab) {
   924       if ("userSearch" in aParams) tab.userSearch = aParams.userSearch;
   925     }
   927     try {
   928       aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
   929     } catch(e) {
   930       if (tab) {
   931         let message = {
   932           type: "Content:LoadError",
   933           tabID: tab.id
   934         };
   935         sendMessageToJava(message);
   936         dump("Handled load error: " + e)
   937       }
   938     }
   939   },
   941   addTab: function addTab(aURI, aParams) {
   942     aParams = aParams || {};
   944     let newTab = new Tab(aURI, aParams);
   945     this._tabs.push(newTab);
   947     let selected = "selected" in aParams ? aParams.selected : true;
   948     if (selected)
   949       this.selectedTab = newTab;
   951     let pinned = "pinned" in aParams ? aParams.pinned : false;
   952     if (pinned) {
   953       let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
   954       ss.setTabValue(newTab, "appOrigin", aURI);
   955     }
   957     let evt = document.createEvent("UIEvents");
   958     evt.initUIEvent("TabOpen", true, false, window, null);
   959     newTab.browser.dispatchEvent(evt);
   961     return newTab;
   962   },
   964   // Use this method to close a tab from JS. This method sends a message
   965   // to Java to close the tab in the Java UI (we'll get a Tab:Closed message
   966   // back from Java when that happens).
   967   closeTab: function closeTab(aTab) {
   968     if (!aTab) {
   969       Cu.reportError("Error trying to close tab (tab doesn't exist)");
   970       return;
   971     }
   973     let message = {
   974       type: "Tab:Close",
   975       tabID: aTab.id
   976     };
   977     sendMessageToJava(message);
   978   },
   980 #ifdef MOZ_ANDROID_SYNTHAPKS
   981   _loadWebapp: function(aMessage) {
   983     this._initRuntime(this._startupStatus, aMessage.url, aUrl => {
   984       this.manifestUrl = aMessage.url;
   985       this.addTab(aUrl, { title: aMessage.name });
   986     });
   987   },
   988 #endif
   990   // Calling this will update the state in BrowserApp after a tab has been
   991   // closed in the Java UI.
   992   _handleTabClosed: function _handleTabClosed(aTab) {
   993     if (aTab == this.selectedTab)
   994       this.selectedTab = null;
   996     let evt = document.createEvent("UIEvents");
   997     evt.initUIEvent("TabClose", true, false, window, null);
   998     aTab.browser.dispatchEvent(evt);
  1000     aTab.destroy();
  1001     this._tabs.splice(this._tabs.indexOf(aTab), 1);
  1002   },
  1004   // Use this method to select a tab from JS. This method sends a message
  1005   // to Java to select the tab in the Java UI (we'll get a Tab:Selected message
  1006   // back from Java when that happens).
  1007   selectTab: function selectTab(aTab) {
  1008     if (!aTab) {
  1009       Cu.reportError("Error trying to select tab (tab doesn't exist)");
  1010       return;
  1013     // There's nothing to do if the tab is already selected
  1014     if (aTab == this.selectedTab)
  1015       return;
  1017     let message = {
  1018       type: "Tab:Select",
  1019       tabID: aTab.id
  1020     };
  1021     sendMessageToJava(message);
  1022   },
  1024   /**
  1025    * Gets an open tab with the given URL.
  1027    * @param  aURL URL to look for
  1028    * @return the tab with the given URL, or null if no such tab exists
  1029    */
  1030   getTabWithURL: function getTabWithURL(aURL) {
  1031     let uri = Services.io.newURI(aURL, null, null);
  1032     for (let i = 0; i < this._tabs.length; ++i) {
  1033       let tab = this._tabs[i];
  1034       if (tab.browser.currentURI.equals(uri)) {
  1035         return tab;
  1038     return null;
  1039   },
  1041   /**
  1042    * If a tab with the given URL already exists, that tab is selected.
  1043    * Otherwise, a new tab is opened with the given URL.
  1045    * @param aURL URL to open
  1046    */
  1047   selectOrOpenTab: function selectOrOpenTab(aURL) {
  1048     let tab = this.getTabWithURL(aURL);
  1049     if (tab == null) {
  1050       this.addTab(aURL);
  1051     } else {
  1052       this.selectTab(tab);
  1054   },
  1056   // This method updates the state in BrowserApp after a tab has been selected
  1057   // in the Java UI.
  1058   _handleTabSelected: function _handleTabSelected(aTab) {
  1059     this.selectedTab = aTab;
  1061     let evt = document.createEvent("UIEvents");
  1062     evt.initUIEvent("TabSelect", true, false, window, null);
  1063     aTab.browser.dispatchEvent(evt);
  1064   },
  1066   quit: function quit() {
  1067     // Figure out if there's at least one other browser window around.
  1068     let lastBrowser = true;
  1069     let e = Services.wm.getEnumerator("navigator:browser");
  1070     while (e.hasMoreElements() && lastBrowser) {
  1071       let win = e.getNext();
  1072       if (!win.closed && win != window)
  1073         lastBrowser = false;
  1076     if (lastBrowser) {
  1077       // Let everyone know we are closing the last browser window
  1078       let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
  1079       Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null);
  1080       if (closingCanceled.data)
  1081         return;
  1083       Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null);
  1086     window.QueryInterface(Ci.nsIDOMChromeWindow).minimize();
  1087     window.close();
  1088   },
  1090   saveAsPDF: function saveAsPDF(aBrowser) {
  1091     // Create the final destination file location
  1092     let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
  1093     fileName = fileName.trim() + ".pdf";
  1095     let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
  1096     let downloadsDir = dm.defaultDownloadsDirectory;
  1098     let file = downloadsDir.clone();
  1099     file.append(fileName);
  1100     file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8));
  1102     let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings;
  1103     printSettings.printSilent = true;
  1104     printSettings.showPrintProgress = false;
  1105     printSettings.printBGImages = true;
  1106     printSettings.printBGColors = true;
  1107     printSettings.printToFile = true;
  1108     printSettings.toFileName = file.path;
  1109     printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
  1110     printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
  1112     //XXX we probably need a preference here, the header can be useful
  1113     printSettings.footerStrCenter = "";
  1114     printSettings.footerStrLeft   = "";
  1115     printSettings.footerStrRight  = "";
  1116     printSettings.headerStrCenter = "";
  1117     printSettings.headerStrLeft   = "";
  1118     printSettings.headerStrRight  = "";
  1120     // Create a valid mimeInfo for the PDF
  1121     let ms = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
  1122     let mimeInfo = ms.getFromTypeAndExtension("application/pdf", "pdf");
  1124     let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
  1125                                                 .getInterface(Ci.nsIWebBrowserPrint);
  1127     let cancelable = {
  1128       cancel: function (aReason) {
  1129         webBrowserPrint.cancel();
  1132     let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aBrowser.contentWindow);
  1133     let download = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,
  1134                                   aBrowser.currentURI,
  1135                                   Services.io.newFileURI(file), "", mimeInfo,
  1136                                   Date.now() * 1000, null, cancelable, isPrivate);
  1138     webBrowserPrint.print(printSettings, download);
  1139   },
  1141   notifyPrefObservers: function(aPref) {
  1142     this._prefObservers[aPref].forEach(function(aRequestId) {
  1143       this.getPreferences(aRequestId, [aPref], 1);
  1144     }, this);
  1145   },
  1147   handlePreferencesRequest: function handlePreferencesRequest(aRequestId,
  1148                                                               aPrefNames,
  1149                                                               aListen) {
  1151     let prefs = [];
  1153     for (let prefName of aPrefNames) {
  1154       let pref = {
  1155         name: prefName,
  1156         type: "",
  1157         value: null
  1158       };
  1160       if (aListen) {
  1161         if (this._prefObservers[prefName])
  1162           this._prefObservers[prefName].push(aRequestId);
  1163         else
  1164           this._prefObservers[prefName] = [ aRequestId ];
  1165         Services.prefs.addObserver(prefName, this, false);
  1168       // These pref names are not "real" pref names.
  1169       // They are used in the setting menu,
  1170       // and these are passed when initializing the setting menu.
  1171       switch (prefName) {
  1172         // The plugin pref is actually two separate prefs, so
  1173         // we need to handle it differently
  1174         case "plugin.enable":
  1175           pref.type = "string";// Use a string type for java's ListPreference
  1176           pref.value = PluginHelper.getPluginPreference();
  1177           prefs.push(pref);
  1178           continue;
  1179         // Handle master password
  1180         case "privacy.masterpassword.enabled":
  1181           pref.type = "bool";
  1182           pref.value = MasterPassword.enabled;
  1183           prefs.push(pref);
  1184           continue;
  1185         // Handle do-not-track preference
  1186         case "privacy.donottrackheader":
  1187           pref.type = "string";
  1189           let enableDNT = Services.prefs.getBoolPref("privacy.donottrackheader.enabled");
  1190           if (!enableDNT) {
  1191             pref.value = kDoNotTrackPrefState.NO_PREF;
  1192           } else {
  1193             let dntState = Services.prefs.getIntPref("privacy.donottrackheader.value");
  1194             pref.value = (dntState === 0) ? kDoNotTrackPrefState.ALLOW_TRACKING :
  1195                                             kDoNotTrackPrefState.DISALLOW_TRACKING;
  1198           prefs.push(pref);
  1199           continue;
  1200 #ifdef MOZ_CRASHREPORTER
  1201         // Crash reporter submit pref must be fetched from nsICrashReporter service.
  1202         case "datareporting.crashreporter.submitEnabled":
  1203           pref.type = "bool";
  1204           pref.value = CrashReporter.submitReports;
  1205           prefs.push(pref);
  1206           continue;
  1207 #endif
  1210       try {
  1211         switch (Services.prefs.getPrefType(prefName)) {
  1212           case Ci.nsIPrefBranch.PREF_BOOL:
  1213             pref.type = "bool";
  1214             pref.value = Services.prefs.getBoolPref(prefName);
  1215             break;
  1216           case Ci.nsIPrefBranch.PREF_INT:
  1217             pref.type = "int";
  1218             pref.value = Services.prefs.getIntPref(prefName);
  1219             break;
  1220           case Ci.nsIPrefBranch.PREF_STRING:
  1221           default:
  1222             pref.type = "string";
  1223             try {
  1224               // Try in case it's a localized string (will throw an exception if not)
  1225               pref.value = Services.prefs.getComplexValue(prefName, Ci.nsIPrefLocalizedString).data;
  1226             } catch (e) {
  1227               pref.value = Services.prefs.getCharPref(prefName);
  1229             break;
  1231       } catch (e) {
  1232         dump("Error reading pref [" + prefName + "]: " + e);
  1233         // preference does not exist; do not send it
  1234         continue;
  1237       // Some Gecko preferences use integers or strings to reference
  1238       // state instead of directly representing the value.
  1239       // Since the Java UI uses the type to determine which ui elements
  1240       // to show and how to handle them, we need to normalize these
  1241       // preferences to the correct type.
  1242       switch (prefName) {
  1243         // (string) index for determining which multiple choice value to display.
  1244         case "browser.chrome.titlebarMode":
  1245         case "network.cookie.cookieBehavior":
  1246         case "font.size.inflation.minTwips":
  1247         case "home.sync.updateMode":
  1248           pref.type = "string";
  1249           pref.value = pref.value.toString();
  1250           break;
  1253       prefs.push(pref);
  1256     sendMessageToJava({
  1257       type: "Preferences:Data",
  1258       requestId: aRequestId,    // opaque request identifier, can be any string/int/whatever
  1259       preferences: prefs
  1260     });
  1261   },
  1263   setPreferences: function setPreferences(aPref) {
  1264     let json = JSON.parse(aPref);
  1266     switch (json.name) {
  1267       // The plugin pref is actually two separate prefs, so
  1268       // we need to handle it differently
  1269       case "plugin.enable":
  1270         PluginHelper.setPluginPreference(json.value);
  1271         return;
  1273       // MasterPassword pref is not real, we just need take action and leave
  1274       case "privacy.masterpassword.enabled":
  1275         if (MasterPassword.enabled)
  1276           MasterPassword.removePassword(json.value);
  1277         else
  1278           MasterPassword.setPassword(json.value);
  1279         return;
  1281       // "privacy.donottrackheader" is not "real" pref name, it's used in the setting menu.
  1282       case "privacy.donottrackheader":
  1283         switch (json.value) {
  1284           // Don't tell anything about tracking me
  1285           case kDoNotTrackPrefState.NO_PREF:
  1286             Services.prefs.setBoolPref("privacy.donottrackheader.enabled", false);
  1287             Services.prefs.clearUserPref("privacy.donottrackheader.value");
  1288             break;
  1289           // Accept tracking me
  1290           case kDoNotTrackPrefState.ALLOW_TRACKING:
  1291             Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true);
  1292             Services.prefs.setIntPref("privacy.donottrackheader.value", 0);
  1293             break;
  1294           // Not accept tracking me
  1295           case kDoNotTrackPrefState.DISALLOW_TRACKING:
  1296             Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true);
  1297             Services.prefs.setIntPref("privacy.donottrackheader.value", 1);
  1298             break;
  1300         return;
  1302       // Enabling or disabling suggestions will prevent future prompts
  1303       case SearchEngines.PREF_SUGGEST_ENABLED:
  1304         Services.prefs.setBoolPref(SearchEngines.PREF_SUGGEST_PROMPTED, true);
  1305         break;
  1307 #ifdef MOZ_CRASHREPORTER
  1308       // Crash reporter preference is in a service; set and return.
  1309       case "datareporting.crashreporter.submitEnabled":
  1310         CrashReporter.submitReports = json.value;
  1311         return;
  1312 #endif
  1313       // When sending to Java, we normalized special preferences that use
  1314       // integers and strings to represent booleans. Here, we convert them back
  1315       // to their actual types so we can store them.
  1316       case "browser.chrome.titlebarMode":
  1317       case "network.cookie.cookieBehavior":
  1318       case "font.size.inflation.minTwips":
  1319       case "home.sync.updateMode":
  1320         json.type = "int";
  1321         json.value = parseInt(json.value);
  1322         break;
  1325     switch (json.type) {
  1326       case "bool":
  1327         Services.prefs.setBoolPref(json.name, json.value);
  1328         break;
  1329       case "int":
  1330         Services.prefs.setIntPref(json.name, json.value);
  1331         break;
  1332       default: {
  1333         let pref = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
  1334         pref.data = json.value;
  1335         Services.prefs.setComplexValue(json.name, Ci.nsISupportsString, pref);
  1336         break;
  1339   },
  1341   sanitize: function (aItems) {
  1342     let json = JSON.parse(aItems);
  1343     let success = true;
  1345     for (let key in json) {
  1346       if (!json[key])
  1347         continue;
  1349       try {
  1350         switch (key) {
  1351           case "cookies_sessions":
  1352             Sanitizer.clearItem("cookies");
  1353             Sanitizer.clearItem("sessions");
  1354             break;
  1355           default:
  1356             Sanitizer.clearItem(key);
  1358       } catch (e) {
  1359         dump("sanitize error: " + e);
  1360         success = false;
  1364     sendMessageToJava({
  1365       type: "Sanitize:Finished",
  1366       success: success
  1367     });
  1368   },
  1370   getFocusedInput: function(aBrowser, aOnlyInputElements = false) {
  1371     if (!aBrowser)
  1372       return null;
  1374     let doc = aBrowser.contentDocument;
  1375     if (!doc)
  1376       return null;
  1378     let focused = doc.activeElement;
  1379     while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) {
  1380       doc = focused.contentDocument;
  1381       focused = doc.activeElement;
  1384     if (focused instanceof HTMLInputElement && focused.mozIsTextField(false))
  1385       return focused;
  1387     if (aOnlyInputElements)
  1388       return null;
  1390     if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) {
  1392       if (focused instanceof HTMLBodyElement) {
  1393         // we are putting focus into a contentEditable frame. scroll the frame into
  1394         // view instead of the contentEditable document contained within, because that
  1395         // results in a better user experience
  1396         focused = focused.ownerDocument.defaultView.frameElement;
  1398       return focused;
  1400     return null;
  1401   },
  1403   scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
  1404     let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
  1405     if (formHelperMode == kFormHelperModeDisabled)
  1406       return;
  1408     let focused = this.getFocusedInput(aBrowser);
  1410     if (focused) {
  1411       let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom");
  1412       if (formHelperMode == kFormHelperModeDynamic && this.isTablet)
  1413         shouldZoom = false;
  1414       // ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen
  1415       ZoomHelper.zoomToElement(focused, -1, false,
  1416           aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified);
  1418   },
  1420   observe: function(aSubject, aTopic, aData) {
  1421     let browser = this.selectedBrowser;
  1423     switch (aTopic) {
  1425       case "Session:Back":
  1426         browser.goBack();
  1427         break;
  1429       case "Session:Forward":
  1430         browser.goForward();
  1431         break;
  1433       case "Session:Reload": {
  1434         let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
  1436         // Check to see if this is a message to enable/disable mixed content blocking.
  1437         if (aData) {
  1438           let allowMixedContent = JSON.parse(aData).allowMixedContent;
  1439           if (allowMixedContent) {
  1440             // Set a flag to disable mixed content blocking.
  1441             flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
  1442           } else {
  1443             // Set mixedContentChannel to null to re-enable mixed content blocking.
  1444             let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell);
  1445             docShell.mixedContentChannel = null;
  1449         // Try to use the session history to reload so that framesets are
  1450         // handled properly. If the window has no session history, fall back
  1451         // to using the web navigation's reload method.
  1452         let webNav = browser.webNavigation;
  1453         try {
  1454           let sh = webNav.sessionHistory;
  1455           if (sh)
  1456             webNav = sh.QueryInterface(Ci.nsIWebNavigation);
  1457         } catch (e) {}
  1458         webNav.reload(flags);
  1459         break;
  1462       case "Session:Stop":
  1463         browser.stop();
  1464         break;
  1466       case "Session:ShowHistory": {
  1467         let data = JSON.parse(aData);
  1468         this.showHistory(data.fromIndex, data.toIndex, data.selIndex);
  1469         break;
  1472       case "Tab:Load": {
  1473         let data = JSON.parse(aData);
  1475         // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from
  1476         // inheriting the currently loaded document's principal.
  1477         let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
  1478                     Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
  1479         if (data.userEntered) {
  1480           flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER;
  1483         let delayLoad = ("delayLoad" in data) ? data.delayLoad : false;
  1484         let params = {
  1485           selected: ("selected" in data) ? data.selected : !delayLoad,
  1486           parentId: ("parentId" in data) ? data.parentId : -1,
  1487           flags: flags,
  1488           tabID: data.tabID,
  1489           isPrivate: (data.isPrivate === true),
  1490           pinned: (data.pinned === true),
  1491           delayLoad: (delayLoad === true),
  1492           desktopMode: (data.desktopMode === true)
  1493         };
  1495         let url = data.url;
  1496         if (data.engine) {
  1497           let engine = Services.search.getEngineByName(data.engine);
  1498           if (engine) {
  1499             params.userSearch = url;
  1500             let submission = engine.getSubmission(url);
  1501             url = submission.uri.spec;
  1502             params.postData = submission.postData;
  1506         if (data.newTab) {
  1507           this.addTab(url, params);
  1508         } else {
  1509           if (data.tabId) {
  1510             // Use a specific browser instead of the selected browser, if it exists
  1511             let specificBrowser = this.getTabForId(data.tabId).browser;
  1512             if (specificBrowser)
  1513               browser = specificBrowser;
  1515           this.loadURI(url, browser, params);
  1517         break;
  1520       case "Tab:Selected":
  1521         this._handleTabSelected(this.getTabForId(parseInt(aData)));
  1522         break;
  1524       case "Tab:Closed":
  1525         this._handleTabClosed(this.getTabForId(parseInt(aData)));
  1526         break;
  1528       case "keyword-search":
  1529         // This event refers to a search via the URL bar, not a bookmarks
  1530         // keyword search. Note that this code assumes that the user can only
  1531         // perform a keyword search on the selected tab.
  1532         this.selectedTab.userSearch = aData;
  1534         let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
  1535         sendMessageToJava({
  1536           type: "Search:Keyword",
  1537           identifier: engine.identifier,
  1538           name: engine.name,
  1539         });
  1540         break;
  1542       case "Browser:Quit":
  1543         this.quit();
  1544         break;
  1546       case "SaveAs:PDF":
  1547         this.saveAsPDF(browser);
  1548         break;
  1550       case "Preferences:Set":
  1551         this.setPreferences(aData);
  1552         break;
  1554       case "ScrollTo:FocusedInput":
  1555         // these messages come from a change in the viewable area and not user interaction
  1556         // we allow scrolling to the selected input, but not zooming the page
  1557         this.scrollToFocusedInput(browser, false);
  1558         break;
  1560       case "Sanitize:ClearData":
  1561         this.sanitize(aData);
  1562         break;
  1564       case "FullScreen:Exit":
  1565         browser.contentDocument.mozCancelFullScreen();
  1566         break;
  1568       case "Viewport:Change":
  1569         if (this.isBrowserContentDocumentDisplayed())
  1570           this.selectedTab.setViewport(JSON.parse(aData));
  1571         break;
  1573       case "Viewport:Flush":
  1574         this.contentDocumentChanged();
  1575         break;
  1577       case "Passwords:Init": {
  1578         let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"].
  1579                       getService(Ci.nsILoginManagerStorage);
  1580         storage.init();
  1581         Services.obs.removeObserver(this, "Passwords:Init");
  1582         break;
  1585       case "FormHistory:Init": {
  1586         // Force creation/upgrade of formhistory.sqlite
  1587         FormHistory.count({});
  1588         Services.obs.removeObserver(this, "FormHistory:Init");
  1589         break;
  1592       case "sessionstore-state-purge-complete":
  1593         sendMessageToJava({ type: "Session:StatePurged" });
  1594         break;
  1596       case "gather-telemetry":
  1597         sendMessageToJava({ type: "Telemetry:Gather" });
  1598         break;
  1600       case "Viewport:FixedMarginsChanged":
  1601         gViewportMargins = JSON.parse(aData);
  1602         this.selectedTab.updateViewportSize(gScreenWidth);
  1603         break;
  1605       case "nsPref:changed":
  1606         this.notifyPrefObservers(aData);
  1607         break;
  1609 #ifdef MOZ_ANDROID_SYNTHAPKS
  1610       case "webapps-runtime-install":
  1611         WebappManager.install(JSON.parse(aData), aSubject);
  1612         break;
  1614       case "webapps-runtime-install-package":
  1615         WebappManager.installPackage(JSON.parse(aData), aSubject);
  1616         break;
  1618       case "webapps-ask-install":
  1619         WebappManager.askInstall(JSON.parse(aData));
  1620         break;
  1622       case "webapps-launch": {
  1623         WebappManager.launch(JSON.parse(aData));
  1624         break;
  1627       case "webapps-uninstall": {
  1628         WebappManager.uninstall(JSON.parse(aData));
  1629         break;
  1632       case "Webapps:AutoInstall":
  1633         WebappManager.autoInstall(JSON.parse(aData));
  1634         break;
  1636       case "Webapps:Load":
  1637         this._loadWebapp(JSON.parse(aData));
  1638         break;
  1640       case "Webapps:AutoUninstall":
  1641         WebappManager.autoUninstall(JSON.parse(aData));
  1642         break;
  1643 #endif
  1645       case "Locale:Changed":
  1646         // The value provided to Locale:Changed should be a BCP47 language tag
  1647         // understood by Gecko -- for example, "es-ES" or "de".
  1648         console.log("Locale:Changed: " + aData);
  1650         // TODO: do we need to be more nuanced here -- e.g., checking for the
  1651         // OS locale -- or should it always be false on Fennec?
  1652         Services.prefs.setBoolPref("intl.locale.matchOS", false);
  1653         Services.prefs.setCharPref("general.useragent.locale", aData);
  1654         break;
  1656       default:
  1657         dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
  1658         break;
  1661   },
  1663   get defaultBrowserWidth() {
  1664     delete this.defaultBrowserWidth;
  1665     let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");
  1666     return this.defaultBrowserWidth = width;
  1667   },
  1669   // nsIAndroidBrowserApp
  1670   getBrowserTab: function(tabId) {
  1671     return this.getTabForId(tabId);
  1672   },
  1674   getUITelemetryObserver: function() {
  1675     return UITelemetry;
  1676   },
  1678   getPreferences: function getPreferences(requestId, prefNames, count) {
  1679     this.handlePreferencesRequest(requestId, prefNames, false);
  1680   },
  1682   observePreferences: function observePreferences(requestId, prefNames, count) {
  1683     this.handlePreferencesRequest(requestId, prefNames, true);
  1684   },
  1686   removePreferenceObservers: function removePreferenceObservers(aRequestId) {
  1687     let newPrefObservers = [];
  1688     for (let prefName in this._prefObservers) {
  1689       let requestIds = this._prefObservers[prefName];
  1690       // Remove the requestID from the preference handlers
  1691       let i = requestIds.indexOf(aRequestId);
  1692       if (i >= 0) {
  1693         requestIds.splice(i, 1);
  1696       // If there are no more request IDs, remove the observer
  1697       if (requestIds.length == 0) {
  1698         Services.prefs.removeObserver(prefName, this);
  1699       } else {
  1700         newPrefObservers[prefName] = requestIds;
  1703     this._prefObservers = newPrefObservers;
  1704   },
  1706   // This method will print a list from fromIndex to toIndex, optionally
  1707   // selecting selIndex(if fromIndex<=selIndex<=toIndex)
  1708   showHistory: function(fromIndex, toIndex, selIndex) {
  1709     let browser = this.selectedBrowser;
  1710     let hist = browser.sessionHistory;
  1711     let listitems = [];
  1712     for (let i = toIndex; i >= fromIndex; i--) {
  1713       let entry = hist.getEntryAtIndex(i, false);
  1714       let item = {
  1715         label: entry.title || entry.URI.spec,
  1716         selected: (i == selIndex)
  1717       };
  1718       listitems.push(item);
  1721     let p = new Prompt({
  1722       window: browser.contentWindow
  1723     }).setSingleChoiceItems(listitems).show(function(data) {
  1724         let selected = data.button;
  1725         if (selected == -1)
  1726           return;
  1728         browser.gotoIndex(toIndex-selected);
  1729     });
  1730   },
  1731 };
  1733 var NativeWindow = {
  1734   init: function() {
  1735     Services.obs.addObserver(this, "Menu:Clicked", false);
  1736     Services.obs.addObserver(this, "PageActions:Clicked", false);
  1737     Services.obs.addObserver(this, "PageActions:LongClicked", false);
  1738     Services.obs.addObserver(this, "Doorhanger:Reply", false);
  1739     Services.obs.addObserver(this, "Toast:Click", false);
  1740     Services.obs.addObserver(this, "Toast:Hidden", false);
  1741     this.contextmenus.init();
  1742   },
  1744   uninit: function() {
  1745     Services.obs.removeObserver(this, "Menu:Clicked");
  1746     Services.obs.removeObserver(this, "PageActions:Clicked");
  1747     Services.obs.removeObserver(this, "PageActions:LongClicked");
  1748     Services.obs.removeObserver(this, "Doorhanger:Reply");
  1749     Services.obs.removeObserver(this, "Toast:Click", false);
  1750     Services.obs.removeObserver(this, "Toast:Hidden", false);
  1751     this.contextmenus.uninit();
  1752   },
  1754   loadDex: function(zipFile, implClass) {
  1755     sendMessageToJava({
  1756       type: "Dex:Load",
  1757       zipfile: zipFile,
  1758       impl: implClass || "Main"
  1759     });
  1760   },
  1762   unloadDex: function(zipFile) {
  1763     sendMessageToJava({
  1764       type: "Dex:Unload",
  1765       zipfile: zipFile
  1766     });
  1767   },
  1769   toast: {
  1770     _callbacks: {},
  1771     show: function(aMessage, aDuration, aOptions) {
  1772       let msg = {
  1773         type: "Toast:Show",
  1774         message: aMessage,
  1775         duration: aDuration
  1776       };
  1778       if (aOptions && aOptions.button) {
  1779         msg.button = {
  1780           label: aOptions.button.label,
  1781           id: uuidgen.generateUUID().toString(),
  1782           // If the caller specified a button, make sure we convert any chrome urls
  1783           // to jar:jar urls so that the frontend can show them
  1784           icon: aOptions.button.icon ? resolveGeckoURI(aOptions.button.icon) : null,
  1785         };
  1786         this._callbacks[msg.button.id] = aOptions.button.callback;
  1789       sendMessageToJava(msg);
  1791   },
  1793   pageactions: {
  1794     _items: { },
  1795     add: function(aOptions) {
  1796       let id = uuidgen.generateUUID().toString();
  1797       sendMessageToJava({
  1798         type: "PageActions:Add",
  1799         id: id,
  1800         title: aOptions.title,
  1801         icon: resolveGeckoURI(aOptions.icon),
  1802         important: "important" in aOptions ? aOptions.important : false
  1803       });
  1804       this._items[id] = {
  1805         clickCallback: aOptions.clickCallback,
  1806         longClickCallback: aOptions.longClickCallback
  1807       };
  1808       return id;
  1809     },
  1810     remove: function(id) {
  1811       sendMessageToJava({
  1812         type: "PageActions:Remove",
  1813         id: id
  1814       });
  1815       delete this._items[id];
  1817   },
  1819   menu: {
  1820     _callbacks: [],
  1821     _menuId: 1,
  1822     toolsMenuID: -1,
  1823     add: function() {
  1824       let options;
  1825       if (arguments.length == 1) {
  1826         options = arguments[0];
  1827       } else if (arguments.length == 3) {
  1828           options = {
  1829             name: arguments[0],
  1830             icon: arguments[1],
  1831             callback: arguments[2]
  1832           };
  1833       } else {
  1834          throw "Incorrect number of parameters";
  1837       options.type = "Menu:Add";
  1838       options.id = this._menuId;
  1840       sendMessageToJava(options);
  1841       this._callbacks[this._menuId] = options.callback;
  1842       this._menuId++;
  1843       return this._menuId - 1;
  1844     },
  1846     remove: function(aId) {
  1847       sendMessageToJava({ type: "Menu:Remove", id: aId });
  1848     },
  1850     update: function(aId, aOptions) {
  1851       if (!aOptions)
  1852         return;
  1854       sendMessageToJava({
  1855         type: "Menu:Update", 
  1856         id: aId,
  1857         options: aOptions
  1858       });
  1860   },
  1862   doorhanger: {
  1863     _callbacks: {},
  1864     _callbacksId: 0,
  1865     _promptId: 0,
  1867   /**
  1868    * @param aOptions
  1869    *        An options JavaScript object holding additional properties for the
  1870    *        notification. The following properties are currently supported:
  1871    *        persistence: An integer. The notification will not automatically
  1872    *                     dismiss for this many page loads. If persistence is set
  1873    *                     to -1, the doorhanger will never automatically dismiss.
  1874    *        persistWhileVisible:
  1875    *                     A boolean. If true, a visible notification will always
  1876    *                     persist across location changes.
  1877    *        timeout:     A time in milliseconds. The notification will not
  1878    *                     automatically dismiss before this time.
  1879    *        checkbox:    A string to appear next to a checkbox under the notification
  1880    *                     message. The button callback functions will be called with
  1881    *                     the checked state as an argument.                   
  1882    */
  1883     show: function(aMessage, aValue, aButtons, aTabID, aOptions) {
  1884       if (aButtons == null) {
  1885         aButtons = [];
  1888       aButtons.forEach((function(aButton) {
  1889         this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId };
  1890         aButton.callback = this._callbacksId;
  1891         this._callbacksId++;
  1892       }).bind(this));
  1894       this._promptId++;
  1895       let json = {
  1896         type: "Doorhanger:Add",
  1897         message: aMessage,
  1898         value: aValue,
  1899         buttons: aButtons,
  1900         // use the current tab if none is provided
  1901         tabID: aTabID || BrowserApp.selectedTab.id,
  1902         options: aOptions || {}
  1903       };
  1904       sendMessageToJava(json);
  1905     },
  1907     hide: function(aValue, aTabID) {
  1908       sendMessageToJava({
  1909         type: "Doorhanger:Remove",
  1910         value: aValue,
  1911         tabID: aTabID
  1912       });
  1914   },
  1916   observe: function(aSubject, aTopic, aData) {
  1917     if (aTopic == "Menu:Clicked") {
  1918       if (this.menu._callbacks[aData])
  1919         this.menu._callbacks[aData]();
  1920     } else if (aTopic == "PageActions:Clicked") {
  1921         if (this.pageactions._items[aData].clickCallback)
  1922           this.pageactions._items[aData].clickCallback();
  1923     } else if (aTopic == "PageActions:LongClicked") {
  1924         if (this.pageactions._items[aData].longClickCallback)
  1925           this.pageactions._items[aData].longClickCallback();
  1926     } else if (aTopic == "Toast:Click") {
  1927       if (this.toast._callbacks[aData]) {
  1928         this.toast._callbacks[aData]();
  1929         delete this.toast._callbacks[aData];
  1931     } else if (aTopic == "Toast:Hidden") {
  1932       if (this.toast._callbacks[aData])
  1933         delete this.toast._callbacks[aData];
  1934     } else if (aTopic == "Doorhanger:Reply") {
  1935       let data = JSON.parse(aData);
  1936       let reply_id = data["callback"];
  1938       if (this.doorhanger._callbacks[reply_id]) {
  1939         // Pass the value of the optional checkbox to the callback
  1940         let checked = data["checked"];
  1941         this.doorhanger._callbacks[reply_id].cb(checked, data.inputs);
  1943         let prompt = this.doorhanger._callbacks[reply_id].prompt;
  1944         for (let id in this.doorhanger._callbacks) {
  1945           if (this.doorhanger._callbacks[id].prompt == prompt) {
  1946             delete this.doorhanger._callbacks[id];
  1951   },
  1952   contextmenus: {
  1953     items: {}, //  a list of context menu items that we may show
  1954     DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items
  1956     init: function() {
  1957       Services.obs.addObserver(this, "Gesture:LongPress", false);
  1958     },
  1960     uninit: function() {
  1961       Services.obs.removeObserver(this, "Gesture:LongPress");
  1962     },
  1964     add: function() {
  1965       let args;
  1966       if (arguments.length == 1) {
  1967         args = arguments[0];
  1968       } else if (arguments.length == 3) {
  1969         args = {
  1970           label : arguments[0],
  1971           selector: arguments[1],
  1972           callback: arguments[2]
  1973         };
  1974       } else {
  1975         throw "Incorrect number of parameters";
  1978       if (!args.label)
  1979         throw "Menu items must have a name";
  1981       let cmItem = new ContextMenuItem(args);
  1982       this.items[cmItem.id] = cmItem;
  1983       return cmItem.id;
  1984     },
  1986     remove: function(aId) {
  1987       delete this.items[aId];
  1988     },
  1990     SelectorContext: function(aSelector) {
  1991       return {
  1992         matches: function(aElt) {
  1993           if (aElt.mozMatchesSelector)
  1994             return aElt.mozMatchesSelector(aSelector);
  1995           return false;
  1997       };
  1998     },
  2000     linkOpenableNonPrivateContext: {
  2001       matches: function linkOpenableNonPrivateContextMatches(aElement) {
  2002         let doc = aElement.ownerDocument;
  2003         if (!doc || PrivateBrowsingUtils.isWindowPrivate(doc.defaultView)) {
  2004           return false;
  2007         return NativeWindow.contextmenus.linkOpenableContext.matches(aElement);
  2009     },
  2011     linkOpenableContext: {
  2012       matches: function linkOpenableContextMatches(aElement) {
  2013         let uri = NativeWindow.contextmenus._getLink(aElement);
  2014         if (uri) {
  2015           let scheme = uri.scheme;
  2016           let dontOpen = /^(javascript|mailto|news|snews|tel)$/;
  2017           return (scheme && !dontOpen.test(scheme));
  2019         return false;
  2021     },
  2023     linkCopyableContext: {
  2024       matches: function linkCopyableContextMatches(aElement) {
  2025         let uri = NativeWindow.contextmenus._getLink(aElement);
  2026         if (uri) {
  2027           let scheme = uri.scheme;
  2028           let dontCopy = /^(mailto|tel)$/;
  2029           return (scheme && !dontCopy.test(scheme));
  2031         return false;
  2033     },
  2035     linkShareableContext: {
  2036       matches: function linkShareableContextMatches(aElement) {
  2037         let uri = NativeWindow.contextmenus._getLink(aElement);
  2038         if (uri) {
  2039           let scheme = uri.scheme;
  2040           let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/;
  2041           return (scheme && !dontShare.test(scheme));
  2043         return false;
  2045     },
  2047     linkBookmarkableContext: {
  2048       matches: function linkBookmarkableContextMatches(aElement) {
  2049         let uri = NativeWindow.contextmenus._getLink(aElement);
  2050         if (uri) {
  2051           let scheme = uri.scheme;
  2052           let dontBookmark = /^(mailto|tel)$/;
  2053           return (scheme && !dontBookmark.test(scheme));
  2055         return false;
  2057     },
  2059     emailLinkContext: {
  2060       matches: function emailLinkContextMatches(aElement) {
  2061         let uri = NativeWindow.contextmenus._getLink(aElement);
  2062         if (uri)
  2063           return uri.schemeIs("mailto");
  2064         return false;
  2066     },
  2068     phoneNumberLinkContext: {
  2069       matches: function phoneNumberLinkContextMatches(aElement) {
  2070         let uri = NativeWindow.contextmenus._getLink(aElement);
  2071         if (uri)
  2072           return uri.schemeIs("tel");
  2073         return false;
  2075     },
  2077     imageLocationCopyableContext: {
  2078       matches: function imageLinkCopyableContextMatches(aElement) {
  2079         return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI);
  2081     },
  2083     imageSaveableContext: {
  2084       matches: function imageSaveableContextMatches(aElement) {
  2085         if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) {
  2086           // The image must be loaded to allow saving
  2087           let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
  2088           return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE));
  2090         return false;
  2092     },
  2094     mediaSaveableContext: {
  2095       matches: function mediaSaveableContextMatches(aElement) {
  2096         return (aElement instanceof HTMLVideoElement ||
  2097                aElement instanceof HTMLAudioElement);
  2099     },
  2101     mediaContext: function(aMode) {
  2102       return {
  2103         matches: function(aElt) {
  2104           if (aElt instanceof Ci.nsIDOMHTMLMediaElement) {
  2105             let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE;
  2106             if (hasError)
  2107               return false;
  2109             let paused = aElt.paused || aElt.ended;
  2110             if (paused && aMode == "media-paused")
  2111               return true;
  2112             if (!paused && aMode == "media-playing")
  2113               return true;
  2114             let controls = aElt.controls;
  2115             if (!controls && aMode == "media-hidingcontrols")
  2116               return true;
  2118             let muted = aElt.muted;
  2119             if (muted && aMode == "media-muted")
  2120               return true;
  2121             else if (!muted && aMode == "media-unmuted")
  2122               return true;
  2124           return false;
  2126       };
  2127     },
  2129     /* Holds a WeakRef to the original target element this context menu was shown for.
  2130      * Most API's will have to walk up the tree from this node to find the correct element
  2131      * to act on
  2132      */
  2133     get _target() {
  2134       if (this._targetRef)
  2135         return this._targetRef.get();
  2136       return null;
  2137     },
  2139     set _target(aTarget) {
  2140       if (aTarget)
  2141         this._targetRef = Cu.getWeakReference(aTarget);
  2142       else this._targetRef = null;
  2143     },
  2145     get defaultContext() {
  2146       delete this.defaultContext;
  2147       return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default");
  2148     },
  2150     /* Gets menuitems for an arbitrary node
  2151      * Parameters:
  2152      *   element - The element to look at. If this element has a contextmenu attribute, the
  2153      *             corresponding contextmenu will be used.
  2154      */
  2155     _getHTMLContextMenuItemsForElement: function(element) {
  2156       let htmlMenu = element.contextMenu;
  2157       if (!htmlMenu) {
  2158         return [];
  2161       htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
  2162       htmlMenu.sendShowEvent();
  2164       return this._getHTMLContextMenuItemsForMenu(htmlMenu, element);
  2165     },
  2167     /* Add a menuitem for an HTML <menu> node
  2168      * Parameters:
  2169      *   menu - The <menu> element to iterate through for menuitems
  2170      *   target - The target element these context menu items are attached to
  2171      */
  2172     _getHTMLContextMenuItemsForMenu: function(menu, target) {
  2173       let items = [];
  2174       for (let i = 0; i < menu.childNodes.length; i++) {
  2175         let elt = menu.childNodes[i];
  2176         if (!elt.label)
  2177           continue;
  2179         items.push(new HTMLContextMenuItem(elt, target));
  2182       return items;
  2183     },
  2185     // Searches the current list of menuitems to show for any that match this id
  2186     _findMenuItem: function(aId) {
  2187       if (!this.menus) {
  2188         return null;
  2191       for (let context in this.menus) {
  2192         let menu = this.menus[context];
  2193         for (let i = 0; i < menu.length; i++) {
  2194           if (menu[i].id === aId) {
  2195             return menu[i];
  2199       return null;
  2200     },
  2202     // Returns true if there are any context menu items to show
  2203     shouldShow: function() {
  2204       for (let context in this.menus) {
  2205         let menu = this.menus[context];
  2206         if (menu.length > 0) {
  2207           return true;
  2210       return false;
  2211     },
  2213     /* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this
  2214      * is an image inside an <a> tag, we may have a "link" context and an "image" one.
  2215      */
  2216     _getContextType: function(element) {
  2217       // For anchor nodes, we try to use the scheme to pick a string
  2218       if (element instanceof Ci.nsIDOMHTMLAnchorElement) {
  2219         let uri = this.makeURI(this._getLinkURL(element));
  2220         try {
  2221           return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme);
  2222         } catch(ex) { }
  2225       // Otherwise we try the nodeName
  2226       try {
  2227         return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase());
  2228       } catch(ex) { }
  2230       // Fallback to the default
  2231       return this.defaultContext;
  2232     },
  2234     // Adds context menu items added through the add-on api
  2235     _getNativeContextMenuItems: function(element, x, y) {
  2236       let res = [];
  2237       for (let itemId of Object.keys(this.items)) {
  2238         let item = this.items[itemId];
  2240         if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
  2241           res.push(item);
  2245       return res;
  2246     },
  2248     /* Checks if there are context menu items to show, and if it finds them
  2249      * sends a contextmenu event to content. We also send showing events to
  2250      * any html5 context menus we are about to show, and fire some local notifications
  2251      * for chrome consumers to do lazy menuitem construction
  2252      */
  2253     _sendToContent: function(x, y) {
  2254       let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(x, y);
  2255       if (!target)
  2256         target = ElementTouchHelper.anyElementFromPoint(x, y);
  2258       if (!target)
  2259         return;
  2261       this._target = target;
  2263       Services.obs.notifyObservers(null, "before-build-contextmenu", "");
  2264       this._buildMenu(x, y);
  2266       // only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap)
  2267       if (this.shouldShow()) {
  2268         let event = target.ownerDocument.createEvent("MouseEvent");
  2269         event.initMouseEvent("contextmenu", true, true, target.defaultView,
  2270                              0, x, y, x, y, false, false, false, false,
  2271                              0, null);
  2272         target.ownerDocument.defaultView.addEventListener("contextmenu", this, false);
  2273         target.dispatchEvent(event);
  2274       } else {
  2275         this.menus = null;
  2276         Services.obs.notifyObservers({target: target, x: x, y: y}, "context-menu-not-shown", "");
  2278         if (SelectionHandler.canSelect(target)) {
  2279           if (!SelectionHandler.startSelection(target, {
  2280             mode: SelectionHandler.SELECT_AT_POINT,
  2281             x: x,
  2282             y: y
  2283           })) { 
  2284             SelectionHandler.attachCaret(target);
  2288     },
  2290     // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
  2291     _getTitle: function(node) {
  2292       if (node.hasAttribute && node.hasAttribute("title")) {
  2293         return node.getAttribute("title");
  2295       return this._getUrl(node);
  2296     },
  2298     // Returns a url associated with a node
  2299     _getUrl: function(node) {
  2300       if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
  2301           (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
  2302         return this._getLinkURL(node);
  2303       } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) {
  2304         return node.currentURI.spec;
  2305       } else if (node instanceof Ci.nsIDOMHTMLMediaElement) {
  2306         return (node.currentSrc || node.src);
  2309       return "";
  2310     },
  2312     // Adds an array of menuitems to the current list of items to show, in the correct context
  2313     _addMenuItems: function(items, context) {
  2314         if (!this.menus[context]) {
  2315           this.menus[context] = [];
  2317         this.menus[context] = this.menus[context].concat(items);
  2318     },
  2320     /* Does the basic work of building a context menu to show. Will combine HTML and Native
  2321      * context menus items, as well as sorting menuitems into different menus based on context.
  2322      */
  2323     _buildMenu: function(x, y) {
  2324       // now walk up the tree and for each node look for any context menu items that apply
  2325       let element = this._target;
  2327       // this.menus holds a hashmap of "contexts" to menuitems associated with that context
  2328       // For instance, if the user taps an image inside a link, we'll have something like:
  2329       // {
  2330       //   link:  [ ContextMenuItem, ContextMenuItem ]
  2331       //   image: [ ContextMenuItem, ContextMenuItem ]
  2332       // }
  2333       this.menus = {};
  2335       while (element) {
  2336         let context = this._getContextType(element);
  2338         // First check for any html5 context menus that might exist...
  2339         var items = this._getHTMLContextMenuItemsForElement(element);
  2340         if (items.length > 0) {
  2341           this._addMenuItems(items, context);
  2344         // then check for any context menu items registered in the ui.
  2345         items = this._getNativeContextMenuItems(element, x, y);
  2346         if (items.length > 0) {
  2347           this._addMenuItems(items, context);
  2350         // walk up the tree and find more items to show
  2351         element = element.parentNode;
  2353     },
  2355     // Actually shows the native context menu by passing a list of context menu items to
  2356     // show to the Java.
  2357     _show: function(aEvent) {
  2358       let popupNode = this._target;
  2359       this._target = null;
  2360       if (aEvent.defaultPrevented || !popupNode) {
  2361         return;
  2363       this._innerShow(popupNode, aEvent.clientX, aEvent.clientY);
  2364     },
  2366     // Walks the DOM tree to find a title from a node
  2367     _findTitle: function(node) {
  2368       let title = "";
  2369       while(node && !title) {
  2370         title = this._getTitle(node);
  2371         node = node.parentNode;
  2373       return title;
  2374     },
  2376     /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm
  2377      * If there is one menu, will return a flat array of menuitems. If there are multiple
  2378      * menus, will return an array with appropriate tabs/items inside it. i.e. :
  2379      * [
  2380      *    { label: "link", items: [...] },
  2381      *    { label: "image", items: [...] }
  2382      * ]
  2383      */
  2384     _reformatList: function(target) {
  2385       let contexts = Object.keys(this.menus);
  2387       if (contexts.length === 1) {
  2388         // If there's only one context, we'll only show a single flat single select list
  2389         return this._reformatMenuItems(target, this.menus[contexts[0]]);
  2392       // If there are multiple contexts, we'll only show a tabbed ui with multiple lists
  2393       return this._reformatListAsTabs(target, this.menus);
  2394     },
  2396     /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's
  2397      * addTabs method. i.e. :
  2398      * { link: [...], image: [...] } becomes
  2399      * [ { label: "link", items: [...] } ]
  2401      * Also reformats items and resolves any parmaeters that aren't known until display time
  2402      * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
  2403      */
  2404     _reformatListAsTabs: function(target, menus) {
  2405       let itemArray = [];
  2407       // Sort the keys so that "link" is always first
  2408       let contexts = Object.keys(this.menus);
  2409       contexts.sort((context1, context2) => {
  2410         if (context1 === this.defaultContext) {
  2411           return -1;
  2412         } else if (context2 === this.defaultContext) {
  2413           return 1;
  2415         return 0;
  2416       });
  2418       contexts.forEach(context => {
  2419         itemArray.push({
  2420           label: context,
  2421           items: this._reformatMenuItems(target, menus[context])
  2422         });
  2423       });
  2425       return itemArray;
  2426     },
  2428     /* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items
  2429      * and resolves any parmaeters that aren't known until display time
  2430      * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
  2431      */
  2432     _reformatMenuItems: function(target, menuitems) {
  2433       let itemArray = [];
  2435       for (let i = 0; i < menuitems.length; i++) {
  2436         let t = target;
  2437         while(t) {
  2438           if (menuitems[i].matches(t)) {
  2439             let val = menuitems[i].getValue(t);
  2441             // hidden menu items will return null from getValue
  2442             if (val) {
  2443               itemArray.push(val);
  2444               break;
  2448           t = t.parentNode;
  2452       return itemArray;
  2453     },
  2455     // Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt.
  2456     _innerShow: function(target, x, y) {
  2457       Haptic.performSimpleAction(Haptic.LongPress);
  2459       // spin through the tree looking for a title for this context menu
  2460       let title = this._findTitle(target);
  2462       for (let context in this.menus) {
  2463         let menu = this.menus[context];
  2464         menu.sort((a,b) => {
  2465           if (a.order === b.order) {
  2466             return 0;
  2468           return (a.order > b.order) ? 1 : -1;
  2469         });
  2472       let useTabs = Object.keys(this.menus).length > 1;
  2473       let prompt = new Prompt({
  2474         window: target.ownerDocument.defaultView,
  2475         title: useTabs ? undefined : title
  2476       });
  2478       let items = this._reformatList(target);
  2479       if (useTabs) {
  2480         prompt.addTabs({
  2481           id: "tabs",
  2482           items: items
  2483         });
  2484       } else {
  2485         prompt.setSingleChoiceItems(items);
  2488       prompt.show(this._promptDone.bind(this, target, x, y, items));
  2489     },
  2491     // Called when the contextmenu prompt is closed
  2492     _promptDone: function(target, x, y, items, data) {
  2493       if (data.button == -1) {
  2494         // Prompt was cancelled, or an ActionView was used.
  2495         return;
  2498       let selectedItemId;
  2499       if (data.tabs) {
  2500         let menu = items[data.tabs.tab];
  2501         selectedItemId = menu.items[data.tabs.item].id;
  2502       } else {
  2503         selectedItemId = items[data.list[0]].id
  2506       let selectedItem = this._findMenuItem(selectedItemId);
  2507       this.menus = null;
  2509       if (!selectedItem || !selectedItem.matches || !selectedItem.callback) {
  2510         return;
  2513       // for menuitems added using the native UI, pass the dom element that matched that item to the callback
  2514       while (target) {
  2515         if (selectedItem.matches(target, x, y)) {
  2516           selectedItem.callback(target, x, y);
  2517           break;
  2519         target = target.parentNode;
  2521     },
  2523     // Called when the contextmenu is done propagating to content. If the event wasn't cancelled, will show a contextmenu.
  2524     handleEvent: function(aEvent) {
  2525       BrowserEventHandler._cancelTapHighlight();
  2526       aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false);
  2527       this._show(aEvent);
  2528     },
  2530     // Called when a long press is observed in the native Java frontend. Will start the process of generating/showing a contextmenu.
  2531     observe: function(aSubject, aTopic, aData) {
  2532       let data = JSON.parse(aData);
  2533       // content gets first crack at cancelling context menus
  2534       this._sendToContent(data.x, data.y);
  2535     },
  2537     // XXX - These are stolen from Util.js, we should remove them if we bring it back
  2538     makeURLAbsolute: function makeURLAbsolute(base, url) {
  2539       // Note:  makeURI() will throw if url is not a valid URI
  2540       return this.makeURI(url, null, this.makeURI(base)).spec;
  2541     },
  2543     makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
  2544       return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
  2545     },
  2547     _getLink: function(aElement) {
  2548       if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
  2549           ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
  2550           (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) ||
  2551           aElement instanceof Ci.nsIDOMHTMLLinkElement ||
  2552           aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) {
  2553         try {
  2554           let url = this._getLinkURL(aElement);
  2555           return Services.io.newURI(url, null, null);
  2556         } catch (e) {}
  2558       return null;
  2559     },
  2561     _disableInGuest: function _disableInGuest(selector) {
  2562       return {
  2563         matches: function _disableInGuestMatches(aElement, aX, aY) {
  2564           if (BrowserApp.isGuest)
  2565             return false;
  2566           return selector.matches(aElement, aX, aY);
  2568       };
  2569     },
  2571     _getLinkURL: function ch_getLinkURL(aLink) {
  2572       let href = aLink.href;
  2573       if (href)
  2574         return href;
  2576       href = aLink.getAttributeNS(kXLinkNamespace, "href");
  2577       if (!href || !href.match(/\S/)) {
  2578         // Without this we try to save as the current doc,
  2579         // for example, HTML case also throws if empty
  2580         throw "Empty href";
  2583       return this.makeURLAbsolute(aLink.baseURI, href);
  2584     },
  2586     _copyStringToDefaultClipboard: function(aString) {
  2587       let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
  2588       clipboard.copyString(aString);
  2589     },
  2591     _shareStringWithDefault: function(aSharedString, aTitle) {
  2592       let sharing = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService);
  2593       sharing.shareWithDefault(aSharedString, "text/plain", aTitle);
  2594     },
  2596     _stripScheme: function(aString) {
  2597       let index = aString.indexOf(":");
  2598       return aString.slice(index + 1);
  2601 };
  2603 var LightWeightThemeWebInstaller = {
  2604   init: function sh_init() {
  2605     let temp = {};
  2606     Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp);
  2607     let theme = new temp.LightweightThemeConsumer(document);
  2608     BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
  2609     BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
  2610     BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
  2611   },
  2613   uninit: function() {
  2614     BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
  2615     BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
  2616     BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
  2617   },
  2619   handleEvent: function (event) {
  2620     switch (event.type) {
  2621       case "InstallBrowserTheme":
  2622       case "PreviewBrowserTheme":
  2623       case "ResetBrowserThemePreview":
  2624         // ignore requests from background tabs
  2625         if (event.target.ownerDocument.defaultView.top != content)
  2626           return;
  2629     switch (event.type) {
  2630       case "InstallBrowserTheme":
  2631         this._installRequest(event);
  2632         break;
  2633       case "PreviewBrowserTheme":
  2634         this._preview(event);
  2635         break;
  2636       case "ResetBrowserThemePreview":
  2637         this._resetPreview(event);
  2638         break;
  2639       case "pagehide":
  2640       case "TabSelect":
  2641         this._resetPreview();
  2642         break;
  2644   },
  2646   get _manager () {
  2647     let temp = {};
  2648     Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
  2649     delete this._manager;
  2650     return this._manager = temp.LightweightThemeManager;
  2651   },
  2653   _installRequest: function (event) {
  2654     let node = event.target;
  2655     let data = this._getThemeFromNode(node);
  2656     if (!data)
  2657       return;
  2659     if (this._isAllowed(node)) {
  2660       this._install(data);
  2661       return;
  2664     let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton");
  2665     let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1);
  2666     let buttons = [{
  2667       label: allowButtonText,
  2668       callback: function () {
  2669         LightWeightThemeWebInstaller._install(data);
  2671     }];
  2673     NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id);
  2674   },
  2676   _install: function (newLWTheme) {
  2677     this._manager.currentTheme = newLWTheme;
  2678   },
  2680   _previewWindow: null,
  2681   _preview: function (event) {
  2682     if (!this._isAllowed(event.target))
  2683       return;
  2684     let data = this._getThemeFromNode(event.target);
  2685     if (!data)
  2686       return;
  2687     this._resetPreview();
  2689     this._previewWindow = event.target.ownerDocument.defaultView;
  2690     this._previewWindow.addEventListener("pagehide", this, true);
  2691     BrowserApp.deck.addEventListener("TabSelect", this, false);
  2692     this._manager.previewTheme(data);
  2693   },
  2695   _resetPreview: function (event) {
  2696     if (!this._previewWindow ||
  2697         event && !this._isAllowed(event.target))
  2698       return;
  2700     this._previewWindow.removeEventListener("pagehide", this, true);
  2701     this._previewWindow = null;
  2702     BrowserApp.deck.removeEventListener("TabSelect", this, false);
  2704     this._manager.resetPreview();
  2705   },
  2707   _isAllowed: function (node) {
  2708     let pm = Services.perms;
  2710     let uri = node.ownerDocument.documentURIObject;
  2711     return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
  2712   },
  2714   _getThemeFromNode: function (node) {
  2715     return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI);
  2717 };
  2719 var DesktopUserAgent = {
  2720   DESKTOP_UA: null,
  2722   init: function ua_init() {
  2723     Services.obs.addObserver(this, "DesktopMode:Change", false);
  2724     UserAgentOverrides.addComplexOverride(this.onRequest.bind(this));
  2726     // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference
  2727     this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
  2728                         .getService(Ci.nsIHttpProtocolHandler).userAgent
  2729                         .replace(/Android; [a-zA-Z]+/, "X11; Linux x86_64")
  2730                         .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
  2731   },
  2733   uninit: function ua_uninit() {
  2734     Services.obs.removeObserver(this, "DesktopMode:Change");
  2735   },
  2737   onRequest: function(channel, defaultUA) {
  2738     let channelWindow = this._getWindowForRequest(channel);
  2739     let tab = BrowserApp.getTabForWindow(channelWindow);
  2740     if (tab == null)
  2741       return null;
  2743     return this.getUserAgentForTab(tab);
  2744   },
  2746   getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) {
  2747     let tab = BrowserApp.getTabForWindow(aWindow.top);
  2748     if (tab)
  2749       return this.getUserAgentForTab(tab);
  2751     return null;
  2752   },
  2754   getUserAgentForTab: function ua_getUserAgentForTab(aTab) {
  2755     // Send desktop UA if "Request Desktop Site" is enabled.
  2756     if (aTab.desktopMode)
  2757       return this.DESKTOP_UA;
  2759     return null;
  2760   },
  2762   _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) {
  2763     if (aRequest && aRequest.notificationCallbacks) {
  2764       try {
  2765         return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
  2766       } catch (ex) { }
  2769     if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) {
  2770       try {
  2771         return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
  2772       } catch (ex) { }
  2775     return null;
  2776   },
  2778   _getWindowForRequest: function ua_getWindowForRequest(aRequest) {
  2779     let loadContext = this._getRequestLoadContext(aRequest);
  2780     if (loadContext) {
  2781       try {
  2782         return loadContext.associatedWindow;
  2783       } catch (e) {
  2784         // loadContext.associatedWindow can throw when there's no window
  2787     return null;
  2788   },
  2790   observe: function ua_observe(aSubject, aTopic, aData) {
  2791     if (aTopic === "DesktopMode:Change") {
  2792       let args = JSON.parse(aData);
  2793       let tab = BrowserApp.getTabForId(args.tabId);
  2794       if (tab != null)
  2795         tab.reloadWithMode(args.desktopMode);
  2798 };
  2801 function nsBrowserAccess() {
  2804 nsBrowserAccess.prototype = {
  2805   QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]),
  2807   _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) {
  2808     let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
  2809     if (isExternal && aURI && aURI.schemeIs("chrome"))
  2810       return null;
  2812     let loadflags = isExternal ?
  2813                       Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
  2814                       Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
  2815     if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
  2816       switch (aContext) {
  2817         case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL:
  2818           aWhere = Services.prefs.getIntPref("browser.link.open_external");
  2819           break;
  2820         default: // OPEN_NEW or an illegal value
  2821           aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
  2825     Services.io.offline = false;
  2827     let referrer;
  2828     if (aOpener) {
  2829       try {
  2830         let location = aOpener.location;
  2831         referrer = Services.io.newURI(location, null, null);
  2832       } catch(e) { }
  2835     let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
  2836     let pinned = false;
  2838     if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
  2839       pinned = true;
  2840       let spec = aURI.spec;
  2841       let tabs = BrowserApp.tabs;
  2842       for (let i = 0; i < tabs.length; i++) {
  2843         let appOrigin = ss.getTabValue(tabs[i], "appOrigin");
  2844         if (appOrigin == spec) {
  2845           let tab = tabs[i];
  2846           BrowserApp.selectTab(tab);
  2847           return tab.browser;
  2852     let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
  2853                   aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
  2854                   aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB);
  2855     let isPrivate = false;
  2857     if (newTab) {
  2858       let parentId = -1;
  2859       if (!isExternal && aOpener) {
  2860         let parent = BrowserApp.getTabForWindow(aOpener.top);
  2861         if (parent) {
  2862           parentId = parent.id;
  2863           isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
  2867       // BrowserApp.addTab calls loadURIWithFlags with the appropriate params
  2868       let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags,
  2869                                                                       referrerURI: referrer,
  2870                                                                       external: isExternal,
  2871                                                                       parentId: parentId,
  2872                                                                       selected: true,
  2873                                                                       isPrivate: isPrivate,
  2874                                                                       pinned: pinned });
  2876       return tab.browser;
  2879     // OPEN_CURRENTWINDOW and illegal values
  2880     let browser = BrowserApp.selectedBrowser;
  2881     if (aURI && browser)
  2882       browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
  2884     return browser;
  2885   },
  2887   openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) {
  2888     let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
  2889     return browser ? browser.contentWindow : null;
  2890   },
  2892   openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) {
  2893     let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
  2894     return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
  2895   },
  2897   isTabContentWindow: function(aWindow) {
  2898     return BrowserApp.getBrowserForWindow(aWindow) != null;
  2899   },
  2901   get contentWindow() {
  2902     return BrowserApp.selectedBrowser.contentWindow;
  2904 };
  2907 // track the last known screen size so that new tabs
  2908 // get created with the right size rather than being 1x1
  2909 let gScreenWidth = 1;
  2910 let gScreenHeight = 1;
  2911 let gReflowPending = null;
  2913 // The margins that should be applied to the viewport for fixed position
  2914 // children. This is used to avoid browser chrome permanently obscuring
  2915 // fixed position content, and also to make sure window-sized pages take
  2916 // into account said browser chrome.
  2917 let gViewportMargins = { top: 0, right: 0, bottom: 0, left: 0};
  2919 function Tab(aURL, aParams) {
  2920   this.browser = null;
  2921   this.id = 0;
  2922   this.lastTouchedAt = Date.now();
  2923   this._zoom = 1.0;
  2924   this._drawZoom = 1.0;
  2925   this._restoreZoom = false;
  2926   this._fixedMarginLeft = 0;
  2927   this._fixedMarginTop = 0;
  2928   this._fixedMarginRight = 0;
  2929   this._fixedMarginBottom = 0;
  2930   this._readerEnabled = false;
  2931   this._readerActive = false;
  2932   this.userScrollPos = { x: 0, y: 0 };
  2933   this.viewportExcludesHorizontalMargins = true;
  2934   this.viewportExcludesVerticalMargins = true;
  2935   this.viewportMeasureCallback = null;
  2936   this.lastPageSizeAfterViewportRemeasure = { width: 0, height: 0 };
  2937   this.contentDocumentIsDisplayed = true;
  2938   this.pluginDoorhangerTimeout = null;
  2939   this.shouldShowPluginDoorhanger = true;
  2940   this.clickToPlayPluginsActivated = false;
  2941   this.desktopMode = false;
  2942   this.originalURI = null;
  2943   this.savedArticle = null;
  2944   this.hasTouchListener = false;
  2945   this.browserWidth = 0;
  2946   this.browserHeight = 0;
  2948   this.create(aURL, aParams);
  2951 Tab.prototype = {
  2952   create: function(aURL, aParams) {
  2953     if (this.browser)
  2954       return;
  2956     aParams = aParams || {};
  2958     this.browser = document.createElement("browser");
  2959     this.browser.setAttribute("type", "content-targetable");
  2960     this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
  2962     // Make sure the previously selected panel remains selected. The selected panel of a deck is
  2963     // not stable when panels are added.
  2964     let selectedPanel = BrowserApp.deck.selectedPanel;
  2965     BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null);
  2966     BrowserApp.deck.selectedPanel = selectedPanel;
  2968     if (BrowserApp.manifestUrl) {
  2969       let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
  2970       let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl);
  2971       if (manifest) {
  2972         let app = manifest.QueryInterface(Ci.mozIApplication);
  2973         this.browser.docShell.setIsApp(app.localId);
  2977     // Must be called after appendChild so the docshell has been created.
  2978     this.setActive(false);
  2980     let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate;
  2981     if (isPrivate) {
  2982       this.browser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = true;
  2985     this.browser.stop();
  2987     let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
  2988     frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL;
  2990     // only set tab uri if uri is valid
  2991     let uri = null;
  2992     let title = aParams.title || aURL;
  2993     try {
  2994       uri = Services.io.newURI(aURL, null, null).spec;
  2995     } catch (e) {}
  2997     // When the tab is stubbed from Java, there's a window between the stub
  2998     // creation and the tab creation in Gecko where the stub could be removed
  2999     // or the selected tab can change (which is easiest to hit during startup).
  3000     // To prevent these races, we need to differentiate between tab stubs from
  3001     // Java and new tabs from Gecko.
  3002     let stub = false;
  3004     if (!aParams.zombifying) {
  3005       if ("tabID" in aParams) {
  3006         this.id = aParams.tabID;
  3007         stub = true;
  3008       } else {
  3009         let jni = new JNI();
  3010         let cls = jni.findClass("org/mozilla/gecko/Tabs");
  3011         let method = jni.getStaticMethodID(cls, "getNextTabId", "()I");
  3012         this.id = jni.callStaticIntMethod(cls, method);
  3013         jni.close();
  3016       this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false;
  3018       let message = {
  3019         type: "Tab:Added",
  3020         tabID: this.id,
  3021         uri: uri,
  3022         parentId: ("parentId" in aParams) ? aParams.parentId : -1,
  3023         external: ("external" in aParams) ? aParams.external : false,
  3024         selected: ("selected" in aParams) ? aParams.selected : true,
  3025         title: title,
  3026         delayLoad: aParams.delayLoad || false,
  3027         desktopMode: this.desktopMode,
  3028         isPrivate: isPrivate,
  3029         stub: stub
  3030       };
  3031       sendMessageToJava(message);
  3033       this.overscrollController = new OverscrollController(this);
  3036     this.browser.contentWindow.controllers.insertControllerAt(0, this.overscrollController);
  3038     let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
  3039                 Ci.nsIWebProgress.NOTIFY_LOCATION |
  3040                 Ci.nsIWebProgress.NOTIFY_SECURITY;
  3041     this.browser.addProgressListener(this, flags);
  3042     this.browser.sessionHistory.addSHistoryListener(this);
  3044     this.browser.addEventListener("DOMContentLoaded", this, true);
  3045     this.browser.addEventListener("DOMFormHasPassword", this, true);
  3046     this.browser.addEventListener("DOMLinkAdded", this, true);
  3047     this.browser.addEventListener("DOMTitleChanged", this, true);
  3048     this.browser.addEventListener("DOMWindowClose", this, true);
  3049     this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
  3050     this.browser.addEventListener("DOMAutoComplete", this, true);
  3051     this.browser.addEventListener("blur", this, true);
  3052     this.browser.addEventListener("scroll", this, true);
  3053     this.browser.addEventListener("MozScrolledAreaChanged", this, true);
  3054     this.browser.addEventListener("pageshow", this, true);
  3055     this.browser.addEventListener("MozApplicationManifest", this, true);
  3057     // Note that the XBL binding is untrusted
  3058     this.browser.addEventListener("PluginBindingAttached", this, true, true);
  3059     this.browser.addEventListener("VideoBindingAttached", this, true, true);
  3060     this.browser.addEventListener("VideoBindingCast", this, true, true);
  3062     Services.obs.addObserver(this, "before-first-paint", false);
  3063     Services.obs.addObserver(this, "after-viewport-change", false);
  3064     Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false);
  3066     if (aParams.delayLoad) {
  3067       // If this is a zombie tab, attach restore data so the tab will be
  3068       // restored when selected
  3069       this.browser.__SS_data = {
  3070         entries: [{
  3071           url: aURL,
  3072           title: title
  3073         }],
  3074         index: 1
  3075       };
  3076       this.browser.__SS_restore = true;
  3077     } else {
  3078       let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
  3079       let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
  3080       let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
  3081       let charset = "charset" in aParams ? aParams.charset : null;
  3083       // The search term the user entered to load the current URL
  3084       this.userSearch = "userSearch" in aParams ? aParams.userSearch : "";
  3086       try {
  3087         this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData);
  3088       } catch(e) {
  3089         let message = {
  3090           type: "Content:LoadError",
  3091           tabID: this.id
  3092         };
  3093         sendMessageToJava(message);
  3094         dump("Handled load error: " + e);
  3097   },
  3099   /**
  3100    * Retrieves the font size in twips for a given element.
  3101    */
  3102   getInflatedFontSizeFor: function(aElement) {
  3103     // GetComputedStyle should always give us CSS pixels for a font size.
  3104     let fontSizeStr = this.window.getComputedStyle(aElement)['fontSize'];
  3105     let fontSize = fontSizeStr.slice(0, -2);
  3106     return aElement.fontSizeInflation * fontSize;
  3107   },
  3109   /**
  3110    * This returns the zoom necessary to match the font size of an element to
  3111    * the minimum font size specified by the browser.zoom.reflowOnZoom.minFontSizeTwips
  3112    * preference.
  3113    */
  3114   getZoomToMinFontSize: function(aElement) {
  3115     // We only use the font.size.inflation.minTwips preference because this is
  3116     // the only one that is controlled by the user-interface in the 'Settings'
  3117     // menu. Thus, if font.size.inflation.emPerLine is changed, this does not
  3118     // effect reflow-on-zoom.
  3119     let minFontSize = convertFromTwipsToPx(Services.prefs.getIntPref("font.size.inflation.minTwips"));
  3120     return minFontSize / this.getInflatedFontSizeFor(aElement);
  3121   },
  3123   clearReflowOnZoomPendingActions: function() {
  3124     // Reflow was completed, so now re-enable painting.
  3125     let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
  3126     let docShell = webNav.QueryInterface(Ci.nsIDocShell);
  3127     let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
  3128     docViewer.resumePainting();
  3130     BrowserApp.selectedTab._mReflozPositioned = false;
  3131   },
  3133   /**
  3134    * Reflow on zoom consists of a few different sub-operations:
  3136    * 1. When a double-tap event is seen, we verify that the correct preferences
  3137    *    are enabled and perform the pre-position handling calculation. We also
  3138    *    signal that reflow-on-zoom should be performed at this time, and pause
  3139    *    painting.
  3140    * 2. During the next call to setViewport(), which is in the Tab prototype,
  3141    *    we detect that a call to changeMaxLineBoxWidth should be performed. If
  3142    *    we're zooming out, then the max line box width should be reset at this
  3143    *    time. Otherwise, we call performReflowOnZoom.
  3144    *   2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to
  3145    *       doChangeMaxLineBoxWidth, based on a timeout specified in preferences.
  3146    * 3. doChangeMaxLineBoxWidth changes the line box width (which also
  3147    *    schedules a reflow event), and then calls ZoomHelper.zoomInAndSnapToRange.
  3148    * 4. ZoomHelper.zoomInAndSnapToRange performs the positioning of reflow-on-zoom
  3149    *    and then re-enables painting.
  3151    * Some of the events happen synchronously, while others happen asynchronously.
  3152    * The following is a rough sketch of the progression of events:
  3154    * double tap event seen -> onDoubleTap() -> ... asynchronous ...
  3155    *   -> setViewport() -> performReflowOnZoom() -> ... asynchronous ...
  3156    *   -> doChangeMaxLineBoxWidth() -> ZoomHelper.zoomInAndSnapToRange()
  3157    *   -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change')
  3158    *   -> resumePainting()
  3159    */
  3160   performReflowOnZoom: function(aViewport) {
  3161     let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom;
  3163     let viewportWidth = gScreenWidth / zoom;
  3164     let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
  3166     if (gReflowPending) {
  3167       clearTimeout(gReflowPending);
  3170     // We add in a bit of fudge just so that the end characters
  3171     // don't accidentally get clipped. 15px is an arbitrary choice.
  3172     gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
  3173                                 reflozTimeout,
  3174                                 viewportWidth - 15);
  3175   },
  3177   /** 
  3178    * Reloads the tab with the desktop mode setting.
  3179    */
  3180   reloadWithMode: function (aDesktopMode) {
  3181     // Set desktop mode for tab and send change to Java
  3182     if (this.desktopMode != aDesktopMode) {
  3183       this.desktopMode = aDesktopMode;
  3184       sendMessageToJava({
  3185         type: "DesktopMode:Changed",
  3186         desktopMode: aDesktopMode,
  3187         tabID: this.id
  3188       });
  3191     // Only reload the page for http/https schemes
  3192     let currentURI = this.browser.currentURI;
  3193     if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https"))
  3194       return;
  3196     let url = currentURI.spec;
  3197     let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
  3198                 Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
  3199     if (this.originalURI && !this.originalURI.equals(currentURI)) {
  3200       // We were redirected; reload the original URL
  3201       url = this.originalURI.spec;
  3204     this.browser.docShell.loadURI(url, flags, null, null, null);
  3205   },
  3207   destroy: function() {
  3208     if (!this.browser)
  3209       return;
  3211     this.browser.contentWindow.controllers.removeController(this.overscrollController);
  3213     this.browser.removeProgressListener(this);
  3214     this.browser.sessionHistory.removeSHistoryListener(this);
  3216     this.browser.removeEventListener("DOMContentLoaded", this, true);
  3217     this.browser.removeEventListener("DOMFormHasPassword", this, true);
  3218     this.browser.removeEventListener("DOMLinkAdded", this, true);
  3219     this.browser.removeEventListener("DOMTitleChanged", this, true);
  3220     this.browser.removeEventListener("DOMWindowClose", this, true);
  3221     this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
  3222     this.browser.removeEventListener("DOMAutoComplete", this, true);
  3223     this.browser.removeEventListener("blur", this, true);
  3224     this.browser.removeEventListener("scroll", this, true);
  3225     this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
  3226     this.browser.removeEventListener("pageshow", this, true);
  3227     this.browser.removeEventListener("MozApplicationManifest", this, true);
  3229     this.browser.removeEventListener("PluginBindingAttached", this, true, true);
  3230     this.browser.removeEventListener("VideoBindingAttached", this, true, true);
  3231     this.browser.removeEventListener("VideoBindingCast", this, true, true);
  3233     Services.obs.removeObserver(this, "before-first-paint");
  3234     Services.obs.removeObserver(this, "after-viewport-change");
  3235     Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this);
  3237     // Make sure the previously selected panel remains selected. The selected panel of a deck is
  3238     // not stable when panels are removed.
  3239     let selectedPanel = BrowserApp.deck.selectedPanel;
  3240     BrowserApp.deck.removeChild(this.browser);
  3241     BrowserApp.deck.selectedPanel = selectedPanel;
  3243     this.browser = null;
  3244     this.savedArticle = null;
  3245   },
  3247   // This should be called to update the browser when the tab gets selected/unselected
  3248   setActive: function setActive(aActive) {
  3249     if (!this.browser || !this.browser.docShell)
  3250       return;
  3252     this.lastTouchedAt = Date.now();
  3254     if (aActive) {
  3255       this.browser.setAttribute("type", "content-primary");
  3256       this.browser.focus();
  3257       this.browser.docShellIsActive = true;
  3258       Reader.updatePageAction(this);
  3259       ExternalApps.updatePageAction(this.browser.currentURI);
  3260     } else {
  3261       this.browser.setAttribute("type", "content-targetable");
  3262       this.browser.docShellIsActive = false;
  3264   },
  3266   getActive: function getActive() {
  3267     return this.browser.docShellIsActive;
  3268   },
  3270   setDisplayPort: function(aDisplayPort) {
  3271     let zoom = this._zoom;
  3272     let resolution = aDisplayPort.resolution;
  3273     if (zoom <= 0 || resolution <= 0)
  3274       return;
  3276     // "zoom" is the user-visible zoom of the "this" tab
  3277     // "resolution" is the zoom at which we wish gecko to render "this" tab at
  3278     // these two may be different if we are, for example, trying to render a
  3279     // large area of the page at low resolution because the user is panning real
  3280     // fast.
  3281     // The gecko scroll position is in CSS pixels. The display port rect
  3282     // values (aDisplayPort), however, are in CSS pixels multiplied by the desired
  3283     // rendering resolution. Therefore care must be taken when doing math with
  3284     // these sets of values, to ensure that they are normalized to the same coordinate
  3285     // space first.
  3287     let element = this.browser.contentDocument.documentElement;
  3288     if (!element)
  3289       return;
  3291     // we should never be drawing background tabs at resolutions other than the user-
  3292     // visible zoom. for foreground tabs, however, if we are drawing at some other
  3293     // resolution, we need to set the resolution as specified.
  3294     let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  3295     if (BrowserApp.selectedTab == this) {
  3296       if (resolution != this._drawZoom) {
  3297         this._drawZoom = resolution;
  3298         cwu.setResolution(resolution / window.devicePixelRatio, resolution / window.devicePixelRatio);
  3300     } else if (!fuzzyEquals(resolution, zoom)) {
  3301       dump("Warning: setDisplayPort resolution did not match zoom for background tab! (" + resolution + " != " + zoom + ")");
  3304     // Finally, we set the display port, taking care to convert everything into the CSS-pixel
  3305     // coordinate space, because that is what the function accepts. Also we have to fudge the
  3306     // displayport somewhat to make sure it gets through all the conversions gecko will do on it
  3307     // without deforming too much. See https://bugzilla.mozilla.org/show_bug.cgi?id=737510#c10
  3308     // for details on what these operations are.
  3309     let geckoScrollX = this.browser.contentWindow.scrollX;
  3310     let geckoScrollY = this.browser.contentWindow.scrollY;
  3311     aDisplayPort = this._dirtiestHackEverToWorkAroundGeckoRounding(aDisplayPort, geckoScrollX, geckoScrollY);
  3313     let displayPort = {
  3314       x: (aDisplayPort.left / resolution) - geckoScrollX,
  3315       y: (aDisplayPort.top / resolution) - geckoScrollY,
  3316       width: (aDisplayPort.right - aDisplayPort.left) / resolution,
  3317       height: (aDisplayPort.bottom - aDisplayPort.top) / resolution
  3318     };
  3320     if (this._oldDisplayPort == null ||
  3321         !fuzzyEquals(displayPort.x, this._oldDisplayPort.x) ||
  3322         !fuzzyEquals(displayPort.y, this._oldDisplayPort.y) ||
  3323         !fuzzyEquals(displayPort.width, this._oldDisplayPort.width) ||
  3324         !fuzzyEquals(displayPort.height, this._oldDisplayPort.height)) {
  3325       if (BrowserApp.gUseLowPrecision) {
  3326         // Set the display-port to be 4x the size of the critical display-port,
  3327         // on each dimension, giving us a 0.25x lower precision buffer around the
  3328         // critical display-port. Spare area is *not* redistributed to the other
  3329         // axis, as display-list building and invalidation cost scales with the
  3330         // size of the display-port.
  3331         let pageRect = cwu.getRootBounds();
  3332         let pageXMost = pageRect.right - geckoScrollX;
  3333         let pageYMost = pageRect.bottom - geckoScrollY;
  3335         let dpW = Math.min(pageRect.right - pageRect.left, displayPort.width * 4);
  3336         let dpH = Math.min(pageRect.bottom - pageRect.top, displayPort.height * 4);
  3338         let dpX = Math.min(Math.max(displayPort.x - displayPort.width * 1.5,
  3339                                     pageRect.left - geckoScrollX), pageXMost - dpW);
  3340         let dpY = Math.min(Math.max(displayPort.y - displayPort.height * 1.5,
  3341                                     pageRect.top - geckoScrollY), pageYMost - dpH);
  3342         cwu.setDisplayPortForElement(dpX, dpY, dpW, dpH, element, 0);
  3343         cwu.setCriticalDisplayPortForElement(displayPort.x, displayPort.y,
  3344                                              displayPort.width, displayPort.height,
  3345                                              element);
  3346       } else {
  3347         cwu.setDisplayPortForElement(displayPort.x, displayPort.y,
  3348                                      displayPort.width, displayPort.height,
  3349                                      element, 0);
  3353     this._oldDisplayPort = displayPort;
  3354   },
  3356   /*
  3357    * Yes, this is ugly. But it's currently the safest way to account for the rounding errors that occur
  3358    * when we pump the displayport coordinates through gecko and they pop out in the compositor.
  3360    * In general, the values are converted from page-relative device pixels to viewport-relative app units,
  3361    * and then back to page-relative device pixels (now as ints). The first half of this is only slightly
  3362    * lossy, but it's enough to throw off the numbers a little. Because of this, when gecko calls
  3363    * ScaleToOutsidePixels to generate the final rect, the rect may get expanded more than it should,
  3364    * ending up a pixel larger than it started off. This is undesirable in general, but specifically
  3365    * bad for tiling, because it means we means we end up painting one line of pixels from a tile,
  3366    * causing an otherwise unnecessary upload of the whole tile.
  3368    * In order to counteract the rounding error, this code simulates the conversions that will happen
  3369    * to the display port, and calculates whether or not that final ScaleToOutsidePixels is actually
  3370    * expanding the rect more than it should. If so, it determines how much rounding error was introduced
  3371    * up until that point, and adjusts the original values to compensate for that rounding error.
  3372    */
  3373   _dirtiestHackEverToWorkAroundGeckoRounding: function(aDisplayPort, aGeckoScrollX, aGeckoScrollY) {
  3374     const APP_UNITS_PER_CSS_PIXEL = 60.0;
  3375     const EXTRA_FUDGE = 0.04;
  3377     let resolution = aDisplayPort.resolution;
  3379     // Some helper functions that simulate conversion processes in gecko
  3381     function cssPixelsToAppUnits(aVal) {
  3382       return Math.floor((aVal * APP_UNITS_PER_CSS_PIXEL) + 0.5);
  3385     function appUnitsToDevicePixels(aVal) {
  3386       return aVal / APP_UNITS_PER_CSS_PIXEL * resolution;
  3389     function devicePixelsToAppUnits(aVal) {
  3390       return cssPixelsToAppUnits(aVal / resolution);
  3393     // Stash our original (desired) displayport width and height away, we need it
  3394     // later and we might modify the displayport in between.
  3395     let originalWidth = aDisplayPort.right - aDisplayPort.left;
  3396     let originalHeight = aDisplayPort.bottom - aDisplayPort.top;
  3398     // This is the first conversion the displayport goes through, going from page-relative
  3399     // device pixels to viewport-relative app units.
  3400     let appUnitDisplayPort = {
  3401       x: cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX),
  3402       y: cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY),
  3403       w: cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution),
  3404       h: cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution)
  3405     };
  3407     // This is the translation gecko applies when converting back from viewport-relative
  3408     // device pixels to page-relative device pixels.
  3409     let geckoTransformX = -Math.floor((-aGeckoScrollX * resolution) + 0.5);
  3410     let geckoTransformY = -Math.floor((-aGeckoScrollY * resolution) + 0.5);
  3412     // The final "left" value as calculated in gecko is:
  3413     //    left = geckoTransformX + Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x))
  3414     // In a perfect world, this value would be identical to aDisplayPort.left, which is what
  3415     // we started with. However, this may not be the case if the value being floored has accumulated
  3416     // enough error to drop below what it should be.
  3417     // For example, assume geckoTransformX is 0, and aDisplayPort.left is 4, but
  3418     // appUnitsToDevicePixels(appUnitsToDevicePixels.x) comes out as 3.9 because of rounding error.
  3419     // That's bad, because the -0.1 error has caused it to floor to 3 instead of 4. (If it had errored
  3420     // the other way and come out as 4.1, there's no problem). In this example, we need to increase the
  3421     // "left" value by some amount so that the 3.9 actually comes out as >= 4, and it gets floored into
  3422     // the expected value of 4. The delta values calculated below calculate that error amount (e.g. -0.1).
  3423     let errorLeft = (geckoTransformX + appUnitsToDevicePixels(appUnitDisplayPort.x)) - aDisplayPort.left;
  3424     let errorTop = (geckoTransformY + appUnitsToDevicePixels(appUnitDisplayPort.y)) - aDisplayPort.top;
  3426     // If the error was negative, that means it will floor incorrectly, so we need to bump up the
  3427     // original aDisplayPort.left and/or aDisplayPort.top values. The amount we bump it up by is
  3428     // the error amount (increased by a small fudge factor to ensure it's sufficient), converted
  3429     // backwards through the conversion process.
  3430     if (errorLeft < 0) {
  3431       aDisplayPort.left += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorLeft));
  3432       // After we modify the left value, we need to re-simulate some values to take that into account
  3433       appUnitDisplayPort.x = cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX);
  3434       appUnitDisplayPort.w = cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution);
  3436     if (errorTop < 0) {
  3437       aDisplayPort.top += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorTop));
  3438       // After we modify the top value, we need to re-simulate some values to take that into account
  3439       appUnitDisplayPort.y = cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY);
  3440       appUnitDisplayPort.h = cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution);
  3443     // At this point, the aDisplayPort.left and aDisplayPort.top values have been corrected to account
  3444     // for the error in conversion such that they end up where we want them. Now we need to also do the
  3445     // same for the right/bottom values so that the width/height end up where we want them.
  3447     // This is the final conversion that the displayport goes through before gecko spits it back to
  3448     // us. Note that the width/height calculates are of the form "ceil(transform(right)) - floor(transform(left))"
  3449     let scaledOutDevicePixels = {
  3450       x: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)),
  3451       y: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)),
  3452       w: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)),
  3453       h: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y))
  3454     };
  3456     // The final "width" value as calculated in gecko is scaledOutDevicePixels.w.
  3457     // In a perfect world, this would equal originalWidth. However, things are not perfect, and as before,
  3458     // we need to calculate how much rounding error has been introduced. In this case the rounding error is causing
  3459     // the Math.ceil call above to ceiling to the wrong final value. For example, 4 gets converted 4.1 and gets
  3460     // ceiling'd to 5; in this case the error is 0.1.
  3461     let errorRight = (appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w) - scaledOutDevicePixels.x) - originalWidth;
  3462     let errorBottom = (appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h) - scaledOutDevicePixels.y) - originalHeight;
  3464     // If the error was positive, that means it will ceiling incorrectly, so we need to bump down the
  3465     // original aDisplayPort.right and/or aDisplayPort.bottom. Again, we back-convert the error amount
  3466     // with a small fudge factor to figure out how much to adjust the original values.
  3467     if (errorRight > 0) aDisplayPort.right -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorRight + EXTRA_FUDGE));
  3468     if (errorBottom > 0) aDisplayPort.bottom -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorBottom + EXTRA_FUDGE));
  3470     // Et voila!
  3471     return aDisplayPort;
  3472   },
  3474   setScrollClampingSize: function(zoom) {
  3475     let viewportWidth = gScreenWidth / zoom;
  3476     let viewportHeight = gScreenHeight / zoom;
  3477     let screenWidth = gScreenWidth;
  3478     let screenHeight = gScreenHeight;
  3480     // Shrink the viewport appropriately if the margins are excluded
  3481     if (this.viewportExcludesVerticalMargins) {
  3482       screenHeight = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
  3483       viewportHeight = screenHeight / zoom;
  3485     if (this.viewportExcludesHorizontalMargins) {
  3486       screenWidth = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
  3487       viewportWidth = screenWidth / zoom;
  3490     // Make sure the aspect ratio of the screen is maintained when setting
  3491     // the clamping scroll-port size.
  3492     let factor = Math.min(viewportWidth / screenWidth,
  3493                           viewportHeight / screenHeight);
  3494     let scrollPortWidth = screenWidth * factor;
  3495     let scrollPortHeight = screenHeight * factor;
  3497     let win = this.browser.contentWindow;
  3498     win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).
  3499         setScrollPositionClampingScrollPortSize(scrollPortWidth, scrollPortHeight);
  3500   },
  3502   setViewport: function(aViewport) {
  3503     // Transform coordinates based on zoom
  3504     let x = aViewport.x / aViewport.zoom;
  3505     let y = aViewport.y / aViewport.zoom;
  3507     this.setScrollClampingSize(aViewport.zoom);
  3509     // Adjust the max line box width to be no more than the viewport width, but
  3510     // only if the reflow-on-zoom preference is enabled.
  3511     let isZooming = !fuzzyEquals(aViewport.zoom, this._zoom);
  3513     let docViewer = null;
  3515     if (isZooming &&
  3516         BrowserEventHandler.mReflozPref &&
  3517         BrowserApp.selectedTab._mReflozPoint &&
  3518         BrowserApp.selectedTab.probablyNeedRefloz) {
  3519       let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
  3520       let docShell = webNav.QueryInterface(Ci.nsIDocShell);
  3521       docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
  3522       docViewer.pausePainting();
  3524       BrowserApp.selectedTab.performReflowOnZoom(aViewport);
  3525       BrowserApp.selectedTab.probablyNeedRefloz = false;
  3528     let win = this.browser.contentWindow;
  3529     win.scrollTo(x, y);
  3530     this.saveSessionZoom(aViewport.zoom);
  3532     this.userScrollPos.x = win.scrollX;
  3533     this.userScrollPos.y = win.scrollY;
  3534     this.setResolution(aViewport.zoom, false);
  3536     if (aViewport.displayPort)
  3537       this.setDisplayPort(aViewport.displayPort);
  3539     // Store fixed margins for later retrieval in getViewport.
  3540     this._fixedMarginLeft = aViewport.fixedMarginLeft;
  3541     this._fixedMarginTop = aViewport.fixedMarginTop;
  3542     this._fixedMarginRight = aViewport.fixedMarginRight;
  3543     this._fixedMarginBottom = aViewport.fixedMarginBottom;
  3545     let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  3546     dwi.setContentDocumentFixedPositionMargins(
  3547       aViewport.fixedMarginTop / aViewport.zoom,
  3548       aViewport.fixedMarginRight / aViewport.zoom,
  3549       aViewport.fixedMarginBottom / aViewport.zoom,
  3550       aViewport.fixedMarginLeft / aViewport.zoom);
  3552     Services.obs.notifyObservers(null, "after-viewport-change", "");
  3553     if (docViewer) {
  3554         docViewer.resumePainting();
  3556   },
  3558   setResolution: function(aZoom, aForce) {
  3559     // Set zoom level
  3560     if (aForce || !fuzzyEquals(aZoom, this._zoom)) {
  3561       this._zoom = aZoom;
  3562       if (BrowserApp.selectedTab == this) {
  3563         let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  3564         this._drawZoom = aZoom;
  3565         cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio);
  3568   },
  3570   getPageSize: function(aDocument, aDefaultWidth, aDefaultHeight) {
  3571     let body = aDocument.body || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight };
  3572     let html = aDocument.documentElement || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight };
  3573     return [Math.max(body.scrollWidth, html.scrollWidth),
  3574       Math.max(body.scrollHeight, html.scrollHeight)];
  3575   },
  3577   getViewport: function() {
  3578     let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
  3579     let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
  3580     let zoom = this.restoredSessionZoom() || this._zoom;
  3582     let viewport = {
  3583       width: screenW,
  3584       height: screenH,
  3585       cssWidth: screenW / zoom,
  3586       cssHeight: screenH / zoom,
  3587       pageLeft: 0,
  3588       pageTop: 0,
  3589       pageRight: screenW,
  3590       pageBottom: screenH,
  3591       // We make up matching css page dimensions
  3592       cssPageLeft: 0,
  3593       cssPageTop: 0,
  3594       cssPageRight: screenW / zoom,
  3595       cssPageBottom: screenH / zoom,
  3596       fixedMarginLeft: this._fixedMarginLeft,
  3597       fixedMarginTop: this._fixedMarginTop,
  3598       fixedMarginRight: this._fixedMarginRight,
  3599       fixedMarginBottom: this._fixedMarginBottom,
  3600       zoom: zoom,
  3601     };
  3603     // Set the viewport offset to current scroll offset
  3604     viewport.cssX = this.browser.contentWindow.scrollX || 0;
  3605     viewport.cssY = this.browser.contentWindow.scrollY || 0;
  3607     // Transform coordinates based on zoom
  3608     viewport.x = Math.round(viewport.cssX * viewport.zoom);
  3609     viewport.y = Math.round(viewport.cssY * viewport.zoom);
  3611     let doc = this.browser.contentDocument;
  3612     if (doc != null) {
  3613       let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  3614       let cssPageRect = cwu.getRootBounds();
  3616       /*
  3617        * Avoid sending page sizes of less than screen size before we hit DOMContentLoaded, because
  3618        * this causes the page size to jump around wildly during page load. After the page is loaded,
  3619        * send updates regardless of page size; we'll zoom to fit the content as needed.
  3621        * In the check below, we floor the viewport size because there might be slight rounding errors
  3622        * introduced in the CSS page size due to the conversion to and from app units in Gecko. The
  3623        * error should be no more than one app unit so doing the floor is overkill, but safe in the
  3624        * sense that the extra page size updates that get sent as a result will be mostly harmless.
  3625        */
  3626       let pageLargerThanScreen = (cssPageRect.width >= Math.floor(viewport.cssWidth))
  3627                               && (cssPageRect.height >= Math.floor(viewport.cssHeight));
  3628       if (doc.readyState === 'complete' || pageLargerThanScreen) {
  3629         viewport.cssPageLeft = cssPageRect.left;
  3630         viewport.cssPageTop = cssPageRect.top;
  3631         viewport.cssPageRight = cssPageRect.right;
  3632         viewport.cssPageBottom = cssPageRect.bottom;
  3633         /* Transform the page width and height based on the zoom factor. */
  3634         viewport.pageLeft = (viewport.cssPageLeft * viewport.zoom);
  3635         viewport.pageTop = (viewport.cssPageTop * viewport.zoom);
  3636         viewport.pageRight = (viewport.cssPageRight * viewport.zoom);
  3637         viewport.pageBottom = (viewport.cssPageBottom * viewport.zoom);
  3641     return viewport;
  3642   },
  3644   sendViewportUpdate: function(aPageSizeUpdate) {
  3645     let viewport = this.getViewport();
  3646     let displayPort = Services.androidBridge.getDisplayPort(aPageSizeUpdate, BrowserApp.isBrowserContentDocumentDisplayed(), this.id, viewport);
  3647     if (displayPort != null)
  3648       this.setDisplayPort(displayPort);
  3649   },
  3651   updateViewportForPageSize: function() {
  3652     let hasHorizontalMargins = gViewportMargins.left != 0 || gViewportMargins.right != 0;
  3653     let hasVerticalMargins = gViewportMargins.top != 0 || gViewportMargins.bottom != 0;
  3655     if (!hasHorizontalMargins && !hasVerticalMargins) {
  3656       // If there are no margins, then we don't need to do any remeasuring
  3657       return;
  3660     // If the page size has changed so that it might or might not fit on the
  3661     // screen with the margins included, run updateViewportSize to resize the
  3662     // browser accordingly.
  3663     // A page will receive the smaller viewport when its page size fits
  3664     // within the screen size, so remeasure when the page size remains within
  3665     // the threshold of screen + margins, in case it's sizing itself relative
  3666     // to the viewport.
  3667     let viewport = this.getViewport();
  3668     let pageWidth = viewport.pageRight - viewport.pageLeft;
  3669     let pageHeight = viewport.pageBottom - viewport.pageTop;
  3670     let remeasureNeeded = false;
  3672     if (hasHorizontalMargins) {
  3673       let viewportShouldExcludeHorizontalMargins = (pageWidth <= gScreenWidth - 0.5);
  3674       if (viewportShouldExcludeHorizontalMargins != this.viewportExcludesHorizontalMargins) {
  3675         remeasureNeeded = true;
  3678     if (hasVerticalMargins) {
  3679       let viewportShouldExcludeVerticalMargins = (pageHeight <= gScreenHeight - 0.5);
  3680       if (viewportShouldExcludeVerticalMargins != this.viewportExcludesVerticalMargins) {
  3681         remeasureNeeded = true;
  3685     if (remeasureNeeded) {
  3686       if (!this.viewportMeasureCallback) {
  3687         this.viewportMeasureCallback = setTimeout(function() {
  3688           this.viewportMeasureCallback = null;
  3690           // Re-fetch the viewport as it may have changed between setting the timeout
  3691           // and running this callback
  3692           let viewport = this.getViewport();
  3693           let pageWidth = viewport.pageRight - viewport.pageLeft;
  3694           let pageHeight = viewport.pageBottom - viewport.pageTop;
  3696           if (Math.abs(pageWidth - this.lastPageSizeAfterViewportRemeasure.width) >= 0.5 ||
  3697               Math.abs(pageHeight - this.lastPageSizeAfterViewportRemeasure.height) >= 0.5) {
  3698             this.updateViewportSize(gScreenWidth);
  3700         }.bind(this), kViewportRemeasureThrottle);
  3702     } else if (this.viewportMeasureCallback) {
  3703       // If the page changed size twice since we last measured the viewport and
  3704       // the latest size change reveals we don't need to remeasure, cancel any
  3705       // pending remeasure.
  3706       clearTimeout(this.viewportMeasureCallback);
  3707       this.viewportMeasureCallback = null;
  3709   },
  3711   handleEvent: function(aEvent) {
  3712     switch (aEvent.type) {
  3713       case "DOMContentLoaded": {
  3714         let target = aEvent.originalTarget;
  3716         // ignore on frames and other documents
  3717         if (target != this.browser.contentDocument)
  3718           return;
  3720         // Sample the background color of the page and pass it along. (This is used to draw the
  3721         // checkerboard.) Right now we don't detect changes in the background color after this
  3722         // event fires; it's not clear that doing so is worth the effort.
  3723         var backgroundColor = null;
  3724         try {
  3725           let { contentDocument, contentWindow } = this.browser;
  3726           let computedStyle = contentWindow.getComputedStyle(contentDocument.body);
  3727           backgroundColor = computedStyle.backgroundColor;
  3728         } catch (e) {
  3729           // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds.
  3732         let docURI = target.documentURI;
  3733         let errorType = "";
  3734         if (docURI.startsWith("about:certerror"))
  3735           errorType = "certerror";
  3736         else if (docURI.startsWith("about:blocked"))
  3737           errorType = "blocked"
  3738         else if (docURI.startsWith("about:neterror"))
  3739           errorType = "neterror";
  3741         sendMessageToJava({
  3742           type: "DOMContentLoaded",
  3743           tabID: this.id,
  3744           bgColor: backgroundColor,
  3745           errorType: errorType
  3746         });
  3748         // Attach a listener to watch for "click" events bubbling up from error
  3749         // pages and other similar page. This lets us fix bugs like 401575 which
  3750         // require error page UI to do privileged things, without letting error
  3751         // pages have any privilege themselves.
  3752         if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) {
  3753           this.browser.addEventListener("click", ErrorPageEventHandler, true);
  3754           let listener = function() {
  3755             this.browser.removeEventListener("click", ErrorPageEventHandler, true);
  3756             this.browser.removeEventListener("pagehide", listener, true);
  3757           }.bind(this);
  3759           this.browser.addEventListener("pagehide", listener, true);
  3762         if (docURI.startsWith("about:reader")) {
  3763           // During browser restart / recovery, duplicate "DOMContentLoaded" messages are received here
  3764           // For the visible tab ... where more than one tab is being reloaded, the inital "DOMContentLoaded"
  3765           // Message can be received before the document body is available ... so we avoid instantiating an
  3766           // AboutReader object, expecting that an eventual valid message will follow.
  3767           let contentDocument = this.browser.contentDocument;
  3768           if (contentDocument.body) {
  3769             new AboutReader(contentDocument, this.browser.contentWindow);
  3773         break;
  3776       case "DOMFormHasPassword": {
  3777         LoginManagerContent.onFormPassword(aEvent);
  3778         break;
  3781       case "DOMLinkAdded": {
  3782         let target = aEvent.originalTarget;
  3783         if (!target.href || target.disabled)
  3784           return;
  3786         // Ignore on frames and other documents
  3787         if (target.ownerDocument != this.browser.contentDocument)
  3788           return;
  3790         // Sanitize the rel string
  3791         let list = [];
  3792         if (target.rel) {
  3793           list = target.rel.toLowerCase().split(/\s+/);
  3794           let hash = {};
  3795           list.forEach(function(value) { hash[value] = true; });
  3796           list = [];
  3797           for (let rel in hash)
  3798             list.push("[" + rel + "]");
  3801         if (list.indexOf("[icon]") != -1) {
  3802           // We want to get the largest icon size possible for our UI.
  3803           let maxSize = 0;
  3805           // We use the sizes attribute if available
  3806           // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
  3807           if (target.hasAttribute("sizes")) {
  3808             let sizes = target.getAttribute("sizes").toLowerCase();
  3810             if (sizes == "any") {
  3811               // Since Java expects an integer, use -1 to represent icons with sizes="any"
  3812               maxSize = -1; 
  3813             } else {
  3814               let tokens = sizes.split(" ");
  3815               tokens.forEach(function(token) {
  3816                 // TODO: check for invalid tokens
  3817                 let [w, h] = token.split("x");
  3818                 maxSize = Math.max(maxSize, Math.max(w, h));
  3819               });
  3823           let json = {
  3824             type: "Link:Favicon",
  3825             tabID: this.id,
  3826             href: resolveGeckoURI(target.href),
  3827             charset: target.ownerDocument.characterSet,
  3828             title: target.title,
  3829             rel: list.join(" "),
  3830             size: maxSize
  3831           };
  3832           sendMessageToJava(json);
  3833         } else if (list.indexOf("[alternate]") != -1) {
  3834           let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
  3835           let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
  3837           if (!isFeed)
  3838             return;
  3840           try {
  3841             // urlSecurityCeck will throw if things are not OK
  3842             ContentAreaUtils.urlSecurityCheck(target.href, target.ownerDocument.nodePrincipal, Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
  3844             if (!this.browser.feeds)
  3845               this.browser.feeds = [];
  3846             this.browser.feeds.push({ href: target.href, title: target.title, type: type });
  3848             let json = {
  3849               type: "Link:Feed",
  3850               tabID: this.id
  3851             };
  3852             sendMessageToJava(json);
  3853           } catch (e) {}
  3854         } else if (list.indexOf("[search]" != -1)) {
  3855           let type = target.type && target.type.toLowerCase();
  3857           // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
  3858           type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
  3860           // Check that type matches opensearch.
  3861           let isOpenSearch = (type == "application/opensearchdescription+xml");
  3862           if (isOpenSearch && target.title && /^(?:https?|ftp):/i.test(target.href)) {
  3863             let visibleEngines = Services.search.getVisibleEngines();
  3864             // NOTE: Engines are currently identified by name, but this can be changed
  3865             // when Engines are identified by URL (see bug 335102).
  3866             if (visibleEngines.some(function(e) {
  3867               return e.name == target.title;
  3868             })) {
  3869               // This engine is already present, do nothing.
  3870               return;
  3873             if (this.browser.engines) {
  3874               // This engine has already been handled, do nothing.
  3875               if (this.browser.engines.some(function(e) {
  3876                 return e.url == target.href;
  3877               })) {
  3878                   return;
  3880             } else {
  3881               this.browser.engines = [];
  3884             // Get favicon.
  3885             let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico";
  3887             let newEngine = {
  3888               title: target.title,
  3889               url: target.href,
  3890               iconURL: iconURL
  3891             };
  3893             this.browser.engines.push(newEngine);
  3895             // Don't send a message to display engines if we've already handled an engine.
  3896             if (this.browser.engines.length > 1)
  3897               return;
  3899             // Broadcast message that this tab contains search engines that should be visible.
  3900             let newEngineMessage = {
  3901               type: "Link:OpenSearch",
  3902               tabID: this.id,
  3903               visible: true
  3904             };
  3906             sendMessageToJava(newEngineMessage);
  3909         break;
  3912       case "DOMTitleChanged": {
  3913         if (!aEvent.isTrusted)
  3914           return;
  3916         // ignore on frames and other documents
  3917         if (aEvent.originalTarget != this.browser.contentDocument)
  3918           return;
  3920         sendMessageToJava({
  3921           type: "DOMTitleChanged",
  3922           tabID: this.id,
  3923           title: aEvent.target.title.substring(0, 255)
  3924         });
  3925         break;
  3928       case "DOMWindowClose": {
  3929         if (!aEvent.isTrusted)
  3930           return;
  3932         // Find the relevant tab, and close it from Java
  3933         if (this.browser.contentWindow == aEvent.target) {
  3934           aEvent.preventDefault();
  3936           sendMessageToJava({
  3937             type: "Tab:Close",
  3938             tabID: this.id
  3939           });
  3941         break;
  3944       case "DOMWillOpenModalDialog": {
  3945         if (!aEvent.isTrusted)
  3946           return;
  3948         // We're about to open a modal dialog, make sure the opening
  3949         // tab is brought to the front.
  3950         let tab = BrowserApp.getTabForWindow(aEvent.target.top);
  3951         BrowserApp.selectTab(tab);
  3952         break;
  3955       case "DOMAutoComplete":
  3956       case "blur": {
  3957         LoginManagerContent.onUsernameInput(aEvent);
  3958         break;
  3961       case "scroll": {
  3962         let win = this.browser.contentWindow;
  3963         if (this.userScrollPos.x != win.scrollX || this.userScrollPos.y != win.scrollY) {
  3964           this.sendViewportUpdate();
  3966         break;
  3969       case "MozScrolledAreaChanged": {
  3970         // This event is only fired for root scroll frames, and only when the
  3971         // scrolled area has actually changed, so no need to check for that.
  3972         // Just make sure it's the event for the correct root scroll frame.
  3973         if (aEvent.originalTarget != this.browser.contentDocument)
  3974           return;
  3976         this.sendViewportUpdate(true);
  3977         this.updateViewportForPageSize();
  3978         break;
  3981       case "PluginBindingAttached": {
  3982         PluginHelper.handlePluginBindingAttached(this, aEvent);
  3983         break;
  3986       case "VideoBindingAttached": {
  3987         CastingApps.handleVideoBindingAttached(this, aEvent);
  3988         break;
  3991       case "VideoBindingCast": {
  3992         CastingApps.handleVideoBindingCast(this, aEvent);
  3993         break;
  3996       case "MozApplicationManifest": {
  3997         OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView);
  3998         break;
  4001       case "pageshow": {
  4002         // only send pageshow for the top-level document
  4003         if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
  4004           return;
  4006         sendMessageToJava({
  4007           type: "Content:PageShow",
  4008           tabID: this.id
  4009         });
  4011         if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
  4012           if (!this._linkifier)
  4013             this._linkifier = new Linkifier();
  4014           this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
  4017         // Update page actions for helper apps.
  4018         let uri = this.browser.currentURI;
  4019         if (BrowserApp.selectedTab == this) {
  4020           if (ExternalApps.shouldCheckUri(uri)) {
  4021             ExternalApps.updatePageAction(uri);
  4022           } else {
  4023             ExternalApps.clearPageAction();
  4027         if (!Reader.isEnabledForParseOnLoad)
  4028           return;
  4030         // Once document is fully loaded, parse it
  4031         Reader.parseDocumentFromTab(this.id, function (article) {
  4032           // Do nothing if there's no article or the page in this tab has
  4033           // changed
  4034           let tabURL = uri.specIgnoringRef;
  4035           if (article == null || (article.url != tabURL)) {
  4036             // Don't clear the article for about:reader pages since we want to
  4037             // use the article from the previous page
  4038             if (!tabURL.startsWith("about:reader")) {
  4039               this.savedArticle = null;
  4040               this.readerEnabled = false;
  4041               this.readerActive = false;
  4042             } else {
  4043               this.readerActive = true;
  4045             return;
  4048           this.savedArticle = article;
  4050           sendMessageToJava({
  4051             type: "Content:ReaderEnabled",
  4052             tabID: this.id
  4053           });
  4055           if(this.readerActive)
  4056             this.readerActive = false;
  4058           if(!this.readerEnabled)
  4059             this.readerEnabled = true;
  4060         }.bind(this));
  4063   },
  4065   onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
  4066     let contentWin = aWebProgress.DOMWindow;
  4067     if (contentWin != contentWin.top)
  4068         return;
  4070     // Filter optimization: Only really send NETWORK state changes to Java listener
  4071     if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
  4072       if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) {
  4073         // We may receive a document stop event while a document is still loading
  4074         // (such as when doing URI fixup). Don't notify Java UI in these cases.
  4075         return;
  4078       // Clear page-specific opensearch engines and feeds for a new request.
  4079       if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) {
  4080         this.browser.engines = null;
  4081         this.browser.feeds = null;
  4084       // true if the page loaded successfully (i.e., no 404s or other errors)
  4085       let success = false;
  4086       let uri = "";
  4087       try {
  4088         // Remember original URI for UA changes on redirected pages
  4089         this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI;
  4091         if (this.originalURI != null)
  4092           uri = this.originalURI.spec;
  4093       } catch (e) { }
  4094       try {
  4095         success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded;
  4096       } catch (e) {
  4097         // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success
  4098         // status. Used for local files. See bug 948849.
  4099         success = aRequest.status == 0;
  4102       // Check to see if we restoring the content from a previous presentation (session)
  4103       // since there should be no real network activity
  4104       let restoring = (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) > 0;
  4106       let message = {
  4107         type: "Content:StateChange",
  4108         tabID: this.id,
  4109         uri: uri,
  4110         state: aStateFlags,
  4111         restoring: restoring,
  4112         success: success
  4113       };
  4114       sendMessageToJava(message);
  4116   },
  4118   onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
  4119     let contentWin = aWebProgress.DOMWindow;
  4121     // Browser webapps may load content inside iframes that can not reach across the app/frame boundary
  4122     // i.e. even though the page is loaded in an iframe window.top != webapp
  4123     // Make cure this window is a top level tab before moving on.
  4124     if (BrowserApp.getBrowserForWindow(contentWin) == null)
  4125       return;
  4127     this._hostChanged = true;
  4129     let fixedURI = aLocationURI;
  4130     try {
  4131       fixedURI = URIFixup.createExposableURI(aLocationURI);
  4132     } catch (ex) { }
  4134     let contentType = contentWin.document.contentType;
  4136     // If fixedURI matches browser.lastURI, we assume this isn't a real location
  4137     // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883.
  4138     // Note that we have to ensure fixedURI is not the same as aLocationURI so we
  4139     // don't false-positive page reloads as spurious additions.
  4140     let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 ||
  4141                        ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI));
  4142     this.browser.lastURI = fixedURI;
  4144     // Reset state of click-to-play plugin notifications.
  4145     clearTimeout(this.pluginDoorhangerTimeout);
  4146     this.pluginDoorhangerTimeout = null;
  4147     this.shouldShowPluginDoorhanger = true;
  4148     this.clickToPlayPluginsActivated = false;
  4149     // Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#174
  4150     let documentURI = contentWin.document.documentURIObject.spec
  4151     let matchedURL = documentURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
  4152     let baseDomain = "";
  4153     if (matchedURL) {
  4154       var domain = "";
  4155       [, , domain] = matchedURL;
  4157       try {
  4158         baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
  4159         if (!domain.endsWith(baseDomain)) {
  4160           // getBaseDomainFromHost converts its resultant to ACE.
  4161           let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
  4162           baseDomain = IDNService.convertACEtoUTF8(baseDomain);
  4164       } catch (e) {}
  4167     // Update the page actions URI for helper apps.
  4168     if (BrowserApp.selectedTab == this) {
  4169       ExternalApps.updatePageActionUri(fixedURI);
  4172     let message = {
  4173       type: "Content:LocationChange",
  4174       tabID: this.id,
  4175       uri: fixedURI.spec,
  4176       userSearch: this.userSearch || "",
  4177       baseDomain: baseDomain,
  4178       contentType: (contentType ? contentType : ""),
  4179       sameDocument: sameDocument
  4180     };
  4182     sendMessageToJava(message);
  4184     // The search term is only valid for this location change event, so reset it here.
  4185     this.userSearch = "";
  4187     if (!sameDocument) {
  4188       // XXX This code assumes that this is the earliest hook we have at which
  4189       // browser.contentDocument is changed to the new document we're loading
  4190       this.contentDocumentIsDisplayed = false;
  4191       this.hasTouchListener = false;
  4192     } else {
  4193       this.sendViewportUpdate();
  4195   },
  4197   // Properties used to cache security state used to update the UI
  4198   _state: null,
  4199   _hostChanged: false, // onLocationChange will flip this bit
  4201   onSecurityChange: function(aWebProgress, aRequest, aState) {
  4202     // Don't need to do anything if the data we use to update the UI hasn't changed
  4203     if (this._state == aState && !this._hostChanged)
  4204       return;
  4206     this._state = aState;
  4207     this._hostChanged = false;
  4209     let identity = IdentityHandler.checkIdentity(aState, this.browser);
  4211     let message = {
  4212       type: "Content:SecurityChange",
  4213       tabID: this.id,
  4214       identity: identity
  4215     };
  4217     sendMessageToJava(message);
  4218   },
  4220   onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
  4221   },
  4223   onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
  4224   },
  4226   _sendHistoryEvent: function(aMessage, aParams) {
  4227     let message = {
  4228       type: "SessionHistory:" + aMessage,
  4229       tabID: this.id,
  4230     };
  4232     // Restore zoom only when moving in session history, not for new page loads.
  4233     this._restoreZoom = aMessage != "New";
  4235     if (aParams) {
  4236       if ("url" in aParams)
  4237         message.url = aParams.url;
  4238       if ("index" in aParams)
  4239         message.index = aParams.index;
  4240       if ("numEntries" in aParams)
  4241         message.numEntries = aParams.numEntries;
  4244     sendMessageToJava(message);
  4245   },
  4247   _getGeckoZoom: function() {
  4248     let res = {x: {}, y: {}};
  4249     let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  4250     cwu.getResolution(res.x, res.y);
  4251     let zoom = res.x.value * window.devicePixelRatio;
  4252     return zoom;
  4253   },
  4255   saveSessionZoom: function(aZoom) {
  4256     let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  4257     cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio);
  4258   },
  4260   restoredSessionZoom: function() {
  4261     let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  4263     if (this._restoreZoom && cwu.isResolutionSet) {
  4264       return this._getGeckoZoom();
  4266     return null;
  4267   },
  4269   OnHistoryNewEntry: function(aUri) {
  4270     this._sendHistoryEvent("New", { url: aUri.spec });
  4271   },
  4273   OnHistoryGoBack: function(aUri) {
  4274     this._sendHistoryEvent("Back");
  4275     return true;
  4276   },
  4278   OnHistoryGoForward: function(aUri) {
  4279     this._sendHistoryEvent("Forward");
  4280     return true;
  4281   },
  4283   OnHistoryReload: function(aUri, aFlags) {
  4284     // we don't do anything with this, so don't propagate it
  4285     // for now anyway
  4286     return true;
  4287   },
  4289   OnHistoryGotoIndex: function(aIndex, aUri) {
  4290     this._sendHistoryEvent("Goto", { index: aIndex });
  4291     return true;
  4292   },
  4294   OnHistoryPurge: function(aNumEntries) {
  4295     this._sendHistoryEvent("Purge", { numEntries: aNumEntries });
  4296     return true;
  4297   },
  4299   OnHistoryReplaceEntry: function(aIndex) {
  4300     // we don't do anything with this, so don't propogate it
  4301     // for now anyway.
  4302   },
  4304   get metadata() {
  4305     return ViewportHandler.getMetadataForDocument(this.browser.contentDocument);
  4306   },
  4308   /** Update viewport when the metadata changes. */
  4309   updateViewportMetadata: function updateViewportMetadata(aMetadata, aInitialLoad) {
  4310     if (Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) {
  4311       aMetadata.allowZoom = true;
  4312       aMetadata.allowDoubleTapZoom = true;
  4313       aMetadata.minZoom = aMetadata.maxZoom = NaN;
  4316     let scaleRatio = window.devicePixelRatio;
  4318     if (aMetadata.defaultZoom > 0)
  4319       aMetadata.defaultZoom *= scaleRatio;
  4320     if (aMetadata.minZoom > 0)
  4321       aMetadata.minZoom *= scaleRatio;
  4322     if (aMetadata.maxZoom > 0)
  4323       aMetadata.maxZoom *= scaleRatio;
  4325     aMetadata.isRTL = this.browser.contentDocument.documentElement.dir == "rtl";
  4327     ViewportHandler.setMetadataForDocument(this.browser.contentDocument, aMetadata);
  4328     this.sendViewportMetadata();
  4330     this.updateViewportSize(gScreenWidth, aInitialLoad);
  4331   },
  4333   /** Update viewport when the metadata or the window size changes. */
  4334   updateViewportSize: function updateViewportSize(aOldScreenWidth, aInitialLoad) {
  4335     // When this function gets called on window resize, we must execute
  4336     // this.sendViewportUpdate() so that refreshDisplayPort is called.
  4337     // Ensure that when making changes to this function that code path
  4338     // is not accidentally removed (the call to sendViewportUpdate() is
  4339     // at the very end).
  4341     if (this.viewportMeasureCallback) {
  4342       clearTimeout(this.viewportMeasureCallback);
  4343       this.viewportMeasureCallback = null;
  4346     let browser = this.browser;
  4347     if (!browser)
  4348       return;
  4350     let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
  4351     let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
  4352     let viewportW, viewportH;
  4354     let metadata = this.metadata;
  4355     if (metadata.autoSize) {
  4356       viewportW = screenW / window.devicePixelRatio;
  4357       viewportH = screenH / window.devicePixelRatio;
  4358     } else {
  4359       viewportW = metadata.width;
  4360       viewportH = metadata.height;
  4362       // If (scale * width) < device-width, increase the width (bug 561413).
  4363       let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom;
  4364       if (maxInitialZoom && viewportW) {
  4365         viewportW = Math.max(viewportW, screenW / maxInitialZoom);
  4368       let validW = viewportW > 0;
  4369       let validH = viewportH > 0;
  4371       if (!validW)
  4372         viewportW = validH ? (viewportH * (screenW / screenH)) : BrowserApp.defaultBrowserWidth;
  4373       if (!validH)
  4374         viewportH = viewportW * (screenH / screenW);
  4377     // Make sure the viewport height is not shorter than the window when
  4378     // the page is zoomed out to show its full width. Note that before
  4379     // we set the viewport width, the "full width" of the page isn't properly
  4380     // defined, so that's why we have to call setBrowserSize twice - once
  4381     // to set the width, and the second time to figure out the height based
  4382     // on the layout at that width.
  4383     let oldBrowserWidth = this.browserWidth;
  4384     this.setBrowserSize(viewportW, viewportH);
  4386     // This change to the zoom accounts for all types of changes I can conceive:
  4387     // 1. screen size changes, CSS viewport does not (pages with no meta viewport
  4388     //    or a fixed size viewport)
  4389     // 2. screen size changes, CSS viewport also does (pages with a device-width
  4390     //    viewport)
  4391     // 3. screen size remains constant, but CSS viewport changes (meta viewport
  4392     //    tag is added or removed)
  4393     // 4. neither screen size nor CSS viewport changes
  4394     //
  4395     // In all of these cases, we maintain how much actual content is visible
  4396     // within the screen width. Note that "actual content" may be different
  4397     // with respect to CSS pixels because of the CSS viewport size changing.
  4398     let zoom = this.restoredSessionZoom() || metadata.defaultZoom;
  4399     if (!zoom || !aInitialLoad) {
  4400       let zoomScale = (screenW * oldBrowserWidth) / (aOldScreenWidth * viewportW);
  4401       zoom = this.clampZoom(this._zoom * zoomScale);
  4403     this.setResolution(zoom, false);
  4404     this.setScrollClampingSize(zoom);
  4406     // if this page has not been painted yet, then this must be getting run
  4407     // because a meta-viewport element was added (via the DOMMetaAdded handler).
  4408     // in this case, we should not do anything that forces a reflow (see bug 759678)
  4409     // such as requesting the page size or sending a viewport update. this code
  4410     // will get run again in the before-first-paint handler and that point we
  4411     // will run though all of it. the reason we even bother executing up to this
  4412     // point on the DOMMetaAdded handler is so that scripts that use window.innerWidth
  4413     // before they are painted have a correct value (bug 771575).
  4414     if (!this.contentDocumentIsDisplayed) {
  4415       return;
  4418     this.viewportExcludesHorizontalMargins = true;
  4419     this.viewportExcludesVerticalMargins = true;
  4420     let minScale = 1.0;
  4421     if (this.browser.contentDocument) {
  4422       // this may get run during a Viewport:Change message while the document
  4423       // has not yet loaded, so need to guard against a null document.
  4424       let [pageWidth, pageHeight] = this.getPageSize(this.browser.contentDocument, viewportW, viewportH);
  4426       // In the situation the page size equals or exceeds the screen size,
  4427       // lengthen the viewport on the corresponding axis to include the margins.
  4428       // The '- 0.5' is to account for rounding errors.
  4429       if (pageWidth * this._zoom > gScreenWidth - 0.5) {
  4430         screenW = gScreenWidth;
  4431         this.viewportExcludesHorizontalMargins = false;
  4433       if (pageHeight * this._zoom > gScreenHeight - 0.5) {
  4434         screenH = gScreenHeight;
  4435         this.viewportExcludesVerticalMargins = false;
  4438       minScale = screenW / pageWidth;
  4440     minScale = this.clampZoom(minScale);
  4441     viewportH = Math.max(viewportH, screenH / minScale);
  4443     // In general we want to keep calls to setBrowserSize and setScrollClampingSize
  4444     // together because setBrowserSize could mark the viewport size as dirty, creating
  4445     // a pending resize event for content. If that resize gets dispatched (which happens
  4446     // on the next reflow) without setScrollClampingSize having being called, then
  4447     // content might be exposed to incorrect innerWidth/innerHeight values.
  4448     this.setBrowserSize(viewportW, viewportH);
  4449     this.setScrollClampingSize(zoom);
  4451     // Avoid having the scroll position jump around after device rotation.
  4452     let win = this.browser.contentWindow;
  4453     this.userScrollPos.x = win.scrollX;
  4454     this.userScrollPos.y = win.scrollY;
  4456     this.sendViewportUpdate();
  4458     if (metadata.allowZoom && !Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) {
  4459       // If the CSS viewport is narrower than the screen (i.e. width <= device-width)
  4460       // then we disable double-tap-to-zoom behaviour.
  4461       var oldAllowDoubleTapZoom = metadata.allowDoubleTapZoom;
  4462       var newAllowDoubleTapZoom = (!metadata.isSpecified) || (viewportW > screenW / window.devicePixelRatio);
  4463       if (oldAllowDoubleTapZoom !== newAllowDoubleTapZoom) {
  4464         metadata.allowDoubleTapZoom = newAllowDoubleTapZoom;
  4465         this.sendViewportMetadata();
  4469     // Store the page size that was used to calculate the viewport so that we
  4470     // can verify it's changed when we consider remeasuring in updateViewportForPageSize
  4471     let viewport = this.getViewport();
  4472     this.lastPageSizeAfterViewportRemeasure = {
  4473       width: viewport.pageRight - viewport.pageLeft,
  4474       height: viewport.pageBottom - viewport.pageTop
  4475     };
  4476   },
  4478   sendViewportMetadata: function sendViewportMetadata() {
  4479     let metadata = this.metadata;
  4480     sendMessageToJava({
  4481       type: "Tab:ViewportMetadata",
  4482       allowZoom: metadata.allowZoom,
  4483       allowDoubleTapZoom: metadata.allowDoubleTapZoom,
  4484       defaultZoom: metadata.defaultZoom || window.devicePixelRatio,
  4485       minZoom: metadata.minZoom || 0,
  4486       maxZoom: metadata.maxZoom || 0,
  4487       isRTL: metadata.isRTL,
  4488       tabID: this.id
  4489     });
  4490   },
  4492   setBrowserSize: function(aWidth, aHeight) {
  4493     if (fuzzyEquals(this.browserWidth, aWidth) && fuzzyEquals(this.browserHeight, aHeight)) {
  4494       return;
  4497     this.browserWidth = aWidth;
  4498     this.browserHeight = aHeight;
  4500     if (!this.browser.contentWindow)
  4501       return;
  4502     let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  4503     cwu.setCSSViewport(aWidth, aHeight);
  4504   },
  4506   /** Takes a scale and restricts it based on this tab's zoom limits. */
  4507   clampZoom: function clampZoom(aZoom) {
  4508     let zoom = ViewportHandler.clamp(aZoom, kViewportMinScale, kViewportMaxScale);
  4510     let md = this.metadata;
  4511     if (!md.allowZoom)
  4512       return md.defaultZoom || zoom;
  4514     if (md && md.minZoom)
  4515       zoom = Math.max(zoom, md.minZoom);
  4516     if (md && md.maxZoom)
  4517       zoom = Math.min(zoom, md.maxZoom);
  4518     return zoom;
  4519   },
  4521   observe: function(aSubject, aTopic, aData) {
  4522     switch (aTopic) {
  4523       case "before-first-paint":
  4524         // Is it on the top level?
  4525         let contentDocument = aSubject;
  4526         if (contentDocument == this.browser.contentDocument) {
  4527           if (BrowserApp.selectedTab == this) {
  4528             BrowserApp.contentDocumentChanged();
  4530           this.contentDocumentIsDisplayed = true;
  4532           // reset CSS viewport and zoom to default on new page, and then calculate
  4533           // them properly using the actual metadata from the page. note that the
  4534           // updateMetadata call takes into account the existing CSS viewport size
  4535           // and zoom when calculating the new ones, so we need to reset these
  4536           // things here before calling updateMetadata.
  4537           this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
  4538           let zoom = this.restoredSessionZoom() || gScreenWidth / this.browserWidth;
  4539           this.setResolution(zoom, true);
  4540           ViewportHandler.updateMetadata(this, true);
  4542           // Note that if we draw without a display-port, things can go wrong. By the
  4543           // time we execute this, it's almost certain a display-port has been set via
  4544           // the MozScrolledAreaChanged event. If that didn't happen, the updateMetadata
  4545           // call above does so at the end of the updateViewportSize function. As long
  4546           // as that is happening, we don't need to do it again here.
  4548           if (!this.restoredSessionZoom() && contentDocument.mozSyntheticDocument) {
  4549             // for images, scale to fit width. this needs to happen *after* the call
  4550             // to updateMetadata above, because that call sets the CSS viewport which
  4551             // will affect the page size (i.e. contentDocument.body.scroll*) that we
  4552             // use in this calculation. also we call sendViewportUpdate after changing
  4553             // the resolution so that the display port gets recalculated appropriately.
  4554             let fitZoom = Math.min(gScreenWidth / contentDocument.body.scrollWidth,
  4555                                    gScreenHeight / contentDocument.body.scrollHeight);
  4556             this.setResolution(fitZoom, false);
  4557             this.sendViewportUpdate();
  4561         // If the reflow-text-on-page-load pref is enabled, and reflow-on-zoom
  4562         // is enabled, and our defaultZoom level is set, then we need to get
  4563         // the default zoom and reflow the text according to the defaultZoom
  4564         // level.
  4565         let rzEnabled = BrowserEventHandler.mReflozPref;
  4566         let rzPl = Services.prefs.getBoolPref("browser.zoom.reflowZoom.reflowTextOnPageLoad");
  4568         if (rzEnabled && rzPl) {
  4569           // Retrieve the viewport width and adjust the max line box width
  4570           // accordingly.
  4571           let vp = BrowserApp.selectedTab.getViewport();
  4572           BrowserApp.selectedTab.performReflowOnZoom(vp);
  4574         break;
  4575       case "after-viewport-change":
  4576         if (BrowserApp.selectedTab._mReflozPositioned) {
  4577           BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
  4579         break;
  4580       case "nsPref:changed":
  4581         if (aData == "browser.ui.zoom.force-user-scalable")
  4582           ViewportHandler.updateMetadata(this, false);
  4583         break;
  4585   },
  4587   set readerEnabled(isReaderEnabled) {
  4588     this._readerEnabled = isReaderEnabled;
  4589     if (this.getActive())
  4590       Reader.updatePageAction(this);
  4591   },
  4593   get readerEnabled() {
  4594     return this._readerEnabled;
  4595   },
  4597   set readerActive(isReaderActive) {
  4598     this._readerActive = isReaderActive;
  4599     if (this.getActive())
  4600       Reader.updatePageAction(this);
  4601   },
  4603   get readerActive() {
  4604     return this._readerActive;
  4605   },
  4607   // nsIBrowserTab
  4608   get window() {
  4609     if (!this.browser)
  4610       return null;
  4611     return this.browser.contentWindow;
  4612   },
  4614   get scale() {
  4615     return this._zoom;
  4616   },
  4618   QueryInterface: XPCOMUtils.generateQI([
  4619     Ci.nsIWebProgressListener,
  4620     Ci.nsISHistoryListener,
  4621     Ci.nsIObserver,
  4622     Ci.nsISupportsWeakReference,
  4623     Ci.nsIBrowserTab
  4624   ])
  4625 };
  4627 var BrowserEventHandler = {
  4628   init: function init() {
  4629     Services.obs.addObserver(this, "Gesture:SingleTap", false);
  4630     Services.obs.addObserver(this, "Gesture:CancelTouch", false);
  4631     Services.obs.addObserver(this, "Gesture:DoubleTap", false);
  4632     Services.obs.addObserver(this, "Gesture:Scroll", false);
  4633     Services.obs.addObserver(this, "dom-touch-listener-added", false);
  4635     BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false);
  4636     BrowserApp.deck.addEventListener("touchstart", this, true);
  4637     BrowserApp.deck.addEventListener("click", InputWidgetHelper, true);
  4638     BrowserApp.deck.addEventListener("click", SelectHelper, true);
  4640     SpatialNavigation.init(BrowserApp.deck, null);
  4642     document.addEventListener("MozMagnifyGesture", this, true);
  4644     Services.prefs.addObserver("browser.zoom.reflowOnZoom", this, false);
  4645     this.updateReflozPref();
  4646   },
  4648   resetMaxLineBoxWidth: function() {
  4649     BrowserApp.selectedTab.probablyNeedRefloz = false;
  4651     if (gReflowPending) {
  4652       clearTimeout(gReflowPending);
  4655     let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
  4656     gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
  4657                                 reflozTimeout, 0);
  4658   },
  4660   updateReflozPref: function() {
  4661      this.mReflozPref = Services.prefs.getBoolPref("browser.zoom.reflowOnZoom");
  4662   },
  4664   handleEvent: function(aEvent) {
  4665     switch (aEvent.type) {
  4666       case 'touchstart':
  4667         this._handleTouchStart(aEvent);
  4668         break;
  4669       case 'MozMagnifyGesture':
  4670         this.observe(this, aEvent.type,
  4671                      JSON.stringify({x: aEvent.screenX, y: aEvent.screenY,
  4672                                      zoomDelta: aEvent.delta}));
  4673         break;
  4675   },
  4677   _handleTouchStart: function(aEvent) {
  4678     if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.touches.length > 1 || aEvent.defaultPrevented)
  4679       return;
  4681     let closest = aEvent.target;
  4683     if (closest) {
  4684       // If we've pressed a scrollable element, let Java know that we may
  4685       // want to override the scroll behaviour (for document sub-frames)
  4686       this._scrollableElement = this._findScrollableElement(closest, true);
  4687       this._firstScrollEvent = true;
  4689       if (this._scrollableElement != null) {
  4690         // Discard if it's the top-level scrollable, we let Java handle this
  4691         // The top-level scrollable is the body in quirks mode and the html element
  4692         // in standards mode
  4693         let doc = BrowserApp.selectedBrowser.contentDocument;
  4694         let rootScrollable = (doc.compatMode === "BackCompat" ? doc.body : doc.documentElement);
  4695         if (this._scrollableElement != rootScrollable) {
  4696           sendMessageToJava({ type: "Panning:Override" });
  4701     if (!ElementTouchHelper.isElementClickable(closest, null, false))
  4702       closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX,
  4703                                                     aEvent.changedTouches[0].screenY);
  4704     if (!closest)
  4705       closest = aEvent.target;
  4707     if (closest) {
  4708       let uri = this._getLinkURI(closest);
  4709       if (uri) {
  4710         try {
  4711           Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
  4712         } catch (e) {}
  4714       this._doTapHighlight(closest);
  4716   },
  4718   _getLinkURI: function(aElement) {
  4719     if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
  4720         ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
  4721         (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) {
  4722       try {
  4723         return Services.io.newURI(aElement.href, null, null);
  4724       } catch (e) {}
  4726     return null;
  4727   },
  4729   observe: function(aSubject, aTopic, aData) {
  4730     if (aTopic == "dom-touch-listener-added") {
  4731       let tab = BrowserApp.getTabForWindow(aSubject.top);
  4732       if (!tab || tab.hasTouchListener)
  4733         return;
  4735       tab.hasTouchListener = true;
  4736       sendMessageToJava({
  4737         type: "Tab:HasTouchListener",
  4738         tabID: tab.id
  4739       });
  4740       return;
  4741     } else if (aTopic == "nsPref:changed") {
  4742       if (aData == "browser.zoom.reflowOnZoom") {
  4743         this.updateReflozPref();
  4745       return;
  4748     // the remaining events are all dependent on the browser content document being the
  4749     // same as the browser displayed document. if they are not the same, we should ignore
  4750     // the event.
  4751     if (BrowserApp.isBrowserContentDocumentDisplayed()) {
  4752       this.handleUserEvent(aTopic, aData);
  4754   },
  4756   handleUserEvent: function(aTopic, aData) {
  4757     switch (aTopic) {
  4759       case "Gesture:Scroll": {
  4760         // If we've lost our scrollable element, return. Don't cancel the
  4761         // override, as we probably don't want Java to handle panning until the
  4762         // user releases their finger.
  4763         if (this._scrollableElement == null)
  4764           return;
  4766         // If this is the first scroll event and we can't scroll in the direction
  4767         // the user wanted, and neither can any non-root sub-frame, cancel the
  4768         // override so that Java can handle panning the main document.
  4769         let data = JSON.parse(aData);
  4771         // round the scroll amounts because they come in as floats and might be
  4772         // subject to minor rounding errors because of zoom values. I've seen values
  4773         // like 0.99 come in here and get truncated to 0; this avoids that problem.
  4774         let zoom = BrowserApp.selectedTab._zoom;
  4775         let x = Math.round(data.x / zoom);
  4776         let y = Math.round(data.y / zoom);
  4778         if (this._firstScrollEvent) {
  4779           while (this._scrollableElement != null &&
  4780                  !this._elementCanScroll(this._scrollableElement, x, y))
  4781             this._scrollableElement = this._findScrollableElement(this._scrollableElement, false);
  4783           let doc = BrowserApp.selectedBrowser.contentDocument;
  4784           if (this._scrollableElement == null ||
  4785               this._scrollableElement == doc.documentElement) {
  4786             sendMessageToJava({ type: "Panning:CancelOverride" });
  4787             return;
  4790           this._firstScrollEvent = false;
  4793         // Scroll the scrollable element
  4794         if (this._elementCanScroll(this._scrollableElement, x, y)) {
  4795           this._scrollElementBy(this._scrollableElement, x, y);
  4796           sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: true });
  4797           SelectionHandler.subdocumentScrolled(this._scrollableElement);
  4798         } else {
  4799           sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: false });
  4802         break;
  4805       case "Gesture:CancelTouch":
  4806         this._cancelTapHighlight();
  4807         break;
  4809       case "Gesture:SingleTap": {
  4810         let element = this._highlightElement;
  4811         if (element) {
  4812           try {
  4813             let data = JSON.parse(aData);
  4814             let [x, y] = [data.x, data.y];
  4815             if (ElementTouchHelper.isElementClickable(element)) {
  4816               [x, y] = this._moveClickPoint(element, x, y);
  4819             // Was the element already focused before it was clicked?
  4820             let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser));
  4822             this._sendMouseEvent("mousemove", element, x, y);
  4823             this._sendMouseEvent("mousedown", element, x, y);
  4824             this._sendMouseEvent("mouseup",   element, x, y);
  4826             // If the element was previously focused, show the caret attached to it.
  4827             if (isFocused)
  4828               SelectionHandler.attachCaret(element);
  4830             // scrollToFocusedInput does its own checks to find out if an element should be zoomed into
  4831             BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser);
  4832           } catch(e) {
  4833             Cu.reportError(e);
  4836         this._cancelTapHighlight();
  4837         break;
  4840       case"Gesture:DoubleTap":
  4841         this._cancelTapHighlight();
  4842         this.onDoubleTap(aData);
  4843         break;
  4845       case "MozMagnifyGesture":
  4846         this.onPinchFinish(aData);
  4847         break;
  4849       default:
  4850         dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
  4851         break;
  4853   },
  4855   onDoubleTap: function(aData) {
  4856     let data = JSON.parse(aData);
  4857     let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y);
  4859     // We only want to do this if reflow-on-zoom is enabled, we don't already
  4860     // have a reflow-on-zoom event pending, and the element upon which the user
  4861     // double-tapped isn't of a type we want to avoid reflow-on-zoom.
  4862     if (BrowserEventHandler.mReflozPref &&
  4863        !BrowserApp.selectedTab._mReflozPoint &&
  4864        !this._shouldSuppressReflowOnZoom(element)) {
  4866       // See comment above performReflowOnZoom() for a detailed description of
  4867       // the events happening in the reflow-on-zoom operation.
  4868       let data = JSON.parse(aData);
  4869       let zoomPointX = data.x;
  4870       let zoomPointY = data.y;
  4872       BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY,
  4873         range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) };
  4875       // Before we perform a reflow on zoom, let's disable painting.
  4876       let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
  4877       let docShell = webNav.QueryInterface(Ci.nsIDocShell);
  4878       let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
  4879       docViewer.pausePainting();
  4881       BrowserApp.selectedTab.probablyNeedRefloz = true;
  4884     if (!element) {
  4885       ZoomHelper.zoomOut();
  4886       return;
  4889     while (element && !this._shouldZoomToElement(element))
  4890       element = element.parentNode;
  4892     if (!element) {
  4893       ZoomHelper.zoomOut();
  4894     } else {
  4895       ZoomHelper.zoomToElement(element, data.y);
  4897   },
  4899   /**
  4900    * Determine if reflow-on-zoom functionality should be suppressed, given a
  4901    * particular element. Double-tapping on the following elements suppresses
  4902    * reflow-on-zoom:
  4904    * <video>, <object>, <embed>, <applet>, <canvas>, <img>, <media>, <pre>
  4905    */
  4906   _shouldSuppressReflowOnZoom: function(aElement) {
  4907     if (aElement instanceof Ci.nsIDOMHTMLVideoElement ||
  4908         aElement instanceof Ci.nsIDOMHTMLObjectElement ||
  4909         aElement instanceof Ci.nsIDOMHTMLEmbedElement ||
  4910         aElement instanceof Ci.nsIDOMHTMLAppletElement ||
  4911         aElement instanceof Ci.nsIDOMHTMLCanvasElement ||
  4912         aElement instanceof Ci.nsIDOMHTMLImageElement ||
  4913         aElement instanceof Ci.nsIDOMHTMLMediaElement ||
  4914         aElement instanceof Ci.nsIDOMHTMLPreElement) {
  4915       return true;
  4918     return false;
  4919   },
  4921   onPinchFinish: function(aData) {
  4922     let data = {};
  4923     try {
  4924       data = JSON.parse(aData);
  4925     } catch(ex) {
  4926       console.log(ex);
  4927       return;
  4930     if (BrowserEventHandler.mReflozPref &&
  4931         data.zoomDelta < 0.0) {
  4932       BrowserEventHandler.resetMaxLineBoxWidth();
  4934   },
  4936   _shouldZoomToElement: function(aElement) {
  4937     let win = aElement.ownerDocument.defaultView;
  4938     if (win.getComputedStyle(aElement, null).display == "inline")
  4939       return false;
  4940     if (aElement instanceof Ci.nsIDOMHTMLLIElement)
  4941       return false;
  4942     if (aElement instanceof Ci.nsIDOMHTMLQuoteElement)
  4943       return false;
  4944     return true;
  4945   },
  4947   _firstScrollEvent: false,
  4949   _scrollableElement: null,
  4951   _highlightElement: null,
  4953   _doTapHighlight: function _doTapHighlight(aElement) {
  4954     DOMUtils.setContentState(aElement, kStateActive);
  4955     this._highlightElement = aElement;
  4956   },
  4958   _cancelTapHighlight: function _cancelTapHighlight() {
  4959     if (!this._highlightElement)
  4960       return;
  4962     // If the active element is in a sub-frame, we need to make that frame's document
  4963     // active to remove the element's active state.
  4964     if (this._highlightElement.ownerDocument != BrowserApp.selectedBrowser.contentWindow.document)
  4965       DOMUtils.setContentState(this._highlightElement.ownerDocument.documentElement, kStateActive);
  4967     DOMUtils.setContentState(BrowserApp.selectedBrowser.contentWindow.document.documentElement, kStateActive);
  4968     this._highlightElement = null;
  4969   },
  4971   _updateLastPosition: function(x, y, dx, dy) {
  4972     this.lastX = x;
  4973     this.lastY = y;
  4974     this.lastTime = Date.now();
  4976     this.motionBuffer.push({ dx: dx, dy: dy, time: this.lastTime });
  4977   },
  4979   _moveClickPoint: function(aElement, aX, aY) {
  4980     // the element can be out of the aX/aY point because of the touch radius
  4981     // if outside, we gracefully move the touch point to the edge of the element
  4982     if (!(aElement instanceof HTMLHtmlElement)) {
  4983       let isTouchClick = true;
  4984       let rects = ElementTouchHelper.getContentClientRects(aElement);
  4985       for (let i = 0; i < rects.length; i++) {
  4986         let rect = rects[i];
  4987         let inBounds =
  4988           (aX > rect.left && aX < (rect.left + rect.width)) &&
  4989           (aY > rect.top && aY < (rect.top + rect.height));
  4990         if (inBounds) {
  4991           isTouchClick = false;
  4992           break;
  4996       if (isTouchClick) {
  4997         let rect = rects[0];
  4998         // if either width or height is zero, we don't want to move the click to the edge of the element. See bug 757208
  4999         if (rect.width != 0 && rect.height != 0) {
  5000           aX = Math.min(Math.ceil(rect.left + rect.width) - 1, Math.max(Math.ceil(rect.left), aX));
  5001           aY = Math.min(Math.ceil(rect.top + rect.height) - 1, Math.max(Math.ceil(rect.top),  aY));
  5005     return [aX, aY];
  5006   },
  5008   _sendMouseEvent: function _sendMouseEvent(aName, aElement, aX, aY) {
  5009     let window = aElement.ownerDocument.defaultView;
  5010     try {
  5011       let cwu = window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  5012       cwu.sendMouseEventToWindow(aName, aX, aY, 0, 1, 0, true);
  5013     } catch(e) {
  5014       Cu.reportError(e);
  5016   },
  5018   _hasScrollableOverflow: function(elem) {
  5019     var win = elem.ownerDocument.defaultView;
  5020     if (!win)
  5021       return false;
  5022     var computedStyle = win.getComputedStyle(elem);
  5023     if (!computedStyle)
  5024       return false;
  5025     // We check for overflow:hidden only because all the other cases are scrollable
  5026     // under various conditions. See https://bugzilla.mozilla.org/show_bug.cgi?id=911574#c24
  5027     // for some more details.
  5028     return !(computedStyle.overflowX == 'hidden' && computedStyle.overflowY == 'hidden');
  5029   },
  5031   _findScrollableElement: function(elem, checkElem) {
  5032     // Walk the DOM tree until we find a scrollable element
  5033     let scrollable = false;
  5034     while (elem) {
  5035       /* Element is scrollable if its scroll-size exceeds its client size, and:
  5036        * - It has overflow other than 'hidden', or
  5037        * - It's a textarea node, or
  5038        * - It's a text input, or
  5039        * - It's a select element showing multiple rows
  5040        */
  5041       if (checkElem) {
  5042         if ((elem.scrollTopMax > 0 || elem.scrollLeftMax > 0) &&
  5043             (this._hasScrollableOverflow(elem) ||
  5044              elem.mozMatchesSelector("textarea")) ||
  5045             (elem instanceof HTMLInputElement && elem.mozIsTextField(false)) ||
  5046             (elem instanceof HTMLSelectElement && (elem.size > 1 || elem.multiple))) {
  5047           scrollable = true;
  5048           break;
  5050       } else {
  5051         checkElem = true;
  5054       // Propagate up iFrames
  5055       if (!elem.parentNode && elem.documentElement && elem.documentElement.ownerDocument)
  5056         elem = elem.documentElement.ownerDocument.defaultView.frameElement;
  5057       else
  5058         elem = elem.parentNode;
  5061     if (!scrollable)
  5062       return null;
  5064     return elem;
  5065   },
  5067   _scrollElementBy: function(elem, x, y) {
  5068     elem.scrollTop = elem.scrollTop + y;
  5069     elem.scrollLeft = elem.scrollLeft + x;
  5070   },
  5072   _elementCanScroll: function(elem, x, y) {
  5073     let scrollX = (x < 0 && elem.scrollLeft > 0)
  5074                || (x > 0 && elem.scrollLeft < elem.scrollLeftMax);
  5076     let scrollY = (y < 0 && elem.scrollTop > 0)
  5077                || (y > 0 && elem.scrollTop < elem.scrollTopMax);
  5079     return scrollX || scrollY;
  5081 };
  5083 const kReferenceDpi = 240; // standard "pixel" size used in some preferences
  5085 const ElementTouchHelper = {
  5086   /* Return the element at the given coordinates, starting from the given window and
  5087      drilling down through frames. If no window is provided, the top-level window of
  5088      the currently selected tab is used. The coordinates provided should be CSS pixels
  5089      relative to the window's scroll position. */
  5090   anyElementFromPoint: function(aX, aY, aWindow) {
  5091     let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow);
  5092     let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  5093     let elem = cwu.elementFromPoint(aX, aY, true, true);
  5095     while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
  5096       let rect = elem.getBoundingClientRect();
  5097       aX -= rect.left;
  5098       aY -= rect.top;
  5099       cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  5100       elem = cwu.elementFromPoint(aX, aY, true, true);
  5103     return elem;
  5104   },
  5106   /* Return the most appropriate clickable element (if any), starting from the given window
  5107      and drilling down through iframes as necessary. If no window is provided, the top-level
  5108      window of the currently selected tab is used. The coordinates provided should be CSS
  5109      pixels relative to the window's scroll position. The element returned may not actually
  5110      contain the coordinates passed in because of touch radius and clickability heuristics. */
  5111   elementFromPoint: function(aX, aY, aWindow) {
  5112     // browser's elementFromPoint expect browser-relative client coordinates.
  5113     // subtract browser's scroll values to adjust
  5114     let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow);
  5115     let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  5116     let elem = this.getClosest(cwu, aX, aY);
  5118     // step through layers of IFRAMEs and FRAMES to find innermost element
  5119     while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
  5120       // adjust client coordinates' origin to be top left of iframe viewport
  5121       let rect = elem.getBoundingClientRect();
  5122       aX -= rect.left;
  5123       aY -= rect.top;
  5124       cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  5125       elem = this.getClosest(cwu, aX, aY);
  5128     return elem;
  5129   },
  5131   /* Returns the touch radius in content px. */
  5132   getTouchRadius: function getTouchRadius() {
  5133     let dpiRatio = ViewportHandler.displayDPI / kReferenceDpi;
  5134     let zoom = BrowserApp.selectedTab._zoom;
  5135     return {
  5136       top: this.radius.top * dpiRatio / zoom,
  5137       right: this.radius.right * dpiRatio / zoom,
  5138       bottom: this.radius.bottom * dpiRatio / zoom,
  5139       left: this.radius.left * dpiRatio / zoom
  5140     };
  5141   },
  5143   /* Returns the touch radius in reference pixels. */
  5144   get radius() {
  5145     let prefs = Services.prefs;
  5146     delete this.radius;
  5147     return this.radius = { "top": prefs.getIntPref("browser.ui.touch.top"),
  5148                            "right": prefs.getIntPref("browser.ui.touch.right"),
  5149                            "bottom": prefs.getIntPref("browser.ui.touch.bottom"),
  5150                            "left": prefs.getIntPref("browser.ui.touch.left")
  5151                          };
  5152   },
  5154   get weight() {
  5155     delete this.weight;
  5156     return this.weight = { "visited": Services.prefs.getIntPref("browser.ui.touch.weight.visited") };
  5157   },
  5159   /* Retrieve the closest element to a point by looking at borders position */
  5160   getClosest: function getClosest(aWindowUtils, aX, aY) {
  5161     let target = aWindowUtils.elementFromPoint(aX, aY,
  5162                                                true,   /* ignore root scroll frame*/
  5163                                                false); /* don't flush layout */
  5165     // if this element is clickable we return quickly. also, if it isn't,
  5166     // use a cache to speed up future calls to isElementClickable in the
  5167     // loop below.
  5168     let unclickableCache = new Array();
  5169     if (this.isElementClickable(target, unclickableCache, false))
  5170       return target;
  5172     target = null;
  5173     let radius = this.getTouchRadius();
  5174     let nodes = aWindowUtils.nodesFromRect(aX, aY, radius.top, radius.right, radius.bottom, radius.left, true, false);
  5176     let threshold = Number.POSITIVE_INFINITY;
  5177     for (let i = 0; i < nodes.length; i++) {
  5178       let current = nodes[i];
  5179       if (!current.mozMatchesSelector || !this.isElementClickable(current, unclickableCache, true))
  5180         continue;
  5182       let rect = current.getBoundingClientRect();
  5183       let distance = this._computeDistanceFromRect(aX, aY, rect);
  5185       // increase a little bit the weight for already visited items
  5186       if (current && current.mozMatchesSelector("*:visited"))
  5187         distance *= (this.weight.visited / 100);
  5189       if (distance < threshold) {
  5190         target = current;
  5191         threshold = distance;
  5195     return target;
  5196   },
  5198   isElementClickable: function isElementClickable(aElement, aUnclickableCache, aAllowBodyListeners) {
  5199     const selector = "a,:link,:visited,[role=button],button,input,select,textarea";
  5201     let stopNode = null;
  5202     if (!aAllowBodyListeners && aElement && aElement.ownerDocument)
  5203       stopNode = aElement.ownerDocument.body;
  5205     for (let elem = aElement; elem && elem != stopNode; elem = elem.parentNode) {
  5206       if (aUnclickableCache && aUnclickableCache.indexOf(elem) != -1)
  5207         continue;
  5208       if (this._hasMouseListener(elem))
  5209         return true;
  5210       if (elem.mozMatchesSelector && elem.mozMatchesSelector(selector))
  5211         return true;
  5212       if (elem instanceof HTMLLabelElement && elem.control != null)
  5213         return true;
  5214       if (aUnclickableCache)
  5215         aUnclickableCache.push(elem);
  5217     return false;
  5218   },
  5220   _computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) {
  5221     let x = 0, y = 0;
  5222     let xmost = aRect.left + aRect.width;
  5223     let ymost = aRect.top + aRect.height;
  5225     // compute horizontal distance from left/right border depending if X is
  5226     // before/inside/after the element's rectangle
  5227     if (aRect.left < aX && aX < xmost)
  5228       x = Math.min(xmost - aX, aX - aRect.left);
  5229     else if (aX < aRect.left)
  5230       x = aRect.left - aX;
  5231     else if (aX > xmost)
  5232       x = aX - xmost;
  5234     // compute vertical distance from top/bottom border depending if Y is
  5235     // above/inside/below the element's rectangle
  5236     if (aRect.top < aY && aY < ymost)
  5237       y = Math.min(ymost - aY, aY - aRect.top);
  5238     else if (aY < aRect.top)
  5239       y = aRect.top - aY;
  5240     if (aY > ymost)
  5241       y = aY - ymost;
  5243     return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
  5244   },
  5246   _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
  5247   _clickableEvents: ["mousedown", "mouseup", "click"],
  5248   _hasMouseListener: function _hasMouseListener(aElement) {
  5249     let els = this._els;
  5250     let listeners = els.getListenerInfoFor(aElement, {});
  5251     for (let i = 0; i < listeners.length; i++) {
  5252       if (this._clickableEvents.indexOf(listeners[i].type) != -1)
  5253         return true;
  5255     return false;
  5256   },
  5258   getContentClientRects: function(aElement) {
  5259     let offset = { x: 0, y: 0 };
  5261     let nativeRects = aElement.getClientRects();
  5262     // step out of iframes and frames, offsetting scroll values
  5263     for (let frame = aElement.ownerDocument.defaultView; frame.frameElement; frame = frame.parent) {
  5264       // adjust client coordinates' origin to be top left of iframe viewport
  5265       let rect = frame.frameElement.getBoundingClientRect();
  5266       let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
  5267       let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
  5268       offset.x += rect.left + parseInt(left);
  5269       offset.y += rect.top + parseInt(top);
  5272     let result = [];
  5273     for (let i = nativeRects.length - 1; i >= 0; i--) {
  5274       let r = nativeRects[i];
  5275       result.push({ left: r.left + offset.x,
  5276                     top: r.top + offset.y,
  5277                     width: r.width,
  5278                     height: r.height
  5279                   });
  5281     return result;
  5282   },
  5284   getBoundingContentRect: function(aElement) {
  5285     if (!aElement)
  5286       return {x: 0, y: 0, w: 0, h: 0};
  5288     let document = aElement.ownerDocument;
  5289     while (document.defaultView.frameElement)
  5290       document = document.defaultView.frameElement.ownerDocument;
  5292     let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  5293     let scrollX = {}, scrollY = {};
  5294     cwu.getScrollXY(false, scrollX, scrollY);
  5296     let r = aElement.getBoundingClientRect();
  5298     // step out of iframes and frames, offsetting scroll values
  5299     for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) {
  5300       // adjust client coordinates' origin to be top left of iframe viewport
  5301       let rect = frame.frameElement.getBoundingClientRect();
  5302       let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
  5303       let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
  5304       scrollX.value += rect.left + parseInt(left);
  5305       scrollY.value += rect.top + parseInt(top);
  5308     return {x: r.left + scrollX.value,
  5309             y: r.top + scrollY.value,
  5310             w: r.width,
  5311             h: r.height };
  5313 };
  5315 var ErrorPageEventHandler = {
  5316   handleEvent: function(aEvent) {
  5317     switch (aEvent.type) {
  5318       case "click": {
  5319         // Don't trust synthetic events
  5320         if (!aEvent.isTrusted)
  5321           return;
  5323         let target = aEvent.originalTarget;
  5324         let errorDoc = target.ownerDocument;
  5326         // If the event came from an ssl error page, it is probably either the "Add
  5327         // Exception…" or "Get me out of here!" button
  5328         if (errorDoc.documentURI.startsWith("about:certerror?e=nssBadCert")) {
  5329           let perm = errorDoc.getElementById("permanentExceptionButton");
  5330           let temp = errorDoc.getElementById("temporaryExceptionButton");
  5331           if (target == temp || target == perm) {
  5332             // Handle setting an cert exception and reloading the page
  5333             try {
  5334               // Add a new SSL exception for this URL
  5335               let uri = Services.io.newURI(errorDoc.location.href, null, null);
  5336               let sslExceptions = new SSLExceptions();
  5338               if (target == perm)
  5339                 sslExceptions.addPermanentException(uri, errorDoc.defaultView);
  5340               else
  5341                 sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
  5342             } catch (e) {
  5343               dump("Failed to set cert exception: " + e + "\n");
  5345             errorDoc.location.reload();
  5346           } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) {
  5347             errorDoc.location = "about:home";
  5349         } else if (errorDoc.documentURI.startsWith("about:blocked")) {
  5350           // The event came from a button on a malware/phishing block page
  5351           // First check whether it's malware or phishing, so that we can
  5352           // use the right strings/links
  5353           let isMalware = errorDoc.documentURI.contains("e=malwareBlocked");
  5354           let bucketName = isMalware ? "WARNING_MALWARE_PAGE_" : "WARNING_PHISHING_PAGE_";
  5355           let nsISecTel = Ci.nsISecurityUITelemetry;
  5356           let isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView);
  5357           bucketName += isIframe ? "TOP_" : "FRAME_";
  5359           let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
  5361           if (target == errorDoc.getElementById("getMeOutButton")) {
  5362             Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
  5363             errorDoc.location = "about:home";
  5364           } else if (target == errorDoc.getElementById("reportButton")) {
  5365             // We log even if malware/phishing info URL couldn't be found:
  5366             // the measurement is for how many users clicked the WHY BLOCKED button
  5367             Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "WHY_BLOCKED"]);
  5369             // This is the "Why is this site blocked" button.  For malware,
  5370             // we can fetch a site-specific report, for phishing, we redirect
  5371             // to the generic page describing phishing protection.
  5372             if (isMalware) {
  5373               // Get the stop badware "why is this blocked" report url, append the current url, and go there.
  5374               try {
  5375                 let reportURL = formatter.formatURLPref("browser.safebrowsing.malware.reportURL");
  5376                 reportURL += errorDoc.location.href;
  5377                 BrowserApp.selectedBrowser.loadURI(reportURL);
  5378               } catch (e) {
  5379                 Cu.reportError("Couldn't get malware report URL: " + e);
  5381             } else {
  5382               // It's a phishing site, just link to the generic information page
  5383               let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
  5384               BrowserApp.selectedBrowser.loadURI(url + "phishing-malware");
  5386           } else if (target == errorDoc.getElementById("ignoreWarningButton")) {
  5387             Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "IGNORE_WARNING"]);
  5389             // Allow users to override and continue through to the site,
  5390             let webNav = BrowserApp.selectedBrowser.docShell.QueryInterface(Ci.nsIWebNavigation);
  5391             let location = BrowserApp.selectedBrowser.contentWindow.location;
  5392             webNav.loadURI(location, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, null, null, null);
  5394             // ....but add a notify bar as a reminder, so that they don't lose
  5395             // track after, e.g., tab switching.
  5396             NativeWindow.doorhanger.show(Strings.browser.GetStringFromName("safeBrowsingDoorhanger"), "safebrowsing-warning", [], BrowserApp.selectedTab.id);
  5399         break;
  5403 };
  5405 var FormAssistant = {
  5406   QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
  5408   // Used to keep track of the element that corresponds to the current
  5409   // autocomplete suggestions
  5410   _currentInputElement: null,
  5412   _isBlocklisted: false,
  5414   // Keep track of whether or not an invalid form has been submitted
  5415   _invalidSubmit: false,
  5417   init: function() {
  5418     Services.obs.addObserver(this, "FormAssist:AutoComplete", false);
  5419     Services.obs.addObserver(this, "FormAssist:Blocklisted", false);
  5420     Services.obs.addObserver(this, "FormAssist:Hidden", false);
  5421     Services.obs.addObserver(this, "invalidformsubmit", false);
  5422     Services.obs.addObserver(this, "PanZoom:StateChange", false);
  5424     // We need to use a capturing listener for focus events
  5425     BrowserApp.deck.addEventListener("focus", this, true);
  5426     BrowserApp.deck.addEventListener("click", this, true);
  5427     BrowserApp.deck.addEventListener("input", this, false);
  5428     BrowserApp.deck.addEventListener("pageshow", this, false);
  5429   },
  5431   uninit: function() {
  5432     Services.obs.removeObserver(this, "FormAssist:AutoComplete");
  5433     Services.obs.removeObserver(this, "FormAssist:Blocklisted");
  5434     Services.obs.removeObserver(this, "FormAssist:Hidden");
  5435     Services.obs.removeObserver(this, "invalidformsubmit");
  5436     Services.obs.removeObserver(this, "PanZoom:StateChange");
  5438     BrowserApp.deck.removeEventListener("focus", this);
  5439     BrowserApp.deck.removeEventListener("click", this);
  5440     BrowserApp.deck.removeEventListener("input", this);
  5441     BrowserApp.deck.removeEventListener("pageshow", this);
  5442   },
  5444   observe: function(aSubject, aTopic, aData) {
  5445     switch (aTopic) {
  5446       case "PanZoom:StateChange":
  5447         // If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing
  5448         if (aData == "TOUCHING" || aData == "WAITING_LISTENERS")
  5449           break;
  5450         if (aData == "NOTHING") {
  5451           // only look for input elements, not contentEditable or multiline text areas
  5452           let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
  5453           if (!focused)
  5454             break;
  5456           if (this._showValidationMessage(focused))
  5457             break;
  5458           this._showAutoCompleteSuggestions(focused, function () {});
  5459         } else {
  5460           // temporarily hide the form assist popup while we're panning or zooming the page
  5461           this._hideFormAssistPopup();
  5463         break;
  5464       case "FormAssist:AutoComplete":
  5465         if (!this._currentInputElement)
  5466           break;
  5468         let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
  5470         // If we have an active composition string, commit it before sending
  5471         // the autocomplete event with the text that will replace it.
  5472         try {
  5473           let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
  5474           if (imeEditor.composing)
  5475             imeEditor.forceCompositionEnd();
  5476         } catch (e) {}
  5478         editableElement.setUserInput(aData);
  5480         let event = this._currentInputElement.ownerDocument.createEvent("Events");
  5481         event.initEvent("DOMAutoComplete", true, true);
  5482         this._currentInputElement.dispatchEvent(event);
  5483         break;
  5485       case "FormAssist:Blocklisted":
  5486         this._isBlocklisted = (aData == "true");
  5487         break;
  5489       case "FormAssist:Hidden":
  5490         this._currentInputElement = null;
  5491         break;
  5493   },
  5495   notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
  5496     if (!aInvalidElements.length)
  5497       return;
  5499     // Ignore this notificaiton if the current tab doesn't contain the invalid form
  5500     if (BrowserApp.selectedBrowser.contentDocument !=
  5501         aFormElement.ownerDocument.defaultView.top.document)
  5502       return;
  5504     this._invalidSubmit = true;
  5506     // Our focus listener will show the element's validation message
  5507     let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
  5508     currentElement.focus();
  5509   },
  5511   handleEvent: function(aEvent) {
  5512     switch (aEvent.type) {
  5513       case "focus":
  5514         let currentElement = aEvent.target;
  5516         // Only show a validation message on focus.
  5517         this._showValidationMessage(currentElement);
  5518         break;
  5520       case "click":
  5521         currentElement = aEvent.target;
  5523         // Prioritize a form validation message over autocomplete suggestions
  5524         // when the element is first focused (a form validation message will
  5525         // only be available if an invalid form was submitted)
  5526         if (this._showValidationMessage(currentElement))
  5527           break;
  5529         let checkResultsClick = hasResults => {
  5530           if (!hasResults) {
  5531             this._hideFormAssistPopup();
  5533         };
  5535         this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
  5536         break;
  5538       case "input":
  5539         currentElement = aEvent.target;
  5541         // Since we can only show one popup at a time, prioritze autocomplete
  5542         // suggestions over a form validation message
  5543         let checkResultsInput = hasResults => {
  5544           if (hasResults)
  5545             return;
  5547           if (this._showValidationMessage(currentElement))
  5548             return;
  5550           // If we're not showing autocomplete suggestions, hide the form assist popup
  5551           this._hideFormAssistPopup();
  5552         };
  5554         this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
  5555         break;
  5557       // Reset invalid submit state on each pageshow
  5558       case "pageshow":
  5559         if (!this._invalidSubmit)
  5560           return;
  5562         let selectedBrowser = BrowserApp.selectedBrowser;
  5563         if (selectedBrowser) {
  5564           let selectedDocument = selectedBrowser.contentDocument;
  5565           let target = aEvent.originalTarget;
  5566           if (target == selectedDocument || target.ownerDocument == selectedDocument)
  5567             this._invalidSubmit = false;
  5570   },
  5572   // We only want to show autocomplete suggestions for certain elements
  5573   _isAutoComplete: function _isAutoComplete(aElement) {
  5574     if (!(aElement instanceof HTMLInputElement) || aElement.readOnly ||
  5575         (aElement.getAttribute("type") == "password") ||
  5576         (aElement.hasAttribute("autocomplete") &&
  5577          aElement.getAttribute("autocomplete").toLowerCase() == "off"))
  5578       return false;
  5580     return true;
  5581   },
  5583   // Retrieves autocomplete suggestions for an element from the form autocomplete service.
  5584   // aCallback(array_of_suggestions) is called when results are available.
  5585   _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
  5586     // Cache the form autocomplete service for future use
  5587     if (!this._formAutoCompleteService)
  5588       this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"].
  5589                                       getService(Ci.nsIFormAutoComplete);
  5591     let resultsAvailable = function (results) {
  5592       let suggestions = [];
  5593       for (let i = 0; i < results.matchCount; i++) {
  5594         let value = results.getValueAt(i);
  5596         // Do not show the value if it is the current one in the input field
  5597         if (value == aSearchString)
  5598           continue;
  5600         // Supply a label and value, since they can differ for datalist suggestions
  5601         suggestions.push({ label: value, value: value });
  5603       aCallback(suggestions);
  5604     };
  5606     this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
  5607                                                           aSearchString, aElement, null,
  5608                                                           resultsAvailable);
  5609   },
  5611   /**
  5612    * (Copied from mobile/xul/chrome/content/forms.js)
  5613    * This function is similar to getListSuggestions from
  5614    * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
  5615    * used by the autocomplete.xml binding which is not in used in fennec
  5616    */
  5617   _getListSuggestions: function _getListSuggestions(aElement) {
  5618     if (!(aElement instanceof HTMLInputElement) || !aElement.list)
  5619       return [];
  5621     let suggestions = [];
  5622     let filter = !aElement.hasAttribute("mozNoFilter");
  5623     let lowerFieldValue = aElement.value.toLowerCase();
  5625     let options = aElement.list.options;
  5626     let length = options.length;
  5627     for (let i = 0; i < length; i++) {
  5628       let item = options.item(i);
  5630       let label = item.value;
  5631       if (item.label)
  5632         label = item.label;
  5633       else if (item.text)
  5634         label = item.text;
  5636       if (filter && !(label.toLowerCase().contains(lowerFieldValue)) )
  5637         continue;
  5638       suggestions.push({ label: label, value: item.value });
  5641     return suggestions;
  5642   },
  5644   // Retrieves autocomplete suggestions for an element from the form autocomplete service
  5645   // and sends the suggestions to the Java UI, along with element position data. As
  5646   // autocomplete queries are asynchronous, calls aCallback when done with a true
  5647   // argument if results were found and false if no results were found.
  5648   _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
  5649     if (!this._isAutoComplete(aElement)) {
  5650       aCallback(false);
  5651       return;
  5654     // Don't display the form auto-complete popup after the user starts typing
  5655     // to avoid confusing somes IME. See bug 758820 and bug 632744.
  5656     if (this._isBlocklisted && aElement.value.length > 0) {
  5657       aCallback(false);
  5658       return;
  5661     let resultsAvailable = autoCompleteSuggestions => {
  5662       // On desktop, we show datalist suggestions below autocomplete suggestions,
  5663       // without duplicates removed.
  5664       let listSuggestions = this._getListSuggestions(aElement);
  5665       let suggestions = autoCompleteSuggestions.concat(listSuggestions);
  5667       // Return false if there are no suggestions to show
  5668       if (!suggestions.length) {
  5669         aCallback(false);
  5670         return;
  5673       sendMessageToJava({
  5674         type:  "FormAssist:AutoComplete",
  5675         suggestions: suggestions,
  5676         rect: ElementTouchHelper.getBoundingContentRect(aElement)
  5677       });
  5679       // Keep track of input element so we can fill it in if the user
  5680       // selects an autocomplete suggestion
  5681       this._currentInputElement = aElement;
  5682       aCallback(true);
  5683     };
  5685     this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
  5686   },
  5688   // Only show a validation message if the user submitted an invalid form,
  5689   // there's a non-empty message string, and the element is the correct type
  5690   _isValidateable: function _isValidateable(aElement) {
  5691     if (!this._invalidSubmit ||
  5692         !aElement.validationMessage ||
  5693         !(aElement instanceof HTMLInputElement ||
  5694           aElement instanceof HTMLTextAreaElement ||
  5695           aElement instanceof HTMLSelectElement ||
  5696           aElement instanceof HTMLButtonElement))
  5697       return false;
  5699     return true;
  5700   },
  5702   // Sends a validation message and position data for an element to the Java UI.
  5703   // Returns true if there's a validation message to show, false otherwise.
  5704   _showValidationMessage: function _sendValidationMessage(aElement) {
  5705     if (!this._isValidateable(aElement))
  5706       return false;
  5708     sendMessageToJava({
  5709       type: "FormAssist:ValidationMessage",
  5710       validationMessage: aElement.validationMessage,
  5711       rect: ElementTouchHelper.getBoundingContentRect(aElement)
  5712     });
  5714     return true;
  5715   },
  5717   _hideFormAssistPopup: function _hideFormAssistPopup() {
  5718     sendMessageToJava({ type: "FormAssist:Hide" });
  5720 };
  5722 /**
  5723  * An object to watch for Gecko status changes -- add-on installs, pref changes
  5724  * -- and reflect them back to Java.
  5725  */
  5726 let HealthReportStatusListener = {
  5727   PREF_ACCEPT_LANG: "intl.accept_languages",
  5728   PREF_BLOCKLIST_ENABLED: "extensions.blocklist.enabled",
  5730   PREF_TELEMETRY_ENABLED:
  5731 #ifdef MOZ_TELEMETRY_REPORTING
  5732     "toolkit.telemetry.enabled",
  5733 #else
  5734     null,
  5735 #endif
  5737   init: function () {
  5738     try {
  5739       AddonManager.addAddonListener(this);
  5740     } catch (ex) {
  5741       console.log("Failed to initialize add-on status listener. FHR cannot report add-on state. " + ex);
  5744     console.log("Adding HealthReport:RequestSnapshot observer.");
  5745     Services.obs.addObserver(this, "HealthReport:RequestSnapshot", false);
  5746     Services.prefs.addObserver(this.PREF_ACCEPT_LANG, this, false);
  5747     Services.prefs.addObserver(this.PREF_BLOCKLIST_ENABLED, this, false);
  5748     if (this.PREF_TELEMETRY_ENABLED) {
  5749       Services.prefs.addObserver(this.PREF_TELEMETRY_ENABLED, this, false);
  5751   },
  5753   uninit: function () {
  5754     Services.obs.removeObserver(this, "HealthReport:RequestSnapshot");
  5755     Services.prefs.removeObserver(this.PREF_ACCEPT_LANG, this);
  5756     Services.prefs.removeObserver(this.PREF_BLOCKLIST_ENABLED, this);
  5757     if (this.PREF_TELEMETRY_ENABLED) {
  5758       Services.prefs.removeObserver(this.PREF_TELEMETRY_ENABLED, this);
  5761     AddonManager.removeAddonListener(this);
  5762   },
  5764   observe: function (aSubject, aTopic, aData) {
  5765     switch (aTopic) {
  5766       case "HealthReport:RequestSnapshot":
  5767         HealthReportStatusListener.sendSnapshotToJava();
  5768         break;
  5769       case "nsPref:changed":
  5770         let response = {
  5771           type: "Pref:Change",
  5772           pref: aData,
  5773           isUserSet: Services.prefs.prefHasUserValue(aData),
  5774         };
  5776         switch (aData) {
  5777           case this.PREF_ACCEPT_LANG:
  5778             response.value = Services.prefs.getCharPref(aData);
  5779             break;
  5780           case this.PREF_TELEMETRY_ENABLED:
  5781           case this.PREF_BLOCKLIST_ENABLED:
  5782             response.value = Services.prefs.getBoolPref(aData);
  5783             break;
  5784           default:
  5785             console.log("Unexpected pref in HealthReportStatusListener: " + aData);
  5786             return;
  5789         sendMessageToJava(response);
  5790         break;
  5792   },
  5794   MILLISECONDS_PER_DAY: 24 * 60 * 60 * 1000,
  5796   COPY_FIELDS: [
  5797     "blocklistState",
  5798     "userDisabled",
  5799     "appDisabled",
  5800     "version",
  5801     "type",
  5802     "scope",
  5803     "foreignInstall",
  5804     "hasBinaryComponents",
  5805   ],
  5807   // Add-on types for which full details are recorded in FHR.
  5808   // All other types are ignored.
  5809   FULL_DETAIL_TYPES: [
  5810     "plugin",
  5811     "extension",
  5812     "service",
  5813   ],
  5815   /**
  5816    * Return true if the add-on is not of a type for which we report full details.
  5817    * These add-ons will still make it over to Java, but will be filtered out.
  5818    */
  5819   _shouldIgnore: function (aAddon) {
  5820     return this.FULL_DETAIL_TYPES.indexOf(aAddon.type) == -1;
  5821   },
  5823   _dateToDays: function (aDate) {
  5824     return Math.floor(aDate.getTime() / this.MILLISECONDS_PER_DAY);
  5825   },
  5827   jsonForAddon: function (aAddon) {
  5828     let o = {};
  5829     if (aAddon.installDate) {
  5830       o.installDay = this._dateToDays(aAddon.installDate);
  5832     if (aAddon.updateDate) {
  5833       o.updateDay = this._dateToDays(aAddon.updateDate);
  5836     for (let field of this.COPY_FIELDS) {
  5837       o[field] = aAddon[field];
  5840     return o;
  5841   },
  5843   notifyJava: function (aAddon, aNeedsRestart, aAction="Addons:Change") {
  5844     let json = this.jsonForAddon(aAddon);
  5845     if (this._shouldIgnore(aAddon)) {
  5846       json.ignore = true;
  5848     sendMessageToJava({ type: aAction, id: aAddon.id, json: json });
  5849   },
  5851   // Add-on listeners.
  5852   onEnabling: function (aAddon, aNeedsRestart) {
  5853     this.notifyJava(aAddon, aNeedsRestart);
  5854   },
  5855   onDisabling: function (aAddon, aNeedsRestart) {
  5856     this.notifyJava(aAddon, aNeedsRestart);
  5857   },
  5858   onInstalling: function (aAddon, aNeedsRestart) {
  5859     this.notifyJava(aAddon, aNeedsRestart);
  5860   },
  5861   onUninstalling: function (aAddon, aNeedsRestart) {
  5862     this.notifyJava(aAddon, aNeedsRestart, "Addons:Uninstalling");
  5863   },
  5864   onPropertyChanged: function (aAddon, aProperties) {
  5865     this.notifyJava(aAddon);
  5866   },
  5867   onOperationCancelled: function (aAddon) {
  5868     this.notifyJava(aAddon);
  5869   },
  5871   sendSnapshotToJava: function () {
  5872     AddonManager.getAllAddons(function (aAddons) {
  5873         let jsonA = {};
  5874         if (aAddons) {
  5875           for (let i = 0; i < aAddons.length; ++i) {
  5876             let addon = aAddons[i];
  5877             try {
  5878               let addonJSON = HealthReportStatusListener.jsonForAddon(addon);
  5879               if (HealthReportStatusListener._shouldIgnore(addon)) {
  5880                 addonJSON.ignore = true;
  5882               jsonA[addon.id] = addonJSON;
  5883             } catch (e) {
  5884               // Just skip this add-on.
  5889         // Now add prefs.
  5890         let jsonP = {};
  5891         for (let pref of [this.PREF_BLOCKLIST_ENABLED, this.PREF_TELEMETRY_ENABLED]) {
  5892           if (!pref) {
  5893             // This will be the case for PREF_TELEMETRY_ENABLED in developer builds.
  5894             continue;
  5896           jsonP[pref] = {
  5897             pref: pref,
  5898             value: Services.prefs.getBoolPref(pref),
  5899             isUserSet: Services.prefs.prefHasUserValue(pref),
  5900           };
  5902         for (let pref of [this.PREF_ACCEPT_LANG]) {
  5903           jsonP[pref] = {
  5904             pref: pref,
  5905             value: Services.prefs.getCharPref(pref),
  5906             isUserSet: Services.prefs.prefHasUserValue(pref),
  5907           };
  5910         console.log("Sending snapshot message.");
  5911         sendMessageToJava({
  5912           type: "HealthReport:Snapshot",
  5913           json: {
  5914             addons: jsonA,
  5915             prefs: jsonP,
  5916           },
  5917         });
  5918       }.bind(this));
  5919   },
  5920 };
  5922 var XPInstallObserver = {
  5923   init: function xpi_init() {
  5924     Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
  5925     Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
  5927     AddonManager.addInstallListener(XPInstallObserver);
  5928   },
  5930   uninit: function xpi_uninit() {
  5931     Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
  5932     Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
  5934     AddonManager.removeInstallListener(XPInstallObserver);
  5935   },
  5937   observe: function xpi_observer(aSubject, aTopic, aData) {
  5938     switch (aTopic) {
  5939       case "addon-install-started":
  5940         NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsDownloading"), "short");
  5941         break;
  5942       case "addon-install-blocked":
  5943         let installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo);
  5944         let win = installInfo.originatingWindow;
  5945         let tab = BrowserApp.getTabForWindow(win.top);
  5946         if (!tab)
  5947           return;
  5949         let host = null;
  5950         if (installInfo.originatingURI) {
  5951           host = installInfo.originatingURI.host;
  5954         let brandShortName = Strings.brand.GetStringFromName("brandShortName");
  5955         let notificationName, buttons, message;
  5956         let strings = Strings.browser;
  5957         let enabled = true;
  5958         try {
  5959           enabled = Services.prefs.getBoolPref("xpinstall.enabled");
  5961         catch (e) {}
  5963         if (!enabled) {
  5964           notificationName = "xpinstall-disabled";
  5965           if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
  5966             message = strings.GetStringFromName("xpinstallDisabledMessageLocked");
  5967             buttons = [];
  5968           } else {
  5969             message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
  5970             buttons = [{
  5971               label: strings.GetStringFromName("xpinstallDisabledButton"),
  5972               callback: function editPrefs() {
  5973                 Services.prefs.setBoolPref("xpinstall.enabled", true);
  5974                 return false;
  5976             }];
  5978         } else {
  5979           notificationName = "xpinstall";
  5980           if (host) {
  5981             // We have a host which asked for the install.
  5982             message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
  5983           } else {
  5984             // Without a host we address the add-on as the initiator of the install.
  5985             let addon = null;
  5986             if (installInfo.installs.length > 0) {
  5987               addon = installInfo.installs[0].name;
  5989             if (addon) {
  5990               // We have an addon name, show the regular message.
  5991               message = strings.formatStringFromName("xpinstallPromptWarningLocal", [brandShortName, addon], 2);
  5992             } else {
  5993               // We don't have an addon name, show an alternative message.
  5994               message = strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1);
  5998           buttons = [{
  5999             label: strings.GetStringFromName("xpinstallPromptAllowButton"),
  6000             callback: function() {
  6001               // Kick off the install
  6002               installInfo.install();
  6003               return false;
  6005           }];
  6007         NativeWindow.doorhanger.show(message, aTopic, buttons, tab.id);
  6008         break;
  6010   },
  6012   onInstallEnded: function(aInstall, aAddon) {
  6013     let needsRestart = false;
  6014     if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
  6015       needsRestart = true;
  6016     else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
  6017       needsRestart = true;
  6019     if (needsRestart) {
  6020       this.showRestartPrompt();
  6021     } else {
  6022       // Display completion message for new installs or updates not done Automatically
  6023       if (!aInstall.existingAddon || !AddonManager.shouldAutoUpdate(aInstall.existingAddon)) {
  6024         let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart");
  6025         NativeWindow.toast.show(message, "short");
  6028   },
  6030   onInstallFailed: function(aInstall) {
  6031     NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short");
  6032   },
  6034   onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {},
  6036   onDownloadFailed: function(aInstall) {
  6037     this.onInstallFailed(aInstall);
  6038   },
  6040   onDownloadCancelled: function(aInstall) {
  6041     let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
  6042     if (!host)
  6043       host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
  6045     let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
  6046     if (aInstall.error != 0)
  6047       error += aInstall.error;
  6048     else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
  6049       error += "Blocklisted";
  6050     else if (aInstall.addon && (!aInstall.addon.isCompatible || !aInstall.addon.isPlatformCompatible))
  6051       error += "Incompatible";
  6052     else
  6053       return; // No need to show anything in this case.
  6055     let msg = Strings.browser.GetStringFromName(error);
  6056     // TODO: formatStringFromName
  6057     msg = msg.replace("#1", aInstall.name);
  6058     if (host)
  6059       msg = msg.replace("#2", host);
  6060     msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName"));
  6061     msg = msg.replace("#4", Services.appinfo.version);
  6063     NativeWindow.toast.show(msg, "short");
  6064   },
  6066   showRestartPrompt: function() {
  6067     let buttons = [{
  6068       label: Strings.browser.GetStringFromName("notificationRestart.button"),
  6069       callback: function() {
  6070         // Notify all windows that an application quit has been requested
  6071         let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
  6072         Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
  6074         // If nothing aborted, quit the app
  6075         if (cancelQuit.data == false) {
  6076           let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
  6077           appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
  6080     }];
  6082     let message = Strings.browser.GetStringFromName("notificationRestart.normal");
  6083     NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
  6084   },
  6086   hideRestartPrompt: function() {
  6087     NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
  6089 };
  6091 // Blindly copied from Safari documentation for now.
  6092 const kViewportMinScale  = 0;
  6093 const kViewportMaxScale  = 10;
  6094 const kViewportMinWidth  = 200;
  6095 const kViewportMaxWidth  = 10000;
  6096 const kViewportMinHeight = 223;
  6097 const kViewportMaxHeight = 10000;
  6099 var ViewportHandler = {
  6100   // The cached viewport metadata for each document. We tie viewport metadata to each document
  6101   // instead of to each tab so that we don't have to update it when the document changes. Using an
  6102   // ES6 weak map lets us avoid leaks.
  6103   _metadata: new WeakMap(),
  6105   init: function init() {
  6106     addEventListener("DOMMetaAdded", this, false);
  6107     Services.obs.addObserver(this, "Window:Resize", false);
  6108   },
  6110   uninit: function uninit() {
  6111     removeEventListener("DOMMetaAdded", this, false);
  6112     Services.obs.removeObserver(this, "Window:Resize");
  6113   },
  6115   handleEvent: function handleEvent(aEvent) {
  6116     switch (aEvent.type) {
  6117       case "DOMMetaAdded":
  6118         let target = aEvent.originalTarget;
  6119         if (target.name != "viewport")
  6120           break;
  6121         let document = target.ownerDocument;
  6122         let browser = BrowserApp.getBrowserForDocument(document);
  6123         let tab = BrowserApp.getTabForBrowser(browser);
  6124         if (tab)
  6125           this.updateMetadata(tab, false);
  6126         break;
  6128   },
  6130   observe: function(aSubject, aTopic, aData) {
  6131     switch (aTopic) {
  6132       case "Window:Resize":
  6133         if (window.outerWidth == gScreenWidth && window.outerHeight == gScreenHeight)
  6134           break;
  6135         if (window.outerWidth == 0 || window.outerHeight == 0)
  6136           break;
  6138         let oldScreenWidth = gScreenWidth;
  6139         gScreenWidth = window.outerWidth * window.devicePixelRatio;
  6140         gScreenHeight = window.outerHeight * window.devicePixelRatio;
  6141         let tabs = BrowserApp.tabs;
  6142         for (let i = 0; i < tabs.length; i++)
  6143           tabs[i].updateViewportSize(oldScreenWidth);
  6144         break;
  6146   },
  6148   updateMetadata: function updateMetadata(tab, aInitialLoad) {
  6149     let contentWindow = tab.browser.contentWindow;
  6150     if (contentWindow.document.documentElement) {
  6151       let metadata = this.getViewportMetadata(contentWindow);
  6152       tab.updateViewportMetadata(metadata, aInitialLoad);
  6154   },
  6156   /**
  6157    * Returns the ViewportMetadata object.
  6158    */
  6159   getViewportMetadata: function getViewportMetadata(aWindow) {
  6160     let windowUtils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  6162     // viewport details found here
  6163     // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
  6164     // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html
  6166     // Note: These values will be NaN if parseFloat or parseInt doesn't find a number.
  6167     // Remember that NaN is contagious: Math.max(1, NaN) == Math.min(1, NaN) == NaN.
  6168     let hasMetaViewport = true;
  6169     let scale = parseFloat(windowUtils.getDocumentMetadata("viewport-initial-scale"));
  6170     let minScale = parseFloat(windowUtils.getDocumentMetadata("viewport-minimum-scale"));
  6171     let maxScale = parseFloat(windowUtils.getDocumentMetadata("viewport-maximum-scale"));
  6173     let widthStr = windowUtils.getDocumentMetadata("viewport-width");
  6174     let heightStr = windowUtils.getDocumentMetadata("viewport-height");
  6175     let width = this.clamp(parseInt(widthStr), kViewportMinWidth, kViewportMaxWidth) || 0;
  6176     let height = this.clamp(parseInt(heightStr), kViewportMinHeight, kViewportMaxHeight) || 0;
  6178     // Allow zoom unless explicity disabled or minScale and maxScale are equal.
  6179     // WebKit allows 0, "no", or "false" for viewport-user-scalable.
  6180     // Note: NaN != NaN. Therefore if minScale and maxScale are undefined the clause has no effect.
  6181     let allowZoomStr = windowUtils.getDocumentMetadata("viewport-user-scalable");
  6182     let allowZoom = !/^(0|no|false)$/.test(allowZoomStr) && (minScale != maxScale);
  6184     // Double-tap should always be disabled if allowZoom is disabled. So we initialize
  6185     // allowDoubleTapZoom to the same value as allowZoom and have additional conditions to
  6186     // disable it in updateViewportSize.
  6187     let allowDoubleTapZoom = allowZoom;
  6189     let autoSize = true;
  6191     if (isNaN(scale) && isNaN(minScale) && isNaN(maxScale) && allowZoomStr == "" && widthStr == "" && heightStr == "") {
  6192       // Only check for HandheldFriendly if we don't have a viewport meta tag
  6193       let handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly");
  6194       if (handheldFriendly == "true") {
  6195         return new ViewportMetadata({
  6196           defaultZoom: 1,
  6197           autoSize: true,
  6198           allowZoom: true,
  6199           allowDoubleTapZoom: false
  6200         });
  6203       let doctype = aWindow.document.doctype;
  6204       if (doctype && /(WAP|WML|Mobile)/.test(doctype.publicId)) {
  6205         return new ViewportMetadata({
  6206           defaultZoom: 1,
  6207           autoSize: true,
  6208           allowZoom: true,
  6209           allowDoubleTapZoom: false
  6210         });
  6213       hasMetaViewport = false;
  6214       let defaultZoom = Services.prefs.getIntPref("browser.viewport.defaultZoom");
  6215       if (defaultZoom >= 0) {
  6216         scale = defaultZoom / 1000;
  6217         autoSize = false;
  6221     scale = this.clamp(scale, kViewportMinScale, kViewportMaxScale);
  6222     minScale = this.clamp(minScale, kViewportMinScale, kViewportMaxScale);
  6223     maxScale = this.clamp(maxScale, minScale, kViewportMaxScale);
  6225     if (autoSize) {
  6226       // If initial scale is 1.0 and width is not set, assume width=device-width
  6227       autoSize = (widthStr == "device-width" ||
  6228                   (!widthStr && (heightStr == "device-height" || scale == 1.0)));
  6231     let isRTL = aWindow.document.documentElement.dir == "rtl";
  6233     return new ViewportMetadata({
  6234       defaultZoom: scale,
  6235       minZoom: minScale,
  6236       maxZoom: maxScale,
  6237       width: width,
  6238       height: height,
  6239       autoSize: autoSize,
  6240       allowZoom: allowZoom,
  6241       allowDoubleTapZoom: allowDoubleTapZoom,
  6242       isSpecified: hasMetaViewport,
  6243       isRTL: isRTL
  6244     });
  6245   },
  6247   clamp: function(num, min, max) {
  6248     return Math.max(min, Math.min(max, num));
  6249   },
  6251   get displayDPI() {
  6252     let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  6253     delete this.displayDPI;
  6254     return this.displayDPI = utils.displayDPI;
  6255   },
  6257   /**
  6258    * Returns the viewport metadata for the given document, or the default metrics if no viewport
  6259    * metadata is available for that document.
  6260    */
  6261   getMetadataForDocument: function getMetadataForDocument(aDocument) {
  6262     let metadata = this._metadata.get(aDocument, new ViewportMetadata());
  6263     return metadata;
  6264   },
  6266   /** Updates the saved viewport metadata for the given content document. */
  6267   setMetadataForDocument: function setMetadataForDocument(aDocument, aMetadata) {
  6268     if (!aMetadata)
  6269       this._metadata.delete(aDocument);
  6270     else
  6271       this._metadata.set(aDocument, aMetadata);
  6274 };
  6276 /**
  6277  * An object which represents the page's preferred viewport properties:
  6278  *   width (int): The CSS viewport width in px.
  6279  *   height (int): The CSS viewport height in px.
  6280  *   defaultZoom (float): The initial scale when the page is loaded.
  6281  *   minZoom (float): The minimum zoom level.
  6282  *   maxZoom (float): The maximum zoom level.
  6283  *   autoSize (boolean): Resize the CSS viewport when the window resizes.
  6284  *   allowZoom (boolean): Let the user zoom in or out.
  6285  *   allowDoubleTapZoom (boolean): Allow double-tap to zoom in.
  6286  *   isSpecified (boolean): Whether the page viewport is specified or not.
  6287  */
  6288 function ViewportMetadata(aMetadata = {}) {
  6289   this.width = ("width" in aMetadata) ? aMetadata.width : 0;
  6290   this.height = ("height" in aMetadata) ? aMetadata.height : 0;
  6291   this.defaultZoom = ("defaultZoom" in aMetadata) ? aMetadata.defaultZoom : 0;
  6292   this.minZoom = ("minZoom" in aMetadata) ? aMetadata.minZoom : 0;
  6293   this.maxZoom = ("maxZoom" in aMetadata) ? aMetadata.maxZoom : 0;
  6294   this.autoSize = ("autoSize" in aMetadata) ? aMetadata.autoSize : false;
  6295   this.allowZoom = ("allowZoom" in aMetadata) ? aMetadata.allowZoom : true;
  6296   this.allowDoubleTapZoom = ("allowDoubleTapZoom" in aMetadata) ? aMetadata.allowDoubleTapZoom : true;
  6297   this.isSpecified = ("isSpecified" in aMetadata) ? aMetadata.isSpecified : false;
  6298   this.isRTL = ("isRTL" in aMetadata) ? aMetadata.isRTL : false;
  6299   Object.seal(this);
  6302 ViewportMetadata.prototype = {
  6303   width: null,
  6304   height: null,
  6305   defaultZoom: null,
  6306   minZoom: null,
  6307   maxZoom: null,
  6308   autoSize: null,
  6309   allowZoom: null,
  6310   allowDoubleTapZoom: null,
  6311   isSpecified: null,
  6312   isRTL: null,
  6314   toString: function() {
  6315     return "width=" + this.width
  6316          + "; height=" + this.height
  6317          + "; defaultZoom=" + this.defaultZoom
  6318          + "; minZoom=" + this.minZoom
  6319          + "; maxZoom=" + this.maxZoom
  6320          + "; autoSize=" + this.autoSize
  6321          + "; allowZoom=" + this.allowZoom
  6322          + "; allowDoubleTapZoom=" + this.allowDoubleTapZoom
  6323          + "; isSpecified=" + this.isSpecified
  6324          + "; isRTL=" + this.isRTL;
  6326 };
  6329 /**
  6330  * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml
  6331  */
  6332 var PopupBlockerObserver = {
  6333   onUpdatePageReport: function onUpdatePageReport(aEvent) {
  6334     let browser = BrowserApp.selectedBrowser;
  6335     if (aEvent.originalTarget != browser)
  6336       return;
  6338     if (!browser.pageReport)
  6339       return;
  6341     let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup");
  6342     if (result == Ci.nsIPermissionManager.DENY_ACTION)
  6343       return;
  6345     // Only show the notification again if we've not already shown it. Since
  6346     // notifications are per-browser, we don't need to worry about re-adding
  6347     // it.
  6348     if (!browser.pageReport.reported) {
  6349       if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
  6350         let brandShortName = Strings.brand.GetStringFromName("brandShortName");
  6351         let popupCount = browser.pageReport.length;
  6353         let strings = Strings.browser;
  6354         let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message"))
  6355                                 .replace("#1", brandShortName)
  6356                                 .replace("#2", popupCount);
  6358         let buttons = [
  6360             label: strings.GetStringFromName("popup.show"),
  6361             callback: function(aChecked) {
  6362               // Set permission before opening popup windows
  6363               if (aChecked)
  6364                 PopupBlockerObserver.allowPopupsForSite(true);
  6366               PopupBlockerObserver.showPopupsForSite();
  6368           },
  6370             label: strings.GetStringFromName("popup.dontShow"),
  6371             callback: function(aChecked) {
  6372               if (aChecked)
  6373                 PopupBlockerObserver.allowPopupsForSite(false);
  6376         ];
  6378         let options = { checkbox: Strings.browser.GetStringFromName("popup.dontAskAgain") };
  6379         NativeWindow.doorhanger.show(message, "popup-blocked", buttons, null, options);
  6381       // Record the fact that we've reported this blocked popup, so we don't
  6382       // show it again.
  6383       browser.pageReport.reported = true;
  6385   },
  6387   allowPopupsForSite: function allowPopupsForSite(aAllow) {
  6388     let currentURI = BrowserApp.selectedBrowser.currentURI;
  6389     Services.perms.add(currentURI, "popup", aAllow
  6390                        ?  Ci.nsIPermissionManager.ALLOW_ACTION
  6391                        :  Ci.nsIPermissionManager.DENY_ACTION);
  6392     dump("Allowing popups for: " + currentURI);
  6393   },
  6395   showPopupsForSite: function showPopupsForSite() {
  6396     let uri = BrowserApp.selectedBrowser.currentURI;
  6397     let pageReport = BrowserApp.selectedBrowser.pageReport;
  6398     if (pageReport) {
  6399       for (let i = 0; i < pageReport.length; ++i) {
  6400         let popupURIspec = pageReport[i].popupWindowURI.spec;
  6402         // Sometimes the popup URI that we get back from the pageReport
  6403         // isn't useful (for instance, netscape.com's popup URI ends up
  6404         // being "http://www.netscape.com", which isn't really the URI of
  6405         // the popup they're trying to show).  This isn't going to be
  6406         // useful to the user, so we won't create a menu item for it.
  6407         if (popupURIspec == "" || popupURIspec == "about:blank" || popupURIspec == uri.spec)
  6408           continue;
  6410         let popupFeatures = pageReport[i].popupWindowFeatures;
  6411         let popupName = pageReport[i].popupWindowName;
  6413         let parent = BrowserApp.selectedTab;
  6414         let isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
  6415         BrowserApp.addTab(popupURIspec, { parentId: parent.id, isPrivate: isPrivate });
  6419 };
  6422 var IndexedDB = {
  6423   _permissionsPrompt: "indexedDB-permissions-prompt",
  6424   _permissionsResponse: "indexedDB-permissions-response",
  6426   _quotaPrompt: "indexedDB-quota-prompt",
  6427   _quotaResponse: "indexedDB-quota-response",
  6428   _quotaCancel: "indexedDB-quota-cancel",
  6430   init: function IndexedDB_init() {
  6431     Services.obs.addObserver(this, this._permissionsPrompt, false);
  6432     Services.obs.addObserver(this, this._quotaPrompt, false);
  6433     Services.obs.addObserver(this, this._quotaCancel, false);
  6434   },
  6436   uninit: function IndexedDB_uninit() {
  6437     Services.obs.removeObserver(this, this._permissionsPrompt);
  6438     Services.obs.removeObserver(this, this._quotaPrompt);
  6439     Services.obs.removeObserver(this, this._quotaCancel);
  6440   },
  6442   observe: function IndexedDB_observe(subject, topic, data) {
  6443     if (topic != this._permissionsPrompt &&
  6444         topic != this._quotaPrompt &&
  6445         topic != this._quotaCancel) {
  6446       throw new Error("Unexpected topic!");
  6449     let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
  6451     let contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
  6452     let contentDocument = contentWindow.document;
  6453     let tab = BrowserApp.getTabForWindow(contentWindow);
  6454     if (!tab)
  6455       return;
  6457     let host = contentDocument.documentURIObject.asciiHost;
  6459     let strings = Strings.browser;
  6461     let message, responseTopic;
  6462     if (topic == this._permissionsPrompt) {
  6463       message = strings.formatStringFromName("offlineApps.ask", [host], 1);
  6464       responseTopic = this._permissionsResponse;
  6465     } else if (topic == this._quotaPrompt) {
  6466       message = strings.formatStringFromName("indexedDBQuota.wantsTo", [ host, data ], 2);
  6467       responseTopic = this._quotaResponse;
  6468     } else if (topic == this._quotaCancel) {
  6469       responseTopic = this._quotaResponse;
  6472     const firstTimeoutDuration = 300000; // 5 minutes
  6474     let timeoutId;
  6476     let notificationID = responseTopic + host;
  6477     let observer = requestor.getInterface(Ci.nsIObserver);
  6479     // This will be set to the result of PopupNotifications.show() below, or to
  6480     // the result of PopupNotifications.getNotification() if this is a
  6481     // quotaCancel notification.
  6482     let notification;
  6484     function timeoutNotification() {
  6485       // Remove the notification.
  6486       NativeWindow.doorhanger.hide(notificationID, tab.id);
  6488       // Clear all of our timeout stuff. We may be called directly, not just
  6489       // when the timeout actually elapses.
  6490       clearTimeout(timeoutId);
  6492       // And tell the page that the popup timed out.
  6493       observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
  6496     if (topic == this._quotaCancel) {
  6497       NativeWindow.doorhanger.hide(notificationID, tab.id);
  6498       timeoutNotification();
  6499       observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
  6500       return;
  6503     let buttons = [{
  6504       label: strings.GetStringFromName("offlineApps.allow"),
  6505       callback: function() {
  6506         clearTimeout(timeoutId);
  6507         observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION);
  6509     },
  6511       label: strings.GetStringFromName("offlineApps.dontAllow2"),
  6512       callback: function(aChecked) {
  6513         clearTimeout(timeoutId);
  6514         let action = aChecked ? Ci.nsIPermissionManager.DENY_ACTION : Ci.nsIPermissionManager.UNKNOWN_ACTION;
  6515         observer.observe(null, responseTopic, action);
  6517     }];
  6519     let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
  6520     NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
  6522     // Set the timeoutId after the popup has been created, and use the long
  6523     // timeout value. If the user doesn't notice the popup after this amount of
  6524     // time then it is most likely not visible and we want to alert the page.
  6525     timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
  6527 };
  6529 var CharacterEncoding = {
  6530   _charsets: [],
  6532   init: function init() {
  6533     Services.obs.addObserver(this, "CharEncoding:Get", false);
  6534     Services.obs.addObserver(this, "CharEncoding:Set", false);
  6535     this.sendState();
  6536   },
  6538   uninit: function uninit() {
  6539     Services.obs.removeObserver(this, "CharEncoding:Get");
  6540     Services.obs.removeObserver(this, "CharEncoding:Set");
  6541   },
  6543   observe: function observe(aSubject, aTopic, aData) {
  6544     switch (aTopic) {
  6545       case "CharEncoding:Get":
  6546         this.getEncoding();
  6547         break;
  6548       case "CharEncoding:Set":
  6549         this.setEncoding(aData);
  6550         break;
  6552   },
  6554   sendState: function sendState() {
  6555     let showCharEncoding = "false";
  6556     try {
  6557       showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
  6558     } catch (e) { /* Optional */ }
  6560     sendMessageToJava({
  6561       type: "CharEncoding:State",
  6562       visible: showCharEncoding
  6563     });
  6564   },
  6566   getEncoding: function getEncoding() {
  6567     function infoToCharset(info) {
  6568       return { code: info.value, title: info.label };
  6571     if (!this._charsets.length) {
  6572       let data = CharsetMenu.getData();
  6574       // In the desktop UI, the pinned charsets are shown above the rest.
  6575       let pinnedCharsets = data.pinnedCharsets.map(infoToCharset);
  6576       let otherCharsets = data.otherCharsets.map(infoToCharset)
  6578       this._charsets = pinnedCharsets.concat(otherCharsets);
  6581     // Look for the index of the selected charset. Default to -1 if the
  6582     // doc charset isn't found in the list of available charsets.
  6583     let docCharset = BrowserApp.selectedBrowser.contentDocument.characterSet;
  6584     let selected = -1;
  6585     let charsetCount = this._charsets.length;
  6587     for (let i = 0; i < charsetCount; i++) {
  6588       if (this._charsets[i].code === docCharset) {
  6589         selected = i;
  6590         break;
  6594     sendMessageToJava({
  6595       type: "CharEncoding:Data",
  6596       charsets: this._charsets,
  6597       selected: selected
  6598     });
  6599   },
  6601   setEncoding: function setEncoding(aEncoding) {
  6602     let browser = BrowserApp.selectedBrowser;
  6603     browser.docShell.gatherCharsetMenuTelemetry();
  6604     browser.docShell.charset = aEncoding;
  6605     browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
  6607 };
  6609 var IdentityHandler = {
  6610   // No trusted identity information. No site identity icon is shown.
  6611   IDENTITY_MODE_UNKNOWN: "unknown",
  6613   // Minimal SSL CA-signed domain verification. Blue lock icon is shown.
  6614   IDENTITY_MODE_DOMAIN_VERIFIED: "verified",
  6616   // High-quality identity information. Green lock icon is shown.
  6617   IDENTITY_MODE_IDENTIFIED: "identified",
  6619   // The following mixed content modes are only used if "security.mixed_content.block_active_content"
  6620   // is enabled. Even though the mixed content state and identitity state are orthogonal,
  6621   // our Java frontend coalesces them into one indicator.
  6623   // Blocked active mixed content. Shield icon is shown, with a popup option to load content.
  6624   IDENTITY_MODE_MIXED_CONTENT_BLOCKED: "mixed_content_blocked",
  6626   // Loaded active mixed content. Yellow triangle icon is shown.
  6627   IDENTITY_MODE_MIXED_CONTENT_LOADED: "mixed_content_loaded",
  6629   // Cache the most recent SSLStatus and Location seen in getIdentityStrings
  6630   _lastStatus : null,
  6631   _lastLocation : null,
  6633   /**
  6634    * Helper to parse out the important parts of _lastStatus (of the SSL cert in
  6635    * particular) for use in constructing identity UI strings
  6636   */
  6637   getIdentityData : function() {
  6638     let result = {};
  6639     let status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus);
  6640     let cert = status.serverCert;
  6642     // Human readable name of Subject
  6643     result.subjectOrg = cert.organization;
  6645     // SubjectName fields, broken up for individual access
  6646     if (cert.subjectName) {
  6647       result.subjectNameFields = {};
  6648       cert.subjectName.split(",").forEach(function(v) {
  6649         let field = v.split("=");
  6650         this[field[0]] = field[1];
  6651       }, result.subjectNameFields);
  6653       // Call out city, state, and country specifically
  6654       result.city = result.subjectNameFields.L;
  6655       result.state = result.subjectNameFields.ST;
  6656       result.country = result.subjectNameFields.C;
  6659     // Human readable name of Certificate Authority
  6660     result.caOrg =  cert.issuerOrganization || cert.issuerCommonName;
  6661     result.cert = cert;
  6663     return result;
  6664   },
  6666   /**
  6667    * Determines the identity mode corresponding to the icon we show in the urlbar.
  6668    */
  6669   getIdentityMode: function getIdentityMode(aState) {
  6670     if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT)
  6671       return this.IDENTITY_MODE_MIXED_CONTENT_BLOCKED;
  6673     // Only show an indicator for loaded mixed content if the pref to block it is enabled
  6674     if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
  6675          Services.prefs.getBoolPref("security.mixed_content.block_active_content"))
  6676       return this.IDENTITY_MODE_MIXED_CONTENT_LOADED;
  6678     if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
  6679       return this.IDENTITY_MODE_IDENTIFIED;
  6681     if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE)
  6682       return this.IDENTITY_MODE_DOMAIN_VERIFIED;
  6684     return this.IDENTITY_MODE_UNKNOWN;
  6685   },
  6687   /**
  6688    * Determine the identity of the page being displayed by examining its SSL cert
  6689    * (if available). Return the data needed to update the UI.
  6690    */
  6691   checkIdentity: function checkIdentity(aState, aBrowser) {
  6692     this._lastStatus = aBrowser.securityUI
  6693                                .QueryInterface(Components.interfaces.nsISSLStatusProvider)
  6694                                .SSLStatus;
  6696     // Don't pass in the actual location object, since it can cause us to 
  6697     // hold on to the window object too long.  Just pass in the fields we
  6698     // care about. (bug 424829)
  6699     let locationObj = {};
  6700     try {
  6701       let location = aBrowser.contentWindow.location;
  6702       locationObj.host = location.host;
  6703       locationObj.hostname = location.hostname;
  6704       locationObj.port = location.port;
  6705     } catch (ex) {
  6706       // Can sometimes throw if the URL being visited has no host/hostname,
  6707       // e.g. about:blank. The _state for these pages means we won't need these
  6708       // properties anyways, though.
  6710     this._lastLocation = locationObj;
  6712     let mode = this.getIdentityMode(aState);
  6713     let result = { mode: mode };
  6715     // Don't show identity data for pages with an unknown identity or if any
  6716     // mixed content is loaded (mixed display content is loaded by default).
  6717     if (mode == this.IDENTITY_MODE_UNKNOWN ||
  6718         aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
  6719       return result;
  6721     // Ideally we'd just make this a Java string
  6722     result.encrypted = Strings.browser.GetStringFromName("identity.encrypted2");
  6723     result.host = this.getEffectiveHost();
  6725     let iData = this.getIdentityData();
  6726     result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
  6728     // If the cert is identified, then we can populate the results with credentials
  6729     if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
  6730       result.owner = iData.subjectOrg;
  6732       // Build an appropriate supplemental block out of whatever location data we have
  6733       let supplemental = "";
  6734       if (iData.city)
  6735         supplemental += iData.city + "\n";
  6736       if (iData.state && iData.country)
  6737         supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2);
  6738       else if (iData.state) // State only
  6739         supplemental += iData.state;
  6740       else if (iData.country) // Country only
  6741         supplemental += iData.country;
  6742       result.supplemental = supplemental;
  6744       return result;
  6747     // Otherwise, we don't know the cert owner
  6748     result.owner = Strings.browser.GetStringFromName("identity.ownerUnknown3");
  6750     // Cache the override service the first time we need to check it
  6751     if (!this._overrideService)
  6752       this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
  6754     // Check whether this site is a security exception. XPConnect does the right
  6755     // thing here in terms of converting _lastLocation.port from string to int, but
  6756     // the overrideService doesn't like undefined ports, so make sure we have
  6757     // something in the default case (bug 432241).
  6758     // .hostname can return an empty string in some exceptional cases -
  6759     // hasMatchingOverride does not handle that, so avoid calling it.
  6760     // Updating the tooltip value in those cases isn't critical.
  6761     // FIXME: Fixing bug 646690 would probably makes this check unnecessary
  6762     if (this._lastLocation.hostname &&
  6763         this._overrideService.hasMatchingOverride(this._lastLocation.hostname,
  6764                                                   (this._lastLocation.port || 443),
  6765                                                   iData.cert, {}, {}))
  6766       result.verifier = Strings.browser.GetStringFromName("identity.identified.verified_by_you");
  6768     return result;
  6769   },
  6771   /**
  6772    * Return the eTLD+1 version of the current hostname
  6773    */
  6774   getEffectiveHost: function getEffectiveHost() {
  6775     if (!this._IDNService)
  6776       this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
  6777                          .getService(Ci.nsIIDNService);
  6778     try {
  6779       let baseDomain = Services.eTLD.getBaseDomainFromHost(this._lastLocation.hostname);
  6780       return this._IDNService.convertToDisplayIDN(baseDomain, {});
  6781     } catch (e) {
  6782       // If something goes wrong (e.g. hostname is an IP address) just fail back
  6783       // to the full domain.
  6784       return this._lastLocation.hostname;
  6787 };
  6789 function OverscrollController(aTab) {
  6790   this.tab = aTab;
  6793 OverscrollController.prototype = {
  6794   supportsCommand : function supportsCommand(aCommand) {
  6795     if (aCommand != "cmd_linePrevious" && aCommand != "cmd_scrollPageUp")
  6796       return false;
  6798     return (this.tab.getViewport().y == 0);
  6799   },
  6801   isCommandEnabled : function isCommandEnabled(aCommand) {
  6802     return this.supportsCommand(aCommand);
  6803   },
  6805   doCommand : function doCommand(aCommand){
  6806     sendMessageToJava({ type: "ToggleChrome:Focus" });
  6807   },
  6809   onEvent : function onEvent(aEvent) { }
  6810 };
  6812 var SearchEngines = {
  6813   _contextMenuId: null,
  6814   PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
  6815   PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
  6817   init: function init() {
  6818     Services.obs.addObserver(this, "SearchEngines:Add", false);
  6819     Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
  6820     Services.obs.addObserver(this, "SearchEngines:Remove", false);
  6821     Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
  6822     Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
  6824     let filter = {
  6825       matches: function (aElement) {
  6826         // Copied from body of isTargetAKeywordField function in nsContextMenu.js
  6827         if(!(aElement instanceof HTMLInputElement))
  6828           return false;
  6829         let form = aElement.form;
  6830         if (!form || aElement.type == "password")
  6831           return false;
  6833         let method = form.method.toUpperCase();
  6835         // These are the following types of forms we can create keywords for:
  6836         //
  6837         // method    encoding type        can create keyword
  6838         // GET       *                                   YES
  6839         //           *                                   YES
  6840         // POST      *                                   YES
  6841         // POST      application/x-www-form-urlencoded   YES
  6842         // POST      text/plain                          NO ( a little tricky to do)
  6843         // POST      multipart/form-data                 NO
  6844         // POST      everything else                     YES
  6845         return (method == "GET" || method == "") ||
  6846                (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
  6848     };
  6849     SelectionHandler.addAction({
  6850       id: "search_add_action",
  6851       label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine"),
  6852       icon: "drawable://ab_add_search_engine",
  6853       selector: filter,
  6854       action: function(aElement) {
  6855         UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
  6856         SearchEngines.addEngine(aElement);
  6858     });
  6859   },
  6861   uninit: function uninit() {
  6862     Services.obs.removeObserver(this, "SearchEngines:Add");
  6863     Services.obs.removeObserver(this, "SearchEngines:GetVisible");
  6864     Services.obs.removeObserver(this, "SearchEngines:Remove");
  6865     Services.obs.removeObserver(this, "SearchEngines:RestoreDefaults");
  6866     Services.obs.removeObserver(this, "SearchEngines:SetDefault");
  6867     if (this._contextMenuId != null)
  6868       NativeWindow.contextmenus.remove(this._contextMenuId);
  6869   },
  6871   // Fetch list of search engines. all ? All engines : Visible engines only.
  6872   _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
  6873     if (!Components.isSuccessCode(rv)) {
  6874       Cu.reportError("Could not initialize search service, bailing out.");
  6875       return;
  6878     let engineData = Services.search.getVisibleEngines({});
  6879     let searchEngines = engineData.map(function (engine) {
  6880       return {
  6881         name: engine.name,
  6882         identifier: engine.identifier,
  6883         iconURI: (engine.iconURI ? engine.iconURI.spec : null),
  6884         hidden: engine.hidden
  6885       };
  6886     });
  6888     let suggestTemplate = null;
  6889     let suggestEngine = null;
  6891     // Check to see if the default engine supports search suggestions. We only need to check
  6892     // the default engine because we only show suggestions for the default engine in the UI.
  6893     let engine = Services.search.defaultEngine;
  6894     if (engine.supportsResponseType("application/x-suggestions+json")) {
  6895       suggestEngine = engine.name;
  6896       suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
  6899     // By convention, the currently configured default engine is at position zero in searchEngines.
  6900     sendMessageToJava({
  6901       type: "SearchEngines:Data",
  6902       searchEngines: searchEngines,
  6903       suggest: {
  6904         engine: suggestEngine,
  6905         template: suggestTemplate,
  6906         enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
  6907         prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
  6909     });
  6911     // Send a speculative connection to the default engine.
  6912     let connector = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
  6913     let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri;
  6914     let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor)
  6915                           .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext);
  6916     try {
  6917       connector.speculativeConnect(searchURI, callbacks);
  6918     } catch (e) {}
  6919   },
  6921   // Helper method to extract the engine name from a JSON. Simplifies the observe function.
  6922   _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
  6923     let data = JSON.parse(aData);
  6924     return Services.search.getEngineByName(data.engine);
  6925   },
  6927   observe: function observe(aSubject, aTopic, aData) {
  6928     let engine;
  6929     switch(aTopic) {
  6930       case "SearchEngines:Add":
  6931         this.displaySearchEnginesList(aData);
  6932         break;
  6933       case "SearchEngines:GetVisible":
  6934         Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
  6935         break;
  6936       case "SearchEngines:Remove":
  6937         // Make sure the engine isn't hidden before removing it, to make sure it's
  6938         // visible if the user later re-adds it (works around bug 341833)
  6939         engine = this._extractEngineFromJSON(aData);
  6940         engine.hidden = false;
  6941         Services.search.removeEngine(engine);
  6942         break;
  6943       case "SearchEngines:RestoreDefaults":
  6944         // Un-hides all default engines.
  6945         Services.search.restoreDefaultEngines();
  6946         break;
  6947       case "SearchEngines:SetDefault":
  6948         engine = this._extractEngineFromJSON(aData);
  6949         // Move the new default search engine to the top of the search engine list.
  6950         Services.search.moveEngine(engine, 0);
  6951         Services.search.defaultEngine = engine;
  6952         break;
  6954       default:
  6955         dump("Unexpected message type observed: " + aTopic);
  6956         break;
  6958   },
  6960   // Display context menu listing names of the search engines available to be added.
  6961   displaySearchEnginesList: function displaySearchEnginesList(aData) {
  6962     let data = JSON.parse(aData);
  6963     let tab = BrowserApp.getTabForId(data.tabId);
  6965     if (!tab)
  6966       return;
  6968     let browser = tab.browser;
  6969     let engines = browser.engines;
  6971     let p = new Prompt({
  6972       window: browser.contentWindow
  6973     }).setSingleChoiceItems(engines.map(function(e) {
  6974       return { label: e.title };
  6975     })).show((function(data) {
  6976       if (data.button == -1)
  6977         return;
  6979       this.addOpenSearchEngine(engines[data.button]);
  6980       engines.splice(data.button, 1);
  6982       if (engines.length < 1) {
  6983         // Broadcast message that there are no more add-able search engines.
  6984         let newEngineMessage = {
  6985           type: "Link:OpenSearch",
  6986           tabID: tab.id,
  6987           visible: false
  6988         };
  6990         sendMessageToJava(newEngineMessage);
  6992     }).bind(this));
  6993   },
  6995   addOpenSearchEngine: function addOpenSearchEngine(engine) {
  6996     Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
  6997       onSuccess: function() {
  6998         // Display a toast confirming addition of new search engine.
  6999         NativeWindow.toast.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), "long");
  7000       },
  7002       onError: function(aCode) {
  7003         let errorMessage;
  7004         if (aCode == 2) {
  7005           // Engine is a duplicate.
  7006           errorMessage = "alertSearchEngineDuplicateToast";
  7008         } else {
  7009           // Unknown failure. Display general error message.
  7010           errorMessage = "alertSearchEngineErrorToast";
  7013         NativeWindow.toast.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), "long");
  7015     });
  7016   },
  7018   addEngine: function addEngine(aElement) {
  7019     let form = aElement.form;
  7020     let charset = aElement.ownerDocument.characterSet;
  7021     let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null);
  7022     let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
  7023     let method = form.method.toUpperCase();
  7024     let formData = [];
  7026     for (let i = 0; i < form.elements.length; ++i) {
  7027       let el = form.elements[i];
  7028       if (!el.type)
  7029         continue;
  7031       // make this text field a generic search parameter
  7032       if (aElement == el) {
  7033         formData.push({ name: el.name, value: "{searchTerms}" });
  7034         continue;
  7037       let type = el.type.toLowerCase();
  7038       let escapedName = escape(el.name);
  7039       let escapedValue = escape(el.value);
  7041       // add other form elements as parameters
  7042       switch (el.type) {
  7043         case "checkbox":
  7044         case "radio":
  7045           if (!el.checked) break;
  7046         case "text":
  7047         case "hidden":
  7048         case "textarea":
  7049           formData.push({ name: escapedName, value: escapedValue });
  7050           break;
  7051         case "select-one":
  7052           for (let option of el.options) {
  7053             if (option.selected) {
  7054               formData.push({ name: escapedName, value: escapedValue });
  7055               break;
  7061     // prompt user for name of search engine
  7062     let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine");
  7063     let title = { value: (aElement.ownerDocument.title || docURI.host) };
  7064     if (!Services.prompt.prompt(null, promptTitle, null, title, null, {}))
  7065       return;
  7067     // fetch the favicon for this page
  7068     let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
  7069     let mDBConn = Services.storage.openDatabase(dbFile);
  7070     let stmts = [];
  7071     stmts[0] = mDBConn.createStatement("SELECT favicon FROM history_with_favicons WHERE url = ?");
  7072     stmts[0].bindStringParameter(0, docURI.spec);
  7073     let favicon = null;
  7074     Services.search.init(function addEngine_cb(rv) {
  7075       if (!Components.isSuccessCode(rv)) {
  7076         Cu.reportError("Could not initialize search service, bailing out.");
  7077         return;
  7079       mDBConn.executeAsync(stmts, stmts.length, {
  7080         handleResult: function (results) {
  7081           let bytes = results.getNextRow().getResultByName("favicon");
  7082           if (bytes && bytes.length) {
  7083             favicon = "data:image/x-icon;base64," + btoa(String.fromCharCode.apply(null, bytes));
  7085         },
  7086         handleCompletion: function (reason) {
  7087           // if there's already an engine with this name, add a number to
  7088           // make the name unique (e.g., "Google" becomes "Google 2")
  7089           let name = title.value;
  7090           for (let i = 2; Services.search.getEngineByName(name); i++)
  7091             name = title.value + " " + i;
  7093           Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL);
  7094           let engine = Services.search.getEngineByName(name);
  7095           engine.wrappedJSObject._queryCharset = charset;
  7096           for (let i = 0; i < formData.length; ++i) {
  7097             let param = formData[i];
  7098             if (param.name && param.value)
  7099               engine.addParam(param.name, param.value, null);
  7102       });
  7103     });
  7105 };
  7107 var ActivityObserver = {
  7108   init: function ao_init() {
  7109     Services.obs.addObserver(this, "application-background", false);
  7110     Services.obs.addObserver(this, "application-foreground", false);
  7111   },
  7113   observe: function ao_observe(aSubject, aTopic, aData) {
  7114     let isForeground = false;
  7115     let tab = BrowserApp.selectedTab;
  7117     switch (aTopic) {
  7118       case "application-background" :
  7119         let doc = (tab ? tab.browser.contentDocument : null);
  7120         if (doc && doc.mozFullScreen) {
  7121           doc.mozCancelFullScreen();
  7123         isForeground = false;
  7124         break;
  7125       case "application-foreground" :
  7126         isForeground = true;
  7127         break;
  7130     if (tab && tab.getActive() != isForeground) {
  7131       tab.setActive(isForeground);
  7134 };
  7136 #ifndef MOZ_ANDROID_SYNTHAPKS
  7137 var WebappsUI = {
  7138   init: function init() {
  7139     Cu.import("resource://gre/modules/Webapps.jsm");
  7140     Cu.import("resource://gre/modules/AppsUtils.jsm");
  7141     DOMApplicationRegistry.allAppsLaunchable = true;
  7143     Services.obs.addObserver(this, "webapps-ask-install", false);
  7144     Services.obs.addObserver(this, "webapps-launch", false);
  7145     Services.obs.addObserver(this, "webapps-uninstall", false);
  7146     Services.obs.addObserver(this, "webapps-install-error", false);
  7147   },
  7149   uninit: function unint() {
  7150     Services.obs.removeObserver(this, "webapps-ask-install");
  7151     Services.obs.removeObserver(this, "webapps-launch");
  7152     Services.obs.removeObserver(this, "webapps-uninstall");
  7153     Services.obs.removeObserver(this, "webapps-install-error");
  7154   },
  7156   DEFAULT_ICON: "chrome://browser/skin/images/default-app-icon.png",
  7157   DEFAULT_PREFS_FILENAME: "default-prefs.js",
  7159   observe: function observe(aSubject, aTopic, aData) {
  7160     let data = {};
  7161     try {
  7162       data = JSON.parse(aData);
  7163       data.mm = aSubject;
  7164     } catch(ex) { }
  7165     switch (aTopic) {
  7166       case "webapps-install-error":
  7167         let msg = "";
  7168         switch (aData) {
  7169           case "INVALID_MANIFEST":
  7170           case "MANIFEST_PARSE_ERROR":
  7171             msg = Strings.browser.GetStringFromName("webapps.manifestInstallError");
  7172             break;
  7173           case "NETWORK_ERROR":
  7174           case "MANIFEST_URL_ERROR":
  7175             msg = Strings.browser.GetStringFromName("webapps.networkInstallError");
  7176             break;
  7177           default:
  7178             msg = Strings.browser.GetStringFromName("webapps.installError");
  7180         NativeWindow.toast.show(msg, "short");
  7181         console.log("Error installing app: " + aData);
  7182         break;
  7183       case "webapps-ask-install":
  7184         this.doInstall(data);
  7185         break;
  7186       case "webapps-launch":
  7187         this.openURL(data.manifestURL, data.origin);
  7188         break;
  7189       case "webapps-uninstall":
  7190         sendMessageToJava({
  7191           type: "Webapps:Uninstall",
  7192           origin: data.origin
  7193         });
  7194         break;
  7196   },
  7198   doInstall: function doInstall(aData) {
  7199     let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
  7200     let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
  7202     if (Services.prompt.confirm(null, Strings.browser.GetStringFromName("webapps.installTitle"), manifest.name + "\n" + aData.app.origin)) {
  7203       // Get a profile for the app to be installed in. We'll download everything before creating the icons.
  7204       let origin = aData.app.origin;
  7205       sendMessageToJava({
  7206          type: "Webapps:Preinstall",
  7207          name: manifest.name,
  7208          manifestURL: aData.app.manifestURL,
  7209          origin: origin
  7210       }, (data) => {
  7211         let profilePath = data.profile;
  7212         if (!profilePath)
  7213           return;
  7215         let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
  7216         file.initWithPath(profilePath);
  7218         let self = this;
  7219         DOMApplicationRegistry.confirmInstall(aData, file,
  7220           function (aManifest) {
  7221             let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
  7223             // the manifest argument is the manifest from within the zip file,
  7224             // TODO so now would be a good time to ask about permissions.
  7225             self.makeBase64Icon(localeManifest.biggestIconURL || this.DEFAULT_ICON,
  7226               function(scaledIcon, fullsizeIcon) {
  7227                 // if java returned a profile path to us, try to use it to pre-populate the app cache
  7228                 // also save the icon so that it can be used in the splash screen
  7229                 try {
  7230                   let iconFile = file.clone();
  7231                   iconFile.append("logo.png");
  7232                   let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
  7233                   persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
  7234                   persist.persistFlags |= Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
  7236                   let source = Services.io.newURI(fullsizeIcon, "UTF8", null);
  7237                   persist.saveURI(source, null, null, null, null, iconFile, null);
  7239                   // aData.app.origin may now point to the app: url that hosts this app
  7240                   sendMessageToJava({
  7241                     type: "Webapps:Postinstall",
  7242                     name: localeManifest.name,
  7243                     manifestURL: aData.app.manifestURL,
  7244                     originalOrigin: origin,
  7245                     origin: aData.app.origin,
  7246                     iconURL: fullsizeIcon
  7247                   });
  7248                   if (!!aData.isPackage) {
  7249                     // For packaged apps, put a notification in the notification bar.
  7250                     let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
  7251                     let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
  7252                     alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", {
  7253                       observe: function () {
  7254                         self.openURL(aData.app.manifestURL, aData.app.origin);
  7256                     }, "webapp");
  7259                   // Create a system notification allowing the user to launch the app
  7260                   let observer = {
  7261                     observe: function (aSubject, aTopic) {
  7262                       if (aTopic == "alertclickcallback") {
  7263                         WebappsUI.openURL(aData.app.manifestURL, origin);
  7266                   };
  7268                   let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
  7269                   let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
  7270                   alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", observer, "webapp");
  7271                 } catch(ex) {
  7272                   console.log(ex);
  7274                 self.writeDefaultPrefs(file, localeManifest);
  7276             );
  7278         );
  7279       });
  7280     } else {
  7281       DOMApplicationRegistry.denyInstall(aData);
  7283   },
  7285   writeDefaultPrefs: function webapps_writeDefaultPrefs(aProfile, aManifest) {
  7286       // build any app specific default prefs
  7287       let prefs = [];
  7288       if (aManifest.orientation) {
  7289         prefs.push({name:"app.orientation.default", value: aManifest.orientation.join(",") });
  7292       // write them into the app profile
  7293       let defaultPrefsFile = aProfile.clone();
  7294       defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME);
  7295       this._writeData(defaultPrefsFile, prefs);
  7296   },
  7298   _writeData: function(aFile, aPrefs) {
  7299     if (aPrefs.length > 0) {
  7300       let array = new TextEncoder().encode(JSON.stringify(aPrefs));
  7301       OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
  7302         console.log("Error writing default prefs: " + reason);
  7303       });
  7305   },
  7307   openURL: function openURL(aManifestURL, aOrigin) {
  7308     sendMessageToJava({
  7309       type: "Webapps:Open",
  7310       manifestURL: aManifestURL,
  7311       origin: aOrigin
  7312     });
  7313   },
  7315   get iconSize() {
  7316     let iconSize = 64;
  7317     try {
  7318       let jni = new JNI();
  7319       let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell");
  7320       let method = jni.getStaticMethodID(cls, "getPreferredIconSize", "()I");
  7321       iconSize = jni.callStaticIntMethod(cls, method);
  7322       jni.close();
  7323     } catch(ex) {
  7324       console.log(ex);
  7327     delete this.iconSize;
  7328     return this.iconSize = iconSize;
  7329   },
  7331   makeBase64Icon: function loadAndMakeBase64Icon(aIconURL, aCallbackFunction) {
  7332     let size = this.iconSize;
  7334     let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
  7335     canvas.width = canvas.height = size;
  7336     let ctx = canvas.getContext("2d");
  7337     let favicon = new Image();
  7338     favicon.onload = function() {
  7339       ctx.drawImage(favicon, 0, 0, size, size);
  7340       let scaledIcon = canvas.toDataURL("image/png", "");
  7342       canvas.width = favicon.width;
  7343       canvas.height = favicon.height;
  7344       ctx.drawImage(favicon, 0, 0, favicon.width, favicon.height);
  7345       let fullsizeIcon = canvas.toDataURL("image/png", "");
  7347       canvas = null;
  7348       aCallbackFunction.call(null, scaledIcon, fullsizeIcon);
  7349     };
  7350     favicon.onerror = function() {
  7351       Cu.reportError("CreateShortcut: favicon image load error");
  7353       // if the image failed to load, and it was not our default icon, attempt to
  7354       // use our default as a fallback
  7355       if (favicon.src != WebappsUI.DEFAULT_ICON) {
  7356         favicon.src = WebappsUI.DEFAULT_ICON;
  7358     };
  7360     favicon.src = aIconURL;
  7361   },
  7363   createShortcut: function createShortcut(aTitle, aURL, aIconURL, aType) {
  7364     this.makeBase64Icon(aIconURL, function _createShortcut(icon) {
  7365       try {
  7366         let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService);
  7367         shell.createShortcut(aTitle, aURL, icon, aType);
  7368       } catch(e) {
  7369         Cu.reportError(e);
  7371     });
  7374 #endif
  7376 var RemoteDebugger = {
  7377   init: function rd_init() {
  7378     Services.prefs.addObserver("devtools.debugger.", this, false);
  7380     if (this._isEnabled())
  7381       this._start();
  7382   },
  7384   observe: function rd_observe(aSubject, aTopic, aData) {
  7385     if (aTopic != "nsPref:changed")
  7386       return;
  7388     switch (aData) {
  7389       case "devtools.debugger.remote-enabled":
  7390         if (this._isEnabled())
  7391           this._start();
  7392         else
  7393           this._stop();
  7394         break;
  7396       case "devtools.debugger.remote-port":
  7397         if (this._isEnabled())
  7398           this._restart();
  7399         break;
  7401   },
  7403   uninit: function rd_uninit() {
  7404     Services.prefs.removeObserver("devtools.debugger.", this);
  7405     this._stop();
  7406   },
  7408   _getPort: function _rd_getPort() {
  7409     return Services.prefs.getIntPref("devtools.debugger.remote-port");
  7410   },
  7412   _isEnabled: function rd_isEnabled() {
  7413     return Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
  7414   },
  7416   /**
  7417    * Prompt the user to accept or decline the incoming connection.
  7418    * This is passed to DebuggerService.init as a callback.
  7420    * @return true if the connection should be permitted, false otherwise
  7421    */
  7422   _showConnectionPrompt: function rd_showConnectionPrompt() {
  7423     let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
  7424     let msg = Strings.browser.GetStringFromName("remoteIncomingPromptMessage");
  7425     let disable = Strings.browser.GetStringFromName("remoteIncomingPromptDisable");
  7426     let cancel = Strings.browser.GetStringFromName("remoteIncomingPromptCancel");
  7427     let agree = Strings.browser.GetStringFromName("remoteIncomingPromptAccept");
  7429     // Make prompt. Note: button order is in reverse.
  7430     let prompt = new Prompt({
  7431       window: null,
  7432       hint: "remotedebug",
  7433       title: title,
  7434       message: msg,
  7435       buttons: [ agree, cancel, disable ],
  7436       priority: 1
  7437     });
  7439     // The debugger server expects a synchronous response, so spin on result since Prompt is async.
  7440     let result = null;
  7442     prompt.show(function(data) {
  7443       result = data.button;
  7444     });
  7446     // Spin this thread while we wait for a result.
  7447     let thread = Services.tm.currentThread;
  7448     while (result == null)
  7449       thread.processNextEvent(true);
  7451     if (result === 0)
  7452       return true;
  7453     if (result === 2) {
  7454       Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
  7455       this._stop();
  7457     return false;
  7458   },
  7460   _restart: function rd_restart() {
  7461     this._stop();
  7462     this._start();
  7463   },
  7465   _start: function rd_start() {
  7466     try {
  7467       if (!DebuggerServer.initialized) {
  7468         DebuggerServer.init(this._showConnectionPrompt.bind(this));
  7469         DebuggerServer.addBrowserActors();
  7470         DebuggerServer.addActors("chrome://browser/content/dbg-browser-actors.js");
  7473       let port = this._getPort();
  7474       DebuggerServer.openListener(port);
  7475       dump("Remote debugger listening on port " + port);
  7476     } catch(e) {
  7477       dump("Remote debugger didn't start: " + e);
  7479   },
  7481   _stop: function rd_start() {
  7482     DebuggerServer.closeListener();
  7483     dump("Remote debugger stopped");
  7485 };
  7487 var Telemetry = {
  7488   addData: function addData(aHistogramId, aValue) {
  7489     let histogram = Services.telemetry.getHistogramById(aHistogramId);
  7490     histogram.add(aValue);
  7491   },
  7492 };
  7494 let Reader = {
  7495   // Version of the cache database schema
  7496   DB_VERSION: 1,
  7498   DEBUG: 0,
  7500   READER_ADD_SUCCESS: 0,
  7501   READER_ADD_FAILED: 1,
  7502   READER_ADD_DUPLICATE: 2,
  7504   // Don't try to parse the page if it has too many elements (for memory and
  7505   // performance reasons)
  7506   MAX_ELEMS_TO_PARSE: 3000,
  7508   isEnabledForParseOnLoad: false,
  7510   init: function Reader_init() {
  7511     this.log("Init()");
  7512     this._requests = {};
  7514     this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
  7516     Services.obs.addObserver(this, "Reader:Add", false);
  7517     Services.obs.addObserver(this, "Reader:Remove", false);
  7519     Services.prefs.addObserver("reader.parse-on-load.", this, false);
  7520   },
  7522   pageAction: {
  7523     readerModeCallback: function(){
  7524       sendMessageToJava({
  7525         type: "Reader:Click",
  7526       });
  7527     },
  7529     readerModeActiveCallback: function(){
  7530       sendMessageToJava({
  7531         type: "Reader:LongClick",
  7532       });
  7534       // Create a relative timestamp for telemetry
  7535       let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
  7536       UITelemetry.addEvent("save.1", "pageaction", uptime, "reader");
  7537     },
  7538   },
  7540   updatePageAction: function(tab) {
  7541     if (this.pageAction.id) {
  7542       NativeWindow.pageactions.remove(this.pageAction.id);
  7543       delete this.pageAction.id;
  7546     // Create a relative timestamp for telemetry
  7547     let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
  7549     if (tab.readerActive) {
  7550       this.pageAction.id = NativeWindow.pageactions.add({
  7551         title: Strings.browser.GetStringFromName("readerMode.exit"),
  7552         icon: "drawable://reader_active",
  7553         clickCallback: this.pageAction.readerModeCallback,
  7554         important: true
  7555       });
  7557       // Only start a reader session if the viewer is in the foreground. We do
  7558       // not track background reader viewers.
  7559       UITelemetry.startSession("reader.1", uptime);
  7560       return;
  7563     // Only stop a reader session if the foreground viewer is not visible.
  7564     UITelemetry.stopSession("reader.1", "", uptime);
  7566     if (tab.readerEnabled) {
  7567       this.pageAction.id = NativeWindow.pageactions.add({
  7568         title: Strings.browser.GetStringFromName("readerMode.enter"),
  7569         icon: "drawable://reader",
  7570         clickCallback:this.pageAction.readerModeCallback,
  7571         longClickCallback: this.pageAction.readerModeActiveCallback,
  7572         important: true
  7573       });
  7575   },
  7577   observe: function(aMessage, aTopic, aData) {
  7578     switch(aTopic) {
  7579       case "Reader:Add": {
  7580         let args = JSON.parse(aData);
  7581         if ('fromAboutReader' in args) {
  7582           // Ignore adds initiated from aboutReader menu banner
  7583           break;
  7586         let tabID = null;
  7587         let url, urlWithoutRef;
  7589         if ('tabID' in args) {
  7590           tabID = args.tabID;
  7592           let tab = BrowserApp.getTabForId(tabID);
  7593           let currentURI = tab.browser.currentURI;
  7595           url = currentURI.spec;
  7596           urlWithoutRef = currentURI.specIgnoringRef;
  7597         } else if ('url' in args) {
  7598           let uri = Services.io.newURI(args.url, null, null);
  7599           url = uri.spec;
  7600           urlWithoutRef = uri.specIgnoringRef;
  7601         } else {
  7602           throw new Error("Reader:Add requires a tabID or an URL as argument");
  7605         let sendResult = function(result, article) {
  7606           article = article || {};
  7607           this.log("Reader:Add success=" + result + ", url=" + url + ", title=" + article.title + ", excerpt=" + article.excerpt);
  7609           sendMessageToJava({
  7610             type: "Reader:Added",
  7611             result: result,
  7612             title: article.title,
  7613             url: url,
  7614             length: article.length,
  7615             excerpt: article.excerpt
  7616           });
  7617         }.bind(this);
  7619         let handleArticle = function(article) {
  7620           if (!article) {
  7621             sendResult(this.READER_ADD_FAILED, null);
  7622             return;
  7625           this.storeArticleInCache(article, function(success) {
  7626             let result = (success ? this.READER_ADD_SUCCESS : this.READER_ADD_FAILED);
  7627             sendResult(result, article);
  7628           }.bind(this));
  7629         }.bind(this);
  7631         this.getArticleFromCache(urlWithoutRef, function (article) {
  7632           // If the article is already in reading list, bail
  7633           if (article) {
  7634             sendResult(this.READER_ADD_DUPLICATE, null);
  7635             return;
  7638           if (tabID != null) {
  7639             this.getArticleForTab(tabID, urlWithoutRef, handleArticle);
  7640           } else {
  7641             this.parseDocumentFromURL(urlWithoutRef, handleArticle);
  7643         }.bind(this));
  7644         break;
  7647       case "Reader:Remove": {
  7648         let url = aData;
  7649         this.removeArticleFromCache(url, function(success) {
  7650           this.log("Reader:Remove success=" + success + ", url=" + url);
  7652           if (success) {
  7653             sendMessageToJava({
  7654               type: "Reader:Removed",
  7655               url: url
  7656             });
  7658         }.bind(this));
  7659         break;
  7662       case "nsPref:changed": {
  7663         if (aData.startsWith("reader.parse-on-load.")) {
  7664           this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
  7666         break;
  7669   },
  7671   getStateForParseOnLoad: function Reader_getStateForParseOnLoad() {
  7672     let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
  7673     let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
  7674     // For low-memory devices, don't allow reader mode since it takes up a lot of memory.
  7675     // See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
  7676     return isForceEnabled || (isEnabled && !BrowserApp.isOnLowMemoryPlatform);
  7677   },
  7679   parseDocumentFromURL: function Reader_parseDocumentFromURL(url, callback) {
  7680     // If there's an on-going request for the same URL, simply append one
  7681     // more callback to it to be called when the request is done.
  7682     if (url in this._requests) {
  7683       let request = this._requests[url];
  7684       request.callbacks.push(callback);
  7685       return;
  7688     let request = { url: url, callbacks: [callback] };
  7689     this._requests[url] = request;
  7691     try {
  7692       this.log("parseDocumentFromURL: " + url);
  7694       // First, try to find a cached parsed article in the DB
  7695       this.getArticleFromCache(url, function(article) {
  7696         if (article) {
  7697           this.log("Page found in cache, return article immediately");
  7698           this._runCallbacksAndFinish(request, article);
  7699           return;
  7702         if (!this._requests) {
  7703           this.log("Reader has been destroyed, abort");
  7704           return;
  7707         // Article hasn't been found in the cache DB, we need to
  7708         // download the page and parse the article out of it.
  7709         this._downloadAndParseDocument(url, request);
  7710       }.bind(this));
  7711     } catch (e) {
  7712       this.log("Error parsing document from URL: " + e);
  7713       this._runCallbacksAndFinish(request, null);
  7715   },
  7717   getArticleForTab: function Reader_getArticleForTab(tabId, url, callback) {
  7718     let tab = BrowserApp.getTabForId(tabId);
  7719     if (tab) {
  7720       let article = tab.savedArticle;
  7721       if (article && article.url == url) {
  7722         this.log("Saved article found in tab");
  7723         callback(article);
  7724         return;
  7728     this.parseDocumentFromURL(url, callback);
  7729   },
  7731   parseDocumentFromTab: function(tabId, callback) {
  7732     try {
  7733       this.log("parseDocumentFromTab: " + tabId);
  7735       let tab = BrowserApp.getTabForId(tabId);
  7736       let url = tab.browser.contentWindow.location.href;
  7737       let uri = Services.io.newURI(url, null, null);
  7739       if (!this._shouldCheckUri(uri)) {
  7740         callback(null);
  7741         return;
  7744       // First, try to find a cached parsed article in the DB
  7745       this.getArticleFromCache(url, function(article) {
  7746         if (article) {
  7747           this.log("Page found in cache, return article immediately");
  7748           callback(article);
  7749           return;
  7752         let doc = tab.browser.contentWindow.document;
  7753         this._readerParse(uri, doc, function (article) {
  7754           if (!article) {
  7755             this.log("Failed to parse page");
  7756             callback(null);
  7757             return;
  7760           callback(article);
  7761         }.bind(this));
  7762       }.bind(this));
  7763     } catch (e) {
  7764       this.log("Error parsing document from tab: " + e);
  7765       callback(null);
  7767   },
  7769   getArticleFromCache: function Reader_getArticleFromCache(url, callback) {
  7770     this._getCacheDB(function(cacheDB) {
  7771       if (!cacheDB) {
  7772         callback(false);
  7773         return;
  7776       let transaction = cacheDB.transaction(cacheDB.objectStoreNames);
  7777       let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
  7779       let request = articles.get(url);
  7781       request.onerror = function(event) {
  7782         this.log("Error getting article from the cache DB: " + url);
  7783         callback(null);
  7784       }.bind(this);
  7786       request.onsuccess = function(event) {
  7787         this.log("Got article from the cache DB: " + event.target.result);
  7788         callback(event.target.result);
  7789       }.bind(this);
  7790     }.bind(this));
  7791   },
  7793   storeArticleInCache: function Reader_storeArticleInCache(article, callback) {
  7794     this._getCacheDB(function(cacheDB) {
  7795       if (!cacheDB) {
  7796         callback(false);
  7797         return;
  7800       let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
  7801       let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
  7803       let request = articles.add(article);
  7805       request.onerror = function(event) {
  7806         this.log("Error storing article in the cache DB: " + article.url);
  7807         callback(false);
  7808       }.bind(this);
  7810       request.onsuccess = function(event) {
  7811         this.log("Stored article in the cache DB: " + article.url);
  7812         callback(true);
  7813       }.bind(this);
  7814     }.bind(this));
  7815   },
  7817   removeArticleFromCache: function Reader_removeArticleFromCache(url, callback) {
  7818     this._getCacheDB(function(cacheDB) {
  7819       if (!cacheDB) {
  7820         callback(false);
  7821         return;
  7824       let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
  7825       let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
  7827       let request = articles.delete(url);
  7829       request.onerror = function(event) {
  7830         this.log("Error removing article from the cache DB: " + url);
  7831         callback(false);
  7832       }.bind(this);
  7834       request.onsuccess = function(event) {
  7835         this.log("Removed article from the cache DB: " + url);
  7836         callback(true);
  7837       }.bind(this);
  7838     }.bind(this));
  7839   },
  7841   uninit: function Reader_uninit() {
  7842     Services.prefs.removeObserver("reader.parse-on-load.", this);
  7844     Services.obs.removeObserver(this, "Reader:Add");
  7845     Services.obs.removeObserver(this, "Reader:Remove");
  7847     let requests = this._requests;
  7848     for (let url in requests) {
  7849       let request = requests[url];
  7850       if (request.browser) {
  7851         let browser = request.browser;
  7852         browser.parentNode.removeChild(browser);
  7855     delete this._requests;
  7857     if (this._cacheDB) {
  7858       this._cacheDB.close();
  7859       delete this._cacheDB;
  7861   },
  7863   log: function(msg) {
  7864     if (this.DEBUG)
  7865       dump("Reader: " + msg);
  7866   },
  7868   _shouldCheckUri: function Reader_shouldCheckUri(uri) {
  7869     if ((uri.prePath + "/") === uri.spec) {
  7870       this.log("Not parsing home page: " + uri.spec);
  7871       return false;
  7874     if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
  7875       this.log("Not parsing URI scheme: " + uri.scheme);
  7876       return false;
  7879     return true;
  7880   },
  7882   _readerParse: function Reader_readerParse(uri, doc, callback) {
  7883     let numTags = doc.getElementsByTagName("*").length;
  7884     if (numTags > this.MAX_ELEMS_TO_PARSE) {
  7885       this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
  7886       callback(null);
  7887       return;
  7890     let worker = new ChromeWorker("readerWorker.js");
  7891     worker.onmessage = function (evt) {
  7892       let article = evt.data;
  7894       // Append URL to the article data. specIgnoringRef will ignore any hash
  7895       // in the URL.
  7896       if (article) {
  7897         article.url = uri.specIgnoringRef;
  7898         let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
  7899         article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
  7900                                                         .convertToPlainText(article.title, flags, 0);
  7903       callback(article);
  7904     };
  7906     try {
  7907       worker.postMessage({
  7908         uri: {
  7909           spec: uri.spec,
  7910           host: uri.host,
  7911           prePath: uri.prePath,
  7912           scheme: uri.scheme,
  7913           pathBase: Services.io.newURI(".", null, uri).spec
  7914         },
  7915         doc: new XMLSerializer().serializeToString(doc)
  7916       });
  7917     } catch (e) {
  7918       dump("Reader: could not build Readability arguments: " + e);
  7919       callback(null);
  7921   },
  7923   _runCallbacksAndFinish: function Reader_runCallbacksAndFinish(request, result) {
  7924     delete this._requests[request.url];
  7926     request.callbacks.forEach(function(callback) {
  7927       callback(result);
  7928     });
  7929   },
  7931   _downloadDocument: function Reader_downloadDocument(url, callback) {
  7932     // We want to parse those arbitrary pages safely, outside the privileged
  7933     // context of chrome. We create a hidden browser element to fetch the
  7934     // loaded page's document object then discard the browser element.
  7936     let browser = document.createElement("browser");
  7937     browser.setAttribute("type", "content");
  7938     browser.setAttribute("collapsed", "true");
  7939     browser.setAttribute("disablehistory", "true");
  7941     document.documentElement.appendChild(browser);
  7942     browser.stop();
  7944     browser.webNavigation.allowAuth = false;
  7945     browser.webNavigation.allowImages = false;
  7946     browser.webNavigation.allowJavascript = false;
  7947     browser.webNavigation.allowMetaRedirects = true;
  7948     browser.webNavigation.allowPlugins = false;
  7950     browser.addEventListener("DOMContentLoaded", function (event) {
  7951       let doc = event.originalTarget;
  7953       // ignore on frames and other documents
  7954       if (doc != browser.contentDocument)
  7955         return;
  7957       this.log("Done loading: " + doc);
  7958       if (doc.location.href == "about:blank") {
  7959         callback(null);
  7961         // Request has finished with error, remove browser element
  7962         browser.parentNode.removeChild(browser);
  7963         return;
  7966       callback(doc);
  7967     }.bind(this));
  7969     browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
  7970                              null, null, null);
  7972     return browser;
  7973   },
  7975   _downloadAndParseDocument: function Reader_downloadAndParseDocument(url, request) {
  7976     try {
  7977       this.log("Needs to fetch page, creating request: " + url);
  7979       request.browser = this._downloadDocument(url, function(doc) {
  7980         this.log("Finished loading page: " + doc);
  7982         if (!doc) {
  7983           this.log("Error loading page");
  7984           this._runCallbacksAndFinish(request, null);
  7985           return;
  7988         this.log("Parsing response with Readability");
  7990         let uri = Services.io.newURI(url, null, null);
  7991         this._readerParse(uri, doc, function (article) {
  7992           // Delete reference to the browser element as we've finished parsing.
  7993           let browser = request.browser;
  7994           if (browser) {
  7995             browser.parentNode.removeChild(browser);
  7996             delete request.browser;
  7999           if (!article) {
  8000             this.log("Failed to parse page");
  8001             this._runCallbacksAndFinish(request, null);
  8002             return;
  8005           this.log("Parsing has been successful");
  8007           this._runCallbacksAndFinish(request, article);
  8008         }.bind(this));
  8009       }.bind(this));
  8010     } catch (e) {
  8011       this.log("Error downloading and parsing document: " + e);
  8012       this._runCallbacksAndFinish(request, null);
  8014   },
  8016   _getCacheDB: function Reader_getCacheDB(callback) {
  8017     if (this._cacheDB) {
  8018       callback(this._cacheDB);
  8019       return;
  8022     let request = window.indexedDB.open("about:reader", this.DB_VERSION);
  8024     request.onerror = function(event) {
  8025       this.log("Error connecting to the cache DB");
  8026       this._cacheDB = null;
  8027       callback(null);
  8028     }.bind(this);
  8030     request.onsuccess = function(event) {
  8031       this.log("Successfully connected to the cache DB");
  8032       this._cacheDB = event.target.result;
  8033       callback(this._cacheDB);
  8034     }.bind(this);
  8036     request.onupgradeneeded = function(event) {
  8037       this.log("Database schema upgrade from " +
  8038            event.oldVersion + " to " + event.newVersion);
  8040       let cacheDB = event.target.result;
  8042       // Create the articles object store
  8043       this.log("Creating articles object store");
  8044       cacheDB.createObjectStore("articles", { keyPath: "url" });
  8046       this.log("Database upgrade done: " + this.DB_VERSION);
  8047     }.bind(this);
  8049 };
  8051 var ExternalApps = {
  8052   _contextMenuId: null,
  8054   // extend _getLink to pickup html5 media links.
  8055   _getMediaLink: function(aElement) {
  8056     let uri = NativeWindow.contextmenus._getLink(aElement);
  8057     if (uri == null && aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement)) {
  8058       try {
  8059         let mediaSrc = aElement.currentSrc || aElement.src;
  8060         uri = ContentAreaUtils.makeURI(mediaSrc, null, null);
  8061       } catch (e) {}
  8063     return uri;
  8064   },
  8066   init: function helper_init() {
  8067     this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) {
  8068       let uri = null;
  8069       var node = aElement;
  8070       while (node && !uri) {
  8071         uri = ExternalApps._getMediaLink(node);
  8072         node = node.parentNode;
  8074       let apps = [];
  8075       if (uri)
  8076         apps = HelperApps.getAppsForUri(uri);
  8078       return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) :
  8079                                 Strings.browser.GetStringFromName("helperapps.openWithList2");
  8080     }, this.filter, this.openExternal);
  8081   },
  8083   uninit: function helper_uninit() {
  8084     if (this._contextMenuId !== null) {
  8085       NativeWindow.contextmenus.remove(this._contextMenuId);
  8087     this._contextMenuId = null;
  8088   },
  8090   filter: {
  8091     matches: function(aElement) {
  8092       let uri = ExternalApps._getMediaLink(aElement);
  8093       let apps = [];
  8094       if (uri) {
  8095         apps = HelperApps.getAppsForUri(uri);
  8097       return apps.length > 0;
  8099   },
  8101   openExternal: function(aElement) {
  8102     let uri = ExternalApps._getMediaLink(aElement);
  8103     HelperApps.launchUri(uri);
  8104   },
  8106   shouldCheckUri: function(uri) {
  8107     if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
  8108       return false;
  8111     return true;
  8112   },
  8114   updatePageAction: function updatePageAction(uri) {
  8115     HelperApps.getAppsForUri(uri, { filterHttp: true }, (apps) => {
  8116       this.clearPageAction();
  8117       if (apps.length > 0)
  8118         this._setUriForPageAction(uri, apps);
  8119     });
  8120   },
  8122   updatePageActionUri: function updatePageActionUri(uri) {
  8123     this._pageActionUri = uri;
  8124   },
  8126   _setUriForPageAction: function setUriForPageAction(uri, apps) {
  8127     this.updatePageActionUri(uri);
  8129     // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
  8130     if (this._pageActionId != undefined)
  8131       return;
  8133     this._pageActionId = NativeWindow.pageactions.add({
  8134       title: Strings.browser.GetStringFromName("openInApp.pageAction"),
  8135       icon: "drawable://icon_openinapp",
  8137       clickCallback: () => {
  8138         // Create a relative timestamp for telemetry
  8139         let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
  8140         UITelemetry.addEvent("launch.1", "pageaction", uptime, "helper");
  8142         if (apps.length > 1) {
  8143           // Use the HelperApps prompt here to filter out any Http handlers
  8144           HelperApps.prompt(apps, {
  8145             title: Strings.browser.GetStringFromName("openInApp.pageAction"),
  8146             buttons: [
  8147               Strings.browser.GetStringFromName("openInApp.ok"),
  8148               Strings.browser.GetStringFromName("openInApp.cancel")
  8150           }, (result) => {
  8151             if (result.button != 0) {
  8152               return;
  8154             apps[result.icongrid0].launch(this._pageActionUri);
  8155           });
  8156         } else {
  8157           apps[0].launch(this._pageActionUri);
  8160     });
  8161   },
  8163   clearPageAction: function clearPageAction() {
  8164     if(!this._pageActionId)
  8165       return;
  8167     NativeWindow.pageactions.remove(this._pageActionId);
  8168     delete this._pageActionId;
  8169   },
  8170 };
  8172 var Distribution = {
  8173   // File used to store campaign data
  8174   _file: null,
  8176   init: function dc_init() {
  8177     Services.obs.addObserver(this, "Distribution:Set", false);
  8178     Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
  8179     Services.obs.addObserver(this, "Campaign:Set", false);
  8181     // Look for file outside the APK:
  8182     // /data/data/org.mozilla.xxx/distribution.json
  8183     this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
  8184     this._file.append("distribution.json");
  8185     this.readJSON(this._file, this.update);
  8186   },
  8188   uninit: function dc_uninit() {
  8189     Services.obs.removeObserver(this, "Distribution:Set");
  8190     Services.obs.removeObserver(this, "prefservice:after-app-defaults");
  8191     Services.obs.removeObserver(this, "Campaign:Set");
  8192   },
  8194   observe: function dc_observe(aSubject, aTopic, aData) {
  8195     switch (aTopic) {
  8196       case "Distribution:Set":
  8197         // Reload the default prefs so we can observe "prefservice:after-app-defaults"
  8198         Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
  8199         break;
  8201       case "prefservice:after-app-defaults":
  8202         this.getPrefs();
  8203         break;
  8205       case "Campaign:Set": {
  8206         // Update the prefs for this session
  8207         try {
  8208           this.update(JSON.parse(aData));
  8209         } catch (ex) {
  8210           Cu.reportError("Distribution: Could not parse JSON: " + ex);
  8211           return;
  8214         // Asynchronously copy the data to the file.
  8215         let array = new TextEncoder().encode(aData);
  8216         OS.File.writeAtomic(this._file.path, array, { tmpPath: this._file.path + ".tmp" });
  8217         break;
  8220   },
  8222   update: function dc_update(aData) {
  8223     // Force the distribution preferences on the default branch
  8224     let defaults = Services.prefs.getDefaultBranch(null);
  8225     defaults.setCharPref("distribution.id", aData.id);
  8226     defaults.setCharPref("distribution.version", aData.version);
  8227   },
  8229   getPrefs: function dc_getPrefs() {
  8230     // Get the distribution directory, and bail if it doesn't exist.
  8231     let file = FileUtils.getDir("XREAppDist", [], false);
  8232     if (!file.exists())
  8233       return;
  8235     file.append("preferences.json");
  8236     this.readJSON(file, this.applyPrefs);
  8237   },
  8239   applyPrefs: function dc_applyPrefs(aData) {
  8240     // Check for required Global preferences
  8241     let global = aData["Global"];
  8242     if (!(global && global["id"] && global["version"] && global["about"])) {
  8243       Cu.reportError("Distribution: missing or incomplete Global preferences");
  8244       return;
  8247     // Force the distribution preferences on the default branch
  8248     let defaults = Services.prefs.getDefaultBranch(null);
  8249     defaults.setCharPref("distribution.id", global["id"]);
  8250     defaults.setCharPref("distribution.version", global["version"]);
  8252     let locale = Services.prefs.getCharPref("general.useragent.locale");
  8253     let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
  8254     aboutString.data = global["about." + locale] || global["about"];
  8255     defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString);
  8257     let prefs = aData["Preferences"];
  8258     for (let key in prefs) {
  8259       try {
  8260         let value = prefs[key];
  8261         switch (typeof value) {
  8262           case "boolean":
  8263             defaults.setBoolPref(key, value);
  8264             break;
  8265           case "number":
  8266             defaults.setIntPref(key, value);
  8267             break;
  8268           case "string":
  8269           case "undefined":
  8270             defaults.setCharPref(key, value);
  8271             break;
  8273       } catch (e) { /* ignore bad prefs and move on */ }
  8276     // Apply a lightweight theme if necessary
  8277     if (prefs["lightweightThemes.isThemeSelected"])
  8278       Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
  8280     let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
  8281     let localizeablePrefs = aData["LocalizablePreferences"];
  8282     for (let key in localizeablePrefs) {
  8283       try {
  8284         let value = localizeablePrefs[key];
  8285         value = value.replace("%LOCALE%", locale, "g");
  8286         localizedString.data = "data:text/plain," + key + "=" + value;
  8287         defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
  8288       } catch (e) { /* ignore bad prefs and move on */ }
  8291     let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale];
  8292     for (let key in localizeablePrefsOverrides) {
  8293       try {
  8294         let value = localizeablePrefsOverrides[key];
  8295         localizedString.data = "data:text/plain," + key + "=" + value;
  8296         defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
  8297       } catch (e) { /* ignore bad prefs and move on */ }
  8300     sendMessageToJava({ type: "Distribution:Set:OK" });
  8301   },
  8303   // aFile is an nsIFile
  8304   // aCallback takes the parsed JSON object as a parameter
  8305   readJSON: function dc_readJSON(aFile, aCallback) {
  8306     Task.spawn(function() {
  8307       let bytes = yield OS.File.read(aFile.path);
  8308       let raw = new TextDecoder().decode(bytes) || "";
  8310       try {
  8311         aCallback(JSON.parse(raw));
  8312       } catch (e) {
  8313         Cu.reportError("Distribution: Could not parse JSON: " + e);
  8315     }).then(null, function onError(reason) {
  8316       if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
  8317         Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
  8319     });
  8321 };
  8323 var Tabs = {
  8324   _enableTabExpiration: false,
  8325   _domains: new Set(),
  8327   init: function() {
  8328     // On low-memory platforms, always allow tab expiration. On high-mem
  8329     // platforms, allow it to be turned on once we hit a low-mem situation.
  8330     if (BrowserApp.isOnLowMemoryPlatform) {
  8331       this._enableTabExpiration = true;
  8332     } else {
  8333       Services.obs.addObserver(this, "memory-pressure", false);
  8336     Services.obs.addObserver(this, "Session:Prefetch", false);
  8338     BrowserApp.deck.addEventListener("pageshow", this, false);
  8339     BrowserApp.deck.addEventListener("TabOpen", this, false);
  8340   },
  8342   uninit: function() {
  8343     if (!this._enableTabExpiration) {
  8344       // If _enableTabExpiration is true then we won't have this
  8345       // observer registered any more.
  8346       Services.obs.removeObserver(this, "memory-pressure");
  8349     Services.obs.removeObserver(this, "Session:Prefetch");
  8351     BrowserApp.deck.removeEventListener("pageshow", this);
  8352     BrowserApp.deck.removeEventListener("TabOpen", this);
  8353   },
  8355   observe: function(aSubject, aTopic, aData) {
  8356     switch (aTopic) {
  8357       case "memory-pressure":
  8358         if (aData != "heap-minimize") {
  8359           // We received a low-memory related notification. This will enable
  8360           // expirations.
  8361           this._enableTabExpiration = true;
  8362           Services.obs.removeObserver(this, "memory-pressure");
  8363         } else {
  8364           // Use "heap-minimize" as a trigger to expire the most stale tab.
  8365           this.expireLruTab();
  8367         break;
  8368       case "Session:Prefetch":
  8369         if (aData) {
  8370           let uri = Services.io.newURI(aData, null, null);
  8371           if (uri && !this._domains.has(uri.host)) {
  8372             try {
  8373               Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
  8374               this._domains.add(uri.host);
  8375             } catch (e) {}
  8378         break;
  8380   },
  8382   handleEvent: function(aEvent) {
  8383     switch (aEvent.type) {
  8384       case "pageshow":
  8385         // Clear the domain cache whenever a page get loaded into any browser.
  8386         this._domains.clear();
  8387         break;
  8388       case "TabOpen":
  8389         // Use opening a new tab as a trigger to expire the most stale tab.
  8390         this.expireLruTab();
  8391         break;
  8393   },
  8395   // Manage the most-recently-used list of tabs. Each tab has a timestamp
  8396   // associated with it that indicates when it was last touched.
  8397   expireLruTab: function() {
  8398     if (!this._enableTabExpiration) {
  8399       return false;
  8401     let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
  8402     if (expireTimeMs < 0) {
  8403       // This behaviour is disabled.
  8404       return false;
  8406     let tabs = BrowserApp.tabs;
  8407     let selected = BrowserApp.selectedTab;
  8408     let lruTab = null;
  8409     // Find the least recently used non-zombie tab.
  8410     for (let i = 0; i < tabs.length; i++) {
  8411       if (tabs[i] == selected || tabs[i].browser.__SS_restore) {
  8412         // This tab is selected or already a zombie, skip it.
  8413         continue;
  8415       if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
  8416         lruTab = tabs[i];
  8419     // If the tab was last touched more than browser.tabs.expireTime seconds ago,
  8420     // zombify it.
  8421     if (lruTab) {
  8422       let tabAgeMs = Date.now() - lruTab.lastTouchedAt;
  8423       if (tabAgeMs > expireTimeMs) {
  8424         MemoryObserver.zombify(lruTab);
  8425         Telemetry.addData("FENNEC_TAB_EXPIRED", tabAgeMs / 1000);
  8426         return true;
  8429     return false;
  8430   },
  8432   // For debugging
  8433   dump: function(aPrefix) {
  8434     let tabs = BrowserApp.tabs;
  8435     for (let i = 0; i < tabs.length; i++) {
  8436       dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore);
  8438   },
  8439 };
  8441 function ContextMenuItem(args) {
  8442   this.id = uuidgen.generateUUID().toString();
  8443   this.args = args;
  8446 ContextMenuItem.prototype = {
  8447   get order() {
  8448     return this.args.order || 0;
  8449   },
  8451   matches: function(elt, x, y) {
  8452     return this.args.selector.matches(elt, x, y);
  8453   },
  8455   callback: function(elt) {
  8456     this.args.callback(elt);
  8457   },
  8459   addVal: function(name, elt, defaultValue) {
  8460     if (!(name in this.args))
  8461       return defaultValue;
  8463     if (typeof this.args[name] == "function")
  8464       return this.args[name](elt);
  8466     return this.args[name];
  8467   },
  8469   getValue: function(elt) {
  8470     return {
  8471       id: this.id,
  8472       label: this.addVal("label", elt),
  8473       showAsActions: this.addVal("showAsActions", elt),
  8474       icon: this.addVal("icon", elt),
  8475       isGroup: this.addVal("isGroup", elt, false),
  8476       inGroup: this.addVal("inGroup", elt, false),
  8477       disabled: this.addVal("disabled", elt, false),
  8478       selected: this.addVal("selected", elt, false),
  8479       isParent: this.addVal("isParent", elt, false),
  8480     };
  8484 function HTMLContextMenuItem(elt, target) {
  8485   ContextMenuItem.call(this, { });
  8487   this.menuElementRef = Cu.getWeakReference(elt);
  8488   this.targetElementRef = Cu.getWeakReference(target);
  8491 HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
  8492   order: {
  8493     value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER
  8494   },
  8496   matches: {
  8497     value: function(target) {
  8498       let t = this.targetElementRef.get();
  8499       return t === target;
  8500     },
  8501   },
  8503   callback: {
  8504     value: function(target) {
  8505       let elt = this.menuElementRef.get();
  8506       if (!elt) {
  8507         return;
  8510       // If this is a menu item, show a new context menu with the submenu in it
  8511       if (elt instanceof Ci.nsIDOMHTMLMenuElement) {
  8512         try {
  8513           NativeWindow.contextmenus.menus = {};
  8515           let elt = this.menuElementRef.get();
  8516           let target = this.targetElementRef.get();
  8517           if (!elt) {
  8518             return;
  8521           var items = NativeWindow.contextmenus._getHTMLContextMenuItemsForMenu(elt, target);
  8522           // This menu will always only have one context, but we still make sure its the "right" one.
  8523           var context = NativeWindow.contextmenus._getContextType(target);
  8524           if (items.length > 0) {
  8525             NativeWindow.contextmenus._addMenuItems(items, context);
  8528         } catch(ex) {
  8529           Cu.reportError(ex);
  8531       } else {
  8532         // otherwise just click the menu item
  8533         elt.click();
  8535     },
  8536   },
  8538   getValue: {
  8539     value: function(target) {
  8540       let elt = this.menuElementRef.get();
  8541       if (!elt) {
  8542         return null;
  8545       if (elt.hasAttribute("hidden")) {
  8546         return null;
  8549       return {
  8550         id: this.id,
  8551         icon: elt.icon,
  8552         label: elt.label,
  8553         disabled: elt.disabled,
  8554         menu: elt instanceof Ci.nsIDOMHTMLMenuElement
  8555       };
  8557   },
  8558 });

mercurial