mobile/android/chrome/content/browser.js

branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
equal deleted inserted replaced
-1:000000000000 0:791c552c0427
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";
7
8 let Cc = Components.classes;
9 let Ci = Components.interfaces;
10 let Cu = Components.utils;
11 let Cr = Components.results;
12
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");
22
23 #ifdef ACCESSIBILITY
24 Cu.import("resource://gre/modules/accessibility/AccessFu.jsm");
25 #endif
26
27 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
28 "resource://gre/modules/PluralForm.jsm");
29
30 XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava",
31 "resource://gre/modules/Messaging.jsm");
32
33 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
34 "resource://gre/modules/devtools/dbg-server.jsm");
35
36 XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
37 "resource://gre/modules/UserAgentOverrides.jsm");
38
39 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
40 "resource://gre/modules/LoginManagerContent.jsm");
41
42 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
43 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
44
45 #ifdef MOZ_SAFE_BROWSING
46 XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
47 "resource://gre/modules/SafeBrowsing.jsm");
48 #endif
49
50 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
51 "resource://gre/modules/PrivateBrowsingUtils.jsm");
52
53 XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
54 "resource://gre/modules/Sanitizer.jsm");
55
56 XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
57 "resource://gre/modules/Prompt.jsm");
58
59 XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
60 "resource://gre/modules/HelperApps.jsm");
61
62 XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
63 "resource://gre/modules/SSLExceptions.jsm");
64
65 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
66 "resource://gre/modules/FormHistory.jsm");
67
68 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
69 "@mozilla.org/uuid-generator;1",
70 "nsIUUIDGenerator");
71
72 XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
73 "resource://gre/modules/SimpleServiceDiscovery.jsm");
74
75 #ifdef NIGHTLY_BUILD
76 XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
77 "resource://shumway/ShumwayUtils.jsm");
78 #endif
79
80 #ifdef MOZ_ANDROID_SYNTHAPKS
81 XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
82 "resource://gre/modules/WebappManager.jsm");
83 #endif
84
85 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
86 "resource://gre/modules/CharsetMenu.jsm");
87
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 });
107
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 });
132
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 });
146
147 XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
148 "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
149
150 XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
151 "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
152
153 XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
154 "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
155
156 #ifdef MOZ_WEBRTC
157 XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
158 "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService");
159 #endif
160
161 const kStateActive = 0x00000001; // :active pseudoclass for elements
162
163 const kXLinkNamespace = "http://www.w3.org/1999/xlink";
164
165 const kDefaultCSSViewportWidth = 980;
166 const kDefaultCSSViewportHeight = 480;
167
168 const kViewportRemeasureThrottle = 500;
169
170 const kDoNotTrackPrefState = Object.freeze({
171 NO_PREF: "0",
172 DISALLOW_TRACKING: "1",
173 ALLOW_TRACKING: "2",
174 });
175
176 function dump(a) {
177 Services.console.logStringMessage(a);
178 }
179
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);
185
186 let range = null;
187 if (BrowserApp.selectedTab._mReflozPoint) {
188 range = BrowserApp.selectedTab._mReflozPoint.range;
189 }
190
191 try {
192 docViewer.pausePainting();
193 docViewer.changeMaxLineBoxWidth(aWidth);
194
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 }
207
208 function fuzzyEquals(a, b) {
209 return (Math.abs(a - b) < 1e-6);
210 }
211
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 }
219
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
225
226 XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
227 let ContentAreaUtils = {};
228 Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
229 return ContentAreaUtils;
230 });
231
232 XPCOMUtils.defineLazyModuleGetter(this, "Rect",
233 "resource://gre/modules/Geometry.jsm");
234
235 function resolveGeckoURI(aURI) {
236 if (!aURI)
237 throw "Can't resolve an empty uri";
238
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 }
248
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 });
262
263 const kFormHelperModeDisabled = 0;
264 const kFormHelperModeEnabled = 1;
265 const kFormHelperModeDynamic = 2; // disabled on tablets
266
267 var BrowserApp = {
268 _tabs: [],
269 _selectedTab: null,
270 _prefObservers: [],
271 isGuest: false,
272
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 },
278
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 },
284
285 deck: null,
286
287 startup: function startup() {
288 window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
289 dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
290
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);
299
300 BrowserEventHandler.init();
301 ViewportHandler.init();
302
303 Services.androidBridge.browserApp = this;
304
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);
338
339 function showFullScreenWarning() {
340 NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
341 }
342
343 window.addEventListener("fullscreen", function() {
344 sendMessageToJava({
345 type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide"
346 });
347 }, false);
348
349 window.addEventListener("mozfullscreenchange", function() {
350 sendMessageToJava({
351 type: document.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop"
352 });
353
354 if (document.mozFullScreen)
355 showFullScreenWarning();
356 }, false);
357
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);
361
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
391
392 // Init LoginManager
393 Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
394
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 }
409
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();
419
420 // XXX maybe we don't do this if the launch was kicked off from external
421 Services.io.offline = false;
422
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);
427
428 if (this._startupStatus)
429 this.onAppUpdated();
430
431 // Store the low-precision buffer pref
432 this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer");
433
434 // notify java that gecko has loaded
435 sendMessageToJava({ type: "Gecko:Ready" });
436
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 },
442
443 get _startupStatus() {
444 delete this._startupStatus;
445
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 }
457
458 return this._startupStatus;
459 },
460
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 },
468
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 },
475
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);
483
484 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
485 ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
486 BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
487
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 });
492
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);
498
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 });
502
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 });
507
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");
512
513 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
514 NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
515 });
516
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");
521
522 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
523 let emailAddr = NativeWindow.contextmenus._stripScheme(url);
524 NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr);
525 });
526
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");
531
532 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
533 let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
534 NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
535 });
536
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 });
552
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 });
571
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 });
590
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");
595
596 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
597 sendMessageToJava({
598 type: "Contact:Add",
599 email: url
600 });
601 });
602
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");
607
608 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
609 sendMessageToJava({
610 type: "Contact:Add",
611 phone: url
612 });
613 });
614
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");
619
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 });
628
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 });
635
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 });
642
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 });
649
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 });
668
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 });
675
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 });
682
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 });
689
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");
694
695 let url = aTarget.src;
696 NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
697 });
698
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 });
721
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");
726
727 ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
728 false, true, aTarget.ownerDocument.documentURIObject,
729 aTarget.ownerDocument);
730 });
731
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");
736
737 let src = aTarget.src;
738 sendMessageToJava({
739 type: "Image:SetAs",
740 url: src
741 });
742 });
743
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");
759
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 },
770
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", "");
775
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 },
783
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 },
806
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 }
819
820 let tab = this.selectedTab;
821 if (!tab)
822 return false;
823 return tab.contentDocumentIsDisplayed;
824 },
825
826 contentDocumentChanged: function() {
827 window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true;
828 Services.androidBridge.contentDocumentChanged();
829 },
830
831 get tabs() {
832 return this._tabs;
833 },
834
835 get selectedTab() {
836 return this._selectedTab;
837 },
838
839 set selectedTab(aTab) {
840 if (this._selectedTab == aTab)
841 return;
842
843 if (this._selectedTab) {
844 this._selectedTab.setActive(false);
845 }
846
847 this._selectedTab = aTab;
848 if (!aTab)
849 return;
850
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 },
858
859 get selectedBrowser() {
860 if (this._selectedTab)
861 return this._selectedTab.browser;
862 return null;
863 },
864
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 },
873
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 },
882
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 },
891
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 },
900
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 },
909
910 loadURI: function loadURI(aURI, aBrowser, aParams) {
911 aBrowser = aBrowser || this.selectedBrowser;
912 if (!aBrowser)
913 return;
914
915 aParams = aParams || {};
916
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;
921
922 let tab = this.getTabForBrowser(aBrowser);
923 if (tab) {
924 if ("userSearch" in aParams) tab.userSearch = aParams.userSearch;
925 }
926
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 },
940
941 addTab: function addTab(aURI, aParams) {
942 aParams = aParams || {};
943
944 let newTab = new Tab(aURI, aParams);
945 this._tabs.push(newTab);
946
947 let selected = "selected" in aParams ? aParams.selected : true;
948 if (selected)
949 this.selectedTab = newTab;
950
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 }
956
957 let evt = document.createEvent("UIEvents");
958 evt.initUIEvent("TabOpen", true, false, window, null);
959 newTab.browser.dispatchEvent(evt);
960
961 return newTab;
962 },
963
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 }
972
973 let message = {
974 type: "Tab:Close",
975 tabID: aTab.id
976 };
977 sendMessageToJava(message);
978 },
979
980 #ifdef MOZ_ANDROID_SYNTHAPKS
981 _loadWebapp: function(aMessage) {
982
983 this._initRuntime(this._startupStatus, aMessage.url, aUrl => {
984 this.manifestUrl = aMessage.url;
985 this.addTab(aUrl, { title: aMessage.name });
986 });
987 },
988 #endif
989
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;
995
996 let evt = document.createEvent("UIEvents");
997 evt.initUIEvent("TabClose", true, false, window, null);
998 aTab.browser.dispatchEvent(evt);
999
1000 aTab.destroy();
1001 this._tabs.splice(this._tabs.indexOf(aTab), 1);
1002 },
1003
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 }
1012
1013 // There's nothing to do if the tab is already selected
1014 if (aTab == this.selectedTab)
1015 return;
1016
1017 let message = {
1018 type: "Tab:Select",
1019 tabID: aTab.id
1020 };
1021 sendMessageToJava(message);
1022 },
1023
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 },
1040
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 },
1055
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;
1060
1061 let evt = document.createEvent("UIEvents");
1062 evt.initUIEvent("TabSelect", true, false, window, null);
1063 aTab.browser.dispatchEvent(evt);
1064 },
1065
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 }
1075
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;
1082
1083 Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null);
1084 }
1085
1086 window.QueryInterface(Ci.nsIDOMChromeWindow).minimize();
1087 window.close();
1088 },
1089
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";
1094
1095 let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
1096 let downloadsDir = dm.defaultDownloadsDirectory;
1097
1098 let file = downloadsDir.clone();
1099 file.append(fileName);
1100 file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8));
1101
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;
1111
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 = "";
1119
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");
1123
1124 let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
1125 .getInterface(Ci.nsIWebBrowserPrint);
1126
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);
1137
1138 webBrowserPrint.print(printSettings, download);
1139 },
1140
1141 notifyPrefObservers: function(aPref) {
1142 this._prefObservers[aPref].forEach(function(aRequestId) {
1143 this.getPreferences(aRequestId, [aPref], 1);
1144 }, this);
1145 },
1146
1147 handlePreferencesRequest: function handlePreferencesRequest(aRequestId,
1148 aPrefNames,
1149 aListen) {
1150
1151 let prefs = [];
1152
1153 for (let prefName of aPrefNames) {
1154 let pref = {
1155 name: prefName,
1156 type: "",
1157 value: null
1158 };
1159
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 }
1167
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";
1188
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 }
1197
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 }
1209
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 }
1236
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 }
1252
1253 prefs.push(pref);
1254 }
1255
1256 sendMessageToJava({
1257 type: "Preferences:Data",
1258 requestId: aRequestId, // opaque request identifier, can be any string/int/whatever
1259 preferences: prefs
1260 });
1261 },
1262
1263 setPreferences: function setPreferences(aPref) {
1264 let json = JSON.parse(aPref);
1265
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;
1272
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;
1280
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;
1301
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;
1306
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 }
1324
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 },
1340
1341 sanitize: function (aItems) {
1342 let json = JSON.parse(aItems);
1343 let success = true;
1344
1345 for (let key in json) {
1346 if (!json[key])
1347 continue;
1348
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 }
1363
1364 sendMessageToJava({
1365 type: "Sanitize:Finished",
1366 success: success
1367 });
1368 },
1369
1370 getFocusedInput: function(aBrowser, aOnlyInputElements = false) {
1371 if (!aBrowser)
1372 return null;
1373
1374 let doc = aBrowser.contentDocument;
1375 if (!doc)
1376 return null;
1377
1378 let focused = doc.activeElement;
1379 while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) {
1380 doc = focused.contentDocument;
1381 focused = doc.activeElement;
1382 }
1383
1384 if (focused instanceof HTMLInputElement && focused.mozIsTextField(false))
1385 return focused;
1386
1387 if (aOnlyInputElements)
1388 return null;
1389
1390 if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) {
1391
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 },
1402
1403 scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
1404 let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
1405 if (formHelperMode == kFormHelperModeDisabled)
1406 return;
1407
1408 let focused = this.getFocusedInput(aBrowser);
1409
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 },
1419
1420 observe: function(aSubject, aTopic, aData) {
1421 let browser = this.selectedBrowser;
1422
1423 switch (aTopic) {
1424
1425 case "Session:Back":
1426 browser.goBack();
1427 break;
1428
1429 case "Session:Forward":
1430 browser.goForward();
1431 break;
1432
1433 case "Session:Reload": {
1434 let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
1435
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 }
1448
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 }
1461
1462 case "Session:Stop":
1463 browser.stop();
1464 break;
1465
1466 case "Session:ShowHistory": {
1467 let data = JSON.parse(aData);
1468 this.showHistory(data.fromIndex, data.toIndex, data.selIndex);
1469 break;
1470 }
1471
1472 case "Tab:Load": {
1473 let data = JSON.parse(aData);
1474
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 }
1482
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 };
1494
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 }
1505
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 }
1519
1520 case "Tab:Selected":
1521 this._handleTabSelected(this.getTabForId(parseInt(aData)));
1522 break;
1523
1524 case "Tab:Closed":
1525 this._handleTabClosed(this.getTabForId(parseInt(aData)));
1526 break;
1527
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;
1533
1534 let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
1535 sendMessageToJava({
1536 type: "Search:Keyword",
1537 identifier: engine.identifier,
1538 name: engine.name,
1539 });
1540 break;
1541
1542 case "Browser:Quit":
1543 this.quit();
1544 break;
1545
1546 case "SaveAs:PDF":
1547 this.saveAsPDF(browser);
1548 break;
1549
1550 case "Preferences:Set":
1551 this.setPreferences(aData);
1552 break;
1553
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;
1559
1560 case "Sanitize:ClearData":
1561 this.sanitize(aData);
1562 break;
1563
1564 case "FullScreen:Exit":
1565 browser.contentDocument.mozCancelFullScreen();
1566 break;
1567
1568 case "Viewport:Change":
1569 if (this.isBrowserContentDocumentDisplayed())
1570 this.selectedTab.setViewport(JSON.parse(aData));
1571 break;
1572
1573 case "Viewport:Flush":
1574 this.contentDocumentChanged();
1575 break;
1576
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 }
1584
1585 case "FormHistory:Init": {
1586 // Force creation/upgrade of formhistory.sqlite
1587 FormHistory.count({});
1588 Services.obs.removeObserver(this, "FormHistory:Init");
1589 break;
1590 }
1591
1592 case "sessionstore-state-purge-complete":
1593 sendMessageToJava({ type: "Session:StatePurged" });
1594 break;
1595
1596 case "gather-telemetry":
1597 sendMessageToJava({ type: "Telemetry:Gather" });
1598 break;
1599
1600 case "Viewport:FixedMarginsChanged":
1601 gViewportMargins = JSON.parse(aData);
1602 this.selectedTab.updateViewportSize(gScreenWidth);
1603 break;
1604
1605 case "nsPref:changed":
1606 this.notifyPrefObservers(aData);
1607 break;
1608
1609 #ifdef MOZ_ANDROID_SYNTHAPKS
1610 case "webapps-runtime-install":
1611 WebappManager.install(JSON.parse(aData), aSubject);
1612 break;
1613
1614 case "webapps-runtime-install-package":
1615 WebappManager.installPackage(JSON.parse(aData), aSubject);
1616 break;
1617
1618 case "webapps-ask-install":
1619 WebappManager.askInstall(JSON.parse(aData));
1620 break;
1621
1622 case "webapps-launch": {
1623 WebappManager.launch(JSON.parse(aData));
1624 break;
1625 }
1626
1627 case "webapps-uninstall": {
1628 WebappManager.uninstall(JSON.parse(aData));
1629 break;
1630 }
1631
1632 case "Webapps:AutoInstall":
1633 WebappManager.autoInstall(JSON.parse(aData));
1634 break;
1635
1636 case "Webapps:Load":
1637 this._loadWebapp(JSON.parse(aData));
1638 break;
1639
1640 case "Webapps:AutoUninstall":
1641 WebappManager.autoUninstall(JSON.parse(aData));
1642 break;
1643 #endif
1644
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);
1649
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;
1655
1656 default:
1657 dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
1658 break;
1659
1660 }
1661 },
1662
1663 get defaultBrowserWidth() {
1664 delete this.defaultBrowserWidth;
1665 let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");
1666 return this.defaultBrowserWidth = width;
1667 },
1668
1669 // nsIAndroidBrowserApp
1670 getBrowserTab: function(tabId) {
1671 return this.getTabForId(tabId);
1672 },
1673
1674 getUITelemetryObserver: function() {
1675 return UITelemetry;
1676 },
1677
1678 getPreferences: function getPreferences(requestId, prefNames, count) {
1679 this.handlePreferencesRequest(requestId, prefNames, false);
1680 },
1681
1682 observePreferences: function observePreferences(requestId, prefNames, count) {
1683 this.handlePreferencesRequest(requestId, prefNames, true);
1684 },
1685
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 }
1695
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 },
1705
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 }
1720
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;
1727
1728 browser.gotoIndex(toIndex-selected);
1729 });
1730 },
1731 };
1732
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 },
1743
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 },
1753
1754 loadDex: function(zipFile, implClass) {
1755 sendMessageToJava({
1756 type: "Dex:Load",
1757 zipfile: zipFile,
1758 impl: implClass || "Main"
1759 });
1760 },
1761
1762 unloadDex: function(zipFile) {
1763 sendMessageToJava({
1764 type: "Dex:Unload",
1765 zipfile: zipFile
1766 });
1767 },
1768
1769 toast: {
1770 _callbacks: {},
1771 show: function(aMessage, aDuration, aOptions) {
1772 let msg = {
1773 type: "Toast:Show",
1774 message: aMessage,
1775 duration: aDuration
1776 };
1777
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 }
1788
1789 sendMessageToJava(msg);
1790 }
1791 },
1792
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 },
1818
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 }
1836
1837 options.type = "Menu:Add";
1838 options.id = this._menuId;
1839
1840 sendMessageToJava(options);
1841 this._callbacks[this._menuId] = options.callback;
1842 this._menuId++;
1843 return this._menuId - 1;
1844 },
1845
1846 remove: function(aId) {
1847 sendMessageToJava({ type: "Menu:Remove", id: aId });
1848 },
1849
1850 update: function(aId, aOptions) {
1851 if (!aOptions)
1852 return;
1853
1854 sendMessageToJava({
1855 type: "Menu:Update",
1856 id: aId,
1857 options: aOptions
1858 });
1859 }
1860 },
1861
1862 doorhanger: {
1863 _callbacks: {},
1864 _callbacksId: 0,
1865 _promptId: 0,
1866
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 }
1887
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));
1893
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 },
1906
1907 hide: function(aValue, aTabID) {
1908 sendMessageToJava({
1909 type: "Doorhanger:Remove",
1910 value: aValue,
1911 tabID: aTabID
1912 });
1913 }
1914 },
1915
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"];
1937
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);
1942
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
1955
1956 init: function() {
1957 Services.obs.addObserver(this, "Gesture:LongPress", false);
1958 },
1959
1960 uninit: function() {
1961 Services.obs.removeObserver(this, "Gesture:LongPress");
1962 },
1963
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 }
1977
1978 if (!args.label)
1979 throw "Menu items must have a name";
1980
1981 let cmItem = new ContextMenuItem(args);
1982 this.items[cmItem.id] = cmItem;
1983 return cmItem.id;
1984 },
1985
1986 remove: function(aId) {
1987 delete this.items[aId];
1988 },
1989
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 },
1999
2000 linkOpenableNonPrivateContext: {
2001 matches: function linkOpenableNonPrivateContextMatches(aElement) {
2002 let doc = aElement.ownerDocument;
2003 if (!doc || PrivateBrowsingUtils.isWindowPrivate(doc.defaultView)) {
2004 return false;
2005 }
2006
2007 return NativeWindow.contextmenus.linkOpenableContext.matches(aElement);
2008 }
2009 },
2010
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 },
2022
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 },
2034
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 },
2046
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 },
2058
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 },
2067
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 },
2076
2077 imageLocationCopyableContext: {
2078 matches: function imageLinkCopyableContextMatches(aElement) {
2079 return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI);
2080 }
2081 },
2082
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 },
2093
2094 mediaSaveableContext: {
2095 matches: function mediaSaveableContextMatches(aElement) {
2096 return (aElement instanceof HTMLVideoElement ||
2097 aElement instanceof HTMLAudioElement);
2098 }
2099 },
2100
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;
2108
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;
2117
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 },
2128
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 },
2138
2139 set _target(aTarget) {
2140 if (aTarget)
2141 this._targetRef = Cu.getWeakReference(aTarget);
2142 else this._targetRef = null;
2143 },
2144
2145 get defaultContext() {
2146 delete this.defaultContext;
2147 return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default");
2148 },
2149
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 }
2160
2161 htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
2162 htmlMenu.sendShowEvent();
2163
2164 return this._getHTMLContextMenuItemsForMenu(htmlMenu, element);
2165 },
2166
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;
2178
2179 items.push(new HTMLContextMenuItem(elt, target));
2180 }
2181
2182 return items;
2183 },
2184
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 }
2190
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 },
2201
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 },
2212
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 }
2224
2225 // Otherwise we try the nodeName
2226 try {
2227 return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase());
2228 } catch(ex) { }
2229
2230 // Fallback to the default
2231 return this.defaultContext;
2232 },
2233
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];
2239
2240 if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
2241 res.push(item);
2242 }
2243 }
2244
2245 return res;
2246 },
2247
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);
2257
2258 if (!target)
2259 return;
2260
2261 this._target = target;
2262
2263 Services.obs.notifyObservers(null, "before-build-contextmenu", "");
2264 this._buildMenu(x, y);
2265
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", "");
2277
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 },
2289
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 },
2297
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 }
2308
2309 return "";
2310 },
2311
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 },
2319
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;
2326
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 = {};
2334
2335 while (element) {
2336 let context = this._getContextType(element);
2337
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 }
2343
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 }
2349
2350 // walk up the tree and find more items to show
2351 element = element.parentNode;
2352 }
2353 },
2354
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 },
2365
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 },
2375
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);
2386
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 }
2391
2392 // If there are multiple contexts, we'll only show a tabbed ui with multiple lists
2393 return this._reformatListAsTabs(target, this.menus);
2394 },
2395
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 = [];
2406
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 });
2417
2418 contexts.forEach(context => {
2419 itemArray.push({
2420 label: context,
2421 items: this._reformatMenuItems(target, menus[context])
2422 });
2423 });
2424
2425 return itemArray;
2426 },
2427
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 = [];
2434
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);
2440
2441 // hidden menu items will return null from getValue
2442 if (val) {
2443 itemArray.push(val);
2444 break;
2445 }
2446 }
2447
2448 t = t.parentNode;
2449 }
2450 }
2451
2452 return itemArray;
2453 },
2454
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);
2458
2459 // spin through the tree looking for a title for this context menu
2460 let title = this._findTitle(target);
2461
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 }
2471
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 });
2477
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 }
2487
2488 prompt.show(this._promptDone.bind(this, target, x, y, items));
2489 },
2490
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 }
2497
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 }
2505
2506 let selectedItem = this._findMenuItem(selectedItemId);
2507 this.menus = null;
2508
2509 if (!selectedItem || !selectedItem.matches || !selectedItem.callback) {
2510 return;
2511 }
2512
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 },
2522
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 },
2529
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 },
2536
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 },
2542
2543 makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
2544 return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
2545 },
2546
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 },
2560
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 },
2570
2571 _getLinkURL: function ch_getLinkURL(aLink) {
2572 let href = aLink.href;
2573 if (href)
2574 return href;
2575
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 }
2582
2583 return this.makeURLAbsolute(aLink.baseURI, href);
2584 },
2585
2586 _copyStringToDefaultClipboard: function(aString) {
2587 let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
2588 clipboard.copyString(aString);
2589 },
2590
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 },
2595
2596 _stripScheme: function(aString) {
2597 let index = aString.indexOf(":");
2598 return aString.slice(index + 1);
2599 }
2600 }
2601 };
2602
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 },
2612
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 },
2618
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 }
2628
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 },
2645
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 },
2652
2653 _installRequest: function (event) {
2654 let node = event.target;
2655 let data = this._getThemeFromNode(node);
2656 if (!data)
2657 return;
2658
2659 if (this._isAllowed(node)) {
2660 this._install(data);
2661 return;
2662 }
2663
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 }];
2672
2673 NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id);
2674 },
2675
2676 _install: function (newLWTheme) {
2677 this._manager.currentTheme = newLWTheme;
2678 },
2679
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();
2688
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 },
2694
2695 _resetPreview: function (event) {
2696 if (!this._previewWindow ||
2697 event && !this._isAllowed(event.target))
2698 return;
2699
2700 this._previewWindow.removeEventListener("pagehide", this, true);
2701 this._previewWindow = null;
2702 BrowserApp.deck.removeEventListener("TabSelect", this, false);
2703
2704 this._manager.resetPreview();
2705 },
2706
2707 _isAllowed: function (node) {
2708 let pm = Services.perms;
2709
2710 let uri = node.ownerDocument.documentURIObject;
2711 return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
2712 },
2713
2714 _getThemeFromNode: function (node) {
2715 return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI);
2716 }
2717 };
2718
2719 var DesktopUserAgent = {
2720 DESKTOP_UA: null,
2721
2722 init: function ua_init() {
2723 Services.obs.addObserver(this, "DesktopMode:Change", false);
2724 UserAgentOverrides.addComplexOverride(this.onRequest.bind(this));
2725
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 },
2732
2733 uninit: function ua_uninit() {
2734 Services.obs.removeObserver(this, "DesktopMode:Change");
2735 },
2736
2737 onRequest: function(channel, defaultUA) {
2738 let channelWindow = this._getWindowForRequest(channel);
2739 let tab = BrowserApp.getTabForWindow(channelWindow);
2740 if (tab == null)
2741 return null;
2742
2743 return this.getUserAgentForTab(tab);
2744 },
2745
2746 getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) {
2747 let tab = BrowserApp.getTabForWindow(aWindow.top);
2748 if (tab)
2749 return this.getUserAgentForTab(tab);
2750
2751 return null;
2752 },
2753
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;
2758
2759 return null;
2760 },
2761
2762 _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) {
2763 if (aRequest && aRequest.notificationCallbacks) {
2764 try {
2765 return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
2766 } catch (ex) { }
2767 }
2768
2769 if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) {
2770 try {
2771 return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
2772 } catch (ex) { }
2773 }
2774
2775 return null;
2776 },
2777
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 },
2789
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 };
2799
2800
2801 function nsBrowserAccess() {
2802 }
2803
2804 nsBrowserAccess.prototype = {
2805 QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]),
2806
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;
2811
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 }
2824
2825 Services.io.offline = false;
2826
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 }
2834
2835 let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
2836 let pinned = false;
2837
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 }
2851
2852 let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
2853 aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
2854 aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB);
2855 let isPrivate = false;
2856
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 }
2866
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 });
2875
2876 return tab.browser;
2877 }
2878
2879 // OPEN_CURRENTWINDOW and illegal values
2880 let browser = BrowserApp.selectedBrowser;
2881 if (aURI && browser)
2882 browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
2883
2884 return browser;
2885 },
2886
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 },
2891
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 },
2896
2897 isTabContentWindow: function(aWindow) {
2898 return BrowserApp.getBrowserForWindow(aWindow) != null;
2899 },
2900
2901 get contentWindow() {
2902 return BrowserApp.selectedBrowser.contentWindow;
2903 }
2904 };
2905
2906
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;
2912
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};
2918
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;
2947
2948 this.create(aURL, aParams);
2949 }
2950
2951 Tab.prototype = {
2952 create: function(aURL, aParams) {
2953 if (this.browser)
2954 return;
2955
2956 aParams = aParams || {};
2957
2958 this.browser = document.createElement("browser");
2959 this.browser.setAttribute("type", "content-targetable");
2960 this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
2961
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;
2967
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 }
2976
2977 // Must be called after appendChild so the docshell has been created.
2978 this.setActive(false);
2979
2980 let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate;
2981 if (isPrivate) {
2982 this.browser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = true;
2983 }
2984
2985 this.browser.stop();
2986
2987 let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
2988 frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL;
2989
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) {}
2996
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;
3003
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 }
3015
3016 this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false;
3017
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);
3032
3033 this.overscrollController = new OverscrollController(this);
3034 }
3035
3036 this.browser.contentWindow.controllers.insertControllerAt(0, this.overscrollController);
3037
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);
3043
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);
3056
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);
3061
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);
3065
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;
3082
3083 // The search term the user entered to load the current URL
3084 this.userSearch = "userSearch" in aParams ? aParams.userSearch : "";
3085
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 },
3098
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 },
3108
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 },
3122
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();
3129
3130 BrowserApp.selectedTab._mReflozPositioned = false;
3131 },
3132
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;
3162
3163 let viewportWidth = gScreenWidth / zoom;
3164 let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
3165
3166 if (gReflowPending) {
3167 clearTimeout(gReflowPending);
3168 }
3169
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 },
3176
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 }
3190
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;
3195
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 }
3203
3204 this.browser.docShell.loadURI(url, flags, null, null, null);
3205 },
3206
3207 destroy: function() {
3208 if (!this.browser)
3209 return;
3210
3211 this.browser.contentWindow.controllers.removeController(this.overscrollController);
3212
3213 this.browser.removeProgressListener(this);
3214 this.browser.sessionHistory.removeSHistoryListener(this);
3215
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);
3228
3229 this.browser.removeEventListener("PluginBindingAttached", this, true, true);
3230 this.browser.removeEventListener("VideoBindingAttached", this, true, true);
3231 this.browser.removeEventListener("VideoBindingCast", this, true, true);
3232
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);
3236
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;
3242
3243 this.browser = null;
3244 this.savedArticle = null;
3245 },
3246
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;
3251
3252 this.lastTouchedAt = Date.now();
3253
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 },
3265
3266 getActive: function getActive() {
3267 return this.browser.docShellIsActive;
3268 },
3269
3270 setDisplayPort: function(aDisplayPort) {
3271 let zoom = this._zoom;
3272 let resolution = aDisplayPort.resolution;
3273 if (zoom <= 0 || resolution <= 0)
3274 return;
3275
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.
3286
3287 let element = this.browser.contentDocument.documentElement;
3288 if (!element)
3289 return;
3290
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 }
3303
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);
3312
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 };
3319
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;
3334
3335 let dpW = Math.min(pageRect.right - pageRect.left, displayPort.width * 4);
3336 let dpH = Math.min(pageRect.bottom - pageRect.top, displayPort.height * 4);
3337
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 }
3352
3353 this._oldDisplayPort = displayPort;
3354 },
3355
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;
3376
3377 let resolution = aDisplayPort.resolution;
3378
3379 // Some helper functions that simulate conversion processes in gecko
3380
3381 function cssPixelsToAppUnits(aVal) {
3382 return Math.floor((aVal * APP_UNITS_PER_CSS_PIXEL) + 0.5);
3383 }
3384
3385 function appUnitsToDevicePixels(aVal) {
3386 return aVal / APP_UNITS_PER_CSS_PIXEL * resolution;
3387 }
3388
3389 function devicePixelsToAppUnits(aVal) {
3390 return cssPixelsToAppUnits(aVal / resolution);
3391 }
3392
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;
3397
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 };
3406
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);
3411
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;
3425
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 }
3442
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.
3446
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 };
3455
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;
3463
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));
3469
3470 // Et voila!
3471 return aDisplayPort;
3472 },
3473
3474 setScrollClampingSize: function(zoom) {
3475 let viewportWidth = gScreenWidth / zoom;
3476 let viewportHeight = gScreenHeight / zoom;
3477 let screenWidth = gScreenWidth;
3478 let screenHeight = gScreenHeight;
3479
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 }
3489
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;
3496
3497 let win = this.browser.contentWindow;
3498 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).
3499 setScrollPositionClampingScrollPortSize(scrollPortWidth, scrollPortHeight);
3500 },
3501
3502 setViewport: function(aViewport) {
3503 // Transform coordinates based on zoom
3504 let x = aViewport.x / aViewport.zoom;
3505 let y = aViewport.y / aViewport.zoom;
3506
3507 this.setScrollClampingSize(aViewport.zoom);
3508
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);
3512
3513 let docViewer = null;
3514
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();
3523
3524 BrowserApp.selectedTab.performReflowOnZoom(aViewport);
3525 BrowserApp.selectedTab.probablyNeedRefloz = false;
3526 }
3527
3528 let win = this.browser.contentWindow;
3529 win.scrollTo(x, y);
3530 this.saveSessionZoom(aViewport.zoom);
3531
3532 this.userScrollPos.x = win.scrollX;
3533 this.userScrollPos.y = win.scrollY;
3534 this.setResolution(aViewport.zoom, false);
3535
3536 if (aViewport.displayPort)
3537 this.setDisplayPort(aViewport.displayPort);
3538
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;
3544
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);
3551
3552 Services.obs.notifyObservers(null, "after-viewport-change", "");
3553 if (docViewer) {
3554 docViewer.resumePainting();
3555 }
3556 },
3557
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 },
3569
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 },
3576
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;
3581
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 };
3602
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;
3606
3607 // Transform coordinates based on zoom
3608 viewport.x = Math.round(viewport.cssX * viewport.zoom);
3609 viewport.y = Math.round(viewport.cssY * viewport.zoom);
3610
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();
3615
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 }
3640
3641 return viewport;
3642 },
3643
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 },
3650
3651 updateViewportForPageSize: function() {
3652 let hasHorizontalMargins = gViewportMargins.left != 0 || gViewportMargins.right != 0;
3653 let hasVerticalMargins = gViewportMargins.top != 0 || gViewportMargins.bottom != 0;
3654
3655 if (!hasHorizontalMargins && !hasVerticalMargins) {
3656 // If there are no margins, then we don't need to do any remeasuring
3657 return;
3658 }
3659
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;
3671
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 }
3684
3685 if (remeasureNeeded) {
3686 if (!this.viewportMeasureCallback) {
3687 this.viewportMeasureCallback = setTimeout(function() {
3688 this.viewportMeasureCallback = null;
3689
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;
3695
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 },
3710
3711 handleEvent: function(aEvent) {
3712 switch (aEvent.type) {
3713 case "DOMContentLoaded": {
3714 let target = aEvent.originalTarget;
3715
3716 // ignore on frames and other documents
3717 if (target != this.browser.contentDocument)
3718 return;
3719
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 }
3731
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";
3740
3741 sendMessageToJava({
3742 type: "DOMContentLoaded",
3743 tabID: this.id,
3744 bgColor: backgroundColor,
3745 errorType: errorType
3746 });
3747
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);
3758
3759 this.browser.addEventListener("pagehide", listener, true);
3760 }
3761
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 }
3772
3773 break;
3774 }
3775
3776 case "DOMFormHasPassword": {
3777 LoginManagerContent.onFormPassword(aEvent);
3778 break;
3779 }
3780
3781 case "DOMLinkAdded": {
3782 let target = aEvent.originalTarget;
3783 if (!target.href || target.disabled)
3784 return;
3785
3786 // Ignore on frames and other documents
3787 if (target.ownerDocument != this.browser.contentDocument)
3788 return;
3789
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 }
3800
3801 if (list.indexOf("[icon]") != -1) {
3802 // We want to get the largest icon size possible for our UI.
3803 let maxSize = 0;
3804
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();
3809
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 }
3822
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");
3836
3837 if (!isFeed)
3838 return;
3839
3840 try {
3841 // urlSecurityCeck will throw if things are not OK
3842 ContentAreaUtils.urlSecurityCheck(target.href, target.ownerDocument.nodePrincipal, Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
3843
3844 if (!this.browser.feeds)
3845 this.browser.feeds = [];
3846 this.browser.feeds.push({ href: target.href, title: target.title, type: type });
3847
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();
3856
3857 // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
3858 type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
3859
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 }
3872
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 }
3883
3884 // Get favicon.
3885 let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico";
3886
3887 let newEngine = {
3888 title: target.title,
3889 url: target.href,
3890 iconURL: iconURL
3891 };
3892
3893 this.browser.engines.push(newEngine);
3894
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;
3898
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 };
3905
3906 sendMessageToJava(newEngineMessage);
3907 }
3908 }
3909 break;
3910 }
3911
3912 case "DOMTitleChanged": {
3913 if (!aEvent.isTrusted)
3914 return;
3915
3916 // ignore on frames and other documents
3917 if (aEvent.originalTarget != this.browser.contentDocument)
3918 return;
3919
3920 sendMessageToJava({
3921 type: "DOMTitleChanged",
3922 tabID: this.id,
3923 title: aEvent.target.title.substring(0, 255)
3924 });
3925 break;
3926 }
3927
3928 case "DOMWindowClose": {
3929 if (!aEvent.isTrusted)
3930 return;
3931
3932 // Find the relevant tab, and close it from Java
3933 if (this.browser.contentWindow == aEvent.target) {
3934 aEvent.preventDefault();
3935
3936 sendMessageToJava({
3937 type: "Tab:Close",
3938 tabID: this.id
3939 });
3940 }
3941 break;
3942 }
3943
3944 case "DOMWillOpenModalDialog": {
3945 if (!aEvent.isTrusted)
3946 return;
3947
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 }
3954
3955 case "DOMAutoComplete":
3956 case "blur": {
3957 LoginManagerContent.onUsernameInput(aEvent);
3958 break;
3959 }
3960
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 }
3968
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;
3975
3976 this.sendViewportUpdate(true);
3977 this.updateViewportForPageSize();
3978 break;
3979 }
3980
3981 case "PluginBindingAttached": {
3982 PluginHelper.handlePluginBindingAttached(this, aEvent);
3983 break;
3984 }
3985
3986 case "VideoBindingAttached": {
3987 CastingApps.handleVideoBindingAttached(this, aEvent);
3988 break;
3989 }
3990
3991 case "VideoBindingCast": {
3992 CastingApps.handleVideoBindingCast(this, aEvent);
3993 break;
3994 }
3995
3996 case "MozApplicationManifest": {
3997 OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView);
3998 break;
3999 }
4000
4001 case "pageshow": {
4002 // only send pageshow for the top-level document
4003 if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
4004 return;
4005
4006 sendMessageToJava({
4007 type: "Content:PageShow",
4008 tabID: this.id
4009 });
4010
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 }
4016
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 }
4026
4027 if (!Reader.isEnabledForParseOnLoad)
4028 return;
4029
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 }
4047
4048 this.savedArticle = article;
4049
4050 sendMessageToJava({
4051 type: "Content:ReaderEnabled",
4052 tabID: this.id
4053 });
4054
4055 if(this.readerActive)
4056 this.readerActive = false;
4057
4058 if(!this.readerEnabled)
4059 this.readerEnabled = true;
4060 }.bind(this));
4061 }
4062 }
4063 },
4064
4065 onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
4066 let contentWin = aWebProgress.DOMWindow;
4067 if (contentWin != contentWin.top)
4068 return;
4069
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 }
4077
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 }
4083
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;
4090
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 }
4101
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;
4105
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 },
4117
4118 onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
4119 let contentWin = aWebProgress.DOMWindow;
4120
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;
4126
4127 this._hostChanged = true;
4128
4129 let fixedURI = aLocationURI;
4130 try {
4131 fixedURI = URIFixup.createExposableURI(aLocationURI);
4132 } catch (ex) { }
4133
4134 let contentType = contentWin.document.contentType;
4135
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;
4143
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;
4156
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 }
4166
4167 // Update the page actions URI for helper apps.
4168 if (BrowserApp.selectedTab == this) {
4169 ExternalApps.updatePageActionUri(fixedURI);
4170 }
4171
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 };
4181
4182 sendMessageToJava(message);
4183
4184 // The search term is only valid for this location change event, so reset it here.
4185 this.userSearch = "";
4186
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 },
4196
4197 // Properties used to cache security state used to update the UI
4198 _state: null,
4199 _hostChanged: false, // onLocationChange will flip this bit
4200
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;
4205
4206 this._state = aState;
4207 this._hostChanged = false;
4208
4209 let identity = IdentityHandler.checkIdentity(aState, this.browser);
4210
4211 let message = {
4212 type: "Content:SecurityChange",
4213 tabID: this.id,
4214 identity: identity
4215 };
4216
4217 sendMessageToJava(message);
4218 },
4219
4220 onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
4221 },
4222
4223 onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
4224 },
4225
4226 _sendHistoryEvent: function(aMessage, aParams) {
4227 let message = {
4228 type: "SessionHistory:" + aMessage,
4229 tabID: this.id,
4230 };
4231
4232 // Restore zoom only when moving in session history, not for new page loads.
4233 this._restoreZoom = aMessage != "New";
4234
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 }
4243
4244 sendMessageToJava(message);
4245 },
4246
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 },
4254
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 },
4259
4260 restoredSessionZoom: function() {
4261 let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
4262
4263 if (this._restoreZoom && cwu.isResolutionSet) {
4264 return this._getGeckoZoom();
4265 }
4266 return null;
4267 },
4268
4269 OnHistoryNewEntry: function(aUri) {
4270 this._sendHistoryEvent("New", { url: aUri.spec });
4271 },
4272
4273 OnHistoryGoBack: function(aUri) {
4274 this._sendHistoryEvent("Back");
4275 return true;
4276 },
4277
4278 OnHistoryGoForward: function(aUri) {
4279 this._sendHistoryEvent("Forward");
4280 return true;
4281 },
4282
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 },
4288
4289 OnHistoryGotoIndex: function(aIndex, aUri) {
4290 this._sendHistoryEvent("Goto", { index: aIndex });
4291 return true;
4292 },
4293
4294 OnHistoryPurge: function(aNumEntries) {
4295 this._sendHistoryEvent("Purge", { numEntries: aNumEntries });
4296 return true;
4297 },
4298
4299 OnHistoryReplaceEntry: function(aIndex) {
4300 // we don't do anything with this, so don't propogate it
4301 // for now anyway.
4302 },
4303
4304 get metadata() {
4305 return ViewportHandler.getMetadataForDocument(this.browser.contentDocument);
4306 },
4307
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 }
4315
4316 let scaleRatio = window.devicePixelRatio;
4317
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;
4324
4325 aMetadata.isRTL = this.browser.contentDocument.documentElement.dir == "rtl";
4326
4327 ViewportHandler.setMetadataForDocument(this.browser.contentDocument, aMetadata);
4328 this.sendViewportMetadata();
4329
4330 this.updateViewportSize(gScreenWidth, aInitialLoad);
4331 },
4332
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).
4340
4341 if (this.viewportMeasureCallback) {
4342 clearTimeout(this.viewportMeasureCallback);
4343 this.viewportMeasureCallback = null;
4344 }
4345
4346 let browser = this.browser;
4347 if (!browser)
4348 return;
4349
4350 let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
4351 let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
4352 let viewportW, viewportH;
4353
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;
4361
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 }
4367
4368 let validW = viewportW > 0;
4369 let validH = viewportH > 0;
4370
4371 if (!validW)
4372 viewportW = validH ? (viewportH * (screenW / screenH)) : BrowserApp.defaultBrowserWidth;
4373 if (!validH)
4374 viewportH = viewportW * (screenH / screenW);
4375 }
4376
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);
4385
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);
4405
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 }
4417
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);
4425
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 }
4437
4438 minScale = screenW / pageWidth;
4439 }
4440 minScale = this.clampZoom(minScale);
4441 viewportH = Math.max(viewportH, screenH / minScale);
4442
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);
4450
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;
4455
4456 this.sendViewportUpdate();
4457
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 }
4468
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 },
4477
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 },
4491
4492 setBrowserSize: function(aWidth, aHeight) {
4493 if (fuzzyEquals(this.browserWidth, aWidth) && fuzzyEquals(this.browserHeight, aHeight)) {
4494 return;
4495 }
4496
4497 this.browserWidth = aWidth;
4498 this.browserHeight = aHeight;
4499
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 },
4505
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);
4509
4510 let md = this.metadata;
4511 if (!md.allowZoom)
4512 return md.defaultZoom || zoom;
4513
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 },
4520
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;
4531
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);
4541
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.
4547
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 }
4560
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");
4567
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 },
4586
4587 set readerEnabled(isReaderEnabled) {
4588 this._readerEnabled = isReaderEnabled;
4589 if (this.getActive())
4590 Reader.updatePageAction(this);
4591 },
4592
4593 get readerEnabled() {
4594 return this._readerEnabled;
4595 },
4596
4597 set readerActive(isReaderActive) {
4598 this._readerActive = isReaderActive;
4599 if (this.getActive())
4600 Reader.updatePageAction(this);
4601 },
4602
4603 get readerActive() {
4604 return this._readerActive;
4605 },
4606
4607 // nsIBrowserTab
4608 get window() {
4609 if (!this.browser)
4610 return null;
4611 return this.browser.contentWindow;
4612 },
4613
4614 get scale() {
4615 return this._zoom;
4616 },
4617
4618 QueryInterface: XPCOMUtils.generateQI([
4619 Ci.nsIWebProgressListener,
4620 Ci.nsISHistoryListener,
4621 Ci.nsIObserver,
4622 Ci.nsISupportsWeakReference,
4623 Ci.nsIBrowserTab
4624 ])
4625 };
4626
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);
4634
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);
4639
4640 SpatialNavigation.init(BrowserApp.deck, null);
4641
4642 document.addEventListener("MozMagnifyGesture", this, true);
4643
4644 Services.prefs.addObserver("browser.zoom.reflowOnZoom", this, false);
4645 this.updateReflozPref();
4646 },
4647
4648 resetMaxLineBoxWidth: function() {
4649 BrowserApp.selectedTab.probablyNeedRefloz = false;
4650
4651 if (gReflowPending) {
4652 clearTimeout(gReflowPending);
4653 }
4654
4655 let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
4656 gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
4657 reflozTimeout, 0);
4658 },
4659
4660 updateReflozPref: function() {
4661 this.mReflozPref = Services.prefs.getBoolPref("browser.zoom.reflowOnZoom");
4662 },
4663
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 },
4676
4677 _handleTouchStart: function(aEvent) {
4678 if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.touches.length > 1 || aEvent.defaultPrevented)
4679 return;
4680
4681 let closest = aEvent.target;
4682
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;
4688
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 }
4700
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;
4706
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 },
4717
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 },
4728
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;
4734
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 }
4747
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 },
4755
4756 handleUserEvent: function(aTopic, aData) {
4757 switch (aTopic) {
4758
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;
4765
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);
4770
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);
4777
4778 if (this._firstScrollEvent) {
4779 while (this._scrollableElement != null &&
4780 !this._elementCanScroll(this._scrollableElement, x, y))
4781 this._scrollableElement = this._findScrollableElement(this._scrollableElement, false);
4782
4783 let doc = BrowserApp.selectedBrowser.contentDocument;
4784 if (this._scrollableElement == null ||
4785 this._scrollableElement == doc.documentElement) {
4786 sendMessageToJava({ type: "Panning:CancelOverride" });
4787 return;
4788 }
4789
4790 this._firstScrollEvent = false;
4791 }
4792
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 }
4801
4802 break;
4803 }
4804
4805 case "Gesture:CancelTouch":
4806 this._cancelTapHighlight();
4807 break;
4808
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 }
4818
4819 // Was the element already focused before it was clicked?
4820 let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser));
4821
4822 this._sendMouseEvent("mousemove", element, x, y);
4823 this._sendMouseEvent("mousedown", element, x, y);
4824 this._sendMouseEvent("mouseup", element, x, y);
4825
4826 // If the element was previously focused, show the caret attached to it.
4827 if (isFocused)
4828 SelectionHandler.attachCaret(element);
4829
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 }
4839
4840 case"Gesture:DoubleTap":
4841 this._cancelTapHighlight();
4842 this.onDoubleTap(aData);
4843 break;
4844
4845 case "MozMagnifyGesture":
4846 this.onPinchFinish(aData);
4847 break;
4848
4849 default:
4850 dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
4851 break;
4852 }
4853 },
4854
4855 onDoubleTap: function(aData) {
4856 let data = JSON.parse(aData);
4857 let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y);
4858
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)) {
4865
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;
4871
4872 BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY,
4873 range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) };
4874
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();
4880
4881 BrowserApp.selectedTab.probablyNeedRefloz = true;
4882 }
4883
4884 if (!element) {
4885 ZoomHelper.zoomOut();
4886 return;
4887 }
4888
4889 while (element && !this._shouldZoomToElement(element))
4890 element = element.parentNode;
4891
4892 if (!element) {
4893 ZoomHelper.zoomOut();
4894 } else {
4895 ZoomHelper.zoomToElement(element, data.y);
4896 }
4897 },
4898
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 }
4917
4918 return false;
4919 },
4920
4921 onPinchFinish: function(aData) {
4922 let data = {};
4923 try {
4924 data = JSON.parse(aData);
4925 } catch(ex) {
4926 console.log(ex);
4927 return;
4928 }
4929
4930 if (BrowserEventHandler.mReflozPref &&
4931 data.zoomDelta < 0.0) {
4932 BrowserEventHandler.resetMaxLineBoxWidth();
4933 }
4934 },
4935
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 },
4946
4947 _firstScrollEvent: false,
4948
4949 _scrollableElement: null,
4950
4951 _highlightElement: null,
4952
4953 _doTapHighlight: function _doTapHighlight(aElement) {
4954 DOMUtils.setContentState(aElement, kStateActive);
4955 this._highlightElement = aElement;
4956 },
4957
4958 _cancelTapHighlight: function _cancelTapHighlight() {
4959 if (!this._highlightElement)
4960 return;
4961
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);
4966
4967 DOMUtils.setContentState(BrowserApp.selectedBrowser.contentWindow.document.documentElement, kStateActive);
4968 this._highlightElement = null;
4969 },
4970
4971 _updateLastPosition: function(x, y, dx, dy) {
4972 this.lastX = x;
4973 this.lastY = y;
4974 this.lastTime = Date.now();
4975
4976 this.motionBuffer.push({ dx: dx, dy: dy, time: this.lastTime });
4977 },
4978
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 }
4995
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 },
5007
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 },
5017
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 },
5030
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 }
5053
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 }
5060
5061 if (!scrollable)
5062 return null;
5063
5064 return elem;
5065 },
5066
5067 _scrollElementBy: function(elem, x, y) {
5068 elem.scrollTop = elem.scrollTop + y;
5069 elem.scrollLeft = elem.scrollLeft + x;
5070 },
5071
5072 _elementCanScroll: function(elem, x, y) {
5073 let scrollX = (x < 0 && elem.scrollLeft > 0)
5074 || (x > 0 && elem.scrollLeft < elem.scrollLeftMax);
5075
5076 let scrollY = (y < 0 && elem.scrollTop > 0)
5077 || (y > 0 && elem.scrollTop < elem.scrollTopMax);
5078
5079 return scrollX || scrollY;
5080 }
5081 };
5082
5083 const kReferenceDpi = 240; // standard "pixel" size used in some preferences
5084
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);
5094
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 }
5102
5103 return elem;
5104 },
5105
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);
5117
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 }
5127
5128 return elem;
5129 },
5130
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 },
5142
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 },
5153
5154 get weight() {
5155 delete this.weight;
5156 return this.weight = { "visited": Services.prefs.getIntPref("browser.ui.touch.weight.visited") };
5157 },
5158
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 */
5164
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;
5171
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);
5175
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;
5181
5182 let rect = current.getBoundingClientRect();
5183 let distance = this._computeDistanceFromRect(aX, aY, rect);
5184
5185 // increase a little bit the weight for already visited items
5186 if (current && current.mozMatchesSelector("*:visited"))
5187 distance *= (this.weight.visited / 100);
5188
5189 if (distance < threshold) {
5190 target = current;
5191 threshold = distance;
5192 }
5193 }
5194
5195 return target;
5196 },
5197
5198 isElementClickable: function isElementClickable(aElement, aUnclickableCache, aAllowBodyListeners) {
5199 const selector = "a,:link,:visited,[role=button],button,input,select,textarea";
5200
5201 let stopNode = null;
5202 if (!aAllowBodyListeners && aElement && aElement.ownerDocument)
5203 stopNode = aElement.ownerDocument.body;
5204
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 },
5219
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;
5224
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;
5233
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;
5242
5243 return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
5244 },
5245
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 },
5257
5258 getContentClientRects: function(aElement) {
5259 let offset = { x: 0, y: 0 };
5260
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 }
5271
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 },
5283
5284 getBoundingContentRect: function(aElement) {
5285 if (!aElement)
5286 return {x: 0, y: 0, w: 0, h: 0};
5287
5288 let document = aElement.ownerDocument;
5289 while (document.defaultView.frameElement)
5290 document = document.defaultView.frameElement.ownerDocument;
5291
5292 let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
5293 let scrollX = {}, scrollY = {};
5294 cwu.getScrollXY(false, scrollX, scrollY);
5295
5296 let r = aElement.getBoundingClientRect();
5297
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 }
5307
5308 return {x: r.left + scrollX.value,
5309 y: r.top + scrollY.value,
5310 w: r.width,
5311 h: r.height };
5312 }
5313 };
5314
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;
5322
5323 let target = aEvent.originalTarget;
5324 let errorDoc = target.ownerDocument;
5325
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();
5337
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_";
5358
5359 let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
5360
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"]);
5368
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"]);
5388
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);
5393
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 };
5404
5405 var FormAssistant = {
5406 QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
5407
5408 // Used to keep track of the element that corresponds to the current
5409 // autocomplete suggestions
5410 _currentInputElement: null,
5411
5412 _isBlocklisted: false,
5413
5414 // Keep track of whether or not an invalid form has been submitted
5415 _invalidSubmit: false,
5416
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);
5423
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 },
5430
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");
5437
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 },
5443
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;
5455
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;
5467
5468 let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
5469
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) {}
5477
5478 editableElement.setUserInput(aData);
5479
5480 let event = this._currentInputElement.ownerDocument.createEvent("Events");
5481 event.initEvent("DOMAutoComplete", true, true);
5482 this._currentInputElement.dispatchEvent(event);
5483 break;
5484
5485 case "FormAssist:Blocklisted":
5486 this._isBlocklisted = (aData == "true");
5487 break;
5488
5489 case "FormAssist:Hidden":
5490 this._currentInputElement = null;
5491 break;
5492 }
5493 },
5494
5495 notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
5496 if (!aInvalidElements.length)
5497 return;
5498
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;
5503
5504 this._invalidSubmit = true;
5505
5506 // Our focus listener will show the element's validation message
5507 let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
5508 currentElement.focus();
5509 },
5510
5511 handleEvent: function(aEvent) {
5512 switch (aEvent.type) {
5513 case "focus":
5514 let currentElement = aEvent.target;
5515
5516 // Only show a validation message on focus.
5517 this._showValidationMessage(currentElement);
5518 break;
5519
5520 case "click":
5521 currentElement = aEvent.target;
5522
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;
5528
5529 let checkResultsClick = hasResults => {
5530 if (!hasResults) {
5531 this._hideFormAssistPopup();
5532 }
5533 };
5534
5535 this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
5536 break;
5537
5538 case "input":
5539 currentElement = aEvent.target;
5540
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;
5546
5547 if (this._showValidationMessage(currentElement))
5548 return;
5549
5550 // If we're not showing autocomplete suggestions, hide the form assist popup
5551 this._hideFormAssistPopup();
5552 };
5553
5554 this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
5555 break;
5556
5557 // Reset invalid submit state on each pageshow
5558 case "pageshow":
5559 if (!this._invalidSubmit)
5560 return;
5561
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 },
5571
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;
5579
5580 return true;
5581 },
5582
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);
5590
5591 let resultsAvailable = function (results) {
5592 let suggestions = [];
5593 for (let i = 0; i < results.matchCount; i++) {
5594 let value = results.getValueAt(i);
5595
5596 // Do not show the value if it is the current one in the input field
5597 if (value == aSearchString)
5598 continue;
5599
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 };
5605
5606 this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
5607 aSearchString, aElement, null,
5608 resultsAvailable);
5609 },
5610
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 [];
5620
5621 let suggestions = [];
5622 let filter = !aElement.hasAttribute("mozNoFilter");
5623 let lowerFieldValue = aElement.value.toLowerCase();
5624
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);
5629
5630 let label = item.value;
5631 if (item.label)
5632 label = item.label;
5633 else if (item.text)
5634 label = item.text;
5635
5636 if (filter && !(label.toLowerCase().contains(lowerFieldValue)) )
5637 continue;
5638 suggestions.push({ label: label, value: item.value });
5639 }
5640
5641 return suggestions;
5642 },
5643
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 }
5653
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 }
5660
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);
5666
5667 // Return false if there are no suggestions to show
5668 if (!suggestions.length) {
5669 aCallback(false);
5670 return;
5671 }
5672
5673 sendMessageToJava({
5674 type: "FormAssist:AutoComplete",
5675 suggestions: suggestions,
5676 rect: ElementTouchHelper.getBoundingContentRect(aElement)
5677 });
5678
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 };
5684
5685 this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
5686 },
5687
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;
5698
5699 return true;
5700 },
5701
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;
5707
5708 sendMessageToJava({
5709 type: "FormAssist:ValidationMessage",
5710 validationMessage: aElement.validationMessage,
5711 rect: ElementTouchHelper.getBoundingContentRect(aElement)
5712 });
5713
5714 return true;
5715 },
5716
5717 _hideFormAssistPopup: function _hideFormAssistPopup() {
5718 sendMessageToJava({ type: "FormAssist:Hide" });
5719 }
5720 };
5721
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",
5729
5730 PREF_TELEMETRY_ENABLED:
5731 #ifdef MOZ_TELEMETRY_REPORTING
5732 "toolkit.telemetry.enabled",
5733 #else
5734 null,
5735 #endif
5736
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 }
5743
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 },
5752
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 }
5760
5761 AddonManager.removeAddonListener(this);
5762 },
5763
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 };
5775
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 }
5788
5789 sendMessageToJava(response);
5790 break;
5791 }
5792 },
5793
5794 MILLISECONDS_PER_DAY: 24 * 60 * 60 * 1000,
5795
5796 COPY_FIELDS: [
5797 "blocklistState",
5798 "userDisabled",
5799 "appDisabled",
5800 "version",
5801 "type",
5802 "scope",
5803 "foreignInstall",
5804 "hasBinaryComponents",
5805 ],
5806
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 ],
5814
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 },
5822
5823 _dateToDays: function (aDate) {
5824 return Math.floor(aDate.getTime() / this.MILLISECONDS_PER_DAY);
5825 },
5826
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 }
5835
5836 for (let field of this.COPY_FIELDS) {
5837 o[field] = aAddon[field];
5838 }
5839
5840 return o;
5841 },
5842
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 },
5850
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 },
5870
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 }
5888
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 }
5909
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 };
5921
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);
5926
5927 AddonManager.addInstallListener(XPInstallObserver);
5928 },
5929
5930 uninit: function xpi_uninit() {
5931 Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
5932 Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
5933
5934 AddonManager.removeInstallListener(XPInstallObserver);
5935 },
5936
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;
5948
5949 let host = null;
5950 if (installInfo.originatingURI) {
5951 host = installInfo.originatingURI.host;
5952 }
5953
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) {}
5962
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 }
5997
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 },
6011
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;
6018
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 },
6029
6030 onInstallFailed: function(aInstall) {
6031 NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short");
6032 },
6033
6034 onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {},
6035
6036 onDownloadFailed: function(aInstall) {
6037 this.onInstallFailed(aInstall);
6038 },
6039
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;
6044
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.
6054
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);
6062
6063 NativeWindow.toast.show(msg, "short");
6064 },
6065
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");
6073
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 }];
6081
6082 let message = Strings.browser.GetStringFromName("notificationRestart.normal");
6083 NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
6084 },
6085
6086 hideRestartPrompt: function() {
6087 NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
6088 }
6089 };
6090
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;
6098
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(),
6104
6105 init: function init() {
6106 addEventListener("DOMMetaAdded", this, false);
6107 Services.obs.addObserver(this, "Window:Resize", false);
6108 },
6109
6110 uninit: function uninit() {
6111 removeEventListener("DOMMetaAdded", this, false);
6112 Services.obs.removeObserver(this, "Window:Resize");
6113 },
6114
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 },
6129
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;
6137
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 },
6147
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 },
6155
6156 /**
6157 * Returns the ViewportMetadata object.
6158 */
6159 getViewportMetadata: function getViewportMetadata(aWindow) {
6160 let windowUtils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
6161
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
6165
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"));
6172
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;
6177
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);
6183
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;
6188
6189 let autoSize = true;
6190
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 }
6202
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 }
6212
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 }
6220
6221 scale = this.clamp(scale, kViewportMinScale, kViewportMaxScale);
6222 minScale = this.clamp(minScale, kViewportMinScale, kViewportMaxScale);
6223 maxScale = this.clamp(maxScale, minScale, kViewportMaxScale);
6224
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 }
6230
6231 let isRTL = aWindow.document.documentElement.dir == "rtl";
6232
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 },
6246
6247 clamp: function(num, min, max) {
6248 return Math.max(min, Math.min(max, num));
6249 },
6250
6251 get displayDPI() {
6252 let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
6253 delete this.displayDPI;
6254 return this.displayDPI = utils.displayDPI;
6255 },
6256
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 },
6265
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 }
6273
6274 };
6275
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 }
6301
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,
6313
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 };
6327
6328
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;
6337
6338 if (!browser.pageReport)
6339 return;
6340
6341 let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup");
6342 if (result == Ci.nsIPermissionManager.DENY_ACTION)
6343 return;
6344
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;
6352
6353 let strings = Strings.browser;
6354 let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message"))
6355 .replace("#1", brandShortName)
6356 .replace("#2", popupCount);
6357
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);
6365
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 ];
6377
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 },
6386
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 },
6394
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;
6401
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;
6409
6410 let popupFeatures = pageReport[i].popupWindowFeatures;
6411 let popupName = pageReport[i].popupWindowName;
6412
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 };
6420
6421
6422 var IndexedDB = {
6423 _permissionsPrompt: "indexedDB-permissions-prompt",
6424 _permissionsResponse: "indexedDB-permissions-response",
6425
6426 _quotaPrompt: "indexedDB-quota-prompt",
6427 _quotaResponse: "indexedDB-quota-response",
6428 _quotaCancel: "indexedDB-quota-cancel",
6429
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 },
6435
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 },
6441
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 }
6448
6449 let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
6450
6451 let contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
6452 let contentDocument = contentWindow.document;
6453 let tab = BrowserApp.getTabForWindow(contentWindow);
6454 if (!tab)
6455 return;
6456
6457 let host = contentDocument.documentURIObject.asciiHost;
6458
6459 let strings = Strings.browser;
6460
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 }
6471
6472 const firstTimeoutDuration = 300000; // 5 minutes
6473
6474 let timeoutId;
6475
6476 let notificationID = responseTopic + host;
6477 let observer = requestor.getInterface(Ci.nsIObserver);
6478
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;
6483
6484 function timeoutNotification() {
6485 // Remove the notification.
6486 NativeWindow.doorhanger.hide(notificationID, tab.id);
6487
6488 // Clear all of our timeout stuff. We may be called directly, not just
6489 // when the timeout actually elapses.
6490 clearTimeout(timeoutId);
6491
6492 // And tell the page that the popup timed out.
6493 observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
6494 }
6495
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 }
6502
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 }];
6518
6519 let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
6520 NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
6521
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 };
6528
6529 var CharacterEncoding = {
6530 _charsets: [],
6531
6532 init: function init() {
6533 Services.obs.addObserver(this, "CharEncoding:Get", false);
6534 Services.obs.addObserver(this, "CharEncoding:Set", false);
6535 this.sendState();
6536 },
6537
6538 uninit: function uninit() {
6539 Services.obs.removeObserver(this, "CharEncoding:Get");
6540 Services.obs.removeObserver(this, "CharEncoding:Set");
6541 },
6542
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 },
6553
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 */ }
6559
6560 sendMessageToJava({
6561 type: "CharEncoding:State",
6562 visible: showCharEncoding
6563 });
6564 },
6565
6566 getEncoding: function getEncoding() {
6567 function infoToCharset(info) {
6568 return { code: info.value, title: info.label };
6569 }
6570
6571 if (!this._charsets.length) {
6572 let data = CharsetMenu.getData();
6573
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)
6577
6578 this._charsets = pinnedCharsets.concat(otherCharsets);
6579 }
6580
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;
6586
6587 for (let i = 0; i < charsetCount; i++) {
6588 if (this._charsets[i].code === docCharset) {
6589 selected = i;
6590 break;
6591 }
6592 }
6593
6594 sendMessageToJava({
6595 type: "CharEncoding:Data",
6596 charsets: this._charsets,
6597 selected: selected
6598 });
6599 },
6600
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 };
6608
6609 var IdentityHandler = {
6610 // No trusted identity information. No site identity icon is shown.
6611 IDENTITY_MODE_UNKNOWN: "unknown",
6612
6613 // Minimal SSL CA-signed domain verification. Blue lock icon is shown.
6614 IDENTITY_MODE_DOMAIN_VERIFIED: "verified",
6615
6616 // High-quality identity information. Green lock icon is shown.
6617 IDENTITY_MODE_IDENTIFIED: "identified",
6618
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.
6622
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",
6625
6626 // Loaded active mixed content. Yellow triangle icon is shown.
6627 IDENTITY_MODE_MIXED_CONTENT_LOADED: "mixed_content_loaded",
6628
6629 // Cache the most recent SSLStatus and Location seen in getIdentityStrings
6630 _lastStatus : null,
6631 _lastLocation : null,
6632
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;
6641
6642 // Human readable name of Subject
6643 result.subjectOrg = cert.organization;
6644
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);
6652
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 }
6658
6659 // Human readable name of Certificate Authority
6660 result.caOrg = cert.issuerOrganization || cert.issuerCommonName;
6661 result.cert = cert;
6662
6663 return result;
6664 },
6665
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;
6672
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;
6677
6678 if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
6679 return this.IDENTITY_MODE_IDENTIFIED;
6680
6681 if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE)
6682 return this.IDENTITY_MODE_DOMAIN_VERIFIED;
6683
6684 return this.IDENTITY_MODE_UNKNOWN;
6685 },
6686
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;
6695
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;
6711
6712 let mode = this.getIdentityMode(aState);
6713 let result = { mode: mode };
6714
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;
6720
6721 // Ideally we'd just make this a Java string
6722 result.encrypted = Strings.browser.GetStringFromName("identity.encrypted2");
6723 result.host = this.getEffectiveHost();
6724
6725 let iData = this.getIdentityData();
6726 result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
6727
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;
6731
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;
6743
6744 return result;
6745 }
6746
6747 // Otherwise, we don't know the cert owner
6748 result.owner = Strings.browser.GetStringFromName("identity.ownerUnknown3");
6749
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);
6753
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");
6767
6768 return result;
6769 },
6770
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 };
6788
6789 function OverscrollController(aTab) {
6790 this.tab = aTab;
6791 }
6792
6793 OverscrollController.prototype = {
6794 supportsCommand : function supportsCommand(aCommand) {
6795 if (aCommand != "cmd_linePrevious" && aCommand != "cmd_scrollPageUp")
6796 return false;
6797
6798 return (this.tab.getViewport().y == 0);
6799 },
6800
6801 isCommandEnabled : function isCommandEnabled(aCommand) {
6802 return this.supportsCommand(aCommand);
6803 },
6804
6805 doCommand : function doCommand(aCommand){
6806 sendMessageToJava({ type: "ToggleChrome:Focus" });
6807 },
6808
6809 onEvent : function onEvent(aEvent) { }
6810 };
6811
6812 var SearchEngines = {
6813 _contextMenuId: null,
6814 PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
6815 PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
6816
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);
6823
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;
6832
6833 let method = form.method.toUpperCase();
6834
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 },
6860
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 },
6870
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 }
6877
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 });
6887
6888 let suggestTemplate = null;
6889 let suggestEngine = null;
6890
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 }
6898
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 });
6910
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 },
6920
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 },
6926
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;
6953
6954 default:
6955 dump("Unexpected message type observed: " + aTopic);
6956 break;
6957 }
6958 },
6959
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);
6964
6965 if (!tab)
6966 return;
6967
6968 let browser = tab.browser;
6969 let engines = browser.engines;
6970
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;
6978
6979 this.addOpenSearchEngine(engines[data.button]);
6980 engines.splice(data.button, 1);
6981
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 };
6989
6990 sendMessageToJava(newEngineMessage);
6991 }
6992 }).bind(this));
6993 },
6994
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 },
7001
7002 onError: function(aCode) {
7003 let errorMessage;
7004 if (aCode == 2) {
7005 // Engine is a duplicate.
7006 errorMessage = "alertSearchEngineDuplicateToast";
7007
7008 } else {
7009 // Unknown failure. Display general error message.
7010 errorMessage = "alertSearchEngineErrorToast";
7011 }
7012
7013 NativeWindow.toast.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), "long");
7014 }
7015 });
7016 },
7017
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 = [];
7025
7026 for (let i = 0; i < form.elements.length; ++i) {
7027 let el = form.elements[i];
7028 if (!el.type)
7029 continue;
7030
7031 // make this text field a generic search parameter
7032 if (aElement == el) {
7033 formData.push({ name: el.name, value: "{searchTerms}" });
7034 continue;
7035 }
7036
7037 let type = el.type.toLowerCase();
7038 let escapedName = escape(el.name);
7039 let escapedValue = escape(el.value);
7040
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 }
7060
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;
7066
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;
7092
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 };
7106
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 },
7112
7113 observe: function ao_observe(aSubject, aTopic, aData) {
7114 let isForeground = false;
7115 let tab = BrowserApp.selectedTab;
7116
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 }
7129
7130 if (tab && tab.getActive() != isForeground) {
7131 tab.setActive(isForeground);
7132 }
7133 }
7134 };
7135
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;
7142
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 },
7148
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 },
7155
7156 DEFAULT_ICON: "chrome://browser/skin/images/default-app-icon.png",
7157 DEFAULT_PREFS_FILENAME: "default-prefs.js",
7158
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 },
7197
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);
7201
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;
7214
7215 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
7216 file.initWithPath(profilePath);
7217
7218 let self = this;
7219 DOMApplicationRegistry.confirmInstall(aData, file,
7220 function (aManifest) {
7221 let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
7222
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;
7235
7236 let source = Services.io.newURI(fullsizeIcon, "UTF8", null);
7237 persist.saveURI(source, null, null, null, null, iconFile, null);
7238
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 }
7258
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 };
7267
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 },
7284
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 }
7291
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 },
7297
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 },
7306
7307 openURL: function openURL(aManifestURL, aOrigin) {
7308 sendMessageToJava({
7309 type: "Webapps:Open",
7310 manifestURL: aManifestURL,
7311 origin: aOrigin
7312 });
7313 },
7314
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 }
7326
7327 delete this.iconSize;
7328 return this.iconSize = iconSize;
7329 },
7330
7331 makeBase64Icon: function loadAndMakeBase64Icon(aIconURL, aCallbackFunction) {
7332 let size = this.iconSize;
7333
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", "");
7341
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", "");
7346
7347 canvas = null;
7348 aCallbackFunction.call(null, scaledIcon, fullsizeIcon);
7349 };
7350 favicon.onerror = function() {
7351 Cu.reportError("CreateShortcut: favicon image load error");
7352
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 };
7359
7360 favicon.src = aIconURL;
7361 },
7362
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
7375
7376 var RemoteDebugger = {
7377 init: function rd_init() {
7378 Services.prefs.addObserver("devtools.debugger.", this, false);
7379
7380 if (this._isEnabled())
7381 this._start();
7382 },
7383
7384 observe: function rd_observe(aSubject, aTopic, aData) {
7385 if (aTopic != "nsPref:changed")
7386 return;
7387
7388 switch (aData) {
7389 case "devtools.debugger.remote-enabled":
7390 if (this._isEnabled())
7391 this._start();
7392 else
7393 this._stop();
7394 break;
7395
7396 case "devtools.debugger.remote-port":
7397 if (this._isEnabled())
7398 this._restart();
7399 break;
7400 }
7401 },
7402
7403 uninit: function rd_uninit() {
7404 Services.prefs.removeObserver("devtools.debugger.", this);
7405 this._stop();
7406 },
7407
7408 _getPort: function _rd_getPort() {
7409 return Services.prefs.getIntPref("devtools.debugger.remote-port");
7410 },
7411
7412 _isEnabled: function rd_isEnabled() {
7413 return Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
7414 },
7415
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");
7428
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 });
7438
7439 // The debugger server expects a synchronous response, so spin on result since Prompt is async.
7440 let result = null;
7441
7442 prompt.show(function(data) {
7443 result = data.button;
7444 });
7445
7446 // Spin this thread while we wait for a result.
7447 let thread = Services.tm.currentThread;
7448 while (result == null)
7449 thread.processNextEvent(true);
7450
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 },
7459
7460 _restart: function rd_restart() {
7461 this._stop();
7462 this._start();
7463 },
7464
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 }
7472
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 },
7480
7481 _stop: function rd_start() {
7482 DebuggerServer.closeListener();
7483 dump("Remote debugger stopped");
7484 }
7485 };
7486
7487 var Telemetry = {
7488 addData: function addData(aHistogramId, aValue) {
7489 let histogram = Services.telemetry.getHistogramById(aHistogramId);
7490 histogram.add(aValue);
7491 },
7492 };
7493
7494 let Reader = {
7495 // Version of the cache database schema
7496 DB_VERSION: 1,
7497
7498 DEBUG: 0,
7499
7500 READER_ADD_SUCCESS: 0,
7501 READER_ADD_FAILED: 1,
7502 READER_ADD_DUPLICATE: 2,
7503
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,
7507
7508 isEnabledForParseOnLoad: false,
7509
7510 init: function Reader_init() {
7511 this.log("Init()");
7512 this._requests = {};
7513
7514 this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
7515
7516 Services.obs.addObserver(this, "Reader:Add", false);
7517 Services.obs.addObserver(this, "Reader:Remove", false);
7518
7519 Services.prefs.addObserver("reader.parse-on-load.", this, false);
7520 },
7521
7522 pageAction: {
7523 readerModeCallback: function(){
7524 sendMessageToJava({
7525 type: "Reader:Click",
7526 });
7527 },
7528
7529 readerModeActiveCallback: function(){
7530 sendMessageToJava({
7531 type: "Reader:LongClick",
7532 });
7533
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 },
7539
7540 updatePageAction: function(tab) {
7541 if (this.pageAction.id) {
7542 NativeWindow.pageactions.remove(this.pageAction.id);
7543 delete this.pageAction.id;
7544 }
7545
7546 // Create a relative timestamp for telemetry
7547 let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
7548
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 });
7556
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 }
7562
7563 // Only stop a reader session if the foreground viewer is not visible.
7564 UITelemetry.stopSession("reader.1", "", uptime);
7565
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 },
7576
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 }
7585
7586 let tabID = null;
7587 let url, urlWithoutRef;
7588
7589 if ('tabID' in args) {
7590 tabID = args.tabID;
7591
7592 let tab = BrowserApp.getTabForId(tabID);
7593 let currentURI = tab.browser.currentURI;
7594
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 }
7604
7605 let sendResult = function(result, article) {
7606 article = article || {};
7607 this.log("Reader:Add success=" + result + ", url=" + url + ", title=" + article.title + ", excerpt=" + article.excerpt);
7608
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);
7618
7619 let handleArticle = function(article) {
7620 if (!article) {
7621 sendResult(this.READER_ADD_FAILED, null);
7622 return;
7623 }
7624
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);
7630
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 }
7637
7638 if (tabID != null) {
7639 this.getArticleForTab(tabID, urlWithoutRef, handleArticle);
7640 } else {
7641 this.parseDocumentFromURL(urlWithoutRef, handleArticle);
7642 }
7643 }.bind(this));
7644 break;
7645 }
7646
7647 case "Reader:Remove": {
7648 let url = aData;
7649 this.removeArticleFromCache(url, function(success) {
7650 this.log("Reader:Remove success=" + success + ", url=" + url);
7651
7652 if (success) {
7653 sendMessageToJava({
7654 type: "Reader:Removed",
7655 url: url
7656 });
7657 }
7658 }.bind(this));
7659 break;
7660 }
7661
7662 case "nsPref:changed": {
7663 if (aData.startsWith("reader.parse-on-load.")) {
7664 this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
7665 }
7666 break;
7667 }
7668 }
7669 },
7670
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 },
7678
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 }
7687
7688 let request = { url: url, callbacks: [callback] };
7689 this._requests[url] = request;
7690
7691 try {
7692 this.log("parseDocumentFromURL: " + url);
7693
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 }
7701
7702 if (!this._requests) {
7703 this.log("Reader has been destroyed, abort");
7704 return;
7705 }
7706
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 },
7716
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 }
7727
7728 this.parseDocumentFromURL(url, callback);
7729 },
7730
7731 parseDocumentFromTab: function(tabId, callback) {
7732 try {
7733 this.log("parseDocumentFromTab: " + tabId);
7734
7735 let tab = BrowserApp.getTabForId(tabId);
7736 let url = tab.browser.contentWindow.location.href;
7737 let uri = Services.io.newURI(url, null, null);
7738
7739 if (!this._shouldCheckUri(uri)) {
7740 callback(null);
7741 return;
7742 }
7743
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 }
7751
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 }
7759
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 },
7768
7769 getArticleFromCache: function Reader_getArticleFromCache(url, callback) {
7770 this._getCacheDB(function(cacheDB) {
7771 if (!cacheDB) {
7772 callback(false);
7773 return;
7774 }
7775
7776 let transaction = cacheDB.transaction(cacheDB.objectStoreNames);
7777 let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
7778
7779 let request = articles.get(url);
7780
7781 request.onerror = function(event) {
7782 this.log("Error getting article from the cache DB: " + url);
7783 callback(null);
7784 }.bind(this);
7785
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 },
7792
7793 storeArticleInCache: function Reader_storeArticleInCache(article, callback) {
7794 this._getCacheDB(function(cacheDB) {
7795 if (!cacheDB) {
7796 callback(false);
7797 return;
7798 }
7799
7800 let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
7801 let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
7802
7803 let request = articles.add(article);
7804
7805 request.onerror = function(event) {
7806 this.log("Error storing article in the cache DB: " + article.url);
7807 callback(false);
7808 }.bind(this);
7809
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 },
7816
7817 removeArticleFromCache: function Reader_removeArticleFromCache(url, callback) {
7818 this._getCacheDB(function(cacheDB) {
7819 if (!cacheDB) {
7820 callback(false);
7821 return;
7822 }
7823
7824 let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
7825 let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
7826
7827 let request = articles.delete(url);
7828
7829 request.onerror = function(event) {
7830 this.log("Error removing article from the cache DB: " + url);
7831 callback(false);
7832 }.bind(this);
7833
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 },
7840
7841 uninit: function Reader_uninit() {
7842 Services.prefs.removeObserver("reader.parse-on-load.", this);
7843
7844 Services.obs.removeObserver(this, "Reader:Add");
7845 Services.obs.removeObserver(this, "Reader:Remove");
7846
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;
7856
7857 if (this._cacheDB) {
7858 this._cacheDB.close();
7859 delete this._cacheDB;
7860 }
7861 },
7862
7863 log: function(msg) {
7864 if (this.DEBUG)
7865 dump("Reader: " + msg);
7866 },
7867
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 }
7873
7874 if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
7875 this.log("Not parsing URI scheme: " + uri.scheme);
7876 return false;
7877 }
7878
7879 return true;
7880 },
7881
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 }
7889
7890 let worker = new ChromeWorker("readerWorker.js");
7891 worker.onmessage = function (evt) {
7892 let article = evt.data;
7893
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 }
7902
7903 callback(article);
7904 };
7905
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 },
7922
7923 _runCallbacksAndFinish: function Reader_runCallbacksAndFinish(request, result) {
7924 delete this._requests[request.url];
7925
7926 request.callbacks.forEach(function(callback) {
7927 callback(result);
7928 });
7929 },
7930
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.
7935
7936 let browser = document.createElement("browser");
7937 browser.setAttribute("type", "content");
7938 browser.setAttribute("collapsed", "true");
7939 browser.setAttribute("disablehistory", "true");
7940
7941 document.documentElement.appendChild(browser);
7942 browser.stop();
7943
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;
7949
7950 browser.addEventListener("DOMContentLoaded", function (event) {
7951 let doc = event.originalTarget;
7952
7953 // ignore on frames and other documents
7954 if (doc != browser.contentDocument)
7955 return;
7956
7957 this.log("Done loading: " + doc);
7958 if (doc.location.href == "about:blank") {
7959 callback(null);
7960
7961 // Request has finished with error, remove browser element
7962 browser.parentNode.removeChild(browser);
7963 return;
7964 }
7965
7966 callback(doc);
7967 }.bind(this));
7968
7969 browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
7970 null, null, null);
7971
7972 return browser;
7973 },
7974
7975 _downloadAndParseDocument: function Reader_downloadAndParseDocument(url, request) {
7976 try {
7977 this.log("Needs to fetch page, creating request: " + url);
7978
7979 request.browser = this._downloadDocument(url, function(doc) {
7980 this.log("Finished loading page: " + doc);
7981
7982 if (!doc) {
7983 this.log("Error loading page");
7984 this._runCallbacksAndFinish(request, null);
7985 return;
7986 }
7987
7988 this.log("Parsing response with Readability");
7989
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 }
7998
7999 if (!article) {
8000 this.log("Failed to parse page");
8001 this._runCallbacksAndFinish(request, null);
8002 return;
8003 }
8004
8005 this.log("Parsing has been successful");
8006
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 },
8015
8016 _getCacheDB: function Reader_getCacheDB(callback) {
8017 if (this._cacheDB) {
8018 callback(this._cacheDB);
8019 return;
8020 }
8021
8022 let request = window.indexedDB.open("about:reader", this.DB_VERSION);
8023
8024 request.onerror = function(event) {
8025 this.log("Error connecting to the cache DB");
8026 this._cacheDB = null;
8027 callback(null);
8028 }.bind(this);
8029
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);
8035
8036 request.onupgradeneeded = function(event) {
8037 this.log("Database schema upgrade from " +
8038 event.oldVersion + " to " + event.newVersion);
8039
8040 let cacheDB = event.target.result;
8041
8042 // Create the articles object store
8043 this.log("Creating articles object store");
8044 cacheDB.createObjectStore("articles", { keyPath: "url" });
8045
8046 this.log("Database upgrade done: " + this.DB_VERSION);
8047 }.bind(this);
8048 }
8049 };
8050
8051 var ExternalApps = {
8052 _contextMenuId: null,
8053
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 },
8065
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);
8077
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 },
8082
8083 uninit: function helper_uninit() {
8084 if (this._contextMenuId !== null) {
8085 NativeWindow.contextmenus.remove(this._contextMenuId);
8086 }
8087 this._contextMenuId = null;
8088 },
8089
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 },
8100
8101 openExternal: function(aElement) {
8102 let uri = ExternalApps._getMediaLink(aElement);
8103 HelperApps.launchUri(uri);
8104 },
8105
8106 shouldCheckUri: function(uri) {
8107 if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
8108 return false;
8109 }
8110
8111 return true;
8112 },
8113
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 },
8121
8122 updatePageActionUri: function updatePageActionUri(uri) {
8123 this._pageActionUri = uri;
8124 },
8125
8126 _setUriForPageAction: function setUriForPageAction(uri, apps) {
8127 this.updatePageActionUri(uri);
8128
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;
8132
8133 this._pageActionId = NativeWindow.pageactions.add({
8134 title: Strings.browser.GetStringFromName("openInApp.pageAction"),
8135 icon: "drawable://icon_openinapp",
8136
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");
8141
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 },
8162
8163 clearPageAction: function clearPageAction() {
8164 if(!this._pageActionId)
8165 return;
8166
8167 NativeWindow.pageactions.remove(this._pageActionId);
8168 delete this._pageActionId;
8169 },
8170 };
8171
8172 var Distribution = {
8173 // File used to store campaign data
8174 _file: null,
8175
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);
8180
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 },
8187
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 },
8193
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;
8200
8201 case "prefservice:after-app-defaults":
8202 this.getPrefs();
8203 break;
8204
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 }
8213
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 },
8221
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 },
8228
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;
8234
8235 file.append("preferences.json");
8236 this.readJSON(file, this.applyPrefs);
8237 },
8238
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 }
8246
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"]);
8251
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);
8256
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 }
8275
8276 // Apply a lightweight theme if necessary
8277 if (prefs["lightweightThemes.isThemeSelected"])
8278 Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
8279
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 }
8290
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 }
8299
8300 sendMessageToJava({ type: "Distribution:Set:OK" });
8301 },
8302
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) || "";
8309
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 };
8322
8323 var Tabs = {
8324 _enableTabExpiration: false,
8325 _domains: new Set(),
8326
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 }
8335
8336 Services.obs.addObserver(this, "Session:Prefetch", false);
8337
8338 BrowserApp.deck.addEventListener("pageshow", this, false);
8339 BrowserApp.deck.addEventListener("TabOpen", this, false);
8340 },
8341
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 }
8348
8349 Services.obs.removeObserver(this, "Session:Prefetch");
8350
8351 BrowserApp.deck.removeEventListener("pageshow", this);
8352 BrowserApp.deck.removeEventListener("TabOpen", this);
8353 },
8354
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 },
8381
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 },
8394
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 },
8431
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 };
8440
8441 function ContextMenuItem(args) {
8442 this.id = uuidgen.generateUUID().toString();
8443 this.args = args;
8444 }
8445
8446 ContextMenuItem.prototype = {
8447 get order() {
8448 return this.args.order || 0;
8449 },
8450
8451 matches: function(elt, x, y) {
8452 return this.args.selector.matches(elt, x, y);
8453 },
8454
8455 callback: function(elt) {
8456 this.args.callback(elt);
8457 },
8458
8459 addVal: function(name, elt, defaultValue) {
8460 if (!(name in this.args))
8461 return defaultValue;
8462
8463 if (typeof this.args[name] == "function")
8464 return this.args[name](elt);
8465
8466 return this.args[name];
8467 },
8468
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 }
8483
8484 function HTMLContextMenuItem(elt, target) {
8485 ContextMenuItem.call(this, { });
8486
8487 this.menuElementRef = Cu.getWeakReference(elt);
8488 this.targetElementRef = Cu.getWeakReference(target);
8489 }
8490
8491 HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
8492 order: {
8493 value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER
8494 },
8495
8496 matches: {
8497 value: function(target) {
8498 let t = this.targetElementRef.get();
8499 return t === target;
8500 },
8501 },
8502
8503 callback: {
8504 value: function(target) {
8505 let elt = this.menuElementRef.get();
8506 if (!elt) {
8507 return;
8508 }
8509
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 = {};
8514
8515 let elt = this.menuElementRef.get();
8516 let target = this.targetElementRef.get();
8517 if (!elt) {
8518 return;
8519 }
8520
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 }
8527
8528 } catch(ex) {
8529 Cu.reportError(ex);
8530 }
8531 } else {
8532 // otherwise just click the menu item
8533 elt.click();
8534 }
8535 },
8536 },
8537
8538 getValue: {
8539 value: function(target) {
8540 let elt = this.menuElementRef.get();
8541 if (!elt) {
8542 return null;
8543 }
8544
8545 if (elt.hasAttribute("hidden")) {
8546 return null;
8547 }
8548
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 });

mercurial