1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/chrome/content/browser.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,8558 @@ 1.4 +#filter substitution 1.5 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 +"use strict"; 1.10 + 1.11 +let Cc = Components.classes; 1.12 +let Ci = Components.interfaces; 1.13 +let Cu = Components.utils; 1.14 +let Cr = Components.results; 1.15 + 1.16 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.17 +Cu.import("resource://gre/modules/Services.jsm"); 1.18 +Cu.import("resource://gre/modules/AddonManager.jsm"); 1.19 +Cu.import("resource://gre/modules/FileUtils.jsm"); 1.20 +Cu.import("resource://gre/modules/JNI.jsm"); 1.21 +Cu.import('resource://gre/modules/Payment.jsm'); 1.22 +Cu.import("resource://gre/modules/NotificationDB.jsm"); 1.23 +Cu.import("resource://gre/modules/SpatialNavigation.jsm"); 1.24 +Cu.import("resource://gre/modules/UITelemetry.jsm"); 1.25 + 1.26 +#ifdef ACCESSIBILITY 1.27 +Cu.import("resource://gre/modules/accessibility/AccessFu.jsm"); 1.28 +#endif 1.29 + 1.30 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", 1.31 + "resource://gre/modules/PluralForm.jsm"); 1.32 + 1.33 +XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", 1.34 + "resource://gre/modules/Messaging.jsm"); 1.35 + 1.36 +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", 1.37 + "resource://gre/modules/devtools/dbg-server.jsm"); 1.38 + 1.39 +XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides", 1.40 + "resource://gre/modules/UserAgentOverrides.jsm"); 1.41 + 1.42 +XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", 1.43 + "resource://gre/modules/LoginManagerContent.jsm"); 1.44 + 1.45 +XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); 1.46 +XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); 1.47 + 1.48 +#ifdef MOZ_SAFE_BROWSING 1.49 +XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", 1.50 + "resource://gre/modules/SafeBrowsing.jsm"); 1.51 +#endif 1.52 + 1.53 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", 1.54 + "resource://gre/modules/PrivateBrowsingUtils.jsm"); 1.55 + 1.56 +XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer", 1.57 + "resource://gre/modules/Sanitizer.jsm"); 1.58 + 1.59 +XPCOMUtils.defineLazyModuleGetter(this, "Prompt", 1.60 + "resource://gre/modules/Prompt.jsm"); 1.61 + 1.62 +XPCOMUtils.defineLazyModuleGetter(this, "HelperApps", 1.63 + "resource://gre/modules/HelperApps.jsm"); 1.64 + 1.65 +XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions", 1.66 + "resource://gre/modules/SSLExceptions.jsm"); 1.67 + 1.68 +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", 1.69 + "resource://gre/modules/FormHistory.jsm"); 1.70 + 1.71 +XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", 1.72 + "@mozilla.org/uuid-generator;1", 1.73 + "nsIUUIDGenerator"); 1.74 + 1.75 +XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery", 1.76 + "resource://gre/modules/SimpleServiceDiscovery.jsm"); 1.77 + 1.78 +#ifdef NIGHTLY_BUILD 1.79 +XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils", 1.80 + "resource://shumway/ShumwayUtils.jsm"); 1.81 +#endif 1.82 + 1.83 +#ifdef MOZ_ANDROID_SYNTHAPKS 1.84 +XPCOMUtils.defineLazyModuleGetter(this, "WebappManager", 1.85 + "resource://gre/modules/WebappManager.jsm"); 1.86 +#endif 1.87 + 1.88 +XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", 1.89 + "resource://gre/modules/CharsetMenu.jsm"); 1.90 + 1.91 +// Lazily-loaded browser scripts: 1.92 +[ 1.93 + ["SelectHelper", "chrome://browser/content/SelectHelper.js"], 1.94 + ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"], 1.95 + ["AboutReader", "chrome://browser/content/aboutReader.js"], 1.96 + ["MasterPassword", "chrome://browser/content/MasterPassword.js"], 1.97 + ["PluginHelper", "chrome://browser/content/PluginHelper.js"], 1.98 + ["OfflineApps", "chrome://browser/content/OfflineApps.js"], 1.99 + ["Linkifier", "chrome://browser/content/Linkify.js"], 1.100 + ["ZoomHelper", "chrome://browser/content/ZoomHelper.js"], 1.101 + ["CastingApps", "chrome://browser/content/CastingApps.js"], 1.102 +].forEach(function (aScript) { 1.103 + let [name, script] = aScript; 1.104 + XPCOMUtils.defineLazyGetter(window, name, function() { 1.105 + let sandbox = {}; 1.106 + Services.scriptloader.loadSubScript(script, sandbox); 1.107 + return sandbox[name]; 1.108 + }); 1.109 +}); 1.110 + 1.111 +[ 1.112 +#ifdef MOZ_WEBRTC 1.113 + ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"], 1.114 +#endif 1.115 + ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"], 1.116 + ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"], 1.117 + ["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"], 1.118 + ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"], 1.119 + ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"], 1.120 + ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"], 1.121 + ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"], 1.122 +].forEach(function (aScript) { 1.123 + let [name, notifications, script] = aScript; 1.124 + XPCOMUtils.defineLazyGetter(window, name, function() { 1.125 + let sandbox = {}; 1.126 + Services.scriptloader.loadSubScript(script, sandbox); 1.127 + return sandbox[name]; 1.128 + }); 1.129 + notifications.forEach(function (aNotification) { 1.130 + Services.obs.addObserver(function(s, t, d) { 1.131 + window[name].observe(s, t, d) 1.132 + }, aNotification, false); 1.133 + }); 1.134 +}); 1.135 + 1.136 +// Lazily-loaded JS modules that use observer notifications 1.137 +[ 1.138 + ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView", 1.139 + "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"], 1.140 +].forEach(module => { 1.141 + let [name, notifications, resource] = module; 1.142 + XPCOMUtils.defineLazyModuleGetter(this, name, resource); 1.143 + notifications.forEach(notification => { 1.144 + Services.obs.addObserver((s,t,d) => { 1.145 + this[name].observe(s,t,d) 1.146 + }, notification, false); 1.147 + }); 1.148 +}); 1.149 + 1.150 +XPCOMUtils.defineLazyServiceGetter(this, "Haptic", 1.151 + "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback"); 1.152 + 1.153 +XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils", 1.154 + "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils"); 1.155 + 1.156 +XPCOMUtils.defineLazyServiceGetter(window, "URIFixup", 1.157 + "@mozilla.org/docshell/urifixup;1", "nsIURIFixup"); 1.158 + 1.159 +#ifdef MOZ_WEBRTC 1.160 +XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", 1.161 + "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService"); 1.162 +#endif 1.163 + 1.164 +const kStateActive = 0x00000001; // :active pseudoclass for elements 1.165 + 1.166 +const kXLinkNamespace = "http://www.w3.org/1999/xlink"; 1.167 + 1.168 +const kDefaultCSSViewportWidth = 980; 1.169 +const kDefaultCSSViewportHeight = 480; 1.170 + 1.171 +const kViewportRemeasureThrottle = 500; 1.172 + 1.173 +const kDoNotTrackPrefState = Object.freeze({ 1.174 + NO_PREF: "0", 1.175 + DISALLOW_TRACKING: "1", 1.176 + ALLOW_TRACKING: "2", 1.177 +}); 1.178 + 1.179 +function dump(a) { 1.180 + Services.console.logStringMessage(a); 1.181 +} 1.182 + 1.183 +function doChangeMaxLineBoxWidth(aWidth) { 1.184 + gReflowPending = null; 1.185 + let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); 1.186 + let docShell = webNav.QueryInterface(Ci.nsIDocShell); 1.187 + let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); 1.188 + 1.189 + let range = null; 1.190 + if (BrowserApp.selectedTab._mReflozPoint) { 1.191 + range = BrowserApp.selectedTab._mReflozPoint.range; 1.192 + } 1.193 + 1.194 + try { 1.195 + docViewer.pausePainting(); 1.196 + docViewer.changeMaxLineBoxWidth(aWidth); 1.197 + 1.198 + if (range) { 1.199 + ZoomHelper.zoomInAndSnapToRange(range); 1.200 + } else { 1.201 + // In this case, we actually didn't zoom into a specific range. It 1.202 + // probably happened from a page load reflow-on-zoom event, so we 1.203 + // need to make sure painting is re-enabled. 1.204 + BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); 1.205 + } 1.206 + } finally { 1.207 + docViewer.resumePainting(); 1.208 + } 1.209 +} 1.210 + 1.211 +function fuzzyEquals(a, b) { 1.212 + return (Math.abs(a - b) < 1e-6); 1.213 +} 1.214 + 1.215 +/** 1.216 + * Convert a font size to CSS pixels (px) from twentieiths-of-a-point 1.217 + * (twips). 1.218 + */ 1.219 +function convertFromTwipsToPx(aSize) { 1.220 + return aSize/240 * 16.0; 1.221 +} 1.222 + 1.223 +#ifdef MOZ_CRASHREPORTER 1.224 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.225 +XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", 1.226 + "@mozilla.org/xre/app-info;1", "nsICrashReporter"); 1.227 +#endif 1.228 + 1.229 +XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() { 1.230 + let ContentAreaUtils = {}; 1.231 + Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils); 1.232 + return ContentAreaUtils; 1.233 +}); 1.234 + 1.235 +XPCOMUtils.defineLazyModuleGetter(this, "Rect", 1.236 + "resource://gre/modules/Geometry.jsm"); 1.237 + 1.238 +function resolveGeckoURI(aURI) { 1.239 + if (!aURI) 1.240 + throw "Can't resolve an empty uri"; 1.241 + 1.242 + if (aURI.startsWith("chrome://")) { 1.243 + let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]); 1.244 + return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec; 1.245 + } else if (aURI.startsWith("resource://")) { 1.246 + let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler); 1.247 + return handler.resolveURI(Services.io.newURI(aURI, null, null)); 1.248 + } 1.249 + return aURI; 1.250 +} 1.251 + 1.252 +/** 1.253 + * Cache of commonly used string bundles. 1.254 + */ 1.255 +var Strings = {}; 1.256 +[ 1.257 + ["brand", "chrome://branding/locale/brand.properties"], 1.258 + ["browser", "chrome://browser/locale/browser.properties"] 1.259 +].forEach(function (aStringBundle) { 1.260 + let [name, bundle] = aStringBundle; 1.261 + XPCOMUtils.defineLazyGetter(Strings, name, function() { 1.262 + return Services.strings.createBundle(bundle); 1.263 + }); 1.264 +}); 1.265 + 1.266 +const kFormHelperModeDisabled = 0; 1.267 +const kFormHelperModeEnabled = 1; 1.268 +const kFormHelperModeDynamic = 2; // disabled on tablets 1.269 + 1.270 +var BrowserApp = { 1.271 + _tabs: [], 1.272 + _selectedTab: null, 1.273 + _prefObservers: [], 1.274 + isGuest: false, 1.275 + 1.276 + get isTablet() { 1.277 + let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); 1.278 + delete this.isTablet; 1.279 + return this.isTablet = sysInfo.get("tablet"); 1.280 + }, 1.281 + 1.282 + get isOnLowMemoryPlatform() { 1.283 + let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); 1.284 + delete this.isOnLowMemoryPlatform; 1.285 + return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform(); 1.286 + }, 1.287 + 1.288 + deck: null, 1.289 + 1.290 + startup: function startup() { 1.291 + window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess(); 1.292 + dump("zerdatime " + Date.now() + " - browser chrome startup finished."); 1.293 + 1.294 + this.deck = document.getElementById("browsers"); 1.295 + this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() { 1.296 + try { 1.297 + BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false); 1.298 + Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); 1.299 + sendMessageToJava({ type: "Gecko:DelayedStartup" }); 1.300 + } catch(ex) { console.log(ex); } 1.301 + }, false); 1.302 + 1.303 + BrowserEventHandler.init(); 1.304 + ViewportHandler.init(); 1.305 + 1.306 + Services.androidBridge.browserApp = this; 1.307 + 1.308 + Services.obs.addObserver(this, "Locale:Changed", false); 1.309 + Services.obs.addObserver(this, "Tab:Load", false); 1.310 + Services.obs.addObserver(this, "Tab:Selected", false); 1.311 + Services.obs.addObserver(this, "Tab:Closed", false); 1.312 + Services.obs.addObserver(this, "Session:Back", false); 1.313 + Services.obs.addObserver(this, "Session:ShowHistory", false); 1.314 + Services.obs.addObserver(this, "Session:Forward", false); 1.315 + Services.obs.addObserver(this, "Session:Reload", false); 1.316 + Services.obs.addObserver(this, "Session:Stop", false); 1.317 + Services.obs.addObserver(this, "SaveAs:PDF", false); 1.318 + Services.obs.addObserver(this, "Browser:Quit", false); 1.319 + Services.obs.addObserver(this, "Preferences:Set", false); 1.320 + Services.obs.addObserver(this, "ScrollTo:FocusedInput", false); 1.321 + Services.obs.addObserver(this, "Sanitize:ClearData", false); 1.322 + Services.obs.addObserver(this, "FullScreen:Exit", false); 1.323 + Services.obs.addObserver(this, "Viewport:Change", false); 1.324 + Services.obs.addObserver(this, "Viewport:Flush", false); 1.325 + Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false); 1.326 + Services.obs.addObserver(this, "Passwords:Init", false); 1.327 + Services.obs.addObserver(this, "FormHistory:Init", false); 1.328 + Services.obs.addObserver(this, "gather-telemetry", false); 1.329 + Services.obs.addObserver(this, "keyword-search", false); 1.330 +#ifdef MOZ_ANDROID_SYNTHAPKS 1.331 + Services.obs.addObserver(this, "webapps-runtime-install", false); 1.332 + Services.obs.addObserver(this, "webapps-runtime-install-package", false); 1.333 + Services.obs.addObserver(this, "webapps-ask-install", false); 1.334 + Services.obs.addObserver(this, "webapps-launch", false); 1.335 + Services.obs.addObserver(this, "webapps-uninstall", false); 1.336 + Services.obs.addObserver(this, "Webapps:AutoInstall", false); 1.337 + Services.obs.addObserver(this, "Webapps:Load", false); 1.338 + Services.obs.addObserver(this, "Webapps:AutoUninstall", false); 1.339 +#endif 1.340 + Services.obs.addObserver(this, "sessionstore-state-purge-complete", false); 1.341 + 1.342 + function showFullScreenWarning() { 1.343 + NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short"); 1.344 + } 1.345 + 1.346 + window.addEventListener("fullscreen", function() { 1.347 + sendMessageToJava({ 1.348 + type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide" 1.349 + }); 1.350 + }, false); 1.351 + 1.352 + window.addEventListener("mozfullscreenchange", function() { 1.353 + sendMessageToJava({ 1.354 + type: document.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop" 1.355 + }); 1.356 + 1.357 + if (document.mozFullScreen) 1.358 + showFullScreenWarning(); 1.359 + }, false); 1.360 + 1.361 + // When a restricted key is pressed in DOM full-screen mode, we should display 1.362 + // the "Press ESC to exit" warning message. 1.363 + window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true); 1.364 + 1.365 + NativeWindow.init(); 1.366 + LightWeightThemeWebInstaller.init(); 1.367 + Downloads.init(); 1.368 + FormAssistant.init(); 1.369 + IndexedDB.init(); 1.370 + HealthReportStatusListener.init(); 1.371 + XPInstallObserver.init(); 1.372 + CharacterEncoding.init(); 1.373 + ActivityObserver.init(); 1.374 +#ifdef MOZ_ANDROID_SYNTHAPKS 1.375 + // TODO: replace with Android implementation of WebappOSUtils.isLaunchable. 1.376 + Cu.import("resource://gre/modules/Webapps.jsm"); 1.377 + DOMApplicationRegistry.allAppsLaunchable = true; 1.378 +#else 1.379 + WebappsUI.init(); 1.380 +#endif 1.381 + RemoteDebugger.init(); 1.382 + Reader.init(); 1.383 + UserAgentOverrides.init(); 1.384 + DesktopUserAgent.init(); 1.385 + CastingApps.init(); 1.386 + Distribution.init(); 1.387 + Tabs.init(); 1.388 +#ifdef ACCESSIBILITY 1.389 + AccessFu.attach(window); 1.390 +#endif 1.391 +#ifdef NIGHTLY_BUILD 1.392 + ShumwayUtils.init(); 1.393 +#endif 1.394 + 1.395 + // Init LoginManager 1.396 + Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); 1.397 + 1.398 + let url = null; 1.399 + let pinned = false; 1.400 + if ("arguments" in window) { 1.401 + if (window.arguments[0]) 1.402 + url = window.arguments[0]; 1.403 + if (window.arguments[1]) 1.404 + gScreenWidth = window.arguments[1]; 1.405 + if (window.arguments[2]) 1.406 + gScreenHeight = window.arguments[2]; 1.407 + if (window.arguments[3]) 1.408 + pinned = window.arguments[3]; 1.409 + if (window.arguments[4]) 1.410 + this.isGuest = window.arguments[4]; 1.411 + } 1.412 + 1.413 + if (pinned) { 1.414 + this._initRuntime(this._startupStatus, url, aUrl => this.addTab(aUrl)); 1.415 + } else { 1.416 + SearchEngines.init(); 1.417 + this.initContextMenu(); 1.418 + } 1.419 + // The order that context menu items are added is important 1.420 + // Make sure the "Open in App" context menu item appears at the bottom of the list 1.421 + ExternalApps.init(); 1.422 + 1.423 + // XXX maybe we don't do this if the launch was kicked off from external 1.424 + Services.io.offline = false; 1.425 + 1.426 + // Broadcast a UIReady message so add-ons know we are finished with startup 1.427 + let event = document.createEvent("Events"); 1.428 + event.initEvent("UIReady", true, false); 1.429 + window.dispatchEvent(event); 1.430 + 1.431 + if (this._startupStatus) 1.432 + this.onAppUpdated(); 1.433 + 1.434 + // Store the low-precision buffer pref 1.435 + this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer"); 1.436 + 1.437 + // notify java that gecko has loaded 1.438 + sendMessageToJava({ type: "Gecko:Ready" }); 1.439 + 1.440 +#ifdef MOZ_SAFE_BROWSING 1.441 + // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008. 1.442 + setTimeout(function() { SafeBrowsing.init(); }, 5000); 1.443 +#endif 1.444 + }, 1.445 + 1.446 + get _startupStatus() { 1.447 + delete this._startupStatus; 1.448 + 1.449 + let savedMilestone = null; 1.450 + try { 1.451 + savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone"); 1.452 + } catch (e) { 1.453 + } 1.454 +#expand let ourMilestone = "__MOZ_APP_VERSION__"; 1.455 + this._startupStatus = ""; 1.456 + if (ourMilestone != savedMilestone) { 1.457 + Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone); 1.458 + this._startupStatus = savedMilestone ? "upgrade" : "new"; 1.459 + } 1.460 + 1.461 + return this._startupStatus; 1.462 + }, 1.463 + 1.464 + /** 1.465 + * Pass this a locale string, such as "fr" or "es_ES". 1.466 + */ 1.467 + setLocale: function (locale) { 1.468 + console.log("browser.js: requesting locale set: " + locale); 1.469 + sendMessageToJava({ type: "Locale:Set", locale: locale }); 1.470 + }, 1.471 + 1.472 + _initRuntime: function(status, url, callback) { 1.473 + let sandbox = {}; 1.474 + Services.scriptloader.loadSubScript("chrome://browser/content/WebappRT.js", sandbox); 1.475 + window.WebappRT = sandbox.WebappRT; 1.476 + WebappRT.init(status, url, callback); 1.477 + }, 1.478 + 1.479 + initContextMenu: function ba_initContextMenu() { 1.480 + // TODO: These should eventually move into more appropriate classes 1.481 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"), 1.482 + NativeWindow.contextmenus.linkOpenableNonPrivateContext, 1.483 + function(aTarget) { 1.484 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab"); 1.485 + UITelemetry.addEvent("loadurl.1", "contextmenu", null); 1.486 + 1.487 + let url = NativeWindow.contextmenus._getLinkURL(aTarget); 1.488 + ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal); 1.489 + BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id }); 1.490 + 1.491 + let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened"); 1.492 + let label = PluralForm.get(1, newtabStrings).replace("#1", 1); 1.493 + NativeWindow.toast.show(label, "short"); 1.494 + }); 1.495 + 1.496 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInPrivateTab"), 1.497 + NativeWindow.contextmenus.linkOpenableContext, 1.498 + function(aTarget) { 1.499 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_private_tab"); 1.500 + UITelemetry.addEvent("loadurl.1", "contextmenu", null); 1.501 + 1.502 + let url = NativeWindow.contextmenus._getLinkURL(aTarget); 1.503 + ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal); 1.504 + BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true }); 1.505 + 1.506 + let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened"); 1.507 + let label = PluralForm.get(1, newtabStrings).replace("#1", 1); 1.508 + NativeWindow.toast.show(label, "short"); 1.509 + }); 1.510 + 1.511 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyLink"), 1.512 + NativeWindow.contextmenus.linkCopyableContext, 1.513 + function(aTarget) { 1.514 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link"); 1.515 + 1.516 + let url = NativeWindow.contextmenus._getLinkURL(aTarget); 1.517 + NativeWindow.contextmenus._copyStringToDefaultClipboard(url); 1.518 + }); 1.519 + 1.520 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyEmailAddress"), 1.521 + NativeWindow.contextmenus.emailLinkContext, 1.522 + function(aTarget) { 1.523 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email"); 1.524 + 1.525 + let url = NativeWindow.contextmenus._getLinkURL(aTarget); 1.526 + let emailAddr = NativeWindow.contextmenus._stripScheme(url); 1.527 + NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr); 1.528 + }); 1.529 + 1.530 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyPhoneNumber"), 1.531 + NativeWindow.contextmenus.phoneNumberLinkContext, 1.532 + function(aTarget) { 1.533 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone"); 1.534 + 1.535 + let url = NativeWindow.contextmenus._getLinkURL(aTarget); 1.536 + let phoneNumber = NativeWindow.contextmenus._stripScheme(url); 1.537 + NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber); 1.538 + }); 1.539 + 1.540 + NativeWindow.contextmenus.add({ 1.541 + label: Strings.browser.GetStringFromName("contextmenu.shareLink"), 1.542 + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items 1.543 + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext), 1.544 + showAsActions: function(aElement) { 1.545 + return { 1.546 + title: aElement.textContent.trim() || aElement.title.trim(), 1.547 + uri: NativeWindow.contextmenus._getLinkURL(aElement), 1.548 + }; 1.549 + }, 1.550 + icon: "drawable://ic_menu_share", 1.551 + callback: function(aTarget) { 1.552 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link"); 1.553 + } 1.554 + }); 1.555 + 1.556 + NativeWindow.contextmenus.add({ 1.557 + label: Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"), 1.558 + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, 1.559 + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), 1.560 + showAsActions: function(aElement) { 1.561 + let url = NativeWindow.contextmenus._getLinkURL(aElement); 1.562 + let emailAddr = NativeWindow.contextmenus._stripScheme(url); 1.563 + let title = aElement.textContent || aElement.title; 1.564 + return { 1.565 + title: title, 1.566 + uri: emailAddr, 1.567 + }; 1.568 + }, 1.569 + icon: "drawable://ic_menu_share", 1.570 + callback: function(aTarget) { 1.571 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email"); 1.572 + } 1.573 + }); 1.574 + 1.575 + NativeWindow.contextmenus.add({ 1.576 + label: Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"), 1.577 + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, 1.578 + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext), 1.579 + showAsActions: function(aElement) { 1.580 + let url = NativeWindow.contextmenus._getLinkURL(aElement); 1.581 + let phoneNumber = NativeWindow.contextmenus._stripScheme(url); 1.582 + let title = aElement.textContent || aElement.title; 1.583 + return { 1.584 + title: title, 1.585 + uri: phoneNumber, 1.586 + }; 1.587 + }, 1.588 + icon: "drawable://ic_menu_share", 1.589 + callback: function(aTarget) { 1.590 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone"); 1.591 + } 1.592 + }); 1.593 + 1.594 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"), 1.595 + NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), 1.596 + function(aTarget) { 1.597 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email"); 1.598 + 1.599 + let url = NativeWindow.contextmenus._getLinkURL(aTarget); 1.600 + sendMessageToJava({ 1.601 + type: "Contact:Add", 1.602 + email: url 1.603 + }); 1.604 + }); 1.605 + 1.606 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"), 1.607 + NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext), 1.608 + function(aTarget) { 1.609 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone"); 1.610 + 1.611 + let url = NativeWindow.contextmenus._getLinkURL(aTarget); 1.612 + sendMessageToJava({ 1.613 + type: "Contact:Add", 1.614 + phone: url 1.615 + }); 1.616 + }); 1.617 + 1.618 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"), 1.619 + NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext), 1.620 + function(aTarget) { 1.621 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark"); 1.622 + 1.623 + let url = NativeWindow.contextmenus._getLinkURL(aTarget); 1.624 + let title = aTarget.textContent || aTarget.title || url; 1.625 + sendMessageToJava({ 1.626 + type: "Bookmark:Insert", 1.627 + url: url, 1.628 + title: title 1.629 + }); 1.630 + }); 1.631 + 1.632 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.playMedia"), 1.633 + NativeWindow.contextmenus.mediaContext("media-paused"), 1.634 + function(aTarget) { 1.635 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_play"); 1.636 + aTarget.play(); 1.637 + }); 1.638 + 1.639 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.pauseMedia"), 1.640 + NativeWindow.contextmenus.mediaContext("media-playing"), 1.641 + function(aTarget) { 1.642 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause"); 1.643 + aTarget.pause(); 1.644 + }); 1.645 + 1.646 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.showControls2"), 1.647 + NativeWindow.contextmenus.mediaContext("media-hidingcontrols"), 1.648 + function(aTarget) { 1.649 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media"); 1.650 + aTarget.setAttribute("controls", true); 1.651 + }); 1.652 + 1.653 + NativeWindow.contextmenus.add({ 1.654 + label: Strings.browser.GetStringFromName("contextmenu.shareMedia"), 1.655 + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, 1.656 + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")), 1.657 + showAsActions: function(aElement) { 1.658 + let url = (aElement.currentSrc || aElement.src); 1.659 + let title = aElement.textContent || aElement.title; 1.660 + return { 1.661 + title: title, 1.662 + uri: url, 1.663 + type: "video/*", 1.664 + }; 1.665 + }, 1.666 + icon: "drawable://ic_menu_share", 1.667 + callback: function(aTarget) { 1.668 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media"); 1.669 + } 1.670 + }); 1.671 + 1.672 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"), 1.673 + NativeWindow.contextmenus.SelectorContext("video:not(:-moz-full-screen)"), 1.674 + function(aTarget) { 1.675 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen"); 1.676 + aTarget.mozRequestFullScreen(); 1.677 + }); 1.678 + 1.679 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.mute"), 1.680 + NativeWindow.contextmenus.mediaContext("media-unmuted"), 1.681 + function(aTarget) { 1.682 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute"); 1.683 + aTarget.muted = true; 1.684 + }); 1.685 + 1.686 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"), 1.687 + NativeWindow.contextmenus.mediaContext("media-muted"), 1.688 + function(aTarget) { 1.689 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute"); 1.690 + aTarget.muted = false; 1.691 + }); 1.692 + 1.693 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyImageLocation"), 1.694 + NativeWindow.contextmenus.imageLocationCopyableContext, 1.695 + function(aTarget) { 1.696 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image"); 1.697 + 1.698 + let url = aTarget.src; 1.699 + NativeWindow.contextmenus._copyStringToDefaultClipboard(url); 1.700 + }); 1.701 + 1.702 + NativeWindow.contextmenus.add({ 1.703 + label: Strings.browser.GetStringFromName("contextmenu.shareImage"), 1.704 + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext), 1.705 + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items 1.706 + showAsActions: function(aTarget) { 1.707 + let doc = aTarget.ownerDocument; 1.708 + let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools) 1.709 + .getImgCacheForDocument(doc); 1.710 + let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet); 1.711 + let src = aTarget.src; 1.712 + return { 1.713 + title: src, 1.714 + uri: src, 1.715 + type: "image/*", 1.716 + }; 1.717 + }, 1.718 + icon: "drawable://ic_menu_share", 1.719 + menu: true, 1.720 + callback: function(aTarget) { 1.721 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image"); 1.722 + } 1.723 + }); 1.724 + 1.725 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.saveImage"), 1.726 + NativeWindow.contextmenus.imageSaveableContext, 1.727 + function(aTarget) { 1.728 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image"); 1.729 + 1.730 + ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle", 1.731 + false, true, aTarget.ownerDocument.documentURIObject, 1.732 + aTarget.ownerDocument); 1.733 + }); 1.734 + 1.735 + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"), 1.736 + NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext), 1.737 + function(aTarget) { 1.738 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image"); 1.739 + 1.740 + let src = aTarget.src; 1.741 + sendMessageToJava({ 1.742 + type: "Image:SetAs", 1.743 + url: src 1.744 + }); 1.745 + }); 1.746 + 1.747 + NativeWindow.contextmenus.add( 1.748 + function(aTarget) { 1.749 + if (aTarget instanceof HTMLVideoElement) { 1.750 + // If a video element is zero width or height, its essentially 1.751 + // an HTMLAudioElement. 1.752 + if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 ) 1.753 + return Strings.browser.GetStringFromName("contextmenu.saveAudio"); 1.754 + return Strings.browser.GetStringFromName("contextmenu.saveVideo"); 1.755 + } else if (aTarget instanceof HTMLAudioElement) { 1.756 + return Strings.browser.GetStringFromName("contextmenu.saveAudio"); 1.757 + } 1.758 + return Strings.browser.GetStringFromName("contextmenu.saveVideo"); 1.759 + }, NativeWindow.contextmenus.mediaSaveableContext, 1.760 + function(aTarget) { 1.761 + UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media"); 1.762 + 1.763 + let url = aTarget.currentSrc || aTarget.src; 1.764 + let filePickerTitleKey = (aTarget instanceof HTMLVideoElement && 1.765 + (aTarget.videoWidth != 0 && aTarget.videoHeight != 0)) 1.766 + ? "SaveVideoTitle" : "SaveAudioTitle"; 1.767 + // Skipped trying to pull MIME type out of cache for now 1.768 + ContentAreaUtils.internalSave(url, null, null, null, null, false, 1.769 + filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject, 1.770 + aTarget.ownerDocument, true, null); 1.771 + }); 1.772 + }, 1.773 + 1.774 + onAppUpdated: function() { 1.775 + // initialize the form history and passwords databases on upgrades 1.776 + Services.obs.notifyObservers(null, "FormHistory:Init", ""); 1.777 + Services.obs.notifyObservers(null, "Passwords:Init", ""); 1.778 + 1.779 + // Migrate user-set "plugins.click_to_play" pref. See bug 884694. 1.780 + // Because the default value is true, a user-set pref means that the pref was set to false. 1.781 + if (Services.prefs.prefHasUserValue("plugins.click_to_play")) { 1.782 + Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED); 1.783 + Services.prefs.clearUserPref("plugins.click_to_play"); 1.784 + } 1.785 + }, 1.786 + 1.787 + shutdown: function shutdown() { 1.788 + NativeWindow.uninit(); 1.789 + LightWeightThemeWebInstaller.uninit(); 1.790 + FormAssistant.uninit(); 1.791 + IndexedDB.uninit(); 1.792 + ViewportHandler.uninit(); 1.793 + XPInstallObserver.uninit(); 1.794 + HealthReportStatusListener.uninit(); 1.795 + CharacterEncoding.uninit(); 1.796 + SearchEngines.uninit(); 1.797 +#ifndef MOZ_ANDROID_SYNTHAPKS 1.798 + WebappsUI.uninit(); 1.799 +#endif 1.800 + RemoteDebugger.uninit(); 1.801 + Reader.uninit(); 1.802 + UserAgentOverrides.uninit(); 1.803 + DesktopUserAgent.uninit(); 1.804 + ExternalApps.uninit(); 1.805 + CastingApps.uninit(); 1.806 + Distribution.uninit(); 1.807 + Tabs.uninit(); 1.808 + }, 1.809 + 1.810 + // This function returns false during periods where the browser displayed document is 1.811 + // different from the browser content document, so user actions and some kinds of viewport 1.812 + // updates should be ignored. This period starts when we start loading a new page or 1.813 + // switch tabs, and ends when the new browser content document has been drawn and handed 1.814 + // off to the compositor. 1.815 + isBrowserContentDocumentDisplayed: function() { 1.816 + try { 1.817 + if (!Services.androidBridge.isContentDocumentDisplayed()) 1.818 + return false; 1.819 + } catch (e) { 1.820 + return false; 1.821 + } 1.822 + 1.823 + let tab = this.selectedTab; 1.824 + if (!tab) 1.825 + return false; 1.826 + return tab.contentDocumentIsDisplayed; 1.827 + }, 1.828 + 1.829 + contentDocumentChanged: function() { 1.830 + window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true; 1.831 + Services.androidBridge.contentDocumentChanged(); 1.832 + }, 1.833 + 1.834 + get tabs() { 1.835 + return this._tabs; 1.836 + }, 1.837 + 1.838 + get selectedTab() { 1.839 + return this._selectedTab; 1.840 + }, 1.841 + 1.842 + set selectedTab(aTab) { 1.843 + if (this._selectedTab == aTab) 1.844 + return; 1.845 + 1.846 + if (this._selectedTab) { 1.847 + this._selectedTab.setActive(false); 1.848 + } 1.849 + 1.850 + this._selectedTab = aTab; 1.851 + if (!aTab) 1.852 + return; 1.853 + 1.854 + aTab.setActive(true); 1.855 + aTab.setResolution(aTab._zoom, true); 1.856 + this.contentDocumentChanged(); 1.857 + this.deck.selectedPanel = aTab.browser; 1.858 + // Focus the browser so that things like selection will be styled correctly. 1.859 + aTab.browser.focus(); 1.860 + }, 1.861 + 1.862 + get selectedBrowser() { 1.863 + if (this._selectedTab) 1.864 + return this._selectedTab.browser; 1.865 + return null; 1.866 + }, 1.867 + 1.868 + getTabForId: function getTabForId(aId) { 1.869 + let tabs = this._tabs; 1.870 + for (let i=0; i < tabs.length; i++) { 1.871 + if (tabs[i].id == aId) 1.872 + return tabs[i]; 1.873 + } 1.874 + return null; 1.875 + }, 1.876 + 1.877 + getTabForBrowser: function getTabForBrowser(aBrowser) { 1.878 + let tabs = this._tabs; 1.879 + for (let i = 0; i < tabs.length; i++) { 1.880 + if (tabs[i].browser == aBrowser) 1.881 + return tabs[i]; 1.882 + } 1.883 + return null; 1.884 + }, 1.885 + 1.886 + getTabForWindow: function getTabForWindow(aWindow) { 1.887 + let tabs = this._tabs; 1.888 + for (let i = 0; i < tabs.length; i++) { 1.889 + if (tabs[i].browser.contentWindow == aWindow) 1.890 + return tabs[i]; 1.891 + } 1.892 + return null; 1.893 + }, 1.894 + 1.895 + getBrowserForWindow: function getBrowserForWindow(aWindow) { 1.896 + let tabs = this._tabs; 1.897 + for (let i = 0; i < tabs.length; i++) { 1.898 + if (tabs[i].browser.contentWindow == aWindow) 1.899 + return tabs[i].browser; 1.900 + } 1.901 + return null; 1.902 + }, 1.903 + 1.904 + getBrowserForDocument: function getBrowserForDocument(aDocument) { 1.905 + let tabs = this._tabs; 1.906 + for (let i = 0; i < tabs.length; i++) { 1.907 + if (tabs[i].browser.contentDocument == aDocument) 1.908 + return tabs[i].browser; 1.909 + } 1.910 + return null; 1.911 + }, 1.912 + 1.913 + loadURI: function loadURI(aURI, aBrowser, aParams) { 1.914 + aBrowser = aBrowser || this.selectedBrowser; 1.915 + if (!aBrowser) 1.916 + return; 1.917 + 1.918 + aParams = aParams || {}; 1.919 + 1.920 + let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; 1.921 + let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null; 1.922 + let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null; 1.923 + let charset = "charset" in aParams ? aParams.charset : null; 1.924 + 1.925 + let tab = this.getTabForBrowser(aBrowser); 1.926 + if (tab) { 1.927 + if ("userSearch" in aParams) tab.userSearch = aParams.userSearch; 1.928 + } 1.929 + 1.930 + try { 1.931 + aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData); 1.932 + } catch(e) { 1.933 + if (tab) { 1.934 + let message = { 1.935 + type: "Content:LoadError", 1.936 + tabID: tab.id 1.937 + }; 1.938 + sendMessageToJava(message); 1.939 + dump("Handled load error: " + e) 1.940 + } 1.941 + } 1.942 + }, 1.943 + 1.944 + addTab: function addTab(aURI, aParams) { 1.945 + aParams = aParams || {}; 1.946 + 1.947 + let newTab = new Tab(aURI, aParams); 1.948 + this._tabs.push(newTab); 1.949 + 1.950 + let selected = "selected" in aParams ? aParams.selected : true; 1.951 + if (selected) 1.952 + this.selectedTab = newTab; 1.953 + 1.954 + let pinned = "pinned" in aParams ? aParams.pinned : false; 1.955 + if (pinned) { 1.956 + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); 1.957 + ss.setTabValue(newTab, "appOrigin", aURI); 1.958 + } 1.959 + 1.960 + let evt = document.createEvent("UIEvents"); 1.961 + evt.initUIEvent("TabOpen", true, false, window, null); 1.962 + newTab.browser.dispatchEvent(evt); 1.963 + 1.964 + return newTab; 1.965 + }, 1.966 + 1.967 + // Use this method to close a tab from JS. This method sends a message 1.968 + // to Java to close the tab in the Java UI (we'll get a Tab:Closed message 1.969 + // back from Java when that happens). 1.970 + closeTab: function closeTab(aTab) { 1.971 + if (!aTab) { 1.972 + Cu.reportError("Error trying to close tab (tab doesn't exist)"); 1.973 + return; 1.974 + } 1.975 + 1.976 + let message = { 1.977 + type: "Tab:Close", 1.978 + tabID: aTab.id 1.979 + }; 1.980 + sendMessageToJava(message); 1.981 + }, 1.982 + 1.983 +#ifdef MOZ_ANDROID_SYNTHAPKS 1.984 + _loadWebapp: function(aMessage) { 1.985 + 1.986 + this._initRuntime(this._startupStatus, aMessage.url, aUrl => { 1.987 + this.manifestUrl = aMessage.url; 1.988 + this.addTab(aUrl, { title: aMessage.name }); 1.989 + }); 1.990 + }, 1.991 +#endif 1.992 + 1.993 + // Calling this will update the state in BrowserApp after a tab has been 1.994 + // closed in the Java UI. 1.995 + _handleTabClosed: function _handleTabClosed(aTab) { 1.996 + if (aTab == this.selectedTab) 1.997 + this.selectedTab = null; 1.998 + 1.999 + let evt = document.createEvent("UIEvents"); 1.1000 + evt.initUIEvent("TabClose", true, false, window, null); 1.1001 + aTab.browser.dispatchEvent(evt); 1.1002 + 1.1003 + aTab.destroy(); 1.1004 + this._tabs.splice(this._tabs.indexOf(aTab), 1); 1.1005 + }, 1.1006 + 1.1007 + // Use this method to select a tab from JS. This method sends a message 1.1008 + // to Java to select the tab in the Java UI (we'll get a Tab:Selected message 1.1009 + // back from Java when that happens). 1.1010 + selectTab: function selectTab(aTab) { 1.1011 + if (!aTab) { 1.1012 + Cu.reportError("Error trying to select tab (tab doesn't exist)"); 1.1013 + return; 1.1014 + } 1.1015 + 1.1016 + // There's nothing to do if the tab is already selected 1.1017 + if (aTab == this.selectedTab) 1.1018 + return; 1.1019 + 1.1020 + let message = { 1.1021 + type: "Tab:Select", 1.1022 + tabID: aTab.id 1.1023 + }; 1.1024 + sendMessageToJava(message); 1.1025 + }, 1.1026 + 1.1027 + /** 1.1028 + * Gets an open tab with the given URL. 1.1029 + * 1.1030 + * @param aURL URL to look for 1.1031 + * @return the tab with the given URL, or null if no such tab exists 1.1032 + */ 1.1033 + getTabWithURL: function getTabWithURL(aURL) { 1.1034 + let uri = Services.io.newURI(aURL, null, null); 1.1035 + for (let i = 0; i < this._tabs.length; ++i) { 1.1036 + let tab = this._tabs[i]; 1.1037 + if (tab.browser.currentURI.equals(uri)) { 1.1038 + return tab; 1.1039 + } 1.1040 + } 1.1041 + return null; 1.1042 + }, 1.1043 + 1.1044 + /** 1.1045 + * If a tab with the given URL already exists, that tab is selected. 1.1046 + * Otherwise, a new tab is opened with the given URL. 1.1047 + * 1.1048 + * @param aURL URL to open 1.1049 + */ 1.1050 + selectOrOpenTab: function selectOrOpenTab(aURL) { 1.1051 + let tab = this.getTabWithURL(aURL); 1.1052 + if (tab == null) { 1.1053 + this.addTab(aURL); 1.1054 + } else { 1.1055 + this.selectTab(tab); 1.1056 + } 1.1057 + }, 1.1058 + 1.1059 + // This method updates the state in BrowserApp after a tab has been selected 1.1060 + // in the Java UI. 1.1061 + _handleTabSelected: function _handleTabSelected(aTab) { 1.1062 + this.selectedTab = aTab; 1.1063 + 1.1064 + let evt = document.createEvent("UIEvents"); 1.1065 + evt.initUIEvent("TabSelect", true, false, window, null); 1.1066 + aTab.browser.dispatchEvent(evt); 1.1067 + }, 1.1068 + 1.1069 + quit: function quit() { 1.1070 + // Figure out if there's at least one other browser window around. 1.1071 + let lastBrowser = true; 1.1072 + let e = Services.wm.getEnumerator("navigator:browser"); 1.1073 + while (e.hasMoreElements() && lastBrowser) { 1.1074 + let win = e.getNext(); 1.1075 + if (!win.closed && win != window) 1.1076 + lastBrowser = false; 1.1077 + } 1.1078 + 1.1079 + if (lastBrowser) { 1.1080 + // Let everyone know we are closing the last browser window 1.1081 + let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); 1.1082 + Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null); 1.1083 + if (closingCanceled.data) 1.1084 + return; 1.1085 + 1.1086 + Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null); 1.1087 + } 1.1088 + 1.1089 + window.QueryInterface(Ci.nsIDOMChromeWindow).minimize(); 1.1090 + window.close(); 1.1091 + }, 1.1092 + 1.1093 + saveAsPDF: function saveAsPDF(aBrowser) { 1.1094 + // Create the final destination file location 1.1095 + let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null); 1.1096 + fileName = fileName.trim() + ".pdf"; 1.1097 + 1.1098 + let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); 1.1099 + let downloadsDir = dm.defaultDownloadsDirectory; 1.1100 + 1.1101 + let file = downloadsDir.clone(); 1.1102 + file.append(fileName); 1.1103 + file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8)); 1.1104 + 1.1105 + let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings; 1.1106 + printSettings.printSilent = true; 1.1107 + printSettings.showPrintProgress = false; 1.1108 + printSettings.printBGImages = true; 1.1109 + printSettings.printBGColors = true; 1.1110 + printSettings.printToFile = true; 1.1111 + printSettings.toFileName = file.path; 1.1112 + printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs; 1.1113 + printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; 1.1114 + 1.1115 + //XXX we probably need a preference here, the header can be useful 1.1116 + printSettings.footerStrCenter = ""; 1.1117 + printSettings.footerStrLeft = ""; 1.1118 + printSettings.footerStrRight = ""; 1.1119 + printSettings.headerStrCenter = ""; 1.1120 + printSettings.headerStrLeft = ""; 1.1121 + printSettings.headerStrRight = ""; 1.1122 + 1.1123 + // Create a valid mimeInfo for the PDF 1.1124 + let ms = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); 1.1125 + let mimeInfo = ms.getFromTypeAndExtension("application/pdf", "pdf"); 1.1126 + 1.1127 + let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.1128 + .getInterface(Ci.nsIWebBrowserPrint); 1.1129 + 1.1130 + let cancelable = { 1.1131 + cancel: function (aReason) { 1.1132 + webBrowserPrint.cancel(); 1.1133 + } 1.1134 + } 1.1135 + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aBrowser.contentWindow); 1.1136 + let download = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD, 1.1137 + aBrowser.currentURI, 1.1138 + Services.io.newFileURI(file), "", mimeInfo, 1.1139 + Date.now() * 1000, null, cancelable, isPrivate); 1.1140 + 1.1141 + webBrowserPrint.print(printSettings, download); 1.1142 + }, 1.1143 + 1.1144 + notifyPrefObservers: function(aPref) { 1.1145 + this._prefObservers[aPref].forEach(function(aRequestId) { 1.1146 + this.getPreferences(aRequestId, [aPref], 1); 1.1147 + }, this); 1.1148 + }, 1.1149 + 1.1150 + handlePreferencesRequest: function handlePreferencesRequest(aRequestId, 1.1151 + aPrefNames, 1.1152 + aListen) { 1.1153 + 1.1154 + let prefs = []; 1.1155 + 1.1156 + for (let prefName of aPrefNames) { 1.1157 + let pref = { 1.1158 + name: prefName, 1.1159 + type: "", 1.1160 + value: null 1.1161 + }; 1.1162 + 1.1163 + if (aListen) { 1.1164 + if (this._prefObservers[prefName]) 1.1165 + this._prefObservers[prefName].push(aRequestId); 1.1166 + else 1.1167 + this._prefObservers[prefName] = [ aRequestId ]; 1.1168 + Services.prefs.addObserver(prefName, this, false); 1.1169 + } 1.1170 + 1.1171 + // These pref names are not "real" pref names. 1.1172 + // They are used in the setting menu, 1.1173 + // and these are passed when initializing the setting menu. 1.1174 + switch (prefName) { 1.1175 + // The plugin pref is actually two separate prefs, so 1.1176 + // we need to handle it differently 1.1177 + case "plugin.enable": 1.1178 + pref.type = "string";// Use a string type for java's ListPreference 1.1179 + pref.value = PluginHelper.getPluginPreference(); 1.1180 + prefs.push(pref); 1.1181 + continue; 1.1182 + // Handle master password 1.1183 + case "privacy.masterpassword.enabled": 1.1184 + pref.type = "bool"; 1.1185 + pref.value = MasterPassword.enabled; 1.1186 + prefs.push(pref); 1.1187 + continue; 1.1188 + // Handle do-not-track preference 1.1189 + case "privacy.donottrackheader": 1.1190 + pref.type = "string"; 1.1191 + 1.1192 + let enableDNT = Services.prefs.getBoolPref("privacy.donottrackheader.enabled"); 1.1193 + if (!enableDNT) { 1.1194 + pref.value = kDoNotTrackPrefState.NO_PREF; 1.1195 + } else { 1.1196 + let dntState = Services.prefs.getIntPref("privacy.donottrackheader.value"); 1.1197 + pref.value = (dntState === 0) ? kDoNotTrackPrefState.ALLOW_TRACKING : 1.1198 + kDoNotTrackPrefState.DISALLOW_TRACKING; 1.1199 + } 1.1200 + 1.1201 + prefs.push(pref); 1.1202 + continue; 1.1203 +#ifdef MOZ_CRASHREPORTER 1.1204 + // Crash reporter submit pref must be fetched from nsICrashReporter service. 1.1205 + case "datareporting.crashreporter.submitEnabled": 1.1206 + pref.type = "bool"; 1.1207 + pref.value = CrashReporter.submitReports; 1.1208 + prefs.push(pref); 1.1209 + continue; 1.1210 +#endif 1.1211 + } 1.1212 + 1.1213 + try { 1.1214 + switch (Services.prefs.getPrefType(prefName)) { 1.1215 + case Ci.nsIPrefBranch.PREF_BOOL: 1.1216 + pref.type = "bool"; 1.1217 + pref.value = Services.prefs.getBoolPref(prefName); 1.1218 + break; 1.1219 + case Ci.nsIPrefBranch.PREF_INT: 1.1220 + pref.type = "int"; 1.1221 + pref.value = Services.prefs.getIntPref(prefName); 1.1222 + break; 1.1223 + case Ci.nsIPrefBranch.PREF_STRING: 1.1224 + default: 1.1225 + pref.type = "string"; 1.1226 + try { 1.1227 + // Try in case it's a localized string (will throw an exception if not) 1.1228 + pref.value = Services.prefs.getComplexValue(prefName, Ci.nsIPrefLocalizedString).data; 1.1229 + } catch (e) { 1.1230 + pref.value = Services.prefs.getCharPref(prefName); 1.1231 + } 1.1232 + break; 1.1233 + } 1.1234 + } catch (e) { 1.1235 + dump("Error reading pref [" + prefName + "]: " + e); 1.1236 + // preference does not exist; do not send it 1.1237 + continue; 1.1238 + } 1.1239 + 1.1240 + // Some Gecko preferences use integers or strings to reference 1.1241 + // state instead of directly representing the value. 1.1242 + // Since the Java UI uses the type to determine which ui elements 1.1243 + // to show and how to handle them, we need to normalize these 1.1244 + // preferences to the correct type. 1.1245 + switch (prefName) { 1.1246 + // (string) index for determining which multiple choice value to display. 1.1247 + case "browser.chrome.titlebarMode": 1.1248 + case "network.cookie.cookieBehavior": 1.1249 + case "font.size.inflation.minTwips": 1.1250 + case "home.sync.updateMode": 1.1251 + pref.type = "string"; 1.1252 + pref.value = pref.value.toString(); 1.1253 + break; 1.1254 + } 1.1255 + 1.1256 + prefs.push(pref); 1.1257 + } 1.1258 + 1.1259 + sendMessageToJava({ 1.1260 + type: "Preferences:Data", 1.1261 + requestId: aRequestId, // opaque request identifier, can be any string/int/whatever 1.1262 + preferences: prefs 1.1263 + }); 1.1264 + }, 1.1265 + 1.1266 + setPreferences: function setPreferences(aPref) { 1.1267 + let json = JSON.parse(aPref); 1.1268 + 1.1269 + switch (json.name) { 1.1270 + // The plugin pref is actually two separate prefs, so 1.1271 + // we need to handle it differently 1.1272 + case "plugin.enable": 1.1273 + PluginHelper.setPluginPreference(json.value); 1.1274 + return; 1.1275 + 1.1276 + // MasterPassword pref is not real, we just need take action and leave 1.1277 + case "privacy.masterpassword.enabled": 1.1278 + if (MasterPassword.enabled) 1.1279 + MasterPassword.removePassword(json.value); 1.1280 + else 1.1281 + MasterPassword.setPassword(json.value); 1.1282 + return; 1.1283 + 1.1284 + // "privacy.donottrackheader" is not "real" pref name, it's used in the setting menu. 1.1285 + case "privacy.donottrackheader": 1.1286 + switch (json.value) { 1.1287 + // Don't tell anything about tracking me 1.1288 + case kDoNotTrackPrefState.NO_PREF: 1.1289 + Services.prefs.setBoolPref("privacy.donottrackheader.enabled", false); 1.1290 + Services.prefs.clearUserPref("privacy.donottrackheader.value"); 1.1291 + break; 1.1292 + // Accept tracking me 1.1293 + case kDoNotTrackPrefState.ALLOW_TRACKING: 1.1294 + Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true); 1.1295 + Services.prefs.setIntPref("privacy.donottrackheader.value", 0); 1.1296 + break; 1.1297 + // Not accept tracking me 1.1298 + case kDoNotTrackPrefState.DISALLOW_TRACKING: 1.1299 + Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true); 1.1300 + Services.prefs.setIntPref("privacy.donottrackheader.value", 1); 1.1301 + break; 1.1302 + } 1.1303 + return; 1.1304 + 1.1305 + // Enabling or disabling suggestions will prevent future prompts 1.1306 + case SearchEngines.PREF_SUGGEST_ENABLED: 1.1307 + Services.prefs.setBoolPref(SearchEngines.PREF_SUGGEST_PROMPTED, true); 1.1308 + break; 1.1309 + 1.1310 +#ifdef MOZ_CRASHREPORTER 1.1311 + // Crash reporter preference is in a service; set and return. 1.1312 + case "datareporting.crashreporter.submitEnabled": 1.1313 + CrashReporter.submitReports = json.value; 1.1314 + return; 1.1315 +#endif 1.1316 + // When sending to Java, we normalized special preferences that use 1.1317 + // integers and strings to represent booleans. Here, we convert them back 1.1318 + // to their actual types so we can store them. 1.1319 + case "browser.chrome.titlebarMode": 1.1320 + case "network.cookie.cookieBehavior": 1.1321 + case "font.size.inflation.minTwips": 1.1322 + case "home.sync.updateMode": 1.1323 + json.type = "int"; 1.1324 + json.value = parseInt(json.value); 1.1325 + break; 1.1326 + } 1.1327 + 1.1328 + switch (json.type) { 1.1329 + case "bool": 1.1330 + Services.prefs.setBoolPref(json.name, json.value); 1.1331 + break; 1.1332 + case "int": 1.1333 + Services.prefs.setIntPref(json.name, json.value); 1.1334 + break; 1.1335 + default: { 1.1336 + let pref = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString); 1.1337 + pref.data = json.value; 1.1338 + Services.prefs.setComplexValue(json.name, Ci.nsISupportsString, pref); 1.1339 + break; 1.1340 + } 1.1341 + } 1.1342 + }, 1.1343 + 1.1344 + sanitize: function (aItems) { 1.1345 + let json = JSON.parse(aItems); 1.1346 + let success = true; 1.1347 + 1.1348 + for (let key in json) { 1.1349 + if (!json[key]) 1.1350 + continue; 1.1351 + 1.1352 + try { 1.1353 + switch (key) { 1.1354 + case "cookies_sessions": 1.1355 + Sanitizer.clearItem("cookies"); 1.1356 + Sanitizer.clearItem("sessions"); 1.1357 + break; 1.1358 + default: 1.1359 + Sanitizer.clearItem(key); 1.1360 + } 1.1361 + } catch (e) { 1.1362 + dump("sanitize error: " + e); 1.1363 + success = false; 1.1364 + } 1.1365 + } 1.1366 + 1.1367 + sendMessageToJava({ 1.1368 + type: "Sanitize:Finished", 1.1369 + success: success 1.1370 + }); 1.1371 + }, 1.1372 + 1.1373 + getFocusedInput: function(aBrowser, aOnlyInputElements = false) { 1.1374 + if (!aBrowser) 1.1375 + return null; 1.1376 + 1.1377 + let doc = aBrowser.contentDocument; 1.1378 + if (!doc) 1.1379 + return null; 1.1380 + 1.1381 + let focused = doc.activeElement; 1.1382 + while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) { 1.1383 + doc = focused.contentDocument; 1.1384 + focused = doc.activeElement; 1.1385 + } 1.1386 + 1.1387 + if (focused instanceof HTMLInputElement && focused.mozIsTextField(false)) 1.1388 + return focused; 1.1389 + 1.1390 + if (aOnlyInputElements) 1.1391 + return null; 1.1392 + 1.1393 + if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) { 1.1394 + 1.1395 + if (focused instanceof HTMLBodyElement) { 1.1396 + // we are putting focus into a contentEditable frame. scroll the frame into 1.1397 + // view instead of the contentEditable document contained within, because that 1.1398 + // results in a better user experience 1.1399 + focused = focused.ownerDocument.defaultView.frameElement; 1.1400 + } 1.1401 + return focused; 1.1402 + } 1.1403 + return null; 1.1404 + }, 1.1405 + 1.1406 + scrollToFocusedInput: function(aBrowser, aAllowZoom = true) { 1.1407 + let formHelperMode = Services.prefs.getIntPref("formhelper.mode"); 1.1408 + if (formHelperMode == kFormHelperModeDisabled) 1.1409 + return; 1.1410 + 1.1411 + let focused = this.getFocusedInput(aBrowser); 1.1412 + 1.1413 + if (focused) { 1.1414 + let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom"); 1.1415 + if (formHelperMode == kFormHelperModeDynamic && this.isTablet) 1.1416 + shouldZoom = false; 1.1417 + // ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen 1.1418 + ZoomHelper.zoomToElement(focused, -1, false, 1.1419 + aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified); 1.1420 + } 1.1421 + }, 1.1422 + 1.1423 + observe: function(aSubject, aTopic, aData) { 1.1424 + let browser = this.selectedBrowser; 1.1425 + 1.1426 + switch (aTopic) { 1.1427 + 1.1428 + case "Session:Back": 1.1429 + browser.goBack(); 1.1430 + break; 1.1431 + 1.1432 + case "Session:Forward": 1.1433 + browser.goForward(); 1.1434 + break; 1.1435 + 1.1436 + case "Session:Reload": { 1.1437 + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; 1.1438 + 1.1439 + // Check to see if this is a message to enable/disable mixed content blocking. 1.1440 + if (aData) { 1.1441 + let allowMixedContent = JSON.parse(aData).allowMixedContent; 1.1442 + if (allowMixedContent) { 1.1443 + // Set a flag to disable mixed content blocking. 1.1444 + flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT; 1.1445 + } else { 1.1446 + // Set mixedContentChannel to null to re-enable mixed content blocking. 1.1447 + let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell); 1.1448 + docShell.mixedContentChannel = null; 1.1449 + } 1.1450 + } 1.1451 + 1.1452 + // Try to use the session history to reload so that framesets are 1.1453 + // handled properly. If the window has no session history, fall back 1.1454 + // to using the web navigation's reload method. 1.1455 + let webNav = browser.webNavigation; 1.1456 + try { 1.1457 + let sh = webNav.sessionHistory; 1.1458 + if (sh) 1.1459 + webNav = sh.QueryInterface(Ci.nsIWebNavigation); 1.1460 + } catch (e) {} 1.1461 + webNav.reload(flags); 1.1462 + break; 1.1463 + } 1.1464 + 1.1465 + case "Session:Stop": 1.1466 + browser.stop(); 1.1467 + break; 1.1468 + 1.1469 + case "Session:ShowHistory": { 1.1470 + let data = JSON.parse(aData); 1.1471 + this.showHistory(data.fromIndex, data.toIndex, data.selIndex); 1.1472 + break; 1.1473 + } 1.1474 + 1.1475 + case "Tab:Load": { 1.1476 + let data = JSON.parse(aData); 1.1477 + 1.1478 + // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from 1.1479 + // inheriting the currently loaded document's principal. 1.1480 + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | 1.1481 + Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; 1.1482 + if (data.userEntered) { 1.1483 + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; 1.1484 + } 1.1485 + 1.1486 + let delayLoad = ("delayLoad" in data) ? data.delayLoad : false; 1.1487 + let params = { 1.1488 + selected: ("selected" in data) ? data.selected : !delayLoad, 1.1489 + parentId: ("parentId" in data) ? data.parentId : -1, 1.1490 + flags: flags, 1.1491 + tabID: data.tabID, 1.1492 + isPrivate: (data.isPrivate === true), 1.1493 + pinned: (data.pinned === true), 1.1494 + delayLoad: (delayLoad === true), 1.1495 + desktopMode: (data.desktopMode === true) 1.1496 + }; 1.1497 + 1.1498 + let url = data.url; 1.1499 + if (data.engine) { 1.1500 + let engine = Services.search.getEngineByName(data.engine); 1.1501 + if (engine) { 1.1502 + params.userSearch = url; 1.1503 + let submission = engine.getSubmission(url); 1.1504 + url = submission.uri.spec; 1.1505 + params.postData = submission.postData; 1.1506 + } 1.1507 + } 1.1508 + 1.1509 + if (data.newTab) { 1.1510 + this.addTab(url, params); 1.1511 + } else { 1.1512 + if (data.tabId) { 1.1513 + // Use a specific browser instead of the selected browser, if it exists 1.1514 + let specificBrowser = this.getTabForId(data.tabId).browser; 1.1515 + if (specificBrowser) 1.1516 + browser = specificBrowser; 1.1517 + } 1.1518 + this.loadURI(url, browser, params); 1.1519 + } 1.1520 + break; 1.1521 + } 1.1522 + 1.1523 + case "Tab:Selected": 1.1524 + this._handleTabSelected(this.getTabForId(parseInt(aData))); 1.1525 + break; 1.1526 + 1.1527 + case "Tab:Closed": 1.1528 + this._handleTabClosed(this.getTabForId(parseInt(aData))); 1.1529 + break; 1.1530 + 1.1531 + case "keyword-search": 1.1532 + // This event refers to a search via the URL bar, not a bookmarks 1.1533 + // keyword search. Note that this code assumes that the user can only 1.1534 + // perform a keyword search on the selected tab. 1.1535 + this.selectedTab.userSearch = aData; 1.1536 + 1.1537 + let engine = aSubject.QueryInterface(Ci.nsISearchEngine); 1.1538 + sendMessageToJava({ 1.1539 + type: "Search:Keyword", 1.1540 + identifier: engine.identifier, 1.1541 + name: engine.name, 1.1542 + }); 1.1543 + break; 1.1544 + 1.1545 + case "Browser:Quit": 1.1546 + this.quit(); 1.1547 + break; 1.1548 + 1.1549 + case "SaveAs:PDF": 1.1550 + this.saveAsPDF(browser); 1.1551 + break; 1.1552 + 1.1553 + case "Preferences:Set": 1.1554 + this.setPreferences(aData); 1.1555 + break; 1.1556 + 1.1557 + case "ScrollTo:FocusedInput": 1.1558 + // these messages come from a change in the viewable area and not user interaction 1.1559 + // we allow scrolling to the selected input, but not zooming the page 1.1560 + this.scrollToFocusedInput(browser, false); 1.1561 + break; 1.1562 + 1.1563 + case "Sanitize:ClearData": 1.1564 + this.sanitize(aData); 1.1565 + break; 1.1566 + 1.1567 + case "FullScreen:Exit": 1.1568 + browser.contentDocument.mozCancelFullScreen(); 1.1569 + break; 1.1570 + 1.1571 + case "Viewport:Change": 1.1572 + if (this.isBrowserContentDocumentDisplayed()) 1.1573 + this.selectedTab.setViewport(JSON.parse(aData)); 1.1574 + break; 1.1575 + 1.1576 + case "Viewport:Flush": 1.1577 + this.contentDocumentChanged(); 1.1578 + break; 1.1579 + 1.1580 + case "Passwords:Init": { 1.1581 + let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"]. 1.1582 + getService(Ci.nsILoginManagerStorage); 1.1583 + storage.init(); 1.1584 + Services.obs.removeObserver(this, "Passwords:Init"); 1.1585 + break; 1.1586 + } 1.1587 + 1.1588 + case "FormHistory:Init": { 1.1589 + // Force creation/upgrade of formhistory.sqlite 1.1590 + FormHistory.count({}); 1.1591 + Services.obs.removeObserver(this, "FormHistory:Init"); 1.1592 + break; 1.1593 + } 1.1594 + 1.1595 + case "sessionstore-state-purge-complete": 1.1596 + sendMessageToJava({ type: "Session:StatePurged" }); 1.1597 + break; 1.1598 + 1.1599 + case "gather-telemetry": 1.1600 + sendMessageToJava({ type: "Telemetry:Gather" }); 1.1601 + break; 1.1602 + 1.1603 + case "Viewport:FixedMarginsChanged": 1.1604 + gViewportMargins = JSON.parse(aData); 1.1605 + this.selectedTab.updateViewportSize(gScreenWidth); 1.1606 + break; 1.1607 + 1.1608 + case "nsPref:changed": 1.1609 + this.notifyPrefObservers(aData); 1.1610 + break; 1.1611 + 1.1612 +#ifdef MOZ_ANDROID_SYNTHAPKS 1.1613 + case "webapps-runtime-install": 1.1614 + WebappManager.install(JSON.parse(aData), aSubject); 1.1615 + break; 1.1616 + 1.1617 + case "webapps-runtime-install-package": 1.1618 + WebappManager.installPackage(JSON.parse(aData), aSubject); 1.1619 + break; 1.1620 + 1.1621 + case "webapps-ask-install": 1.1622 + WebappManager.askInstall(JSON.parse(aData)); 1.1623 + break; 1.1624 + 1.1625 + case "webapps-launch": { 1.1626 + WebappManager.launch(JSON.parse(aData)); 1.1627 + break; 1.1628 + } 1.1629 + 1.1630 + case "webapps-uninstall": { 1.1631 + WebappManager.uninstall(JSON.parse(aData)); 1.1632 + break; 1.1633 + } 1.1634 + 1.1635 + case "Webapps:AutoInstall": 1.1636 + WebappManager.autoInstall(JSON.parse(aData)); 1.1637 + break; 1.1638 + 1.1639 + case "Webapps:Load": 1.1640 + this._loadWebapp(JSON.parse(aData)); 1.1641 + break; 1.1642 + 1.1643 + case "Webapps:AutoUninstall": 1.1644 + WebappManager.autoUninstall(JSON.parse(aData)); 1.1645 + break; 1.1646 +#endif 1.1647 + 1.1648 + case "Locale:Changed": 1.1649 + // The value provided to Locale:Changed should be a BCP47 language tag 1.1650 + // understood by Gecko -- for example, "es-ES" or "de". 1.1651 + console.log("Locale:Changed: " + aData); 1.1652 + 1.1653 + // TODO: do we need to be more nuanced here -- e.g., checking for the 1.1654 + // OS locale -- or should it always be false on Fennec? 1.1655 + Services.prefs.setBoolPref("intl.locale.matchOS", false); 1.1656 + Services.prefs.setCharPref("general.useragent.locale", aData); 1.1657 + break; 1.1658 + 1.1659 + default: 1.1660 + dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n'); 1.1661 + break; 1.1662 + 1.1663 + } 1.1664 + }, 1.1665 + 1.1666 + get defaultBrowserWidth() { 1.1667 + delete this.defaultBrowserWidth; 1.1668 + let width = Services.prefs.getIntPref("browser.viewport.desktopWidth"); 1.1669 + return this.defaultBrowserWidth = width; 1.1670 + }, 1.1671 + 1.1672 + // nsIAndroidBrowserApp 1.1673 + getBrowserTab: function(tabId) { 1.1674 + return this.getTabForId(tabId); 1.1675 + }, 1.1676 + 1.1677 + getUITelemetryObserver: function() { 1.1678 + return UITelemetry; 1.1679 + }, 1.1680 + 1.1681 + getPreferences: function getPreferences(requestId, prefNames, count) { 1.1682 + this.handlePreferencesRequest(requestId, prefNames, false); 1.1683 + }, 1.1684 + 1.1685 + observePreferences: function observePreferences(requestId, prefNames, count) { 1.1686 + this.handlePreferencesRequest(requestId, prefNames, true); 1.1687 + }, 1.1688 + 1.1689 + removePreferenceObservers: function removePreferenceObservers(aRequestId) { 1.1690 + let newPrefObservers = []; 1.1691 + for (let prefName in this._prefObservers) { 1.1692 + let requestIds = this._prefObservers[prefName]; 1.1693 + // Remove the requestID from the preference handlers 1.1694 + let i = requestIds.indexOf(aRequestId); 1.1695 + if (i >= 0) { 1.1696 + requestIds.splice(i, 1); 1.1697 + } 1.1698 + 1.1699 + // If there are no more request IDs, remove the observer 1.1700 + if (requestIds.length == 0) { 1.1701 + Services.prefs.removeObserver(prefName, this); 1.1702 + } else { 1.1703 + newPrefObservers[prefName] = requestIds; 1.1704 + } 1.1705 + } 1.1706 + this._prefObservers = newPrefObservers; 1.1707 + }, 1.1708 + 1.1709 + // This method will print a list from fromIndex to toIndex, optionally 1.1710 + // selecting selIndex(if fromIndex<=selIndex<=toIndex) 1.1711 + showHistory: function(fromIndex, toIndex, selIndex) { 1.1712 + let browser = this.selectedBrowser; 1.1713 + let hist = browser.sessionHistory; 1.1714 + let listitems = []; 1.1715 + for (let i = toIndex; i >= fromIndex; i--) { 1.1716 + let entry = hist.getEntryAtIndex(i, false); 1.1717 + let item = { 1.1718 + label: entry.title || entry.URI.spec, 1.1719 + selected: (i == selIndex) 1.1720 + }; 1.1721 + listitems.push(item); 1.1722 + } 1.1723 + 1.1724 + let p = new Prompt({ 1.1725 + window: browser.contentWindow 1.1726 + }).setSingleChoiceItems(listitems).show(function(data) { 1.1727 + let selected = data.button; 1.1728 + if (selected == -1) 1.1729 + return; 1.1730 + 1.1731 + browser.gotoIndex(toIndex-selected); 1.1732 + }); 1.1733 + }, 1.1734 +}; 1.1735 + 1.1736 +var NativeWindow = { 1.1737 + init: function() { 1.1738 + Services.obs.addObserver(this, "Menu:Clicked", false); 1.1739 + Services.obs.addObserver(this, "PageActions:Clicked", false); 1.1740 + Services.obs.addObserver(this, "PageActions:LongClicked", false); 1.1741 + Services.obs.addObserver(this, "Doorhanger:Reply", false); 1.1742 + Services.obs.addObserver(this, "Toast:Click", false); 1.1743 + Services.obs.addObserver(this, "Toast:Hidden", false); 1.1744 + this.contextmenus.init(); 1.1745 + }, 1.1746 + 1.1747 + uninit: function() { 1.1748 + Services.obs.removeObserver(this, "Menu:Clicked"); 1.1749 + Services.obs.removeObserver(this, "PageActions:Clicked"); 1.1750 + Services.obs.removeObserver(this, "PageActions:LongClicked"); 1.1751 + Services.obs.removeObserver(this, "Doorhanger:Reply"); 1.1752 + Services.obs.removeObserver(this, "Toast:Click", false); 1.1753 + Services.obs.removeObserver(this, "Toast:Hidden", false); 1.1754 + this.contextmenus.uninit(); 1.1755 + }, 1.1756 + 1.1757 + loadDex: function(zipFile, implClass) { 1.1758 + sendMessageToJava({ 1.1759 + type: "Dex:Load", 1.1760 + zipfile: zipFile, 1.1761 + impl: implClass || "Main" 1.1762 + }); 1.1763 + }, 1.1764 + 1.1765 + unloadDex: function(zipFile) { 1.1766 + sendMessageToJava({ 1.1767 + type: "Dex:Unload", 1.1768 + zipfile: zipFile 1.1769 + }); 1.1770 + }, 1.1771 + 1.1772 + toast: { 1.1773 + _callbacks: {}, 1.1774 + show: function(aMessage, aDuration, aOptions) { 1.1775 + let msg = { 1.1776 + type: "Toast:Show", 1.1777 + message: aMessage, 1.1778 + duration: aDuration 1.1779 + }; 1.1780 + 1.1781 + if (aOptions && aOptions.button) { 1.1782 + msg.button = { 1.1783 + label: aOptions.button.label, 1.1784 + id: uuidgen.generateUUID().toString(), 1.1785 + // If the caller specified a button, make sure we convert any chrome urls 1.1786 + // to jar:jar urls so that the frontend can show them 1.1787 + icon: aOptions.button.icon ? resolveGeckoURI(aOptions.button.icon) : null, 1.1788 + }; 1.1789 + this._callbacks[msg.button.id] = aOptions.button.callback; 1.1790 + } 1.1791 + 1.1792 + sendMessageToJava(msg); 1.1793 + } 1.1794 + }, 1.1795 + 1.1796 + pageactions: { 1.1797 + _items: { }, 1.1798 + add: function(aOptions) { 1.1799 + let id = uuidgen.generateUUID().toString(); 1.1800 + sendMessageToJava({ 1.1801 + type: "PageActions:Add", 1.1802 + id: id, 1.1803 + title: aOptions.title, 1.1804 + icon: resolveGeckoURI(aOptions.icon), 1.1805 + important: "important" in aOptions ? aOptions.important : false 1.1806 + }); 1.1807 + this._items[id] = { 1.1808 + clickCallback: aOptions.clickCallback, 1.1809 + longClickCallback: aOptions.longClickCallback 1.1810 + }; 1.1811 + return id; 1.1812 + }, 1.1813 + remove: function(id) { 1.1814 + sendMessageToJava({ 1.1815 + type: "PageActions:Remove", 1.1816 + id: id 1.1817 + }); 1.1818 + delete this._items[id]; 1.1819 + } 1.1820 + }, 1.1821 + 1.1822 + menu: { 1.1823 + _callbacks: [], 1.1824 + _menuId: 1, 1.1825 + toolsMenuID: -1, 1.1826 + add: function() { 1.1827 + let options; 1.1828 + if (arguments.length == 1) { 1.1829 + options = arguments[0]; 1.1830 + } else if (arguments.length == 3) { 1.1831 + options = { 1.1832 + name: arguments[0], 1.1833 + icon: arguments[1], 1.1834 + callback: arguments[2] 1.1835 + }; 1.1836 + } else { 1.1837 + throw "Incorrect number of parameters"; 1.1838 + } 1.1839 + 1.1840 + options.type = "Menu:Add"; 1.1841 + options.id = this._menuId; 1.1842 + 1.1843 + sendMessageToJava(options); 1.1844 + this._callbacks[this._menuId] = options.callback; 1.1845 + this._menuId++; 1.1846 + return this._menuId - 1; 1.1847 + }, 1.1848 + 1.1849 + remove: function(aId) { 1.1850 + sendMessageToJava({ type: "Menu:Remove", id: aId }); 1.1851 + }, 1.1852 + 1.1853 + update: function(aId, aOptions) { 1.1854 + if (!aOptions) 1.1855 + return; 1.1856 + 1.1857 + sendMessageToJava({ 1.1858 + type: "Menu:Update", 1.1859 + id: aId, 1.1860 + options: aOptions 1.1861 + }); 1.1862 + } 1.1863 + }, 1.1864 + 1.1865 + doorhanger: { 1.1866 + _callbacks: {}, 1.1867 + _callbacksId: 0, 1.1868 + _promptId: 0, 1.1869 + 1.1870 + /** 1.1871 + * @param aOptions 1.1872 + * An options JavaScript object holding additional properties for the 1.1873 + * notification. The following properties are currently supported: 1.1874 + * persistence: An integer. The notification will not automatically 1.1875 + * dismiss for this many page loads. If persistence is set 1.1876 + * to -1, the doorhanger will never automatically dismiss. 1.1877 + * persistWhileVisible: 1.1878 + * A boolean. If true, a visible notification will always 1.1879 + * persist across location changes. 1.1880 + * timeout: A time in milliseconds. The notification will not 1.1881 + * automatically dismiss before this time. 1.1882 + * checkbox: A string to appear next to a checkbox under the notification 1.1883 + * message. The button callback functions will be called with 1.1884 + * the checked state as an argument. 1.1885 + */ 1.1886 + show: function(aMessage, aValue, aButtons, aTabID, aOptions) { 1.1887 + if (aButtons == null) { 1.1888 + aButtons = []; 1.1889 + } 1.1890 + 1.1891 + aButtons.forEach((function(aButton) { 1.1892 + this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId }; 1.1893 + aButton.callback = this._callbacksId; 1.1894 + this._callbacksId++; 1.1895 + }).bind(this)); 1.1896 + 1.1897 + this._promptId++; 1.1898 + let json = { 1.1899 + type: "Doorhanger:Add", 1.1900 + message: aMessage, 1.1901 + value: aValue, 1.1902 + buttons: aButtons, 1.1903 + // use the current tab if none is provided 1.1904 + tabID: aTabID || BrowserApp.selectedTab.id, 1.1905 + options: aOptions || {} 1.1906 + }; 1.1907 + sendMessageToJava(json); 1.1908 + }, 1.1909 + 1.1910 + hide: function(aValue, aTabID) { 1.1911 + sendMessageToJava({ 1.1912 + type: "Doorhanger:Remove", 1.1913 + value: aValue, 1.1914 + tabID: aTabID 1.1915 + }); 1.1916 + } 1.1917 + }, 1.1918 + 1.1919 + observe: function(aSubject, aTopic, aData) { 1.1920 + if (aTopic == "Menu:Clicked") { 1.1921 + if (this.menu._callbacks[aData]) 1.1922 + this.menu._callbacks[aData](); 1.1923 + } else if (aTopic == "PageActions:Clicked") { 1.1924 + if (this.pageactions._items[aData].clickCallback) 1.1925 + this.pageactions._items[aData].clickCallback(); 1.1926 + } else if (aTopic == "PageActions:LongClicked") { 1.1927 + if (this.pageactions._items[aData].longClickCallback) 1.1928 + this.pageactions._items[aData].longClickCallback(); 1.1929 + } else if (aTopic == "Toast:Click") { 1.1930 + if (this.toast._callbacks[aData]) { 1.1931 + this.toast._callbacks[aData](); 1.1932 + delete this.toast._callbacks[aData]; 1.1933 + } 1.1934 + } else if (aTopic == "Toast:Hidden") { 1.1935 + if (this.toast._callbacks[aData]) 1.1936 + delete this.toast._callbacks[aData]; 1.1937 + } else if (aTopic == "Doorhanger:Reply") { 1.1938 + let data = JSON.parse(aData); 1.1939 + let reply_id = data["callback"]; 1.1940 + 1.1941 + if (this.doorhanger._callbacks[reply_id]) { 1.1942 + // Pass the value of the optional checkbox to the callback 1.1943 + let checked = data["checked"]; 1.1944 + this.doorhanger._callbacks[reply_id].cb(checked, data.inputs); 1.1945 + 1.1946 + let prompt = this.doorhanger._callbacks[reply_id].prompt; 1.1947 + for (let id in this.doorhanger._callbacks) { 1.1948 + if (this.doorhanger._callbacks[id].prompt == prompt) { 1.1949 + delete this.doorhanger._callbacks[id]; 1.1950 + } 1.1951 + } 1.1952 + } 1.1953 + } 1.1954 + }, 1.1955 + contextmenus: { 1.1956 + items: {}, // a list of context menu items that we may show 1.1957 + DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items 1.1958 + 1.1959 + init: function() { 1.1960 + Services.obs.addObserver(this, "Gesture:LongPress", false); 1.1961 + }, 1.1962 + 1.1963 + uninit: function() { 1.1964 + Services.obs.removeObserver(this, "Gesture:LongPress"); 1.1965 + }, 1.1966 + 1.1967 + add: function() { 1.1968 + let args; 1.1969 + if (arguments.length == 1) { 1.1970 + args = arguments[0]; 1.1971 + } else if (arguments.length == 3) { 1.1972 + args = { 1.1973 + label : arguments[0], 1.1974 + selector: arguments[1], 1.1975 + callback: arguments[2] 1.1976 + }; 1.1977 + } else { 1.1978 + throw "Incorrect number of parameters"; 1.1979 + } 1.1980 + 1.1981 + if (!args.label) 1.1982 + throw "Menu items must have a name"; 1.1983 + 1.1984 + let cmItem = new ContextMenuItem(args); 1.1985 + this.items[cmItem.id] = cmItem; 1.1986 + return cmItem.id; 1.1987 + }, 1.1988 + 1.1989 + remove: function(aId) { 1.1990 + delete this.items[aId]; 1.1991 + }, 1.1992 + 1.1993 + SelectorContext: function(aSelector) { 1.1994 + return { 1.1995 + matches: function(aElt) { 1.1996 + if (aElt.mozMatchesSelector) 1.1997 + return aElt.mozMatchesSelector(aSelector); 1.1998 + return false; 1.1999 + } 1.2000 + }; 1.2001 + }, 1.2002 + 1.2003 + linkOpenableNonPrivateContext: { 1.2004 + matches: function linkOpenableNonPrivateContextMatches(aElement) { 1.2005 + let doc = aElement.ownerDocument; 1.2006 + if (!doc || PrivateBrowsingUtils.isWindowPrivate(doc.defaultView)) { 1.2007 + return false; 1.2008 + } 1.2009 + 1.2010 + return NativeWindow.contextmenus.linkOpenableContext.matches(aElement); 1.2011 + } 1.2012 + }, 1.2013 + 1.2014 + linkOpenableContext: { 1.2015 + matches: function linkOpenableContextMatches(aElement) { 1.2016 + let uri = NativeWindow.contextmenus._getLink(aElement); 1.2017 + if (uri) { 1.2018 + let scheme = uri.scheme; 1.2019 + let dontOpen = /^(javascript|mailto|news|snews|tel)$/; 1.2020 + return (scheme && !dontOpen.test(scheme)); 1.2021 + } 1.2022 + return false; 1.2023 + } 1.2024 + }, 1.2025 + 1.2026 + linkCopyableContext: { 1.2027 + matches: function linkCopyableContextMatches(aElement) { 1.2028 + let uri = NativeWindow.contextmenus._getLink(aElement); 1.2029 + if (uri) { 1.2030 + let scheme = uri.scheme; 1.2031 + let dontCopy = /^(mailto|tel)$/; 1.2032 + return (scheme && !dontCopy.test(scheme)); 1.2033 + } 1.2034 + return false; 1.2035 + } 1.2036 + }, 1.2037 + 1.2038 + linkShareableContext: { 1.2039 + matches: function linkShareableContextMatches(aElement) { 1.2040 + let uri = NativeWindow.contextmenus._getLink(aElement); 1.2041 + if (uri) { 1.2042 + let scheme = uri.scheme; 1.2043 + let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/; 1.2044 + return (scheme && !dontShare.test(scheme)); 1.2045 + } 1.2046 + return false; 1.2047 + } 1.2048 + }, 1.2049 + 1.2050 + linkBookmarkableContext: { 1.2051 + matches: function linkBookmarkableContextMatches(aElement) { 1.2052 + let uri = NativeWindow.contextmenus._getLink(aElement); 1.2053 + if (uri) { 1.2054 + let scheme = uri.scheme; 1.2055 + let dontBookmark = /^(mailto|tel)$/; 1.2056 + return (scheme && !dontBookmark.test(scheme)); 1.2057 + } 1.2058 + return false; 1.2059 + } 1.2060 + }, 1.2061 + 1.2062 + emailLinkContext: { 1.2063 + matches: function emailLinkContextMatches(aElement) { 1.2064 + let uri = NativeWindow.contextmenus._getLink(aElement); 1.2065 + if (uri) 1.2066 + return uri.schemeIs("mailto"); 1.2067 + return false; 1.2068 + } 1.2069 + }, 1.2070 + 1.2071 + phoneNumberLinkContext: { 1.2072 + matches: function phoneNumberLinkContextMatches(aElement) { 1.2073 + let uri = NativeWindow.contextmenus._getLink(aElement); 1.2074 + if (uri) 1.2075 + return uri.schemeIs("tel"); 1.2076 + return false; 1.2077 + } 1.2078 + }, 1.2079 + 1.2080 + imageLocationCopyableContext: { 1.2081 + matches: function imageLinkCopyableContextMatches(aElement) { 1.2082 + return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI); 1.2083 + } 1.2084 + }, 1.2085 + 1.2086 + imageSaveableContext: { 1.2087 + matches: function imageSaveableContextMatches(aElement) { 1.2088 + if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) { 1.2089 + // The image must be loaded to allow saving 1.2090 + let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); 1.2091 + return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)); 1.2092 + } 1.2093 + return false; 1.2094 + } 1.2095 + }, 1.2096 + 1.2097 + mediaSaveableContext: { 1.2098 + matches: function mediaSaveableContextMatches(aElement) { 1.2099 + return (aElement instanceof HTMLVideoElement || 1.2100 + aElement instanceof HTMLAudioElement); 1.2101 + } 1.2102 + }, 1.2103 + 1.2104 + mediaContext: function(aMode) { 1.2105 + return { 1.2106 + matches: function(aElt) { 1.2107 + if (aElt instanceof Ci.nsIDOMHTMLMediaElement) { 1.2108 + let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE; 1.2109 + if (hasError) 1.2110 + return false; 1.2111 + 1.2112 + let paused = aElt.paused || aElt.ended; 1.2113 + if (paused && aMode == "media-paused") 1.2114 + return true; 1.2115 + if (!paused && aMode == "media-playing") 1.2116 + return true; 1.2117 + let controls = aElt.controls; 1.2118 + if (!controls && aMode == "media-hidingcontrols") 1.2119 + return true; 1.2120 + 1.2121 + let muted = aElt.muted; 1.2122 + if (muted && aMode == "media-muted") 1.2123 + return true; 1.2124 + else if (!muted && aMode == "media-unmuted") 1.2125 + return true; 1.2126 + } 1.2127 + return false; 1.2128 + } 1.2129 + }; 1.2130 + }, 1.2131 + 1.2132 + /* Holds a WeakRef to the original target element this context menu was shown for. 1.2133 + * Most API's will have to walk up the tree from this node to find the correct element 1.2134 + * to act on 1.2135 + */ 1.2136 + get _target() { 1.2137 + if (this._targetRef) 1.2138 + return this._targetRef.get(); 1.2139 + return null; 1.2140 + }, 1.2141 + 1.2142 + set _target(aTarget) { 1.2143 + if (aTarget) 1.2144 + this._targetRef = Cu.getWeakReference(aTarget); 1.2145 + else this._targetRef = null; 1.2146 + }, 1.2147 + 1.2148 + get defaultContext() { 1.2149 + delete this.defaultContext; 1.2150 + return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default"); 1.2151 + }, 1.2152 + 1.2153 + /* Gets menuitems for an arbitrary node 1.2154 + * Parameters: 1.2155 + * element - The element to look at. If this element has a contextmenu attribute, the 1.2156 + * corresponding contextmenu will be used. 1.2157 + */ 1.2158 + _getHTMLContextMenuItemsForElement: function(element) { 1.2159 + let htmlMenu = element.contextMenu; 1.2160 + if (!htmlMenu) { 1.2161 + return []; 1.2162 + } 1.2163 + 1.2164 + htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu); 1.2165 + htmlMenu.sendShowEvent(); 1.2166 + 1.2167 + return this._getHTMLContextMenuItemsForMenu(htmlMenu, element); 1.2168 + }, 1.2169 + 1.2170 + /* Add a menuitem for an HTML <menu> node 1.2171 + * Parameters: 1.2172 + * menu - The <menu> element to iterate through for menuitems 1.2173 + * target - The target element these context menu items are attached to 1.2174 + */ 1.2175 + _getHTMLContextMenuItemsForMenu: function(menu, target) { 1.2176 + let items = []; 1.2177 + for (let i = 0; i < menu.childNodes.length; i++) { 1.2178 + let elt = menu.childNodes[i]; 1.2179 + if (!elt.label) 1.2180 + continue; 1.2181 + 1.2182 + items.push(new HTMLContextMenuItem(elt, target)); 1.2183 + } 1.2184 + 1.2185 + return items; 1.2186 + }, 1.2187 + 1.2188 + // Searches the current list of menuitems to show for any that match this id 1.2189 + _findMenuItem: function(aId) { 1.2190 + if (!this.menus) { 1.2191 + return null; 1.2192 + } 1.2193 + 1.2194 + for (let context in this.menus) { 1.2195 + let menu = this.menus[context]; 1.2196 + for (let i = 0; i < menu.length; i++) { 1.2197 + if (menu[i].id === aId) { 1.2198 + return menu[i]; 1.2199 + } 1.2200 + } 1.2201 + } 1.2202 + return null; 1.2203 + }, 1.2204 + 1.2205 + // Returns true if there are any context menu items to show 1.2206 + shouldShow: function() { 1.2207 + for (let context in this.menus) { 1.2208 + let menu = this.menus[context]; 1.2209 + if (menu.length > 0) { 1.2210 + return true; 1.2211 + } 1.2212 + } 1.2213 + return false; 1.2214 + }, 1.2215 + 1.2216 + /* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this 1.2217 + * is an image inside an <a> tag, we may have a "link" context and an "image" one. 1.2218 + */ 1.2219 + _getContextType: function(element) { 1.2220 + // For anchor nodes, we try to use the scheme to pick a string 1.2221 + if (element instanceof Ci.nsIDOMHTMLAnchorElement) { 1.2222 + let uri = this.makeURI(this._getLinkURL(element)); 1.2223 + try { 1.2224 + return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme); 1.2225 + } catch(ex) { } 1.2226 + } 1.2227 + 1.2228 + // Otherwise we try the nodeName 1.2229 + try { 1.2230 + return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase()); 1.2231 + } catch(ex) { } 1.2232 + 1.2233 + // Fallback to the default 1.2234 + return this.defaultContext; 1.2235 + }, 1.2236 + 1.2237 + // Adds context menu items added through the add-on api 1.2238 + _getNativeContextMenuItems: function(element, x, y) { 1.2239 + let res = []; 1.2240 + for (let itemId of Object.keys(this.items)) { 1.2241 + let item = this.items[itemId]; 1.2242 + 1.2243 + if (!this._findMenuItem(item.id) && item.matches(element, x, y)) { 1.2244 + res.push(item); 1.2245 + } 1.2246 + } 1.2247 + 1.2248 + return res; 1.2249 + }, 1.2250 + 1.2251 + /* Checks if there are context menu items to show, and if it finds them 1.2252 + * sends a contextmenu event to content. We also send showing events to 1.2253 + * any html5 context menus we are about to show, and fire some local notifications 1.2254 + * for chrome consumers to do lazy menuitem construction 1.2255 + */ 1.2256 + _sendToContent: function(x, y) { 1.2257 + let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(x, y); 1.2258 + if (!target) 1.2259 + target = ElementTouchHelper.anyElementFromPoint(x, y); 1.2260 + 1.2261 + if (!target) 1.2262 + return; 1.2263 + 1.2264 + this._target = target; 1.2265 + 1.2266 + Services.obs.notifyObservers(null, "before-build-contextmenu", ""); 1.2267 + this._buildMenu(x, y); 1.2268 + 1.2269 + // only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap) 1.2270 + if (this.shouldShow()) { 1.2271 + let event = target.ownerDocument.createEvent("MouseEvent"); 1.2272 + event.initMouseEvent("contextmenu", true, true, target.defaultView, 1.2273 + 0, x, y, x, y, false, false, false, false, 1.2274 + 0, null); 1.2275 + target.ownerDocument.defaultView.addEventListener("contextmenu", this, false); 1.2276 + target.dispatchEvent(event); 1.2277 + } else { 1.2278 + this.menus = null; 1.2279 + Services.obs.notifyObservers({target: target, x: x, y: y}, "context-menu-not-shown", ""); 1.2280 + 1.2281 + if (SelectionHandler.canSelect(target)) { 1.2282 + if (!SelectionHandler.startSelection(target, { 1.2283 + mode: SelectionHandler.SELECT_AT_POINT, 1.2284 + x: x, 1.2285 + y: y 1.2286 + })) { 1.2287 + SelectionHandler.attachCaret(target); 1.2288 + } 1.2289 + } 1.2290 + } 1.2291 + }, 1.2292 + 1.2293 + // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url 1.2294 + _getTitle: function(node) { 1.2295 + if (node.hasAttribute && node.hasAttribute("title")) { 1.2296 + return node.getAttribute("title"); 1.2297 + } 1.2298 + return this._getUrl(node); 1.2299 + }, 1.2300 + 1.2301 + // Returns a url associated with a node 1.2302 + _getUrl: function(node) { 1.2303 + if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) || 1.2304 + (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) { 1.2305 + return this._getLinkURL(node); 1.2306 + } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) { 1.2307 + return node.currentURI.spec; 1.2308 + } else if (node instanceof Ci.nsIDOMHTMLMediaElement) { 1.2309 + return (node.currentSrc || node.src); 1.2310 + } 1.2311 + 1.2312 + return ""; 1.2313 + }, 1.2314 + 1.2315 + // Adds an array of menuitems to the current list of items to show, in the correct context 1.2316 + _addMenuItems: function(items, context) { 1.2317 + if (!this.menus[context]) { 1.2318 + this.menus[context] = []; 1.2319 + } 1.2320 + this.menus[context] = this.menus[context].concat(items); 1.2321 + }, 1.2322 + 1.2323 + /* Does the basic work of building a context menu to show. Will combine HTML and Native 1.2324 + * context menus items, as well as sorting menuitems into different menus based on context. 1.2325 + */ 1.2326 + _buildMenu: function(x, y) { 1.2327 + // now walk up the tree and for each node look for any context menu items that apply 1.2328 + let element = this._target; 1.2329 + 1.2330 + // this.menus holds a hashmap of "contexts" to menuitems associated with that context 1.2331 + // For instance, if the user taps an image inside a link, we'll have something like: 1.2332 + // { 1.2333 + // link: [ ContextMenuItem, ContextMenuItem ] 1.2334 + // image: [ ContextMenuItem, ContextMenuItem ] 1.2335 + // } 1.2336 + this.menus = {}; 1.2337 + 1.2338 + while (element) { 1.2339 + let context = this._getContextType(element); 1.2340 + 1.2341 + // First check for any html5 context menus that might exist... 1.2342 + var items = this._getHTMLContextMenuItemsForElement(element); 1.2343 + if (items.length > 0) { 1.2344 + this._addMenuItems(items, context); 1.2345 + } 1.2346 + 1.2347 + // then check for any context menu items registered in the ui. 1.2348 + items = this._getNativeContextMenuItems(element, x, y); 1.2349 + if (items.length > 0) { 1.2350 + this._addMenuItems(items, context); 1.2351 + } 1.2352 + 1.2353 + // walk up the tree and find more items to show 1.2354 + element = element.parentNode; 1.2355 + } 1.2356 + }, 1.2357 + 1.2358 + // Actually shows the native context menu by passing a list of context menu items to 1.2359 + // show to the Java. 1.2360 + _show: function(aEvent) { 1.2361 + let popupNode = this._target; 1.2362 + this._target = null; 1.2363 + if (aEvent.defaultPrevented || !popupNode) { 1.2364 + return; 1.2365 + } 1.2366 + this._innerShow(popupNode, aEvent.clientX, aEvent.clientY); 1.2367 + }, 1.2368 + 1.2369 + // Walks the DOM tree to find a title from a node 1.2370 + _findTitle: function(node) { 1.2371 + let title = ""; 1.2372 + while(node && !title) { 1.2373 + title = this._getTitle(node); 1.2374 + node = node.parentNode; 1.2375 + } 1.2376 + return title; 1.2377 + }, 1.2378 + 1.2379 + /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm 1.2380 + * If there is one menu, will return a flat array of menuitems. If there are multiple 1.2381 + * menus, will return an array with appropriate tabs/items inside it. i.e. : 1.2382 + * [ 1.2383 + * { label: "link", items: [...] }, 1.2384 + * { label: "image", items: [...] } 1.2385 + * ] 1.2386 + */ 1.2387 + _reformatList: function(target) { 1.2388 + let contexts = Object.keys(this.menus); 1.2389 + 1.2390 + if (contexts.length === 1) { 1.2391 + // If there's only one context, we'll only show a single flat single select list 1.2392 + return this._reformatMenuItems(target, this.menus[contexts[0]]); 1.2393 + } 1.2394 + 1.2395 + // If there are multiple contexts, we'll only show a tabbed ui with multiple lists 1.2396 + return this._reformatListAsTabs(target, this.menus); 1.2397 + }, 1.2398 + 1.2399 + /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's 1.2400 + * addTabs method. i.e. : 1.2401 + * { link: [...], image: [...] } becomes 1.2402 + * [ { label: "link", items: [...] } ] 1.2403 + * 1.2404 + * Also reformats items and resolves any parmaeters that aren't known until display time 1.2405 + * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link). 1.2406 + */ 1.2407 + _reformatListAsTabs: function(target, menus) { 1.2408 + let itemArray = []; 1.2409 + 1.2410 + // Sort the keys so that "link" is always first 1.2411 + let contexts = Object.keys(this.menus); 1.2412 + contexts.sort((context1, context2) => { 1.2413 + if (context1 === this.defaultContext) { 1.2414 + return -1; 1.2415 + } else if (context2 === this.defaultContext) { 1.2416 + return 1; 1.2417 + } 1.2418 + return 0; 1.2419 + }); 1.2420 + 1.2421 + contexts.forEach(context => { 1.2422 + itemArray.push({ 1.2423 + label: context, 1.2424 + items: this._reformatMenuItems(target, menus[context]) 1.2425 + }); 1.2426 + }); 1.2427 + 1.2428 + return itemArray; 1.2429 + }, 1.2430 + 1.2431 + /* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items 1.2432 + * and resolves any parmaeters that aren't known until display time 1.2433 + * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link). 1.2434 + */ 1.2435 + _reformatMenuItems: function(target, menuitems) { 1.2436 + let itemArray = []; 1.2437 + 1.2438 + for (let i = 0; i < menuitems.length; i++) { 1.2439 + let t = target; 1.2440 + while(t) { 1.2441 + if (menuitems[i].matches(t)) { 1.2442 + let val = menuitems[i].getValue(t); 1.2443 + 1.2444 + // hidden menu items will return null from getValue 1.2445 + if (val) { 1.2446 + itemArray.push(val); 1.2447 + break; 1.2448 + } 1.2449 + } 1.2450 + 1.2451 + t = t.parentNode; 1.2452 + } 1.2453 + } 1.2454 + 1.2455 + return itemArray; 1.2456 + }, 1.2457 + 1.2458 + // Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt. 1.2459 + _innerShow: function(target, x, y) { 1.2460 + Haptic.performSimpleAction(Haptic.LongPress); 1.2461 + 1.2462 + // spin through the tree looking for a title for this context menu 1.2463 + let title = this._findTitle(target); 1.2464 + 1.2465 + for (let context in this.menus) { 1.2466 + let menu = this.menus[context]; 1.2467 + menu.sort((a,b) => { 1.2468 + if (a.order === b.order) { 1.2469 + return 0; 1.2470 + } 1.2471 + return (a.order > b.order) ? 1 : -1; 1.2472 + }); 1.2473 + } 1.2474 + 1.2475 + let useTabs = Object.keys(this.menus).length > 1; 1.2476 + let prompt = new Prompt({ 1.2477 + window: target.ownerDocument.defaultView, 1.2478 + title: useTabs ? undefined : title 1.2479 + }); 1.2480 + 1.2481 + let items = this._reformatList(target); 1.2482 + if (useTabs) { 1.2483 + prompt.addTabs({ 1.2484 + id: "tabs", 1.2485 + items: items 1.2486 + }); 1.2487 + } else { 1.2488 + prompt.setSingleChoiceItems(items); 1.2489 + } 1.2490 + 1.2491 + prompt.show(this._promptDone.bind(this, target, x, y, items)); 1.2492 + }, 1.2493 + 1.2494 + // Called when the contextmenu prompt is closed 1.2495 + _promptDone: function(target, x, y, items, data) { 1.2496 + if (data.button == -1) { 1.2497 + // Prompt was cancelled, or an ActionView was used. 1.2498 + return; 1.2499 + } 1.2500 + 1.2501 + let selectedItemId; 1.2502 + if (data.tabs) { 1.2503 + let menu = items[data.tabs.tab]; 1.2504 + selectedItemId = menu.items[data.tabs.item].id; 1.2505 + } else { 1.2506 + selectedItemId = items[data.list[0]].id 1.2507 + } 1.2508 + 1.2509 + let selectedItem = this._findMenuItem(selectedItemId); 1.2510 + this.menus = null; 1.2511 + 1.2512 + if (!selectedItem || !selectedItem.matches || !selectedItem.callback) { 1.2513 + return; 1.2514 + } 1.2515 + 1.2516 + // for menuitems added using the native UI, pass the dom element that matched that item to the callback 1.2517 + while (target) { 1.2518 + if (selectedItem.matches(target, x, y)) { 1.2519 + selectedItem.callback(target, x, y); 1.2520 + break; 1.2521 + } 1.2522 + target = target.parentNode; 1.2523 + } 1.2524 + }, 1.2525 + 1.2526 + // Called when the contextmenu is done propagating to content. If the event wasn't cancelled, will show a contextmenu. 1.2527 + handleEvent: function(aEvent) { 1.2528 + BrowserEventHandler._cancelTapHighlight(); 1.2529 + aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false); 1.2530 + this._show(aEvent); 1.2531 + }, 1.2532 + 1.2533 + // Called when a long press is observed in the native Java frontend. Will start the process of generating/showing a contextmenu. 1.2534 + observe: function(aSubject, aTopic, aData) { 1.2535 + let data = JSON.parse(aData); 1.2536 + // content gets first crack at cancelling context menus 1.2537 + this._sendToContent(data.x, data.y); 1.2538 + }, 1.2539 + 1.2540 + // XXX - These are stolen from Util.js, we should remove them if we bring it back 1.2541 + makeURLAbsolute: function makeURLAbsolute(base, url) { 1.2542 + // Note: makeURI() will throw if url is not a valid URI 1.2543 + return this.makeURI(url, null, this.makeURI(base)).spec; 1.2544 + }, 1.2545 + 1.2546 + makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) { 1.2547 + return Services.io.newURI(aURL, aOriginCharset, aBaseURI); 1.2548 + }, 1.2549 + 1.2550 + _getLink: function(aElement) { 1.2551 + if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && 1.2552 + ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) || 1.2553 + (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) || 1.2554 + aElement instanceof Ci.nsIDOMHTMLLinkElement || 1.2555 + aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) { 1.2556 + try { 1.2557 + let url = this._getLinkURL(aElement); 1.2558 + return Services.io.newURI(url, null, null); 1.2559 + } catch (e) {} 1.2560 + } 1.2561 + return null; 1.2562 + }, 1.2563 + 1.2564 + _disableInGuest: function _disableInGuest(selector) { 1.2565 + return { 1.2566 + matches: function _disableInGuestMatches(aElement, aX, aY) { 1.2567 + if (BrowserApp.isGuest) 1.2568 + return false; 1.2569 + return selector.matches(aElement, aX, aY); 1.2570 + } 1.2571 + }; 1.2572 + }, 1.2573 + 1.2574 + _getLinkURL: function ch_getLinkURL(aLink) { 1.2575 + let href = aLink.href; 1.2576 + if (href) 1.2577 + return href; 1.2578 + 1.2579 + href = aLink.getAttributeNS(kXLinkNamespace, "href"); 1.2580 + if (!href || !href.match(/\S/)) { 1.2581 + // Without this we try to save as the current doc, 1.2582 + // for example, HTML case also throws if empty 1.2583 + throw "Empty href"; 1.2584 + } 1.2585 + 1.2586 + return this.makeURLAbsolute(aLink.baseURI, href); 1.2587 + }, 1.2588 + 1.2589 + _copyStringToDefaultClipboard: function(aString) { 1.2590 + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); 1.2591 + clipboard.copyString(aString); 1.2592 + }, 1.2593 + 1.2594 + _shareStringWithDefault: function(aSharedString, aTitle) { 1.2595 + let sharing = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService); 1.2596 + sharing.shareWithDefault(aSharedString, "text/plain", aTitle); 1.2597 + }, 1.2598 + 1.2599 + _stripScheme: function(aString) { 1.2600 + let index = aString.indexOf(":"); 1.2601 + return aString.slice(index + 1); 1.2602 + } 1.2603 + } 1.2604 +}; 1.2605 + 1.2606 +var LightWeightThemeWebInstaller = { 1.2607 + init: function sh_init() { 1.2608 + let temp = {}; 1.2609 + Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp); 1.2610 + let theme = new temp.LightweightThemeConsumer(document); 1.2611 + BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true); 1.2612 + BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true); 1.2613 + BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true); 1.2614 + }, 1.2615 + 1.2616 + uninit: function() { 1.2617 + BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true); 1.2618 + BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true); 1.2619 + BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true); 1.2620 + }, 1.2621 + 1.2622 + handleEvent: function (event) { 1.2623 + switch (event.type) { 1.2624 + case "InstallBrowserTheme": 1.2625 + case "PreviewBrowserTheme": 1.2626 + case "ResetBrowserThemePreview": 1.2627 + // ignore requests from background tabs 1.2628 + if (event.target.ownerDocument.defaultView.top != content) 1.2629 + return; 1.2630 + } 1.2631 + 1.2632 + switch (event.type) { 1.2633 + case "InstallBrowserTheme": 1.2634 + this._installRequest(event); 1.2635 + break; 1.2636 + case "PreviewBrowserTheme": 1.2637 + this._preview(event); 1.2638 + break; 1.2639 + case "ResetBrowserThemePreview": 1.2640 + this._resetPreview(event); 1.2641 + break; 1.2642 + case "pagehide": 1.2643 + case "TabSelect": 1.2644 + this._resetPreview(); 1.2645 + break; 1.2646 + } 1.2647 + }, 1.2648 + 1.2649 + get _manager () { 1.2650 + let temp = {}; 1.2651 + Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); 1.2652 + delete this._manager; 1.2653 + return this._manager = temp.LightweightThemeManager; 1.2654 + }, 1.2655 + 1.2656 + _installRequest: function (event) { 1.2657 + let node = event.target; 1.2658 + let data = this._getThemeFromNode(node); 1.2659 + if (!data) 1.2660 + return; 1.2661 + 1.2662 + if (this._isAllowed(node)) { 1.2663 + this._install(data); 1.2664 + return; 1.2665 + } 1.2666 + 1.2667 + let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton"); 1.2668 + let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1); 1.2669 + let buttons = [{ 1.2670 + label: allowButtonText, 1.2671 + callback: function () { 1.2672 + LightWeightThemeWebInstaller._install(data); 1.2673 + } 1.2674 + }]; 1.2675 + 1.2676 + NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id); 1.2677 + }, 1.2678 + 1.2679 + _install: function (newLWTheme) { 1.2680 + this._manager.currentTheme = newLWTheme; 1.2681 + }, 1.2682 + 1.2683 + _previewWindow: null, 1.2684 + _preview: function (event) { 1.2685 + if (!this._isAllowed(event.target)) 1.2686 + return; 1.2687 + let data = this._getThemeFromNode(event.target); 1.2688 + if (!data) 1.2689 + return; 1.2690 + this._resetPreview(); 1.2691 + 1.2692 + this._previewWindow = event.target.ownerDocument.defaultView; 1.2693 + this._previewWindow.addEventListener("pagehide", this, true); 1.2694 + BrowserApp.deck.addEventListener("TabSelect", this, false); 1.2695 + this._manager.previewTheme(data); 1.2696 + }, 1.2697 + 1.2698 + _resetPreview: function (event) { 1.2699 + if (!this._previewWindow || 1.2700 + event && !this._isAllowed(event.target)) 1.2701 + return; 1.2702 + 1.2703 + this._previewWindow.removeEventListener("pagehide", this, true); 1.2704 + this._previewWindow = null; 1.2705 + BrowserApp.deck.removeEventListener("TabSelect", this, false); 1.2706 + 1.2707 + this._manager.resetPreview(); 1.2708 + }, 1.2709 + 1.2710 + _isAllowed: function (node) { 1.2711 + let pm = Services.perms; 1.2712 + 1.2713 + let uri = node.ownerDocument.documentURIObject; 1.2714 + return pm.testPermission(uri, "install") == pm.ALLOW_ACTION; 1.2715 + }, 1.2716 + 1.2717 + _getThemeFromNode: function (node) { 1.2718 + return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI); 1.2719 + } 1.2720 +}; 1.2721 + 1.2722 +var DesktopUserAgent = { 1.2723 + DESKTOP_UA: null, 1.2724 + 1.2725 + init: function ua_init() { 1.2726 + Services.obs.addObserver(this, "DesktopMode:Change", false); 1.2727 + UserAgentOverrides.addComplexOverride(this.onRequest.bind(this)); 1.2728 + 1.2729 + // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference 1.2730 + this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"] 1.2731 + .getService(Ci.nsIHttpProtocolHandler).userAgent 1.2732 + .replace(/Android; [a-zA-Z]+/, "X11; Linux x86_64") 1.2733 + .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101"); 1.2734 + }, 1.2735 + 1.2736 + uninit: function ua_uninit() { 1.2737 + Services.obs.removeObserver(this, "DesktopMode:Change"); 1.2738 + }, 1.2739 + 1.2740 + onRequest: function(channel, defaultUA) { 1.2741 + let channelWindow = this._getWindowForRequest(channel); 1.2742 + let tab = BrowserApp.getTabForWindow(channelWindow); 1.2743 + if (tab == null) 1.2744 + return null; 1.2745 + 1.2746 + return this.getUserAgentForTab(tab); 1.2747 + }, 1.2748 + 1.2749 + getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) { 1.2750 + let tab = BrowserApp.getTabForWindow(aWindow.top); 1.2751 + if (tab) 1.2752 + return this.getUserAgentForTab(tab); 1.2753 + 1.2754 + return null; 1.2755 + }, 1.2756 + 1.2757 + getUserAgentForTab: function ua_getUserAgentForTab(aTab) { 1.2758 + // Send desktop UA if "Request Desktop Site" is enabled. 1.2759 + if (aTab.desktopMode) 1.2760 + return this.DESKTOP_UA; 1.2761 + 1.2762 + return null; 1.2763 + }, 1.2764 + 1.2765 + _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) { 1.2766 + if (aRequest && aRequest.notificationCallbacks) { 1.2767 + try { 1.2768 + return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext); 1.2769 + } catch (ex) { } 1.2770 + } 1.2771 + 1.2772 + if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) { 1.2773 + try { 1.2774 + return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); 1.2775 + } catch (ex) { } 1.2776 + } 1.2777 + 1.2778 + return null; 1.2779 + }, 1.2780 + 1.2781 + _getWindowForRequest: function ua_getWindowForRequest(aRequest) { 1.2782 + let loadContext = this._getRequestLoadContext(aRequest); 1.2783 + if (loadContext) { 1.2784 + try { 1.2785 + return loadContext.associatedWindow; 1.2786 + } catch (e) { 1.2787 + // loadContext.associatedWindow can throw when there's no window 1.2788 + } 1.2789 + } 1.2790 + return null; 1.2791 + }, 1.2792 + 1.2793 + observe: function ua_observe(aSubject, aTopic, aData) { 1.2794 + if (aTopic === "DesktopMode:Change") { 1.2795 + let args = JSON.parse(aData); 1.2796 + let tab = BrowserApp.getTabForId(args.tabId); 1.2797 + if (tab != null) 1.2798 + tab.reloadWithMode(args.desktopMode); 1.2799 + } 1.2800 + } 1.2801 +}; 1.2802 + 1.2803 + 1.2804 +function nsBrowserAccess() { 1.2805 +} 1.2806 + 1.2807 +nsBrowserAccess.prototype = { 1.2808 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]), 1.2809 + 1.2810 + _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) { 1.2811 + let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); 1.2812 + if (isExternal && aURI && aURI.schemeIs("chrome")) 1.2813 + return null; 1.2814 + 1.2815 + let loadflags = isExternal ? 1.2816 + Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL : 1.2817 + Ci.nsIWebNavigation.LOAD_FLAGS_NONE; 1.2818 + if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) { 1.2819 + switch (aContext) { 1.2820 + case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL: 1.2821 + aWhere = Services.prefs.getIntPref("browser.link.open_external"); 1.2822 + break; 1.2823 + default: // OPEN_NEW or an illegal value 1.2824 + aWhere = Services.prefs.getIntPref("browser.link.open_newwindow"); 1.2825 + } 1.2826 + } 1.2827 + 1.2828 + Services.io.offline = false; 1.2829 + 1.2830 + let referrer; 1.2831 + if (aOpener) { 1.2832 + try { 1.2833 + let location = aOpener.location; 1.2834 + referrer = Services.io.newURI(location, null, null); 1.2835 + } catch(e) { } 1.2836 + } 1.2837 + 1.2838 + let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); 1.2839 + let pinned = false; 1.2840 + 1.2841 + if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) { 1.2842 + pinned = true; 1.2843 + let spec = aURI.spec; 1.2844 + let tabs = BrowserApp.tabs; 1.2845 + for (let i = 0; i < tabs.length; i++) { 1.2846 + let appOrigin = ss.getTabValue(tabs[i], "appOrigin"); 1.2847 + if (appOrigin == spec) { 1.2848 + let tab = tabs[i]; 1.2849 + BrowserApp.selectTab(tab); 1.2850 + return tab.browser; 1.2851 + } 1.2852 + } 1.2853 + } 1.2854 + 1.2855 + let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW || 1.2856 + aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB || 1.2857 + aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB); 1.2858 + let isPrivate = false; 1.2859 + 1.2860 + if (newTab) { 1.2861 + let parentId = -1; 1.2862 + if (!isExternal && aOpener) { 1.2863 + let parent = BrowserApp.getTabForWindow(aOpener.top); 1.2864 + if (parent) { 1.2865 + parentId = parent.id; 1.2866 + isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow); 1.2867 + } 1.2868 + } 1.2869 + 1.2870 + // BrowserApp.addTab calls loadURIWithFlags with the appropriate params 1.2871 + let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags, 1.2872 + referrerURI: referrer, 1.2873 + external: isExternal, 1.2874 + parentId: parentId, 1.2875 + selected: true, 1.2876 + isPrivate: isPrivate, 1.2877 + pinned: pinned }); 1.2878 + 1.2879 + return tab.browser; 1.2880 + } 1.2881 + 1.2882 + // OPEN_CURRENTWINDOW and illegal values 1.2883 + let browser = BrowserApp.selectedBrowser; 1.2884 + if (aURI && browser) 1.2885 + browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null); 1.2886 + 1.2887 + return browser; 1.2888 + }, 1.2889 + 1.2890 + openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) { 1.2891 + let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); 1.2892 + return browser ? browser.contentWindow : null; 1.2893 + }, 1.2894 + 1.2895 + openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) { 1.2896 + let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); 1.2897 + return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null; 1.2898 + }, 1.2899 + 1.2900 + isTabContentWindow: function(aWindow) { 1.2901 + return BrowserApp.getBrowserForWindow(aWindow) != null; 1.2902 + }, 1.2903 + 1.2904 + get contentWindow() { 1.2905 + return BrowserApp.selectedBrowser.contentWindow; 1.2906 + } 1.2907 +}; 1.2908 + 1.2909 + 1.2910 +// track the last known screen size so that new tabs 1.2911 +// get created with the right size rather than being 1x1 1.2912 +let gScreenWidth = 1; 1.2913 +let gScreenHeight = 1; 1.2914 +let gReflowPending = null; 1.2915 + 1.2916 +// The margins that should be applied to the viewport for fixed position 1.2917 +// children. This is used to avoid browser chrome permanently obscuring 1.2918 +// fixed position content, and also to make sure window-sized pages take 1.2919 +// into account said browser chrome. 1.2920 +let gViewportMargins = { top: 0, right: 0, bottom: 0, left: 0}; 1.2921 + 1.2922 +function Tab(aURL, aParams) { 1.2923 + this.browser = null; 1.2924 + this.id = 0; 1.2925 + this.lastTouchedAt = Date.now(); 1.2926 + this._zoom = 1.0; 1.2927 + this._drawZoom = 1.0; 1.2928 + this._restoreZoom = false; 1.2929 + this._fixedMarginLeft = 0; 1.2930 + this._fixedMarginTop = 0; 1.2931 + this._fixedMarginRight = 0; 1.2932 + this._fixedMarginBottom = 0; 1.2933 + this._readerEnabled = false; 1.2934 + this._readerActive = false; 1.2935 + this.userScrollPos = { x: 0, y: 0 }; 1.2936 + this.viewportExcludesHorizontalMargins = true; 1.2937 + this.viewportExcludesVerticalMargins = true; 1.2938 + this.viewportMeasureCallback = null; 1.2939 + this.lastPageSizeAfterViewportRemeasure = { width: 0, height: 0 }; 1.2940 + this.contentDocumentIsDisplayed = true; 1.2941 + this.pluginDoorhangerTimeout = null; 1.2942 + this.shouldShowPluginDoorhanger = true; 1.2943 + this.clickToPlayPluginsActivated = false; 1.2944 + this.desktopMode = false; 1.2945 + this.originalURI = null; 1.2946 + this.savedArticle = null; 1.2947 + this.hasTouchListener = false; 1.2948 + this.browserWidth = 0; 1.2949 + this.browserHeight = 0; 1.2950 + 1.2951 + this.create(aURL, aParams); 1.2952 +} 1.2953 + 1.2954 +Tab.prototype = { 1.2955 + create: function(aURL, aParams) { 1.2956 + if (this.browser) 1.2957 + return; 1.2958 + 1.2959 + aParams = aParams || {}; 1.2960 + 1.2961 + this.browser = document.createElement("browser"); 1.2962 + this.browser.setAttribute("type", "content-targetable"); 1.2963 + this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight); 1.2964 + 1.2965 + // Make sure the previously selected panel remains selected. The selected panel of a deck is 1.2966 + // not stable when panels are added. 1.2967 + let selectedPanel = BrowserApp.deck.selectedPanel; 1.2968 + BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null); 1.2969 + BrowserApp.deck.selectedPanel = selectedPanel; 1.2970 + 1.2971 + if (BrowserApp.manifestUrl) { 1.2972 + let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService); 1.2973 + let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl); 1.2974 + if (manifest) { 1.2975 + let app = manifest.QueryInterface(Ci.mozIApplication); 1.2976 + this.browser.docShell.setIsApp(app.localId); 1.2977 + } 1.2978 + } 1.2979 + 1.2980 + // Must be called after appendChild so the docshell has been created. 1.2981 + this.setActive(false); 1.2982 + 1.2983 + let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate; 1.2984 + if (isPrivate) { 1.2985 + this.browser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = true; 1.2986 + } 1.2987 + 1.2988 + this.browser.stop(); 1.2989 + 1.2990 + let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; 1.2991 + frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL; 1.2992 + 1.2993 + // only set tab uri if uri is valid 1.2994 + let uri = null; 1.2995 + let title = aParams.title || aURL; 1.2996 + try { 1.2997 + uri = Services.io.newURI(aURL, null, null).spec; 1.2998 + } catch (e) {} 1.2999 + 1.3000 + // When the tab is stubbed from Java, there's a window between the stub 1.3001 + // creation and the tab creation in Gecko where the stub could be removed 1.3002 + // or the selected tab can change (which is easiest to hit during startup). 1.3003 + // To prevent these races, we need to differentiate between tab stubs from 1.3004 + // Java and new tabs from Gecko. 1.3005 + let stub = false; 1.3006 + 1.3007 + if (!aParams.zombifying) { 1.3008 + if ("tabID" in aParams) { 1.3009 + this.id = aParams.tabID; 1.3010 + stub = true; 1.3011 + } else { 1.3012 + let jni = new JNI(); 1.3013 + let cls = jni.findClass("org/mozilla/gecko/Tabs"); 1.3014 + let method = jni.getStaticMethodID(cls, "getNextTabId", "()I"); 1.3015 + this.id = jni.callStaticIntMethod(cls, method); 1.3016 + jni.close(); 1.3017 + } 1.3018 + 1.3019 + this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false; 1.3020 + 1.3021 + let message = { 1.3022 + type: "Tab:Added", 1.3023 + tabID: this.id, 1.3024 + uri: uri, 1.3025 + parentId: ("parentId" in aParams) ? aParams.parentId : -1, 1.3026 + external: ("external" in aParams) ? aParams.external : false, 1.3027 + selected: ("selected" in aParams) ? aParams.selected : true, 1.3028 + title: title, 1.3029 + delayLoad: aParams.delayLoad || false, 1.3030 + desktopMode: this.desktopMode, 1.3031 + isPrivate: isPrivate, 1.3032 + stub: stub 1.3033 + }; 1.3034 + sendMessageToJava(message); 1.3035 + 1.3036 + this.overscrollController = new OverscrollController(this); 1.3037 + } 1.3038 + 1.3039 + this.browser.contentWindow.controllers.insertControllerAt(0, this.overscrollController); 1.3040 + 1.3041 + let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL | 1.3042 + Ci.nsIWebProgress.NOTIFY_LOCATION | 1.3043 + Ci.nsIWebProgress.NOTIFY_SECURITY; 1.3044 + this.browser.addProgressListener(this, flags); 1.3045 + this.browser.sessionHistory.addSHistoryListener(this); 1.3046 + 1.3047 + this.browser.addEventListener("DOMContentLoaded", this, true); 1.3048 + this.browser.addEventListener("DOMFormHasPassword", this, true); 1.3049 + this.browser.addEventListener("DOMLinkAdded", this, true); 1.3050 + this.browser.addEventListener("DOMTitleChanged", this, true); 1.3051 + this.browser.addEventListener("DOMWindowClose", this, true); 1.3052 + this.browser.addEventListener("DOMWillOpenModalDialog", this, true); 1.3053 + this.browser.addEventListener("DOMAutoComplete", this, true); 1.3054 + this.browser.addEventListener("blur", this, true); 1.3055 + this.browser.addEventListener("scroll", this, true); 1.3056 + this.browser.addEventListener("MozScrolledAreaChanged", this, true); 1.3057 + this.browser.addEventListener("pageshow", this, true); 1.3058 + this.browser.addEventListener("MozApplicationManifest", this, true); 1.3059 + 1.3060 + // Note that the XBL binding is untrusted 1.3061 + this.browser.addEventListener("PluginBindingAttached", this, true, true); 1.3062 + this.browser.addEventListener("VideoBindingAttached", this, true, true); 1.3063 + this.browser.addEventListener("VideoBindingCast", this, true, true); 1.3064 + 1.3065 + Services.obs.addObserver(this, "before-first-paint", false); 1.3066 + Services.obs.addObserver(this, "after-viewport-change", false); 1.3067 + Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false); 1.3068 + 1.3069 + if (aParams.delayLoad) { 1.3070 + // If this is a zombie tab, attach restore data so the tab will be 1.3071 + // restored when selected 1.3072 + this.browser.__SS_data = { 1.3073 + entries: [{ 1.3074 + url: aURL, 1.3075 + title: title 1.3076 + }], 1.3077 + index: 1 1.3078 + }; 1.3079 + this.browser.__SS_restore = true; 1.3080 + } else { 1.3081 + let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; 1.3082 + let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null; 1.3083 + let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null; 1.3084 + let charset = "charset" in aParams ? aParams.charset : null; 1.3085 + 1.3086 + // The search term the user entered to load the current URL 1.3087 + this.userSearch = "userSearch" in aParams ? aParams.userSearch : ""; 1.3088 + 1.3089 + try { 1.3090 + this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData); 1.3091 + } catch(e) { 1.3092 + let message = { 1.3093 + type: "Content:LoadError", 1.3094 + tabID: this.id 1.3095 + }; 1.3096 + sendMessageToJava(message); 1.3097 + dump("Handled load error: " + e); 1.3098 + } 1.3099 + } 1.3100 + }, 1.3101 + 1.3102 + /** 1.3103 + * Retrieves the font size in twips for a given element. 1.3104 + */ 1.3105 + getInflatedFontSizeFor: function(aElement) { 1.3106 + // GetComputedStyle should always give us CSS pixels for a font size. 1.3107 + let fontSizeStr = this.window.getComputedStyle(aElement)['fontSize']; 1.3108 + let fontSize = fontSizeStr.slice(0, -2); 1.3109 + return aElement.fontSizeInflation * fontSize; 1.3110 + }, 1.3111 + 1.3112 + /** 1.3113 + * This returns the zoom necessary to match the font size of an element to 1.3114 + * the minimum font size specified by the browser.zoom.reflowOnZoom.minFontSizeTwips 1.3115 + * preference. 1.3116 + */ 1.3117 + getZoomToMinFontSize: function(aElement) { 1.3118 + // We only use the font.size.inflation.minTwips preference because this is 1.3119 + // the only one that is controlled by the user-interface in the 'Settings' 1.3120 + // menu. Thus, if font.size.inflation.emPerLine is changed, this does not 1.3121 + // effect reflow-on-zoom. 1.3122 + let minFontSize = convertFromTwipsToPx(Services.prefs.getIntPref("font.size.inflation.minTwips")); 1.3123 + return minFontSize / this.getInflatedFontSizeFor(aElement); 1.3124 + }, 1.3125 + 1.3126 + clearReflowOnZoomPendingActions: function() { 1.3127 + // Reflow was completed, so now re-enable painting. 1.3128 + let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); 1.3129 + let docShell = webNav.QueryInterface(Ci.nsIDocShell); 1.3130 + let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); 1.3131 + docViewer.resumePainting(); 1.3132 + 1.3133 + BrowserApp.selectedTab._mReflozPositioned = false; 1.3134 + }, 1.3135 + 1.3136 + /** 1.3137 + * Reflow on zoom consists of a few different sub-operations: 1.3138 + * 1.3139 + * 1. When a double-tap event is seen, we verify that the correct preferences 1.3140 + * are enabled and perform the pre-position handling calculation. We also 1.3141 + * signal that reflow-on-zoom should be performed at this time, and pause 1.3142 + * painting. 1.3143 + * 2. During the next call to setViewport(), which is in the Tab prototype, 1.3144 + * we detect that a call to changeMaxLineBoxWidth should be performed. If 1.3145 + * we're zooming out, then the max line box width should be reset at this 1.3146 + * time. Otherwise, we call performReflowOnZoom. 1.3147 + * 2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to 1.3148 + * doChangeMaxLineBoxWidth, based on a timeout specified in preferences. 1.3149 + * 3. doChangeMaxLineBoxWidth changes the line box width (which also 1.3150 + * schedules a reflow event), and then calls ZoomHelper.zoomInAndSnapToRange. 1.3151 + * 4. ZoomHelper.zoomInAndSnapToRange performs the positioning of reflow-on-zoom 1.3152 + * and then re-enables painting. 1.3153 + * 1.3154 + * Some of the events happen synchronously, while others happen asynchronously. 1.3155 + * The following is a rough sketch of the progression of events: 1.3156 + * 1.3157 + * double tap event seen -> onDoubleTap() -> ... asynchronous ... 1.3158 + * -> setViewport() -> performReflowOnZoom() -> ... asynchronous ... 1.3159 + * -> doChangeMaxLineBoxWidth() -> ZoomHelper.zoomInAndSnapToRange() 1.3160 + * -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change') 1.3161 + * -> resumePainting() 1.3162 + */ 1.3163 + performReflowOnZoom: function(aViewport) { 1.3164 + let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom; 1.3165 + 1.3166 + let viewportWidth = gScreenWidth / zoom; 1.3167 + let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout"); 1.3168 + 1.3169 + if (gReflowPending) { 1.3170 + clearTimeout(gReflowPending); 1.3171 + } 1.3172 + 1.3173 + // We add in a bit of fudge just so that the end characters 1.3174 + // don't accidentally get clipped. 15px is an arbitrary choice. 1.3175 + gReflowPending = setTimeout(doChangeMaxLineBoxWidth, 1.3176 + reflozTimeout, 1.3177 + viewportWidth - 15); 1.3178 + }, 1.3179 + 1.3180 + /** 1.3181 + * Reloads the tab with the desktop mode setting. 1.3182 + */ 1.3183 + reloadWithMode: function (aDesktopMode) { 1.3184 + // Set desktop mode for tab and send change to Java 1.3185 + if (this.desktopMode != aDesktopMode) { 1.3186 + this.desktopMode = aDesktopMode; 1.3187 + sendMessageToJava({ 1.3188 + type: "DesktopMode:Changed", 1.3189 + desktopMode: aDesktopMode, 1.3190 + tabID: this.id 1.3191 + }); 1.3192 + } 1.3193 + 1.3194 + // Only reload the page for http/https schemes 1.3195 + let currentURI = this.browser.currentURI; 1.3196 + if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https")) 1.3197 + return; 1.3198 + 1.3199 + let url = currentURI.spec; 1.3200 + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE | 1.3201 + Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY; 1.3202 + if (this.originalURI && !this.originalURI.equals(currentURI)) { 1.3203 + // We were redirected; reload the original URL 1.3204 + url = this.originalURI.spec; 1.3205 + } 1.3206 + 1.3207 + this.browser.docShell.loadURI(url, flags, null, null, null); 1.3208 + }, 1.3209 + 1.3210 + destroy: function() { 1.3211 + if (!this.browser) 1.3212 + return; 1.3213 + 1.3214 + this.browser.contentWindow.controllers.removeController(this.overscrollController); 1.3215 + 1.3216 + this.browser.removeProgressListener(this); 1.3217 + this.browser.sessionHistory.removeSHistoryListener(this); 1.3218 + 1.3219 + this.browser.removeEventListener("DOMContentLoaded", this, true); 1.3220 + this.browser.removeEventListener("DOMFormHasPassword", this, true); 1.3221 + this.browser.removeEventListener("DOMLinkAdded", this, true); 1.3222 + this.browser.removeEventListener("DOMTitleChanged", this, true); 1.3223 + this.browser.removeEventListener("DOMWindowClose", this, true); 1.3224 + this.browser.removeEventListener("DOMWillOpenModalDialog", this, true); 1.3225 + this.browser.removeEventListener("DOMAutoComplete", this, true); 1.3226 + this.browser.removeEventListener("blur", this, true); 1.3227 + this.browser.removeEventListener("scroll", this, true); 1.3228 + this.browser.removeEventListener("MozScrolledAreaChanged", this, true); 1.3229 + this.browser.removeEventListener("pageshow", this, true); 1.3230 + this.browser.removeEventListener("MozApplicationManifest", this, true); 1.3231 + 1.3232 + this.browser.removeEventListener("PluginBindingAttached", this, true, true); 1.3233 + this.browser.removeEventListener("VideoBindingAttached", this, true, true); 1.3234 + this.browser.removeEventListener("VideoBindingCast", this, true, true); 1.3235 + 1.3236 + Services.obs.removeObserver(this, "before-first-paint"); 1.3237 + Services.obs.removeObserver(this, "after-viewport-change"); 1.3238 + Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this); 1.3239 + 1.3240 + // Make sure the previously selected panel remains selected. The selected panel of a deck is 1.3241 + // not stable when panels are removed. 1.3242 + let selectedPanel = BrowserApp.deck.selectedPanel; 1.3243 + BrowserApp.deck.removeChild(this.browser); 1.3244 + BrowserApp.deck.selectedPanel = selectedPanel; 1.3245 + 1.3246 + this.browser = null; 1.3247 + this.savedArticle = null; 1.3248 + }, 1.3249 + 1.3250 + // This should be called to update the browser when the tab gets selected/unselected 1.3251 + setActive: function setActive(aActive) { 1.3252 + if (!this.browser || !this.browser.docShell) 1.3253 + return; 1.3254 + 1.3255 + this.lastTouchedAt = Date.now(); 1.3256 + 1.3257 + if (aActive) { 1.3258 + this.browser.setAttribute("type", "content-primary"); 1.3259 + this.browser.focus(); 1.3260 + this.browser.docShellIsActive = true; 1.3261 + Reader.updatePageAction(this); 1.3262 + ExternalApps.updatePageAction(this.browser.currentURI); 1.3263 + } else { 1.3264 + this.browser.setAttribute("type", "content-targetable"); 1.3265 + this.browser.docShellIsActive = false; 1.3266 + } 1.3267 + }, 1.3268 + 1.3269 + getActive: function getActive() { 1.3270 + return this.browser.docShellIsActive; 1.3271 + }, 1.3272 + 1.3273 + setDisplayPort: function(aDisplayPort) { 1.3274 + let zoom = this._zoom; 1.3275 + let resolution = aDisplayPort.resolution; 1.3276 + if (zoom <= 0 || resolution <= 0) 1.3277 + return; 1.3278 + 1.3279 + // "zoom" is the user-visible zoom of the "this" tab 1.3280 + // "resolution" is the zoom at which we wish gecko to render "this" tab at 1.3281 + // these two may be different if we are, for example, trying to render a 1.3282 + // large area of the page at low resolution because the user is panning real 1.3283 + // fast. 1.3284 + // The gecko scroll position is in CSS pixels. The display port rect 1.3285 + // values (aDisplayPort), however, are in CSS pixels multiplied by the desired 1.3286 + // rendering resolution. Therefore care must be taken when doing math with 1.3287 + // these sets of values, to ensure that they are normalized to the same coordinate 1.3288 + // space first. 1.3289 + 1.3290 + let element = this.browser.contentDocument.documentElement; 1.3291 + if (!element) 1.3292 + return; 1.3293 + 1.3294 + // we should never be drawing background tabs at resolutions other than the user- 1.3295 + // visible zoom. for foreground tabs, however, if we are drawing at some other 1.3296 + // resolution, we need to set the resolution as specified. 1.3297 + let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.3298 + if (BrowserApp.selectedTab == this) { 1.3299 + if (resolution != this._drawZoom) { 1.3300 + this._drawZoom = resolution; 1.3301 + cwu.setResolution(resolution / window.devicePixelRatio, resolution / window.devicePixelRatio); 1.3302 + } 1.3303 + } else if (!fuzzyEquals(resolution, zoom)) { 1.3304 + dump("Warning: setDisplayPort resolution did not match zoom for background tab! (" + resolution + " != " + zoom + ")"); 1.3305 + } 1.3306 + 1.3307 + // Finally, we set the display port, taking care to convert everything into the CSS-pixel 1.3308 + // coordinate space, because that is what the function accepts. Also we have to fudge the 1.3309 + // displayport somewhat to make sure it gets through all the conversions gecko will do on it 1.3310 + // without deforming too much. See https://bugzilla.mozilla.org/show_bug.cgi?id=737510#c10 1.3311 + // for details on what these operations are. 1.3312 + let geckoScrollX = this.browser.contentWindow.scrollX; 1.3313 + let geckoScrollY = this.browser.contentWindow.scrollY; 1.3314 + aDisplayPort = this._dirtiestHackEverToWorkAroundGeckoRounding(aDisplayPort, geckoScrollX, geckoScrollY); 1.3315 + 1.3316 + let displayPort = { 1.3317 + x: (aDisplayPort.left / resolution) - geckoScrollX, 1.3318 + y: (aDisplayPort.top / resolution) - geckoScrollY, 1.3319 + width: (aDisplayPort.right - aDisplayPort.left) / resolution, 1.3320 + height: (aDisplayPort.bottom - aDisplayPort.top) / resolution 1.3321 + }; 1.3322 + 1.3323 + if (this._oldDisplayPort == null || 1.3324 + !fuzzyEquals(displayPort.x, this._oldDisplayPort.x) || 1.3325 + !fuzzyEquals(displayPort.y, this._oldDisplayPort.y) || 1.3326 + !fuzzyEquals(displayPort.width, this._oldDisplayPort.width) || 1.3327 + !fuzzyEquals(displayPort.height, this._oldDisplayPort.height)) { 1.3328 + if (BrowserApp.gUseLowPrecision) { 1.3329 + // Set the display-port to be 4x the size of the critical display-port, 1.3330 + // on each dimension, giving us a 0.25x lower precision buffer around the 1.3331 + // critical display-port. Spare area is *not* redistributed to the other 1.3332 + // axis, as display-list building and invalidation cost scales with the 1.3333 + // size of the display-port. 1.3334 + let pageRect = cwu.getRootBounds(); 1.3335 + let pageXMost = pageRect.right - geckoScrollX; 1.3336 + let pageYMost = pageRect.bottom - geckoScrollY; 1.3337 + 1.3338 + let dpW = Math.min(pageRect.right - pageRect.left, displayPort.width * 4); 1.3339 + let dpH = Math.min(pageRect.bottom - pageRect.top, displayPort.height * 4); 1.3340 + 1.3341 + let dpX = Math.min(Math.max(displayPort.x - displayPort.width * 1.5, 1.3342 + pageRect.left - geckoScrollX), pageXMost - dpW); 1.3343 + let dpY = Math.min(Math.max(displayPort.y - displayPort.height * 1.5, 1.3344 + pageRect.top - geckoScrollY), pageYMost - dpH); 1.3345 + cwu.setDisplayPortForElement(dpX, dpY, dpW, dpH, element, 0); 1.3346 + cwu.setCriticalDisplayPortForElement(displayPort.x, displayPort.y, 1.3347 + displayPort.width, displayPort.height, 1.3348 + element); 1.3349 + } else { 1.3350 + cwu.setDisplayPortForElement(displayPort.x, displayPort.y, 1.3351 + displayPort.width, displayPort.height, 1.3352 + element, 0); 1.3353 + } 1.3354 + } 1.3355 + 1.3356 + this._oldDisplayPort = displayPort; 1.3357 + }, 1.3358 + 1.3359 + /* 1.3360 + * Yes, this is ugly. But it's currently the safest way to account for the rounding errors that occur 1.3361 + * when we pump the displayport coordinates through gecko and they pop out in the compositor. 1.3362 + * 1.3363 + * In general, the values are converted from page-relative device pixels to viewport-relative app units, 1.3364 + * and then back to page-relative device pixels (now as ints). The first half of this is only slightly 1.3365 + * lossy, but it's enough to throw off the numbers a little. Because of this, when gecko calls 1.3366 + * ScaleToOutsidePixels to generate the final rect, the rect may get expanded more than it should, 1.3367 + * ending up a pixel larger than it started off. This is undesirable in general, but specifically 1.3368 + * bad for tiling, because it means we means we end up painting one line of pixels from a tile, 1.3369 + * causing an otherwise unnecessary upload of the whole tile. 1.3370 + * 1.3371 + * In order to counteract the rounding error, this code simulates the conversions that will happen 1.3372 + * to the display port, and calculates whether or not that final ScaleToOutsidePixels is actually 1.3373 + * expanding the rect more than it should. If so, it determines how much rounding error was introduced 1.3374 + * up until that point, and adjusts the original values to compensate for that rounding error. 1.3375 + */ 1.3376 + _dirtiestHackEverToWorkAroundGeckoRounding: function(aDisplayPort, aGeckoScrollX, aGeckoScrollY) { 1.3377 + const APP_UNITS_PER_CSS_PIXEL = 60.0; 1.3378 + const EXTRA_FUDGE = 0.04; 1.3379 + 1.3380 + let resolution = aDisplayPort.resolution; 1.3381 + 1.3382 + // Some helper functions that simulate conversion processes in gecko 1.3383 + 1.3384 + function cssPixelsToAppUnits(aVal) { 1.3385 + return Math.floor((aVal * APP_UNITS_PER_CSS_PIXEL) + 0.5); 1.3386 + } 1.3387 + 1.3388 + function appUnitsToDevicePixels(aVal) { 1.3389 + return aVal / APP_UNITS_PER_CSS_PIXEL * resolution; 1.3390 + } 1.3391 + 1.3392 + function devicePixelsToAppUnits(aVal) { 1.3393 + return cssPixelsToAppUnits(aVal / resolution); 1.3394 + } 1.3395 + 1.3396 + // Stash our original (desired) displayport width and height away, we need it 1.3397 + // later and we might modify the displayport in between. 1.3398 + let originalWidth = aDisplayPort.right - aDisplayPort.left; 1.3399 + let originalHeight = aDisplayPort.bottom - aDisplayPort.top; 1.3400 + 1.3401 + // This is the first conversion the displayport goes through, going from page-relative 1.3402 + // device pixels to viewport-relative app units. 1.3403 + let appUnitDisplayPort = { 1.3404 + x: cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX), 1.3405 + y: cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY), 1.3406 + w: cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution), 1.3407 + h: cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution) 1.3408 + }; 1.3409 + 1.3410 + // This is the translation gecko applies when converting back from viewport-relative 1.3411 + // device pixels to page-relative device pixels. 1.3412 + let geckoTransformX = -Math.floor((-aGeckoScrollX * resolution) + 0.5); 1.3413 + let geckoTransformY = -Math.floor((-aGeckoScrollY * resolution) + 0.5); 1.3414 + 1.3415 + // The final "left" value as calculated in gecko is: 1.3416 + // left = geckoTransformX + Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)) 1.3417 + // In a perfect world, this value would be identical to aDisplayPort.left, which is what 1.3418 + // we started with. However, this may not be the case if the value being floored has accumulated 1.3419 + // enough error to drop below what it should be. 1.3420 + // For example, assume geckoTransformX is 0, and aDisplayPort.left is 4, but 1.3421 + // appUnitsToDevicePixels(appUnitsToDevicePixels.x) comes out as 3.9 because of rounding error. 1.3422 + // That's bad, because the -0.1 error has caused it to floor to 3 instead of 4. (If it had errored 1.3423 + // the other way and come out as 4.1, there's no problem). In this example, we need to increase the 1.3424 + // "left" value by some amount so that the 3.9 actually comes out as >= 4, and it gets floored into 1.3425 + // the expected value of 4. The delta values calculated below calculate that error amount (e.g. -0.1). 1.3426 + let errorLeft = (geckoTransformX + appUnitsToDevicePixels(appUnitDisplayPort.x)) - aDisplayPort.left; 1.3427 + let errorTop = (geckoTransformY + appUnitsToDevicePixels(appUnitDisplayPort.y)) - aDisplayPort.top; 1.3428 + 1.3429 + // If the error was negative, that means it will floor incorrectly, so we need to bump up the 1.3430 + // original aDisplayPort.left and/or aDisplayPort.top values. The amount we bump it up by is 1.3431 + // the error amount (increased by a small fudge factor to ensure it's sufficient), converted 1.3432 + // backwards through the conversion process. 1.3433 + if (errorLeft < 0) { 1.3434 + aDisplayPort.left += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorLeft)); 1.3435 + // After we modify the left value, we need to re-simulate some values to take that into account 1.3436 + appUnitDisplayPort.x = cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX); 1.3437 + appUnitDisplayPort.w = cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution); 1.3438 + } 1.3439 + if (errorTop < 0) { 1.3440 + aDisplayPort.top += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorTop)); 1.3441 + // After we modify the top value, we need to re-simulate some values to take that into account 1.3442 + appUnitDisplayPort.y = cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY); 1.3443 + appUnitDisplayPort.h = cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution); 1.3444 + } 1.3445 + 1.3446 + // At this point, the aDisplayPort.left and aDisplayPort.top values have been corrected to account 1.3447 + // for the error in conversion such that they end up where we want them. Now we need to also do the 1.3448 + // same for the right/bottom values so that the width/height end up where we want them. 1.3449 + 1.3450 + // This is the final conversion that the displayport goes through before gecko spits it back to 1.3451 + // us. Note that the width/height calculates are of the form "ceil(transform(right)) - floor(transform(left))" 1.3452 + let scaledOutDevicePixels = { 1.3453 + x: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)), 1.3454 + y: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)), 1.3455 + w: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)), 1.3456 + h: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)) 1.3457 + }; 1.3458 + 1.3459 + // The final "width" value as calculated in gecko is scaledOutDevicePixels.w. 1.3460 + // In a perfect world, this would equal originalWidth. However, things are not perfect, and as before, 1.3461 + // we need to calculate how much rounding error has been introduced. In this case the rounding error is causing 1.3462 + // the Math.ceil call above to ceiling to the wrong final value. For example, 4 gets converted 4.1 and gets 1.3463 + // ceiling'd to 5; in this case the error is 0.1. 1.3464 + let errorRight = (appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w) - scaledOutDevicePixels.x) - originalWidth; 1.3465 + let errorBottom = (appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h) - scaledOutDevicePixels.y) - originalHeight; 1.3466 + 1.3467 + // If the error was positive, that means it will ceiling incorrectly, so we need to bump down the 1.3468 + // original aDisplayPort.right and/or aDisplayPort.bottom. Again, we back-convert the error amount 1.3469 + // with a small fudge factor to figure out how much to adjust the original values. 1.3470 + if (errorRight > 0) aDisplayPort.right -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorRight + EXTRA_FUDGE)); 1.3471 + if (errorBottom > 0) aDisplayPort.bottom -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorBottom + EXTRA_FUDGE)); 1.3472 + 1.3473 + // Et voila! 1.3474 + return aDisplayPort; 1.3475 + }, 1.3476 + 1.3477 + setScrollClampingSize: function(zoom) { 1.3478 + let viewportWidth = gScreenWidth / zoom; 1.3479 + let viewportHeight = gScreenHeight / zoom; 1.3480 + let screenWidth = gScreenWidth; 1.3481 + let screenHeight = gScreenHeight; 1.3482 + 1.3483 + // Shrink the viewport appropriately if the margins are excluded 1.3484 + if (this.viewportExcludesVerticalMargins) { 1.3485 + screenHeight = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom; 1.3486 + viewportHeight = screenHeight / zoom; 1.3487 + } 1.3488 + if (this.viewportExcludesHorizontalMargins) { 1.3489 + screenWidth = gScreenWidth - gViewportMargins.left - gViewportMargins.right; 1.3490 + viewportWidth = screenWidth / zoom; 1.3491 + } 1.3492 + 1.3493 + // Make sure the aspect ratio of the screen is maintained when setting 1.3494 + // the clamping scroll-port size. 1.3495 + let factor = Math.min(viewportWidth / screenWidth, 1.3496 + viewportHeight / screenHeight); 1.3497 + let scrollPortWidth = screenWidth * factor; 1.3498 + let scrollPortHeight = screenHeight * factor; 1.3499 + 1.3500 + let win = this.browser.contentWindow; 1.3501 + win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils). 1.3502 + setScrollPositionClampingScrollPortSize(scrollPortWidth, scrollPortHeight); 1.3503 + }, 1.3504 + 1.3505 + setViewport: function(aViewport) { 1.3506 + // Transform coordinates based on zoom 1.3507 + let x = aViewport.x / aViewport.zoom; 1.3508 + let y = aViewport.y / aViewport.zoom; 1.3509 + 1.3510 + this.setScrollClampingSize(aViewport.zoom); 1.3511 + 1.3512 + // Adjust the max line box width to be no more than the viewport width, but 1.3513 + // only if the reflow-on-zoom preference is enabled. 1.3514 + let isZooming = !fuzzyEquals(aViewport.zoom, this._zoom); 1.3515 + 1.3516 + let docViewer = null; 1.3517 + 1.3518 + if (isZooming && 1.3519 + BrowserEventHandler.mReflozPref && 1.3520 + BrowserApp.selectedTab._mReflozPoint && 1.3521 + BrowserApp.selectedTab.probablyNeedRefloz) { 1.3522 + let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); 1.3523 + let docShell = webNav.QueryInterface(Ci.nsIDocShell); 1.3524 + docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); 1.3525 + docViewer.pausePainting(); 1.3526 + 1.3527 + BrowserApp.selectedTab.performReflowOnZoom(aViewport); 1.3528 + BrowserApp.selectedTab.probablyNeedRefloz = false; 1.3529 + } 1.3530 + 1.3531 + let win = this.browser.contentWindow; 1.3532 + win.scrollTo(x, y); 1.3533 + this.saveSessionZoom(aViewport.zoom); 1.3534 + 1.3535 + this.userScrollPos.x = win.scrollX; 1.3536 + this.userScrollPos.y = win.scrollY; 1.3537 + this.setResolution(aViewport.zoom, false); 1.3538 + 1.3539 + if (aViewport.displayPort) 1.3540 + this.setDisplayPort(aViewport.displayPort); 1.3541 + 1.3542 + // Store fixed margins for later retrieval in getViewport. 1.3543 + this._fixedMarginLeft = aViewport.fixedMarginLeft; 1.3544 + this._fixedMarginTop = aViewport.fixedMarginTop; 1.3545 + this._fixedMarginRight = aViewport.fixedMarginRight; 1.3546 + this._fixedMarginBottom = aViewport.fixedMarginBottom; 1.3547 + 1.3548 + let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.3549 + dwi.setContentDocumentFixedPositionMargins( 1.3550 + aViewport.fixedMarginTop / aViewport.zoom, 1.3551 + aViewport.fixedMarginRight / aViewport.zoom, 1.3552 + aViewport.fixedMarginBottom / aViewport.zoom, 1.3553 + aViewport.fixedMarginLeft / aViewport.zoom); 1.3554 + 1.3555 + Services.obs.notifyObservers(null, "after-viewport-change", ""); 1.3556 + if (docViewer) { 1.3557 + docViewer.resumePainting(); 1.3558 + } 1.3559 + }, 1.3560 + 1.3561 + setResolution: function(aZoom, aForce) { 1.3562 + // Set zoom level 1.3563 + if (aForce || !fuzzyEquals(aZoom, this._zoom)) { 1.3564 + this._zoom = aZoom; 1.3565 + if (BrowserApp.selectedTab == this) { 1.3566 + let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.3567 + this._drawZoom = aZoom; 1.3568 + cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio); 1.3569 + } 1.3570 + } 1.3571 + }, 1.3572 + 1.3573 + getPageSize: function(aDocument, aDefaultWidth, aDefaultHeight) { 1.3574 + let body = aDocument.body || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight }; 1.3575 + let html = aDocument.documentElement || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight }; 1.3576 + return [Math.max(body.scrollWidth, html.scrollWidth), 1.3577 + Math.max(body.scrollHeight, html.scrollHeight)]; 1.3578 + }, 1.3579 + 1.3580 + getViewport: function() { 1.3581 + let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right; 1.3582 + let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom; 1.3583 + let zoom = this.restoredSessionZoom() || this._zoom; 1.3584 + 1.3585 + let viewport = { 1.3586 + width: screenW, 1.3587 + height: screenH, 1.3588 + cssWidth: screenW / zoom, 1.3589 + cssHeight: screenH / zoom, 1.3590 + pageLeft: 0, 1.3591 + pageTop: 0, 1.3592 + pageRight: screenW, 1.3593 + pageBottom: screenH, 1.3594 + // We make up matching css page dimensions 1.3595 + cssPageLeft: 0, 1.3596 + cssPageTop: 0, 1.3597 + cssPageRight: screenW / zoom, 1.3598 + cssPageBottom: screenH / zoom, 1.3599 + fixedMarginLeft: this._fixedMarginLeft, 1.3600 + fixedMarginTop: this._fixedMarginTop, 1.3601 + fixedMarginRight: this._fixedMarginRight, 1.3602 + fixedMarginBottom: this._fixedMarginBottom, 1.3603 + zoom: zoom, 1.3604 + }; 1.3605 + 1.3606 + // Set the viewport offset to current scroll offset 1.3607 + viewport.cssX = this.browser.contentWindow.scrollX || 0; 1.3608 + viewport.cssY = this.browser.contentWindow.scrollY || 0; 1.3609 + 1.3610 + // Transform coordinates based on zoom 1.3611 + viewport.x = Math.round(viewport.cssX * viewport.zoom); 1.3612 + viewport.y = Math.round(viewport.cssY * viewport.zoom); 1.3613 + 1.3614 + let doc = this.browser.contentDocument; 1.3615 + if (doc != null) { 1.3616 + let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.3617 + let cssPageRect = cwu.getRootBounds(); 1.3618 + 1.3619 + /* 1.3620 + * Avoid sending page sizes of less than screen size before we hit DOMContentLoaded, because 1.3621 + * this causes the page size to jump around wildly during page load. After the page is loaded, 1.3622 + * send updates regardless of page size; we'll zoom to fit the content as needed. 1.3623 + * 1.3624 + * In the check below, we floor the viewport size because there might be slight rounding errors 1.3625 + * introduced in the CSS page size due to the conversion to and from app units in Gecko. The 1.3626 + * error should be no more than one app unit so doing the floor is overkill, but safe in the 1.3627 + * sense that the extra page size updates that get sent as a result will be mostly harmless. 1.3628 + */ 1.3629 + let pageLargerThanScreen = (cssPageRect.width >= Math.floor(viewport.cssWidth)) 1.3630 + && (cssPageRect.height >= Math.floor(viewport.cssHeight)); 1.3631 + if (doc.readyState === 'complete' || pageLargerThanScreen) { 1.3632 + viewport.cssPageLeft = cssPageRect.left; 1.3633 + viewport.cssPageTop = cssPageRect.top; 1.3634 + viewport.cssPageRight = cssPageRect.right; 1.3635 + viewport.cssPageBottom = cssPageRect.bottom; 1.3636 + /* Transform the page width and height based on the zoom factor. */ 1.3637 + viewport.pageLeft = (viewport.cssPageLeft * viewport.zoom); 1.3638 + viewport.pageTop = (viewport.cssPageTop * viewport.zoom); 1.3639 + viewport.pageRight = (viewport.cssPageRight * viewport.zoom); 1.3640 + viewport.pageBottom = (viewport.cssPageBottom * viewport.zoom); 1.3641 + } 1.3642 + } 1.3643 + 1.3644 + return viewport; 1.3645 + }, 1.3646 + 1.3647 + sendViewportUpdate: function(aPageSizeUpdate) { 1.3648 + let viewport = this.getViewport(); 1.3649 + let displayPort = Services.androidBridge.getDisplayPort(aPageSizeUpdate, BrowserApp.isBrowserContentDocumentDisplayed(), this.id, viewport); 1.3650 + if (displayPort != null) 1.3651 + this.setDisplayPort(displayPort); 1.3652 + }, 1.3653 + 1.3654 + updateViewportForPageSize: function() { 1.3655 + let hasHorizontalMargins = gViewportMargins.left != 0 || gViewportMargins.right != 0; 1.3656 + let hasVerticalMargins = gViewportMargins.top != 0 || gViewportMargins.bottom != 0; 1.3657 + 1.3658 + if (!hasHorizontalMargins && !hasVerticalMargins) { 1.3659 + // If there are no margins, then we don't need to do any remeasuring 1.3660 + return; 1.3661 + } 1.3662 + 1.3663 + // If the page size has changed so that it might or might not fit on the 1.3664 + // screen with the margins included, run updateViewportSize to resize the 1.3665 + // browser accordingly. 1.3666 + // A page will receive the smaller viewport when its page size fits 1.3667 + // within the screen size, so remeasure when the page size remains within 1.3668 + // the threshold of screen + margins, in case it's sizing itself relative 1.3669 + // to the viewport. 1.3670 + let viewport = this.getViewport(); 1.3671 + let pageWidth = viewport.pageRight - viewport.pageLeft; 1.3672 + let pageHeight = viewport.pageBottom - viewport.pageTop; 1.3673 + let remeasureNeeded = false; 1.3674 + 1.3675 + if (hasHorizontalMargins) { 1.3676 + let viewportShouldExcludeHorizontalMargins = (pageWidth <= gScreenWidth - 0.5); 1.3677 + if (viewportShouldExcludeHorizontalMargins != this.viewportExcludesHorizontalMargins) { 1.3678 + remeasureNeeded = true; 1.3679 + } 1.3680 + } 1.3681 + if (hasVerticalMargins) { 1.3682 + let viewportShouldExcludeVerticalMargins = (pageHeight <= gScreenHeight - 0.5); 1.3683 + if (viewportShouldExcludeVerticalMargins != this.viewportExcludesVerticalMargins) { 1.3684 + remeasureNeeded = true; 1.3685 + } 1.3686 + } 1.3687 + 1.3688 + if (remeasureNeeded) { 1.3689 + if (!this.viewportMeasureCallback) { 1.3690 + this.viewportMeasureCallback = setTimeout(function() { 1.3691 + this.viewportMeasureCallback = null; 1.3692 + 1.3693 + // Re-fetch the viewport as it may have changed between setting the timeout 1.3694 + // and running this callback 1.3695 + let viewport = this.getViewport(); 1.3696 + let pageWidth = viewport.pageRight - viewport.pageLeft; 1.3697 + let pageHeight = viewport.pageBottom - viewport.pageTop; 1.3698 + 1.3699 + if (Math.abs(pageWidth - this.lastPageSizeAfterViewportRemeasure.width) >= 0.5 || 1.3700 + Math.abs(pageHeight - this.lastPageSizeAfterViewportRemeasure.height) >= 0.5) { 1.3701 + this.updateViewportSize(gScreenWidth); 1.3702 + } 1.3703 + }.bind(this), kViewportRemeasureThrottle); 1.3704 + } 1.3705 + } else if (this.viewportMeasureCallback) { 1.3706 + // If the page changed size twice since we last measured the viewport and 1.3707 + // the latest size change reveals we don't need to remeasure, cancel any 1.3708 + // pending remeasure. 1.3709 + clearTimeout(this.viewportMeasureCallback); 1.3710 + this.viewportMeasureCallback = null; 1.3711 + } 1.3712 + }, 1.3713 + 1.3714 + handleEvent: function(aEvent) { 1.3715 + switch (aEvent.type) { 1.3716 + case "DOMContentLoaded": { 1.3717 + let target = aEvent.originalTarget; 1.3718 + 1.3719 + // ignore on frames and other documents 1.3720 + if (target != this.browser.contentDocument) 1.3721 + return; 1.3722 + 1.3723 + // Sample the background color of the page and pass it along. (This is used to draw the 1.3724 + // checkerboard.) Right now we don't detect changes in the background color after this 1.3725 + // event fires; it's not clear that doing so is worth the effort. 1.3726 + var backgroundColor = null; 1.3727 + try { 1.3728 + let { contentDocument, contentWindow } = this.browser; 1.3729 + let computedStyle = contentWindow.getComputedStyle(contentDocument.body); 1.3730 + backgroundColor = computedStyle.backgroundColor; 1.3731 + } catch (e) { 1.3732 + // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds. 1.3733 + } 1.3734 + 1.3735 + let docURI = target.documentURI; 1.3736 + let errorType = ""; 1.3737 + if (docURI.startsWith("about:certerror")) 1.3738 + errorType = "certerror"; 1.3739 + else if (docURI.startsWith("about:blocked")) 1.3740 + errorType = "blocked" 1.3741 + else if (docURI.startsWith("about:neterror")) 1.3742 + errorType = "neterror"; 1.3743 + 1.3744 + sendMessageToJava({ 1.3745 + type: "DOMContentLoaded", 1.3746 + tabID: this.id, 1.3747 + bgColor: backgroundColor, 1.3748 + errorType: errorType 1.3749 + }); 1.3750 + 1.3751 + // Attach a listener to watch for "click" events bubbling up from error 1.3752 + // pages and other similar page. This lets us fix bugs like 401575 which 1.3753 + // require error page UI to do privileged things, without letting error 1.3754 + // pages have any privilege themselves. 1.3755 + if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) { 1.3756 + this.browser.addEventListener("click", ErrorPageEventHandler, true); 1.3757 + let listener = function() { 1.3758 + this.browser.removeEventListener("click", ErrorPageEventHandler, true); 1.3759 + this.browser.removeEventListener("pagehide", listener, true); 1.3760 + }.bind(this); 1.3761 + 1.3762 + this.browser.addEventListener("pagehide", listener, true); 1.3763 + } 1.3764 + 1.3765 + if (docURI.startsWith("about:reader")) { 1.3766 + // During browser restart / recovery, duplicate "DOMContentLoaded" messages are received here 1.3767 + // For the visible tab ... where more than one tab is being reloaded, the inital "DOMContentLoaded" 1.3768 + // Message can be received before the document body is available ... so we avoid instantiating an 1.3769 + // AboutReader object, expecting that an eventual valid message will follow. 1.3770 + let contentDocument = this.browser.contentDocument; 1.3771 + if (contentDocument.body) { 1.3772 + new AboutReader(contentDocument, this.browser.contentWindow); 1.3773 + } 1.3774 + } 1.3775 + 1.3776 + break; 1.3777 + } 1.3778 + 1.3779 + case "DOMFormHasPassword": { 1.3780 + LoginManagerContent.onFormPassword(aEvent); 1.3781 + break; 1.3782 + } 1.3783 + 1.3784 + case "DOMLinkAdded": { 1.3785 + let target = aEvent.originalTarget; 1.3786 + if (!target.href || target.disabled) 1.3787 + return; 1.3788 + 1.3789 + // Ignore on frames and other documents 1.3790 + if (target.ownerDocument != this.browser.contentDocument) 1.3791 + return; 1.3792 + 1.3793 + // Sanitize the rel string 1.3794 + let list = []; 1.3795 + if (target.rel) { 1.3796 + list = target.rel.toLowerCase().split(/\s+/); 1.3797 + let hash = {}; 1.3798 + list.forEach(function(value) { hash[value] = true; }); 1.3799 + list = []; 1.3800 + for (let rel in hash) 1.3801 + list.push("[" + rel + "]"); 1.3802 + } 1.3803 + 1.3804 + if (list.indexOf("[icon]") != -1) { 1.3805 + // We want to get the largest icon size possible for our UI. 1.3806 + let maxSize = 0; 1.3807 + 1.3808 + // We use the sizes attribute if available 1.3809 + // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon 1.3810 + if (target.hasAttribute("sizes")) { 1.3811 + let sizes = target.getAttribute("sizes").toLowerCase(); 1.3812 + 1.3813 + if (sizes == "any") { 1.3814 + // Since Java expects an integer, use -1 to represent icons with sizes="any" 1.3815 + maxSize = -1; 1.3816 + } else { 1.3817 + let tokens = sizes.split(" "); 1.3818 + tokens.forEach(function(token) { 1.3819 + // TODO: check for invalid tokens 1.3820 + let [w, h] = token.split("x"); 1.3821 + maxSize = Math.max(maxSize, Math.max(w, h)); 1.3822 + }); 1.3823 + } 1.3824 + } 1.3825 + 1.3826 + let json = { 1.3827 + type: "Link:Favicon", 1.3828 + tabID: this.id, 1.3829 + href: resolveGeckoURI(target.href), 1.3830 + charset: target.ownerDocument.characterSet, 1.3831 + title: target.title, 1.3832 + rel: list.join(" "), 1.3833 + size: maxSize 1.3834 + }; 1.3835 + sendMessageToJava(json); 1.3836 + } else if (list.indexOf("[alternate]") != -1) { 1.3837 + let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, ""); 1.3838 + let isFeed = (type == "application/rss+xml" || type == "application/atom+xml"); 1.3839 + 1.3840 + if (!isFeed) 1.3841 + return; 1.3842 + 1.3843 + try { 1.3844 + // urlSecurityCeck will throw if things are not OK 1.3845 + ContentAreaUtils.urlSecurityCheck(target.href, target.ownerDocument.nodePrincipal, Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); 1.3846 + 1.3847 + if (!this.browser.feeds) 1.3848 + this.browser.feeds = []; 1.3849 + this.browser.feeds.push({ href: target.href, title: target.title, type: type }); 1.3850 + 1.3851 + let json = { 1.3852 + type: "Link:Feed", 1.3853 + tabID: this.id 1.3854 + }; 1.3855 + sendMessageToJava(json); 1.3856 + } catch (e) {} 1.3857 + } else if (list.indexOf("[search]" != -1)) { 1.3858 + let type = target.type && target.type.toLowerCase(); 1.3859 + 1.3860 + // Replace all starting or trailing spaces or spaces before "*;" globally w/ "". 1.3861 + type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); 1.3862 + 1.3863 + // Check that type matches opensearch. 1.3864 + let isOpenSearch = (type == "application/opensearchdescription+xml"); 1.3865 + if (isOpenSearch && target.title && /^(?:https?|ftp):/i.test(target.href)) { 1.3866 + let visibleEngines = Services.search.getVisibleEngines(); 1.3867 + // NOTE: Engines are currently identified by name, but this can be changed 1.3868 + // when Engines are identified by URL (see bug 335102). 1.3869 + if (visibleEngines.some(function(e) { 1.3870 + return e.name == target.title; 1.3871 + })) { 1.3872 + // This engine is already present, do nothing. 1.3873 + return; 1.3874 + } 1.3875 + 1.3876 + if (this.browser.engines) { 1.3877 + // This engine has already been handled, do nothing. 1.3878 + if (this.browser.engines.some(function(e) { 1.3879 + return e.url == target.href; 1.3880 + })) { 1.3881 + return; 1.3882 + } 1.3883 + } else { 1.3884 + this.browser.engines = []; 1.3885 + } 1.3886 + 1.3887 + // Get favicon. 1.3888 + let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico"; 1.3889 + 1.3890 + let newEngine = { 1.3891 + title: target.title, 1.3892 + url: target.href, 1.3893 + iconURL: iconURL 1.3894 + }; 1.3895 + 1.3896 + this.browser.engines.push(newEngine); 1.3897 + 1.3898 + // Don't send a message to display engines if we've already handled an engine. 1.3899 + if (this.browser.engines.length > 1) 1.3900 + return; 1.3901 + 1.3902 + // Broadcast message that this tab contains search engines that should be visible. 1.3903 + let newEngineMessage = { 1.3904 + type: "Link:OpenSearch", 1.3905 + tabID: this.id, 1.3906 + visible: true 1.3907 + }; 1.3908 + 1.3909 + sendMessageToJava(newEngineMessage); 1.3910 + } 1.3911 + } 1.3912 + break; 1.3913 + } 1.3914 + 1.3915 + case "DOMTitleChanged": { 1.3916 + if (!aEvent.isTrusted) 1.3917 + return; 1.3918 + 1.3919 + // ignore on frames and other documents 1.3920 + if (aEvent.originalTarget != this.browser.contentDocument) 1.3921 + return; 1.3922 + 1.3923 + sendMessageToJava({ 1.3924 + type: "DOMTitleChanged", 1.3925 + tabID: this.id, 1.3926 + title: aEvent.target.title.substring(0, 255) 1.3927 + }); 1.3928 + break; 1.3929 + } 1.3930 + 1.3931 + case "DOMWindowClose": { 1.3932 + if (!aEvent.isTrusted) 1.3933 + return; 1.3934 + 1.3935 + // Find the relevant tab, and close it from Java 1.3936 + if (this.browser.contentWindow == aEvent.target) { 1.3937 + aEvent.preventDefault(); 1.3938 + 1.3939 + sendMessageToJava({ 1.3940 + type: "Tab:Close", 1.3941 + tabID: this.id 1.3942 + }); 1.3943 + } 1.3944 + break; 1.3945 + } 1.3946 + 1.3947 + case "DOMWillOpenModalDialog": { 1.3948 + if (!aEvent.isTrusted) 1.3949 + return; 1.3950 + 1.3951 + // We're about to open a modal dialog, make sure the opening 1.3952 + // tab is brought to the front. 1.3953 + let tab = BrowserApp.getTabForWindow(aEvent.target.top); 1.3954 + BrowserApp.selectTab(tab); 1.3955 + break; 1.3956 + } 1.3957 + 1.3958 + case "DOMAutoComplete": 1.3959 + case "blur": { 1.3960 + LoginManagerContent.onUsernameInput(aEvent); 1.3961 + break; 1.3962 + } 1.3963 + 1.3964 + case "scroll": { 1.3965 + let win = this.browser.contentWindow; 1.3966 + if (this.userScrollPos.x != win.scrollX || this.userScrollPos.y != win.scrollY) { 1.3967 + this.sendViewportUpdate(); 1.3968 + } 1.3969 + break; 1.3970 + } 1.3971 + 1.3972 + case "MozScrolledAreaChanged": { 1.3973 + // This event is only fired for root scroll frames, and only when the 1.3974 + // scrolled area has actually changed, so no need to check for that. 1.3975 + // Just make sure it's the event for the correct root scroll frame. 1.3976 + if (aEvent.originalTarget != this.browser.contentDocument) 1.3977 + return; 1.3978 + 1.3979 + this.sendViewportUpdate(true); 1.3980 + this.updateViewportForPageSize(); 1.3981 + break; 1.3982 + } 1.3983 + 1.3984 + case "PluginBindingAttached": { 1.3985 + PluginHelper.handlePluginBindingAttached(this, aEvent); 1.3986 + break; 1.3987 + } 1.3988 + 1.3989 + case "VideoBindingAttached": { 1.3990 + CastingApps.handleVideoBindingAttached(this, aEvent); 1.3991 + break; 1.3992 + } 1.3993 + 1.3994 + case "VideoBindingCast": { 1.3995 + CastingApps.handleVideoBindingCast(this, aEvent); 1.3996 + break; 1.3997 + } 1.3998 + 1.3999 + case "MozApplicationManifest": { 1.4000 + OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView); 1.4001 + break; 1.4002 + } 1.4003 + 1.4004 + case "pageshow": { 1.4005 + // only send pageshow for the top-level document 1.4006 + if (aEvent.originalTarget.defaultView != this.browser.contentWindow) 1.4007 + return; 1.4008 + 1.4009 + sendMessageToJava({ 1.4010 + type: "Content:PageShow", 1.4011 + tabID: this.id 1.4012 + }); 1.4013 + 1.4014 + if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) { 1.4015 + if (!this._linkifier) 1.4016 + this._linkifier = new Linkifier(); 1.4017 + this._linkifier.linkifyNumbers(this.browser.contentWindow.document); 1.4018 + } 1.4019 + 1.4020 + // Update page actions for helper apps. 1.4021 + let uri = this.browser.currentURI; 1.4022 + if (BrowserApp.selectedTab == this) { 1.4023 + if (ExternalApps.shouldCheckUri(uri)) { 1.4024 + ExternalApps.updatePageAction(uri); 1.4025 + } else { 1.4026 + ExternalApps.clearPageAction(); 1.4027 + } 1.4028 + } 1.4029 + 1.4030 + if (!Reader.isEnabledForParseOnLoad) 1.4031 + return; 1.4032 + 1.4033 + // Once document is fully loaded, parse it 1.4034 + Reader.parseDocumentFromTab(this.id, function (article) { 1.4035 + // Do nothing if there's no article or the page in this tab has 1.4036 + // changed 1.4037 + let tabURL = uri.specIgnoringRef; 1.4038 + if (article == null || (article.url != tabURL)) { 1.4039 + // Don't clear the article for about:reader pages since we want to 1.4040 + // use the article from the previous page 1.4041 + if (!tabURL.startsWith("about:reader")) { 1.4042 + this.savedArticle = null; 1.4043 + this.readerEnabled = false; 1.4044 + this.readerActive = false; 1.4045 + } else { 1.4046 + this.readerActive = true; 1.4047 + } 1.4048 + return; 1.4049 + } 1.4050 + 1.4051 + this.savedArticle = article; 1.4052 + 1.4053 + sendMessageToJava({ 1.4054 + type: "Content:ReaderEnabled", 1.4055 + tabID: this.id 1.4056 + }); 1.4057 + 1.4058 + if(this.readerActive) 1.4059 + this.readerActive = false; 1.4060 + 1.4061 + if(!this.readerEnabled) 1.4062 + this.readerEnabled = true; 1.4063 + }.bind(this)); 1.4064 + } 1.4065 + } 1.4066 + }, 1.4067 + 1.4068 + onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { 1.4069 + let contentWin = aWebProgress.DOMWindow; 1.4070 + if (contentWin != contentWin.top) 1.4071 + return; 1.4072 + 1.4073 + // Filter optimization: Only really send NETWORK state changes to Java listener 1.4074 + if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { 1.4075 + if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) { 1.4076 + // We may receive a document stop event while a document is still loading 1.4077 + // (such as when doing URI fixup). Don't notify Java UI in these cases. 1.4078 + return; 1.4079 + } 1.4080 + 1.4081 + // Clear page-specific opensearch engines and feeds for a new request. 1.4082 + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) { 1.4083 + this.browser.engines = null; 1.4084 + this.browser.feeds = null; 1.4085 + } 1.4086 + 1.4087 + // true if the page loaded successfully (i.e., no 404s or other errors) 1.4088 + let success = false; 1.4089 + let uri = ""; 1.4090 + try { 1.4091 + // Remember original URI for UA changes on redirected pages 1.4092 + this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI; 1.4093 + 1.4094 + if (this.originalURI != null) 1.4095 + uri = this.originalURI.spec; 1.4096 + } catch (e) { } 1.4097 + try { 1.4098 + success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded; 1.4099 + } catch (e) { 1.4100 + // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success 1.4101 + // status. Used for local files. See bug 948849. 1.4102 + success = aRequest.status == 0; 1.4103 + } 1.4104 + 1.4105 + // Check to see if we restoring the content from a previous presentation (session) 1.4106 + // since there should be no real network activity 1.4107 + let restoring = (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) > 0; 1.4108 + 1.4109 + let message = { 1.4110 + type: "Content:StateChange", 1.4111 + tabID: this.id, 1.4112 + uri: uri, 1.4113 + state: aStateFlags, 1.4114 + restoring: restoring, 1.4115 + success: success 1.4116 + }; 1.4117 + sendMessageToJava(message); 1.4118 + } 1.4119 + }, 1.4120 + 1.4121 + onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) { 1.4122 + let contentWin = aWebProgress.DOMWindow; 1.4123 + 1.4124 + // Browser webapps may load content inside iframes that can not reach across the app/frame boundary 1.4125 + // i.e. even though the page is loaded in an iframe window.top != webapp 1.4126 + // Make cure this window is a top level tab before moving on. 1.4127 + if (BrowserApp.getBrowserForWindow(contentWin) == null) 1.4128 + return; 1.4129 + 1.4130 + this._hostChanged = true; 1.4131 + 1.4132 + let fixedURI = aLocationURI; 1.4133 + try { 1.4134 + fixedURI = URIFixup.createExposableURI(aLocationURI); 1.4135 + } catch (ex) { } 1.4136 + 1.4137 + let contentType = contentWin.document.contentType; 1.4138 + 1.4139 + // If fixedURI matches browser.lastURI, we assume this isn't a real location 1.4140 + // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883. 1.4141 + // Note that we have to ensure fixedURI is not the same as aLocationURI so we 1.4142 + // don't false-positive page reloads as spurious additions. 1.4143 + let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 || 1.4144 + ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI)); 1.4145 + this.browser.lastURI = fixedURI; 1.4146 + 1.4147 + // Reset state of click-to-play plugin notifications. 1.4148 + clearTimeout(this.pluginDoorhangerTimeout); 1.4149 + this.pluginDoorhangerTimeout = null; 1.4150 + this.shouldShowPluginDoorhanger = true; 1.4151 + this.clickToPlayPluginsActivated = false; 1.4152 + // Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#174 1.4153 + let documentURI = contentWin.document.documentURIObject.spec 1.4154 + let matchedURL = documentURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/); 1.4155 + let baseDomain = ""; 1.4156 + if (matchedURL) { 1.4157 + var domain = ""; 1.4158 + [, , domain] = matchedURL; 1.4159 + 1.4160 + try { 1.4161 + baseDomain = Services.eTLD.getBaseDomainFromHost(domain); 1.4162 + if (!domain.endsWith(baseDomain)) { 1.4163 + // getBaseDomainFromHost converts its resultant to ACE. 1.4164 + let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService); 1.4165 + baseDomain = IDNService.convertACEtoUTF8(baseDomain); 1.4166 + } 1.4167 + } catch (e) {} 1.4168 + } 1.4169 + 1.4170 + // Update the page actions URI for helper apps. 1.4171 + if (BrowserApp.selectedTab == this) { 1.4172 + ExternalApps.updatePageActionUri(fixedURI); 1.4173 + } 1.4174 + 1.4175 + let message = { 1.4176 + type: "Content:LocationChange", 1.4177 + tabID: this.id, 1.4178 + uri: fixedURI.spec, 1.4179 + userSearch: this.userSearch || "", 1.4180 + baseDomain: baseDomain, 1.4181 + contentType: (contentType ? contentType : ""), 1.4182 + sameDocument: sameDocument 1.4183 + }; 1.4184 + 1.4185 + sendMessageToJava(message); 1.4186 + 1.4187 + // The search term is only valid for this location change event, so reset it here. 1.4188 + this.userSearch = ""; 1.4189 + 1.4190 + if (!sameDocument) { 1.4191 + // XXX This code assumes that this is the earliest hook we have at which 1.4192 + // browser.contentDocument is changed to the new document we're loading 1.4193 + this.contentDocumentIsDisplayed = false; 1.4194 + this.hasTouchListener = false; 1.4195 + } else { 1.4196 + this.sendViewportUpdate(); 1.4197 + } 1.4198 + }, 1.4199 + 1.4200 + // Properties used to cache security state used to update the UI 1.4201 + _state: null, 1.4202 + _hostChanged: false, // onLocationChange will flip this bit 1.4203 + 1.4204 + onSecurityChange: function(aWebProgress, aRequest, aState) { 1.4205 + // Don't need to do anything if the data we use to update the UI hasn't changed 1.4206 + if (this._state == aState && !this._hostChanged) 1.4207 + return; 1.4208 + 1.4209 + this._state = aState; 1.4210 + this._hostChanged = false; 1.4211 + 1.4212 + let identity = IdentityHandler.checkIdentity(aState, this.browser); 1.4213 + 1.4214 + let message = { 1.4215 + type: "Content:SecurityChange", 1.4216 + tabID: this.id, 1.4217 + identity: identity 1.4218 + }; 1.4219 + 1.4220 + sendMessageToJava(message); 1.4221 + }, 1.4222 + 1.4223 + onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { 1.4224 + }, 1.4225 + 1.4226 + onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) { 1.4227 + }, 1.4228 + 1.4229 + _sendHistoryEvent: function(aMessage, aParams) { 1.4230 + let message = { 1.4231 + type: "SessionHistory:" + aMessage, 1.4232 + tabID: this.id, 1.4233 + }; 1.4234 + 1.4235 + // Restore zoom only when moving in session history, not for new page loads. 1.4236 + this._restoreZoom = aMessage != "New"; 1.4237 + 1.4238 + if (aParams) { 1.4239 + if ("url" in aParams) 1.4240 + message.url = aParams.url; 1.4241 + if ("index" in aParams) 1.4242 + message.index = aParams.index; 1.4243 + if ("numEntries" in aParams) 1.4244 + message.numEntries = aParams.numEntries; 1.4245 + } 1.4246 + 1.4247 + sendMessageToJava(message); 1.4248 + }, 1.4249 + 1.4250 + _getGeckoZoom: function() { 1.4251 + let res = {x: {}, y: {}}; 1.4252 + let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.4253 + cwu.getResolution(res.x, res.y); 1.4254 + let zoom = res.x.value * window.devicePixelRatio; 1.4255 + return zoom; 1.4256 + }, 1.4257 + 1.4258 + saveSessionZoom: function(aZoom) { 1.4259 + let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.4260 + cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio); 1.4261 + }, 1.4262 + 1.4263 + restoredSessionZoom: function() { 1.4264 + let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.4265 + 1.4266 + if (this._restoreZoom && cwu.isResolutionSet) { 1.4267 + return this._getGeckoZoom(); 1.4268 + } 1.4269 + return null; 1.4270 + }, 1.4271 + 1.4272 + OnHistoryNewEntry: function(aUri) { 1.4273 + this._sendHistoryEvent("New", { url: aUri.spec }); 1.4274 + }, 1.4275 + 1.4276 + OnHistoryGoBack: function(aUri) { 1.4277 + this._sendHistoryEvent("Back"); 1.4278 + return true; 1.4279 + }, 1.4280 + 1.4281 + OnHistoryGoForward: function(aUri) { 1.4282 + this._sendHistoryEvent("Forward"); 1.4283 + return true; 1.4284 + }, 1.4285 + 1.4286 + OnHistoryReload: function(aUri, aFlags) { 1.4287 + // we don't do anything with this, so don't propagate it 1.4288 + // for now anyway 1.4289 + return true; 1.4290 + }, 1.4291 + 1.4292 + OnHistoryGotoIndex: function(aIndex, aUri) { 1.4293 + this._sendHistoryEvent("Goto", { index: aIndex }); 1.4294 + return true; 1.4295 + }, 1.4296 + 1.4297 + OnHistoryPurge: function(aNumEntries) { 1.4298 + this._sendHistoryEvent("Purge", { numEntries: aNumEntries }); 1.4299 + return true; 1.4300 + }, 1.4301 + 1.4302 + OnHistoryReplaceEntry: function(aIndex) { 1.4303 + // we don't do anything with this, so don't propogate it 1.4304 + // for now anyway. 1.4305 + }, 1.4306 + 1.4307 + get metadata() { 1.4308 + return ViewportHandler.getMetadataForDocument(this.browser.contentDocument); 1.4309 + }, 1.4310 + 1.4311 + /** Update viewport when the metadata changes. */ 1.4312 + updateViewportMetadata: function updateViewportMetadata(aMetadata, aInitialLoad) { 1.4313 + if (Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) { 1.4314 + aMetadata.allowZoom = true; 1.4315 + aMetadata.allowDoubleTapZoom = true; 1.4316 + aMetadata.minZoom = aMetadata.maxZoom = NaN; 1.4317 + } 1.4318 + 1.4319 + let scaleRatio = window.devicePixelRatio; 1.4320 + 1.4321 + if (aMetadata.defaultZoom > 0) 1.4322 + aMetadata.defaultZoom *= scaleRatio; 1.4323 + if (aMetadata.minZoom > 0) 1.4324 + aMetadata.minZoom *= scaleRatio; 1.4325 + if (aMetadata.maxZoom > 0) 1.4326 + aMetadata.maxZoom *= scaleRatio; 1.4327 + 1.4328 + aMetadata.isRTL = this.browser.contentDocument.documentElement.dir == "rtl"; 1.4329 + 1.4330 + ViewportHandler.setMetadataForDocument(this.browser.contentDocument, aMetadata); 1.4331 + this.sendViewportMetadata(); 1.4332 + 1.4333 + this.updateViewportSize(gScreenWidth, aInitialLoad); 1.4334 + }, 1.4335 + 1.4336 + /** Update viewport when the metadata or the window size changes. */ 1.4337 + updateViewportSize: function updateViewportSize(aOldScreenWidth, aInitialLoad) { 1.4338 + // When this function gets called on window resize, we must execute 1.4339 + // this.sendViewportUpdate() so that refreshDisplayPort is called. 1.4340 + // Ensure that when making changes to this function that code path 1.4341 + // is not accidentally removed (the call to sendViewportUpdate() is 1.4342 + // at the very end). 1.4343 + 1.4344 + if (this.viewportMeasureCallback) { 1.4345 + clearTimeout(this.viewportMeasureCallback); 1.4346 + this.viewportMeasureCallback = null; 1.4347 + } 1.4348 + 1.4349 + let browser = this.browser; 1.4350 + if (!browser) 1.4351 + return; 1.4352 + 1.4353 + let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right; 1.4354 + let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom; 1.4355 + let viewportW, viewportH; 1.4356 + 1.4357 + let metadata = this.metadata; 1.4358 + if (metadata.autoSize) { 1.4359 + viewportW = screenW / window.devicePixelRatio; 1.4360 + viewportH = screenH / window.devicePixelRatio; 1.4361 + } else { 1.4362 + viewportW = metadata.width; 1.4363 + viewportH = metadata.height; 1.4364 + 1.4365 + // If (scale * width) < device-width, increase the width (bug 561413). 1.4366 + let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom; 1.4367 + if (maxInitialZoom && viewportW) { 1.4368 + viewportW = Math.max(viewportW, screenW / maxInitialZoom); 1.4369 + } 1.4370 + 1.4371 + let validW = viewportW > 0; 1.4372 + let validH = viewportH > 0; 1.4373 + 1.4374 + if (!validW) 1.4375 + viewportW = validH ? (viewportH * (screenW / screenH)) : BrowserApp.defaultBrowserWidth; 1.4376 + if (!validH) 1.4377 + viewportH = viewportW * (screenH / screenW); 1.4378 + } 1.4379 + 1.4380 + // Make sure the viewport height is not shorter than the window when 1.4381 + // the page is zoomed out to show its full width. Note that before 1.4382 + // we set the viewport width, the "full width" of the page isn't properly 1.4383 + // defined, so that's why we have to call setBrowserSize twice - once 1.4384 + // to set the width, and the second time to figure out the height based 1.4385 + // on the layout at that width. 1.4386 + let oldBrowserWidth = this.browserWidth; 1.4387 + this.setBrowserSize(viewportW, viewportH); 1.4388 + 1.4389 + // This change to the zoom accounts for all types of changes I can conceive: 1.4390 + // 1. screen size changes, CSS viewport does not (pages with no meta viewport 1.4391 + // or a fixed size viewport) 1.4392 + // 2. screen size changes, CSS viewport also does (pages with a device-width 1.4393 + // viewport) 1.4394 + // 3. screen size remains constant, but CSS viewport changes (meta viewport 1.4395 + // tag is added or removed) 1.4396 + // 4. neither screen size nor CSS viewport changes 1.4397 + // 1.4398 + // In all of these cases, we maintain how much actual content is visible 1.4399 + // within the screen width. Note that "actual content" may be different 1.4400 + // with respect to CSS pixels because of the CSS viewport size changing. 1.4401 + let zoom = this.restoredSessionZoom() || metadata.defaultZoom; 1.4402 + if (!zoom || !aInitialLoad) { 1.4403 + let zoomScale = (screenW * oldBrowserWidth) / (aOldScreenWidth * viewportW); 1.4404 + zoom = this.clampZoom(this._zoom * zoomScale); 1.4405 + } 1.4406 + this.setResolution(zoom, false); 1.4407 + this.setScrollClampingSize(zoom); 1.4408 + 1.4409 + // if this page has not been painted yet, then this must be getting run 1.4410 + // because a meta-viewport element was added (via the DOMMetaAdded handler). 1.4411 + // in this case, we should not do anything that forces a reflow (see bug 759678) 1.4412 + // such as requesting the page size or sending a viewport update. this code 1.4413 + // will get run again in the before-first-paint handler and that point we 1.4414 + // will run though all of it. the reason we even bother executing up to this 1.4415 + // point on the DOMMetaAdded handler is so that scripts that use window.innerWidth 1.4416 + // before they are painted have a correct value (bug 771575). 1.4417 + if (!this.contentDocumentIsDisplayed) { 1.4418 + return; 1.4419 + } 1.4420 + 1.4421 + this.viewportExcludesHorizontalMargins = true; 1.4422 + this.viewportExcludesVerticalMargins = true; 1.4423 + let minScale = 1.0; 1.4424 + if (this.browser.contentDocument) { 1.4425 + // this may get run during a Viewport:Change message while the document 1.4426 + // has not yet loaded, so need to guard against a null document. 1.4427 + let [pageWidth, pageHeight] = this.getPageSize(this.browser.contentDocument, viewportW, viewportH); 1.4428 + 1.4429 + // In the situation the page size equals or exceeds the screen size, 1.4430 + // lengthen the viewport on the corresponding axis to include the margins. 1.4431 + // The '- 0.5' is to account for rounding errors. 1.4432 + if (pageWidth * this._zoom > gScreenWidth - 0.5) { 1.4433 + screenW = gScreenWidth; 1.4434 + this.viewportExcludesHorizontalMargins = false; 1.4435 + } 1.4436 + if (pageHeight * this._zoom > gScreenHeight - 0.5) { 1.4437 + screenH = gScreenHeight; 1.4438 + this.viewportExcludesVerticalMargins = false; 1.4439 + } 1.4440 + 1.4441 + minScale = screenW / pageWidth; 1.4442 + } 1.4443 + minScale = this.clampZoom(minScale); 1.4444 + viewportH = Math.max(viewportH, screenH / minScale); 1.4445 + 1.4446 + // In general we want to keep calls to setBrowserSize and setScrollClampingSize 1.4447 + // together because setBrowserSize could mark the viewport size as dirty, creating 1.4448 + // a pending resize event for content. If that resize gets dispatched (which happens 1.4449 + // on the next reflow) without setScrollClampingSize having being called, then 1.4450 + // content might be exposed to incorrect innerWidth/innerHeight values. 1.4451 + this.setBrowserSize(viewportW, viewportH); 1.4452 + this.setScrollClampingSize(zoom); 1.4453 + 1.4454 + // Avoid having the scroll position jump around after device rotation. 1.4455 + let win = this.browser.contentWindow; 1.4456 + this.userScrollPos.x = win.scrollX; 1.4457 + this.userScrollPos.y = win.scrollY; 1.4458 + 1.4459 + this.sendViewportUpdate(); 1.4460 + 1.4461 + if (metadata.allowZoom && !Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) { 1.4462 + // If the CSS viewport is narrower than the screen (i.e. width <= device-width) 1.4463 + // then we disable double-tap-to-zoom behaviour. 1.4464 + var oldAllowDoubleTapZoom = metadata.allowDoubleTapZoom; 1.4465 + var newAllowDoubleTapZoom = (!metadata.isSpecified) || (viewportW > screenW / window.devicePixelRatio); 1.4466 + if (oldAllowDoubleTapZoom !== newAllowDoubleTapZoom) { 1.4467 + metadata.allowDoubleTapZoom = newAllowDoubleTapZoom; 1.4468 + this.sendViewportMetadata(); 1.4469 + } 1.4470 + } 1.4471 + 1.4472 + // Store the page size that was used to calculate the viewport so that we 1.4473 + // can verify it's changed when we consider remeasuring in updateViewportForPageSize 1.4474 + let viewport = this.getViewport(); 1.4475 + this.lastPageSizeAfterViewportRemeasure = { 1.4476 + width: viewport.pageRight - viewport.pageLeft, 1.4477 + height: viewport.pageBottom - viewport.pageTop 1.4478 + }; 1.4479 + }, 1.4480 + 1.4481 + sendViewportMetadata: function sendViewportMetadata() { 1.4482 + let metadata = this.metadata; 1.4483 + sendMessageToJava({ 1.4484 + type: "Tab:ViewportMetadata", 1.4485 + allowZoom: metadata.allowZoom, 1.4486 + allowDoubleTapZoom: metadata.allowDoubleTapZoom, 1.4487 + defaultZoom: metadata.defaultZoom || window.devicePixelRatio, 1.4488 + minZoom: metadata.minZoom || 0, 1.4489 + maxZoom: metadata.maxZoom || 0, 1.4490 + isRTL: metadata.isRTL, 1.4491 + tabID: this.id 1.4492 + }); 1.4493 + }, 1.4494 + 1.4495 + setBrowserSize: function(aWidth, aHeight) { 1.4496 + if (fuzzyEquals(this.browserWidth, aWidth) && fuzzyEquals(this.browserHeight, aHeight)) { 1.4497 + return; 1.4498 + } 1.4499 + 1.4500 + this.browserWidth = aWidth; 1.4501 + this.browserHeight = aHeight; 1.4502 + 1.4503 + if (!this.browser.contentWindow) 1.4504 + return; 1.4505 + let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.4506 + cwu.setCSSViewport(aWidth, aHeight); 1.4507 + }, 1.4508 + 1.4509 + /** Takes a scale and restricts it based on this tab's zoom limits. */ 1.4510 + clampZoom: function clampZoom(aZoom) { 1.4511 + let zoom = ViewportHandler.clamp(aZoom, kViewportMinScale, kViewportMaxScale); 1.4512 + 1.4513 + let md = this.metadata; 1.4514 + if (!md.allowZoom) 1.4515 + return md.defaultZoom || zoom; 1.4516 + 1.4517 + if (md && md.minZoom) 1.4518 + zoom = Math.max(zoom, md.minZoom); 1.4519 + if (md && md.maxZoom) 1.4520 + zoom = Math.min(zoom, md.maxZoom); 1.4521 + return zoom; 1.4522 + }, 1.4523 + 1.4524 + observe: function(aSubject, aTopic, aData) { 1.4525 + switch (aTopic) { 1.4526 + case "before-first-paint": 1.4527 + // Is it on the top level? 1.4528 + let contentDocument = aSubject; 1.4529 + if (contentDocument == this.browser.contentDocument) { 1.4530 + if (BrowserApp.selectedTab == this) { 1.4531 + BrowserApp.contentDocumentChanged(); 1.4532 + } 1.4533 + this.contentDocumentIsDisplayed = true; 1.4534 + 1.4535 + // reset CSS viewport and zoom to default on new page, and then calculate 1.4536 + // them properly using the actual metadata from the page. note that the 1.4537 + // updateMetadata call takes into account the existing CSS viewport size 1.4538 + // and zoom when calculating the new ones, so we need to reset these 1.4539 + // things here before calling updateMetadata. 1.4540 + this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight); 1.4541 + let zoom = this.restoredSessionZoom() || gScreenWidth / this.browserWidth; 1.4542 + this.setResolution(zoom, true); 1.4543 + ViewportHandler.updateMetadata(this, true); 1.4544 + 1.4545 + // Note that if we draw without a display-port, things can go wrong. By the 1.4546 + // time we execute this, it's almost certain a display-port has been set via 1.4547 + // the MozScrolledAreaChanged event. If that didn't happen, the updateMetadata 1.4548 + // call above does so at the end of the updateViewportSize function. As long 1.4549 + // as that is happening, we don't need to do it again here. 1.4550 + 1.4551 + if (!this.restoredSessionZoom() && contentDocument.mozSyntheticDocument) { 1.4552 + // for images, scale to fit width. this needs to happen *after* the call 1.4553 + // to updateMetadata above, because that call sets the CSS viewport which 1.4554 + // will affect the page size (i.e. contentDocument.body.scroll*) that we 1.4555 + // use in this calculation. also we call sendViewportUpdate after changing 1.4556 + // the resolution so that the display port gets recalculated appropriately. 1.4557 + let fitZoom = Math.min(gScreenWidth / contentDocument.body.scrollWidth, 1.4558 + gScreenHeight / contentDocument.body.scrollHeight); 1.4559 + this.setResolution(fitZoom, false); 1.4560 + this.sendViewportUpdate(); 1.4561 + } 1.4562 + } 1.4563 + 1.4564 + // If the reflow-text-on-page-load pref is enabled, and reflow-on-zoom 1.4565 + // is enabled, and our defaultZoom level is set, then we need to get 1.4566 + // the default zoom and reflow the text according to the defaultZoom 1.4567 + // level. 1.4568 + let rzEnabled = BrowserEventHandler.mReflozPref; 1.4569 + let rzPl = Services.prefs.getBoolPref("browser.zoom.reflowZoom.reflowTextOnPageLoad"); 1.4570 + 1.4571 + if (rzEnabled && rzPl) { 1.4572 + // Retrieve the viewport width and adjust the max line box width 1.4573 + // accordingly. 1.4574 + let vp = BrowserApp.selectedTab.getViewport(); 1.4575 + BrowserApp.selectedTab.performReflowOnZoom(vp); 1.4576 + } 1.4577 + break; 1.4578 + case "after-viewport-change": 1.4579 + if (BrowserApp.selectedTab._mReflozPositioned) { 1.4580 + BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); 1.4581 + } 1.4582 + break; 1.4583 + case "nsPref:changed": 1.4584 + if (aData == "browser.ui.zoom.force-user-scalable") 1.4585 + ViewportHandler.updateMetadata(this, false); 1.4586 + break; 1.4587 + } 1.4588 + }, 1.4589 + 1.4590 + set readerEnabled(isReaderEnabled) { 1.4591 + this._readerEnabled = isReaderEnabled; 1.4592 + if (this.getActive()) 1.4593 + Reader.updatePageAction(this); 1.4594 + }, 1.4595 + 1.4596 + get readerEnabled() { 1.4597 + return this._readerEnabled; 1.4598 + }, 1.4599 + 1.4600 + set readerActive(isReaderActive) { 1.4601 + this._readerActive = isReaderActive; 1.4602 + if (this.getActive()) 1.4603 + Reader.updatePageAction(this); 1.4604 + }, 1.4605 + 1.4606 + get readerActive() { 1.4607 + return this._readerActive; 1.4608 + }, 1.4609 + 1.4610 + // nsIBrowserTab 1.4611 + get window() { 1.4612 + if (!this.browser) 1.4613 + return null; 1.4614 + return this.browser.contentWindow; 1.4615 + }, 1.4616 + 1.4617 + get scale() { 1.4618 + return this._zoom; 1.4619 + }, 1.4620 + 1.4621 + QueryInterface: XPCOMUtils.generateQI([ 1.4622 + Ci.nsIWebProgressListener, 1.4623 + Ci.nsISHistoryListener, 1.4624 + Ci.nsIObserver, 1.4625 + Ci.nsISupportsWeakReference, 1.4626 + Ci.nsIBrowserTab 1.4627 + ]) 1.4628 +}; 1.4629 + 1.4630 +var BrowserEventHandler = { 1.4631 + init: function init() { 1.4632 + Services.obs.addObserver(this, "Gesture:SingleTap", false); 1.4633 + Services.obs.addObserver(this, "Gesture:CancelTouch", false); 1.4634 + Services.obs.addObserver(this, "Gesture:DoubleTap", false); 1.4635 + Services.obs.addObserver(this, "Gesture:Scroll", false); 1.4636 + Services.obs.addObserver(this, "dom-touch-listener-added", false); 1.4637 + 1.4638 + BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false); 1.4639 + BrowserApp.deck.addEventListener("touchstart", this, true); 1.4640 + BrowserApp.deck.addEventListener("click", InputWidgetHelper, true); 1.4641 + BrowserApp.deck.addEventListener("click", SelectHelper, true); 1.4642 + 1.4643 + SpatialNavigation.init(BrowserApp.deck, null); 1.4644 + 1.4645 + document.addEventListener("MozMagnifyGesture", this, true); 1.4646 + 1.4647 + Services.prefs.addObserver("browser.zoom.reflowOnZoom", this, false); 1.4648 + this.updateReflozPref(); 1.4649 + }, 1.4650 + 1.4651 + resetMaxLineBoxWidth: function() { 1.4652 + BrowserApp.selectedTab.probablyNeedRefloz = false; 1.4653 + 1.4654 + if (gReflowPending) { 1.4655 + clearTimeout(gReflowPending); 1.4656 + } 1.4657 + 1.4658 + let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout"); 1.4659 + gReflowPending = setTimeout(doChangeMaxLineBoxWidth, 1.4660 + reflozTimeout, 0); 1.4661 + }, 1.4662 + 1.4663 + updateReflozPref: function() { 1.4664 + this.mReflozPref = Services.prefs.getBoolPref("browser.zoom.reflowOnZoom"); 1.4665 + }, 1.4666 + 1.4667 + handleEvent: function(aEvent) { 1.4668 + switch (aEvent.type) { 1.4669 + case 'touchstart': 1.4670 + this._handleTouchStart(aEvent); 1.4671 + break; 1.4672 + case 'MozMagnifyGesture': 1.4673 + this.observe(this, aEvent.type, 1.4674 + JSON.stringify({x: aEvent.screenX, y: aEvent.screenY, 1.4675 + zoomDelta: aEvent.delta})); 1.4676 + break; 1.4677 + } 1.4678 + }, 1.4679 + 1.4680 + _handleTouchStart: function(aEvent) { 1.4681 + if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.touches.length > 1 || aEvent.defaultPrevented) 1.4682 + return; 1.4683 + 1.4684 + let closest = aEvent.target; 1.4685 + 1.4686 + if (closest) { 1.4687 + // If we've pressed a scrollable element, let Java know that we may 1.4688 + // want to override the scroll behaviour (for document sub-frames) 1.4689 + this._scrollableElement = this._findScrollableElement(closest, true); 1.4690 + this._firstScrollEvent = true; 1.4691 + 1.4692 + if (this._scrollableElement != null) { 1.4693 + // Discard if it's the top-level scrollable, we let Java handle this 1.4694 + // The top-level scrollable is the body in quirks mode and the html element 1.4695 + // in standards mode 1.4696 + let doc = BrowserApp.selectedBrowser.contentDocument; 1.4697 + let rootScrollable = (doc.compatMode === "BackCompat" ? doc.body : doc.documentElement); 1.4698 + if (this._scrollableElement != rootScrollable) { 1.4699 + sendMessageToJava({ type: "Panning:Override" }); 1.4700 + } 1.4701 + } 1.4702 + } 1.4703 + 1.4704 + if (!ElementTouchHelper.isElementClickable(closest, null, false)) 1.4705 + closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX, 1.4706 + aEvent.changedTouches[0].screenY); 1.4707 + if (!closest) 1.4708 + closest = aEvent.target; 1.4709 + 1.4710 + if (closest) { 1.4711 + let uri = this._getLinkURI(closest); 1.4712 + if (uri) { 1.4713 + try { 1.4714 + Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null); 1.4715 + } catch (e) {} 1.4716 + } 1.4717 + this._doTapHighlight(closest); 1.4718 + } 1.4719 + }, 1.4720 + 1.4721 + _getLinkURI: function(aElement) { 1.4722 + if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && 1.4723 + ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) || 1.4724 + (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) { 1.4725 + try { 1.4726 + return Services.io.newURI(aElement.href, null, null); 1.4727 + } catch (e) {} 1.4728 + } 1.4729 + return null; 1.4730 + }, 1.4731 + 1.4732 + observe: function(aSubject, aTopic, aData) { 1.4733 + if (aTopic == "dom-touch-listener-added") { 1.4734 + let tab = BrowserApp.getTabForWindow(aSubject.top); 1.4735 + if (!tab || tab.hasTouchListener) 1.4736 + return; 1.4737 + 1.4738 + tab.hasTouchListener = true; 1.4739 + sendMessageToJava({ 1.4740 + type: "Tab:HasTouchListener", 1.4741 + tabID: tab.id 1.4742 + }); 1.4743 + return; 1.4744 + } else if (aTopic == "nsPref:changed") { 1.4745 + if (aData == "browser.zoom.reflowOnZoom") { 1.4746 + this.updateReflozPref(); 1.4747 + } 1.4748 + return; 1.4749 + } 1.4750 + 1.4751 + // the remaining events are all dependent on the browser content document being the 1.4752 + // same as the browser displayed document. if they are not the same, we should ignore 1.4753 + // the event. 1.4754 + if (BrowserApp.isBrowserContentDocumentDisplayed()) { 1.4755 + this.handleUserEvent(aTopic, aData); 1.4756 + } 1.4757 + }, 1.4758 + 1.4759 + handleUserEvent: function(aTopic, aData) { 1.4760 + switch (aTopic) { 1.4761 + 1.4762 + case "Gesture:Scroll": { 1.4763 + // If we've lost our scrollable element, return. Don't cancel the 1.4764 + // override, as we probably don't want Java to handle panning until the 1.4765 + // user releases their finger. 1.4766 + if (this._scrollableElement == null) 1.4767 + return; 1.4768 + 1.4769 + // If this is the first scroll event and we can't scroll in the direction 1.4770 + // the user wanted, and neither can any non-root sub-frame, cancel the 1.4771 + // override so that Java can handle panning the main document. 1.4772 + let data = JSON.parse(aData); 1.4773 + 1.4774 + // round the scroll amounts because they come in as floats and might be 1.4775 + // subject to minor rounding errors because of zoom values. I've seen values 1.4776 + // like 0.99 come in here and get truncated to 0; this avoids that problem. 1.4777 + let zoom = BrowserApp.selectedTab._zoom; 1.4778 + let x = Math.round(data.x / zoom); 1.4779 + let y = Math.round(data.y / zoom); 1.4780 + 1.4781 + if (this._firstScrollEvent) { 1.4782 + while (this._scrollableElement != null && 1.4783 + !this._elementCanScroll(this._scrollableElement, x, y)) 1.4784 + this._scrollableElement = this._findScrollableElement(this._scrollableElement, false); 1.4785 + 1.4786 + let doc = BrowserApp.selectedBrowser.contentDocument; 1.4787 + if (this._scrollableElement == null || 1.4788 + this._scrollableElement == doc.documentElement) { 1.4789 + sendMessageToJava({ type: "Panning:CancelOverride" }); 1.4790 + return; 1.4791 + } 1.4792 + 1.4793 + this._firstScrollEvent = false; 1.4794 + } 1.4795 + 1.4796 + // Scroll the scrollable element 1.4797 + if (this._elementCanScroll(this._scrollableElement, x, y)) { 1.4798 + this._scrollElementBy(this._scrollableElement, x, y); 1.4799 + sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: true }); 1.4800 + SelectionHandler.subdocumentScrolled(this._scrollableElement); 1.4801 + } else { 1.4802 + sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: false }); 1.4803 + } 1.4804 + 1.4805 + break; 1.4806 + } 1.4807 + 1.4808 + case "Gesture:CancelTouch": 1.4809 + this._cancelTapHighlight(); 1.4810 + break; 1.4811 + 1.4812 + case "Gesture:SingleTap": { 1.4813 + let element = this._highlightElement; 1.4814 + if (element) { 1.4815 + try { 1.4816 + let data = JSON.parse(aData); 1.4817 + let [x, y] = [data.x, data.y]; 1.4818 + if (ElementTouchHelper.isElementClickable(element)) { 1.4819 + [x, y] = this._moveClickPoint(element, x, y); 1.4820 + } 1.4821 + 1.4822 + // Was the element already focused before it was clicked? 1.4823 + let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser)); 1.4824 + 1.4825 + this._sendMouseEvent("mousemove", element, x, y); 1.4826 + this._sendMouseEvent("mousedown", element, x, y); 1.4827 + this._sendMouseEvent("mouseup", element, x, y); 1.4828 + 1.4829 + // If the element was previously focused, show the caret attached to it. 1.4830 + if (isFocused) 1.4831 + SelectionHandler.attachCaret(element); 1.4832 + 1.4833 + // scrollToFocusedInput does its own checks to find out if an element should be zoomed into 1.4834 + BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser); 1.4835 + } catch(e) { 1.4836 + Cu.reportError(e); 1.4837 + } 1.4838 + } 1.4839 + this._cancelTapHighlight(); 1.4840 + break; 1.4841 + } 1.4842 + 1.4843 + case"Gesture:DoubleTap": 1.4844 + this._cancelTapHighlight(); 1.4845 + this.onDoubleTap(aData); 1.4846 + break; 1.4847 + 1.4848 + case "MozMagnifyGesture": 1.4849 + this.onPinchFinish(aData); 1.4850 + break; 1.4851 + 1.4852 + default: 1.4853 + dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"'); 1.4854 + break; 1.4855 + } 1.4856 + }, 1.4857 + 1.4858 + onDoubleTap: function(aData) { 1.4859 + let data = JSON.parse(aData); 1.4860 + let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y); 1.4861 + 1.4862 + // We only want to do this if reflow-on-zoom is enabled, we don't already 1.4863 + // have a reflow-on-zoom event pending, and the element upon which the user 1.4864 + // double-tapped isn't of a type we want to avoid reflow-on-zoom. 1.4865 + if (BrowserEventHandler.mReflozPref && 1.4866 + !BrowserApp.selectedTab._mReflozPoint && 1.4867 + !this._shouldSuppressReflowOnZoom(element)) { 1.4868 + 1.4869 + // See comment above performReflowOnZoom() for a detailed description of 1.4870 + // the events happening in the reflow-on-zoom operation. 1.4871 + let data = JSON.parse(aData); 1.4872 + let zoomPointX = data.x; 1.4873 + let zoomPointY = data.y; 1.4874 + 1.4875 + BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY, 1.4876 + range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) }; 1.4877 + 1.4878 + // Before we perform a reflow on zoom, let's disable painting. 1.4879 + let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); 1.4880 + let docShell = webNav.QueryInterface(Ci.nsIDocShell); 1.4881 + let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); 1.4882 + docViewer.pausePainting(); 1.4883 + 1.4884 + BrowserApp.selectedTab.probablyNeedRefloz = true; 1.4885 + } 1.4886 + 1.4887 + if (!element) { 1.4888 + ZoomHelper.zoomOut(); 1.4889 + return; 1.4890 + } 1.4891 + 1.4892 + while (element && !this._shouldZoomToElement(element)) 1.4893 + element = element.parentNode; 1.4894 + 1.4895 + if (!element) { 1.4896 + ZoomHelper.zoomOut(); 1.4897 + } else { 1.4898 + ZoomHelper.zoomToElement(element, data.y); 1.4899 + } 1.4900 + }, 1.4901 + 1.4902 + /** 1.4903 + * Determine if reflow-on-zoom functionality should be suppressed, given a 1.4904 + * particular element. Double-tapping on the following elements suppresses 1.4905 + * reflow-on-zoom: 1.4906 + * 1.4907 + * <video>, <object>, <embed>, <applet>, <canvas>, <img>, <media>, <pre> 1.4908 + */ 1.4909 + _shouldSuppressReflowOnZoom: function(aElement) { 1.4910 + if (aElement instanceof Ci.nsIDOMHTMLVideoElement || 1.4911 + aElement instanceof Ci.nsIDOMHTMLObjectElement || 1.4912 + aElement instanceof Ci.nsIDOMHTMLEmbedElement || 1.4913 + aElement instanceof Ci.nsIDOMHTMLAppletElement || 1.4914 + aElement instanceof Ci.nsIDOMHTMLCanvasElement || 1.4915 + aElement instanceof Ci.nsIDOMHTMLImageElement || 1.4916 + aElement instanceof Ci.nsIDOMHTMLMediaElement || 1.4917 + aElement instanceof Ci.nsIDOMHTMLPreElement) { 1.4918 + return true; 1.4919 + } 1.4920 + 1.4921 + return false; 1.4922 + }, 1.4923 + 1.4924 + onPinchFinish: function(aData) { 1.4925 + let data = {}; 1.4926 + try { 1.4927 + data = JSON.parse(aData); 1.4928 + } catch(ex) { 1.4929 + console.log(ex); 1.4930 + return; 1.4931 + } 1.4932 + 1.4933 + if (BrowserEventHandler.mReflozPref && 1.4934 + data.zoomDelta < 0.0) { 1.4935 + BrowserEventHandler.resetMaxLineBoxWidth(); 1.4936 + } 1.4937 + }, 1.4938 + 1.4939 + _shouldZoomToElement: function(aElement) { 1.4940 + let win = aElement.ownerDocument.defaultView; 1.4941 + if (win.getComputedStyle(aElement, null).display == "inline") 1.4942 + return false; 1.4943 + if (aElement instanceof Ci.nsIDOMHTMLLIElement) 1.4944 + return false; 1.4945 + if (aElement instanceof Ci.nsIDOMHTMLQuoteElement) 1.4946 + return false; 1.4947 + return true; 1.4948 + }, 1.4949 + 1.4950 + _firstScrollEvent: false, 1.4951 + 1.4952 + _scrollableElement: null, 1.4953 + 1.4954 + _highlightElement: null, 1.4955 + 1.4956 + _doTapHighlight: function _doTapHighlight(aElement) { 1.4957 + DOMUtils.setContentState(aElement, kStateActive); 1.4958 + this._highlightElement = aElement; 1.4959 + }, 1.4960 + 1.4961 + _cancelTapHighlight: function _cancelTapHighlight() { 1.4962 + if (!this._highlightElement) 1.4963 + return; 1.4964 + 1.4965 + // If the active element is in a sub-frame, we need to make that frame's document 1.4966 + // active to remove the element's active state. 1.4967 + if (this._highlightElement.ownerDocument != BrowserApp.selectedBrowser.contentWindow.document) 1.4968 + DOMUtils.setContentState(this._highlightElement.ownerDocument.documentElement, kStateActive); 1.4969 + 1.4970 + DOMUtils.setContentState(BrowserApp.selectedBrowser.contentWindow.document.documentElement, kStateActive); 1.4971 + this._highlightElement = null; 1.4972 + }, 1.4973 + 1.4974 + _updateLastPosition: function(x, y, dx, dy) { 1.4975 + this.lastX = x; 1.4976 + this.lastY = y; 1.4977 + this.lastTime = Date.now(); 1.4978 + 1.4979 + this.motionBuffer.push({ dx: dx, dy: dy, time: this.lastTime }); 1.4980 + }, 1.4981 + 1.4982 + _moveClickPoint: function(aElement, aX, aY) { 1.4983 + // the element can be out of the aX/aY point because of the touch radius 1.4984 + // if outside, we gracefully move the touch point to the edge of the element 1.4985 + if (!(aElement instanceof HTMLHtmlElement)) { 1.4986 + let isTouchClick = true; 1.4987 + let rects = ElementTouchHelper.getContentClientRects(aElement); 1.4988 + for (let i = 0; i < rects.length; i++) { 1.4989 + let rect = rects[i]; 1.4990 + let inBounds = 1.4991 + (aX > rect.left && aX < (rect.left + rect.width)) && 1.4992 + (aY > rect.top && aY < (rect.top + rect.height)); 1.4993 + if (inBounds) { 1.4994 + isTouchClick = false; 1.4995 + break; 1.4996 + } 1.4997 + } 1.4998 + 1.4999 + if (isTouchClick) { 1.5000 + let rect = rects[0]; 1.5001 + // if either width or height is zero, we don't want to move the click to the edge of the element. See bug 757208 1.5002 + if (rect.width != 0 && rect.height != 0) { 1.5003 + aX = Math.min(Math.ceil(rect.left + rect.width) - 1, Math.max(Math.ceil(rect.left), aX)); 1.5004 + aY = Math.min(Math.ceil(rect.top + rect.height) - 1, Math.max(Math.ceil(rect.top), aY)); 1.5005 + } 1.5006 + } 1.5007 + } 1.5008 + return [aX, aY]; 1.5009 + }, 1.5010 + 1.5011 + _sendMouseEvent: function _sendMouseEvent(aName, aElement, aX, aY) { 1.5012 + let window = aElement.ownerDocument.defaultView; 1.5013 + try { 1.5014 + let cwu = window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.5015 + cwu.sendMouseEventToWindow(aName, aX, aY, 0, 1, 0, true); 1.5016 + } catch(e) { 1.5017 + Cu.reportError(e); 1.5018 + } 1.5019 + }, 1.5020 + 1.5021 + _hasScrollableOverflow: function(elem) { 1.5022 + var win = elem.ownerDocument.defaultView; 1.5023 + if (!win) 1.5024 + return false; 1.5025 + var computedStyle = win.getComputedStyle(elem); 1.5026 + if (!computedStyle) 1.5027 + return false; 1.5028 + // We check for overflow:hidden only because all the other cases are scrollable 1.5029 + // under various conditions. See https://bugzilla.mozilla.org/show_bug.cgi?id=911574#c24 1.5030 + // for some more details. 1.5031 + return !(computedStyle.overflowX == 'hidden' && computedStyle.overflowY == 'hidden'); 1.5032 + }, 1.5033 + 1.5034 + _findScrollableElement: function(elem, checkElem) { 1.5035 + // Walk the DOM tree until we find a scrollable element 1.5036 + let scrollable = false; 1.5037 + while (elem) { 1.5038 + /* Element is scrollable if its scroll-size exceeds its client size, and: 1.5039 + * - It has overflow other than 'hidden', or 1.5040 + * - It's a textarea node, or 1.5041 + * - It's a text input, or 1.5042 + * - It's a select element showing multiple rows 1.5043 + */ 1.5044 + if (checkElem) { 1.5045 + if ((elem.scrollTopMax > 0 || elem.scrollLeftMax > 0) && 1.5046 + (this._hasScrollableOverflow(elem) || 1.5047 + elem.mozMatchesSelector("textarea")) || 1.5048 + (elem instanceof HTMLInputElement && elem.mozIsTextField(false)) || 1.5049 + (elem instanceof HTMLSelectElement && (elem.size > 1 || elem.multiple))) { 1.5050 + scrollable = true; 1.5051 + break; 1.5052 + } 1.5053 + } else { 1.5054 + checkElem = true; 1.5055 + } 1.5056 + 1.5057 + // Propagate up iFrames 1.5058 + if (!elem.parentNode && elem.documentElement && elem.documentElement.ownerDocument) 1.5059 + elem = elem.documentElement.ownerDocument.defaultView.frameElement; 1.5060 + else 1.5061 + elem = elem.parentNode; 1.5062 + } 1.5063 + 1.5064 + if (!scrollable) 1.5065 + return null; 1.5066 + 1.5067 + return elem; 1.5068 + }, 1.5069 + 1.5070 + _scrollElementBy: function(elem, x, y) { 1.5071 + elem.scrollTop = elem.scrollTop + y; 1.5072 + elem.scrollLeft = elem.scrollLeft + x; 1.5073 + }, 1.5074 + 1.5075 + _elementCanScroll: function(elem, x, y) { 1.5076 + let scrollX = (x < 0 && elem.scrollLeft > 0) 1.5077 + || (x > 0 && elem.scrollLeft < elem.scrollLeftMax); 1.5078 + 1.5079 + let scrollY = (y < 0 && elem.scrollTop > 0) 1.5080 + || (y > 0 && elem.scrollTop < elem.scrollTopMax); 1.5081 + 1.5082 + return scrollX || scrollY; 1.5083 + } 1.5084 +}; 1.5085 + 1.5086 +const kReferenceDpi = 240; // standard "pixel" size used in some preferences 1.5087 + 1.5088 +const ElementTouchHelper = { 1.5089 + /* Return the element at the given coordinates, starting from the given window and 1.5090 + drilling down through frames. If no window is provided, the top-level window of 1.5091 + the currently selected tab is used. The coordinates provided should be CSS pixels 1.5092 + relative to the window's scroll position. */ 1.5093 + anyElementFromPoint: function(aX, aY, aWindow) { 1.5094 + let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow); 1.5095 + let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.5096 + let elem = cwu.elementFromPoint(aX, aY, true, true); 1.5097 + 1.5098 + while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) { 1.5099 + let rect = elem.getBoundingClientRect(); 1.5100 + aX -= rect.left; 1.5101 + aY -= rect.top; 1.5102 + cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.5103 + elem = cwu.elementFromPoint(aX, aY, true, true); 1.5104 + } 1.5105 + 1.5106 + return elem; 1.5107 + }, 1.5108 + 1.5109 + /* Return the most appropriate clickable element (if any), starting from the given window 1.5110 + and drilling down through iframes as necessary. If no window is provided, the top-level 1.5111 + window of the currently selected tab is used. The coordinates provided should be CSS 1.5112 + pixels relative to the window's scroll position. The element returned may not actually 1.5113 + contain the coordinates passed in because of touch radius and clickability heuristics. */ 1.5114 + elementFromPoint: function(aX, aY, aWindow) { 1.5115 + // browser's elementFromPoint expect browser-relative client coordinates. 1.5116 + // subtract browser's scroll values to adjust 1.5117 + let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow); 1.5118 + let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.5119 + let elem = this.getClosest(cwu, aX, aY); 1.5120 + 1.5121 + // step through layers of IFRAMEs and FRAMES to find innermost element 1.5122 + while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) { 1.5123 + // adjust client coordinates' origin to be top left of iframe viewport 1.5124 + let rect = elem.getBoundingClientRect(); 1.5125 + aX -= rect.left; 1.5126 + aY -= rect.top; 1.5127 + cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.5128 + elem = this.getClosest(cwu, aX, aY); 1.5129 + } 1.5130 + 1.5131 + return elem; 1.5132 + }, 1.5133 + 1.5134 + /* Returns the touch radius in content px. */ 1.5135 + getTouchRadius: function getTouchRadius() { 1.5136 + let dpiRatio = ViewportHandler.displayDPI / kReferenceDpi; 1.5137 + let zoom = BrowserApp.selectedTab._zoom; 1.5138 + return { 1.5139 + top: this.radius.top * dpiRatio / zoom, 1.5140 + right: this.radius.right * dpiRatio / zoom, 1.5141 + bottom: this.radius.bottom * dpiRatio / zoom, 1.5142 + left: this.radius.left * dpiRatio / zoom 1.5143 + }; 1.5144 + }, 1.5145 + 1.5146 + /* Returns the touch radius in reference pixels. */ 1.5147 + get radius() { 1.5148 + let prefs = Services.prefs; 1.5149 + delete this.radius; 1.5150 + return this.radius = { "top": prefs.getIntPref("browser.ui.touch.top"), 1.5151 + "right": prefs.getIntPref("browser.ui.touch.right"), 1.5152 + "bottom": prefs.getIntPref("browser.ui.touch.bottom"), 1.5153 + "left": prefs.getIntPref("browser.ui.touch.left") 1.5154 + }; 1.5155 + }, 1.5156 + 1.5157 + get weight() { 1.5158 + delete this.weight; 1.5159 + return this.weight = { "visited": Services.prefs.getIntPref("browser.ui.touch.weight.visited") }; 1.5160 + }, 1.5161 + 1.5162 + /* Retrieve the closest element to a point by looking at borders position */ 1.5163 + getClosest: function getClosest(aWindowUtils, aX, aY) { 1.5164 + let target = aWindowUtils.elementFromPoint(aX, aY, 1.5165 + true, /* ignore root scroll frame*/ 1.5166 + false); /* don't flush layout */ 1.5167 + 1.5168 + // if this element is clickable we return quickly. also, if it isn't, 1.5169 + // use a cache to speed up future calls to isElementClickable in the 1.5170 + // loop below. 1.5171 + let unclickableCache = new Array(); 1.5172 + if (this.isElementClickable(target, unclickableCache, false)) 1.5173 + return target; 1.5174 + 1.5175 + target = null; 1.5176 + let radius = this.getTouchRadius(); 1.5177 + let nodes = aWindowUtils.nodesFromRect(aX, aY, radius.top, radius.right, radius.bottom, radius.left, true, false); 1.5178 + 1.5179 + let threshold = Number.POSITIVE_INFINITY; 1.5180 + for (let i = 0; i < nodes.length; i++) { 1.5181 + let current = nodes[i]; 1.5182 + if (!current.mozMatchesSelector || !this.isElementClickable(current, unclickableCache, true)) 1.5183 + continue; 1.5184 + 1.5185 + let rect = current.getBoundingClientRect(); 1.5186 + let distance = this._computeDistanceFromRect(aX, aY, rect); 1.5187 + 1.5188 + // increase a little bit the weight for already visited items 1.5189 + if (current && current.mozMatchesSelector("*:visited")) 1.5190 + distance *= (this.weight.visited / 100); 1.5191 + 1.5192 + if (distance < threshold) { 1.5193 + target = current; 1.5194 + threshold = distance; 1.5195 + } 1.5196 + } 1.5197 + 1.5198 + return target; 1.5199 + }, 1.5200 + 1.5201 + isElementClickable: function isElementClickable(aElement, aUnclickableCache, aAllowBodyListeners) { 1.5202 + const selector = "a,:link,:visited,[role=button],button,input,select,textarea"; 1.5203 + 1.5204 + let stopNode = null; 1.5205 + if (!aAllowBodyListeners && aElement && aElement.ownerDocument) 1.5206 + stopNode = aElement.ownerDocument.body; 1.5207 + 1.5208 + for (let elem = aElement; elem && elem != stopNode; elem = elem.parentNode) { 1.5209 + if (aUnclickableCache && aUnclickableCache.indexOf(elem) != -1) 1.5210 + continue; 1.5211 + if (this._hasMouseListener(elem)) 1.5212 + return true; 1.5213 + if (elem.mozMatchesSelector && elem.mozMatchesSelector(selector)) 1.5214 + return true; 1.5215 + if (elem instanceof HTMLLabelElement && elem.control != null) 1.5216 + return true; 1.5217 + if (aUnclickableCache) 1.5218 + aUnclickableCache.push(elem); 1.5219 + } 1.5220 + return false; 1.5221 + }, 1.5222 + 1.5223 + _computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) { 1.5224 + let x = 0, y = 0; 1.5225 + let xmost = aRect.left + aRect.width; 1.5226 + let ymost = aRect.top + aRect.height; 1.5227 + 1.5228 + // compute horizontal distance from left/right border depending if X is 1.5229 + // before/inside/after the element's rectangle 1.5230 + if (aRect.left < aX && aX < xmost) 1.5231 + x = Math.min(xmost - aX, aX - aRect.left); 1.5232 + else if (aX < aRect.left) 1.5233 + x = aRect.left - aX; 1.5234 + else if (aX > xmost) 1.5235 + x = aX - xmost; 1.5236 + 1.5237 + // compute vertical distance from top/bottom border depending if Y is 1.5238 + // above/inside/below the element's rectangle 1.5239 + if (aRect.top < aY && aY < ymost) 1.5240 + y = Math.min(ymost - aY, aY - aRect.top); 1.5241 + else if (aY < aRect.top) 1.5242 + y = aRect.top - aY; 1.5243 + if (aY > ymost) 1.5244 + y = aY - ymost; 1.5245 + 1.5246 + return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); 1.5247 + }, 1.5248 + 1.5249 + _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService), 1.5250 + _clickableEvents: ["mousedown", "mouseup", "click"], 1.5251 + _hasMouseListener: function _hasMouseListener(aElement) { 1.5252 + let els = this._els; 1.5253 + let listeners = els.getListenerInfoFor(aElement, {}); 1.5254 + for (let i = 0; i < listeners.length; i++) { 1.5255 + if (this._clickableEvents.indexOf(listeners[i].type) != -1) 1.5256 + return true; 1.5257 + } 1.5258 + return false; 1.5259 + }, 1.5260 + 1.5261 + getContentClientRects: function(aElement) { 1.5262 + let offset = { x: 0, y: 0 }; 1.5263 + 1.5264 + let nativeRects = aElement.getClientRects(); 1.5265 + // step out of iframes and frames, offsetting scroll values 1.5266 + for (let frame = aElement.ownerDocument.defaultView; frame.frameElement; frame = frame.parent) { 1.5267 + // adjust client coordinates' origin to be top left of iframe viewport 1.5268 + let rect = frame.frameElement.getBoundingClientRect(); 1.5269 + let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; 1.5270 + let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; 1.5271 + offset.x += rect.left + parseInt(left); 1.5272 + offset.y += rect.top + parseInt(top); 1.5273 + } 1.5274 + 1.5275 + let result = []; 1.5276 + for (let i = nativeRects.length - 1; i >= 0; i--) { 1.5277 + let r = nativeRects[i]; 1.5278 + result.push({ left: r.left + offset.x, 1.5279 + top: r.top + offset.y, 1.5280 + width: r.width, 1.5281 + height: r.height 1.5282 + }); 1.5283 + } 1.5284 + return result; 1.5285 + }, 1.5286 + 1.5287 + getBoundingContentRect: function(aElement) { 1.5288 + if (!aElement) 1.5289 + return {x: 0, y: 0, w: 0, h: 0}; 1.5290 + 1.5291 + let document = aElement.ownerDocument; 1.5292 + while (document.defaultView.frameElement) 1.5293 + document = document.defaultView.frameElement.ownerDocument; 1.5294 + 1.5295 + let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.5296 + let scrollX = {}, scrollY = {}; 1.5297 + cwu.getScrollXY(false, scrollX, scrollY); 1.5298 + 1.5299 + let r = aElement.getBoundingClientRect(); 1.5300 + 1.5301 + // step out of iframes and frames, offsetting scroll values 1.5302 + for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) { 1.5303 + // adjust client coordinates' origin to be top left of iframe viewport 1.5304 + let rect = frame.frameElement.getBoundingClientRect(); 1.5305 + let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth; 1.5306 + let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth; 1.5307 + scrollX.value += rect.left + parseInt(left); 1.5308 + scrollY.value += rect.top + parseInt(top); 1.5309 + } 1.5310 + 1.5311 + return {x: r.left + scrollX.value, 1.5312 + y: r.top + scrollY.value, 1.5313 + w: r.width, 1.5314 + h: r.height }; 1.5315 + } 1.5316 +}; 1.5317 + 1.5318 +var ErrorPageEventHandler = { 1.5319 + handleEvent: function(aEvent) { 1.5320 + switch (aEvent.type) { 1.5321 + case "click": { 1.5322 + // Don't trust synthetic events 1.5323 + if (!aEvent.isTrusted) 1.5324 + return; 1.5325 + 1.5326 + let target = aEvent.originalTarget; 1.5327 + let errorDoc = target.ownerDocument; 1.5328 + 1.5329 + // If the event came from an ssl error page, it is probably either the "Add 1.5330 + // Exception…" or "Get me out of here!" button 1.5331 + if (errorDoc.documentURI.startsWith("about:certerror?e=nssBadCert")) { 1.5332 + let perm = errorDoc.getElementById("permanentExceptionButton"); 1.5333 + let temp = errorDoc.getElementById("temporaryExceptionButton"); 1.5334 + if (target == temp || target == perm) { 1.5335 + // Handle setting an cert exception and reloading the page 1.5336 + try { 1.5337 + // Add a new SSL exception for this URL 1.5338 + let uri = Services.io.newURI(errorDoc.location.href, null, null); 1.5339 + let sslExceptions = new SSLExceptions(); 1.5340 + 1.5341 + if (target == perm) 1.5342 + sslExceptions.addPermanentException(uri, errorDoc.defaultView); 1.5343 + else 1.5344 + sslExceptions.addTemporaryException(uri, errorDoc.defaultView); 1.5345 + } catch (e) { 1.5346 + dump("Failed to set cert exception: " + e + "\n"); 1.5347 + } 1.5348 + errorDoc.location.reload(); 1.5349 + } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) { 1.5350 + errorDoc.location = "about:home"; 1.5351 + } 1.5352 + } else if (errorDoc.documentURI.startsWith("about:blocked")) { 1.5353 + // The event came from a button on a malware/phishing block page 1.5354 + // First check whether it's malware or phishing, so that we can 1.5355 + // use the right strings/links 1.5356 + let isMalware = errorDoc.documentURI.contains("e=malwareBlocked"); 1.5357 + let bucketName = isMalware ? "WARNING_MALWARE_PAGE_" : "WARNING_PHISHING_PAGE_"; 1.5358 + let nsISecTel = Ci.nsISecurityUITelemetry; 1.5359 + let isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView); 1.5360 + bucketName += isIframe ? "TOP_" : "FRAME_"; 1.5361 + 1.5362 + let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter); 1.5363 + 1.5364 + if (target == errorDoc.getElementById("getMeOutButton")) { 1.5365 + Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]); 1.5366 + errorDoc.location = "about:home"; 1.5367 + } else if (target == errorDoc.getElementById("reportButton")) { 1.5368 + // We log even if malware/phishing info URL couldn't be found: 1.5369 + // the measurement is for how many users clicked the WHY BLOCKED button 1.5370 + Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "WHY_BLOCKED"]); 1.5371 + 1.5372 + // This is the "Why is this site blocked" button. For malware, 1.5373 + // we can fetch a site-specific report, for phishing, we redirect 1.5374 + // to the generic page describing phishing protection. 1.5375 + if (isMalware) { 1.5376 + // Get the stop badware "why is this blocked" report url, append the current url, and go there. 1.5377 + try { 1.5378 + let reportURL = formatter.formatURLPref("browser.safebrowsing.malware.reportURL"); 1.5379 + reportURL += errorDoc.location.href; 1.5380 + BrowserApp.selectedBrowser.loadURI(reportURL); 1.5381 + } catch (e) { 1.5382 + Cu.reportError("Couldn't get malware report URL: " + e); 1.5383 + } 1.5384 + } else { 1.5385 + // It's a phishing site, just link to the generic information page 1.5386 + let url = Services.urlFormatter.formatURLPref("app.support.baseURL"); 1.5387 + BrowserApp.selectedBrowser.loadURI(url + "phishing-malware"); 1.5388 + } 1.5389 + } else if (target == errorDoc.getElementById("ignoreWarningButton")) { 1.5390 + Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "IGNORE_WARNING"]); 1.5391 + 1.5392 + // Allow users to override and continue through to the site, 1.5393 + let webNav = BrowserApp.selectedBrowser.docShell.QueryInterface(Ci.nsIWebNavigation); 1.5394 + let location = BrowserApp.selectedBrowser.contentWindow.location; 1.5395 + webNav.loadURI(location, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, null, null, null); 1.5396 + 1.5397 + // ....but add a notify bar as a reminder, so that they don't lose 1.5398 + // track after, e.g., tab switching. 1.5399 + NativeWindow.doorhanger.show(Strings.browser.GetStringFromName("safeBrowsingDoorhanger"), "safebrowsing-warning", [], BrowserApp.selectedTab.id); 1.5400 + } 1.5401 + } 1.5402 + break; 1.5403 + } 1.5404 + } 1.5405 + } 1.5406 +}; 1.5407 + 1.5408 +var FormAssistant = { 1.5409 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]), 1.5410 + 1.5411 + // Used to keep track of the element that corresponds to the current 1.5412 + // autocomplete suggestions 1.5413 + _currentInputElement: null, 1.5414 + 1.5415 + _isBlocklisted: false, 1.5416 + 1.5417 + // Keep track of whether or not an invalid form has been submitted 1.5418 + _invalidSubmit: false, 1.5419 + 1.5420 + init: function() { 1.5421 + Services.obs.addObserver(this, "FormAssist:AutoComplete", false); 1.5422 + Services.obs.addObserver(this, "FormAssist:Blocklisted", false); 1.5423 + Services.obs.addObserver(this, "FormAssist:Hidden", false); 1.5424 + Services.obs.addObserver(this, "invalidformsubmit", false); 1.5425 + Services.obs.addObserver(this, "PanZoom:StateChange", false); 1.5426 + 1.5427 + // We need to use a capturing listener for focus events 1.5428 + BrowserApp.deck.addEventListener("focus", this, true); 1.5429 + BrowserApp.deck.addEventListener("click", this, true); 1.5430 + BrowserApp.deck.addEventListener("input", this, false); 1.5431 + BrowserApp.deck.addEventListener("pageshow", this, false); 1.5432 + }, 1.5433 + 1.5434 + uninit: function() { 1.5435 + Services.obs.removeObserver(this, "FormAssist:AutoComplete"); 1.5436 + Services.obs.removeObserver(this, "FormAssist:Blocklisted"); 1.5437 + Services.obs.removeObserver(this, "FormAssist:Hidden"); 1.5438 + Services.obs.removeObserver(this, "invalidformsubmit"); 1.5439 + Services.obs.removeObserver(this, "PanZoom:StateChange"); 1.5440 + 1.5441 + BrowserApp.deck.removeEventListener("focus", this); 1.5442 + BrowserApp.deck.removeEventListener("click", this); 1.5443 + BrowserApp.deck.removeEventListener("input", this); 1.5444 + BrowserApp.deck.removeEventListener("pageshow", this); 1.5445 + }, 1.5446 + 1.5447 + observe: function(aSubject, aTopic, aData) { 1.5448 + switch (aTopic) { 1.5449 + case "PanZoom:StateChange": 1.5450 + // If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing 1.5451 + if (aData == "TOUCHING" || aData == "WAITING_LISTENERS") 1.5452 + break; 1.5453 + if (aData == "NOTHING") { 1.5454 + // only look for input elements, not contentEditable or multiline text areas 1.5455 + let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true); 1.5456 + if (!focused) 1.5457 + break; 1.5458 + 1.5459 + if (this._showValidationMessage(focused)) 1.5460 + break; 1.5461 + this._showAutoCompleteSuggestions(focused, function () {}); 1.5462 + } else { 1.5463 + // temporarily hide the form assist popup while we're panning or zooming the page 1.5464 + this._hideFormAssistPopup(); 1.5465 + } 1.5466 + break; 1.5467 + case "FormAssist:AutoComplete": 1.5468 + if (!this._currentInputElement) 1.5469 + break; 1.5470 + 1.5471 + let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement); 1.5472 + 1.5473 + // If we have an active composition string, commit it before sending 1.5474 + // the autocomplete event with the text that will replace it. 1.5475 + try { 1.5476 + let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport); 1.5477 + if (imeEditor.composing) 1.5478 + imeEditor.forceCompositionEnd(); 1.5479 + } catch (e) {} 1.5480 + 1.5481 + editableElement.setUserInput(aData); 1.5482 + 1.5483 + let event = this._currentInputElement.ownerDocument.createEvent("Events"); 1.5484 + event.initEvent("DOMAutoComplete", true, true); 1.5485 + this._currentInputElement.dispatchEvent(event); 1.5486 + break; 1.5487 + 1.5488 + case "FormAssist:Blocklisted": 1.5489 + this._isBlocklisted = (aData == "true"); 1.5490 + break; 1.5491 + 1.5492 + case "FormAssist:Hidden": 1.5493 + this._currentInputElement = null; 1.5494 + break; 1.5495 + } 1.5496 + }, 1.5497 + 1.5498 + notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) { 1.5499 + if (!aInvalidElements.length) 1.5500 + return; 1.5501 + 1.5502 + // Ignore this notificaiton if the current tab doesn't contain the invalid form 1.5503 + if (BrowserApp.selectedBrowser.contentDocument != 1.5504 + aFormElement.ownerDocument.defaultView.top.document) 1.5505 + return; 1.5506 + 1.5507 + this._invalidSubmit = true; 1.5508 + 1.5509 + // Our focus listener will show the element's validation message 1.5510 + let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports); 1.5511 + currentElement.focus(); 1.5512 + }, 1.5513 + 1.5514 + handleEvent: function(aEvent) { 1.5515 + switch (aEvent.type) { 1.5516 + case "focus": 1.5517 + let currentElement = aEvent.target; 1.5518 + 1.5519 + // Only show a validation message on focus. 1.5520 + this._showValidationMessage(currentElement); 1.5521 + break; 1.5522 + 1.5523 + case "click": 1.5524 + currentElement = aEvent.target; 1.5525 + 1.5526 + // Prioritize a form validation message over autocomplete suggestions 1.5527 + // when the element is first focused (a form validation message will 1.5528 + // only be available if an invalid form was submitted) 1.5529 + if (this._showValidationMessage(currentElement)) 1.5530 + break; 1.5531 + 1.5532 + let checkResultsClick = hasResults => { 1.5533 + if (!hasResults) { 1.5534 + this._hideFormAssistPopup(); 1.5535 + } 1.5536 + }; 1.5537 + 1.5538 + this._showAutoCompleteSuggestions(currentElement, checkResultsClick); 1.5539 + break; 1.5540 + 1.5541 + case "input": 1.5542 + currentElement = aEvent.target; 1.5543 + 1.5544 + // Since we can only show one popup at a time, prioritze autocomplete 1.5545 + // suggestions over a form validation message 1.5546 + let checkResultsInput = hasResults => { 1.5547 + if (hasResults) 1.5548 + return; 1.5549 + 1.5550 + if (this._showValidationMessage(currentElement)) 1.5551 + return; 1.5552 + 1.5553 + // If we're not showing autocomplete suggestions, hide the form assist popup 1.5554 + this._hideFormAssistPopup(); 1.5555 + }; 1.5556 + 1.5557 + this._showAutoCompleteSuggestions(currentElement, checkResultsInput); 1.5558 + break; 1.5559 + 1.5560 + // Reset invalid submit state on each pageshow 1.5561 + case "pageshow": 1.5562 + if (!this._invalidSubmit) 1.5563 + return; 1.5564 + 1.5565 + let selectedBrowser = BrowserApp.selectedBrowser; 1.5566 + if (selectedBrowser) { 1.5567 + let selectedDocument = selectedBrowser.contentDocument; 1.5568 + let target = aEvent.originalTarget; 1.5569 + if (target == selectedDocument || target.ownerDocument == selectedDocument) 1.5570 + this._invalidSubmit = false; 1.5571 + } 1.5572 + } 1.5573 + }, 1.5574 + 1.5575 + // We only want to show autocomplete suggestions for certain elements 1.5576 + _isAutoComplete: function _isAutoComplete(aElement) { 1.5577 + if (!(aElement instanceof HTMLInputElement) || aElement.readOnly || 1.5578 + (aElement.getAttribute("type") == "password") || 1.5579 + (aElement.hasAttribute("autocomplete") && 1.5580 + aElement.getAttribute("autocomplete").toLowerCase() == "off")) 1.5581 + return false; 1.5582 + 1.5583 + return true; 1.5584 + }, 1.5585 + 1.5586 + // Retrieves autocomplete suggestions for an element from the form autocomplete service. 1.5587 + // aCallback(array_of_suggestions) is called when results are available. 1.5588 + _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) { 1.5589 + // Cache the form autocomplete service for future use 1.5590 + if (!this._formAutoCompleteService) 1.5591 + this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"]. 1.5592 + getService(Ci.nsIFormAutoComplete); 1.5593 + 1.5594 + let resultsAvailable = function (results) { 1.5595 + let suggestions = []; 1.5596 + for (let i = 0; i < results.matchCount; i++) { 1.5597 + let value = results.getValueAt(i); 1.5598 + 1.5599 + // Do not show the value if it is the current one in the input field 1.5600 + if (value == aSearchString) 1.5601 + continue; 1.5602 + 1.5603 + // Supply a label and value, since they can differ for datalist suggestions 1.5604 + suggestions.push({ label: value, value: value }); 1.5605 + } 1.5606 + aCallback(suggestions); 1.5607 + }; 1.5608 + 1.5609 + this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id, 1.5610 + aSearchString, aElement, null, 1.5611 + resultsAvailable); 1.5612 + }, 1.5613 + 1.5614 + /** 1.5615 + * (Copied from mobile/xul/chrome/content/forms.js) 1.5616 + * This function is similar to getListSuggestions from 1.5617 + * components/satchel/src/nsInputListAutoComplete.js but sadly this one is 1.5618 + * used by the autocomplete.xml binding which is not in used in fennec 1.5619 + */ 1.5620 + _getListSuggestions: function _getListSuggestions(aElement) { 1.5621 + if (!(aElement instanceof HTMLInputElement) || !aElement.list) 1.5622 + return []; 1.5623 + 1.5624 + let suggestions = []; 1.5625 + let filter = !aElement.hasAttribute("mozNoFilter"); 1.5626 + let lowerFieldValue = aElement.value.toLowerCase(); 1.5627 + 1.5628 + let options = aElement.list.options; 1.5629 + let length = options.length; 1.5630 + for (let i = 0; i < length; i++) { 1.5631 + let item = options.item(i); 1.5632 + 1.5633 + let label = item.value; 1.5634 + if (item.label) 1.5635 + label = item.label; 1.5636 + else if (item.text) 1.5637 + label = item.text; 1.5638 + 1.5639 + if (filter && !(label.toLowerCase().contains(lowerFieldValue)) ) 1.5640 + continue; 1.5641 + suggestions.push({ label: label, value: item.value }); 1.5642 + } 1.5643 + 1.5644 + return suggestions; 1.5645 + }, 1.5646 + 1.5647 + // Retrieves autocomplete suggestions for an element from the form autocomplete service 1.5648 + // and sends the suggestions to the Java UI, along with element position data. As 1.5649 + // autocomplete queries are asynchronous, calls aCallback when done with a true 1.5650 + // argument if results were found and false if no results were found. 1.5651 + _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) { 1.5652 + if (!this._isAutoComplete(aElement)) { 1.5653 + aCallback(false); 1.5654 + return; 1.5655 + } 1.5656 + 1.5657 + // Don't display the form auto-complete popup after the user starts typing 1.5658 + // to avoid confusing somes IME. See bug 758820 and bug 632744. 1.5659 + if (this._isBlocklisted && aElement.value.length > 0) { 1.5660 + aCallback(false); 1.5661 + return; 1.5662 + } 1.5663 + 1.5664 + let resultsAvailable = autoCompleteSuggestions => { 1.5665 + // On desktop, we show datalist suggestions below autocomplete suggestions, 1.5666 + // without duplicates removed. 1.5667 + let listSuggestions = this._getListSuggestions(aElement); 1.5668 + let suggestions = autoCompleteSuggestions.concat(listSuggestions); 1.5669 + 1.5670 + // Return false if there are no suggestions to show 1.5671 + if (!suggestions.length) { 1.5672 + aCallback(false); 1.5673 + return; 1.5674 + } 1.5675 + 1.5676 + sendMessageToJava({ 1.5677 + type: "FormAssist:AutoComplete", 1.5678 + suggestions: suggestions, 1.5679 + rect: ElementTouchHelper.getBoundingContentRect(aElement) 1.5680 + }); 1.5681 + 1.5682 + // Keep track of input element so we can fill it in if the user 1.5683 + // selects an autocomplete suggestion 1.5684 + this._currentInputElement = aElement; 1.5685 + aCallback(true); 1.5686 + }; 1.5687 + 1.5688 + this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable); 1.5689 + }, 1.5690 + 1.5691 + // Only show a validation message if the user submitted an invalid form, 1.5692 + // there's a non-empty message string, and the element is the correct type 1.5693 + _isValidateable: function _isValidateable(aElement) { 1.5694 + if (!this._invalidSubmit || 1.5695 + !aElement.validationMessage || 1.5696 + !(aElement instanceof HTMLInputElement || 1.5697 + aElement instanceof HTMLTextAreaElement || 1.5698 + aElement instanceof HTMLSelectElement || 1.5699 + aElement instanceof HTMLButtonElement)) 1.5700 + return false; 1.5701 + 1.5702 + return true; 1.5703 + }, 1.5704 + 1.5705 + // Sends a validation message and position data for an element to the Java UI. 1.5706 + // Returns true if there's a validation message to show, false otherwise. 1.5707 + _showValidationMessage: function _sendValidationMessage(aElement) { 1.5708 + if (!this._isValidateable(aElement)) 1.5709 + return false; 1.5710 + 1.5711 + sendMessageToJava({ 1.5712 + type: "FormAssist:ValidationMessage", 1.5713 + validationMessage: aElement.validationMessage, 1.5714 + rect: ElementTouchHelper.getBoundingContentRect(aElement) 1.5715 + }); 1.5716 + 1.5717 + return true; 1.5718 + }, 1.5719 + 1.5720 + _hideFormAssistPopup: function _hideFormAssistPopup() { 1.5721 + sendMessageToJava({ type: "FormAssist:Hide" }); 1.5722 + } 1.5723 +}; 1.5724 + 1.5725 +/** 1.5726 + * An object to watch for Gecko status changes -- add-on installs, pref changes 1.5727 + * -- and reflect them back to Java. 1.5728 + */ 1.5729 +let HealthReportStatusListener = { 1.5730 + PREF_ACCEPT_LANG: "intl.accept_languages", 1.5731 + PREF_BLOCKLIST_ENABLED: "extensions.blocklist.enabled", 1.5732 + 1.5733 + PREF_TELEMETRY_ENABLED: 1.5734 +#ifdef MOZ_TELEMETRY_REPORTING 1.5735 + "toolkit.telemetry.enabled", 1.5736 +#else 1.5737 + null, 1.5738 +#endif 1.5739 + 1.5740 + init: function () { 1.5741 + try { 1.5742 + AddonManager.addAddonListener(this); 1.5743 + } catch (ex) { 1.5744 + console.log("Failed to initialize add-on status listener. FHR cannot report add-on state. " + ex); 1.5745 + } 1.5746 + 1.5747 + console.log("Adding HealthReport:RequestSnapshot observer."); 1.5748 + Services.obs.addObserver(this, "HealthReport:RequestSnapshot", false); 1.5749 + Services.prefs.addObserver(this.PREF_ACCEPT_LANG, this, false); 1.5750 + Services.prefs.addObserver(this.PREF_BLOCKLIST_ENABLED, this, false); 1.5751 + if (this.PREF_TELEMETRY_ENABLED) { 1.5752 + Services.prefs.addObserver(this.PREF_TELEMETRY_ENABLED, this, false); 1.5753 + } 1.5754 + }, 1.5755 + 1.5756 + uninit: function () { 1.5757 + Services.obs.removeObserver(this, "HealthReport:RequestSnapshot"); 1.5758 + Services.prefs.removeObserver(this.PREF_ACCEPT_LANG, this); 1.5759 + Services.prefs.removeObserver(this.PREF_BLOCKLIST_ENABLED, this); 1.5760 + if (this.PREF_TELEMETRY_ENABLED) { 1.5761 + Services.prefs.removeObserver(this.PREF_TELEMETRY_ENABLED, this); 1.5762 + } 1.5763 + 1.5764 + AddonManager.removeAddonListener(this); 1.5765 + }, 1.5766 + 1.5767 + observe: function (aSubject, aTopic, aData) { 1.5768 + switch (aTopic) { 1.5769 + case "HealthReport:RequestSnapshot": 1.5770 + HealthReportStatusListener.sendSnapshotToJava(); 1.5771 + break; 1.5772 + case "nsPref:changed": 1.5773 + let response = { 1.5774 + type: "Pref:Change", 1.5775 + pref: aData, 1.5776 + isUserSet: Services.prefs.prefHasUserValue(aData), 1.5777 + }; 1.5778 + 1.5779 + switch (aData) { 1.5780 + case this.PREF_ACCEPT_LANG: 1.5781 + response.value = Services.prefs.getCharPref(aData); 1.5782 + break; 1.5783 + case this.PREF_TELEMETRY_ENABLED: 1.5784 + case this.PREF_BLOCKLIST_ENABLED: 1.5785 + response.value = Services.prefs.getBoolPref(aData); 1.5786 + break; 1.5787 + default: 1.5788 + console.log("Unexpected pref in HealthReportStatusListener: " + aData); 1.5789 + return; 1.5790 + } 1.5791 + 1.5792 + sendMessageToJava(response); 1.5793 + break; 1.5794 + } 1.5795 + }, 1.5796 + 1.5797 + MILLISECONDS_PER_DAY: 24 * 60 * 60 * 1000, 1.5798 + 1.5799 + COPY_FIELDS: [ 1.5800 + "blocklistState", 1.5801 + "userDisabled", 1.5802 + "appDisabled", 1.5803 + "version", 1.5804 + "type", 1.5805 + "scope", 1.5806 + "foreignInstall", 1.5807 + "hasBinaryComponents", 1.5808 + ], 1.5809 + 1.5810 + // Add-on types for which full details are recorded in FHR. 1.5811 + // All other types are ignored. 1.5812 + FULL_DETAIL_TYPES: [ 1.5813 + "plugin", 1.5814 + "extension", 1.5815 + "service", 1.5816 + ], 1.5817 + 1.5818 + /** 1.5819 + * Return true if the add-on is not of a type for which we report full details. 1.5820 + * These add-ons will still make it over to Java, but will be filtered out. 1.5821 + */ 1.5822 + _shouldIgnore: function (aAddon) { 1.5823 + return this.FULL_DETAIL_TYPES.indexOf(aAddon.type) == -1; 1.5824 + }, 1.5825 + 1.5826 + _dateToDays: function (aDate) { 1.5827 + return Math.floor(aDate.getTime() / this.MILLISECONDS_PER_DAY); 1.5828 + }, 1.5829 + 1.5830 + jsonForAddon: function (aAddon) { 1.5831 + let o = {}; 1.5832 + if (aAddon.installDate) { 1.5833 + o.installDay = this._dateToDays(aAddon.installDate); 1.5834 + } 1.5835 + if (aAddon.updateDate) { 1.5836 + o.updateDay = this._dateToDays(aAddon.updateDate); 1.5837 + } 1.5838 + 1.5839 + for (let field of this.COPY_FIELDS) { 1.5840 + o[field] = aAddon[field]; 1.5841 + } 1.5842 + 1.5843 + return o; 1.5844 + }, 1.5845 + 1.5846 + notifyJava: function (aAddon, aNeedsRestart, aAction="Addons:Change") { 1.5847 + let json = this.jsonForAddon(aAddon); 1.5848 + if (this._shouldIgnore(aAddon)) { 1.5849 + json.ignore = true; 1.5850 + } 1.5851 + sendMessageToJava({ type: aAction, id: aAddon.id, json: json }); 1.5852 + }, 1.5853 + 1.5854 + // Add-on listeners. 1.5855 + onEnabling: function (aAddon, aNeedsRestart) { 1.5856 + this.notifyJava(aAddon, aNeedsRestart); 1.5857 + }, 1.5858 + onDisabling: function (aAddon, aNeedsRestart) { 1.5859 + this.notifyJava(aAddon, aNeedsRestart); 1.5860 + }, 1.5861 + onInstalling: function (aAddon, aNeedsRestart) { 1.5862 + this.notifyJava(aAddon, aNeedsRestart); 1.5863 + }, 1.5864 + onUninstalling: function (aAddon, aNeedsRestart) { 1.5865 + this.notifyJava(aAddon, aNeedsRestart, "Addons:Uninstalling"); 1.5866 + }, 1.5867 + onPropertyChanged: function (aAddon, aProperties) { 1.5868 + this.notifyJava(aAddon); 1.5869 + }, 1.5870 + onOperationCancelled: function (aAddon) { 1.5871 + this.notifyJava(aAddon); 1.5872 + }, 1.5873 + 1.5874 + sendSnapshotToJava: function () { 1.5875 + AddonManager.getAllAddons(function (aAddons) { 1.5876 + let jsonA = {}; 1.5877 + if (aAddons) { 1.5878 + for (let i = 0; i < aAddons.length; ++i) { 1.5879 + let addon = aAddons[i]; 1.5880 + try { 1.5881 + let addonJSON = HealthReportStatusListener.jsonForAddon(addon); 1.5882 + if (HealthReportStatusListener._shouldIgnore(addon)) { 1.5883 + addonJSON.ignore = true; 1.5884 + } 1.5885 + jsonA[addon.id] = addonJSON; 1.5886 + } catch (e) { 1.5887 + // Just skip this add-on. 1.5888 + } 1.5889 + } 1.5890 + } 1.5891 + 1.5892 + // Now add prefs. 1.5893 + let jsonP = {}; 1.5894 + for (let pref of [this.PREF_BLOCKLIST_ENABLED, this.PREF_TELEMETRY_ENABLED]) { 1.5895 + if (!pref) { 1.5896 + // This will be the case for PREF_TELEMETRY_ENABLED in developer builds. 1.5897 + continue; 1.5898 + } 1.5899 + jsonP[pref] = { 1.5900 + pref: pref, 1.5901 + value: Services.prefs.getBoolPref(pref), 1.5902 + isUserSet: Services.prefs.prefHasUserValue(pref), 1.5903 + }; 1.5904 + } 1.5905 + for (let pref of [this.PREF_ACCEPT_LANG]) { 1.5906 + jsonP[pref] = { 1.5907 + pref: pref, 1.5908 + value: Services.prefs.getCharPref(pref), 1.5909 + isUserSet: Services.prefs.prefHasUserValue(pref), 1.5910 + }; 1.5911 + } 1.5912 + 1.5913 + console.log("Sending snapshot message."); 1.5914 + sendMessageToJava({ 1.5915 + type: "HealthReport:Snapshot", 1.5916 + json: { 1.5917 + addons: jsonA, 1.5918 + prefs: jsonP, 1.5919 + }, 1.5920 + }); 1.5921 + }.bind(this)); 1.5922 + }, 1.5923 +}; 1.5924 + 1.5925 +var XPInstallObserver = { 1.5926 + init: function xpi_init() { 1.5927 + Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false); 1.5928 + Services.obs.addObserver(XPInstallObserver, "addon-install-started", false); 1.5929 + 1.5930 + AddonManager.addInstallListener(XPInstallObserver); 1.5931 + }, 1.5932 + 1.5933 + uninit: function xpi_uninit() { 1.5934 + Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked"); 1.5935 + Services.obs.removeObserver(XPInstallObserver, "addon-install-started"); 1.5936 + 1.5937 + AddonManager.removeInstallListener(XPInstallObserver); 1.5938 + }, 1.5939 + 1.5940 + observe: function xpi_observer(aSubject, aTopic, aData) { 1.5941 + switch (aTopic) { 1.5942 + case "addon-install-started": 1.5943 + NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsDownloading"), "short"); 1.5944 + break; 1.5945 + case "addon-install-blocked": 1.5946 + let installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo); 1.5947 + let win = installInfo.originatingWindow; 1.5948 + let tab = BrowserApp.getTabForWindow(win.top); 1.5949 + if (!tab) 1.5950 + return; 1.5951 + 1.5952 + let host = null; 1.5953 + if (installInfo.originatingURI) { 1.5954 + host = installInfo.originatingURI.host; 1.5955 + } 1.5956 + 1.5957 + let brandShortName = Strings.brand.GetStringFromName("brandShortName"); 1.5958 + let notificationName, buttons, message; 1.5959 + let strings = Strings.browser; 1.5960 + let enabled = true; 1.5961 + try { 1.5962 + enabled = Services.prefs.getBoolPref("xpinstall.enabled"); 1.5963 + } 1.5964 + catch (e) {} 1.5965 + 1.5966 + if (!enabled) { 1.5967 + notificationName = "xpinstall-disabled"; 1.5968 + if (Services.prefs.prefIsLocked("xpinstall.enabled")) { 1.5969 + message = strings.GetStringFromName("xpinstallDisabledMessageLocked"); 1.5970 + buttons = []; 1.5971 + } else { 1.5972 + message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2); 1.5973 + buttons = [{ 1.5974 + label: strings.GetStringFromName("xpinstallDisabledButton"), 1.5975 + callback: function editPrefs() { 1.5976 + Services.prefs.setBoolPref("xpinstall.enabled", true); 1.5977 + return false; 1.5978 + } 1.5979 + }]; 1.5980 + } 1.5981 + } else { 1.5982 + notificationName = "xpinstall"; 1.5983 + if (host) { 1.5984 + // We have a host which asked for the install. 1.5985 + message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2); 1.5986 + } else { 1.5987 + // Without a host we address the add-on as the initiator of the install. 1.5988 + let addon = null; 1.5989 + if (installInfo.installs.length > 0) { 1.5990 + addon = installInfo.installs[0].name; 1.5991 + } 1.5992 + if (addon) { 1.5993 + // We have an addon name, show the regular message. 1.5994 + message = strings.formatStringFromName("xpinstallPromptWarningLocal", [brandShortName, addon], 2); 1.5995 + } else { 1.5996 + // We don't have an addon name, show an alternative message. 1.5997 + message = strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1); 1.5998 + } 1.5999 + } 1.6000 + 1.6001 + buttons = [{ 1.6002 + label: strings.GetStringFromName("xpinstallPromptAllowButton"), 1.6003 + callback: function() { 1.6004 + // Kick off the install 1.6005 + installInfo.install(); 1.6006 + return false; 1.6007 + } 1.6008 + }]; 1.6009 + } 1.6010 + NativeWindow.doorhanger.show(message, aTopic, buttons, tab.id); 1.6011 + break; 1.6012 + } 1.6013 + }, 1.6014 + 1.6015 + onInstallEnded: function(aInstall, aAddon) { 1.6016 + let needsRestart = false; 1.6017 + if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE)) 1.6018 + needsRestart = true; 1.6019 + else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL) 1.6020 + needsRestart = true; 1.6021 + 1.6022 + if (needsRestart) { 1.6023 + this.showRestartPrompt(); 1.6024 + } else { 1.6025 + // Display completion message for new installs or updates not done Automatically 1.6026 + if (!aInstall.existingAddon || !AddonManager.shouldAutoUpdate(aInstall.existingAddon)) { 1.6027 + let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart"); 1.6028 + NativeWindow.toast.show(message, "short"); 1.6029 + } 1.6030 + } 1.6031 + }, 1.6032 + 1.6033 + onInstallFailed: function(aInstall) { 1.6034 + NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short"); 1.6035 + }, 1.6036 + 1.6037 + onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {}, 1.6038 + 1.6039 + onDownloadFailed: function(aInstall) { 1.6040 + this.onInstallFailed(aInstall); 1.6041 + }, 1.6042 + 1.6043 + onDownloadCancelled: function(aInstall) { 1.6044 + let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host; 1.6045 + if (!host) 1.6046 + host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host; 1.6047 + 1.6048 + let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError"; 1.6049 + if (aInstall.error != 0) 1.6050 + error += aInstall.error; 1.6051 + else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) 1.6052 + error += "Blocklisted"; 1.6053 + else if (aInstall.addon && (!aInstall.addon.isCompatible || !aInstall.addon.isPlatformCompatible)) 1.6054 + error += "Incompatible"; 1.6055 + else 1.6056 + return; // No need to show anything in this case. 1.6057 + 1.6058 + let msg = Strings.browser.GetStringFromName(error); 1.6059 + // TODO: formatStringFromName 1.6060 + msg = msg.replace("#1", aInstall.name); 1.6061 + if (host) 1.6062 + msg = msg.replace("#2", host); 1.6063 + msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName")); 1.6064 + msg = msg.replace("#4", Services.appinfo.version); 1.6065 + 1.6066 + NativeWindow.toast.show(msg, "short"); 1.6067 + }, 1.6068 + 1.6069 + showRestartPrompt: function() { 1.6070 + let buttons = [{ 1.6071 + label: Strings.browser.GetStringFromName("notificationRestart.button"), 1.6072 + callback: function() { 1.6073 + // Notify all windows that an application quit has been requested 1.6074 + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); 1.6075 + Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart"); 1.6076 + 1.6077 + // If nothing aborted, quit the app 1.6078 + if (cancelQuit.data == false) { 1.6079 + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup); 1.6080 + appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit); 1.6081 + } 1.6082 + } 1.6083 + }]; 1.6084 + 1.6085 + let message = Strings.browser.GetStringFromName("notificationRestart.normal"); 1.6086 + NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 }); 1.6087 + }, 1.6088 + 1.6089 + hideRestartPrompt: function() { 1.6090 + NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id); 1.6091 + } 1.6092 +}; 1.6093 + 1.6094 +// Blindly copied from Safari documentation for now. 1.6095 +const kViewportMinScale = 0; 1.6096 +const kViewportMaxScale = 10; 1.6097 +const kViewportMinWidth = 200; 1.6098 +const kViewportMaxWidth = 10000; 1.6099 +const kViewportMinHeight = 223; 1.6100 +const kViewportMaxHeight = 10000; 1.6101 + 1.6102 +var ViewportHandler = { 1.6103 + // The cached viewport metadata for each document. We tie viewport metadata to each document 1.6104 + // instead of to each tab so that we don't have to update it when the document changes. Using an 1.6105 + // ES6 weak map lets us avoid leaks. 1.6106 + _metadata: new WeakMap(), 1.6107 + 1.6108 + init: function init() { 1.6109 + addEventListener("DOMMetaAdded", this, false); 1.6110 + Services.obs.addObserver(this, "Window:Resize", false); 1.6111 + }, 1.6112 + 1.6113 + uninit: function uninit() { 1.6114 + removeEventListener("DOMMetaAdded", this, false); 1.6115 + Services.obs.removeObserver(this, "Window:Resize"); 1.6116 + }, 1.6117 + 1.6118 + handleEvent: function handleEvent(aEvent) { 1.6119 + switch (aEvent.type) { 1.6120 + case "DOMMetaAdded": 1.6121 + let target = aEvent.originalTarget; 1.6122 + if (target.name != "viewport") 1.6123 + break; 1.6124 + let document = target.ownerDocument; 1.6125 + let browser = BrowserApp.getBrowserForDocument(document); 1.6126 + let tab = BrowserApp.getTabForBrowser(browser); 1.6127 + if (tab) 1.6128 + this.updateMetadata(tab, false); 1.6129 + break; 1.6130 + } 1.6131 + }, 1.6132 + 1.6133 + observe: function(aSubject, aTopic, aData) { 1.6134 + switch (aTopic) { 1.6135 + case "Window:Resize": 1.6136 + if (window.outerWidth == gScreenWidth && window.outerHeight == gScreenHeight) 1.6137 + break; 1.6138 + if (window.outerWidth == 0 || window.outerHeight == 0) 1.6139 + break; 1.6140 + 1.6141 + let oldScreenWidth = gScreenWidth; 1.6142 + gScreenWidth = window.outerWidth * window.devicePixelRatio; 1.6143 + gScreenHeight = window.outerHeight * window.devicePixelRatio; 1.6144 + let tabs = BrowserApp.tabs; 1.6145 + for (let i = 0; i < tabs.length; i++) 1.6146 + tabs[i].updateViewportSize(oldScreenWidth); 1.6147 + break; 1.6148 + } 1.6149 + }, 1.6150 + 1.6151 + updateMetadata: function updateMetadata(tab, aInitialLoad) { 1.6152 + let contentWindow = tab.browser.contentWindow; 1.6153 + if (contentWindow.document.documentElement) { 1.6154 + let metadata = this.getViewportMetadata(contentWindow); 1.6155 + tab.updateViewportMetadata(metadata, aInitialLoad); 1.6156 + } 1.6157 + }, 1.6158 + 1.6159 + /** 1.6160 + * Returns the ViewportMetadata object. 1.6161 + */ 1.6162 + getViewportMetadata: function getViewportMetadata(aWindow) { 1.6163 + let windowUtils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.6164 + 1.6165 + // viewport details found here 1.6166 + // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html 1.6167 + // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html 1.6168 + 1.6169 + // Note: These values will be NaN if parseFloat or parseInt doesn't find a number. 1.6170 + // Remember that NaN is contagious: Math.max(1, NaN) == Math.min(1, NaN) == NaN. 1.6171 + let hasMetaViewport = true; 1.6172 + let scale = parseFloat(windowUtils.getDocumentMetadata("viewport-initial-scale")); 1.6173 + let minScale = parseFloat(windowUtils.getDocumentMetadata("viewport-minimum-scale")); 1.6174 + let maxScale = parseFloat(windowUtils.getDocumentMetadata("viewport-maximum-scale")); 1.6175 + 1.6176 + let widthStr = windowUtils.getDocumentMetadata("viewport-width"); 1.6177 + let heightStr = windowUtils.getDocumentMetadata("viewport-height"); 1.6178 + let width = this.clamp(parseInt(widthStr), kViewportMinWidth, kViewportMaxWidth) || 0; 1.6179 + let height = this.clamp(parseInt(heightStr), kViewportMinHeight, kViewportMaxHeight) || 0; 1.6180 + 1.6181 + // Allow zoom unless explicity disabled or minScale and maxScale are equal. 1.6182 + // WebKit allows 0, "no", or "false" for viewport-user-scalable. 1.6183 + // Note: NaN != NaN. Therefore if minScale and maxScale are undefined the clause has no effect. 1.6184 + let allowZoomStr = windowUtils.getDocumentMetadata("viewport-user-scalable"); 1.6185 + let allowZoom = !/^(0|no|false)$/.test(allowZoomStr) && (minScale != maxScale); 1.6186 + 1.6187 + // Double-tap should always be disabled if allowZoom is disabled. So we initialize 1.6188 + // allowDoubleTapZoom to the same value as allowZoom and have additional conditions to 1.6189 + // disable it in updateViewportSize. 1.6190 + let allowDoubleTapZoom = allowZoom; 1.6191 + 1.6192 + let autoSize = true; 1.6193 + 1.6194 + if (isNaN(scale) && isNaN(minScale) && isNaN(maxScale) && allowZoomStr == "" && widthStr == "" && heightStr == "") { 1.6195 + // Only check for HandheldFriendly if we don't have a viewport meta tag 1.6196 + let handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly"); 1.6197 + if (handheldFriendly == "true") { 1.6198 + return new ViewportMetadata({ 1.6199 + defaultZoom: 1, 1.6200 + autoSize: true, 1.6201 + allowZoom: true, 1.6202 + allowDoubleTapZoom: false 1.6203 + }); 1.6204 + } 1.6205 + 1.6206 + let doctype = aWindow.document.doctype; 1.6207 + if (doctype && /(WAP|WML|Mobile)/.test(doctype.publicId)) { 1.6208 + return new ViewportMetadata({ 1.6209 + defaultZoom: 1, 1.6210 + autoSize: true, 1.6211 + allowZoom: true, 1.6212 + allowDoubleTapZoom: false 1.6213 + }); 1.6214 + } 1.6215 + 1.6216 + hasMetaViewport = false; 1.6217 + let defaultZoom = Services.prefs.getIntPref("browser.viewport.defaultZoom"); 1.6218 + if (defaultZoom >= 0) { 1.6219 + scale = defaultZoom / 1000; 1.6220 + autoSize = false; 1.6221 + } 1.6222 + } 1.6223 + 1.6224 + scale = this.clamp(scale, kViewportMinScale, kViewportMaxScale); 1.6225 + minScale = this.clamp(minScale, kViewportMinScale, kViewportMaxScale); 1.6226 + maxScale = this.clamp(maxScale, minScale, kViewportMaxScale); 1.6227 + 1.6228 + if (autoSize) { 1.6229 + // If initial scale is 1.0 and width is not set, assume width=device-width 1.6230 + autoSize = (widthStr == "device-width" || 1.6231 + (!widthStr && (heightStr == "device-height" || scale == 1.0))); 1.6232 + } 1.6233 + 1.6234 + let isRTL = aWindow.document.documentElement.dir == "rtl"; 1.6235 + 1.6236 + return new ViewportMetadata({ 1.6237 + defaultZoom: scale, 1.6238 + minZoom: minScale, 1.6239 + maxZoom: maxScale, 1.6240 + width: width, 1.6241 + height: height, 1.6242 + autoSize: autoSize, 1.6243 + allowZoom: allowZoom, 1.6244 + allowDoubleTapZoom: allowDoubleTapZoom, 1.6245 + isSpecified: hasMetaViewport, 1.6246 + isRTL: isRTL 1.6247 + }); 1.6248 + }, 1.6249 + 1.6250 + clamp: function(num, min, max) { 1.6251 + return Math.max(min, Math.min(max, num)); 1.6252 + }, 1.6253 + 1.6254 + get displayDPI() { 1.6255 + let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); 1.6256 + delete this.displayDPI; 1.6257 + return this.displayDPI = utils.displayDPI; 1.6258 + }, 1.6259 + 1.6260 + /** 1.6261 + * Returns the viewport metadata for the given document, or the default metrics if no viewport 1.6262 + * metadata is available for that document. 1.6263 + */ 1.6264 + getMetadataForDocument: function getMetadataForDocument(aDocument) { 1.6265 + let metadata = this._metadata.get(aDocument, new ViewportMetadata()); 1.6266 + return metadata; 1.6267 + }, 1.6268 + 1.6269 + /** Updates the saved viewport metadata for the given content document. */ 1.6270 + setMetadataForDocument: function setMetadataForDocument(aDocument, aMetadata) { 1.6271 + if (!aMetadata) 1.6272 + this._metadata.delete(aDocument); 1.6273 + else 1.6274 + this._metadata.set(aDocument, aMetadata); 1.6275 + } 1.6276 + 1.6277 +}; 1.6278 + 1.6279 +/** 1.6280 + * An object which represents the page's preferred viewport properties: 1.6281 + * width (int): The CSS viewport width in px. 1.6282 + * height (int): The CSS viewport height in px. 1.6283 + * defaultZoom (float): The initial scale when the page is loaded. 1.6284 + * minZoom (float): The minimum zoom level. 1.6285 + * maxZoom (float): The maximum zoom level. 1.6286 + * autoSize (boolean): Resize the CSS viewport when the window resizes. 1.6287 + * allowZoom (boolean): Let the user zoom in or out. 1.6288 + * allowDoubleTapZoom (boolean): Allow double-tap to zoom in. 1.6289 + * isSpecified (boolean): Whether the page viewport is specified or not. 1.6290 + */ 1.6291 +function ViewportMetadata(aMetadata = {}) { 1.6292 + this.width = ("width" in aMetadata) ? aMetadata.width : 0; 1.6293 + this.height = ("height" in aMetadata) ? aMetadata.height : 0; 1.6294 + this.defaultZoom = ("defaultZoom" in aMetadata) ? aMetadata.defaultZoom : 0; 1.6295 + this.minZoom = ("minZoom" in aMetadata) ? aMetadata.minZoom : 0; 1.6296 + this.maxZoom = ("maxZoom" in aMetadata) ? aMetadata.maxZoom : 0; 1.6297 + this.autoSize = ("autoSize" in aMetadata) ? aMetadata.autoSize : false; 1.6298 + this.allowZoom = ("allowZoom" in aMetadata) ? aMetadata.allowZoom : true; 1.6299 + this.allowDoubleTapZoom = ("allowDoubleTapZoom" in aMetadata) ? aMetadata.allowDoubleTapZoom : true; 1.6300 + this.isSpecified = ("isSpecified" in aMetadata) ? aMetadata.isSpecified : false; 1.6301 + this.isRTL = ("isRTL" in aMetadata) ? aMetadata.isRTL : false; 1.6302 + Object.seal(this); 1.6303 +} 1.6304 + 1.6305 +ViewportMetadata.prototype = { 1.6306 + width: null, 1.6307 + height: null, 1.6308 + defaultZoom: null, 1.6309 + minZoom: null, 1.6310 + maxZoom: null, 1.6311 + autoSize: null, 1.6312 + allowZoom: null, 1.6313 + allowDoubleTapZoom: null, 1.6314 + isSpecified: null, 1.6315 + isRTL: null, 1.6316 + 1.6317 + toString: function() { 1.6318 + return "width=" + this.width 1.6319 + + "; height=" + this.height 1.6320 + + "; defaultZoom=" + this.defaultZoom 1.6321 + + "; minZoom=" + this.minZoom 1.6322 + + "; maxZoom=" + this.maxZoom 1.6323 + + "; autoSize=" + this.autoSize 1.6324 + + "; allowZoom=" + this.allowZoom 1.6325 + + "; allowDoubleTapZoom=" + this.allowDoubleTapZoom 1.6326 + + "; isSpecified=" + this.isSpecified 1.6327 + + "; isRTL=" + this.isRTL; 1.6328 + } 1.6329 +}; 1.6330 + 1.6331 + 1.6332 +/** 1.6333 + * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml 1.6334 + */ 1.6335 +var PopupBlockerObserver = { 1.6336 + onUpdatePageReport: function onUpdatePageReport(aEvent) { 1.6337 + let browser = BrowserApp.selectedBrowser; 1.6338 + if (aEvent.originalTarget != browser) 1.6339 + return; 1.6340 + 1.6341 + if (!browser.pageReport) 1.6342 + return; 1.6343 + 1.6344 + let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup"); 1.6345 + if (result == Ci.nsIPermissionManager.DENY_ACTION) 1.6346 + return; 1.6347 + 1.6348 + // Only show the notification again if we've not already shown it. Since 1.6349 + // notifications are per-browser, we don't need to worry about re-adding 1.6350 + // it. 1.6351 + if (!browser.pageReport.reported) { 1.6352 + if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) { 1.6353 + let brandShortName = Strings.brand.GetStringFromName("brandShortName"); 1.6354 + let popupCount = browser.pageReport.length; 1.6355 + 1.6356 + let strings = Strings.browser; 1.6357 + let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message")) 1.6358 + .replace("#1", brandShortName) 1.6359 + .replace("#2", popupCount); 1.6360 + 1.6361 + let buttons = [ 1.6362 + { 1.6363 + label: strings.GetStringFromName("popup.show"), 1.6364 + callback: function(aChecked) { 1.6365 + // Set permission before opening popup windows 1.6366 + if (aChecked) 1.6367 + PopupBlockerObserver.allowPopupsForSite(true); 1.6368 + 1.6369 + PopupBlockerObserver.showPopupsForSite(); 1.6370 + } 1.6371 + }, 1.6372 + { 1.6373 + label: strings.GetStringFromName("popup.dontShow"), 1.6374 + callback: function(aChecked) { 1.6375 + if (aChecked) 1.6376 + PopupBlockerObserver.allowPopupsForSite(false); 1.6377 + } 1.6378 + } 1.6379 + ]; 1.6380 + 1.6381 + let options = { checkbox: Strings.browser.GetStringFromName("popup.dontAskAgain") }; 1.6382 + NativeWindow.doorhanger.show(message, "popup-blocked", buttons, null, options); 1.6383 + } 1.6384 + // Record the fact that we've reported this blocked popup, so we don't 1.6385 + // show it again. 1.6386 + browser.pageReport.reported = true; 1.6387 + } 1.6388 + }, 1.6389 + 1.6390 + allowPopupsForSite: function allowPopupsForSite(aAllow) { 1.6391 + let currentURI = BrowserApp.selectedBrowser.currentURI; 1.6392 + Services.perms.add(currentURI, "popup", aAllow 1.6393 + ? Ci.nsIPermissionManager.ALLOW_ACTION 1.6394 + : Ci.nsIPermissionManager.DENY_ACTION); 1.6395 + dump("Allowing popups for: " + currentURI); 1.6396 + }, 1.6397 + 1.6398 + showPopupsForSite: function showPopupsForSite() { 1.6399 + let uri = BrowserApp.selectedBrowser.currentURI; 1.6400 + let pageReport = BrowserApp.selectedBrowser.pageReport; 1.6401 + if (pageReport) { 1.6402 + for (let i = 0; i < pageReport.length; ++i) { 1.6403 + let popupURIspec = pageReport[i].popupWindowURI.spec; 1.6404 + 1.6405 + // Sometimes the popup URI that we get back from the pageReport 1.6406 + // isn't useful (for instance, netscape.com's popup URI ends up 1.6407 + // being "http://www.netscape.com", which isn't really the URI of 1.6408 + // the popup they're trying to show). This isn't going to be 1.6409 + // useful to the user, so we won't create a menu item for it. 1.6410 + if (popupURIspec == "" || popupURIspec == "about:blank" || popupURIspec == uri.spec) 1.6411 + continue; 1.6412 + 1.6413 + let popupFeatures = pageReport[i].popupWindowFeatures; 1.6414 + let popupName = pageReport[i].popupWindowName; 1.6415 + 1.6416 + let parent = BrowserApp.selectedTab; 1.6417 + let isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow); 1.6418 + BrowserApp.addTab(popupURIspec, { parentId: parent.id, isPrivate: isPrivate }); 1.6419 + } 1.6420 + } 1.6421 + } 1.6422 +}; 1.6423 + 1.6424 + 1.6425 +var IndexedDB = { 1.6426 + _permissionsPrompt: "indexedDB-permissions-prompt", 1.6427 + _permissionsResponse: "indexedDB-permissions-response", 1.6428 + 1.6429 + _quotaPrompt: "indexedDB-quota-prompt", 1.6430 + _quotaResponse: "indexedDB-quota-response", 1.6431 + _quotaCancel: "indexedDB-quota-cancel", 1.6432 + 1.6433 + init: function IndexedDB_init() { 1.6434 + Services.obs.addObserver(this, this._permissionsPrompt, false); 1.6435 + Services.obs.addObserver(this, this._quotaPrompt, false); 1.6436 + Services.obs.addObserver(this, this._quotaCancel, false); 1.6437 + }, 1.6438 + 1.6439 + uninit: function IndexedDB_uninit() { 1.6440 + Services.obs.removeObserver(this, this._permissionsPrompt); 1.6441 + Services.obs.removeObserver(this, this._quotaPrompt); 1.6442 + Services.obs.removeObserver(this, this._quotaCancel); 1.6443 + }, 1.6444 + 1.6445 + observe: function IndexedDB_observe(subject, topic, data) { 1.6446 + if (topic != this._permissionsPrompt && 1.6447 + topic != this._quotaPrompt && 1.6448 + topic != this._quotaCancel) { 1.6449 + throw new Error("Unexpected topic!"); 1.6450 + } 1.6451 + 1.6452 + let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor); 1.6453 + 1.6454 + let contentWindow = requestor.getInterface(Ci.nsIDOMWindow); 1.6455 + let contentDocument = contentWindow.document; 1.6456 + let tab = BrowserApp.getTabForWindow(contentWindow); 1.6457 + if (!tab) 1.6458 + return; 1.6459 + 1.6460 + let host = contentDocument.documentURIObject.asciiHost; 1.6461 + 1.6462 + let strings = Strings.browser; 1.6463 + 1.6464 + let message, responseTopic; 1.6465 + if (topic == this._permissionsPrompt) { 1.6466 + message = strings.formatStringFromName("offlineApps.ask", [host], 1); 1.6467 + responseTopic = this._permissionsResponse; 1.6468 + } else if (topic == this._quotaPrompt) { 1.6469 + message = strings.formatStringFromName("indexedDBQuota.wantsTo", [ host, data ], 2); 1.6470 + responseTopic = this._quotaResponse; 1.6471 + } else if (topic == this._quotaCancel) { 1.6472 + responseTopic = this._quotaResponse; 1.6473 + } 1.6474 + 1.6475 + const firstTimeoutDuration = 300000; // 5 minutes 1.6476 + 1.6477 + let timeoutId; 1.6478 + 1.6479 + let notificationID = responseTopic + host; 1.6480 + let observer = requestor.getInterface(Ci.nsIObserver); 1.6481 + 1.6482 + // This will be set to the result of PopupNotifications.show() below, or to 1.6483 + // the result of PopupNotifications.getNotification() if this is a 1.6484 + // quotaCancel notification. 1.6485 + let notification; 1.6486 + 1.6487 + function timeoutNotification() { 1.6488 + // Remove the notification. 1.6489 + NativeWindow.doorhanger.hide(notificationID, tab.id); 1.6490 + 1.6491 + // Clear all of our timeout stuff. We may be called directly, not just 1.6492 + // when the timeout actually elapses. 1.6493 + clearTimeout(timeoutId); 1.6494 + 1.6495 + // And tell the page that the popup timed out. 1.6496 + observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION); 1.6497 + } 1.6498 + 1.6499 + if (topic == this._quotaCancel) { 1.6500 + NativeWindow.doorhanger.hide(notificationID, tab.id); 1.6501 + timeoutNotification(); 1.6502 + observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION); 1.6503 + return; 1.6504 + } 1.6505 + 1.6506 + let buttons = [{ 1.6507 + label: strings.GetStringFromName("offlineApps.allow"), 1.6508 + callback: function() { 1.6509 + clearTimeout(timeoutId); 1.6510 + observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION); 1.6511 + } 1.6512 + }, 1.6513 + { 1.6514 + label: strings.GetStringFromName("offlineApps.dontAllow2"), 1.6515 + callback: function(aChecked) { 1.6516 + clearTimeout(timeoutId); 1.6517 + let action = aChecked ? Ci.nsIPermissionManager.DENY_ACTION : Ci.nsIPermissionManager.UNKNOWN_ACTION; 1.6518 + observer.observe(null, responseTopic, action); 1.6519 + } 1.6520 + }]; 1.6521 + 1.6522 + let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") }; 1.6523 + NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options); 1.6524 + 1.6525 + // Set the timeoutId after the popup has been created, and use the long 1.6526 + // timeout value. If the user doesn't notice the popup after this amount of 1.6527 + // time then it is most likely not visible and we want to alert the page. 1.6528 + timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration); 1.6529 + } 1.6530 +}; 1.6531 + 1.6532 +var CharacterEncoding = { 1.6533 + _charsets: [], 1.6534 + 1.6535 + init: function init() { 1.6536 + Services.obs.addObserver(this, "CharEncoding:Get", false); 1.6537 + Services.obs.addObserver(this, "CharEncoding:Set", false); 1.6538 + this.sendState(); 1.6539 + }, 1.6540 + 1.6541 + uninit: function uninit() { 1.6542 + Services.obs.removeObserver(this, "CharEncoding:Get"); 1.6543 + Services.obs.removeObserver(this, "CharEncoding:Set"); 1.6544 + }, 1.6545 + 1.6546 + observe: function observe(aSubject, aTopic, aData) { 1.6547 + switch (aTopic) { 1.6548 + case "CharEncoding:Get": 1.6549 + this.getEncoding(); 1.6550 + break; 1.6551 + case "CharEncoding:Set": 1.6552 + this.setEncoding(aData); 1.6553 + break; 1.6554 + } 1.6555 + }, 1.6556 + 1.6557 + sendState: function sendState() { 1.6558 + let showCharEncoding = "false"; 1.6559 + try { 1.6560 + showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data; 1.6561 + } catch (e) { /* Optional */ } 1.6562 + 1.6563 + sendMessageToJava({ 1.6564 + type: "CharEncoding:State", 1.6565 + visible: showCharEncoding 1.6566 + }); 1.6567 + }, 1.6568 + 1.6569 + getEncoding: function getEncoding() { 1.6570 + function infoToCharset(info) { 1.6571 + return { code: info.value, title: info.label }; 1.6572 + } 1.6573 + 1.6574 + if (!this._charsets.length) { 1.6575 + let data = CharsetMenu.getData(); 1.6576 + 1.6577 + // In the desktop UI, the pinned charsets are shown above the rest. 1.6578 + let pinnedCharsets = data.pinnedCharsets.map(infoToCharset); 1.6579 + let otherCharsets = data.otherCharsets.map(infoToCharset) 1.6580 + 1.6581 + this._charsets = pinnedCharsets.concat(otherCharsets); 1.6582 + } 1.6583 + 1.6584 + // Look for the index of the selected charset. Default to -1 if the 1.6585 + // doc charset isn't found in the list of available charsets. 1.6586 + let docCharset = BrowserApp.selectedBrowser.contentDocument.characterSet; 1.6587 + let selected = -1; 1.6588 + let charsetCount = this._charsets.length; 1.6589 + 1.6590 + for (let i = 0; i < charsetCount; i++) { 1.6591 + if (this._charsets[i].code === docCharset) { 1.6592 + selected = i; 1.6593 + break; 1.6594 + } 1.6595 + } 1.6596 + 1.6597 + sendMessageToJava({ 1.6598 + type: "CharEncoding:Data", 1.6599 + charsets: this._charsets, 1.6600 + selected: selected 1.6601 + }); 1.6602 + }, 1.6603 + 1.6604 + setEncoding: function setEncoding(aEncoding) { 1.6605 + let browser = BrowserApp.selectedBrowser; 1.6606 + browser.docShell.gatherCharsetMenuTelemetry(); 1.6607 + browser.docShell.charset = aEncoding; 1.6608 + browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE); 1.6609 + } 1.6610 +}; 1.6611 + 1.6612 +var IdentityHandler = { 1.6613 + // No trusted identity information. No site identity icon is shown. 1.6614 + IDENTITY_MODE_UNKNOWN: "unknown", 1.6615 + 1.6616 + // Minimal SSL CA-signed domain verification. Blue lock icon is shown. 1.6617 + IDENTITY_MODE_DOMAIN_VERIFIED: "verified", 1.6618 + 1.6619 + // High-quality identity information. Green lock icon is shown. 1.6620 + IDENTITY_MODE_IDENTIFIED: "identified", 1.6621 + 1.6622 + // The following mixed content modes are only used if "security.mixed_content.block_active_content" 1.6623 + // is enabled. Even though the mixed content state and identitity state are orthogonal, 1.6624 + // our Java frontend coalesces them into one indicator. 1.6625 + 1.6626 + // Blocked active mixed content. Shield icon is shown, with a popup option to load content. 1.6627 + IDENTITY_MODE_MIXED_CONTENT_BLOCKED: "mixed_content_blocked", 1.6628 + 1.6629 + // Loaded active mixed content. Yellow triangle icon is shown. 1.6630 + IDENTITY_MODE_MIXED_CONTENT_LOADED: "mixed_content_loaded", 1.6631 + 1.6632 + // Cache the most recent SSLStatus and Location seen in getIdentityStrings 1.6633 + _lastStatus : null, 1.6634 + _lastLocation : null, 1.6635 + 1.6636 + /** 1.6637 + * Helper to parse out the important parts of _lastStatus (of the SSL cert in 1.6638 + * particular) for use in constructing identity UI strings 1.6639 + */ 1.6640 + getIdentityData : function() { 1.6641 + let result = {}; 1.6642 + let status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus); 1.6643 + let cert = status.serverCert; 1.6644 + 1.6645 + // Human readable name of Subject 1.6646 + result.subjectOrg = cert.organization; 1.6647 + 1.6648 + // SubjectName fields, broken up for individual access 1.6649 + if (cert.subjectName) { 1.6650 + result.subjectNameFields = {}; 1.6651 + cert.subjectName.split(",").forEach(function(v) { 1.6652 + let field = v.split("="); 1.6653 + this[field[0]] = field[1]; 1.6654 + }, result.subjectNameFields); 1.6655 + 1.6656 + // Call out city, state, and country specifically 1.6657 + result.city = result.subjectNameFields.L; 1.6658 + result.state = result.subjectNameFields.ST; 1.6659 + result.country = result.subjectNameFields.C; 1.6660 + } 1.6661 + 1.6662 + // Human readable name of Certificate Authority 1.6663 + result.caOrg = cert.issuerOrganization || cert.issuerCommonName; 1.6664 + result.cert = cert; 1.6665 + 1.6666 + return result; 1.6667 + }, 1.6668 + 1.6669 + /** 1.6670 + * Determines the identity mode corresponding to the icon we show in the urlbar. 1.6671 + */ 1.6672 + getIdentityMode: function getIdentityMode(aState) { 1.6673 + if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) 1.6674 + return this.IDENTITY_MODE_MIXED_CONTENT_BLOCKED; 1.6675 + 1.6676 + // Only show an indicator for loaded mixed content if the pref to block it is enabled 1.6677 + if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) && 1.6678 + Services.prefs.getBoolPref("security.mixed_content.block_active_content")) 1.6679 + return this.IDENTITY_MODE_MIXED_CONTENT_LOADED; 1.6680 + 1.6681 + if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) 1.6682 + return this.IDENTITY_MODE_IDENTIFIED; 1.6683 + 1.6684 + if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE) 1.6685 + return this.IDENTITY_MODE_DOMAIN_VERIFIED; 1.6686 + 1.6687 + return this.IDENTITY_MODE_UNKNOWN; 1.6688 + }, 1.6689 + 1.6690 + /** 1.6691 + * Determine the identity of the page being displayed by examining its SSL cert 1.6692 + * (if available). Return the data needed to update the UI. 1.6693 + */ 1.6694 + checkIdentity: function checkIdentity(aState, aBrowser) { 1.6695 + this._lastStatus = aBrowser.securityUI 1.6696 + .QueryInterface(Components.interfaces.nsISSLStatusProvider) 1.6697 + .SSLStatus; 1.6698 + 1.6699 + // Don't pass in the actual location object, since it can cause us to 1.6700 + // hold on to the window object too long. Just pass in the fields we 1.6701 + // care about. (bug 424829) 1.6702 + let locationObj = {}; 1.6703 + try { 1.6704 + let location = aBrowser.contentWindow.location; 1.6705 + locationObj.host = location.host; 1.6706 + locationObj.hostname = location.hostname; 1.6707 + locationObj.port = location.port; 1.6708 + } catch (ex) { 1.6709 + // Can sometimes throw if the URL being visited has no host/hostname, 1.6710 + // e.g. about:blank. The _state for these pages means we won't need these 1.6711 + // properties anyways, though. 1.6712 + } 1.6713 + this._lastLocation = locationObj; 1.6714 + 1.6715 + let mode = this.getIdentityMode(aState); 1.6716 + let result = { mode: mode }; 1.6717 + 1.6718 + // Don't show identity data for pages with an unknown identity or if any 1.6719 + // mixed content is loaded (mixed display content is loaded by default). 1.6720 + if (mode == this.IDENTITY_MODE_UNKNOWN || 1.6721 + aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) 1.6722 + return result; 1.6723 + 1.6724 + // Ideally we'd just make this a Java string 1.6725 + result.encrypted = Strings.browser.GetStringFromName("identity.encrypted2"); 1.6726 + result.host = this.getEffectiveHost(); 1.6727 + 1.6728 + let iData = this.getIdentityData(); 1.6729 + result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1); 1.6730 + 1.6731 + // If the cert is identified, then we can populate the results with credentials 1.6732 + if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) { 1.6733 + result.owner = iData.subjectOrg; 1.6734 + 1.6735 + // Build an appropriate supplemental block out of whatever location data we have 1.6736 + let supplemental = ""; 1.6737 + if (iData.city) 1.6738 + supplemental += iData.city + "\n"; 1.6739 + if (iData.state && iData.country) 1.6740 + supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2); 1.6741 + else if (iData.state) // State only 1.6742 + supplemental += iData.state; 1.6743 + else if (iData.country) // Country only 1.6744 + supplemental += iData.country; 1.6745 + result.supplemental = supplemental; 1.6746 + 1.6747 + return result; 1.6748 + } 1.6749 + 1.6750 + // Otherwise, we don't know the cert owner 1.6751 + result.owner = Strings.browser.GetStringFromName("identity.ownerUnknown3"); 1.6752 + 1.6753 + // Cache the override service the first time we need to check it 1.6754 + if (!this._overrideService) 1.6755 + this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService); 1.6756 + 1.6757 + // Check whether this site is a security exception. XPConnect does the right 1.6758 + // thing here in terms of converting _lastLocation.port from string to int, but 1.6759 + // the overrideService doesn't like undefined ports, so make sure we have 1.6760 + // something in the default case (bug 432241). 1.6761 + // .hostname can return an empty string in some exceptional cases - 1.6762 + // hasMatchingOverride does not handle that, so avoid calling it. 1.6763 + // Updating the tooltip value in those cases isn't critical. 1.6764 + // FIXME: Fixing bug 646690 would probably makes this check unnecessary 1.6765 + if (this._lastLocation.hostname && 1.6766 + this._overrideService.hasMatchingOverride(this._lastLocation.hostname, 1.6767 + (this._lastLocation.port || 443), 1.6768 + iData.cert, {}, {})) 1.6769 + result.verifier = Strings.browser.GetStringFromName("identity.identified.verified_by_you"); 1.6770 + 1.6771 + return result; 1.6772 + }, 1.6773 + 1.6774 + /** 1.6775 + * Return the eTLD+1 version of the current hostname 1.6776 + */ 1.6777 + getEffectiveHost: function getEffectiveHost() { 1.6778 + if (!this._IDNService) 1.6779 + this._IDNService = Cc["@mozilla.org/network/idn-service;1"] 1.6780 + .getService(Ci.nsIIDNService); 1.6781 + try { 1.6782 + let baseDomain = Services.eTLD.getBaseDomainFromHost(this._lastLocation.hostname); 1.6783 + return this._IDNService.convertToDisplayIDN(baseDomain, {}); 1.6784 + } catch (e) { 1.6785 + // If something goes wrong (e.g. hostname is an IP address) just fail back 1.6786 + // to the full domain. 1.6787 + return this._lastLocation.hostname; 1.6788 + } 1.6789 + } 1.6790 +}; 1.6791 + 1.6792 +function OverscrollController(aTab) { 1.6793 + this.tab = aTab; 1.6794 +} 1.6795 + 1.6796 +OverscrollController.prototype = { 1.6797 + supportsCommand : function supportsCommand(aCommand) { 1.6798 + if (aCommand != "cmd_linePrevious" && aCommand != "cmd_scrollPageUp") 1.6799 + return false; 1.6800 + 1.6801 + return (this.tab.getViewport().y == 0); 1.6802 + }, 1.6803 + 1.6804 + isCommandEnabled : function isCommandEnabled(aCommand) { 1.6805 + return this.supportsCommand(aCommand); 1.6806 + }, 1.6807 + 1.6808 + doCommand : function doCommand(aCommand){ 1.6809 + sendMessageToJava({ type: "ToggleChrome:Focus" }); 1.6810 + }, 1.6811 + 1.6812 + onEvent : function onEvent(aEvent) { } 1.6813 +}; 1.6814 + 1.6815 +var SearchEngines = { 1.6816 + _contextMenuId: null, 1.6817 + PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled", 1.6818 + PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted", 1.6819 + 1.6820 + init: function init() { 1.6821 + Services.obs.addObserver(this, "SearchEngines:Add", false); 1.6822 + Services.obs.addObserver(this, "SearchEngines:GetVisible", false); 1.6823 + Services.obs.addObserver(this, "SearchEngines:Remove", false); 1.6824 + Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false); 1.6825 + Services.obs.addObserver(this, "SearchEngines:SetDefault", false); 1.6826 + 1.6827 + let filter = { 1.6828 + matches: function (aElement) { 1.6829 + // Copied from body of isTargetAKeywordField function in nsContextMenu.js 1.6830 + if(!(aElement instanceof HTMLInputElement)) 1.6831 + return false; 1.6832 + let form = aElement.form; 1.6833 + if (!form || aElement.type == "password") 1.6834 + return false; 1.6835 + 1.6836 + let method = form.method.toUpperCase(); 1.6837 + 1.6838 + // These are the following types of forms we can create keywords for: 1.6839 + // 1.6840 + // method encoding type can create keyword 1.6841 + // GET * YES 1.6842 + // * YES 1.6843 + // POST * YES 1.6844 + // POST application/x-www-form-urlencoded YES 1.6845 + // POST text/plain NO ( a little tricky to do) 1.6846 + // POST multipart/form-data NO 1.6847 + // POST everything else YES 1.6848 + return (method == "GET" || method == "") || 1.6849 + (form.enctype != "text/plain") && (form.enctype != "multipart/form-data"); 1.6850 + } 1.6851 + }; 1.6852 + SelectionHandler.addAction({ 1.6853 + id: "search_add_action", 1.6854 + label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine"), 1.6855 + icon: "drawable://ab_add_search_engine", 1.6856 + selector: filter, 1.6857 + action: function(aElement) { 1.6858 + UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine"); 1.6859 + SearchEngines.addEngine(aElement); 1.6860 + } 1.6861 + }); 1.6862 + }, 1.6863 + 1.6864 + uninit: function uninit() { 1.6865 + Services.obs.removeObserver(this, "SearchEngines:Add"); 1.6866 + Services.obs.removeObserver(this, "SearchEngines:GetVisible"); 1.6867 + Services.obs.removeObserver(this, "SearchEngines:Remove"); 1.6868 + Services.obs.removeObserver(this, "SearchEngines:RestoreDefaults"); 1.6869 + Services.obs.removeObserver(this, "SearchEngines:SetDefault"); 1.6870 + if (this._contextMenuId != null) 1.6871 + NativeWindow.contextmenus.remove(this._contextMenuId); 1.6872 + }, 1.6873 + 1.6874 + // Fetch list of search engines. all ? All engines : Visible engines only. 1.6875 + _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) { 1.6876 + if (!Components.isSuccessCode(rv)) { 1.6877 + Cu.reportError("Could not initialize search service, bailing out."); 1.6878 + return; 1.6879 + } 1.6880 + 1.6881 + let engineData = Services.search.getVisibleEngines({}); 1.6882 + let searchEngines = engineData.map(function (engine) { 1.6883 + return { 1.6884 + name: engine.name, 1.6885 + identifier: engine.identifier, 1.6886 + iconURI: (engine.iconURI ? engine.iconURI.spec : null), 1.6887 + hidden: engine.hidden 1.6888 + }; 1.6889 + }); 1.6890 + 1.6891 + let suggestTemplate = null; 1.6892 + let suggestEngine = null; 1.6893 + 1.6894 + // Check to see if the default engine supports search suggestions. We only need to check 1.6895 + // the default engine because we only show suggestions for the default engine in the UI. 1.6896 + let engine = Services.search.defaultEngine; 1.6897 + if (engine.supportsResponseType("application/x-suggestions+json")) { 1.6898 + suggestEngine = engine.name; 1.6899 + suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec; 1.6900 + } 1.6901 + 1.6902 + // By convention, the currently configured default engine is at position zero in searchEngines. 1.6903 + sendMessageToJava({ 1.6904 + type: "SearchEngines:Data", 1.6905 + searchEngines: searchEngines, 1.6906 + suggest: { 1.6907 + engine: suggestEngine, 1.6908 + template: suggestTemplate, 1.6909 + enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED), 1.6910 + prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED) 1.6911 + } 1.6912 + }); 1.6913 + 1.6914 + // Send a speculative connection to the default engine. 1.6915 + let connector = Services.io.QueryInterface(Ci.nsISpeculativeConnect); 1.6916 + let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri; 1.6917 + let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor) 1.6918 + .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext); 1.6919 + try { 1.6920 + connector.speculativeConnect(searchURI, callbacks); 1.6921 + } catch (e) {} 1.6922 + }, 1.6923 + 1.6924 + // Helper method to extract the engine name from a JSON. Simplifies the observe function. 1.6925 + _extractEngineFromJSON: function _extractEngineFromJSON(aData) { 1.6926 + let data = JSON.parse(aData); 1.6927 + return Services.search.getEngineByName(data.engine); 1.6928 + }, 1.6929 + 1.6930 + observe: function observe(aSubject, aTopic, aData) { 1.6931 + let engine; 1.6932 + switch(aTopic) { 1.6933 + case "SearchEngines:Add": 1.6934 + this.displaySearchEnginesList(aData); 1.6935 + break; 1.6936 + case "SearchEngines:GetVisible": 1.6937 + Services.search.init(this._handleSearchEnginesGetVisible.bind(this)); 1.6938 + break; 1.6939 + case "SearchEngines:Remove": 1.6940 + // Make sure the engine isn't hidden before removing it, to make sure it's 1.6941 + // visible if the user later re-adds it (works around bug 341833) 1.6942 + engine = this._extractEngineFromJSON(aData); 1.6943 + engine.hidden = false; 1.6944 + Services.search.removeEngine(engine); 1.6945 + break; 1.6946 + case "SearchEngines:RestoreDefaults": 1.6947 + // Un-hides all default engines. 1.6948 + Services.search.restoreDefaultEngines(); 1.6949 + break; 1.6950 + case "SearchEngines:SetDefault": 1.6951 + engine = this._extractEngineFromJSON(aData); 1.6952 + // Move the new default search engine to the top of the search engine list. 1.6953 + Services.search.moveEngine(engine, 0); 1.6954 + Services.search.defaultEngine = engine; 1.6955 + break; 1.6956 + 1.6957 + default: 1.6958 + dump("Unexpected message type observed: " + aTopic); 1.6959 + break; 1.6960 + } 1.6961 + }, 1.6962 + 1.6963 + // Display context menu listing names of the search engines available to be added. 1.6964 + displaySearchEnginesList: function displaySearchEnginesList(aData) { 1.6965 + let data = JSON.parse(aData); 1.6966 + let tab = BrowserApp.getTabForId(data.tabId); 1.6967 + 1.6968 + if (!tab) 1.6969 + return; 1.6970 + 1.6971 + let browser = tab.browser; 1.6972 + let engines = browser.engines; 1.6973 + 1.6974 + let p = new Prompt({ 1.6975 + window: browser.contentWindow 1.6976 + }).setSingleChoiceItems(engines.map(function(e) { 1.6977 + return { label: e.title }; 1.6978 + })).show((function(data) { 1.6979 + if (data.button == -1) 1.6980 + return; 1.6981 + 1.6982 + this.addOpenSearchEngine(engines[data.button]); 1.6983 + engines.splice(data.button, 1); 1.6984 + 1.6985 + if (engines.length < 1) { 1.6986 + // Broadcast message that there are no more add-able search engines. 1.6987 + let newEngineMessage = { 1.6988 + type: "Link:OpenSearch", 1.6989 + tabID: tab.id, 1.6990 + visible: false 1.6991 + }; 1.6992 + 1.6993 + sendMessageToJava(newEngineMessage); 1.6994 + } 1.6995 + }).bind(this)); 1.6996 + }, 1.6997 + 1.6998 + addOpenSearchEngine: function addOpenSearchEngine(engine) { 1.6999 + Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, { 1.7000 + onSuccess: function() { 1.7001 + // Display a toast confirming addition of new search engine. 1.7002 + NativeWindow.toast.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), "long"); 1.7003 + }, 1.7004 + 1.7005 + onError: function(aCode) { 1.7006 + let errorMessage; 1.7007 + if (aCode == 2) { 1.7008 + // Engine is a duplicate. 1.7009 + errorMessage = "alertSearchEngineDuplicateToast"; 1.7010 + 1.7011 + } else { 1.7012 + // Unknown failure. Display general error message. 1.7013 + errorMessage = "alertSearchEngineErrorToast"; 1.7014 + } 1.7015 + 1.7016 + NativeWindow.toast.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), "long"); 1.7017 + } 1.7018 + }); 1.7019 + }, 1.7020 + 1.7021 + addEngine: function addEngine(aElement) { 1.7022 + let form = aElement.form; 1.7023 + let charset = aElement.ownerDocument.characterSet; 1.7024 + let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null); 1.7025 + let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec; 1.7026 + let method = form.method.toUpperCase(); 1.7027 + let formData = []; 1.7028 + 1.7029 + for (let i = 0; i < form.elements.length; ++i) { 1.7030 + let el = form.elements[i]; 1.7031 + if (!el.type) 1.7032 + continue; 1.7033 + 1.7034 + // make this text field a generic search parameter 1.7035 + if (aElement == el) { 1.7036 + formData.push({ name: el.name, value: "{searchTerms}" }); 1.7037 + continue; 1.7038 + } 1.7039 + 1.7040 + let type = el.type.toLowerCase(); 1.7041 + let escapedName = escape(el.name); 1.7042 + let escapedValue = escape(el.value); 1.7043 + 1.7044 + // add other form elements as parameters 1.7045 + switch (el.type) { 1.7046 + case "checkbox": 1.7047 + case "radio": 1.7048 + if (!el.checked) break; 1.7049 + case "text": 1.7050 + case "hidden": 1.7051 + case "textarea": 1.7052 + formData.push({ name: escapedName, value: escapedValue }); 1.7053 + break; 1.7054 + case "select-one": 1.7055 + for (let option of el.options) { 1.7056 + if (option.selected) { 1.7057 + formData.push({ name: escapedName, value: escapedValue }); 1.7058 + break; 1.7059 + } 1.7060 + } 1.7061 + } 1.7062 + } 1.7063 + 1.7064 + // prompt user for name of search engine 1.7065 + let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine"); 1.7066 + let title = { value: (aElement.ownerDocument.title || docURI.host) }; 1.7067 + if (!Services.prompt.prompt(null, promptTitle, null, title, null, {})) 1.7068 + return; 1.7069 + 1.7070 + // fetch the favicon for this page 1.7071 + let dbFile = FileUtils.getFile("ProfD", ["browser.db"]); 1.7072 + let mDBConn = Services.storage.openDatabase(dbFile); 1.7073 + let stmts = []; 1.7074 + stmts[0] = mDBConn.createStatement("SELECT favicon FROM history_with_favicons WHERE url = ?"); 1.7075 + stmts[0].bindStringParameter(0, docURI.spec); 1.7076 + let favicon = null; 1.7077 + Services.search.init(function addEngine_cb(rv) { 1.7078 + if (!Components.isSuccessCode(rv)) { 1.7079 + Cu.reportError("Could not initialize search service, bailing out."); 1.7080 + return; 1.7081 + } 1.7082 + mDBConn.executeAsync(stmts, stmts.length, { 1.7083 + handleResult: function (results) { 1.7084 + let bytes = results.getNextRow().getResultByName("favicon"); 1.7085 + if (bytes && bytes.length) { 1.7086 + favicon = "data:image/x-icon;base64," + btoa(String.fromCharCode.apply(null, bytes)); 1.7087 + } 1.7088 + }, 1.7089 + handleCompletion: function (reason) { 1.7090 + // if there's already an engine with this name, add a number to 1.7091 + // make the name unique (e.g., "Google" becomes "Google 2") 1.7092 + let name = title.value; 1.7093 + for (let i = 2; Services.search.getEngineByName(name); i++) 1.7094 + name = title.value + " " + i; 1.7095 + 1.7096 + Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL); 1.7097 + let engine = Services.search.getEngineByName(name); 1.7098 + engine.wrappedJSObject._queryCharset = charset; 1.7099 + for (let i = 0; i < formData.length; ++i) { 1.7100 + let param = formData[i]; 1.7101 + if (param.name && param.value) 1.7102 + engine.addParam(param.name, param.value, null); 1.7103 + } 1.7104 + } 1.7105 + }); 1.7106 + }); 1.7107 + } 1.7108 +}; 1.7109 + 1.7110 +var ActivityObserver = { 1.7111 + init: function ao_init() { 1.7112 + Services.obs.addObserver(this, "application-background", false); 1.7113 + Services.obs.addObserver(this, "application-foreground", false); 1.7114 + }, 1.7115 + 1.7116 + observe: function ao_observe(aSubject, aTopic, aData) { 1.7117 + let isForeground = false; 1.7118 + let tab = BrowserApp.selectedTab; 1.7119 + 1.7120 + switch (aTopic) { 1.7121 + case "application-background" : 1.7122 + let doc = (tab ? tab.browser.contentDocument : null); 1.7123 + if (doc && doc.mozFullScreen) { 1.7124 + doc.mozCancelFullScreen(); 1.7125 + } 1.7126 + isForeground = false; 1.7127 + break; 1.7128 + case "application-foreground" : 1.7129 + isForeground = true; 1.7130 + break; 1.7131 + } 1.7132 + 1.7133 + if (tab && tab.getActive() != isForeground) { 1.7134 + tab.setActive(isForeground); 1.7135 + } 1.7136 + } 1.7137 +}; 1.7138 + 1.7139 +#ifndef MOZ_ANDROID_SYNTHAPKS 1.7140 +var WebappsUI = { 1.7141 + init: function init() { 1.7142 + Cu.import("resource://gre/modules/Webapps.jsm"); 1.7143 + Cu.import("resource://gre/modules/AppsUtils.jsm"); 1.7144 + DOMApplicationRegistry.allAppsLaunchable = true; 1.7145 + 1.7146 + Services.obs.addObserver(this, "webapps-ask-install", false); 1.7147 + Services.obs.addObserver(this, "webapps-launch", false); 1.7148 + Services.obs.addObserver(this, "webapps-uninstall", false); 1.7149 + Services.obs.addObserver(this, "webapps-install-error", false); 1.7150 + }, 1.7151 + 1.7152 + uninit: function unint() { 1.7153 + Services.obs.removeObserver(this, "webapps-ask-install"); 1.7154 + Services.obs.removeObserver(this, "webapps-launch"); 1.7155 + Services.obs.removeObserver(this, "webapps-uninstall"); 1.7156 + Services.obs.removeObserver(this, "webapps-install-error"); 1.7157 + }, 1.7158 + 1.7159 + DEFAULT_ICON: "chrome://browser/skin/images/default-app-icon.png", 1.7160 + DEFAULT_PREFS_FILENAME: "default-prefs.js", 1.7161 + 1.7162 + observe: function observe(aSubject, aTopic, aData) { 1.7163 + let data = {}; 1.7164 + try { 1.7165 + data = JSON.parse(aData); 1.7166 + data.mm = aSubject; 1.7167 + } catch(ex) { } 1.7168 + switch (aTopic) { 1.7169 + case "webapps-install-error": 1.7170 + let msg = ""; 1.7171 + switch (aData) { 1.7172 + case "INVALID_MANIFEST": 1.7173 + case "MANIFEST_PARSE_ERROR": 1.7174 + msg = Strings.browser.GetStringFromName("webapps.manifestInstallError"); 1.7175 + break; 1.7176 + case "NETWORK_ERROR": 1.7177 + case "MANIFEST_URL_ERROR": 1.7178 + msg = Strings.browser.GetStringFromName("webapps.networkInstallError"); 1.7179 + break; 1.7180 + default: 1.7181 + msg = Strings.browser.GetStringFromName("webapps.installError"); 1.7182 + } 1.7183 + NativeWindow.toast.show(msg, "short"); 1.7184 + console.log("Error installing app: " + aData); 1.7185 + break; 1.7186 + case "webapps-ask-install": 1.7187 + this.doInstall(data); 1.7188 + break; 1.7189 + case "webapps-launch": 1.7190 + this.openURL(data.manifestURL, data.origin); 1.7191 + break; 1.7192 + case "webapps-uninstall": 1.7193 + sendMessageToJava({ 1.7194 + type: "Webapps:Uninstall", 1.7195 + origin: data.origin 1.7196 + }); 1.7197 + break; 1.7198 + } 1.7199 + }, 1.7200 + 1.7201 + doInstall: function doInstall(aData) { 1.7202 + let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest; 1.7203 + let manifest = new ManifestHelper(jsonManifest, aData.app.origin); 1.7204 + 1.7205 + if (Services.prompt.confirm(null, Strings.browser.GetStringFromName("webapps.installTitle"), manifest.name + "\n" + aData.app.origin)) { 1.7206 + // Get a profile for the app to be installed in. We'll download everything before creating the icons. 1.7207 + let origin = aData.app.origin; 1.7208 + sendMessageToJava({ 1.7209 + type: "Webapps:Preinstall", 1.7210 + name: manifest.name, 1.7211 + manifestURL: aData.app.manifestURL, 1.7212 + origin: origin 1.7213 + }, (data) => { 1.7214 + let profilePath = data.profile; 1.7215 + if (!profilePath) 1.7216 + return; 1.7217 + 1.7218 + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile); 1.7219 + file.initWithPath(profilePath); 1.7220 + 1.7221 + let self = this; 1.7222 + DOMApplicationRegistry.confirmInstall(aData, file, 1.7223 + function (aManifest) { 1.7224 + let localeManifest = new ManifestHelper(aManifest, aData.app.origin); 1.7225 + 1.7226 + // the manifest argument is the manifest from within the zip file, 1.7227 + // TODO so now would be a good time to ask about permissions. 1.7228 + self.makeBase64Icon(localeManifest.biggestIconURL || this.DEFAULT_ICON, 1.7229 + function(scaledIcon, fullsizeIcon) { 1.7230 + // if java returned a profile path to us, try to use it to pre-populate the app cache 1.7231 + // also save the icon so that it can be used in the splash screen 1.7232 + try { 1.7233 + let iconFile = file.clone(); 1.7234 + iconFile.append("logo.png"); 1.7235 + let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist); 1.7236 + persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES; 1.7237 + persist.persistFlags |= Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION; 1.7238 + 1.7239 + let source = Services.io.newURI(fullsizeIcon, "UTF8", null); 1.7240 + persist.saveURI(source, null, null, null, null, iconFile, null); 1.7241 + 1.7242 + // aData.app.origin may now point to the app: url that hosts this app 1.7243 + sendMessageToJava({ 1.7244 + type: "Webapps:Postinstall", 1.7245 + name: localeManifest.name, 1.7246 + manifestURL: aData.app.manifestURL, 1.7247 + originalOrigin: origin, 1.7248 + origin: aData.app.origin, 1.7249 + iconURL: fullsizeIcon 1.7250 + }); 1.7251 + if (!!aData.isPackage) { 1.7252 + // For packaged apps, put a notification in the notification bar. 1.7253 + let message = Strings.browser.GetStringFromName("webapps.alertSuccess"); 1.7254 + let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); 1.7255 + alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", { 1.7256 + observe: function () { 1.7257 + self.openURL(aData.app.manifestURL, aData.app.origin); 1.7258 + } 1.7259 + }, "webapp"); 1.7260 + } 1.7261 + 1.7262 + // Create a system notification allowing the user to launch the app 1.7263 + let observer = { 1.7264 + observe: function (aSubject, aTopic) { 1.7265 + if (aTopic == "alertclickcallback") { 1.7266 + WebappsUI.openURL(aData.app.manifestURL, origin); 1.7267 + } 1.7268 + } 1.7269 + }; 1.7270 + 1.7271 + let message = Strings.browser.GetStringFromName("webapps.alertSuccess"); 1.7272 + let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); 1.7273 + alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", observer, "webapp"); 1.7274 + } catch(ex) { 1.7275 + console.log(ex); 1.7276 + } 1.7277 + self.writeDefaultPrefs(file, localeManifest); 1.7278 + } 1.7279 + ); 1.7280 + } 1.7281 + ); 1.7282 + }); 1.7283 + } else { 1.7284 + DOMApplicationRegistry.denyInstall(aData); 1.7285 + } 1.7286 + }, 1.7287 + 1.7288 + writeDefaultPrefs: function webapps_writeDefaultPrefs(aProfile, aManifest) { 1.7289 + // build any app specific default prefs 1.7290 + let prefs = []; 1.7291 + if (aManifest.orientation) { 1.7292 + prefs.push({name:"app.orientation.default", value: aManifest.orientation.join(",") }); 1.7293 + } 1.7294 + 1.7295 + // write them into the app profile 1.7296 + let defaultPrefsFile = aProfile.clone(); 1.7297 + defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME); 1.7298 + this._writeData(defaultPrefsFile, prefs); 1.7299 + }, 1.7300 + 1.7301 + _writeData: function(aFile, aPrefs) { 1.7302 + if (aPrefs.length > 0) { 1.7303 + let array = new TextEncoder().encode(JSON.stringify(aPrefs)); 1.7304 + OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) { 1.7305 + console.log("Error writing default prefs: " + reason); 1.7306 + }); 1.7307 + } 1.7308 + }, 1.7309 + 1.7310 + openURL: function openURL(aManifestURL, aOrigin) { 1.7311 + sendMessageToJava({ 1.7312 + type: "Webapps:Open", 1.7313 + manifestURL: aManifestURL, 1.7314 + origin: aOrigin 1.7315 + }); 1.7316 + }, 1.7317 + 1.7318 + get iconSize() { 1.7319 + let iconSize = 64; 1.7320 + try { 1.7321 + let jni = new JNI(); 1.7322 + let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell"); 1.7323 + let method = jni.getStaticMethodID(cls, "getPreferredIconSize", "()I"); 1.7324 + iconSize = jni.callStaticIntMethod(cls, method); 1.7325 + jni.close(); 1.7326 + } catch(ex) { 1.7327 + console.log(ex); 1.7328 + } 1.7329 + 1.7330 + delete this.iconSize; 1.7331 + return this.iconSize = iconSize; 1.7332 + }, 1.7333 + 1.7334 + makeBase64Icon: function loadAndMakeBase64Icon(aIconURL, aCallbackFunction) { 1.7335 + let size = this.iconSize; 1.7336 + 1.7337 + let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); 1.7338 + canvas.width = canvas.height = size; 1.7339 + let ctx = canvas.getContext("2d"); 1.7340 + let favicon = new Image(); 1.7341 + favicon.onload = function() { 1.7342 + ctx.drawImage(favicon, 0, 0, size, size); 1.7343 + let scaledIcon = canvas.toDataURL("image/png", ""); 1.7344 + 1.7345 + canvas.width = favicon.width; 1.7346 + canvas.height = favicon.height; 1.7347 + ctx.drawImage(favicon, 0, 0, favicon.width, favicon.height); 1.7348 + let fullsizeIcon = canvas.toDataURL("image/png", ""); 1.7349 + 1.7350 + canvas = null; 1.7351 + aCallbackFunction.call(null, scaledIcon, fullsizeIcon); 1.7352 + }; 1.7353 + favicon.onerror = function() { 1.7354 + Cu.reportError("CreateShortcut: favicon image load error"); 1.7355 + 1.7356 + // if the image failed to load, and it was not our default icon, attempt to 1.7357 + // use our default as a fallback 1.7358 + if (favicon.src != WebappsUI.DEFAULT_ICON) { 1.7359 + favicon.src = WebappsUI.DEFAULT_ICON; 1.7360 + } 1.7361 + }; 1.7362 + 1.7363 + favicon.src = aIconURL; 1.7364 + }, 1.7365 + 1.7366 + createShortcut: function createShortcut(aTitle, aURL, aIconURL, aType) { 1.7367 + this.makeBase64Icon(aIconURL, function _createShortcut(icon) { 1.7368 + try { 1.7369 + let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService); 1.7370 + shell.createShortcut(aTitle, aURL, icon, aType); 1.7371 + } catch(e) { 1.7372 + Cu.reportError(e); 1.7373 + } 1.7374 + }); 1.7375 + } 1.7376 +} 1.7377 +#endif 1.7378 + 1.7379 +var RemoteDebugger = { 1.7380 + init: function rd_init() { 1.7381 + Services.prefs.addObserver("devtools.debugger.", this, false); 1.7382 + 1.7383 + if (this._isEnabled()) 1.7384 + this._start(); 1.7385 + }, 1.7386 + 1.7387 + observe: function rd_observe(aSubject, aTopic, aData) { 1.7388 + if (aTopic != "nsPref:changed") 1.7389 + return; 1.7390 + 1.7391 + switch (aData) { 1.7392 + case "devtools.debugger.remote-enabled": 1.7393 + if (this._isEnabled()) 1.7394 + this._start(); 1.7395 + else 1.7396 + this._stop(); 1.7397 + break; 1.7398 + 1.7399 + case "devtools.debugger.remote-port": 1.7400 + if (this._isEnabled()) 1.7401 + this._restart(); 1.7402 + break; 1.7403 + } 1.7404 + }, 1.7405 + 1.7406 + uninit: function rd_uninit() { 1.7407 + Services.prefs.removeObserver("devtools.debugger.", this); 1.7408 + this._stop(); 1.7409 + }, 1.7410 + 1.7411 + _getPort: function _rd_getPort() { 1.7412 + return Services.prefs.getIntPref("devtools.debugger.remote-port"); 1.7413 + }, 1.7414 + 1.7415 + _isEnabled: function rd_isEnabled() { 1.7416 + return Services.prefs.getBoolPref("devtools.debugger.remote-enabled"); 1.7417 + }, 1.7418 + 1.7419 + /** 1.7420 + * Prompt the user to accept or decline the incoming connection. 1.7421 + * This is passed to DebuggerService.init as a callback. 1.7422 + * 1.7423 + * @return true if the connection should be permitted, false otherwise 1.7424 + */ 1.7425 + _showConnectionPrompt: function rd_showConnectionPrompt() { 1.7426 + let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle"); 1.7427 + let msg = Strings.browser.GetStringFromName("remoteIncomingPromptMessage"); 1.7428 + let disable = Strings.browser.GetStringFromName("remoteIncomingPromptDisable"); 1.7429 + let cancel = Strings.browser.GetStringFromName("remoteIncomingPromptCancel"); 1.7430 + let agree = Strings.browser.GetStringFromName("remoteIncomingPromptAccept"); 1.7431 + 1.7432 + // Make prompt. Note: button order is in reverse. 1.7433 + let prompt = new Prompt({ 1.7434 + window: null, 1.7435 + hint: "remotedebug", 1.7436 + title: title, 1.7437 + message: msg, 1.7438 + buttons: [ agree, cancel, disable ], 1.7439 + priority: 1 1.7440 + }); 1.7441 + 1.7442 + // The debugger server expects a synchronous response, so spin on result since Prompt is async. 1.7443 + let result = null; 1.7444 + 1.7445 + prompt.show(function(data) { 1.7446 + result = data.button; 1.7447 + }); 1.7448 + 1.7449 + // Spin this thread while we wait for a result. 1.7450 + let thread = Services.tm.currentThread; 1.7451 + while (result == null) 1.7452 + thread.processNextEvent(true); 1.7453 + 1.7454 + if (result === 0) 1.7455 + return true; 1.7456 + if (result === 2) { 1.7457 + Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false); 1.7458 + this._stop(); 1.7459 + } 1.7460 + return false; 1.7461 + }, 1.7462 + 1.7463 + _restart: function rd_restart() { 1.7464 + this._stop(); 1.7465 + this._start(); 1.7466 + }, 1.7467 + 1.7468 + _start: function rd_start() { 1.7469 + try { 1.7470 + if (!DebuggerServer.initialized) { 1.7471 + DebuggerServer.init(this._showConnectionPrompt.bind(this)); 1.7472 + DebuggerServer.addBrowserActors(); 1.7473 + DebuggerServer.addActors("chrome://browser/content/dbg-browser-actors.js"); 1.7474 + } 1.7475 + 1.7476 + let port = this._getPort(); 1.7477 + DebuggerServer.openListener(port); 1.7478 + dump("Remote debugger listening on port " + port); 1.7479 + } catch(e) { 1.7480 + dump("Remote debugger didn't start: " + e); 1.7481 + } 1.7482 + }, 1.7483 + 1.7484 + _stop: function rd_start() { 1.7485 + DebuggerServer.closeListener(); 1.7486 + dump("Remote debugger stopped"); 1.7487 + } 1.7488 +}; 1.7489 + 1.7490 +var Telemetry = { 1.7491 + addData: function addData(aHistogramId, aValue) { 1.7492 + let histogram = Services.telemetry.getHistogramById(aHistogramId); 1.7493 + histogram.add(aValue); 1.7494 + }, 1.7495 +}; 1.7496 + 1.7497 +let Reader = { 1.7498 + // Version of the cache database schema 1.7499 + DB_VERSION: 1, 1.7500 + 1.7501 + DEBUG: 0, 1.7502 + 1.7503 + READER_ADD_SUCCESS: 0, 1.7504 + READER_ADD_FAILED: 1, 1.7505 + READER_ADD_DUPLICATE: 2, 1.7506 + 1.7507 + // Don't try to parse the page if it has too many elements (for memory and 1.7508 + // performance reasons) 1.7509 + MAX_ELEMS_TO_PARSE: 3000, 1.7510 + 1.7511 + isEnabledForParseOnLoad: false, 1.7512 + 1.7513 + init: function Reader_init() { 1.7514 + this.log("Init()"); 1.7515 + this._requests = {}; 1.7516 + 1.7517 + this.isEnabledForParseOnLoad = this.getStateForParseOnLoad(); 1.7518 + 1.7519 + Services.obs.addObserver(this, "Reader:Add", false); 1.7520 + Services.obs.addObserver(this, "Reader:Remove", false); 1.7521 + 1.7522 + Services.prefs.addObserver("reader.parse-on-load.", this, false); 1.7523 + }, 1.7524 + 1.7525 + pageAction: { 1.7526 + readerModeCallback: function(){ 1.7527 + sendMessageToJava({ 1.7528 + type: "Reader:Click", 1.7529 + }); 1.7530 + }, 1.7531 + 1.7532 + readerModeActiveCallback: function(){ 1.7533 + sendMessageToJava({ 1.7534 + type: "Reader:LongClick", 1.7535 + }); 1.7536 + 1.7537 + // Create a relative timestamp for telemetry 1.7538 + let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; 1.7539 + UITelemetry.addEvent("save.1", "pageaction", uptime, "reader"); 1.7540 + }, 1.7541 + }, 1.7542 + 1.7543 + updatePageAction: function(tab) { 1.7544 + if (this.pageAction.id) { 1.7545 + NativeWindow.pageactions.remove(this.pageAction.id); 1.7546 + delete this.pageAction.id; 1.7547 + } 1.7548 + 1.7549 + // Create a relative timestamp for telemetry 1.7550 + let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; 1.7551 + 1.7552 + if (tab.readerActive) { 1.7553 + this.pageAction.id = NativeWindow.pageactions.add({ 1.7554 + title: Strings.browser.GetStringFromName("readerMode.exit"), 1.7555 + icon: "drawable://reader_active", 1.7556 + clickCallback: this.pageAction.readerModeCallback, 1.7557 + important: true 1.7558 + }); 1.7559 + 1.7560 + // Only start a reader session if the viewer is in the foreground. We do 1.7561 + // not track background reader viewers. 1.7562 + UITelemetry.startSession("reader.1", uptime); 1.7563 + return; 1.7564 + } 1.7565 + 1.7566 + // Only stop a reader session if the foreground viewer is not visible. 1.7567 + UITelemetry.stopSession("reader.1", "", uptime); 1.7568 + 1.7569 + if (tab.readerEnabled) { 1.7570 + this.pageAction.id = NativeWindow.pageactions.add({ 1.7571 + title: Strings.browser.GetStringFromName("readerMode.enter"), 1.7572 + icon: "drawable://reader", 1.7573 + clickCallback:this.pageAction.readerModeCallback, 1.7574 + longClickCallback: this.pageAction.readerModeActiveCallback, 1.7575 + important: true 1.7576 + }); 1.7577 + } 1.7578 + }, 1.7579 + 1.7580 + observe: function(aMessage, aTopic, aData) { 1.7581 + switch(aTopic) { 1.7582 + case "Reader:Add": { 1.7583 + let args = JSON.parse(aData); 1.7584 + if ('fromAboutReader' in args) { 1.7585 + // Ignore adds initiated from aboutReader menu banner 1.7586 + break; 1.7587 + } 1.7588 + 1.7589 + let tabID = null; 1.7590 + let url, urlWithoutRef; 1.7591 + 1.7592 + if ('tabID' in args) { 1.7593 + tabID = args.tabID; 1.7594 + 1.7595 + let tab = BrowserApp.getTabForId(tabID); 1.7596 + let currentURI = tab.browser.currentURI; 1.7597 + 1.7598 + url = currentURI.spec; 1.7599 + urlWithoutRef = currentURI.specIgnoringRef; 1.7600 + } else if ('url' in args) { 1.7601 + let uri = Services.io.newURI(args.url, null, null); 1.7602 + url = uri.spec; 1.7603 + urlWithoutRef = uri.specIgnoringRef; 1.7604 + } else { 1.7605 + throw new Error("Reader:Add requires a tabID or an URL as argument"); 1.7606 + } 1.7607 + 1.7608 + let sendResult = function(result, article) { 1.7609 + article = article || {}; 1.7610 + this.log("Reader:Add success=" + result + ", url=" + url + ", title=" + article.title + ", excerpt=" + article.excerpt); 1.7611 + 1.7612 + sendMessageToJava({ 1.7613 + type: "Reader:Added", 1.7614 + result: result, 1.7615 + title: article.title, 1.7616 + url: url, 1.7617 + length: article.length, 1.7618 + excerpt: article.excerpt 1.7619 + }); 1.7620 + }.bind(this); 1.7621 + 1.7622 + let handleArticle = function(article) { 1.7623 + if (!article) { 1.7624 + sendResult(this.READER_ADD_FAILED, null); 1.7625 + return; 1.7626 + } 1.7627 + 1.7628 + this.storeArticleInCache(article, function(success) { 1.7629 + let result = (success ? this.READER_ADD_SUCCESS : this.READER_ADD_FAILED); 1.7630 + sendResult(result, article); 1.7631 + }.bind(this)); 1.7632 + }.bind(this); 1.7633 + 1.7634 + this.getArticleFromCache(urlWithoutRef, function (article) { 1.7635 + // If the article is already in reading list, bail 1.7636 + if (article) { 1.7637 + sendResult(this.READER_ADD_DUPLICATE, null); 1.7638 + return; 1.7639 + } 1.7640 + 1.7641 + if (tabID != null) { 1.7642 + this.getArticleForTab(tabID, urlWithoutRef, handleArticle); 1.7643 + } else { 1.7644 + this.parseDocumentFromURL(urlWithoutRef, handleArticle); 1.7645 + } 1.7646 + }.bind(this)); 1.7647 + break; 1.7648 + } 1.7649 + 1.7650 + case "Reader:Remove": { 1.7651 + let url = aData; 1.7652 + this.removeArticleFromCache(url, function(success) { 1.7653 + this.log("Reader:Remove success=" + success + ", url=" + url); 1.7654 + 1.7655 + if (success) { 1.7656 + sendMessageToJava({ 1.7657 + type: "Reader:Removed", 1.7658 + url: url 1.7659 + }); 1.7660 + } 1.7661 + }.bind(this)); 1.7662 + break; 1.7663 + } 1.7664 + 1.7665 + case "nsPref:changed": { 1.7666 + if (aData.startsWith("reader.parse-on-load.")) { 1.7667 + this.isEnabledForParseOnLoad = this.getStateForParseOnLoad(); 1.7668 + } 1.7669 + break; 1.7670 + } 1.7671 + } 1.7672 + }, 1.7673 + 1.7674 + getStateForParseOnLoad: function Reader_getStateForParseOnLoad() { 1.7675 + let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled"); 1.7676 + let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled"); 1.7677 + // For low-memory devices, don't allow reader mode since it takes up a lot of memory. 1.7678 + // See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details. 1.7679 + return isForceEnabled || (isEnabled && !BrowserApp.isOnLowMemoryPlatform); 1.7680 + }, 1.7681 + 1.7682 + parseDocumentFromURL: function Reader_parseDocumentFromURL(url, callback) { 1.7683 + // If there's an on-going request for the same URL, simply append one 1.7684 + // more callback to it to be called when the request is done. 1.7685 + if (url in this._requests) { 1.7686 + let request = this._requests[url]; 1.7687 + request.callbacks.push(callback); 1.7688 + return; 1.7689 + } 1.7690 + 1.7691 + let request = { url: url, callbacks: [callback] }; 1.7692 + this._requests[url] = request; 1.7693 + 1.7694 + try { 1.7695 + this.log("parseDocumentFromURL: " + url); 1.7696 + 1.7697 + // First, try to find a cached parsed article in the DB 1.7698 + this.getArticleFromCache(url, function(article) { 1.7699 + if (article) { 1.7700 + this.log("Page found in cache, return article immediately"); 1.7701 + this._runCallbacksAndFinish(request, article); 1.7702 + return; 1.7703 + } 1.7704 + 1.7705 + if (!this._requests) { 1.7706 + this.log("Reader has been destroyed, abort"); 1.7707 + return; 1.7708 + } 1.7709 + 1.7710 + // Article hasn't been found in the cache DB, we need to 1.7711 + // download the page and parse the article out of it. 1.7712 + this._downloadAndParseDocument(url, request); 1.7713 + }.bind(this)); 1.7714 + } catch (e) { 1.7715 + this.log("Error parsing document from URL: " + e); 1.7716 + this._runCallbacksAndFinish(request, null); 1.7717 + } 1.7718 + }, 1.7719 + 1.7720 + getArticleForTab: function Reader_getArticleForTab(tabId, url, callback) { 1.7721 + let tab = BrowserApp.getTabForId(tabId); 1.7722 + if (tab) { 1.7723 + let article = tab.savedArticle; 1.7724 + if (article && article.url == url) { 1.7725 + this.log("Saved article found in tab"); 1.7726 + callback(article); 1.7727 + return; 1.7728 + } 1.7729 + } 1.7730 + 1.7731 + this.parseDocumentFromURL(url, callback); 1.7732 + }, 1.7733 + 1.7734 + parseDocumentFromTab: function(tabId, callback) { 1.7735 + try { 1.7736 + this.log("parseDocumentFromTab: " + tabId); 1.7737 + 1.7738 + let tab = BrowserApp.getTabForId(tabId); 1.7739 + let url = tab.browser.contentWindow.location.href; 1.7740 + let uri = Services.io.newURI(url, null, null); 1.7741 + 1.7742 + if (!this._shouldCheckUri(uri)) { 1.7743 + callback(null); 1.7744 + return; 1.7745 + } 1.7746 + 1.7747 + // First, try to find a cached parsed article in the DB 1.7748 + this.getArticleFromCache(url, function(article) { 1.7749 + if (article) { 1.7750 + this.log("Page found in cache, return article immediately"); 1.7751 + callback(article); 1.7752 + return; 1.7753 + } 1.7754 + 1.7755 + let doc = tab.browser.contentWindow.document; 1.7756 + this._readerParse(uri, doc, function (article) { 1.7757 + if (!article) { 1.7758 + this.log("Failed to parse page"); 1.7759 + callback(null); 1.7760 + return; 1.7761 + } 1.7762 + 1.7763 + callback(article); 1.7764 + }.bind(this)); 1.7765 + }.bind(this)); 1.7766 + } catch (e) { 1.7767 + this.log("Error parsing document from tab: " + e); 1.7768 + callback(null); 1.7769 + } 1.7770 + }, 1.7771 + 1.7772 + getArticleFromCache: function Reader_getArticleFromCache(url, callback) { 1.7773 + this._getCacheDB(function(cacheDB) { 1.7774 + if (!cacheDB) { 1.7775 + callback(false); 1.7776 + return; 1.7777 + } 1.7778 + 1.7779 + let transaction = cacheDB.transaction(cacheDB.objectStoreNames); 1.7780 + let articles = transaction.objectStore(cacheDB.objectStoreNames[0]); 1.7781 + 1.7782 + let request = articles.get(url); 1.7783 + 1.7784 + request.onerror = function(event) { 1.7785 + this.log("Error getting article from the cache DB: " + url); 1.7786 + callback(null); 1.7787 + }.bind(this); 1.7788 + 1.7789 + request.onsuccess = function(event) { 1.7790 + this.log("Got article from the cache DB: " + event.target.result); 1.7791 + callback(event.target.result); 1.7792 + }.bind(this); 1.7793 + }.bind(this)); 1.7794 + }, 1.7795 + 1.7796 + storeArticleInCache: function Reader_storeArticleInCache(article, callback) { 1.7797 + this._getCacheDB(function(cacheDB) { 1.7798 + if (!cacheDB) { 1.7799 + callback(false); 1.7800 + return; 1.7801 + } 1.7802 + 1.7803 + let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite"); 1.7804 + let articles = transaction.objectStore(cacheDB.objectStoreNames[0]); 1.7805 + 1.7806 + let request = articles.add(article); 1.7807 + 1.7808 + request.onerror = function(event) { 1.7809 + this.log("Error storing article in the cache DB: " + article.url); 1.7810 + callback(false); 1.7811 + }.bind(this); 1.7812 + 1.7813 + request.onsuccess = function(event) { 1.7814 + this.log("Stored article in the cache DB: " + article.url); 1.7815 + callback(true); 1.7816 + }.bind(this); 1.7817 + }.bind(this)); 1.7818 + }, 1.7819 + 1.7820 + removeArticleFromCache: function Reader_removeArticleFromCache(url, callback) { 1.7821 + this._getCacheDB(function(cacheDB) { 1.7822 + if (!cacheDB) { 1.7823 + callback(false); 1.7824 + return; 1.7825 + } 1.7826 + 1.7827 + let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite"); 1.7828 + let articles = transaction.objectStore(cacheDB.objectStoreNames[0]); 1.7829 + 1.7830 + let request = articles.delete(url); 1.7831 + 1.7832 + request.onerror = function(event) { 1.7833 + this.log("Error removing article from the cache DB: " + url); 1.7834 + callback(false); 1.7835 + }.bind(this); 1.7836 + 1.7837 + request.onsuccess = function(event) { 1.7838 + this.log("Removed article from the cache DB: " + url); 1.7839 + callback(true); 1.7840 + }.bind(this); 1.7841 + }.bind(this)); 1.7842 + }, 1.7843 + 1.7844 + uninit: function Reader_uninit() { 1.7845 + Services.prefs.removeObserver("reader.parse-on-load.", this); 1.7846 + 1.7847 + Services.obs.removeObserver(this, "Reader:Add"); 1.7848 + Services.obs.removeObserver(this, "Reader:Remove"); 1.7849 + 1.7850 + let requests = this._requests; 1.7851 + for (let url in requests) { 1.7852 + let request = requests[url]; 1.7853 + if (request.browser) { 1.7854 + let browser = request.browser; 1.7855 + browser.parentNode.removeChild(browser); 1.7856 + } 1.7857 + } 1.7858 + delete this._requests; 1.7859 + 1.7860 + if (this._cacheDB) { 1.7861 + this._cacheDB.close(); 1.7862 + delete this._cacheDB; 1.7863 + } 1.7864 + }, 1.7865 + 1.7866 + log: function(msg) { 1.7867 + if (this.DEBUG) 1.7868 + dump("Reader: " + msg); 1.7869 + }, 1.7870 + 1.7871 + _shouldCheckUri: function Reader_shouldCheckUri(uri) { 1.7872 + if ((uri.prePath + "/") === uri.spec) { 1.7873 + this.log("Not parsing home page: " + uri.spec); 1.7874 + return false; 1.7875 + } 1.7876 + 1.7877 + if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) { 1.7878 + this.log("Not parsing URI scheme: " + uri.scheme); 1.7879 + return false; 1.7880 + } 1.7881 + 1.7882 + return true; 1.7883 + }, 1.7884 + 1.7885 + _readerParse: function Reader_readerParse(uri, doc, callback) { 1.7886 + let numTags = doc.getElementsByTagName("*").length; 1.7887 + if (numTags > this.MAX_ELEMS_TO_PARSE) { 1.7888 + this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found"); 1.7889 + callback(null); 1.7890 + return; 1.7891 + } 1.7892 + 1.7893 + let worker = new ChromeWorker("readerWorker.js"); 1.7894 + worker.onmessage = function (evt) { 1.7895 + let article = evt.data; 1.7896 + 1.7897 + // Append URL to the article data. specIgnoringRef will ignore any hash 1.7898 + // in the URL. 1.7899 + if (article) { 1.7900 + article.url = uri.specIgnoringRef; 1.7901 + let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks; 1.7902 + article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils) 1.7903 + .convertToPlainText(article.title, flags, 0); 1.7904 + } 1.7905 + 1.7906 + callback(article); 1.7907 + }; 1.7908 + 1.7909 + try { 1.7910 + worker.postMessage({ 1.7911 + uri: { 1.7912 + spec: uri.spec, 1.7913 + host: uri.host, 1.7914 + prePath: uri.prePath, 1.7915 + scheme: uri.scheme, 1.7916 + pathBase: Services.io.newURI(".", null, uri).spec 1.7917 + }, 1.7918 + doc: new XMLSerializer().serializeToString(doc) 1.7919 + }); 1.7920 + } catch (e) { 1.7921 + dump("Reader: could not build Readability arguments: " + e); 1.7922 + callback(null); 1.7923 + } 1.7924 + }, 1.7925 + 1.7926 + _runCallbacksAndFinish: function Reader_runCallbacksAndFinish(request, result) { 1.7927 + delete this._requests[request.url]; 1.7928 + 1.7929 + request.callbacks.forEach(function(callback) { 1.7930 + callback(result); 1.7931 + }); 1.7932 + }, 1.7933 + 1.7934 + _downloadDocument: function Reader_downloadDocument(url, callback) { 1.7935 + // We want to parse those arbitrary pages safely, outside the privileged 1.7936 + // context of chrome. We create a hidden browser element to fetch the 1.7937 + // loaded page's document object then discard the browser element. 1.7938 + 1.7939 + let browser = document.createElement("browser"); 1.7940 + browser.setAttribute("type", "content"); 1.7941 + browser.setAttribute("collapsed", "true"); 1.7942 + browser.setAttribute("disablehistory", "true"); 1.7943 + 1.7944 + document.documentElement.appendChild(browser); 1.7945 + browser.stop(); 1.7946 + 1.7947 + browser.webNavigation.allowAuth = false; 1.7948 + browser.webNavigation.allowImages = false; 1.7949 + browser.webNavigation.allowJavascript = false; 1.7950 + browser.webNavigation.allowMetaRedirects = true; 1.7951 + browser.webNavigation.allowPlugins = false; 1.7952 + 1.7953 + browser.addEventListener("DOMContentLoaded", function (event) { 1.7954 + let doc = event.originalTarget; 1.7955 + 1.7956 + // ignore on frames and other documents 1.7957 + if (doc != browser.contentDocument) 1.7958 + return; 1.7959 + 1.7960 + this.log("Done loading: " + doc); 1.7961 + if (doc.location.href == "about:blank") { 1.7962 + callback(null); 1.7963 + 1.7964 + // Request has finished with error, remove browser element 1.7965 + browser.parentNode.removeChild(browser); 1.7966 + return; 1.7967 + } 1.7968 + 1.7969 + callback(doc); 1.7970 + }.bind(this)); 1.7971 + 1.7972 + browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, 1.7973 + null, null, null); 1.7974 + 1.7975 + return browser; 1.7976 + }, 1.7977 + 1.7978 + _downloadAndParseDocument: function Reader_downloadAndParseDocument(url, request) { 1.7979 + try { 1.7980 + this.log("Needs to fetch page, creating request: " + url); 1.7981 + 1.7982 + request.browser = this._downloadDocument(url, function(doc) { 1.7983 + this.log("Finished loading page: " + doc); 1.7984 + 1.7985 + if (!doc) { 1.7986 + this.log("Error loading page"); 1.7987 + this._runCallbacksAndFinish(request, null); 1.7988 + return; 1.7989 + } 1.7990 + 1.7991 + this.log("Parsing response with Readability"); 1.7992 + 1.7993 + let uri = Services.io.newURI(url, null, null); 1.7994 + this._readerParse(uri, doc, function (article) { 1.7995 + // Delete reference to the browser element as we've finished parsing. 1.7996 + let browser = request.browser; 1.7997 + if (browser) { 1.7998 + browser.parentNode.removeChild(browser); 1.7999 + delete request.browser; 1.8000 + } 1.8001 + 1.8002 + if (!article) { 1.8003 + this.log("Failed to parse page"); 1.8004 + this._runCallbacksAndFinish(request, null); 1.8005 + return; 1.8006 + } 1.8007 + 1.8008 + this.log("Parsing has been successful"); 1.8009 + 1.8010 + this._runCallbacksAndFinish(request, article); 1.8011 + }.bind(this)); 1.8012 + }.bind(this)); 1.8013 + } catch (e) { 1.8014 + this.log("Error downloading and parsing document: " + e); 1.8015 + this._runCallbacksAndFinish(request, null); 1.8016 + } 1.8017 + }, 1.8018 + 1.8019 + _getCacheDB: function Reader_getCacheDB(callback) { 1.8020 + if (this._cacheDB) { 1.8021 + callback(this._cacheDB); 1.8022 + return; 1.8023 + } 1.8024 + 1.8025 + let request = window.indexedDB.open("about:reader", this.DB_VERSION); 1.8026 + 1.8027 + request.onerror = function(event) { 1.8028 + this.log("Error connecting to the cache DB"); 1.8029 + this._cacheDB = null; 1.8030 + callback(null); 1.8031 + }.bind(this); 1.8032 + 1.8033 + request.onsuccess = function(event) { 1.8034 + this.log("Successfully connected to the cache DB"); 1.8035 + this._cacheDB = event.target.result; 1.8036 + callback(this._cacheDB); 1.8037 + }.bind(this); 1.8038 + 1.8039 + request.onupgradeneeded = function(event) { 1.8040 + this.log("Database schema upgrade from " + 1.8041 + event.oldVersion + " to " + event.newVersion); 1.8042 + 1.8043 + let cacheDB = event.target.result; 1.8044 + 1.8045 + // Create the articles object store 1.8046 + this.log("Creating articles object store"); 1.8047 + cacheDB.createObjectStore("articles", { keyPath: "url" }); 1.8048 + 1.8049 + this.log("Database upgrade done: " + this.DB_VERSION); 1.8050 + }.bind(this); 1.8051 + } 1.8052 +}; 1.8053 + 1.8054 +var ExternalApps = { 1.8055 + _contextMenuId: null, 1.8056 + 1.8057 + // extend _getLink to pickup html5 media links. 1.8058 + _getMediaLink: function(aElement) { 1.8059 + let uri = NativeWindow.contextmenus._getLink(aElement); 1.8060 + if (uri == null && aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement)) { 1.8061 + try { 1.8062 + let mediaSrc = aElement.currentSrc || aElement.src; 1.8063 + uri = ContentAreaUtils.makeURI(mediaSrc, null, null); 1.8064 + } catch (e) {} 1.8065 + } 1.8066 + return uri; 1.8067 + }, 1.8068 + 1.8069 + init: function helper_init() { 1.8070 + this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) { 1.8071 + let uri = null; 1.8072 + var node = aElement; 1.8073 + while (node && !uri) { 1.8074 + uri = ExternalApps._getMediaLink(node); 1.8075 + node = node.parentNode; 1.8076 + } 1.8077 + let apps = []; 1.8078 + if (uri) 1.8079 + apps = HelperApps.getAppsForUri(uri); 1.8080 + 1.8081 + return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) : 1.8082 + Strings.browser.GetStringFromName("helperapps.openWithList2"); 1.8083 + }, this.filter, this.openExternal); 1.8084 + }, 1.8085 + 1.8086 + uninit: function helper_uninit() { 1.8087 + if (this._contextMenuId !== null) { 1.8088 + NativeWindow.contextmenus.remove(this._contextMenuId); 1.8089 + } 1.8090 + this._contextMenuId = null; 1.8091 + }, 1.8092 + 1.8093 + filter: { 1.8094 + matches: function(aElement) { 1.8095 + let uri = ExternalApps._getMediaLink(aElement); 1.8096 + let apps = []; 1.8097 + if (uri) { 1.8098 + apps = HelperApps.getAppsForUri(uri); 1.8099 + } 1.8100 + return apps.length > 0; 1.8101 + } 1.8102 + }, 1.8103 + 1.8104 + openExternal: function(aElement) { 1.8105 + let uri = ExternalApps._getMediaLink(aElement); 1.8106 + HelperApps.launchUri(uri); 1.8107 + }, 1.8108 + 1.8109 + shouldCheckUri: function(uri) { 1.8110 + if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) { 1.8111 + return false; 1.8112 + } 1.8113 + 1.8114 + return true; 1.8115 + }, 1.8116 + 1.8117 + updatePageAction: function updatePageAction(uri) { 1.8118 + HelperApps.getAppsForUri(uri, { filterHttp: true }, (apps) => { 1.8119 + this.clearPageAction(); 1.8120 + if (apps.length > 0) 1.8121 + this._setUriForPageAction(uri, apps); 1.8122 + }); 1.8123 + }, 1.8124 + 1.8125 + updatePageActionUri: function updatePageActionUri(uri) { 1.8126 + this._pageActionUri = uri; 1.8127 + }, 1.8128 + 1.8129 + _setUriForPageAction: function setUriForPageAction(uri, apps) { 1.8130 + this.updatePageActionUri(uri); 1.8131 + 1.8132 + // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered. 1.8133 + if (this._pageActionId != undefined) 1.8134 + return; 1.8135 + 1.8136 + this._pageActionId = NativeWindow.pageactions.add({ 1.8137 + title: Strings.browser.GetStringFromName("openInApp.pageAction"), 1.8138 + icon: "drawable://icon_openinapp", 1.8139 + 1.8140 + clickCallback: () => { 1.8141 + // Create a relative timestamp for telemetry 1.8142 + let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized; 1.8143 + UITelemetry.addEvent("launch.1", "pageaction", uptime, "helper"); 1.8144 + 1.8145 + if (apps.length > 1) { 1.8146 + // Use the HelperApps prompt here to filter out any Http handlers 1.8147 + HelperApps.prompt(apps, { 1.8148 + title: Strings.browser.GetStringFromName("openInApp.pageAction"), 1.8149 + buttons: [ 1.8150 + Strings.browser.GetStringFromName("openInApp.ok"), 1.8151 + Strings.browser.GetStringFromName("openInApp.cancel") 1.8152 + ] 1.8153 + }, (result) => { 1.8154 + if (result.button != 0) { 1.8155 + return; 1.8156 + } 1.8157 + apps[result.icongrid0].launch(this._pageActionUri); 1.8158 + }); 1.8159 + } else { 1.8160 + apps[0].launch(this._pageActionUri); 1.8161 + } 1.8162 + } 1.8163 + }); 1.8164 + }, 1.8165 + 1.8166 + clearPageAction: function clearPageAction() { 1.8167 + if(!this._pageActionId) 1.8168 + return; 1.8169 + 1.8170 + NativeWindow.pageactions.remove(this._pageActionId); 1.8171 + delete this._pageActionId; 1.8172 + }, 1.8173 +}; 1.8174 + 1.8175 +var Distribution = { 1.8176 + // File used to store campaign data 1.8177 + _file: null, 1.8178 + 1.8179 + init: function dc_init() { 1.8180 + Services.obs.addObserver(this, "Distribution:Set", false); 1.8181 + Services.obs.addObserver(this, "prefservice:after-app-defaults", false); 1.8182 + Services.obs.addObserver(this, "Campaign:Set", false); 1.8183 + 1.8184 + // Look for file outside the APK: 1.8185 + // /data/data/org.mozilla.xxx/distribution.json 1.8186 + this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile); 1.8187 + this._file.append("distribution.json"); 1.8188 + this.readJSON(this._file, this.update); 1.8189 + }, 1.8190 + 1.8191 + uninit: function dc_uninit() { 1.8192 + Services.obs.removeObserver(this, "Distribution:Set"); 1.8193 + Services.obs.removeObserver(this, "prefservice:after-app-defaults"); 1.8194 + Services.obs.removeObserver(this, "Campaign:Set"); 1.8195 + }, 1.8196 + 1.8197 + observe: function dc_observe(aSubject, aTopic, aData) { 1.8198 + switch (aTopic) { 1.8199 + case "Distribution:Set": 1.8200 + // Reload the default prefs so we can observe "prefservice:after-app-defaults" 1.8201 + Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null); 1.8202 + break; 1.8203 + 1.8204 + case "prefservice:after-app-defaults": 1.8205 + this.getPrefs(); 1.8206 + break; 1.8207 + 1.8208 + case "Campaign:Set": { 1.8209 + // Update the prefs for this session 1.8210 + try { 1.8211 + this.update(JSON.parse(aData)); 1.8212 + } catch (ex) { 1.8213 + Cu.reportError("Distribution: Could not parse JSON: " + ex); 1.8214 + return; 1.8215 + } 1.8216 + 1.8217 + // Asynchronously copy the data to the file. 1.8218 + let array = new TextEncoder().encode(aData); 1.8219 + OS.File.writeAtomic(this._file.path, array, { tmpPath: this._file.path + ".tmp" }); 1.8220 + break; 1.8221 + } 1.8222 + } 1.8223 + }, 1.8224 + 1.8225 + update: function dc_update(aData) { 1.8226 + // Force the distribution preferences on the default branch 1.8227 + let defaults = Services.prefs.getDefaultBranch(null); 1.8228 + defaults.setCharPref("distribution.id", aData.id); 1.8229 + defaults.setCharPref("distribution.version", aData.version); 1.8230 + }, 1.8231 + 1.8232 + getPrefs: function dc_getPrefs() { 1.8233 + // Get the distribution directory, and bail if it doesn't exist. 1.8234 + let file = FileUtils.getDir("XREAppDist", [], false); 1.8235 + if (!file.exists()) 1.8236 + return; 1.8237 + 1.8238 + file.append("preferences.json"); 1.8239 + this.readJSON(file, this.applyPrefs); 1.8240 + }, 1.8241 + 1.8242 + applyPrefs: function dc_applyPrefs(aData) { 1.8243 + // Check for required Global preferences 1.8244 + let global = aData["Global"]; 1.8245 + if (!(global && global["id"] && global["version"] && global["about"])) { 1.8246 + Cu.reportError("Distribution: missing or incomplete Global preferences"); 1.8247 + return; 1.8248 + } 1.8249 + 1.8250 + // Force the distribution preferences on the default branch 1.8251 + let defaults = Services.prefs.getDefaultBranch(null); 1.8252 + defaults.setCharPref("distribution.id", global["id"]); 1.8253 + defaults.setCharPref("distribution.version", global["version"]); 1.8254 + 1.8255 + let locale = Services.prefs.getCharPref("general.useragent.locale"); 1.8256 + let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); 1.8257 + aboutString.data = global["about." + locale] || global["about"]; 1.8258 + defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString); 1.8259 + 1.8260 + let prefs = aData["Preferences"]; 1.8261 + for (let key in prefs) { 1.8262 + try { 1.8263 + let value = prefs[key]; 1.8264 + switch (typeof value) { 1.8265 + case "boolean": 1.8266 + defaults.setBoolPref(key, value); 1.8267 + break; 1.8268 + case "number": 1.8269 + defaults.setIntPref(key, value); 1.8270 + break; 1.8271 + case "string": 1.8272 + case "undefined": 1.8273 + defaults.setCharPref(key, value); 1.8274 + break; 1.8275 + } 1.8276 + } catch (e) { /* ignore bad prefs and move on */ } 1.8277 + } 1.8278 + 1.8279 + // Apply a lightweight theme if necessary 1.8280 + if (prefs["lightweightThemes.isThemeSelected"]) 1.8281 + Services.obs.notifyObservers(null, "lightweight-theme-apply", ""); 1.8282 + 1.8283 + let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString); 1.8284 + let localizeablePrefs = aData["LocalizablePreferences"]; 1.8285 + for (let key in localizeablePrefs) { 1.8286 + try { 1.8287 + let value = localizeablePrefs[key]; 1.8288 + value = value.replace("%LOCALE%", locale, "g"); 1.8289 + localizedString.data = "data:text/plain," + key + "=" + value; 1.8290 + defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString); 1.8291 + } catch (e) { /* ignore bad prefs and move on */ } 1.8292 + } 1.8293 + 1.8294 + let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale]; 1.8295 + for (let key in localizeablePrefsOverrides) { 1.8296 + try { 1.8297 + let value = localizeablePrefsOverrides[key]; 1.8298 + localizedString.data = "data:text/plain," + key + "=" + value; 1.8299 + defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString); 1.8300 + } catch (e) { /* ignore bad prefs and move on */ } 1.8301 + } 1.8302 + 1.8303 + sendMessageToJava({ type: "Distribution:Set:OK" }); 1.8304 + }, 1.8305 + 1.8306 + // aFile is an nsIFile 1.8307 + // aCallback takes the parsed JSON object as a parameter 1.8308 + readJSON: function dc_readJSON(aFile, aCallback) { 1.8309 + Task.spawn(function() { 1.8310 + let bytes = yield OS.File.read(aFile.path); 1.8311 + let raw = new TextDecoder().decode(bytes) || ""; 1.8312 + 1.8313 + try { 1.8314 + aCallback(JSON.parse(raw)); 1.8315 + } catch (e) { 1.8316 + Cu.reportError("Distribution: Could not parse JSON: " + e); 1.8317 + } 1.8318 + }).then(null, function onError(reason) { 1.8319 + if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) { 1.8320 + Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file"); 1.8321 + } 1.8322 + }); 1.8323 + } 1.8324 +}; 1.8325 + 1.8326 +var Tabs = { 1.8327 + _enableTabExpiration: false, 1.8328 + _domains: new Set(), 1.8329 + 1.8330 + init: function() { 1.8331 + // On low-memory platforms, always allow tab expiration. On high-mem 1.8332 + // platforms, allow it to be turned on once we hit a low-mem situation. 1.8333 + if (BrowserApp.isOnLowMemoryPlatform) { 1.8334 + this._enableTabExpiration = true; 1.8335 + } else { 1.8336 + Services.obs.addObserver(this, "memory-pressure", false); 1.8337 + } 1.8338 + 1.8339 + Services.obs.addObserver(this, "Session:Prefetch", false); 1.8340 + 1.8341 + BrowserApp.deck.addEventListener("pageshow", this, false); 1.8342 + BrowserApp.deck.addEventListener("TabOpen", this, false); 1.8343 + }, 1.8344 + 1.8345 + uninit: function() { 1.8346 + if (!this._enableTabExpiration) { 1.8347 + // If _enableTabExpiration is true then we won't have this 1.8348 + // observer registered any more. 1.8349 + Services.obs.removeObserver(this, "memory-pressure"); 1.8350 + } 1.8351 + 1.8352 + Services.obs.removeObserver(this, "Session:Prefetch"); 1.8353 + 1.8354 + BrowserApp.deck.removeEventListener("pageshow", this); 1.8355 + BrowserApp.deck.removeEventListener("TabOpen", this); 1.8356 + }, 1.8357 + 1.8358 + observe: function(aSubject, aTopic, aData) { 1.8359 + switch (aTopic) { 1.8360 + case "memory-pressure": 1.8361 + if (aData != "heap-minimize") { 1.8362 + // We received a low-memory related notification. This will enable 1.8363 + // expirations. 1.8364 + this._enableTabExpiration = true; 1.8365 + Services.obs.removeObserver(this, "memory-pressure"); 1.8366 + } else { 1.8367 + // Use "heap-minimize" as a trigger to expire the most stale tab. 1.8368 + this.expireLruTab(); 1.8369 + } 1.8370 + break; 1.8371 + case "Session:Prefetch": 1.8372 + if (aData) { 1.8373 + let uri = Services.io.newURI(aData, null, null); 1.8374 + if (uri && !this._domains.has(uri.host)) { 1.8375 + try { 1.8376 + Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null); 1.8377 + this._domains.add(uri.host); 1.8378 + } catch (e) {} 1.8379 + } 1.8380 + } 1.8381 + break; 1.8382 + } 1.8383 + }, 1.8384 + 1.8385 + handleEvent: function(aEvent) { 1.8386 + switch (aEvent.type) { 1.8387 + case "pageshow": 1.8388 + // Clear the domain cache whenever a page get loaded into any browser. 1.8389 + this._domains.clear(); 1.8390 + break; 1.8391 + case "TabOpen": 1.8392 + // Use opening a new tab as a trigger to expire the most stale tab. 1.8393 + this.expireLruTab(); 1.8394 + break; 1.8395 + } 1.8396 + }, 1.8397 + 1.8398 + // Manage the most-recently-used list of tabs. Each tab has a timestamp 1.8399 + // associated with it that indicates when it was last touched. 1.8400 + expireLruTab: function() { 1.8401 + if (!this._enableTabExpiration) { 1.8402 + return false; 1.8403 + } 1.8404 + let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000; 1.8405 + if (expireTimeMs < 0) { 1.8406 + // This behaviour is disabled. 1.8407 + return false; 1.8408 + } 1.8409 + let tabs = BrowserApp.tabs; 1.8410 + let selected = BrowserApp.selectedTab; 1.8411 + let lruTab = null; 1.8412 + // Find the least recently used non-zombie tab. 1.8413 + for (let i = 0; i < tabs.length; i++) { 1.8414 + if (tabs[i] == selected || tabs[i].browser.__SS_restore) { 1.8415 + // This tab is selected or already a zombie, skip it. 1.8416 + continue; 1.8417 + } 1.8418 + if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) { 1.8419 + lruTab = tabs[i]; 1.8420 + } 1.8421 + } 1.8422 + // If the tab was last touched more than browser.tabs.expireTime seconds ago, 1.8423 + // zombify it. 1.8424 + if (lruTab) { 1.8425 + let tabAgeMs = Date.now() - lruTab.lastTouchedAt; 1.8426 + if (tabAgeMs > expireTimeMs) { 1.8427 + MemoryObserver.zombify(lruTab); 1.8428 + Telemetry.addData("FENNEC_TAB_EXPIRED", tabAgeMs / 1000); 1.8429 + return true; 1.8430 + } 1.8431 + } 1.8432 + return false; 1.8433 + }, 1.8434 + 1.8435 + // For debugging 1.8436 + dump: function(aPrefix) { 1.8437 + let tabs = BrowserApp.tabs; 1.8438 + for (let i = 0; i < tabs.length; i++) { 1.8439 + dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore); 1.8440 + } 1.8441 + }, 1.8442 +}; 1.8443 + 1.8444 +function ContextMenuItem(args) { 1.8445 + this.id = uuidgen.generateUUID().toString(); 1.8446 + this.args = args; 1.8447 +} 1.8448 + 1.8449 +ContextMenuItem.prototype = { 1.8450 + get order() { 1.8451 + return this.args.order || 0; 1.8452 + }, 1.8453 + 1.8454 + matches: function(elt, x, y) { 1.8455 + return this.args.selector.matches(elt, x, y); 1.8456 + }, 1.8457 + 1.8458 + callback: function(elt) { 1.8459 + this.args.callback(elt); 1.8460 + }, 1.8461 + 1.8462 + addVal: function(name, elt, defaultValue) { 1.8463 + if (!(name in this.args)) 1.8464 + return defaultValue; 1.8465 + 1.8466 + if (typeof this.args[name] == "function") 1.8467 + return this.args[name](elt); 1.8468 + 1.8469 + return this.args[name]; 1.8470 + }, 1.8471 + 1.8472 + getValue: function(elt) { 1.8473 + return { 1.8474 + id: this.id, 1.8475 + label: this.addVal("label", elt), 1.8476 + showAsActions: this.addVal("showAsActions", elt), 1.8477 + icon: this.addVal("icon", elt), 1.8478 + isGroup: this.addVal("isGroup", elt, false), 1.8479 + inGroup: this.addVal("inGroup", elt, false), 1.8480 + disabled: this.addVal("disabled", elt, false), 1.8481 + selected: this.addVal("selected", elt, false), 1.8482 + isParent: this.addVal("isParent", elt, false), 1.8483 + }; 1.8484 + } 1.8485 +} 1.8486 + 1.8487 +function HTMLContextMenuItem(elt, target) { 1.8488 + ContextMenuItem.call(this, { }); 1.8489 + 1.8490 + this.menuElementRef = Cu.getWeakReference(elt); 1.8491 + this.targetElementRef = Cu.getWeakReference(target); 1.8492 +} 1.8493 + 1.8494 +HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, { 1.8495 + order: { 1.8496 + value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER 1.8497 + }, 1.8498 + 1.8499 + matches: { 1.8500 + value: function(target) { 1.8501 + let t = this.targetElementRef.get(); 1.8502 + return t === target; 1.8503 + }, 1.8504 + }, 1.8505 + 1.8506 + callback: { 1.8507 + value: function(target) { 1.8508 + let elt = this.menuElementRef.get(); 1.8509 + if (!elt) { 1.8510 + return; 1.8511 + } 1.8512 + 1.8513 + // If this is a menu item, show a new context menu with the submenu in it 1.8514 + if (elt instanceof Ci.nsIDOMHTMLMenuElement) { 1.8515 + try { 1.8516 + NativeWindow.contextmenus.menus = {}; 1.8517 + 1.8518 + let elt = this.menuElementRef.get(); 1.8519 + let target = this.targetElementRef.get(); 1.8520 + if (!elt) { 1.8521 + return; 1.8522 + } 1.8523 + 1.8524 + var items = NativeWindow.contextmenus._getHTMLContextMenuItemsForMenu(elt, target); 1.8525 + // This menu will always only have one context, but we still make sure its the "right" one. 1.8526 + var context = NativeWindow.contextmenus._getContextType(target); 1.8527 + if (items.length > 0) { 1.8528 + NativeWindow.contextmenus._addMenuItems(items, context); 1.8529 + } 1.8530 + 1.8531 + } catch(ex) { 1.8532 + Cu.reportError(ex); 1.8533 + } 1.8534 + } else { 1.8535 + // otherwise just click the menu item 1.8536 + elt.click(); 1.8537 + } 1.8538 + }, 1.8539 + }, 1.8540 + 1.8541 + getValue: { 1.8542 + value: function(target) { 1.8543 + let elt = this.menuElementRef.get(); 1.8544 + if (!elt) { 1.8545 + return null; 1.8546 + } 1.8547 + 1.8548 + if (elt.hasAttribute("hidden")) { 1.8549 + return null; 1.8550 + } 1.8551 + 1.8552 + return { 1.8553 + id: this.id, 1.8554 + icon: elt.icon, 1.8555 + label: elt.label, 1.8556 + disabled: elt.disabled, 1.8557 + menu: elt instanceof Ci.nsIDOMHTMLMenuElement 1.8558 + }; 1.8559 + } 1.8560 + }, 1.8561 +});