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