gecko-dev/mobile/android/chrome/content/browser.js

8588 lines
300 KiB
JavaScript

#filter substitution
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/JNI.jsm");
Cu.import('resource://gre/modules/Payment.jsm');
Cu.import("resource://gre/modules/NotificationDB.jsm");
Cu.import("resource://gre/modules/SpatialNavigation.jsm");
Cu.import("resource://gre/modules/UITelemetry.jsm");
#ifdef ACCESSIBILITY
Cu.import("resource://gre/modules/accessibility/AccessFu.jsm");
#endif
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava",
"resource://gre/modules/Messaging.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
"resource://gre/modules/devtools/dbg-server.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
"resource://gre/modules/UserAgentOverrides.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
"resource://gre/modules/LoginManagerContent.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
#ifdef MOZ_SAFE_BROWSING
XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
"resource://gre/modules/SafeBrowsing.jsm");
#endif
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
"resource://gre/modules/Sanitizer.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
"resource://gre/modules/Prompt.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
"resource://gre/modules/HelperApps.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
"resource://gre/modules/SSLExceptions.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
"resource://gre/modules/FormHistory.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
"resource://gre/modules/SimpleServiceDiscovery.jsm");
#ifdef NIGHTLY_BUILD
XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
"resource://shumway/ShumwayUtils.jsm");
#endif
#ifdef MOZ_ANDROID_SYNTHAPKS
XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
"resource://gre/modules/WebappManager.jsm");
#endif
XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
"resource://gre/modules/CharsetMenu.jsm");
// Lazily-loaded browser scripts:
[
["SelectHelper", "chrome://browser/content/SelectHelper.js"],
["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
["AboutReader", "chrome://browser/content/aboutReader.js"],
["MasterPassword", "chrome://browser/content/MasterPassword.js"],
["PluginHelper", "chrome://browser/content/PluginHelper.js"],
["OfflineApps", "chrome://browser/content/OfflineApps.js"],
["Linkifier", "chrome://browser/content/Linkify.js"],
["ZoomHelper", "chrome://browser/content/ZoomHelper.js"],
["CastingApps", "chrome://browser/content/CastingApps.js"],
].forEach(function (aScript) {
let [name, script] = aScript;
XPCOMUtils.defineLazyGetter(window, name, function() {
let sandbox = {};
Services.scriptloader.loadSubScript(script, sandbox);
return sandbox[name];
});
});
[
#ifdef MOZ_WEBRTC
["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"],
#endif
["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
].forEach(function (aScript) {
let [name, notifications, script] = aScript;
XPCOMUtils.defineLazyGetter(window, name, function() {
let sandbox = {};
Services.scriptloader.loadSubScript(script, sandbox);
return sandbox[name];
});
notifications.forEach(function (aNotification) {
Services.obs.addObserver(function(s, t, d) {
window[name].observe(s, t, d)
}, aNotification, false);
});
});
// Lazily-loaded JS modules that use observer notifications
[
["Home", ["HomePanels:Get", "HomePanels:Authenticate"], "resource://gre/modules/Home.jsm"],
].forEach(module => {
let [name, notifications, resource] = module;
XPCOMUtils.defineLazyModuleGetter(this, name, resource);
notifications.forEach(notification => {
Services.obs.addObserver((s,t,d) => {
this[name].observe(s,t,d)
}, notification, false);
});
});
XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
"@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
"@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
"@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
#ifdef MOZ_WEBRTC
XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
"@mozilla.org/mediaManagerService;1", "nsIMediaManagerService");
#endif
const kStateActive = 0x00000001; // :active pseudoclass for elements
const kXLinkNamespace = "http://www.w3.org/1999/xlink";
const kDefaultCSSViewportWidth = 980;
const kDefaultCSSViewportHeight = 480;
const kViewportRemeasureThrottle = 500;
const kDoNotTrackPrefState = Object.freeze({
NO_PREF: "0",
DISALLOW_TRACKING: "1",
ALLOW_TRACKING: "2",
});
function dump(a) {
Services.console.logStringMessage(a);
}
function doChangeMaxLineBoxWidth(aWidth) {
gReflowPending = null;
let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
let docShell = webNav.QueryInterface(Ci.nsIDocShell);
let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
let range = null;
if (BrowserApp.selectedTab._mReflozPoint) {
range = BrowserApp.selectedTab._mReflozPoint.range;
}
try {
docViewer.pausePainting();
docViewer.changeMaxLineBoxWidth(aWidth);
if (range) {
ZoomHelper.zoomInAndSnapToRange(range);
} else {
// In this case, we actually didn't zoom into a specific range. It
// probably happened from a page load reflow-on-zoom event, so we
// need to make sure painting is re-enabled.
BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
}
} finally {
docViewer.resumePainting();
}
}
function fuzzyEquals(a, b) {
return (Math.abs(a - b) < 1e-6);
}
/**
* Convert a font size to CSS pixels (px) from twentieiths-of-a-point
* (twips).
*/
function convertFromTwipsToPx(aSize) {
return aSize/240 * 16.0;
}
#ifdef MOZ_CRASHREPORTER
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
"@mozilla.org/xre/app-info;1", "nsICrashReporter");
#endif
XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
let ContentAreaUtils = {};
Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
return ContentAreaUtils;
});
XPCOMUtils.defineLazyModuleGetter(this, "Rect",
"resource://gre/modules/Geometry.jsm");
function resolveGeckoURI(aURI) {
if (!aURI)
throw "Can't resolve an empty uri";
if (aURI.startsWith("chrome://")) {
let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
} else if (aURI.startsWith("resource://")) {
let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
return handler.resolveURI(Services.io.newURI(aURI, null, null));
}
return aURI;
}
/**
* Cache of commonly used string bundles.
*/
var Strings = {};
[
["brand", "chrome://branding/locale/brand.properties"],
["browser", "chrome://browser/locale/browser.properties"]
].forEach(function (aStringBundle) {
let [name, bundle] = aStringBundle;
XPCOMUtils.defineLazyGetter(Strings, name, function() {
return Services.strings.createBundle(bundle);
});
});
const kFormHelperModeDisabled = 0;
const kFormHelperModeEnabled = 1;
const kFormHelperModeDynamic = 2; // disabled on tablets
var BrowserApp = {
_tabs: [],
_selectedTab: null,
_prefObservers: [],
isGuest: false,
get isTablet() {
let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
delete this.isTablet;
return this.isTablet = sysInfo.get("tablet");
},
get isOnLowMemoryPlatform() {
let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
delete this.isOnLowMemoryPlatform;
return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
},
deck: null,
startup: function startup() {
window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
this.deck = document.getElementById("browsers");
this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
try {
BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
sendMessageToJava({ type: "Gecko:DelayedStartup" });
} catch(ex) { console.log(ex); }
}, false);
BrowserEventHandler.init();
ViewportHandler.init();
Services.androidBridge.browserApp = this;
Services.obs.addObserver(this, "Locale:Changed", false);
Services.obs.addObserver(this, "Tab:Load", false);
Services.obs.addObserver(this, "Tab:Selected", false);
Services.obs.addObserver(this, "Tab:Closed", false);
Services.obs.addObserver(this, "Session:Back", false);
Services.obs.addObserver(this, "Session:ShowHistory", false);
Services.obs.addObserver(this, "Session:Forward", false);
Services.obs.addObserver(this, "Session:Reload", false);
Services.obs.addObserver(this, "Session:Stop", false);
Services.obs.addObserver(this, "SaveAs:PDF", false);
Services.obs.addObserver(this, "Browser:Quit", false);
Services.obs.addObserver(this, "Preferences:Set", false);
Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
Services.obs.addObserver(this, "Sanitize:ClearData", false);
Services.obs.addObserver(this, "FullScreen:Exit", false);
Services.obs.addObserver(this, "Viewport:Change", false);
Services.obs.addObserver(this, "Viewport:Flush", false);
Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false);
Services.obs.addObserver(this, "Passwords:Init", false);
Services.obs.addObserver(this, "FormHistory:Init", false);
Services.obs.addObserver(this, "gather-telemetry", false);
Services.obs.addObserver(this, "keyword-search", false);
#ifdef MOZ_ANDROID_SYNTHAPKS
Services.obs.addObserver(this, "webapps-runtime-install", false);
Services.obs.addObserver(this, "webapps-runtime-install-package", false);
Services.obs.addObserver(this, "webapps-ask-install", false);
Services.obs.addObserver(this, "webapps-launch", false);
Services.obs.addObserver(this, "webapps-uninstall", false);
Services.obs.addObserver(this, "Webapps:AutoInstall", false);
Services.obs.addObserver(this, "Webapps:Load", false);
Services.obs.addObserver(this, "Webapps:AutoUninstall", false);
#endif
Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
function showFullScreenWarning() {
NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
}
window.addEventListener("fullscreen", function() {
sendMessageToJava({
type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide"
});
}, false);
window.addEventListener("mozfullscreenchange", function() {
sendMessageToJava({
type: document.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop"
});
if (document.mozFullScreen)
showFullScreenWarning();
}, false);
// When a restricted key is pressed in DOM full-screen mode, we should display
// the "Press ESC to exit" warning message.
window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true);
NativeWindow.init();
LightWeightThemeWebInstaller.init();
Downloads.init();
FormAssistant.init();
IndexedDB.init();
HealthReportStatusListener.init();
XPInstallObserver.init();
CharacterEncoding.init();
ActivityObserver.init();
#ifdef MOZ_ANDROID_SYNTHAPKS
// TODO: replace with Android implementation of WebappOSUtils.isLaunchable.
Cu.import("resource://gre/modules/Webapps.jsm");
DOMApplicationRegistry.allAppsLaunchable = true;
#else
WebappsUI.init();
#endif
RemoteDebugger.init();
Reader.init();
UserAgentOverrides.init();
DesktopUserAgent.init();
CastingApps.init();
Distribution.init();
Tabs.init();
#ifdef ACCESSIBILITY
AccessFu.attach(window);
#endif
#ifdef NIGHTLY_BUILD
ShumwayUtils.init();
#endif
// Init LoginManager
Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
let url = null;
let pinned = false;
if ("arguments" in window) {
if (window.arguments[0])
url = window.arguments[0];
if (window.arguments[1])
gScreenWidth = window.arguments[1];
if (window.arguments[2])
gScreenHeight = window.arguments[2];
if (window.arguments[3])
pinned = window.arguments[3];
if (window.arguments[4])
this.isGuest = window.arguments[4];
}
if (pinned) {
this._initRuntime(this._startupStatus, url, aUrl => this.addTab(aUrl));
} else {
SearchEngines.init();
this.initContextMenu();
}
// The order that context menu items are added is important
// Make sure the "Open in App" context menu item appears at the bottom of the list
ExternalApps.init();
// XXX maybe we don't do this if the launch was kicked off from external
Services.io.offline = false;
// Broadcast a UIReady message so add-ons know we are finished with startup
let event = document.createEvent("Events");
event.initEvent("UIReady", true, false);
window.dispatchEvent(event);
if (this._startupStatus)
this.onAppUpdated();
// Store the low-precision buffer pref
this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer");
// notify java that gecko has loaded
sendMessageToJava({ type: "Gecko:Ready" });
#ifdef MOZ_SAFE_BROWSING
// Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
setTimeout(function() { SafeBrowsing.init(); }, 5000);
#endif
},
get _startupStatus() {
delete this._startupStatus;
let savedMilestone = null;
try {
savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
} catch (e) {
}
#expand let ourMilestone = "__MOZ_APP_VERSION__";
this._startupStatus = "";
if (ourMilestone != savedMilestone) {
Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone);
this._startupStatus = savedMilestone ? "upgrade" : "new";
}
return this._startupStatus;
},
/**
* Pass this a locale string, such as "fr" or "es_ES".
*/
setLocale: function (locale) {
console.log("browser.js: requesting locale set: " + locale);
sendMessageToJava({ type: "Locale:Set", locale: locale });
},
_initRuntime: function(status, url, callback) {
let sandbox = {};
Services.scriptloader.loadSubScript("chrome://browser/content/WebappRT.js", sandbox);
window.WebappRT = sandbox.WebappRT;
WebappRT.init(status, url, callback);
},
initContextMenu: function ba_initContextMenu() {
// TODO: These should eventually move into more appropriate classes
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"),
NativeWindow.contextmenus.linkOpenableNonPrivateContext,
function(aTarget) {
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened");
let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
NativeWindow.toast.show(label, "short");
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInPrivateTab"),
NativeWindow.contextmenus.linkOpenableContext,
function(aTarget) {
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true });
let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened");
let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
NativeWindow.toast.show(label, "short");
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyLink"),
NativeWindow.contextmenus.linkCopyableContext,
function(aTarget) {
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyEmailAddress"),
NativeWindow.contextmenus.emailLinkContext,
function(aTarget) {
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
let emailAddr = NativeWindow.contextmenus._stripScheme(url);
NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr);
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyPhoneNumber"),
NativeWindow.contextmenus.phoneNumberLinkContext,
function(aTarget) {
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
});
NativeWindow.contextmenus.add({
label: Strings.browser.GetStringFromName("contextmenu.shareLink"),
order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext),
showAsActions: function(aElement) {
return {
title: aElement.textContent.trim() || aElement.title.trim(),
uri: NativeWindow.contextmenus._getLinkURL(aElement),
};
},
icon: "drawable://ic_menu_share",
callback: function(aTarget) { }
});
NativeWindow.contextmenus.add({
label: Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"),
order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
showAsActions: function(aElement) {
let url = NativeWindow.contextmenus._getLinkURL(aElement);
let emailAddr = NativeWindow.contextmenus._stripScheme(url);
let title = aElement.textContent || aElement.title;
return {
title: title,
uri: emailAddr,
type: "text/mailto",
};
},
icon: "drawable://ic_menu_share",
callback: function(aTarget) { }
});
NativeWindow.contextmenus.add({
label: Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"),
order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
showAsActions: function(aElement) {
let url = NativeWindow.contextmenus._getLinkURL(aElement);
let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
let title = aElement.textContent || aElement.title;
return {
title: title,
uri: phoneNumber,
type: "text/tel",
};
},
icon: "drawable://ic_menu_share",
callback: function(aTarget) { }
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
function(aTarget) {
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
sendMessageToJava({
type: "Contact:Add",
email: url
});
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
function(aTarget) {
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
sendMessageToJava({
type: "Contact:Add",
phone: url
});
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"),
NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext),
function(aTarget) {
let url = NativeWindow.contextmenus._getLinkURL(aTarget);
let title = aTarget.textContent || aTarget.title || url;
sendMessageToJava({
type: "Bookmark:Insert",
url: url,
title: title
});
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.playMedia"),
NativeWindow.contextmenus.mediaContext("media-paused"),
function(aTarget) {
aTarget.play();
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.pauseMedia"),
NativeWindow.contextmenus.mediaContext("media-playing"),
function(aTarget) {
aTarget.pause();
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.showControls2"),
NativeWindow.contextmenus.mediaContext("media-hidingcontrols"),
function(aTarget) {
aTarget.setAttribute("controls", true);
});
NativeWindow.contextmenus.add({
label: Strings.browser.GetStringFromName("contextmenu.shareMedia"),
order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")),
showAsActions: function(aElement) {
let url = (aElement.currentSrc || aElement.src);
let title = aElement.textContent || aElement.title;
return {
title: title,
uri: url,
type: "video/*",
};
},
icon: "drawable://ic_menu_share",
callback: function(aTarget) {
}
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"),
NativeWindow.contextmenus.SelectorContext("video:not(:-moz-full-screen)"),
function(aTarget) {
aTarget.mozRequestFullScreen();
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.mute"),
NativeWindow.contextmenus.mediaContext("media-unmuted"),
function(aTarget) {
aTarget.muted = true;
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"),
NativeWindow.contextmenus.mediaContext("media-muted"),
function(aTarget) {
aTarget.muted = false;
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyImageLocation"),
NativeWindow.contextmenus.imageLocationCopyableContext,
function(aTarget) {
let url = aTarget.src;
NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
});
NativeWindow.contextmenus.add({
label: Strings.browser.GetStringFromName("contextmenu.shareImage"),
selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
showAsActions: function(aTarget) {
let doc = aTarget.ownerDocument;
let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
.getImgCacheForDocument(doc);
let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet);
let src = aTarget.src;
return {
title: src,
uri: src,
type: "image/*",
};
},
icon: "drawable://ic_menu_share",
menu: true,
callback: function(aTarget) { }
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.saveImage"),
NativeWindow.contextmenus.imageSaveableContext,
function(aTarget) {
ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
false, true, aTarget.ownerDocument.documentURIObject,
aTarget.ownerDocument);
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"),
NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
function(aTarget) {
let src = aTarget.src;
sendMessageToJava({
type: "Image:SetAs",
url: src
});
});
NativeWindow.contextmenus.add(
function(aTarget) {
if (aTarget instanceof HTMLVideoElement) {
// If a video element is zero width or height, its essentially
// an HTMLAudioElement.
if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 )
return Strings.browser.GetStringFromName("contextmenu.saveAudio");
return Strings.browser.GetStringFromName("contextmenu.saveVideo");
} else if (aTarget instanceof HTMLAudioElement) {
return Strings.browser.GetStringFromName("contextmenu.saveAudio");
}
return Strings.browser.GetStringFromName("contextmenu.saveVideo");
}, NativeWindow.contextmenus.mediaSaveableContext,
function(aTarget) {
let url = aTarget.currentSrc || aTarget.src;
let filePickerTitleKey = (aTarget instanceof HTMLVideoElement &&
(aTarget.videoWidth != 0 && aTarget.videoHeight != 0))
? "SaveVideoTitle" : "SaveAudioTitle";
// Skipped trying to pull MIME type out of cache for now
ContentAreaUtils.internalSave(url, null, null, null, null, false,
filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
aTarget.ownerDocument, true, null);
});
},
onAppUpdated: function() {
// initialize the form history and passwords databases on upgrades
Services.obs.notifyObservers(null, "FormHistory:Init", "");
Services.obs.notifyObservers(null, "Passwords:Init", "");
// Migrate user-set "plugins.click_to_play" pref. See bug 884694.
// Because the default value is true, a user-set pref means that the pref was set to false.
if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
Services.prefs.clearUserPref("plugins.click_to_play");
}
},
shutdown: function shutdown() {
NativeWindow.uninit();
LightWeightThemeWebInstaller.uninit();
FormAssistant.uninit();
IndexedDB.uninit();
ViewportHandler.uninit();
XPInstallObserver.uninit();
HealthReportStatusListener.uninit();
CharacterEncoding.uninit();
SearchEngines.uninit();
#ifndef MOZ_ANDROID_SYNTHAPKS
WebappsUI.uninit();
#endif
RemoteDebugger.uninit();
Reader.uninit();
UserAgentOverrides.uninit();
DesktopUserAgent.uninit();
ExternalApps.uninit();
CastingApps.uninit();
Distribution.uninit();
Tabs.uninit();
},
// This function returns false during periods where the browser displayed document is
// different from the browser content document, so user actions and some kinds of viewport
// updates should be ignored. This period starts when we start loading a new page or
// switch tabs, and ends when the new browser content document has been drawn and handed
// off to the compositor.
isBrowserContentDocumentDisplayed: function() {
try {
if (!Services.androidBridge.isContentDocumentDisplayed())
return false;
} catch (e) {
return false;
}
let tab = this.selectedTab;
if (!tab)
return false;
return tab.contentDocumentIsDisplayed;
},
contentDocumentChanged: function() {
window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true;
Services.androidBridge.contentDocumentChanged();
},
get tabs() {
return this._tabs;
},
get selectedTab() {
return this._selectedTab;
},
set selectedTab(aTab) {
if (this._selectedTab == aTab)
return;
if (this._selectedTab) {
this._selectedTab.setActive(false);
}
this._selectedTab = aTab;
if (!aTab)
return;
aTab.setActive(true);
aTab.setResolution(aTab._zoom, true);
this.contentDocumentChanged();
this.deck.selectedPanel = aTab.browser;
// Focus the browser so that things like selection will be styled correctly.
aTab.browser.focus();
},
get selectedBrowser() {
if (this._selectedTab)
return this._selectedTab.browser;
return null;
},
getTabForId: function getTabForId(aId) {
let tabs = this._tabs;
for (let i=0; i < tabs.length; i++) {
if (tabs[i].id == aId)
return tabs[i];
}
return null;
},
getTabForBrowser: function getTabForBrowser(aBrowser) {
let tabs = this._tabs;
for (let i = 0; i < tabs.length; i++) {
if (tabs[i].browser == aBrowser)
return tabs[i];
}
return null;
},
getTabForWindow: function getTabForWindow(aWindow) {
let tabs = this._tabs;
for (let i = 0; i < tabs.length; i++) {
if (tabs[i].browser.contentWindow == aWindow)
return tabs[i];
}
return null;
},
getBrowserForWindow: function getBrowserForWindow(aWindow) {
let tabs = this._tabs;
for (let i = 0; i < tabs.length; i++) {
if (tabs[i].browser.contentWindow == aWindow)
return tabs[i].browser;
}
return null;
},
getBrowserForDocument: function getBrowserForDocument(aDocument) {
let tabs = this._tabs;
for (let i = 0; i < tabs.length; i++) {
if (tabs[i].browser.contentDocument == aDocument)
return tabs[i].browser;
}
return null;
},
loadURI: function loadURI(aURI, aBrowser, aParams) {
aBrowser = aBrowser || this.selectedBrowser;
if (!aBrowser)
return;
aParams = aParams || {};
let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null;
let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
let charset = "charset" in aParams ? aParams.charset : null;
let tab = this.getTabForBrowser(aBrowser);
if (tab) {
if ("userSearch" in aParams) tab.userSearch = aParams.userSearch;
}
try {
aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
} catch(e) {
if (tab) {
let message = {
type: "Content:LoadError",
tabID: tab.id
};
sendMessageToJava(message);
dump("Handled load error: " + e)
}
}
},
addTab: function addTab(aURI, aParams) {
aParams = aParams || {};
let newTab = new Tab(aURI, aParams);
this._tabs.push(newTab);
let selected = "selected" in aParams ? aParams.selected : true;
if (selected)
this.selectedTab = newTab;
let pinned = "pinned" in aParams ? aParams.pinned : false;
if (pinned) {
let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
ss.setTabValue(newTab, "appOrigin", aURI);
}
let evt = document.createEvent("UIEvents");
evt.initUIEvent("TabOpen", true, false, window, null);
newTab.browser.dispatchEvent(evt);
return newTab;
},
// Use this method to close a tab from JS. This method sends a message
// to Java to close the tab in the Java UI (we'll get a Tab:Closed message
// back from Java when that happens).
closeTab: function closeTab(aTab) {
if (!aTab) {
Cu.reportError("Error trying to close tab (tab doesn't exist)");
return;
}
let message = {
type: "Tab:Close",
tabID: aTab.id
};
sendMessageToJava(message);
},
#ifdef MOZ_ANDROID_SYNTHAPKS
_loadWebapp: function(aMessage) {
this._initRuntime(this._startupStatus, aMessage.url, aUrl => {
this.manifestUrl = aMessage.url;
this.addTab(aUrl, { title: aMessage.name });
});
},
#endif
// Calling this will update the state in BrowserApp after a tab has been
// closed in the Java UI.
_handleTabClosed: function _handleTabClosed(aTab) {
if (aTab == this.selectedTab)
this.selectedTab = null;
let evt = document.createEvent("UIEvents");
evt.initUIEvent("TabClose", true, false, window, null);
aTab.browser.dispatchEvent(evt);
aTab.destroy();
this._tabs.splice(this._tabs.indexOf(aTab), 1);
},
// Use this method to select a tab from JS. This method sends a message
// to Java to select the tab in the Java UI (we'll get a Tab:Selected message
// back from Java when that happens).
selectTab: function selectTab(aTab) {
if (!aTab) {
Cu.reportError("Error trying to select tab (tab doesn't exist)");
return;
}
// There's nothing to do if the tab is already selected
if (aTab == this.selectedTab)
return;
let message = {
type: "Tab:Select",
tabID: aTab.id
};
sendMessageToJava(message);
},
/**
* Gets an open tab with the given URL.
*
* @param aURL URL to look for
* @return the tab with the given URL, or null if no such tab exists
*/
getTabWithURL: function getTabWithURL(aURL) {
let uri = Services.io.newURI(aURL, null, null);
for (let i = 0; i < this._tabs.length; ++i) {
let tab = this._tabs[i];
if (tab.browser.currentURI.equals(uri)) {
return tab;
}
}
return null;
},
/**
* If a tab with the given URL already exists, that tab is selected.
* Otherwise, a new tab is opened with the given URL.
*
* @param aURL URL to open
*/
selectOrOpenTab: function selectOrOpenTab(aURL) {
let tab = this.getTabWithURL(aURL);
if (tab == null) {
this.addTab(aURL);
} else {
this.selectTab(tab);
}
},
// This method updates the state in BrowserApp after a tab has been selected
// in the Java UI.
_handleTabSelected: function _handleTabSelected(aTab) {
this.selectedTab = aTab;
let evt = document.createEvent("UIEvents");
evt.initUIEvent("TabSelect", true, false, window, null);
aTab.browser.dispatchEvent(evt);
},
quit: function quit() {
// Figure out if there's at least one other browser window around.
let lastBrowser = true;
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements() && lastBrowser) {
let win = e.getNext();
if (!win.closed && win != window)
lastBrowser = false;
}
if (lastBrowser) {
// Let everyone know we are closing the last browser window
let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null);
if (closingCanceled.data)
return;
Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null);
}
window.QueryInterface(Ci.nsIDOMChromeWindow).minimize();
window.close();
},
saveAsPDF: function saveAsPDF(aBrowser) {
// Create the final destination file location
let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
fileName = fileName.trim() + ".pdf";
let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
let downloadsDir = dm.defaultDownloadsDirectory;
let file = downloadsDir.clone();
file.append(fileName);
file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8));
let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings;
printSettings.printSilent = true;
printSettings.showPrintProgress = false;
printSettings.printBGImages = true;
printSettings.printBGColors = true;
printSettings.printToFile = true;
printSettings.toFileName = file.path;
printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
//XXX we probably need a preference here, the header can be useful
printSettings.footerStrCenter = "";
printSettings.footerStrLeft = "";
printSettings.footerStrRight = "";
printSettings.headerStrCenter = "";
printSettings.headerStrLeft = "";
printSettings.headerStrRight = "";
// Create a valid mimeInfo for the PDF
let ms = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
let mimeInfo = ms.getFromTypeAndExtension("application/pdf", "pdf");
let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebBrowserPrint);
let cancelable = {
cancel: function (aReason) {
webBrowserPrint.cancel();
}
}
let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aBrowser.contentWindow);
let download = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,
aBrowser.currentURI,
Services.io.newFileURI(file), "", mimeInfo,
Date.now() * 1000, null, cancelable, isPrivate);
webBrowserPrint.print(printSettings, download);
},
notifyPrefObservers: function(aPref) {
this._prefObservers[aPref].forEach(function(aRequestId) {
this.getPreferences(aRequestId, [aPref], 1);
}, this);
},
handlePreferencesRequest: function handlePreferencesRequest(aRequestId,
aPrefNames,
aListen) {
let prefs = [];
for (let prefName of aPrefNames) {
let pref = {
name: prefName,
type: "",
value: null
};
if (aListen) {
if (this._prefObservers[prefName])
this._prefObservers[prefName].push(aRequestId);
else
this._prefObservers[prefName] = [ aRequestId ];
Services.prefs.addObserver(prefName, this, false);
}
// These pref names are not "real" pref names.
// They are used in the setting menu,
// and these are passed when initializing the setting menu.
switch (prefName) {
// The plugin pref is actually two separate prefs, so
// we need to handle it differently
case "plugin.enable":
pref.type = "string";// Use a string type for java's ListPreference
pref.value = PluginHelper.getPluginPreference();
prefs.push(pref);
continue;
// Handle master password
case "privacy.masterpassword.enabled":
pref.type = "bool";
pref.value = MasterPassword.enabled;
prefs.push(pref);
continue;
// Handle do-not-track preference
case "privacy.donottrackheader":
pref.type = "string";
let enableDNT = Services.prefs.getBoolPref("privacy.donottrackheader.enabled");
if (!enableDNT) {
pref.value = kDoNotTrackPrefState.NO_PREF;
} else {
let dntState = Services.prefs.getIntPref("privacy.donottrackheader.value");
pref.value = (dntState === 0) ? kDoNotTrackPrefState.ALLOW_TRACKING :
kDoNotTrackPrefState.DISALLOW_TRACKING;
}
prefs.push(pref);
continue;
#ifdef MOZ_CRASHREPORTER
// Crash reporter submit pref must be fetched from nsICrashReporter service.
case "datareporting.crashreporter.submitEnabled":
pref.type = "bool";
pref.value = CrashReporter.submitReports;
prefs.push(pref);
continue;
#endif
}
// Pref name translation.
switch (prefName) {
#ifdef MOZ_TELEMETRY_REPORTING
// Telemetry pref differs based on build.
case Telemetry.SHARED_PREF_TELEMETRY_ENABLED:
#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
prefName = "toolkit.telemetry.enabledPreRelease";
#else
prefName = "toolkit.telemetry.enabled";
#endif
break;
#endif
}
try {
switch (Services.prefs.getPrefType(prefName)) {
case Ci.nsIPrefBranch.PREF_BOOL:
pref.type = "bool";
pref.value = Services.prefs.getBoolPref(prefName);
break;
case Ci.nsIPrefBranch.PREF_INT:
pref.type = "int";
pref.value = Services.prefs.getIntPref(prefName);
break;
case Ci.nsIPrefBranch.PREF_STRING:
default:
pref.type = "string";
try {
// Try in case it's a localized string (will throw an exception if not)
pref.value = Services.prefs.getComplexValue(prefName, Ci.nsIPrefLocalizedString).data;
} catch (e) {
pref.value = Services.prefs.getCharPref(prefName);
}
break;
}
} catch (e) {
dump("Error reading pref [" + prefName + "]: " + e);
// preference does not exist; do not send it
continue;
}
// Some Gecko preferences use integers or strings to reference
// state instead of directly representing the value.
// Since the Java UI uses the type to determine which ui elements
// to show and how to handle them, we need to normalize these
// preferences to the correct type.
switch (prefName) {
// (string) index for determining which multiple choice value to display.
case "browser.chrome.titlebarMode":
case "network.cookie.cookieBehavior":
case "font.size.inflation.minTwips":
case "home.sync.updateMode":
pref.type = "string";
pref.value = pref.value.toString();
break;
}
prefs.push(pref);
}
sendMessageToJava({
type: "Preferences:Data",
requestId: aRequestId, // opaque request identifier, can be any string/int/whatever
preferences: prefs
});
},
setPreferences: function setPreferences(aPref) {
let json = JSON.parse(aPref);
switch (json.name) {
// The plugin pref is actually two separate prefs, so
// we need to handle it differently
case "plugin.enable":
PluginHelper.setPluginPreference(json.value);
return;
// MasterPassword pref is not real, we just need take action and leave
case "privacy.masterpassword.enabled":
if (MasterPassword.enabled)
MasterPassword.removePassword(json.value);
else
MasterPassword.setPassword(json.value);
return;
// "privacy.donottrackheader" is not "real" pref name, it's used in the setting menu.
case "privacy.donottrackheader":
switch (json.value) {
// Don't tell anything about tracking me
case kDoNotTrackPrefState.NO_PREF:
Services.prefs.setBoolPref("privacy.donottrackheader.enabled", false);
Services.prefs.clearUserPref("privacy.donottrackheader.value");
break;
// Accept tracking me
case kDoNotTrackPrefState.ALLOW_TRACKING:
Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true);
Services.prefs.setIntPref("privacy.donottrackheader.value", 0);
break;
// Not accept tracking me
case kDoNotTrackPrefState.DISALLOW_TRACKING:
Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true);
Services.prefs.setIntPref("privacy.donottrackheader.value", 1);
break;
}
return;
// Enabling or disabling suggestions will prevent future prompts
case SearchEngines.PREF_SUGGEST_ENABLED:
Services.prefs.setBoolPref(SearchEngines.PREF_SUGGEST_PROMPTED, true);
break;
#ifdef MOZ_CRASHREPORTER
// Crash reporter preference is in a service; set and return.
case "datareporting.crashreporter.submitEnabled":
CrashReporter.submitReports = json.value;
return;
#endif
// When sending to Java, we normalized special preferences that use
// integers and strings to represent booleans. Here, we convert them back
// to their actual types so we can store them.
case "browser.chrome.titlebarMode":
case "network.cookie.cookieBehavior":
case "font.size.inflation.minTwips":
case "home.sync.updateMode":
json.type = "int";
json.value = parseInt(json.value);
break;
}
// Pref name translation.
switch (json.name) {
#ifdef MOZ_TELEMETRY_REPORTING
// Telemetry pref differs based on build.
case Telemetry.SHARED_PREF_TELEMETRY_ENABLED:
#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
json.name = "toolkit.telemetry.enabledPreRelease";
#else
json.name = "toolkit.telemetry.enabled";
#endif
break;
#endif
}
switch (json.type) {
case "bool":
Services.prefs.setBoolPref(json.name, json.value);
break;
case "int":
Services.prefs.setIntPref(json.name, json.value);
break;
default: {
let pref = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
pref.data = json.value;
Services.prefs.setComplexValue(json.name, Ci.nsISupportsString, pref);
break;
}
}
},
sanitize: function (aItems) {
let json = JSON.parse(aItems);
let success = true;
for (let key in json) {
if (!json[key])
continue;
try {
switch (key) {
case "history_downloads":
Sanitizer.clearItem("history");
// If we're also removing downloaded files, don't clear the
// download history yet since it will be handled when the files are
// removed.
if (!json["downloadFiles"]) {
Sanitizer.clearItem("downloads");
}
break;
case "cookies_sessions":
Sanitizer.clearItem("cookies");
Sanitizer.clearItem("sessions");
break;
default:
Sanitizer.clearItem(key);
}
} catch (e) {
dump("sanitize error: " + e);
success = false;
}
}
sendMessageToJava({
type: "Sanitize:Finished",
success: success
});
},
getFocusedInput: function(aBrowser, aOnlyInputElements = false) {
if (!aBrowser)
return null;
let doc = aBrowser.contentDocument;
if (!doc)
return null;
let focused = doc.activeElement;
while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) {
doc = focused.contentDocument;
focused = doc.activeElement;
}
if (focused instanceof HTMLInputElement && focused.mozIsTextField(false))
return focused;
if (aOnlyInputElements)
return null;
if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) {
if (focused instanceof HTMLBodyElement) {
// we are putting focus into a contentEditable frame. scroll the frame into
// view instead of the contentEditable document contained within, because that
// results in a better user experience
focused = focused.ownerDocument.defaultView.frameElement;
}
return focused;
}
return null;
},
scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
if (formHelperMode == kFormHelperModeDisabled)
return;
let focused = this.getFocusedInput(aBrowser);
if (focused) {
let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom");
if (formHelperMode == kFormHelperModeDynamic && this.isTablet)
shouldZoom = false;
// ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen
ZoomHelper.zoomToElement(focused, -1, false,
aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified);
}
},
observe: function(aSubject, aTopic, aData) {
let browser = this.selectedBrowser;
switch (aTopic) {
case "Session:Back":
browser.goBack();
break;
case "Session:Forward":
browser.goForward();
break;
case "Session:Reload": {
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
// Check to see if this is a message to enable/disable mixed content blocking.
if (aData) {
let allowMixedContent = JSON.parse(aData).allowMixedContent;
if (allowMixedContent) {
// Set a flag to disable mixed content blocking.
flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
} else {
// Set mixedContentChannel to null to re-enable mixed content blocking.
let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell);
docShell.mixedContentChannel = null;
}
}
// Try to use the session history to reload so that framesets are
// handled properly. If the window has no session history, fall back
// to using the web navigation's reload method.
let webNav = browser.webNavigation;
try {
let sh = webNav.sessionHistory;
if (sh)
webNav = sh.QueryInterface(Ci.nsIWebNavigation);
} catch (e) {}
webNav.reload(flags);
break;
}
case "Session:Stop":
browser.stop();
break;
case "Session:ShowHistory": {
let data = JSON.parse(aData);
this.showHistory(data.fromIndex, data.toIndex, data.selIndex);
break;
}
case "Tab:Load": {
let data = JSON.parse(aData);
// Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from
// inheriting the currently loaded document's principal.
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
if (data.userEntered) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER;
}
let delayLoad = ("delayLoad" in data) ? data.delayLoad : false;
let params = {
selected: ("selected" in data) ? data.selected : !delayLoad,
parentId: ("parentId" in data) ? data.parentId : -1,
flags: flags,
tabID: data.tabID,
isPrivate: (data.isPrivate === true),
pinned: (data.pinned === true),
delayLoad: (delayLoad === true),
desktopMode: (data.desktopMode === true)
};
let url = data.url;
if (data.engine) {
let engine = Services.search.getEngineByName(data.engine);
if (engine) {
params.userSearch = url;
let submission = engine.getSubmission(url);
url = submission.uri.spec;
params.postData = submission.postData;
}
}
if (data.newTab) {
this.addTab(url, params);
} else {
if (data.tabId) {
// Use a specific browser instead of the selected browser, if it exists
let specificBrowser = this.getTabForId(data.tabId).browser;
if (specificBrowser)
browser = specificBrowser;
}
this.loadURI(url, browser, params);
}
break;
}
case "Tab:Selected":
this._handleTabSelected(this.getTabForId(parseInt(aData)));
break;
case "Tab:Closed":
this._handleTabClosed(this.getTabForId(parseInt(aData)));
break;
case "keyword-search":
// This event refers to a search via the URL bar, not a bookmarks
// keyword search. Note that this code assumes that the user can only
// perform a keyword search on the selected tab.
this.selectedTab.userSearch = aData;
let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
sendMessageToJava({
type: "Search:Keyword",
identifier: engine.identifier,
name: engine.name,
});
break;
case "Browser:Quit":
this.quit();
break;
case "SaveAs:PDF":
this.saveAsPDF(browser);
break;
case "Preferences:Set":
this.setPreferences(aData);
break;
case "ScrollTo:FocusedInput":
// these messages come from a change in the viewable area and not user interaction
// we allow scrolling to the selected input, but not zooming the page
this.scrollToFocusedInput(browser, false);
break;
case "Sanitize:ClearData":
this.sanitize(aData);
break;
case "FullScreen:Exit":
browser.contentDocument.mozCancelFullScreen();
break;
case "Viewport:Change":
if (this.isBrowserContentDocumentDisplayed())
this.selectedTab.setViewport(JSON.parse(aData));
break;
case "Viewport:Flush":
this.contentDocumentChanged();
break;
case "Passwords:Init": {
let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"].
getService(Ci.nsILoginManagerStorage);
storage.init();
Services.obs.removeObserver(this, "Passwords:Init");
break;
}
case "FormHistory:Init": {
// Force creation/upgrade of formhistory.sqlite
FormHistory.count({});
Services.obs.removeObserver(this, "FormHistory:Init");
break;
}
case "sessionstore-state-purge-complete":
sendMessageToJava({ type: "Session:StatePurged" });
break;
case "gather-telemetry":
sendMessageToJava({ type: "Telemetry:Gather" });
break;
case "Viewport:FixedMarginsChanged":
gViewportMargins = JSON.parse(aData);
this.selectedTab.updateViewportSize(gScreenWidth);
break;
case "nsPref:changed":
this.notifyPrefObservers(aData);
break;
#ifdef MOZ_ANDROID_SYNTHAPKS
case "webapps-runtime-install":
WebappManager.install(JSON.parse(aData), aSubject);
break;
case "webapps-runtime-install-package":
WebappManager.installPackage(JSON.parse(aData), aSubject);
break;
case "webapps-ask-install":
WebappManager.askInstall(JSON.parse(aData));
break;
case "webapps-launch": {
WebappManager.launch(JSON.parse(aData));
break;
}
case "webapps-uninstall": {
WebappManager.uninstall(JSON.parse(aData));
break;
}
case "Webapps:AutoInstall":
WebappManager.autoInstall(JSON.parse(aData));
break;
case "Webapps:Load":
this._loadWebapp(JSON.parse(aData));
break;
case "Webapps:AutoUninstall":
WebappManager.autoUninstall(JSON.parse(aData));
break;
#endif
case "Locale:Changed":
// The value provided to Locale:Changed should be a BCP47 language tag
// understood by Gecko -- for example, "es-ES" or "de".
console.log("Locale:Changed: " + aData);
// TODO: do we need to be more nuanced here -- e.g., checking for the
// OS locale -- or should it always be false on Fennec?
Services.prefs.setBoolPref("intl.locale.matchOS", false);
Services.prefs.setCharPref("general.useragent.locale", aData);
break;
default:
dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
break;
}
},
get defaultBrowserWidth() {
delete this.defaultBrowserWidth;
let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");
return this.defaultBrowserWidth = width;
},
// nsIAndroidBrowserApp
getBrowserTab: function(tabId) {
return this.getTabForId(tabId);
},
getUITelemetryObserver: function() {
return UITelemetry;
},
getPreferences: function getPreferences(requestId, prefNames, count) {
this.handlePreferencesRequest(requestId, prefNames, false);
},
observePreferences: function observePreferences(requestId, prefNames, count) {
this.handlePreferencesRequest(requestId, prefNames, true);
},
removePreferenceObservers: function removePreferenceObservers(aRequestId) {
let newPrefObservers = [];
for (let prefName in this._prefObservers) {
let requestIds = this._prefObservers[prefName];
// Remove the requestID from the preference handlers
let i = requestIds.indexOf(aRequestId);
if (i >= 0) {
requestIds.splice(i, 1);
}
// If there are no more request IDs, remove the observer
if (requestIds.length == 0) {
Services.prefs.removeObserver(prefName, this);
} else {
newPrefObservers[prefName] = requestIds;
}
}
this._prefObservers = newPrefObservers;
},
// This method will print a list from fromIndex to toIndex, optionally
// selecting selIndex(if fromIndex<=selIndex<=toIndex)
showHistory: function(fromIndex, toIndex, selIndex) {
let browser = this.selectedBrowser;
let hist = browser.sessionHistory;
let listitems = [];
for (let i = toIndex; i >= fromIndex; i--) {
let entry = hist.getEntryAtIndex(i, false);
let item = {
label: entry.title || entry.URI.spec,
selected: (i == selIndex)
};
listitems.push(item);
}
let p = new Prompt({
window: browser.contentWindow
}).setSingleChoiceItems(listitems).show(function(data) {
let selected = data.button;
if (selected == -1)
return;
browser.gotoIndex(toIndex-selected);
});
},
};
var NativeWindow = {
init: function() {
Services.obs.addObserver(this, "Menu:Clicked", false);
Services.obs.addObserver(this, "PageActions:Clicked", false);
Services.obs.addObserver(this, "PageActions:LongClicked", false);
Services.obs.addObserver(this, "Doorhanger:Reply", false);
Services.obs.addObserver(this, "Toast:Click", false);
Services.obs.addObserver(this, "Toast:Hidden", false);
this.contextmenus.init();
},
uninit: function() {
Services.obs.removeObserver(this, "Menu:Clicked");
Services.obs.removeObserver(this, "PageActions:Clicked");
Services.obs.removeObserver(this, "PageActions:LongClicked");
Services.obs.removeObserver(this, "Doorhanger:Reply");
Services.obs.removeObserver(this, "Toast:Click", false);
Services.obs.removeObserver(this, "Toast:Hidden", false);
this.contextmenus.uninit();
},
loadDex: function(zipFile, implClass) {
sendMessageToJava({
type: "Dex:Load",
zipfile: zipFile,
impl: implClass || "Main"
});
},
unloadDex: function(zipFile) {
sendMessageToJava({
type: "Dex:Unload",
zipfile: zipFile
});
},
toast: {
_callbacks: {},
show: function(aMessage, aDuration, aOptions) {
let msg = {
type: "Toast:Show",
message: aMessage,
duration: aDuration
};
if (aOptions && aOptions.button) {
msg.button = {
label: aOptions.button.label,
id: uuidgen.generateUUID().toString(),
// If the caller specified a button, make sure we convert any chrome urls
// to jar:jar urls so that the frontend can show them
icon: aOptions.button.icon ? resolveGeckoURI(aOptions.button.icon) : null,
};
this._callbacks[msg.button.id] = aOptions.button.callback;
}
sendMessageToJava(msg);
}
},
pageactions: {
_items: { },
add: function(aOptions) {
let id = uuidgen.generateUUID().toString();
sendMessageToJava({
type: "PageActions:Add",
id: id,
title: aOptions.title,
icon: resolveGeckoURI(aOptions.icon),
important: "important" in aOptions ? aOptions.important : false
});
this._items[id] = {
clickCallback: aOptions.clickCallback,
longClickCallback: aOptions.longClickCallback
};
return id;
},
remove: function(id) {
sendMessageToJava({
type: "PageActions:Remove",
id: id
});
delete this._items[id];
}
},
menu: {
_callbacks: [],
_menuId: 1,
toolsMenuID: -1,
add: function() {
let options;
if (arguments.length == 1) {
options = arguments[0];
} else if (arguments.length == 3) {
options = {
name: arguments[0],
icon: arguments[1],
callback: arguments[2]
};
} else {
throw "Incorrect number of parameters";
}
options.type = "Menu:Add";
options.id = this._menuId;
sendMessageToJava(options);
this._callbacks[this._menuId] = options.callback;
this._menuId++;
return this._menuId - 1;
},
remove: function(aId) {
sendMessageToJava({ type: "Menu:Remove", id: aId });
},
update: function(aId, aOptions) {
if (!aOptions)
return;
sendMessageToJava({
type: "Menu:Update",
id: aId,
options: aOptions
});
}
},
doorhanger: {
_callbacks: {},
_callbacksId: 0,
_promptId: 0,
/**
* @param aOptions
* An options JavaScript object holding additional properties for the
* notification. The following properties are currently supported:
* persistence: An integer. The notification will not automatically
* dismiss for this many page loads. If persistence is set
* to -1, the doorhanger will never automatically dismiss.
* persistWhileVisible:
* A boolean. If true, a visible notification will always
* persist across location changes.
* timeout: A time in milliseconds. The notification will not
* automatically dismiss before this time.
* checkbox: A string to appear next to a checkbox under the notification
* message. The button callback functions will be called with
* the checked state as an argument.
*/
show: function(aMessage, aValue, aButtons, aTabID, aOptions) {
if (aButtons == null) {
aButtons = [];
}
aButtons.forEach((function(aButton) {
this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId };
aButton.callback = this._callbacksId;
this._callbacksId++;
}).bind(this));
this._promptId++;
let json = {
type: "Doorhanger:Add",
message: aMessage,
value: aValue,
buttons: aButtons,
// use the current tab if none is provided
tabID: aTabID || BrowserApp.selectedTab.id,
options: aOptions || {}
};
sendMessageToJava(json);
},
hide: function(aValue, aTabID) {
sendMessageToJava({
type: "Doorhanger:Remove",
value: aValue,
tabID: aTabID
});
}
},
observe: function(aSubject, aTopic, aData) {
if (aTopic == "Menu:Clicked") {
if (this.menu._callbacks[aData])
this.menu._callbacks[aData]();
} else if (aTopic == "PageActions:Clicked") {
if (this.pageactions._items[aData].clickCallback)
this.pageactions._items[aData].clickCallback();
} else if (aTopic == "PageActions:LongClicked") {
if (this.pageactions._items[aData].longClickCallback)
this.pageactions._items[aData].longClickCallback();
} else if (aTopic == "Toast:Click") {
if (this.toast._callbacks[aData]) {
this.toast._callbacks[aData]();
delete this.toast._callbacks[aData];
}
} else if (aTopic == "Toast:Hidden") {
if (this.toast._callbacks[aData])
delete this.toast._callbacks[aData];
} else if (aTopic == "Doorhanger:Reply") {
let data = JSON.parse(aData);
let reply_id = data["callback"];
if (this.doorhanger._callbacks[reply_id]) {
// Pass the value of the optional checkbox to the callback
let checked = data["checked"];
this.doorhanger._callbacks[reply_id].cb(checked, data.inputs);
let prompt = this.doorhanger._callbacks[reply_id].prompt;
for (let id in this.doorhanger._callbacks) {
if (this.doorhanger._callbacks[id].prompt == prompt) {
delete this.doorhanger._callbacks[id];
}
}
}
}
},
contextmenus: {
items: {}, // a list of context menu items that we may show
DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items
init: function() {
Services.obs.addObserver(this, "Gesture:LongPress", false);
},
uninit: function() {
Services.obs.removeObserver(this, "Gesture:LongPress");
},
add: function() {
let args;
if (arguments.length == 1) {
args = arguments[0];
} else if (arguments.length == 3) {
args = {
label : arguments[0],
selector: arguments[1],
callback: arguments[2]
};
} else {
throw "Incorrect number of parameters";
}
if (!args.label)
throw "Menu items must have a name";
let cmItem = new ContextMenuItem(args);
this.items[cmItem.id] = cmItem;
return cmItem.id;
},
remove: function(aId) {
delete this.items[aId];
},
SelectorContext: function(aSelector) {
return {
matches: function(aElt) {
if (aElt.mozMatchesSelector)
return aElt.mozMatchesSelector(aSelector);
return false;
}
};
},
linkOpenableNonPrivateContext: {
matches: function linkOpenableNonPrivateContextMatches(aElement) {
let doc = aElement.ownerDocument;
if (!doc || PrivateBrowsingUtils.isWindowPrivate(doc.defaultView)) {
return false;
}
return NativeWindow.contextmenus.linkOpenableContext.matches(aElement);
}
},
linkOpenableContext: {
matches: function linkOpenableContextMatches(aElement) {
let uri = NativeWindow.contextmenus._getLink(aElement);
if (uri) {
let scheme = uri.scheme;
let dontOpen = /^(javascript|mailto|news|snews|tel)$/;
return (scheme && !dontOpen.test(scheme));
}
return false;
}
},
linkCopyableContext: {
matches: function linkCopyableContextMatches(aElement) {
let uri = NativeWindow.contextmenus._getLink(aElement);
if (uri) {
let scheme = uri.scheme;
let dontCopy = /^(mailto|tel)$/;
return (scheme && !dontCopy.test(scheme));
}
return false;
}
},
linkShareableContext: {
matches: function linkShareableContextMatches(aElement) {
let uri = NativeWindow.contextmenus._getLink(aElement);
if (uri) {
let scheme = uri.scheme;
let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/;
return (scheme && !dontShare.test(scheme));
}
return false;
}
},
linkBookmarkableContext: {
matches: function linkBookmarkableContextMatches(aElement) {
let uri = NativeWindow.contextmenus._getLink(aElement);
if (uri) {
let scheme = uri.scheme;
let dontBookmark = /^(mailto|tel)$/;
return (scheme && !dontBookmark.test(scheme));
}
return false;
}
},
emailLinkContext: {
matches: function emailLinkContextMatches(aElement) {
let uri = NativeWindow.contextmenus._getLink(aElement);
if (uri)
return uri.schemeIs("mailto");
return false;
}
},
phoneNumberLinkContext: {
matches: function phoneNumberLinkContextMatches(aElement) {
let uri = NativeWindow.contextmenus._getLink(aElement);
if (uri)
return uri.schemeIs("tel");
return false;
}
},
textContext: {
matches: function textContext(aElement) {
return ((aElement instanceof Ci.nsIDOMHTMLInputElement && aElement.mozIsTextField(false))
|| aElement instanceof Ci.nsIDOMHTMLTextAreaElement);
}
},
imageLocationCopyableContext: {
matches: function imageLinkCopyableContextMatches(aElement) {
return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI);
}
},
imageSaveableContext: {
matches: function imageSaveableContextMatches(aElement) {
if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) {
// The image must be loaded to allow saving
let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE));
}
return false;
}
},
mediaSaveableContext: {
matches: function mediaSaveableContextMatches(aElement) {
return (aElement instanceof HTMLVideoElement ||
aElement instanceof HTMLAudioElement);
}
},
mediaContext: function(aMode) {
return {
matches: function(aElt) {
if (aElt instanceof Ci.nsIDOMHTMLMediaElement) {
let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE;
if (hasError)
return false;
let paused = aElt.paused || aElt.ended;
if (paused && aMode == "media-paused")
return true;
if (!paused && aMode == "media-playing")
return true;
let controls = aElt.controls;
if (!controls && aMode == "media-hidingcontrols")
return true;
let muted = aElt.muted;
if (muted && aMode == "media-muted")
return true;
else if (!muted && aMode == "media-unmuted")
return true;
}
return false;
}
};
},
/* Holds a WeakRef to the original target element this context menu was shown for.
* Most API's will have to walk up the tree from this node to find the correct element
* to act on
*/
get _target() {
if (this._targetRef)
return this._targetRef.get();
return null;
},
set _target(aTarget) {
if (aTarget)
this._targetRef = Cu.getWeakReference(aTarget);
else this._targetRef = null;
},
/* Gets menuitems for an arbitrary node
* Parameters:
* element - The element to look at. If this element has a contextmenu attribute, the
* corresponding contextmenu will be used.
*/
_getHTMLContextMenuItemsForElement: function(element) {
let htmlMenu = element.contextMenu;
if (!htmlMenu) {
return [];
}
htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
htmlMenu.sendShowEvent();
return this._getHTMLContextMenuItemsForMenu(htmlMenu, element);
},
/* Add a menuitem for an HTML <menu> node
* Parameters:
* menu - The <menu> element to iterate through for menuitems
* target - The target element these context menu items are attached to
*/
_getHTMLContextMenuItemsForMenu: function(menu, target) {
let items = [];
for (let i = 0; i < menu.childNodes.length; i++) {
let elt = menu.childNodes[i];
if (!elt.label)
continue;
items.push(new HTMLContextMenuItem(elt, target));
}
return items;
},
// Searches the current list of menuitems to show for any that match this id
_findMenuItem: function(aId) {
if (!this.menus) {
return null;
}
for (let context in this.menus) {
let menu = this.menus[context];
for (let i = 0; i < menu.length; i++) {
if (menu[i].id === aId) {
return menu[i];
}
}
}
return null;
},
// Returns true if there are any context menu items to show
shouldShow: function() {
for (let context in this.menus) {
let menu = this.menus[context];
if (menu.length > 0) {
return true;
}
}
return false;
},
/* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this
* is an image inside an <a> tag, we may have a "link" context and an "image" one.
*/
_getContextType: function(element) {
// For anchor nodes, we try to use the scheme to pick a string
if (element instanceof Ci.nsIDOMHTMLAnchorElement) {
let uri = this.makeURI(this._getLinkURL(element));
try {
return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme);
} catch(ex) { }
}
// Otherwise we try the nodeName
try {
return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase());
} catch(ex) { }
// Fallback to the default
return Strings.browser.GetStringFromName("browser.menu.context.default");
},
// Adds context menu items added through the add-on api
_getNativeContextMenuItems: function(element, x, y) {
let res = [];
for (let itemId of Object.keys(this.items)) {
let item = this.items[itemId];
if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
res.push(item);
}
}
return res;
},
/* Checks if there are context menu items to show, and if it finds them
* sends a contextmenu event to content. We also send showing events to
* any html5 context menus we are about to show, and fire some local notifications
* for chrome consumers to do lazy menuitem construction
*/
_sendToContent: function(x, y) {
let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(x, y);
if (!target)
target = ElementTouchHelper.anyElementFromPoint(x, y);
if (!target)
return;
this._target = target;
Services.obs.notifyObservers(null, "before-build-contextmenu", "");
this._buildMenu(x, y);
// only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap)
if (this.shouldShow()) {
let event = target.ownerDocument.createEvent("MouseEvent");
event.initMouseEvent("contextmenu", true, true, target.defaultView,
0, x, y, x, y, false, false, false, false,
0, null);
target.ownerDocument.defaultView.addEventListener("contextmenu", this, false);
target.dispatchEvent(event);
} else {
this.menus = null;
Services.obs.notifyObservers({target: target, x: x, y: y}, "context-menu-not-shown", "");
if (SelectionHandler.canSelect(target)) {
if (!SelectionHandler.startSelection(target, {
mode: SelectionHandler.SELECT_AT_POINT,
x: x,
y: y
})) {
SelectionHandler.attachCaret(target);
}
}
}
},
// Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
_getTitle: function(node) {
if (node.hasAttribute && node.hasAttribute("title")) {
return node.getAttribute("title");
}
return this._getUrl(node);
},
// Returns a url associated with a node
_getUrl: function(node) {
if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
(node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
return this._getLinkURL(node);
} else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) {
return node.currentURI.spec;
} else if (node instanceof Ci.nsIDOMHTMLMediaElement) {
return (node.currentSrc || node.src);
}
return "";
},
// Adds an array of menuitems to the current list of items to show, in the correct context
_addMenuItems: function(items, context) {
if (!this.menus[context]) {
this.menus[context] = [];
}
this.menus[context] = this.menus[context].concat(items);
},
/* Does the basic work of building a context menu to show. Will combine HTML and Native
* context menus items, as well as sorting menuitems into different menus based on context.
*/
_buildMenu: function(x, y) {
// now walk up the tree and for each node look for any context menu items that apply
let element = this._target;
// this.menus holds a hashmap of "contexts" to menuitems associated with that context
// For instance, if the user taps an image inside a link, we'll have something like:
// {
// link: [ ContextMenuItem, ContextMenuItem ]
// image: [ ContextMenuItem, ContextMenuItem ]
// }
this.menus = {};
while (element) {
let context = this._getContextType(element);
// First check for any html5 context menus that might exist...
var items = this._getHTMLContextMenuItemsForElement(element);
if (items.length > 0) {
this._addMenuItems(items, context);
}
// then check for any context menu items registered in the ui.
items = this._getNativeContextMenuItems(element, x, y);
if (items.length > 0) {
this._addMenuItems(items, context);
}
// walk up the tree and find more items to show
element = element.parentNode;
}
},
// Actually shows the native context menu by passing a list of context menu items to
// show to the Java.
_show: function(aEvent) {
let popupNode = this._target;
this._target = null;
if (aEvent.defaultPrevented || !popupNode) {
return;
}
this._innerShow(popupNode, aEvent.clientX, aEvent.clientY);
},
// Walks the DOM tree to find a title from a node
_findTitle: function(node) {
let title = "";
while(node && !title) {
title = this._getTitle(node);
node = node.parentNode;
}
return title;
},
/* Reformats the list of menus to show into an object that can be sent to Prompt.jsm
* If there is one menu, will return a flat array of menuitems. If there are multiple
* menus, will return an array with appropriate tabs/items inside it. i.e. :
* [
* { label: "link", items: [...] },
* { label: "image", items: [...] }
* ]
*/
_reformatList: function(target) {
let contexts = Object.keys(this.menus);
if (contexts.length == 1) {
// If there's only one context, we'll only show a single flat single select list
return this._reformatMenuItems(target, this.menus[contexts[0]]);
}
// If there are multiple contexts, we'll only show a tabbed ui with multiple lists
return this._reformatListAsTabs(target, this.menus);
},
/* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's
* addTabs method. i.e. :
* { link: [...], image: [...] } becomes
* [ { label: "link", items: [...] } ]
*
* Also reformats items and resolves any parmaeters that aren't known until display time
* (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
*/
_reformatListAsTabs: function(target, menus) {
let itemArray = [];
for (let context in menus) {
itemArray.push({
label: context,
items: this._reformatMenuItems(target, menus[context])
});
}
return itemArray;
},
/* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items
* and resolves any parmaeters that aren't known until display time
* (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
*/
_reformatMenuItems: function(target, menuitems) {
let itemArray = [];
for (let i = 0; i < menuitems.length; i++) {
let t = target;
while(t) {
if (menuitems[i].matches(t)) {
let val = menuitems[i].getValue(t);
// hidden menu items will return null from getValue
if (val) {
itemArray.push(val);
break;
}
}
t = t.parentNode;
}
}
return itemArray;
},
// Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt.
_innerShow: function(target, x, y) {
Haptic.performSimpleAction(Haptic.LongPress);
// spin through the tree looking for a title for this context menu
let title = this._findTitle(target);
for (let context in this.menus) {
let menu = this.menus[context];
menu.sort((a,b) => {
if (a.order === b.order) {
return 0;
}
return (a.order > b.order) ? 1 : -1;
});
}
let useTabs = Object.keys(this.menus).length > 1;
let prompt = new Prompt({
window: target.ownerDocument.defaultView,
title: useTabs ? undefined : title
});
let items = this._reformatList(target);
if (useTabs) {
prompt.addTabs({
id: "tabs",
items: items
});
} else {
prompt.setSingleChoiceItems(items);
}
prompt.show(this._promptDone.bind(this, target, x, y, items));
},
// Called when the contextmenu prompt is closed
_promptDone: function(target, x, y, items, data) {
if (data.button == -1) {
// Prompt was cancelled, or an ActionView was used.
return;
}
let selectedItemId;
if (data.tabs) {
let menu = items[data.tabs.tab];
selectedItemId = menu.items[data.tabs.item].id;
} else {
selectedItemId = items[data.list[0]].id
}
let selectedItem = this._findMenuItem(selectedItemId);
this.menus = null;
if (!selectedItem || !selectedItem.matches || !selectedItem.callback) {
return;
}
// for menuitems added using the native UI, pass the dom element that matched that item to the callback
while (target) {
if (selectedItem.matches(target, x, y)) {
selectedItem.callback(target, x, y);
break;
}
target = target.parentNode;
}
},
// Called when the contextmenu is done propagating to content. If the event wasn't cancelled, will show a contextmenu.
handleEvent: function(aEvent) {
BrowserEventHandler._cancelTapHighlight();
aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false);
this._show(aEvent);
},
// Called when a long press is observed in the native Java frontend. Will start the process of generating/showing a contextmenu.
observe: function(aSubject, aTopic, aData) {
let data = JSON.parse(aData);
// content gets first crack at cancelling context menus
this._sendToContent(data.x, data.y);
},
// XXX - These are stolen from Util.js, we should remove them if we bring it back
makeURLAbsolute: function makeURLAbsolute(base, url) {
// Note: makeURI() will throw if url is not a valid URI
return this.makeURI(url, null, this.makeURI(base)).spec;
},
makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
},
_getLink: function(aElement) {
if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
(aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) ||
aElement instanceof Ci.nsIDOMHTMLLinkElement ||
aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) {
try {
let url = this._getLinkURL(aElement);
return Services.io.newURI(url, null, null);
} catch (e) {}
}
return null;
},
_disableInGuest: function _disableInGuest(selector) {
return {
matches: function _disableInGuestMatches(aElement, aX, aY) {
if (BrowserApp.isGuest)
return false;
return selector.matches(aElement, aX, aY);
}
};
},
_getLinkURL: function ch_getLinkURL(aLink) {
let href = aLink.href;
if (href)
return href;
href = aLink.getAttributeNS(kXLinkNamespace, "href");
if (!href || !href.match(/\S/)) {
// Without this we try to save as the current doc,
// for example, HTML case also throws if empty
throw "Empty href";
}
return this.makeURLAbsolute(aLink.baseURI, href);
},
_copyStringToDefaultClipboard: function(aString) {
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
clipboard.copyString(aString);
},
_shareStringWithDefault: function(aSharedString, aTitle) {
let sharing = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService);
sharing.shareWithDefault(aSharedString, "text/plain", aTitle);
},
_stripScheme: function(aString) {
let index = aString.indexOf(":");
return aString.slice(index + 1);
}
}
};
var LightWeightThemeWebInstaller = {
init: function sh_init() {
let temp = {};
Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp);
let theme = new temp.LightweightThemeConsumer(document);
BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
},
uninit: function() {
BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
},
handleEvent: function (event) {
switch (event.type) {
case "InstallBrowserTheme":
case "PreviewBrowserTheme":
case "ResetBrowserThemePreview":
// ignore requests from background tabs
if (event.target.ownerDocument.defaultView.top != content)
return;
}
switch (event.type) {
case "InstallBrowserTheme":
this._installRequest(event);
break;
case "PreviewBrowserTheme":
this._preview(event);
break;
case "ResetBrowserThemePreview":
this._resetPreview(event);
break;
case "pagehide":
case "TabSelect":
this._resetPreview();
break;
}
},
get _manager () {
let temp = {};
Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
delete this._manager;
return this._manager = temp.LightweightThemeManager;
},
_installRequest: function (event) {
let node = event.target;
let data = this._getThemeFromNode(node);
if (!data)
return;
if (this._isAllowed(node)) {
this._install(data);
return;
}
let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton");
let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1);
let buttons = [{
label: allowButtonText,
callback: function () {
LightWeightThemeWebInstaller._install(data);
}
}];
NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id);
},
_install: function (newLWTheme) {
this._manager.currentTheme = newLWTheme;
},
_previewWindow: null,
_preview: function (event) {
if (!this._isAllowed(event.target))
return;
let data = this._getThemeFromNode(event.target);
if (!data)
return;
this._resetPreview();
this._previewWindow = event.target.ownerDocument.defaultView;
this._previewWindow.addEventListener("pagehide", this, true);
BrowserApp.deck.addEventListener("TabSelect", this, false);
this._manager.previewTheme(data);
},
_resetPreview: function (event) {
if (!this._previewWindow ||
event && !this._isAllowed(event.target))
return;
this._previewWindow.removeEventListener("pagehide", this, true);
this._previewWindow = null;
BrowserApp.deck.removeEventListener("TabSelect", this, false);
this._manager.resetPreview();
},
_isAllowed: function (node) {
let pm = Services.perms;
let uri = node.ownerDocument.documentURIObject;
return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
},
_getThemeFromNode: function (node) {
return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI);
}
};
var DesktopUserAgent = {
DESKTOP_UA: null,
init: function ua_init() {
Services.obs.addObserver(this, "DesktopMode:Change", false);
UserAgentOverrides.addComplexOverride(this.onRequest.bind(this));
// See https://developer.mozilla.org/en/Gecko_user_agent_string_reference
this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
.getService(Ci.nsIHttpProtocolHandler).userAgent
.replace(/Android; [a-zA-Z]+/, "X11; Linux x86_64")
.replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
},
uninit: function ua_uninit() {
Services.obs.removeObserver(this, "DesktopMode:Change");
},
onRequest: function(channel, defaultUA) {
let channelWindow = this._getWindowForRequest(channel);
let tab = BrowserApp.getTabForWindow(channelWindow);
if (tab == null)
return null;
return this.getUserAgentForTab(tab);
},
getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) {
let tab = BrowserApp.getTabForWindow(aWindow.top);
if (tab)
return this.getUserAgentForTab(tab);
return null;
},
getUserAgentForTab: function ua_getUserAgentForTab(aTab) {
// Send desktop UA if "Request Desktop Site" is enabled.
if (aTab.desktopMode)
return this.DESKTOP_UA;
return null;
},
_getRequestLoadContext: function ua_getRequestLoadContext(aRequest) {
if (aRequest && aRequest.notificationCallbacks) {
try {
return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
} catch (ex) { }
}
if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) {
try {
return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
} catch (ex) { }
}
return null;
},
_getWindowForRequest: function ua_getWindowForRequest(aRequest) {
let loadContext = this._getRequestLoadContext(aRequest);
if (loadContext) {
try {
return loadContext.associatedWindow;
} catch (e) {
// loadContext.associatedWindow can throw when there's no window
}
}
return null;
},
observe: function ua_observe(aSubject, aTopic, aData) {
if (aTopic === "DesktopMode:Change") {
let args = JSON.parse(aData);
let tab = BrowserApp.getTabForId(args.tabId);
if (tab != null)
tab.reloadWithMode(args.desktopMode);
}
}
};
function nsBrowserAccess() {
}
nsBrowserAccess.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]),
_getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) {
let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
if (isExternal && aURI && aURI.schemeIs("chrome"))
return null;
let loadflags = isExternal ?
Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
switch (aContext) {
case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL:
aWhere = Services.prefs.getIntPref("browser.link.open_external");
break;
default: // OPEN_NEW or an illegal value
aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
}
}
Services.io.offline = false;
let referrer;
if (aOpener) {
try {
let location = aOpener.location;
referrer = Services.io.newURI(location, null, null);
} catch(e) { }
}
let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
let pinned = false;
if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
pinned = true;
let spec = aURI.spec;
let tabs = BrowserApp.tabs;
for (let i = 0; i < tabs.length; i++) {
let appOrigin = ss.getTabValue(tabs[i], "appOrigin");
if (appOrigin == spec) {
let tab = tabs[i];
BrowserApp.selectTab(tab);
return tab.browser;
}
}
}
let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB);
let isPrivate = false;
if (newTab) {
let parentId = -1;
if (!isExternal && aOpener) {
let parent = BrowserApp.getTabForWindow(aOpener.top);
if (parent) {
parentId = parent.id;
isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
}
}
// BrowserApp.addTab calls loadURIWithFlags with the appropriate params
let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags,
referrerURI: referrer,
external: isExternal,
parentId: parentId,
selected: true,
isPrivate: isPrivate,
pinned: pinned });
return tab.browser;
}
// OPEN_CURRENTWINDOW and illegal values
let browser = BrowserApp.selectedBrowser;
if (aURI && browser)
browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
return browser;
},
openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) {
let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
return browser ? browser.contentWindow : null;
},
openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) {
let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
},
isTabContentWindow: function(aWindow) {
return BrowserApp.getBrowserForWindow(aWindow) != null;
},
get contentWindow() {
return BrowserApp.selectedBrowser.contentWindow;
}
};
// track the last known screen size so that new tabs
// get created with the right size rather than being 1x1
let gScreenWidth = 1;
let gScreenHeight = 1;
let gReflowPending = null;
// The margins that should be applied to the viewport for fixed position
// children. This is used to avoid browser chrome permanently obscuring
// fixed position content, and also to make sure window-sized pages take
// into account said browser chrome.
let gViewportMargins = { top: 0, right: 0, bottom: 0, left: 0};
function Tab(aURL, aParams) {
this.browser = null;
this.id = 0;
this.lastTouchedAt = Date.now();
this._zoom = 1.0;
this._drawZoom = 1.0;
this._fixedMarginLeft = 0;
this._fixedMarginTop = 0;
this._fixedMarginRight = 0;
this._fixedMarginBottom = 0;
this._readerEnabled = false;
this._readerActive = false;
this.userScrollPos = { x: 0, y: 0 };
this.viewportExcludesHorizontalMargins = true;
this.viewportExcludesVerticalMargins = true;
this.viewportMeasureCallback = null;
this.lastPageSizeAfterViewportRemeasure = { width: 0, height: 0 };
this.contentDocumentIsDisplayed = true;
this.pluginDoorhangerTimeout = null;
this.shouldShowPluginDoorhanger = true;
this.clickToPlayPluginsActivated = false;
this.desktopMode = false;
this.originalURI = null;
this.savedArticle = null;
this.hasTouchListener = false;
this.browserWidth = 0;
this.browserHeight = 0;
this.create(aURL, aParams);
}
Tab.prototype = {
create: function(aURL, aParams) {
if (this.browser)
return;
aParams = aParams || {};
this.browser = document.createElement("browser");
this.browser.setAttribute("type", "content-targetable");
this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
// Make sure the previously selected panel remains selected. The selected panel of a deck is
// not stable when panels are added.
let selectedPanel = BrowserApp.deck.selectedPanel;
BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null);
BrowserApp.deck.selectedPanel = selectedPanel;
if (BrowserApp.manifestUrl) {
let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl);
if (manifest) {
let app = manifest.QueryInterface(Ci.mozIApplication);
this.browser.docShell.setIsApp(app.localId);
}
}
// Must be called after appendChild so the docshell has been created.
this.setActive(false);
let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate;
if (isPrivate) {
this.browser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = true;
}
this.browser.stop();
let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL;
// only set tab uri if uri is valid
let uri = null;
let title = aParams.title || aURL;
try {
uri = Services.io.newURI(aURL, null, null).spec;
} catch (e) {}
// When the tab is stubbed from Java, there's a window between the stub
// creation and the tab creation in Gecko where the stub could be removed
// or the selected tab can change (which is easiest to hit during startup).
// To prevent these races, we need to differentiate between tab stubs from
// Java and new tabs from Gecko.
let stub = false;
if (!aParams.zombifying) {
if ("tabID" in aParams) {
this.id = aParams.tabID;
stub = true;
} else {
let jni = new JNI();
let cls = jni.findClass("org/mozilla/gecko/Tabs");
let method = jni.getStaticMethodID(cls, "getNextTabId", "()I");
this.id = jni.callStaticIntMethod(cls, method);
jni.close();
}
this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false;
let message = {
type: "Tab:Added",
tabID: this.id,
uri: uri,
parentId: ("parentId" in aParams) ? aParams.parentId : -1,
external: ("external" in aParams) ? aParams.external : false,
selected: ("selected" in aParams) ? aParams.selected : true,
title: title,
delayLoad: aParams.delayLoad || false,
desktopMode: this.desktopMode,
isPrivate: isPrivate,
stub: stub
};
sendMessageToJava(message);
this.overscrollController = new OverscrollController(this);
}
this.browser.contentWindow.controllers.insertControllerAt(0, this.overscrollController);
let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
Ci.nsIWebProgress.NOTIFY_LOCATION |
Ci.nsIWebProgress.NOTIFY_SECURITY;
this.browser.addProgressListener(this, flags);
this.browser.sessionHistory.addSHistoryListener(this);
this.browser.addEventListener("DOMContentLoaded", this, true);
this.browser.addEventListener("DOMFormHasPassword", this, true);
this.browser.addEventListener("DOMLinkAdded", this, true);
this.browser.addEventListener("DOMTitleChanged", this, true);
this.browser.addEventListener("DOMWindowClose", this, true);
this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
this.browser.addEventListener("DOMAutoComplete", this, true);
this.browser.addEventListener("blur", this, true);
this.browser.addEventListener("scroll", this, true);
this.browser.addEventListener("MozScrolledAreaChanged", this, true);
// Note that the XBL binding is untrusted
this.browser.addEventListener("PluginBindingAttached", this, true, true);
this.browser.addEventListener("pageshow", this, true);
this.browser.addEventListener("MozApplicationManifest", this, true);
Services.obs.addObserver(this, "before-first-paint", false);
Services.obs.addObserver(this, "after-viewport-change", false);
Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false);
if (aParams.delayLoad) {
// If this is a zombie tab, attach restore data so the tab will be
// restored when selected
this.browser.__SS_data = {
entries: [{
url: aURL,
title: title
}],
index: 1
};
this.browser.__SS_restore = true;
} else {
let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
let charset = "charset" in aParams ? aParams.charset : null;
// The search term the user entered to load the current URL
this.userSearch = "userSearch" in aParams ? aParams.userSearch : "";
try {
this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData);
} catch(e) {
let message = {
type: "Content:LoadError",
tabID: this.id
};
sendMessageToJava(message);
dump("Handled load error: " + e);
}
}
},
/**
* Retrieves the font size in twips for a given element.
*/
getInflatedFontSizeFor: function(aElement) {
// GetComputedStyle should always give us CSS pixels for a font size.
let fontSizeStr = this.window.getComputedStyle(aElement)['fontSize'];
let fontSize = fontSizeStr.slice(0, -2);
return aElement.fontSizeInflation * fontSize;
},
/**
* This returns the zoom necessary to match the font size of an element to
* the minimum font size specified by the browser.zoom.reflowOnZoom.minFontSizeTwips
* preference.
*/
getZoomToMinFontSize: function(aElement) {
// We only use the font.size.inflation.minTwips preference because this is
// the only one that is controlled by the user-interface in the 'Settings'
// menu. Thus, if font.size.inflation.emPerLine is changed, this does not
// effect reflow-on-zoom.
let minFontSize = convertFromTwipsToPx(Services.prefs.getIntPref("font.size.inflation.minTwips"));
return minFontSize / this.getInflatedFontSizeFor(aElement);
},
clearReflowOnZoomPendingActions: function() {
// Reflow was completed, so now re-enable painting.
let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
let docShell = webNav.QueryInterface(Ci.nsIDocShell);
let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
docViewer.resumePainting();
BrowserApp.selectedTab._mReflozPositioned = false;
},
/**
* Reflow on zoom consists of a few different sub-operations:
*
* 1. When a double-tap event is seen, we verify that the correct preferences
* are enabled and perform the pre-position handling calculation. We also
* signal that reflow-on-zoom should be performed at this time, and pause
* painting.
* 2. During the next call to setViewport(), which is in the Tab prototype,
* we detect that a call to changeMaxLineBoxWidth should be performed. If
* we're zooming out, then the max line box width should be reset at this
* time. Otherwise, we call performReflowOnZoom.
* 2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to
* doChangeMaxLineBoxWidth, based on a timeout specified in preferences.
* 3. doChangeMaxLineBoxWidth changes the line box width (which also
* schedules a reflow event), and then calls ZoomHelper.zoomInAndSnapToRange.
* 4. ZoomHelper.zoomInAndSnapToRange performs the positioning of reflow-on-zoom
* and then re-enables painting.
*
* Some of the events happen synchronously, while others happen asynchronously.
* The following is a rough sketch of the progression of events:
*
* double tap event seen -> onDoubleTap() -> ... asynchronous ...
* -> setViewport() -> performReflowOnZoom() -> ... asynchronous ...
* -> doChangeMaxLineBoxWidth() -> ZoomHelper.zoomInAndSnapToRange()
* -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change')
* -> resumePainting()
*/
performReflowOnZoom: function(aViewport) {
let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom;
let viewportWidth = gScreenWidth / zoom;
let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
if (gReflowPending) {
clearTimeout(gReflowPending);
}
// We add in a bit of fudge just so that the end characters
// don't accidentally get clipped. 15px is an arbitrary choice.
gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
reflozTimeout,
viewportWidth - 15);
},
/**
* Reloads the tab with the desktop mode setting.
*/
reloadWithMode: function (aDesktopMode) {
// Set desktop mode for tab and send change to Java
if (this.desktopMode != aDesktopMode) {
this.desktopMode = aDesktopMode;
sendMessageToJava({
type: "DesktopMode:Changed",
desktopMode: aDesktopMode,
tabID: this.id
});
}
// Only reload the page for http/https schemes
let currentURI = this.browser.currentURI;
if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https"))
return;
let url = currentURI.spec;
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
if (this.originalURI && !this.originalURI.equals(currentURI)) {
// We were redirected; reload the original URL
url = this.originalURI.spec;
}
this.browser.docShell.loadURI(url, flags, null, null, null);
},
destroy: function() {
if (!this.browser)
return;
this.browser.contentWindow.controllers.removeController(this.overscrollController);
this.browser.removeProgressListener(this);
this.browser.sessionHistory.removeSHistoryListener(this);
this.browser.removeEventListener("DOMContentLoaded", this, true);
this.browser.removeEventListener("DOMFormHasPassword", this, true);
this.browser.removeEventListener("DOMLinkAdded", this, true);
this.browser.removeEventListener("DOMTitleChanged", this, true);
this.browser.removeEventListener("DOMWindowClose", this, true);
this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
this.browser.removeEventListener("DOMAutoComplete", this, true);
this.browser.removeEventListener("blur", this, true);
this.browser.removeEventListener("scroll", this, true);
this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
this.browser.removeEventListener("PluginBindingAttached", this, true);
this.browser.removeEventListener("pageshow", this, true);
this.browser.removeEventListener("MozApplicationManifest", this, true);
Services.obs.removeObserver(this, "before-first-paint");
Services.obs.removeObserver(this, "after-viewport-change");
Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this);
// Make sure the previously selected panel remains selected. The selected panel of a deck is
// not stable when panels are removed.
let selectedPanel = BrowserApp.deck.selectedPanel;
BrowserApp.deck.removeChild(this.browser);
BrowserApp.deck.selectedPanel = selectedPanel;
this.browser = null;
this.savedArticle = null;
},
// This should be called to update the browser when the tab gets selected/unselected
setActive: function setActive(aActive) {
if (!this.browser || !this.browser.docShell)
return;
this.lastTouchedAt = Date.now();
if (aActive) {
this.browser.setAttribute("type", "content-primary");
this.browser.focus();
this.browser.docShellIsActive = true;
Reader.updatePageAction(this);
ExternalApps.updatePageAction(this.browser.currentURI);
} else {
this.browser.setAttribute("type", "content-targetable");
this.browser.docShellIsActive = false;
}
},
getActive: function getActive() {
return this.browser.docShellIsActive;
},
setDisplayPort: function(aDisplayPort) {
let zoom = this._zoom;
let resolution = aDisplayPort.resolution;
if (zoom <= 0 || resolution <= 0)
return;
// "zoom" is the user-visible zoom of the "this" tab
// "resolution" is the zoom at which we wish gecko to render "this" tab at
// these two may be different if we are, for example, trying to render a
// large area of the page at low resolution because the user is panning real
// fast.
// The gecko scroll position is in CSS pixels. The display port rect
// values (aDisplayPort), however, are in CSS pixels multiplied by the desired
// rendering resolution. Therefore care must be taken when doing math with
// these sets of values, to ensure that they are normalized to the same coordinate
// space first.
let element = this.browser.contentDocument.documentElement;
if (!element)
return;
// we should never be drawing background tabs at resolutions other than the user-
// visible zoom. for foreground tabs, however, if we are drawing at some other
// resolution, we need to set the resolution as specified.
let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
if (BrowserApp.selectedTab == this) {
if (resolution != this._drawZoom) {
this._drawZoom = resolution;
cwu.setResolution(resolution / window.devicePixelRatio, resolution / window.devicePixelRatio);
}
} else if (!fuzzyEquals(resolution, zoom)) {
dump("Warning: setDisplayPort resolution did not match zoom for background tab! (" + resolution + " != " + zoom + ")");
}
// Finally, we set the display port, taking care to convert everything into the CSS-pixel
// coordinate space, because that is what the function accepts. Also we have to fudge the
// displayport somewhat to make sure it gets through all the conversions gecko will do on it
// without deforming too much. See https://bugzilla.mozilla.org/show_bug.cgi?id=737510#c10
// for details on what these operations are.
let geckoScrollX = this.browser.contentWindow.scrollX;
let geckoScrollY = this.browser.contentWindow.scrollY;
aDisplayPort = this._dirtiestHackEverToWorkAroundGeckoRounding(aDisplayPort, geckoScrollX, geckoScrollY);
let displayPort = {
x: (aDisplayPort.left / resolution) - geckoScrollX,
y: (aDisplayPort.top / resolution) - geckoScrollY,
width: (aDisplayPort.right - aDisplayPort.left) / resolution,
height: (aDisplayPort.bottom - aDisplayPort.top) / resolution
};
if (this._oldDisplayPort == null ||
!fuzzyEquals(displayPort.x, this._oldDisplayPort.x) ||
!fuzzyEquals(displayPort.y, this._oldDisplayPort.y) ||
!fuzzyEquals(displayPort.width, this._oldDisplayPort.width) ||
!fuzzyEquals(displayPort.height, this._oldDisplayPort.height)) {
if (BrowserApp.gUseLowPrecision) {
// Set the display-port to be 4x the size of the critical display-port,
// on each dimension, giving us a 0.25x lower precision buffer around the
// critical display-port. Spare area is *not* redistributed to the other
// axis, as display-list building and invalidation cost scales with the
// size of the display-port.
let pageRect = cwu.getRootBounds();
let pageXMost = pageRect.right - geckoScrollX;
let pageYMost = pageRect.bottom - geckoScrollY;
let dpW = Math.min(pageRect.right - pageRect.left, displayPort.width * 4);
let dpH = Math.min(pageRect.bottom - pageRect.top, displayPort.height * 4);
let dpX = Math.min(Math.max(displayPort.x - displayPort.width * 1.5,
pageRect.left - geckoScrollX), pageXMost - dpW);
let dpY = Math.min(Math.max(displayPort.y - displayPort.height * 1.5,
pageRect.top - geckoScrollY), pageYMost - dpH);
cwu.setDisplayPortForElement(dpX, dpY, dpW, dpH, element, 0);
cwu.setCriticalDisplayPortForElement(displayPort.x, displayPort.y,
displayPort.width, displayPort.height,
element);
} else {
cwu.setDisplayPortForElement(displayPort.x, displayPort.y,
displayPort.width, displayPort.height,
element, 0);
}
}
this._oldDisplayPort = displayPort;
},
/*
* Yes, this is ugly. But it's currently the safest way to account for the rounding errors that occur
* when we pump the displayport coordinates through gecko and they pop out in the compositor.
*
* In general, the values are converted from page-relative device pixels to viewport-relative app units,
* and then back to page-relative device pixels (now as ints). The first half of this is only slightly
* lossy, but it's enough to throw off the numbers a little. Because of this, when gecko calls
* ScaleToOutsidePixels to generate the final rect, the rect may get expanded more than it should,
* ending up a pixel larger than it started off. This is undesirable in general, but specifically
* bad for tiling, because it means we means we end up painting one line of pixels from a tile,
* causing an otherwise unnecessary upload of the whole tile.
*
* In order to counteract the rounding error, this code simulates the conversions that will happen
* to the display port, and calculates whether or not that final ScaleToOutsidePixels is actually
* expanding the rect more than it should. If so, it determines how much rounding error was introduced
* up until that point, and adjusts the original values to compensate for that rounding error.
*/
_dirtiestHackEverToWorkAroundGeckoRounding: function(aDisplayPort, aGeckoScrollX, aGeckoScrollY) {
const APP_UNITS_PER_CSS_PIXEL = 60.0;
const EXTRA_FUDGE = 0.04;
let resolution = aDisplayPort.resolution;
// Some helper functions that simulate conversion processes in gecko
function cssPixelsToAppUnits(aVal) {
return Math.floor((aVal * APP_UNITS_PER_CSS_PIXEL) + 0.5);
}
function appUnitsToDevicePixels(aVal) {
return aVal / APP_UNITS_PER_CSS_PIXEL * resolution;
}
function devicePixelsToAppUnits(aVal) {
return cssPixelsToAppUnits(aVal / resolution);
}
// Stash our original (desired) displayport width and height away, we need it
// later and we might modify the displayport in between.
let originalWidth = aDisplayPort.right - aDisplayPort.left;
let originalHeight = aDisplayPort.bottom - aDisplayPort.top;
// This is the first conversion the displayport goes through, going from page-relative
// device pixels to viewport-relative app units.
let appUnitDisplayPort = {
x: cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX),
y: cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY),
w: cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution),
h: cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution)
};
// This is the translation gecko applies when converting back from viewport-relative
// device pixels to page-relative device pixels.
let geckoTransformX = -Math.floor((-aGeckoScrollX * resolution) + 0.5);
let geckoTransformY = -Math.floor((-aGeckoScrollY * resolution) + 0.5);
// The final "left" value as calculated in gecko is:
// left = geckoTransformX + Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x))
// In a perfect world, this value would be identical to aDisplayPort.left, which is what
// we started with. However, this may not be the case if the value being floored has accumulated
// enough error to drop below what it should be.
// For example, assume geckoTransformX is 0, and aDisplayPort.left is 4, but
// appUnitsToDevicePixels(appUnitsToDevicePixels.x) comes out as 3.9 because of rounding error.
// That's bad, because the -0.1 error has caused it to floor to 3 instead of 4. (If it had errored
// the other way and come out as 4.1, there's no problem). In this example, we need to increase the
// "left" value by some amount so that the 3.9 actually comes out as >= 4, and it gets floored into
// the expected value of 4. The delta values calculated below calculate that error amount (e.g. -0.1).
let errorLeft = (geckoTransformX + appUnitsToDevicePixels(appUnitDisplayPort.x)) - aDisplayPort.left;
let errorTop = (geckoTransformY + appUnitsToDevicePixels(appUnitDisplayPort.y)) - aDisplayPort.top;
// If the error was negative, that means it will floor incorrectly, so we need to bump up the
// original aDisplayPort.left and/or aDisplayPort.top values. The amount we bump it up by is
// the error amount (increased by a small fudge factor to ensure it's sufficient), converted
// backwards through the conversion process.
if (errorLeft < 0) {
aDisplayPort.left += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorLeft));
// After we modify the left value, we need to re-simulate some values to take that into account
appUnitDisplayPort.x = cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX);
appUnitDisplayPort.w = cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution);
}
if (errorTop < 0) {
aDisplayPort.top += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorTop));
// After we modify the top value, we need to re-simulate some values to take that into account
appUnitDisplayPort.y = cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY);
appUnitDisplayPort.h = cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution);
}
// At this point, the aDisplayPort.left and aDisplayPort.top values have been corrected to account
// for the error in conversion such that they end up where we want them. Now we need to also do the
// same for the right/bottom values so that the width/height end up where we want them.
// This is the final conversion that the displayport goes through before gecko spits it back to
// us. Note that the width/height calculates are of the form "ceil(transform(right)) - floor(transform(left))"
let scaledOutDevicePixels = {
x: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)),
y: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)),
w: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)),
h: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y))
};
// The final "width" value as calculated in gecko is scaledOutDevicePixels.w.
// In a perfect world, this would equal originalWidth. However, things are not perfect, and as before,
// we need to calculate how much rounding error has been introduced. In this case the rounding error is causing
// the Math.ceil call above to ceiling to the wrong final value. For example, 4 gets converted 4.1 and gets
// ceiling'd to 5; in this case the error is 0.1.
let errorRight = (appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w) - scaledOutDevicePixels.x) - originalWidth;
let errorBottom = (appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h) - scaledOutDevicePixels.y) - originalHeight;
// If the error was positive, that means it will ceiling incorrectly, so we need to bump down the
// original aDisplayPort.right and/or aDisplayPort.bottom. Again, we back-convert the error amount
// with a small fudge factor to figure out how much to adjust the original values.
if (errorRight > 0) aDisplayPort.right -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorRight + EXTRA_FUDGE));
if (errorBottom > 0) aDisplayPort.bottom -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorBottom + EXTRA_FUDGE));
// Et voila!
return aDisplayPort;
},
setScrollClampingSize: function(zoom) {
let viewportWidth = gScreenWidth / zoom;
let viewportHeight = gScreenHeight / zoom;
let screenWidth = gScreenWidth;
let screenHeight = gScreenHeight;
// Shrink the viewport appropriately if the margins are excluded
if (this.viewportExcludesVerticalMargins) {
screenHeight = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
viewportHeight = screenHeight / zoom;
}
if (this.viewportExcludesHorizontalMargins) {
screenWidth = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
viewportWidth = screenWidth / zoom;
}
// Make sure the aspect ratio of the screen is maintained when setting
// the clamping scroll-port size.
let factor = Math.min(viewportWidth / screenWidth,
viewportHeight / screenHeight);
let scrollPortWidth = screenWidth * factor;
let scrollPortHeight = screenHeight * factor;
let win = this.browser.contentWindow;
win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).
setScrollPositionClampingScrollPortSize(scrollPortWidth, scrollPortHeight);
},
setViewport: function(aViewport) {
// Transform coordinates based on zoom
let x = aViewport.x / aViewport.zoom;
let y = aViewport.y / aViewport.zoom;
this.setScrollClampingSize(aViewport.zoom);
// Adjust the max line box width to be no more than the viewport width, but
// only if the reflow-on-zoom preference is enabled.
let isZooming = !fuzzyEquals(aViewport.zoom, this._zoom);
let docViewer = null;
if (isZooming &&
BrowserEventHandler.mReflozPref &&
BrowserApp.selectedTab._mReflozPoint &&
BrowserApp.selectedTab.probablyNeedRefloz) {
let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
let docShell = webNav.QueryInterface(Ci.nsIDocShell);
docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
docViewer.pausePainting();
BrowserApp.selectedTab.performReflowOnZoom(aViewport);
BrowserApp.selectedTab.probablyNeedRefloz = false;
}
let win = this.browser.contentWindow;
win.scrollTo(x, y);
this.userScrollPos.x = win.scrollX;
this.userScrollPos.y = win.scrollY;
this.setResolution(aViewport.zoom, false);
if (aViewport.displayPort)
this.setDisplayPort(aViewport.displayPort);
// Store fixed margins for later retrieval in getViewport.
this._fixedMarginLeft = aViewport.fixedMarginLeft;
this._fixedMarginTop = aViewport.fixedMarginTop;
this._fixedMarginRight = aViewport.fixedMarginRight;
this._fixedMarginBottom = aViewport.fixedMarginBottom;
let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
dwi.setContentDocumentFixedPositionMargins(
aViewport.fixedMarginTop / aViewport.zoom,
aViewport.fixedMarginRight / aViewport.zoom,
aViewport.fixedMarginBottom / aViewport.zoom,
aViewport.fixedMarginLeft / aViewport.zoom);
Services.obs.notifyObservers(null, "after-viewport-change", "");
if (docViewer) {
docViewer.resumePainting();
}
},
setResolution: function(aZoom, aForce) {
// Set zoom level
if (aForce || !fuzzyEquals(aZoom, this._zoom)) {
this._zoom = aZoom;
if (BrowserApp.selectedTab == this) {
let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
this._drawZoom = aZoom;
cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio);
}
}
},
getPageSize: function(aDocument, aDefaultWidth, aDefaultHeight) {
let body = aDocument.body || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight };
let html = aDocument.documentElement || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight };
return [Math.max(body.scrollWidth, html.scrollWidth),
Math.max(body.scrollHeight, html.scrollHeight)];
},
getViewport: function() {
let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
let viewport = {
width: screenW,
height: screenH,
cssWidth: screenW / this._zoom,
cssHeight: screenH / this._zoom,
pageLeft: 0,
pageTop: 0,
pageRight: screenW,
pageBottom: screenH,
// We make up matching css page dimensions
cssPageLeft: 0,
cssPageTop: 0,
cssPageRight: screenW / this._zoom,
cssPageBottom: screenH / this._zoom,
fixedMarginLeft: this._fixedMarginLeft,
fixedMarginTop: this._fixedMarginTop,
fixedMarginRight: this._fixedMarginRight,
fixedMarginBottom: this._fixedMarginBottom,
zoom: this._zoom,
};
// Set the viewport offset to current scroll offset
viewport.cssX = this.browser.contentWindow.scrollX || 0;
viewport.cssY = this.browser.contentWindow.scrollY || 0;
// Transform coordinates based on zoom
viewport.x = Math.round(viewport.cssX * viewport.zoom);
viewport.y = Math.round(viewport.cssY * viewport.zoom);
let doc = this.browser.contentDocument;
if (doc != null) {
let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
let cssPageRect = cwu.getRootBounds();
/*
* Avoid sending page sizes of less than screen size before we hit DOMContentLoaded, because
* this causes the page size to jump around wildly during page load. After the page is loaded,
* send updates regardless of page size; we'll zoom to fit the content as needed.
*
* In the check below, we floor the viewport size because there might be slight rounding errors
* introduced in the CSS page size due to the conversion to and from app units in Gecko. The
* error should be no more than one app unit so doing the floor is overkill, but safe in the
* sense that the extra page size updates that get sent as a result will be mostly harmless.
*/
let pageLargerThanScreen = (cssPageRect.width >= Math.floor(viewport.cssWidth))
&& (cssPageRect.height >= Math.floor(viewport.cssHeight));
if (doc.readyState === 'complete' || pageLargerThanScreen) {
viewport.cssPageLeft = cssPageRect.left;
viewport.cssPageTop = cssPageRect.top;
viewport.cssPageRight = cssPageRect.right;
viewport.cssPageBottom = cssPageRect.bottom;
/* Transform the page width and height based on the zoom factor. */
viewport.pageLeft = (viewport.cssPageLeft * viewport.zoom);
viewport.pageTop = (viewport.cssPageTop * viewport.zoom);
viewport.pageRight = (viewport.cssPageRight * viewport.zoom);
viewport.pageBottom = (viewport.cssPageBottom * viewport.zoom);
}
}
return viewport;
},
sendViewportUpdate: function(aPageSizeUpdate) {
let viewport = this.getViewport();
let displayPort = Services.androidBridge.getDisplayPort(aPageSizeUpdate, BrowserApp.isBrowserContentDocumentDisplayed(), this.id, viewport);
if (displayPort != null)
this.setDisplayPort(displayPort);
},
updateViewportForPageSize: function() {
let hasHorizontalMargins = gViewportMargins.left != 0 || gViewportMargins.right != 0;
let hasVerticalMargins = gViewportMargins.top != 0 || gViewportMargins.bottom != 0;
if (!hasHorizontalMargins && !hasVerticalMargins) {
// If there are no margins, then we don't need to do any remeasuring
return;
}
// If the page size has changed so that it might or might not fit on the
// screen with the margins included, run updateViewportSize to resize the
// browser accordingly.
// A page will receive the smaller viewport when its page size fits
// within the screen size, so remeasure when the page size remains within
// the threshold of screen + margins, in case it's sizing itself relative
// to the viewport.
let viewport = this.getViewport();
let pageWidth = viewport.pageRight - viewport.pageLeft;
let pageHeight = viewport.pageBottom - viewport.pageTop;
let remeasureNeeded = false;
if (hasHorizontalMargins) {
let viewportShouldExcludeHorizontalMargins = (pageWidth <= gScreenWidth - 0.5);
if (viewportShouldExcludeHorizontalMargins != this.viewportExcludesHorizontalMargins) {
remeasureNeeded = true;
}
}
if (hasVerticalMargins) {
let viewportShouldExcludeVerticalMargins = (pageHeight <= gScreenHeight - 0.5);
if (viewportShouldExcludeVerticalMargins != this.viewportExcludesVerticalMargins) {
remeasureNeeded = true;
}
}
if (remeasureNeeded) {
if (!this.viewportMeasureCallback) {
this.viewportMeasureCallback = setTimeout(function() {
this.viewportMeasureCallback = null;
// Re-fetch the viewport as it may have changed between setting the timeout
// and running this callback
let viewport = this.getViewport();
let pageWidth = viewport.pageRight - viewport.pageLeft;
let pageHeight = viewport.pageBottom - viewport.pageTop;
if (Math.abs(pageWidth - this.lastPageSizeAfterViewportRemeasure.width) >= 0.5 ||
Math.abs(pageHeight - this.lastPageSizeAfterViewportRemeasure.height) >= 0.5) {
this.updateViewportSize(gScreenWidth);
}
}.bind(this), kViewportRemeasureThrottle);
}
} else if (this.viewportMeasureCallback) {
// If the page changed size twice since we last measured the viewport and
// the latest size change reveals we don't need to remeasure, cancel any
// pending remeasure.
clearTimeout(this.viewportMeasureCallback);
this.viewportMeasureCallback = null;
}
},
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "DOMContentLoaded": {
let target = aEvent.originalTarget;
// ignore on frames and other documents
if (target != this.browser.contentDocument)
return;
// Sample the background color of the page and pass it along. (This is used to draw the
// checkerboard.) Right now we don't detect changes in the background color after this
// event fires; it's not clear that doing so is worth the effort.
var backgroundColor = null;
try {
let { contentDocument, contentWindow } = this.browser;
let computedStyle = contentWindow.getComputedStyle(contentDocument.body);
backgroundColor = computedStyle.backgroundColor;
} catch (e) {
// Ignore. Catching and ignoring exceptions here ensures that Talos succeeds.
}
let docURI = target.documentURI;
let errorType = "";
if (docURI.startsWith("about:certerror"))
errorType = "certerror";
else if (docURI.startsWith("about:blocked"))
errorType = "blocked"
else if (docURI.startsWith("about:neterror"))
errorType = "neterror";
sendMessageToJava({
type: "DOMContentLoaded",
tabID: this.id,
bgColor: backgroundColor,
errorType: errorType
});
// Attach a listener to watch for "click" events bubbling up from error
// pages and other similar page. This lets us fix bugs like 401575 which
// require error page UI to do privileged things, without letting error
// pages have any privilege themselves.
if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) {
this.browser.addEventListener("click", ErrorPageEventHandler, true);
let listener = function() {
this.browser.removeEventListener("click", ErrorPageEventHandler, true);
this.browser.removeEventListener("pagehide", listener, true);
}.bind(this);
this.browser.addEventListener("pagehide", listener, true);
}
if (docURI.startsWith("about:reader")) {
// During browser restart / recovery, duplicate "DOMContentLoaded" messages are received here
// For the visible tab ... where more than one tab is being reloaded, the inital "DOMContentLoaded"
// Message can be received before the document body is available ... so we avoid instantiating an
// AboutReader object, expecting that an eventual valid message will follow.
let contentDocument = this.browser.contentDocument;
if (contentDocument.body) {
new AboutReader(contentDocument, this.browser.contentWindow);
}
}
break;
}
case "DOMFormHasPassword": {
LoginManagerContent.onFormPassword(aEvent);
break;
}
case "DOMLinkAdded": {
let target = aEvent.originalTarget;
if (!target.href || target.disabled)
return;
// Ignore on frames and other documents
if (target.ownerDocument != this.browser.contentDocument)
return;
// Sanitize the rel string
let list = [];
if (target.rel) {
list = target.rel.toLowerCase().split(/\s+/);
let hash = {};
list.forEach(function(value) { hash[value] = true; });
list = [];
for (let rel in hash)
list.push("[" + rel + "]");
}
if (list.indexOf("[icon]") != -1) {
// We want to get the largest icon size possible for our UI.
let maxSize = 0;
// We use the sizes attribute if available
// see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
if (target.hasAttribute("sizes")) {
let sizes = target.getAttribute("sizes").toLowerCase();
if (sizes == "any") {
// Since Java expects an integer, use -1 to represent icons with sizes="any"
maxSize = -1;
} else {
let tokens = sizes.split(" ");
tokens.forEach(function(token) {
// TODO: check for invalid tokens
let [w, h] = token.split("x");
maxSize = Math.max(maxSize, Math.max(w, h));
});
}
}
let json = {
type: "Link:Favicon",
tabID: this.id,
href: resolveGeckoURI(target.href),
charset: target.ownerDocument.characterSet,
title: target.title,
rel: list.join(" "),
size: maxSize
};
sendMessageToJava(json);
} else if (list.indexOf("[alternate]") != -1) {
let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
if (!isFeed)
return;
try {
// urlSecurityCeck will throw if things are not OK
ContentAreaUtils.urlSecurityCheck(target.href, target.ownerDocument.nodePrincipal, Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
if (!this.browser.feeds)
this.browser.feeds = [];
this.browser.feeds.push({ href: target.href, title: target.title, type: type });
let json = {
type: "Link:Feed",
tabID: this.id
};
sendMessageToJava(json);
} catch (e) {}
} else if (list.indexOf("[search]" != -1)) {
let type = target.type && target.type.toLowerCase();
// Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
// Check that type matches opensearch.
let isOpenSearch = (type == "application/opensearchdescription+xml");
if (isOpenSearch && target.title && /^(?:https?|ftp):/i.test(target.href)) {
let visibleEngines = Services.search.getVisibleEngines();
// NOTE: Engines are currently identified by name, but this can be changed
// when Engines are identified by URL (see bug 335102).
if (visibleEngines.some(function(e) {
return e.name == target.title;
})) {
// This engine is already present, do nothing.
return;
}
if (this.browser.engines) {
// This engine has already been handled, do nothing.
if (this.browser.engines.some(function(e) {
return e.url == target.href;
})) {
return;
}
} else {
this.browser.engines = [];
}
// Get favicon.
let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico";
let newEngine = {
title: target.title,
url: target.href,
iconURL: iconURL
};
this.browser.engines.push(newEngine);
// Don't send a message to display engines if we've already handled an engine.
if (this.browser.engines.length > 1)
return;
// Broadcast message that this tab contains search engines that should be visible.
let newEngineMessage = {
type: "Link:OpenSearch",
tabID: this.id,
visible: true
};
sendMessageToJava(newEngineMessage);
}
}
break;
}
case "DOMTitleChanged": {
if (!aEvent.isTrusted)
return;
// ignore on frames and other documents
if (aEvent.originalTarget != this.browser.contentDocument)
return;
sendMessageToJava({
type: "DOMTitleChanged",
tabID: this.id,
title: aEvent.target.title.substring(0, 255)
});
break;
}
case "DOMWindowClose": {
if (!aEvent.isTrusted)
return;
// Find the relevant tab, and close it from Java
if (this.browser.contentWindow == aEvent.target) {
aEvent.preventDefault();
sendMessageToJava({
type: "Tab:Close",
tabID: this.id
});
}
break;
}
case "DOMWillOpenModalDialog": {
if (!aEvent.isTrusted)
return;
// We're about to open a modal dialog, make sure the opening
// tab is brought to the front.
let tab = BrowserApp.getTabForWindow(aEvent.target.top);
BrowserApp.selectTab(tab);
break;
}
case "DOMAutoComplete":
case "blur": {
LoginManagerContent.onUsernameInput(aEvent);
break;
}
case "scroll": {
let win = this.browser.contentWindow;
if (this.userScrollPos.x != win.scrollX || this.userScrollPos.y != win.scrollY) {
this.sendViewportUpdate();
}
break;
}
case "MozScrolledAreaChanged": {
// This event is only fired for root scroll frames, and only when the
// scrolled area has actually changed, so no need to check for that.
// Just make sure it's the event for the correct root scroll frame.
if (aEvent.originalTarget != this.browser.contentDocument)
return;
this.sendViewportUpdate(true);
this.updateViewportForPageSize();
break;
}
case "PluginBindingAttached": {
PluginHelper.handlePluginBindingAttached(this, aEvent);
break;
}
case "MozApplicationManifest": {
OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView);
break;
}
case "pageshow": {
// only send pageshow for the top-level document
if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
return;
sendMessageToJava({
type: "Content:PageShow",
tabID: this.id
});
if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
if (!this._linkifier)
this._linkifier = new Linkifier();
this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
}
// Update page actions for helper apps.
let uri = this.browser.currentURI;
if (BrowserApp.selectedTab == this) {
if (ExternalApps.shouldCheckUri(uri)) {
ExternalApps.updatePageAction(uri);
} else {
ExternalApps.clearPageAction();
}
}
if (!Reader.isEnabledForParseOnLoad)
return;
// Once document is fully loaded, parse it
Reader.parseDocumentFromTab(this.id, function (article) {
// Do nothing if there's no article or the page in this tab has
// changed
let tabURL = uri.specIgnoringRef;
if (article == null || (article.url != tabURL)) {
// Don't clear the article for about:reader pages since we want to
// use the article from the previous page
if (!tabURL.startsWith("about:reader")) {
this.savedArticle = null;
this.readerEnabled = false;
this.readerActive = false;
} else {
this.readerActive = true;
}
return;
}
this.savedArticle = article;
sendMessageToJava({
type: "Content:ReaderEnabled",
tabID: this.id
});
if(this.readerActive)
this.readerActive = false;
if(!this.readerEnabled)
this.readerEnabled = true;
}.bind(this));
}
}
},
onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
let contentWin = aWebProgress.DOMWindow;
if (contentWin != contentWin.top)
return;
// Filter optimization: Only really send NETWORK state changes to Java listener
if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) {
// We may receive a document stop event while a document is still loading
// (such as when doing URI fixup). Don't notify Java UI in these cases.
return;
}
// Clear page-specific opensearch engines and feeds for a new request.
if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) {
this.browser.engines = null;
this.browser.feeds = null;
}
// true if the page loaded successfully (i.e., no 404s or other errors)
let success = false;
let uri = "";
try {
// Remember original URI for UA changes on redirected pages
this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI;
if (this.originalURI != null)
uri = this.originalURI.spec;
} catch (e) { }
try {
success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded;
} catch (e) {
// If the request does not handle the nsIHttpChannel interface, use nsIRequest's success
// status. Used for local files. See bug 948849.
success = aRequest.status == 0;
}
// Check to see if we restoring the content from a previous presentation (session)
// since there should be no real network activity
let restoring = (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) > 0;
let message = {
type: "Content:StateChange",
tabID: this.id,
uri: uri,
state: aStateFlags,
restoring: restoring,
success: success
};
sendMessageToJava(message);
}
},
onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
let contentWin = aWebProgress.DOMWindow;
// Browser webapps may load content inside iframes that can not reach across the app/frame boundary
// i.e. even though the page is loaded in an iframe window.top != webapp
// Make cure this window is a top level tab before moving on.
if (BrowserApp.getBrowserForWindow(contentWin) == null)
return;
this._hostChanged = true;
let fixedURI = aLocationURI;
try {
fixedURI = URIFixup.createExposableURI(aLocationURI);
} catch (ex) { }
let contentType = contentWin.document.contentType;
// If fixedURI matches browser.lastURI, we assume this isn't a real location
// change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883.
// Note that we have to ensure fixedURI is not the same as aLocationURI so we
// don't false-positive page reloads as spurious additions.
let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 ||
((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI));
this.browser.lastURI = fixedURI;
// Reset state of click-to-play plugin notifications.
clearTimeout(this.pluginDoorhangerTimeout);
this.pluginDoorhangerTimeout = null;
this.shouldShowPluginDoorhanger = true;
this.clickToPlayPluginsActivated = false;
// Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#174
let documentURI = contentWin.document.documentURIObject.spec
let matchedURL = documentURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
let baseDomain = "";
if (matchedURL) {
var domain = "";
[, , domain] = matchedURL;
try {
baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
if (!domain.endsWith(baseDomain)) {
// getBaseDomainFromHost converts its resultant to ACE.
let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
baseDomain = IDNService.convertACEtoUTF8(baseDomain);
}
} catch (e) {}
}
// Update the page actions URI for helper apps.
if (BrowserApp.selectedTab == this) {
ExternalApps.updatePageActionUri(fixedURI);
}
let message = {
type: "Content:LocationChange",
tabID: this.id,
uri: fixedURI.spec,
userSearch: this.userSearch || "",
baseDomain: baseDomain,
contentType: (contentType ? contentType : ""),
sameDocument: sameDocument
};
sendMessageToJava(message);
// The search term is only valid for this location change event, so reset it here.
this.userSearch = "";
if (!sameDocument) {
// XXX This code assumes that this is the earliest hook we have at which
// browser.contentDocument is changed to the new document we're loading
this.contentDocumentIsDisplayed = false;
this.hasTouchListener = false;
} else {
this.sendViewportUpdate();
}
},
// Properties used to cache security state used to update the UI
_state: null,
_hostChanged: false, // onLocationChange will flip this bit
onSecurityChange: function(aWebProgress, aRequest, aState) {
// Don't need to do anything if the data we use to update the UI hasn't changed
if (this._state == aState && !this._hostChanged)
return;
this._state = aState;
this._hostChanged = false;
let identity = IdentityHandler.checkIdentity(aState, this.browser);
let message = {
type: "Content:SecurityChange",
tabID: this.id,
identity: identity
};
sendMessageToJava(message);
},
onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
},
onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
},
_sendHistoryEvent: function(aMessage, aParams) {
let message = {
type: "SessionHistory:" + aMessage,
tabID: this.id,
};
if (aParams) {
if ("url" in aParams)
message.url = aParams.url;
if ("index" in aParams)
message.index = aParams.index;
if ("numEntries" in aParams)
message.numEntries = aParams.numEntries;
}
sendMessageToJava(message);
},
OnHistoryNewEntry: function(aUri) {
this._sendHistoryEvent("New", { url: aUri.spec });
},
OnHistoryGoBack: function(aUri) {
this._sendHistoryEvent("Back");
return true;
},
OnHistoryGoForward: function(aUri) {
this._sendHistoryEvent("Forward");
return true;
},
OnHistoryReload: function(aUri, aFlags) {
// we don't do anything with this, so don't propagate it
// for now anyway
return true;
},
OnHistoryGotoIndex: function(aIndex, aUri) {
this._sendHistoryEvent("Goto", { index: aIndex });
return true;
},
OnHistoryPurge: function(aNumEntries) {
this._sendHistoryEvent("Purge", { numEntries: aNumEntries });
return true;
},
get metadata() {
return ViewportHandler.getMetadataForDocument(this.browser.contentDocument);
},
/** Update viewport when the metadata changes. */
updateViewportMetadata: function updateViewportMetadata(aMetadata, aInitialLoad) {
if (Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) {
aMetadata.allowZoom = true;
aMetadata.allowDoubleTapZoom = true;
aMetadata.minZoom = aMetadata.maxZoom = NaN;
}
let scaleRatio = window.devicePixelRatio;
if (aMetadata.defaultZoom > 0)
aMetadata.defaultZoom *= scaleRatio;
if (aMetadata.minZoom > 0)
aMetadata.minZoom *= scaleRatio;
if (aMetadata.maxZoom > 0)
aMetadata.maxZoom *= scaleRatio;
aMetadata.isRTL = this.browser.contentDocument.documentElement.dir == "rtl";
ViewportHandler.setMetadataForDocument(this.browser.contentDocument, aMetadata);
this.sendViewportMetadata();
this.updateViewportSize(gScreenWidth, aInitialLoad);
},
/** Update viewport when the metadata or the window size changes. */
updateViewportSize: function updateViewportSize(aOldScreenWidth, aInitialLoad) {
// When this function gets called on window resize, we must execute
// this.sendViewportUpdate() so that refreshDisplayPort is called.
// Ensure that when making changes to this function that code path
// is not accidentally removed (the call to sendViewportUpdate() is
// at the very end).
if (this.viewportMeasureCallback) {
clearTimeout(this.viewportMeasureCallback);
this.viewportMeasureCallback = null;
}
let browser = this.browser;
if (!browser)
return;
let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
let viewportW, viewportH;
let metadata = this.metadata;
if (metadata.autoSize) {
viewportW = screenW / window.devicePixelRatio;
viewportH = screenH / window.devicePixelRatio;
} else {
viewportW = metadata.width;
viewportH = metadata.height;
// If (scale * width) < device-width, increase the width (bug 561413).
let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom;
if (maxInitialZoom && viewportW) {
viewportW = Math.max(viewportW, screenW / maxInitialZoom);
}
let validW = viewportW > 0;
let validH = viewportH > 0;
if (!validW)
viewportW = validH ? (viewportH * (screenW / screenH)) : BrowserApp.defaultBrowserWidth;
if (!validH)
viewportH = viewportW * (screenH / screenW);
}
// Make sure the viewport height is not shorter than the window when
// the page is zoomed out to show its full width. Note that before
// we set the viewport width, the "full width" of the page isn't properly
// defined, so that's why we have to call setBrowserSize twice - once
// to set the width, and the second time to figure out the height based
// on the layout at that width.
let oldBrowserWidth = this.browserWidth;
this.setBrowserSize(viewportW, viewportH);
// This change to the zoom accounts for all types of changes I can conceive:
// 1. screen size changes, CSS viewport does not (pages with no meta viewport
// or a fixed size viewport)
// 2. screen size changes, CSS viewport also does (pages with a device-width
// viewport)
// 3. screen size remains constant, but CSS viewport changes (meta viewport
// tag is added or removed)
// 4. neither screen size nor CSS viewport changes
//
// In all of these cases, we maintain how much actual content is visible
// within the screen width. Note that "actual content" may be different
// with respect to CSS pixels because of the CSS viewport size changing.
let zoomScale = (screenW * oldBrowserWidth) / (aOldScreenWidth * viewportW);
let zoom = (aInitialLoad && metadata.defaultZoom) ? metadata.defaultZoom : this.clampZoom(this._zoom * zoomScale);
this.setResolution(zoom, false);
this.setScrollClampingSize(zoom);
// if this page has not been painted yet, then this must be getting run
// because a meta-viewport element was added (via the DOMMetaAdded handler).
// in this case, we should not do anything that forces a reflow (see bug 759678)
// such as requesting the page size or sending a viewport update. this code
// will get run again in the before-first-paint handler and that point we
// will run though all of it. the reason we even bother executing up to this
// point on the DOMMetaAdded handler is so that scripts that use window.innerWidth
// before they are painted have a correct value (bug 771575).
if (!this.contentDocumentIsDisplayed) {
return;
}
this.viewportExcludesHorizontalMargins = true;
this.viewportExcludesVerticalMargins = true;
let minScale = 1.0;
if (this.browser.contentDocument) {
// this may get run during a Viewport:Change message while the document
// has not yet loaded, so need to guard against a null document.
let [pageWidth, pageHeight] = this.getPageSize(this.browser.contentDocument, viewportW, viewportH);
// In the situation the page size equals or exceeds the screen size,
// lengthen the viewport on the corresponding axis to include the margins.
// The '- 0.5' is to account for rounding errors.
if (pageWidth * this._zoom > gScreenWidth - 0.5) {
screenW = gScreenWidth;
this.viewportExcludesHorizontalMargins = false;
}
if (pageHeight * this._zoom > gScreenHeight - 0.5) {
screenH = gScreenHeight;
this.viewportExcludesVerticalMargins = false;
}
minScale = screenW / pageWidth;
}
minScale = this.clampZoom(minScale);
viewportH = Math.max(viewportH, screenH / minScale);
// In general we want to keep calls to setBrowserSize and setScrollClampingSize
// together because setBrowserSize could mark the viewport size as dirty, creating
// a pending resize event for content. If that resize gets dispatched (which happens
// on the next reflow) without setScrollClampingSize having being called, then
// content might be exposed to incorrect innerWidth/innerHeight values.
this.setBrowserSize(viewportW, viewportH);
this.setScrollClampingSize(zoom);
// Avoid having the scroll position jump around after device rotation.
let win = this.browser.contentWindow;
this.userScrollPos.x = win.scrollX;
this.userScrollPos.y = win.scrollY;
this.sendViewportUpdate();
if (metadata.allowZoom && !Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) {
// If the CSS viewport is narrower than the screen (i.e. width <= device-width)
// then we disable double-tap-to-zoom behaviour.
var oldAllowDoubleTapZoom = metadata.allowDoubleTapZoom;
var newAllowDoubleTapZoom = (!metadata.isSpecified) || (viewportW > screenW / window.devicePixelRatio);
if (oldAllowDoubleTapZoom !== newAllowDoubleTapZoom) {
metadata.allowDoubleTapZoom = newAllowDoubleTapZoom;
this.sendViewportMetadata();
}
}
// Store the page size that was used to calculate the viewport so that we
// can verify it's changed when we consider remeasuring in updateViewportForPageSize
let viewport = this.getViewport();
this.lastPageSizeAfterViewportRemeasure = {
width: viewport.pageRight - viewport.pageLeft,
height: viewport.pageBottom - viewport.pageTop
};
},
sendViewportMetadata: function sendViewportMetadata() {
let metadata = this.metadata;
sendMessageToJava({
type: "Tab:ViewportMetadata",
allowZoom: metadata.allowZoom,
allowDoubleTapZoom: metadata.allowDoubleTapZoom,
defaultZoom: metadata.defaultZoom || window.devicePixelRatio,
minZoom: metadata.minZoom || 0,
maxZoom: metadata.maxZoom || 0,
isRTL: metadata.isRTL,
tabID: this.id
});
},
setBrowserSize: function(aWidth, aHeight) {
if (fuzzyEquals(this.browserWidth, aWidth) && fuzzyEquals(this.browserHeight, aHeight)) {
return;
}
this.browserWidth = aWidth;
this.browserHeight = aHeight;
if (!this.browser.contentWindow)
return;
let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
cwu.setCSSViewport(aWidth, aHeight);
},
/** Takes a scale and restricts it based on this tab's zoom limits. */
clampZoom: function clampZoom(aZoom) {
let zoom = ViewportHandler.clamp(aZoom, kViewportMinScale, kViewportMaxScale);
let md = this.metadata;
if (!md.allowZoom)
return md.defaultZoom || zoom;
if (md && md.minZoom)
zoom = Math.max(zoom, md.minZoom);
if (md && md.maxZoom)
zoom = Math.min(zoom, md.maxZoom);
return zoom;
},
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case "before-first-paint":
// Is it on the top level?
let contentDocument = aSubject;
if (contentDocument == this.browser.contentDocument) {
if (BrowserApp.selectedTab == this) {
BrowserApp.contentDocumentChanged();
}
this.contentDocumentIsDisplayed = true;
// reset CSS viewport and zoom to default on new page, and then calculate
// them properly using the actual metadata from the page. note that the
// updateMetadata call takes into account the existing CSS viewport size
// and zoom when calculating the new ones, so we need to reset these
// things here before calling updateMetadata.
this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
this.setResolution(gScreenWidth / this.browserWidth, false);
ViewportHandler.updateMetadata(this, true);
// Note that if we draw without a display-port, things can go wrong. By the
// time we execute this, it's almost certain a display-port has been set via
// the MozScrolledAreaChanged event. If that didn't happen, the updateMetadata
// call above does so at the end of the updateViewportSize function. As long
// as that is happening, we don't need to do it again here.
if (contentDocument.mozSyntheticDocument) {
// for images, scale to fit width. this needs to happen *after* the call
// to updateMetadata above, because that call sets the CSS viewport which
// will affect the page size (i.e. contentDocument.body.scroll*) that we
// use in this calculation. also we call sendViewportUpdate after changing
// the resolution so that the display port gets recalculated appropriately.
let fitZoom = Math.min(gScreenWidth / contentDocument.body.scrollWidth,
gScreenHeight / contentDocument.body.scrollHeight);
this.setResolution(fitZoom, false);
this.sendViewportUpdate();
}
}
// If the reflow-text-on-page-load pref is enabled, and reflow-on-zoom
// is enabled, and our defaultZoom level is set, then we need to get
// the default zoom and reflow the text according to the defaultZoom
// level.
let rzEnabled = BrowserEventHandler.mReflozPref;
let rzPl = Services.prefs.getBoolPref("browser.zoom.reflowZoom.reflowTextOnPageLoad");
if (rzEnabled && rzPl) {
// Retrieve the viewport width and adjust the max line box width
// accordingly.
let vp = BrowserApp.selectedTab.getViewport();
BrowserApp.selectedTab.performReflowOnZoom(vp);
}
break;
case "after-viewport-change":
if (BrowserApp.selectedTab._mReflozPositioned) {
BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
}
break;
case "nsPref:changed":
if (aData == "browser.ui.zoom.force-user-scalable")
ViewportHandler.updateMetadata(this, false);
break;
}
},
set readerEnabled(isReaderEnabled) {
this._readerEnabled = isReaderEnabled;
if (this.getActive())
Reader.updatePageAction(this);
},
get readerEnabled() {
return this._readerEnabled;
},
set readerActive(isReaderActive) {
this._readerActive = isReaderActive;
if (this.getActive())
Reader.updatePageAction(this);
},
get readerActive() {
return this._readerActive;
},
// nsIBrowserTab
get window() {
if (!this.browser)
return null;
return this.browser.contentWindow;
},
get scale() {
return this._zoom;
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIWebProgressListener,
Ci.nsISHistoryListener,
Ci.nsIObserver,
Ci.nsISupportsWeakReference,
Ci.nsIBrowserTab
])
};
var BrowserEventHandler = {
init: function init() {
Services.obs.addObserver(this, "Gesture:SingleTap", false);
Services.obs.addObserver(this, "Gesture:CancelTouch", false);
Services.obs.addObserver(this, "Gesture:DoubleTap", false);
Services.obs.addObserver(this, "Gesture:Scroll", false);
Services.obs.addObserver(this, "dom-touch-listener-added", false);
BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false);
BrowserApp.deck.addEventListener("touchstart", this, true);
BrowserApp.deck.addEventListener("click", InputWidgetHelper, true);
BrowserApp.deck.addEventListener("click", SelectHelper, true);
SpatialNavigation.init(BrowserApp.deck, null);
document.addEventListener("MozMagnifyGesture", this, true);
Services.prefs.addObserver("browser.zoom.reflowOnZoom", this, false);
this.updateReflozPref();
},
resetMaxLineBoxWidth: function() {
BrowserApp.selectedTab.probablyNeedRefloz = false;
if (gReflowPending) {
clearTimeout(gReflowPending);
}
let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
reflozTimeout, 0);
},
updateReflozPref: function() {
this.mReflozPref = Services.prefs.getBoolPref("browser.zoom.reflowOnZoom");
},
handleEvent: function(aEvent) {
switch (aEvent.type) {
case 'touchstart':
this._handleTouchStart(aEvent);
break;
case 'MozMagnifyGesture':
this.observe(this, aEvent.type,
JSON.stringify({x: aEvent.screenX, y: aEvent.screenY,
zoomDelta: aEvent.delta}));
break;
}
},
_handleTouchStart: function(aEvent) {
if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.touches.length > 1 || aEvent.defaultPrevented)
return;
let closest = aEvent.target;
if (closest) {
// If we've pressed a scrollable element, let Java know that we may
// want to override the scroll behaviour (for document sub-frames)
this._scrollableElement = this._findScrollableElement(closest, true);
this._firstScrollEvent = true;
if (this._scrollableElement != null) {
// Discard if it's the top-level scrollable, we let Java handle this
let doc = BrowserApp.selectedBrowser.contentDocument;
if (this._scrollableElement != doc.body && this._scrollableElement != doc.documentElement)
sendMessageToJava({ type: "Panning:Override" });
}
}
if (!ElementTouchHelper.isElementClickable(closest, null, false))
closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX,
aEvent.changedTouches[0].screenY);
if (!closest)
closest = aEvent.target;
if (closest) {
let uri = this._getLinkURI(closest);
if (uri) {
try {
Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
} catch (e) {}
}
this._doTapHighlight(closest);
}
},
_getLinkURI: function(aElement) {
if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
(aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) {
try {
return Services.io.newURI(aElement.href, null, null);
} catch (e) {}
}
return null;
},
observe: function(aSubject, aTopic, aData) {
if (aTopic == "dom-touch-listener-added") {
let tab = BrowserApp.getTabForWindow(aSubject.top);
if (!tab || tab.hasTouchListener)
return;
tab.hasTouchListener = true;
sendMessageToJava({
type: "Tab:HasTouchListener",
tabID: tab.id
});
return;
} else if (aTopic == "nsPref:changed") {
if (aData == "browser.zoom.reflowOnZoom") {
this.updateReflozPref();
}
return;
}
// the remaining events are all dependent on the browser content document being the
// same as the browser displayed document. if they are not the same, we should ignore
// the event.
if (BrowserApp.isBrowserContentDocumentDisplayed()) {
this.handleUserEvent(aTopic, aData);
}
},
handleUserEvent: function(aTopic, aData) {
switch (aTopic) {
case "Gesture:Scroll": {
// If we've lost our scrollable element, return. Don't cancel the
// override, as we probably don't want Java to handle panning until the
// user releases their finger.
if (this._scrollableElement == null)
return;
// If this is the first scroll event and we can't scroll in the direction
// the user wanted, and neither can any non-root sub-frame, cancel the
// override so that Java can handle panning the main document.
let data = JSON.parse(aData);
// round the scroll amounts because they come in as floats and might be
// subject to minor rounding errors because of zoom values. I've seen values
// like 0.99 come in here and get truncated to 0; this avoids that problem.
let zoom = BrowserApp.selectedTab._zoom;
let x = Math.round(data.x / zoom);
let y = Math.round(data.y / zoom);
if (this._firstScrollEvent) {
while (this._scrollableElement != null &&
!this._elementCanScroll(this._scrollableElement, x, y))
this._scrollableElement = this._findScrollableElement(this._scrollableElement, false);
let doc = BrowserApp.selectedBrowser.contentDocument;
if (this._scrollableElement == null ||
this._scrollableElement == doc.body ||
this._scrollableElement == doc.documentElement) {
sendMessageToJava({ type: "Panning:CancelOverride" });
return;
}
this._firstScrollEvent = false;
}
// Scroll the scrollable element
if (this._elementCanScroll(this._scrollableElement, x, y)) {
this._scrollElementBy(this._scrollableElement, x, y);
sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: true });
SelectionHandler.subdocumentScrolled(this._scrollableElement);
} else {
sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: false });
}
break;
}
case "Gesture:CancelTouch":
this._cancelTapHighlight();
break;
case "Gesture:SingleTap": {
let element = this._highlightElement;
if (element) {
try {
let data = JSON.parse(aData);
let [x, y] = [data.x, data.y];
if (ElementTouchHelper.isElementClickable(element)) {
[x, y] = this._moveClickPoint(element, x, y);
}
// Was the element already focused before it was clicked?
let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser));
this._sendMouseEvent("mousemove", element, x, y);
this._sendMouseEvent("mousedown", element, x, y);
this._sendMouseEvent("mouseup", element, x, y);
// If the element was previously focused, show the caret attached to it.
if (isFocused)
SelectionHandler.attachCaret(element);
// scrollToFocusedInput does its own checks to find out if an element should be zoomed into
BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser);
} catch(e) {
Cu.reportError(e);
}
}
this._cancelTapHighlight();
break;
}
case"Gesture:DoubleTap":
this._cancelTapHighlight();
this.onDoubleTap(aData);
break;
case "MozMagnifyGesture":
this.onPinchFinish(aData);
break;
default:
dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
break;
}
},
onDoubleTap: function(aData) {
let data = JSON.parse(aData);
let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y);
// We only want to do this if reflow-on-zoom is enabled, we don't already
// have a reflow-on-zoom event pending, and the element upon which the user
// double-tapped isn't of a type we want to avoid reflow-on-zoom.
if (BrowserEventHandler.mReflozPref &&
!BrowserApp.selectedTab._mReflozPoint &&
!this._shouldSuppressReflowOnZoom(element)) {
// See comment above performReflowOnZoom() for a detailed description of
// the events happening in the reflow-on-zoom operation.
let data = JSON.parse(aData);
let zoomPointX = data.x;
let zoomPointY = data.y;
BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY,
range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) };
// Before we perform a reflow on zoom, let's disable painting.
let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
let docShell = webNav.QueryInterface(Ci.nsIDocShell);
let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
docViewer.pausePainting();
BrowserApp.selectedTab.probablyNeedRefloz = true;
}
if (!element) {
ZoomHelper.zoomOut();
return;
}
while (element && !this._shouldZoomToElement(element))
element = element.parentNode;
if (!element) {
ZoomHelper.zoomOut();
} else {
ZoomHelper.zoomToElement(element, data.y);
}
},
/**
* Determine if reflow-on-zoom functionality should be suppressed, given a
* particular element. Double-tapping on the following elements suppresses
* reflow-on-zoom:
*
* <video>, <object>, <embed>, <applet>, <canvas>, <img>, <media>, <pre>
*/
_shouldSuppressReflowOnZoom: function(aElement) {
if (aElement instanceof Ci.nsIDOMHTMLVideoElement ||
aElement instanceof Ci.nsIDOMHTMLObjectElement ||
aElement instanceof Ci.nsIDOMHTMLEmbedElement ||
aElement instanceof Ci.nsIDOMHTMLAppletElement ||
aElement instanceof Ci.nsIDOMHTMLCanvasElement ||
aElement instanceof Ci.nsIDOMHTMLImageElement ||
aElement instanceof Ci.nsIDOMHTMLMediaElement ||
aElement instanceof Ci.nsIDOMHTMLPreElement) {
return true;
}
return false;
},
onPinchFinish: function(aData) {
let data = {};
try {
data = JSON.parse(aData);
} catch(ex) {
console.log(ex);
return;
}
if (BrowserEventHandler.mReflozPref &&
data.zoomDelta < 0.0) {
BrowserEventHandler.resetMaxLineBoxWidth();
}
},
_shouldZoomToElement: function(aElement) {
let win = aElement.ownerDocument.defaultView;
if (win.getComputedStyle(aElement, null).display == "inline")
return false;
if (aElement instanceof Ci.nsIDOMHTMLLIElement)
return false;
if (aElement instanceof Ci.nsIDOMHTMLQuoteElement)
return false;
return true;
},
_firstScrollEvent: false,
_scrollableElement: null,
_highlightElement: null,
_doTapHighlight: function _doTapHighlight(aElement) {
DOMUtils.setContentState(aElement, kStateActive);
this._highlightElement = aElement;
},
_cancelTapHighlight: function _cancelTapHighlight() {
if (!this._highlightElement)
return;
// If the active element is in a sub-frame, we need to make that frame's document
// active to remove the element's active state.
if (this._highlightElement.ownerDocument != BrowserApp.selectedBrowser.contentWindow.document)
DOMUtils.setContentState(this._highlightElement.ownerDocument.documentElement, kStateActive);
DOMUtils.setContentState(BrowserApp.selectedBrowser.contentWindow.document.documentElement, kStateActive);
this._highlightElement = null;
},
_updateLastPosition: function(x, y, dx, dy) {
this.lastX = x;
this.lastY = y;
this.lastTime = Date.now();
this.motionBuffer.push({ dx: dx, dy: dy, time: this.lastTime });
},
_moveClickPoint: function(aElement, aX, aY) {
// the element can be out of the aX/aY point because of the touch radius
// if outside, we gracefully move the touch point to the edge of the element
if (!(aElement instanceof HTMLHtmlElement)) {
let isTouchClick = true;
let rects = ElementTouchHelper.getContentClientRects(aElement);
for (let i = 0; i < rects.length; i++) {
let rect = rects[i];
let inBounds =
(aX > rect.left && aX < (rect.left + rect.width)) &&
(aY > rect.top && aY < (rect.top + rect.height));
if (inBounds) {
isTouchClick = false;
break;
}
}
if (isTouchClick) {
let rect = rects[0];
// if either width or height is zero, we don't want to move the click to the edge of the element. See bug 757208
if (rect.width != 0 && rect.height != 0) {
aX = Math.min(Math.ceil(rect.left + rect.width) - 1, Math.max(Math.ceil(rect.left), aX));
aY = Math.min(Math.ceil(rect.top + rect.height) - 1, Math.max(Math.ceil(rect.top), aY));
}
}
}
return [aX, aY];
},
_sendMouseEvent: function _sendMouseEvent(aName, aElement, aX, aY) {
let window = aElement.ownerDocument.defaultView;
try {
let cwu = window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
cwu.sendMouseEventToWindow(aName, aX, aY, 0, 1, 0, true);
} catch(e) {
Cu.reportError(e);
}
},
_hasScrollableOverflow: function(elem) {
var win = elem.ownerDocument.defaultView;
if (!win)
return false;
var computedStyle = win.getComputedStyle(elem);
if (!computedStyle)
return false;
return computedStyle.overflowX == 'auto' || computedStyle.overflowX == 'scroll'
|| computedStyle.overflowY == 'auto' || computedStyle.overflowY == 'scroll';
},
_findScrollableElement: function(elem, checkElem) {
// Walk the DOM tree until we find a scrollable element
let scrollable = false;
while (elem) {
/* Element is scrollable if its scroll-size exceeds its client size, and:
* - It has overflow 'auto' or 'scroll'
* - It's a textarea
* - It's an HTML/BODY node
* - It's a text input
* - It's a select element showing multiple rows
*/
if (checkElem) {
if ((elem.scrollTopMax > 0 || elem.scrollLeftMax > 0) &&
(this._hasScrollableOverflow(elem) ||
elem.mozMatchesSelector("html, body, textarea")) ||
(elem instanceof HTMLInputElement && elem.mozIsTextField(false)) ||
(elem instanceof HTMLSelectElement && (elem.size > 1 || elem.multiple))) {
scrollable = true;
break;
}
} else {
checkElem = true;
}
// Propagate up iFrames
if (!elem.parentNode && elem.documentElement && elem.documentElement.ownerDocument)
elem = elem.documentElement.ownerDocument.defaultView.frameElement;
else
elem = elem.parentNode;
}
if (!scrollable)
return null;
return elem;
},
_scrollElementBy: function(elem, x, y) {
elem.scrollTop = elem.scrollTop + y;
elem.scrollLeft = elem.scrollLeft + x;
},
_elementCanScroll: function(elem, x, y) {
let scrollX = (x < 0 && elem.scrollLeft > 0)
|| (x > 0 && elem.scrollLeft < elem.scrollLeftMax);
let scrollY = (y < 0 && elem.scrollTop > 0)
|| (y > 0 && elem.scrollTop < elem.scrollTopMax);
return scrollX || scrollY;
}
};
const kReferenceDpi = 240; // standard "pixel" size used in some preferences
const ElementTouchHelper = {
/* Return the element at the given coordinates, starting from the given window and
drilling down through frames. If no window is provided, the top-level window of
the currently selected tab is used. The coordinates provided should be CSS pixels
relative to the window's scroll position. */
anyElementFromPoint: function(aX, aY, aWindow) {
let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow);
let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
let elem = cwu.elementFromPoint(aX, aY, true, true);
while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
let rect = elem.getBoundingClientRect();
aX -= rect.left;
aY -= rect.top;
cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
elem = cwu.elementFromPoint(aX, aY, true, true);
}
return elem;
},
/* Return the most appropriate clickable element (if any), starting from the given window
and drilling down through iframes as necessary. If no window is provided, the top-level
window of the currently selected tab is used. The coordinates provided should be CSS
pixels relative to the window's scroll position. The element returned may not actually
contain the coordinates passed in because of touch radius and clickability heuristics. */
elementFromPoint: function(aX, aY, aWindow) {
// browser's elementFromPoint expect browser-relative client coordinates.
// subtract browser's scroll values to adjust
let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow);
let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
let elem = this.getClosest(cwu, aX, aY);
// step through layers of IFRAMEs and FRAMES to find innermost element
while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
// adjust client coordinates' origin to be top left of iframe viewport
let rect = elem.getBoundingClientRect();
aX -= rect.left;
aY -= rect.top;
cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
elem = this.getClosest(cwu, aX, aY);
}
return elem;
},
/* Returns the touch radius in content px. */
getTouchRadius: function getTouchRadius() {
let dpiRatio = ViewportHandler.displayDPI / kReferenceDpi;
let zoom = BrowserApp.selectedTab._zoom;
return {
top: this.radius.top * dpiRatio / zoom,
right: this.radius.right * dpiRatio / zoom,
bottom: this.radius.bottom * dpiRatio / zoom,
left: this.radius.left * dpiRatio / zoom
};
},
/* Returns the touch radius in reference pixels. */
get radius() {
let prefs = Services.prefs;
delete this.radius;
return this.radius = { "top": prefs.getIntPref("browser.ui.touch.top"),
"right": prefs.getIntPref("browser.ui.touch.right"),
"bottom": prefs.getIntPref("browser.ui.touch.bottom"),
"left": prefs.getIntPref("browser.ui.touch.left")
};
},
get weight() {
delete this.weight;
return this.weight = { "visited": Services.prefs.getIntPref("browser.ui.touch.weight.visited") };
},
/* Retrieve the closest element to a point by looking at borders position */
getClosest: function getClosest(aWindowUtils, aX, aY) {
let target = aWindowUtils.elementFromPoint(aX, aY,
true, /* ignore root scroll frame*/
false); /* don't flush layout */
// if this element is clickable we return quickly. also, if it isn't,
// use a cache to speed up future calls to isElementClickable in the
// loop below.
let unclickableCache = new Array();
if (this.isElementClickable(target, unclickableCache, false))
return target;
target = null;
let radius = this.getTouchRadius();
let nodes = aWindowUtils.nodesFromRect(aX, aY, radius.top, radius.right, radius.bottom, radius.left, true, false);
let threshold = Number.POSITIVE_INFINITY;
for (let i = 0; i < nodes.length; i++) {
let current = nodes[i];
if (!current.mozMatchesSelector || !this.isElementClickable(current, unclickableCache, true))
continue;
let rect = current.getBoundingClientRect();
let distance = this._computeDistanceFromRect(aX, aY, rect);
// increase a little bit the weight for already visited items
if (current && current.mozMatchesSelector("*:visited"))
distance *= (this.weight.visited / 100);
if (distance < threshold) {
target = current;
threshold = distance;
}
}
return target;
},
isElementClickable: function isElementClickable(aElement, aUnclickableCache, aAllowBodyListeners) {
const selector = "a,:link,:visited,[role=button],button,input,select,textarea";
let stopNode = null;
if (!aAllowBodyListeners && aElement && aElement.ownerDocument)
stopNode = aElement.ownerDocument.body;
for (let elem = aElement; elem && elem != stopNode; elem = elem.parentNode) {
if (aUnclickableCache && aUnclickableCache.indexOf(elem) != -1)
continue;
if (this._hasMouseListener(elem))
return true;
if (elem.mozMatchesSelector && elem.mozMatchesSelector(selector))
return true;
if (elem instanceof HTMLLabelElement && elem.control != null)
return true;
if (aUnclickableCache)
aUnclickableCache.push(elem);
}
return false;
},
_computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) {
let x = 0, y = 0;
let xmost = aRect.left + aRect.width;
let ymost = aRect.top + aRect.height;
// compute horizontal distance from left/right border depending if X is
// before/inside/after the element's rectangle
if (aRect.left < aX && aX < xmost)
x = Math.min(xmost - aX, aX - aRect.left);
else if (aX < aRect.left)
x = aRect.left - aX;
else if (aX > xmost)
x = aX - xmost;
// compute vertical distance from top/bottom border depending if Y is
// above/inside/below the element's rectangle
if (aRect.top < aY && aY < ymost)
y = Math.min(ymost - aY, aY - aRect.top);
else if (aY < aRect.top)
y = aRect.top - aY;
if (aY > ymost)
y = aY - ymost;
return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
},
_els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
_clickableEvents: ["mousedown", "mouseup", "click"],
_hasMouseListener: function _hasMouseListener(aElement) {
let els = this._els;
let listeners = els.getListenerInfoFor(aElement, {});
for (let i = 0; i < listeners.length; i++) {
if (this._clickableEvents.indexOf(listeners[i].type) != -1)
return true;
}
return false;
},
getContentClientRects: function(aElement) {
let offset = { x: 0, y: 0 };
let nativeRects = aElement.getClientRects();
// step out of iframes and frames, offsetting scroll values
for (let frame = aElement.ownerDocument.defaultView; frame.frameElement; frame = frame.parent) {
// adjust client coordinates' origin to be top left of iframe viewport
let rect = frame.frameElement.getBoundingClientRect();
let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
offset.x += rect.left + parseInt(left);
offset.y += rect.top + parseInt(top);
}
let result = [];
for (let i = nativeRects.length - 1; i >= 0; i--) {
let r = nativeRects[i];
result.push({ left: r.left + offset.x,
top: r.top + offset.y,
width: r.width,
height: r.height
});
}
return result;
},
getBoundingContentRect: function(aElement) {
if (!aElement)
return {x: 0, y: 0, w: 0, h: 0};
let document = aElement.ownerDocument;
while (document.defaultView.frameElement)
document = document.defaultView.frameElement.ownerDocument;
let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
let scrollX = {}, scrollY = {};
cwu.getScrollXY(false, scrollX, scrollY);
let r = aElement.getBoundingClientRect();
// step out of iframes and frames, offsetting scroll values
for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) {
// adjust client coordinates' origin to be top left of iframe viewport
let rect = frame.frameElement.getBoundingClientRect();
let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
scrollX.value += rect.left + parseInt(left);
scrollY.value += rect.top + parseInt(top);
}
return {x: r.left + scrollX.value,
y: r.top + scrollY.value,
w: r.width,
h: r.height };
}
};
var ErrorPageEventHandler = {
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "click": {
// Don't trust synthetic events
if (!aEvent.isTrusted)
return;
let target = aEvent.originalTarget;
let errorDoc = target.ownerDocument;
// If the event came from an ssl error page, it is probably either the "Add
// Exception…" or "Get me out of here!" button
if (errorDoc.documentURI.startsWith("about:certerror?e=nssBadCert")) {
let perm = errorDoc.getElementById("permanentExceptionButton");
let temp = errorDoc.getElementById("temporaryExceptionButton");
if (target == temp || target == perm) {
// Handle setting an cert exception and reloading the page
try {
// Add a new SSL exception for this URL
let uri = Services.io.newURI(errorDoc.location.href, null, null);
let sslExceptions = new SSLExceptions();
if (target == perm)
sslExceptions.addPermanentException(uri, errorDoc.defaultView);
else
sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
} catch (e) {
dump("Failed to set cert exception: " + e + "\n");
}
errorDoc.location.reload();
} else if (target == errorDoc.getElementById("getMeOutOfHereButton")) {
errorDoc.location = "about:home";
}
} else if (errorDoc.documentURI.startsWith("about:blocked")) {
// The event came from a button on a malware/phishing block page
// First check whether it's malware or phishing, so that we can
// use the right strings/links
let isMalware = errorDoc.documentURI.contains("e=malwareBlocked");
let bucketName = isMalware ? "WARNING_MALWARE_PAGE_" : "WARNING_PHISHING_PAGE_";
let nsISecTel = Ci.nsISecurityUITelemetry;
let isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView);
bucketName += isIframe ? "TOP_" : "FRAME_";
let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
if (target == errorDoc.getElementById("getMeOutButton")) {
Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
errorDoc.location = "about:home";
} else if (target == errorDoc.getElementById("reportButton")) {
// We log even if malware/phishing info URL couldn't be found:
// the measurement is for how many users clicked the WHY BLOCKED button
Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "WHY_BLOCKED"]);
// This is the "Why is this site blocked" button. For malware,
// we can fetch a site-specific report, for phishing, we redirect
// to the generic page describing phishing protection.
if (isMalware) {
// Get the stop badware "why is this blocked" report url, append the current url, and go there.
try {
let reportURL = formatter.formatURLPref("browser.safebrowsing.malware.reportURL");
reportURL += errorDoc.location.href;
BrowserApp.selectedBrowser.loadURI(reportURL);
} catch (e) {
Cu.reportError("Couldn't get malware report URL: " + e);
}
} else {
// It's a phishing site, just link to the generic information page
let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
BrowserApp.selectedBrowser.loadURI(url + "phishing-malware");
}
} else if (target == errorDoc.getElementById("ignoreWarningButton")) {
Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "IGNORE_WARNING"]);
// Allow users to override and continue through to the site,
let webNav = BrowserApp.selectedBrowser.docShell.QueryInterface(Ci.nsIWebNavigation);
let location = BrowserApp.selectedBrowser.contentWindow.location;
webNav.loadURI(location, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, null, null, null);
// ....but add a notify bar as a reminder, so that they don't lose
// track after, e.g., tab switching.
NativeWindow.doorhanger.show(Strings.browser.GetStringFromName("safeBrowsingDoorhanger"), "safebrowsing-warning", [], BrowserApp.selectedTab.id);
}
}
break;
}
}
}
};
var FormAssistant = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
// Used to keep track of the element that corresponds to the current
// autocomplete suggestions
_currentInputElement: null,
_isBlocklisted: false,
// Keep track of whether or not an invalid form has been submitted
_invalidSubmit: false,
init: function() {
Services.obs.addObserver(this, "FormAssist:AutoComplete", false);
Services.obs.addObserver(this, "FormAssist:Blocklisted", false);
Services.obs.addObserver(this, "FormAssist:Hidden", false);
Services.obs.addObserver(this, "invalidformsubmit", false);
Services.obs.addObserver(this, "PanZoom:StateChange", false);
// We need to use a capturing listener for focus events
BrowserApp.deck.addEventListener("focus", this, true);
BrowserApp.deck.addEventListener("click", this, true);
BrowserApp.deck.addEventListener("input", this, false);
BrowserApp.deck.addEventListener("pageshow", this, false);
},
uninit: function() {
Services.obs.removeObserver(this, "FormAssist:AutoComplete");
Services.obs.removeObserver(this, "FormAssist:Blocklisted");
Services.obs.removeObserver(this, "FormAssist:Hidden");
Services.obs.removeObserver(this, "invalidformsubmit");
Services.obs.removeObserver(this, "PanZoom:StateChange");
BrowserApp.deck.removeEventListener("focus", this);
BrowserApp.deck.removeEventListener("click", this);
BrowserApp.deck.removeEventListener("input", this);
BrowserApp.deck.removeEventListener("pageshow", this);
},
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case "PanZoom:StateChange":
// If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing
if (aData == "TOUCHING" || aData == "WAITING_LISTENERS")
break;
if (aData == "NOTHING") {
// only look for input elements, not contentEditable or multiline text areas
let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
if (!focused)
break;
if (this._showValidationMessage(focused))
break;
this._showAutoCompleteSuggestions(focused, function () {});
} else {
// temporarily hide the form assist popup while we're panning or zooming the page
this._hideFormAssistPopup();
}
break;
case "FormAssist:AutoComplete":
if (!this._currentInputElement)
break;
let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
// If we have an active composition string, commit it before sending
// the autocomplete event with the text that will replace it.
try {
let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
if (imeEditor.composing)
imeEditor.forceCompositionEnd();
} catch (e) {}
editableElement.setUserInput(aData);
let event = this._currentInputElement.ownerDocument.createEvent("Events");
event.initEvent("DOMAutoComplete", true, true);
this._currentInputElement.dispatchEvent(event);
break;
case "FormAssist:Blocklisted":
this._isBlocklisted = (aData == "true");
break;
case "FormAssist:Hidden":
this._currentInputElement = null;
break;
}
},
notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
if (!aInvalidElements.length)
return;
// Ignore this notificaiton if the current tab doesn't contain the invalid form
if (BrowserApp.selectedBrowser.contentDocument !=
aFormElement.ownerDocument.defaultView.top.document)
return;
this._invalidSubmit = true;
// Our focus listener will show the element's validation message
let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
currentElement.focus();
},
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "focus":
let currentElement = aEvent.target;
// Only show a validation message on focus.
this._showValidationMessage(currentElement);
break;
case "click":
currentElement = aEvent.target;
// Prioritize a form validation message over autocomplete suggestions
// when the element is first focused (a form validation message will
// only be available if an invalid form was submitted)
if (this._showValidationMessage(currentElement))
break;
let checkResultsClick = hasResults => {
if (!hasResults) {
this._hideFormAssistPopup();
}
};
this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
break;
case "input":
currentElement = aEvent.target;
// Since we can only show one popup at a time, prioritze autocomplete
// suggestions over a form validation message
let checkResultsInput = hasResults => {
if (hasResults)
return;
if (this._showValidationMessage(currentElement))
return;
// If we're not showing autocomplete suggestions, hide the form assist popup
this._hideFormAssistPopup();
};
this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
break;
// Reset invalid submit state on each pageshow
case "pageshow":
if (!this._invalidSubmit)
return;
let selectedBrowser = BrowserApp.selectedBrowser;
if (selectedBrowser) {
let selectedDocument = selectedBrowser.contentDocument;
let target = aEvent.originalTarget;
if (target == selectedDocument || target.ownerDocument == selectedDocument)
this._invalidSubmit = false;
}
}
},
// We only want to show autocomplete suggestions for certain elements
_isAutoComplete: function _isAutoComplete(aElement) {
if (!(aElement instanceof HTMLInputElement) || aElement.readOnly ||
(aElement.getAttribute("type") == "password") ||
(aElement.hasAttribute("autocomplete") &&
aElement.getAttribute("autocomplete").toLowerCase() == "off"))
return false;
return true;
},
// Retrieves autocomplete suggestions for an element from the form autocomplete service.
// aCallback(array_of_suggestions) is called when results are available.
_getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
// Cache the form autocomplete service for future use
if (!this._formAutoCompleteService)
this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"].
getService(Ci.nsIFormAutoComplete);
let resultsAvailable = function (results) {
let suggestions = [];
for (let i = 0; i < results.matchCount; i++) {
let value = results.getValueAt(i);
// Do not show the value if it is the current one in the input field
if (value == aSearchString)
continue;
// Supply a label and value, since they can differ for datalist suggestions
suggestions.push({ label: value, value: value });
}
aCallback(suggestions);
};
this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
aSearchString, aElement, null,
resultsAvailable);
},
/**
* (Copied from mobile/xul/chrome/content/forms.js)
* This function is similar to getListSuggestions from
* components/satchel/src/nsInputListAutoComplete.js but sadly this one is
* used by the autocomplete.xml binding which is not in used in fennec
*/
_getListSuggestions: function _getListSuggestions(aElement) {
if (!(aElement instanceof HTMLInputElement) || !aElement.list)
return [];
let suggestions = [];
let filter = !aElement.hasAttribute("mozNoFilter");
let lowerFieldValue = aElement.value.toLowerCase();
let options = aElement.list.options;
let length = options.length;
for (let i = 0; i < length; i++) {
let item = options.item(i);
let label = item.value;
if (item.label)
label = item.label;
else if (item.text)
label = item.text;
if (filter && !(label.toLowerCase().contains(lowerFieldValue)) )
continue;
suggestions.push({ label: label, value: item.value });
}
return suggestions;
},
// Retrieves autocomplete suggestions for an element from the form autocomplete service
// and sends the suggestions to the Java UI, along with element position data. As
// autocomplete queries are asynchronous, calls aCallback when done with a true
// argument if results were found and false if no results were found.
_showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
if (!this._isAutoComplete(aElement)) {
aCallback(false);
return;
}
// Don't display the form auto-complete popup after the user starts typing
// to avoid confusing somes IME. See bug 758820 and bug 632744.
if (this._isBlocklisted && aElement.value.length > 0) {
aCallback(false);
return;
}
let resultsAvailable = autoCompleteSuggestions => {
// On desktop, we show datalist suggestions below autocomplete suggestions,
// without duplicates removed.
let listSuggestions = this._getListSuggestions(aElement);
let suggestions = autoCompleteSuggestions.concat(listSuggestions);
// Return false if there are no suggestions to show
if (!suggestions.length) {
aCallback(false);
return;
}
sendMessageToJava({
type: "FormAssist:AutoComplete",
suggestions: suggestions,
rect: ElementTouchHelper.getBoundingContentRect(aElement)
});
// Keep track of input element so we can fill it in if the user
// selects an autocomplete suggestion
this._currentInputElement = aElement;
aCallback(true);
};
this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
},
// Only show a validation message if the user submitted an invalid form,
// there's a non-empty message string, and the element is the correct type
_isValidateable: function _isValidateable(aElement) {
if (!this._invalidSubmit ||
!aElement.validationMessage ||
!(aElement instanceof HTMLInputElement ||
aElement instanceof HTMLTextAreaElement ||
aElement instanceof HTMLSelectElement ||
aElement instanceof HTMLButtonElement))
return false;
return true;
},
// Sends a validation message and position data for an element to the Java UI.
// Returns true if there's a validation message to show, false otherwise.
_showValidationMessage: function _sendValidationMessage(aElement) {
if (!this._isValidateable(aElement))
return false;
sendMessageToJava({
type: "FormAssist:ValidationMessage",
validationMessage: aElement.validationMessage,
rect: ElementTouchHelper.getBoundingContentRect(aElement)
});
return true;
},
_hideFormAssistPopup: function _hideFormAssistPopup() {
sendMessageToJava({ type: "FormAssist:Hide" });
}
};
/**
* An object to watch for Gecko status changes -- add-on installs, pref changes
* -- and reflect them back to Java.
*/
let HealthReportStatusListener = {
PREF_ACCEPT_LANG: "intl.accept_languages",
PREF_BLOCKLIST_ENABLED: "extensions.blocklist.enabled",
PREF_TELEMETRY_ENABLED:
#ifdef MOZ_TELEMETRY_REPORTING
// Telemetry pref differs based on build.
#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
"toolkit.telemetry.enabledPreRelease",
#else
"toolkit.telemetry.enabled",
#endif
#else
null,
#endif
init: function () {
try {
AddonManager.addAddonListener(this);
} catch (ex) {
console.log("Failed to initialize add-on status listener. FHR cannot report add-on state. " + ex);
}
console.log("Adding HealthReport:RequestSnapshot observer.");
Services.obs.addObserver(this, "HealthReport:RequestSnapshot", false);
Services.prefs.addObserver(this.PREF_ACCEPT_LANG, this, false);
Services.prefs.addObserver(this.PREF_BLOCKLIST_ENABLED, this, false);
if (this.PREF_TELEMETRY_ENABLED) {
Services.prefs.addObserver(this.PREF_TELEMETRY_ENABLED, this, false);
}
},
uninit: function () {
Services.obs.removeObserver(this, "HealthReport:RequestSnapshot");
Services.prefs.removeObserver(this.PREF_ACCEPT_LANG, this);
Services.prefs.removeObserver(this.PREF_BLOCKLIST_ENABLED, this);
if (this.PREF_TELEMETRY_ENABLED) {
Services.prefs.removeObserver(this.PREF_TELEMETRY_ENABLED, this);
}
AddonManager.removeAddonListener(this);
},
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "HealthReport:RequestSnapshot":
HealthReportStatusListener.sendSnapshotToJava();
break;
case "nsPref:changed":
let response = {
type: "Pref:Change",
pref: aData,
isUserSet: Services.prefs.prefHasUserValue(aData),
};
switch (aData) {
case this.PREF_ACCEPT_LANG:
response.value = Services.prefs.getCharPref(aData);
break;
case this.PREF_TELEMETRY_ENABLED:
case this.PREF_BLOCKLIST_ENABLED:
response.value = Services.prefs.getBoolPref(aData);
break;
default:
console.log("Unexpected pref in HealthReportStatusListener: " + aData);
return;
}
sendMessageToJava(response);
break;
}
},
MILLISECONDS_PER_DAY: 24 * 60 * 60 * 1000,
COPY_FIELDS: [
"blocklistState",
"userDisabled",
"appDisabled",
"version",
"type",
"scope",
"foreignInstall",
"hasBinaryComponents",
],
// Add-on types for which full details are recorded in FHR.
// All other types are ignored.
FULL_DETAIL_TYPES: [
"plugin",
"extension",
"service",
],
/**
* Return true if the add-on is not of a type for which we report full details.
* These add-ons will still make it over to Java, but will be filtered out.
*/
_shouldIgnore: function (aAddon) {
return this.FULL_DETAIL_TYPES.indexOf(aAddon.type) == -1;
},
_dateToDays: function (aDate) {
return Math.floor(aDate.getTime() / this.MILLISECONDS_PER_DAY);
},
jsonForAddon: function (aAddon) {
let o = {};
if (aAddon.installDate) {
o.installDay = this._dateToDays(aAddon.installDate);
}
if (aAddon.updateDate) {
o.updateDay = this._dateToDays(aAddon.updateDate);
}
for (let field of this.COPY_FIELDS) {
o[field] = aAddon[field];
}
return o;
},
notifyJava: function (aAddon, aNeedsRestart, aAction="Addons:Change") {
let json = this.jsonForAddon(aAddon);
if (this._shouldIgnore(aAddon)) {
json.ignore = true;
}
sendMessageToJava({ type: aAction, id: aAddon.id, json: json });
},
// Add-on listeners.
onEnabling: function (aAddon, aNeedsRestart) {
this.notifyJava(aAddon, aNeedsRestart);
},
onDisabling: function (aAddon, aNeedsRestart) {
this.notifyJava(aAddon, aNeedsRestart);
},
onInstalling: function (aAddon, aNeedsRestart) {
this.notifyJava(aAddon, aNeedsRestart);
},
onUninstalling: function (aAddon, aNeedsRestart) {
this.notifyJava(aAddon, aNeedsRestart, "Addons:Uninstalling");
},
onPropertyChanged: function (aAddon, aProperties) {
this.notifyJava(aAddon);
},
onOperationCancelled: function (aAddon) {
this.notifyJava(aAddon);
},
sendSnapshotToJava: function () {
AddonManager.getAllAddons(function (aAddons) {
let jsonA = {};
if (aAddons) {
for (let i = 0; i < aAddons.length; ++i) {
let addon = aAddons[i];
try {
let addonJSON = HealthReportStatusListener.jsonForAddon(addon);
if (HealthReportStatusListener._shouldIgnore(addon)) {
addonJSON.ignore = true;
}
jsonA[addon.id] = addonJSON;
} catch (e) {
// Just skip this add-on.
}
}
}
// Now add prefs.
let jsonP = {};
for (let pref of [this.PREF_BLOCKLIST_ENABLED, this.PREF_TELEMETRY_ENABLED]) {
if (!pref) {
// This will be the case for PREF_TELEMETRY_ENABLED in developer builds.
continue;
}
jsonP[pref] = {
pref: pref,
value: Services.prefs.getBoolPref(pref),
isUserSet: Services.prefs.prefHasUserValue(pref),
};
}
for (let pref of [this.PREF_ACCEPT_LANG]) {
jsonP[pref] = {
pref: pref,
value: Services.prefs.getCharPref(pref),
isUserSet: Services.prefs.prefHasUserValue(pref),
};
}
console.log("Sending snapshot message.");
sendMessageToJava({
type: "HealthReport:Snapshot",
json: {
addons: jsonA,
prefs: jsonP,
},
});
}.bind(this));
},
};
var XPInstallObserver = {
init: function xpi_init() {
Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
AddonManager.addInstallListener(XPInstallObserver);
},
uninit: function xpi_uninit() {
Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
AddonManager.removeInstallListener(XPInstallObserver);
},
observe: function xpi_observer(aSubject, aTopic, aData) {
switch (aTopic) {
case "addon-install-started":
NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsDownloading"), "short");
break;
case "addon-install-blocked":
let installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo);
let win = installInfo.originatingWindow;
let tab = BrowserApp.getTabForWindow(win.top);
if (!tab)
return;
let host = null;
if (installInfo.originatingURI) {
host = installInfo.originatingURI.host;
}
let brandShortName = Strings.brand.GetStringFromName("brandShortName");
let notificationName, buttons, message;
let strings = Strings.browser;
let enabled = true;
try {
enabled = Services.prefs.getBoolPref("xpinstall.enabled");
}
catch (e) {}
if (!enabled) {
notificationName = "xpinstall-disabled";
if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
message = strings.GetStringFromName("xpinstallDisabledMessageLocked");
buttons = [];
} else {
message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
buttons = [{
label: strings.GetStringFromName("xpinstallDisabledButton"),
callback: function editPrefs() {
Services.prefs.setBoolPref("xpinstall.enabled", true);
return false;
}
}];
}
} else {
notificationName = "xpinstall";
if (host) {
// We have a host which asked for the install.
message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
} else {
// Without a host we address the add-on as the initiator of the install.
let addon = null;
if (installInfo.installs.length > 0) {
addon = installInfo.installs[0].name;
}
if (addon) {
// We have an addon name, show the regular message.
message = strings.formatStringFromName("xpinstallPromptWarningLocal", [brandShortName, addon], 2);
} else {
// We don't have an addon name, show an alternative message.
message = strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1);
}
}
buttons = [{
label: strings.GetStringFromName("xpinstallPromptAllowButton"),
callback: function() {
// Kick off the install
installInfo.install();
return false;
}
}];
}
NativeWindow.doorhanger.show(message, aTopic, buttons, tab.id);
break;
}
},
onInstallEnded: function(aInstall, aAddon) {
let needsRestart = false;
if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
needsRestart = true;
else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
needsRestart = true;
if (needsRestart) {
this.showRestartPrompt();
} else {
// Display completion message for new installs or updates not done Automatically
if (!aInstall.existingAddon || !AddonManager.shouldAutoUpdate(aInstall.existingAddon)) {
let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart");
NativeWindow.toast.show(message, "short");
}
}
},
onInstallFailed: function(aInstall) {
NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short");
},
onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {},
onDownloadFailed: function(aInstall) {
this.onInstallFailed(aInstall);
},
onDownloadCancelled: function(aInstall) {
let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
if (!host)
host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
if (aInstall.error != 0)
error += aInstall.error;
else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
error += "Blocklisted";
else if (aInstall.addon && (!aInstall.addon.isCompatible || !aInstall.addon.isPlatformCompatible))
error += "Incompatible";
else
return; // No need to show anything in this case.
let msg = Strings.browser.GetStringFromName(error);
// TODO: formatStringFromName
msg = msg.replace("#1", aInstall.name);
if (host)
msg = msg.replace("#2", host);
msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName"));
msg = msg.replace("#4", Services.appinfo.version);
NativeWindow.toast.show(msg, "short");
},
showRestartPrompt: function() {
let buttons = [{
label: Strings.browser.GetStringFromName("notificationRestart.button"),
callback: function() {
// Notify all windows that an application quit has been requested
let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
// If nothing aborted, quit the app
if (cancelQuit.data == false) {
let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
}
}
}];
let message = Strings.browser.GetStringFromName("notificationRestart.normal");
NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
},
hideRestartPrompt: function() {
NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
}
};
// Blindly copied from Safari documentation for now.
const kViewportMinScale = 0;
const kViewportMaxScale = 10;
const kViewportMinWidth = 200;
const kViewportMaxWidth = 10000;
const kViewportMinHeight = 223;
const kViewportMaxHeight = 10000;
var ViewportHandler = {
// The cached viewport metadata for each document. We tie viewport metadata to each document
// instead of to each tab so that we don't have to update it when the document changes. Using an
// ES6 weak map lets us avoid leaks.
_metadata: new WeakMap(),
init: function init() {
addEventListener("DOMMetaAdded", this, false);
Services.obs.addObserver(this, "Window:Resize", false);
},
uninit: function uninit() {
removeEventListener("DOMMetaAdded", this, false);
Services.obs.removeObserver(this, "Window:Resize");
},
handleEvent: function handleEvent(aEvent) {
switch (aEvent.type) {
case "DOMMetaAdded":
let target = aEvent.originalTarget;
if (target.name != "viewport")
break;
let document = target.ownerDocument;
let browser = BrowserApp.getBrowserForDocument(document);
let tab = BrowserApp.getTabForBrowser(browser);
if (tab)
this.updateMetadata(tab, false);
break;
}
},
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case "Window:Resize":
if (window.outerWidth == gScreenWidth && window.outerHeight == gScreenHeight)
break;
if (window.outerWidth == 0 || window.outerHeight == 0)
break;
let oldScreenWidth = gScreenWidth;
gScreenWidth = window.outerWidth * window.devicePixelRatio;
gScreenHeight = window.outerHeight * window.devicePixelRatio;
let tabs = BrowserApp.tabs;
for (let i = 0; i < tabs.length; i++)
tabs[i].updateViewportSize(oldScreenWidth);
break;
}
},
updateMetadata: function updateMetadata(tab, aInitialLoad) {
let contentWindow = tab.browser.contentWindow;
if (contentWindow.document.documentElement) {
let metadata = this.getViewportMetadata(contentWindow);
tab.updateViewportMetadata(metadata, aInitialLoad);
}
},
/**
* Returns the ViewportMetadata object.
*/
getViewportMetadata: function getViewportMetadata(aWindow) {
let windowUtils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
// viewport details found here
// http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
// http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html
// Note: These values will be NaN if parseFloat or parseInt doesn't find a number.
// Remember that NaN is contagious: Math.max(1, NaN) == Math.min(1, NaN) == NaN.
let hasMetaViewport = true;
let scale = parseFloat(windowUtils.getDocumentMetadata("viewport-initial-scale"));
let minScale = parseFloat(windowUtils.getDocumentMetadata("viewport-minimum-scale"));
let maxScale = parseFloat(windowUtils.getDocumentMetadata("viewport-maximum-scale"));
let widthStr = windowUtils.getDocumentMetadata("viewport-width");
let heightStr = windowUtils.getDocumentMetadata("viewport-height");
let width = this.clamp(parseInt(widthStr), kViewportMinWidth, kViewportMaxWidth) || 0;
let height = this.clamp(parseInt(heightStr), kViewportMinHeight, kViewportMaxHeight) || 0;
// Allow zoom unless explicity disabled or minScale and maxScale are equal.
// WebKit allows 0, "no", or "false" for viewport-user-scalable.
// Note: NaN != NaN. Therefore if minScale and maxScale are undefined the clause has no effect.
let allowZoomStr = windowUtils.getDocumentMetadata("viewport-user-scalable");
let allowZoom = !/^(0|no|false)$/.test(allowZoomStr) && (minScale != maxScale);
// Double-tap should always be disabled if allowZoom is disabled. So we initialize
// allowDoubleTapZoom to the same value as allowZoom and have additional conditions to
// disable it in updateViewportSize.
let allowDoubleTapZoom = allowZoom;
let autoSize = true;
if (isNaN(scale) && isNaN(minScale) && isNaN(maxScale) && allowZoomStr == "" && widthStr == "" && heightStr == "") {
// Only check for HandheldFriendly if we don't have a viewport meta tag
let handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly");
if (handheldFriendly == "true") {
return new ViewportMetadata({
defaultZoom: 1,
autoSize: true,
allowZoom: true,
allowDoubleTapZoom: false
});
}
let doctype = aWindow.document.doctype;
if (doctype && /(WAP|WML|Mobile)/.test(doctype.publicId)) {
return new ViewportMetadata({
defaultZoom: 1,
autoSize: true,
allowZoom: true,
allowDoubleTapZoom: false
});
}
hasMetaViewport = false;
let defaultZoom = Services.prefs.getIntPref("browser.viewport.defaultZoom");
if (defaultZoom >= 0) {
scale = defaultZoom / 1000;
autoSize = false;
}
}
scale = this.clamp(scale, kViewportMinScale, kViewportMaxScale);
minScale = this.clamp(minScale, kViewportMinScale, kViewportMaxScale);
maxScale = this.clamp(maxScale, minScale, kViewportMaxScale);
if (autoSize) {
// If initial scale is 1.0 and width is not set, assume width=device-width
autoSize = (widthStr == "device-width" ||
(!widthStr && (heightStr == "device-height" || scale == 1.0)));
}
let isRTL = aWindow.document.documentElement.dir == "rtl";
return new ViewportMetadata({
defaultZoom: scale,
minZoom: minScale,
maxZoom: maxScale,
width: width,
height: height,
autoSize: autoSize,
allowZoom: allowZoom,
allowDoubleTapZoom: allowDoubleTapZoom,
isSpecified: hasMetaViewport,
isRTL: isRTL
});
},
clamp: function(num, min, max) {
return Math.max(min, Math.min(max, num));
},
get displayDPI() {
let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
delete this.displayDPI;
return this.displayDPI = utils.displayDPI;
},
/**
* Returns the viewport metadata for the given document, or the default metrics if no viewport
* metadata is available for that document.
*/
getMetadataForDocument: function getMetadataForDocument(aDocument) {
let metadata = this._metadata.get(aDocument, new ViewportMetadata());
return metadata;
},
/** Updates the saved viewport metadata for the given content document. */
setMetadataForDocument: function setMetadataForDocument(aDocument, aMetadata) {
if (!aMetadata)
this._metadata.delete(aDocument);
else
this._metadata.set(aDocument, aMetadata);
}
};
/**
* An object which represents the page's preferred viewport properties:
* width (int): The CSS viewport width in px.
* height (int): The CSS viewport height in px.
* defaultZoom (float): The initial scale when the page is loaded.
* minZoom (float): The minimum zoom level.
* maxZoom (float): The maximum zoom level.
* autoSize (boolean): Resize the CSS viewport when the window resizes.
* allowZoom (boolean): Let the user zoom in or out.
* allowDoubleTapZoom (boolean): Allow double-tap to zoom in.
* isSpecified (boolean): Whether the page viewport is specified or not.
*/
function ViewportMetadata(aMetadata = {}) {
this.width = ("width" in aMetadata) ? aMetadata.width : 0;
this.height = ("height" in aMetadata) ? aMetadata.height : 0;
this.defaultZoom = ("defaultZoom" in aMetadata) ? aMetadata.defaultZoom : 0;
this.minZoom = ("minZoom" in aMetadata) ? aMetadata.minZoom : 0;
this.maxZoom = ("maxZoom" in aMetadata) ? aMetadata.maxZoom : 0;
this.autoSize = ("autoSize" in aMetadata) ? aMetadata.autoSize : false;
this.allowZoom = ("allowZoom" in aMetadata) ? aMetadata.allowZoom : true;
this.allowDoubleTapZoom = ("allowDoubleTapZoom" in aMetadata) ? aMetadata.allowDoubleTapZoom : true;
this.isSpecified = ("isSpecified" in aMetadata) ? aMetadata.isSpecified : false;
this.isRTL = ("isRTL" in aMetadata) ? aMetadata.isRTL : false;
Object.seal(this);
}
ViewportMetadata.prototype = {
width: null,
height: null,
defaultZoom: null,
minZoom: null,
maxZoom: null,
autoSize: null,
allowZoom: null,
allowDoubleTapZoom: null,
isSpecified: null,
isRTL: null,
toString: function() {
return "width=" + this.width
+ "; height=" + this.height
+ "; defaultZoom=" + this.defaultZoom
+ "; minZoom=" + this.minZoom
+ "; maxZoom=" + this.maxZoom
+ "; autoSize=" + this.autoSize
+ "; allowZoom=" + this.allowZoom
+ "; allowDoubleTapZoom=" + this.allowDoubleTapZoom
+ "; isSpecified=" + this.isSpecified
+ "; isRTL=" + this.isRTL;
}
};
/**
* Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml
*/
var PopupBlockerObserver = {
onUpdatePageReport: function onUpdatePageReport(aEvent) {
let browser = BrowserApp.selectedBrowser;
if (aEvent.originalTarget != browser)
return;
if (!browser.pageReport)
return;
let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup");
if (result == Ci.nsIPermissionManager.DENY_ACTION)
return;
// Only show the notification again if we've not already shown it. Since
// notifications are per-browser, we don't need to worry about re-adding
// it.
if (!browser.pageReport.reported) {
if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
let brandShortName = Strings.brand.GetStringFromName("brandShortName");
let popupCount = browser.pageReport.length;
let strings = Strings.browser;
let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message"))
.replace("#1", brandShortName)
.replace("#2", popupCount);
let buttons = [
{
label: strings.GetStringFromName("popup.show"),
callback: function(aChecked) {
// Set permission before opening popup windows
if (aChecked)
PopupBlockerObserver.allowPopupsForSite(true);
PopupBlockerObserver.showPopupsForSite();
}
},
{
label: strings.GetStringFromName("popup.dontShow"),
callback: function(aChecked) {
if (aChecked)
PopupBlockerObserver.allowPopupsForSite(false);
}
}
];
let options = { checkbox: Strings.browser.GetStringFromName("popup.dontAskAgain") };
NativeWindow.doorhanger.show(message, "popup-blocked", buttons, null, options);
}
// Record the fact that we've reported this blocked popup, so we don't
// show it again.
browser.pageReport.reported = true;
}
},
allowPopupsForSite: function allowPopupsForSite(aAllow) {
let currentURI = BrowserApp.selectedBrowser.currentURI;
Services.perms.add(currentURI, "popup", aAllow
? Ci.nsIPermissionManager.ALLOW_ACTION
: Ci.nsIPermissionManager.DENY_ACTION);
dump("Allowing popups for: " + currentURI);
},
showPopupsForSite: function showPopupsForSite() {
let uri = BrowserApp.selectedBrowser.currentURI;
let pageReport = BrowserApp.selectedBrowser.pageReport;
if (pageReport) {
for (let i = 0; i < pageReport.length; ++i) {
let popupURIspec = pageReport[i].popupWindowURI.spec;
// Sometimes the popup URI that we get back from the pageReport
// isn't useful (for instance, netscape.com's popup URI ends up
// being "http://www.netscape.com", which isn't really the URI of
// the popup they're trying to show). This isn't going to be
// useful to the user, so we won't create a menu item for it.
if (popupURIspec == "" || popupURIspec == "about:blank" || popupURIspec == uri.spec)
continue;
let popupFeatures = pageReport[i].popupWindowFeatures;
let popupName = pageReport[i].popupWindowName;
let parent = BrowserApp.selectedTab;
let isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
BrowserApp.addTab(popupURIspec, { parentId: parent.id, isPrivate: isPrivate });
}
}
}
};
var IndexedDB = {
_permissionsPrompt: "indexedDB-permissions-prompt",
_permissionsResponse: "indexedDB-permissions-response",
_quotaPrompt: "indexedDB-quota-prompt",
_quotaResponse: "indexedDB-quota-response",
_quotaCancel: "indexedDB-quota-cancel",
init: function IndexedDB_init() {
Services.obs.addObserver(this, this._permissionsPrompt, false);
Services.obs.addObserver(this, this._quotaPrompt, false);
Services.obs.addObserver(this, this._quotaCancel, false);
},
uninit: function IndexedDB_uninit() {
Services.obs.removeObserver(this, this._permissionsPrompt);
Services.obs.removeObserver(this, this._quotaPrompt);
Services.obs.removeObserver(this, this._quotaCancel);
},
observe: function IndexedDB_observe(subject, topic, data) {
if (topic != this._permissionsPrompt &&
topic != this._quotaPrompt &&
topic != this._quotaCancel) {
throw new Error("Unexpected topic!");
}
let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
let contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
let contentDocument = contentWindow.document;
let tab = BrowserApp.getTabForWindow(contentWindow);
if (!tab)
return;
let host = contentDocument.documentURIObject.asciiHost;
let strings = Strings.browser;
let message, responseTopic;
if (topic == this._permissionsPrompt) {
message = strings.formatStringFromName("offlineApps.ask", [host], 1);
responseTopic = this._permissionsResponse;
} else if (topic == this._quotaPrompt) {
message = strings.formatStringFromName("indexedDBQuota.wantsTo", [ host, data ], 2);
responseTopic = this._quotaResponse;
} else if (topic == this._quotaCancel) {
responseTopic = this._quotaResponse;
}
const firstTimeoutDuration = 300000; // 5 minutes
let timeoutId;
let notificationID = responseTopic + host;
let observer = requestor.getInterface(Ci.nsIObserver);
// This will be set to the result of PopupNotifications.show() below, or to
// the result of PopupNotifications.getNotification() if this is a
// quotaCancel notification.
let notification;
function timeoutNotification() {
// Remove the notification.
NativeWindow.doorhanger.hide(notificationID, tab.id);
// Clear all of our timeout stuff. We may be called directly, not just
// when the timeout actually elapses.
clearTimeout(timeoutId);
// And tell the page that the popup timed out.
observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
}
if (topic == this._quotaCancel) {
NativeWindow.doorhanger.hide(notificationID, tab.id);
timeoutNotification();
observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
return;
}
let buttons = [{
label: strings.GetStringFromName("offlineApps.allow"),
callback: function() {
clearTimeout(timeoutId);
observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION);
}
},
{
label: strings.GetStringFromName("offlineApps.dontAllow2"),
callback: function(aChecked) {
clearTimeout(timeoutId);
let action = aChecked ? Ci.nsIPermissionManager.DENY_ACTION : Ci.nsIPermissionManager.UNKNOWN_ACTION;
observer.observe(null, responseTopic, action);
}
}];
let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
// Set the timeoutId after the popup has been created, and use the long
// timeout value. If the user doesn't notice the popup after this amount of
// time then it is most likely not visible and we want to alert the page.
timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
}
};
var ClipboardHelper = {
// Recorded so search with option can be removed/replaced when default engine changed.
_searchMenuItem: -1,
get clipboardHelper() {
delete this.clipboardHelper;
return this.clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
},
get clipboard() {
delete this.clipboard;
return this.clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
},
copy: function(aElement, aX, aY) {
if (SelectionHandler.isSelectionActive()) {
SelectionHandler.copySelection();
return;
}
let selectionStart = aElement.selectionStart;
let selectionEnd = aElement.selectionEnd;
if (selectionStart != selectionEnd) {
string = aElement.value.slice(selectionStart, selectionEnd);
this.clipboardHelper.copyString(string, aElement.ownerDocument);
} else {
this.clipboardHelper.copyString(aElement.value, aElement.ownerDocument);
}
},
paste: function(aElement) {
if (!aElement || !(aElement instanceof Ci.nsIDOMNSEditableElement))
return;
let target = aElement.QueryInterface(Ci.nsIDOMNSEditableElement);
target.editor.paste(Ci.nsIClipboard.kGlobalClipboard);
target.focus();
},
inputMethod: function(aElement) {
Cc["@mozilla.org/imepicker;1"].getService(Ci.nsIIMEPicker).show();
},
getCopyContext: function(isCopyAll) {
return {
matches: function(aElement, aX, aY) {
// Do not show "Copy All" for normal non-input text selection.
if (!isCopyAll && SelectionHandler.isSelectionActive())
return true;
if (NativeWindow.contextmenus.textContext.matches(aElement)) {
// Don't include "copy" for password fields.
// mozIsTextField(true) tests for only non-password fields.
if (aElement instanceof Ci.nsIDOMHTMLInputElement && !aElement.mozIsTextField(true))
return false;
let selectionStart = aElement.selectionStart;
let selectionEnd = aElement.selectionEnd;
if (selectionStart != selectionEnd)
return true;
if (isCopyAll && aElement.textLength > 0)
return true;
}
return false;
}
};
},
selectAllContext: {
matches: function selectAllContextMatches(aElement, aX, aY) {
if (SelectionHandler.isSelectionActive())
return true;
if (NativeWindow.contextmenus.textContext.matches(aElement))
return (aElement.selectionStart > 0 || aElement.selectionEnd < aElement.textLength);
return false;
}
},
shareContext: {
matches: function shareContextMatches(aElement, aX, aY) {
return SelectionHandler.isSelectionActive();
}
},
searchWithContext: {
matches: function searchWithContextMatches(aElement, aX, aY) {
return SelectionHandler.isSelectionActive();
}
},
pasteContext: {
matches: function(aElement) {
if (NativeWindow.contextmenus.textContext.matches(aElement)) {
let flavors = ["text/unicode"];
return ClipboardHelper.clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard);
}
return false;
}
},
cutContext: {
matches: function(aElement) {
let copyctx = ClipboardHelper.getCopyContext(false);
if (NativeWindow.contextmenus.textContext.matches(aElement)) {
return copyctx.matches(aElement);
}
return false;
}
}
};
var CharacterEncoding = {
_charsets: [],
init: function init() {
Services.obs.addObserver(this, "CharEncoding:Get", false);
Services.obs.addObserver(this, "CharEncoding:Set", false);
this.sendState();
},
uninit: function uninit() {
Services.obs.removeObserver(this, "CharEncoding:Get");
Services.obs.removeObserver(this, "CharEncoding:Set");
},
observe: function observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "CharEncoding:Get":
this.getEncoding();
break;
case "CharEncoding:Set":
this.setEncoding(aData);
break;
}
},
sendState: function sendState() {
let showCharEncoding = "false";
try {
showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
} catch (e) { /* Optional */ }
sendMessageToJava({
type: "CharEncoding:State",
visible: showCharEncoding
});
},
getEncoding: function getEncoding() {
function infoToCharset(info) {
return { code: info.value, title: info.label };
}
if (!this._charsets.length) {
let data = CharsetMenu.getData();
// In the desktop UI, the pinned charsets are shown above the rest.
let pinnedCharsets = data.pinnedCharsets.map(infoToCharset);
let otherCharsets = data.otherCharsets.map(infoToCharset)
this._charsets = pinnedCharsets.concat(otherCharsets);
}
// Look for the index of the selected charset. Default to -1 if the
// doc charset isn't found in the list of available charsets.
let docCharset = BrowserApp.selectedBrowser.contentDocument.characterSet;
let selected = -1;
let charsetCount = this._charsets.length;
for (let i = 0; i < charsetCount; i++) {
if (this._charsets[i].code === docCharset) {
selected = i;
break;
}
}
sendMessageToJava({
type: "CharEncoding:Data",
charsets: this._charsets,
selected: selected
});
},
setEncoding: function setEncoding(aEncoding) {
let browser = BrowserApp.selectedBrowser;
browser.docShell.gatherCharsetMenuTelemetry();
browser.docShell.charset = aEncoding;
browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
}
};
var IdentityHandler = {
// No trusted identity information. No site identity icon is shown.
IDENTITY_MODE_UNKNOWN: "unknown",
// Minimal SSL CA-signed domain verification. Blue lock icon is shown.
IDENTITY_MODE_DOMAIN_VERIFIED: "verified",
// High-quality identity information. Green lock icon is shown.
IDENTITY_MODE_IDENTIFIED: "identified",
// The following mixed content modes are only used if "security.mixed_content.block_active_content"
// is enabled. Even though the mixed content state and identitity state are orthogonal,
// our Java frontend coalesces them into one indicator.
// Blocked active mixed content. Shield icon is shown, with a popup option to load content.
IDENTITY_MODE_MIXED_CONTENT_BLOCKED: "mixed_content_blocked",
// Loaded active mixed content. Yellow triangle icon is shown.
IDENTITY_MODE_MIXED_CONTENT_LOADED: "mixed_content_loaded",
// Cache the most recent SSLStatus and Location seen in getIdentityStrings
_lastStatus : null,
_lastLocation : null,
/**
* Helper to parse out the important parts of _lastStatus (of the SSL cert in
* particular) for use in constructing identity UI strings
*/
getIdentityData : function() {
let result = {};
let status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus);
let cert = status.serverCert;
// Human readable name of Subject
result.subjectOrg = cert.organization;
// SubjectName fields, broken up for individual access
if (cert.subjectName) {
result.subjectNameFields = {};
cert.subjectName.split(",").forEach(function(v) {
let field = v.split("=");
this[field[0]] = field[1];
}, result.subjectNameFields);
// Call out city, state, and country specifically
result.city = result.subjectNameFields.L;
result.state = result.subjectNameFields.ST;
result.country = result.subjectNameFields.C;
}
// Human readable name of Certificate Authority
result.caOrg = cert.issuerOrganization || cert.issuerCommonName;
result.cert = cert;
return result;
},
/**
* Determines the identity mode corresponding to the icon we show in the urlbar.
*/
getIdentityMode: function getIdentityMode(aState) {
if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT)
return this.IDENTITY_MODE_MIXED_CONTENT_BLOCKED;
// Only show an indicator for loaded mixed content if the pref to block it is enabled
if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
Services.prefs.getBoolPref("security.mixed_content.block_active_content"))
return this.IDENTITY_MODE_MIXED_CONTENT_LOADED;
if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
return this.IDENTITY_MODE_IDENTIFIED;
if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE)
return this.IDENTITY_MODE_DOMAIN_VERIFIED;
return this.IDENTITY_MODE_UNKNOWN;
},
/**
* Determine the identity of the page being displayed by examining its SSL cert
* (if available). Return the data needed to update the UI.
*/
checkIdentity: function checkIdentity(aState, aBrowser) {
this._lastStatus = aBrowser.securityUI
.QueryInterface(Components.interfaces.nsISSLStatusProvider)
.SSLStatus;
// Don't pass in the actual location object, since it can cause us to
// hold on to the window object too long. Just pass in the fields we
// care about. (bug 424829)
let locationObj = {};
try {
let location = aBrowser.contentWindow.location;
locationObj.host = location.host;
locationObj.hostname = location.hostname;
locationObj.port = location.port;
} catch (ex) {
// Can sometimes throw if the URL being visited has no host/hostname,
// e.g. about:blank. The _state for these pages means we won't need these
// properties anyways, though.
}
this._lastLocation = locationObj;
let mode = this.getIdentityMode(aState);
let result = { mode: mode };
// Don't show identity data for pages with an unknown identity or if any
// mixed content is loaded (mixed display content is loaded by default).
if (mode == this.IDENTITY_MODE_UNKNOWN ||
aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
return result;
// Ideally we'd just make this a Java string
result.encrypted = Strings.browser.GetStringFromName("identity.encrypted2");
result.host = this.getEffectiveHost();
let iData = this.getIdentityData();
result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
// If the cert is identified, then we can populate the results with credentials
if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
result.owner = iData.subjectOrg;
// Build an appropriate supplemental block out of whatever location data we have
let supplemental = "";
if (iData.city)
supplemental += iData.city + "\n";
if (iData.state && iData.country)
supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2);
else if (iData.state) // State only
supplemental += iData.state;
else if (iData.country) // Country only
supplemental += iData.country;
result.supplemental = supplemental;
return result;
}
// Otherwise, we don't know the cert owner
result.owner = Strings.browser.GetStringFromName("identity.ownerUnknown3");
// Cache the override service the first time we need to check it
if (!this._overrideService)
this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
// Check whether this site is a security exception. XPConnect does the right
// thing here in terms of converting _lastLocation.port from string to int, but
// the overrideService doesn't like undefined ports, so make sure we have
// something in the default case (bug 432241).
// .hostname can return an empty string in some exceptional cases -
// hasMatchingOverride does not handle that, so avoid calling it.
// Updating the tooltip value in those cases isn't critical.
// FIXME: Fixing bug 646690 would probably makes this check unnecessary
if (this._lastLocation.hostname &&
this._overrideService.hasMatchingOverride(this._lastLocation.hostname,
(this._lastLocation.port || 443),
iData.cert, {}, {}))
result.verifier = Strings.browser.GetStringFromName("identity.identified.verified_by_you");
return result;
},
/**
* Return the eTLD+1 version of the current hostname
*/
getEffectiveHost: function getEffectiveHost() {
if (!this._IDNService)
this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
.getService(Ci.nsIIDNService);
try {
let baseDomain = Services.eTLD.getBaseDomainFromHost(this._lastLocation.hostname);
return this._IDNService.convertToDisplayIDN(baseDomain, {});
} catch (e) {
// If something goes wrong (e.g. hostname is an IP address) just fail back
// to the full domain.
return this._lastLocation.hostname;
}
}
};
function OverscrollController(aTab) {
this.tab = aTab;
}
OverscrollController.prototype = {
supportsCommand : function supportsCommand(aCommand) {
if (aCommand != "cmd_linePrevious" && aCommand != "cmd_scrollPageUp")
return false;
return (this.tab.getViewport().y == 0);
},
isCommandEnabled : function isCommandEnabled(aCommand) {
return this.supportsCommand(aCommand);
},
doCommand : function doCommand(aCommand){
sendMessageToJava({ type: "ToggleChrome:Focus" });
},
onEvent : function onEvent(aEvent) { }
};
var SearchEngines = {
_contextMenuId: null,
PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
init: function init() {
Services.obs.addObserver(this, "SearchEngines:Add", false);
Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
Services.obs.addObserver(this, "SearchEngines:Remove", false);
Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
let filter = {
matches: function (aElement) {
// Copied from body of isTargetAKeywordField function in nsContextMenu.js
if(!(aElement instanceof HTMLInputElement))
return false;
let form = aElement.form;
if (!form || aElement.type == "password")
return false;
let method = form.method.toUpperCase();
// These are the following types of forms we can create keywords for:
//
// method encoding type can create keyword
// GET * YES
// * YES
// POST * YES
// POST application/x-www-form-urlencoded YES
// POST text/plain NO ( a little tricky to do)
// POST multipart/form-data NO
// POST everything else YES
return (method == "GET" || method == "") ||
(form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
}
};
SelectionHandler.addAction({
id: "search_add_action",
label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine"),
icon: "drawable://ab_add_search_engine",
selector: filter,
action: function(aElement) {
SearchEngines.addEngine(aElement);
}
});
},
uninit: function uninit() {
Services.obs.removeObserver(this, "SearchEngines:Add");
Services.obs.removeObserver(this, "SearchEngines:GetVisible");
Services.obs.removeObserver(this, "SearchEngines:Remove");
Services.obs.removeObserver(this, "SearchEngines:RestoreDefaults");
Services.obs.removeObserver(this, "SearchEngines:SetDefault");
if (this._contextMenuId != null)
NativeWindow.contextmenus.remove(this._contextMenuId);
},
// Fetch list of search engines. all ? All engines : Visible engines only.
_handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
if (!Components.isSuccessCode(rv)) {
Cu.reportError("Could not initialize search service, bailing out.");
return;
}
let engineData = Services.search.getVisibleEngines({});
let searchEngines = engineData.map(function (engine) {
return {
name: engine.name,
identifier: engine.identifier,
iconURI: (engine.iconURI ? engine.iconURI.spec : null),
hidden: engine.hidden
};
});
let suggestTemplate = null;
let suggestEngine = null;
// Check to see if the default engine supports search suggestions. We only need to check
// the default engine because we only show suggestions for the default engine in the UI.
let engine = Services.search.defaultEngine;
if (engine.supportsResponseType("application/x-suggestions+json")) {
suggestEngine = engine.name;
suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
}
// By convention, the currently configured default engine is at position zero in searchEngines.
sendMessageToJava({
type: "SearchEngines:Data",
searchEngines: searchEngines,
suggest: {
engine: suggestEngine,
template: suggestTemplate,
enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
}
});
// Send a speculative connection to the default engine.
let connector = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri;
let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext);
try {
connector.speculativeConnect(searchURI, callbacks);
} catch (e) {}
},
// Helper method to extract the engine name from a JSON. Simplifies the observe function.
_extractEngineFromJSON: function _extractEngineFromJSON(aData) {
let data = JSON.parse(aData);
return Services.search.getEngineByName(data.engine);
},
observe: function observe(aSubject, aTopic, aData) {
let engine;
switch(aTopic) {
case "SearchEngines:Add":
this.displaySearchEnginesList(aData);
break;
case "SearchEngines:GetVisible":
Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
break;
case "SearchEngines:Remove":
// Make sure the engine isn't hidden before removing it, to make sure it's
// visible if the user later re-adds it (works around bug 341833)
engine = this._extractEngineFromJSON(aData);
engine.hidden = false;
Services.search.removeEngine(engine);
break;
case "SearchEngines:RestoreDefaults":
// Un-hides all default engines.
Services.search.restoreDefaultEngines();
break;
case "SearchEngines:SetDefault":
engine = this._extractEngineFromJSON(aData);
// Move the new default search engine to the top of the search engine list.
Services.search.moveEngine(engine, 0);
Services.search.defaultEngine = engine;
break;
default:
dump("Unexpected message type observed: " + aTopic);
break;
}
},
// Display context menu listing names of the search engines available to be added.
displaySearchEnginesList: function displaySearchEnginesList(aData) {
let data = JSON.parse(aData);
let tab = BrowserApp.getTabForId(data.tabId);
if (!tab)
return;
let browser = tab.browser;
let engines = browser.engines;
let p = new Prompt({
window: browser.contentWindow
}).setSingleChoiceItems(engines.map(function(e) {
return { label: e.title };
})).show((function(data) {
if (data.button == -1)
return;
this.addOpenSearchEngine(engines[data.button]);
engines.splice(data.button, 1);
if (engines.length < 1) {
// Broadcast message that there are no more add-able search engines.
let newEngineMessage = {
type: "Link:OpenSearch",
tabID: tab.id,
visible: false
};
sendMessageToJava(newEngineMessage);
}
}).bind(this));
},
addOpenSearchEngine: function addOpenSearchEngine(engine) {
Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
onSuccess: function() {
// Display a toast confirming addition of new search engine.
NativeWindow.toast.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), "long");
},
onError: function(aCode) {
let errorMessage;
if (aCode == 2) {
// Engine is a duplicate.
errorMessage = "alertSearchEngineDuplicateToast";
} else {
// Unknown failure. Display general error message.
errorMessage = "alertSearchEngineErrorToast";
}
NativeWindow.toast.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), "long");
}
});
},
addEngine: function addEngine(aElement) {
let form = aElement.form;
let charset = aElement.ownerDocument.characterSet;
let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null);
let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
let method = form.method.toUpperCase();
let formData = [];
for (let i = 0; i < form.elements.length; ++i) {
let el = form.elements[i];
if (!el.type)
continue;
// make this text field a generic search parameter
if (aElement == el) {
formData.push({ name: el.name, value: "{searchTerms}" });
continue;
}
let type = el.type.toLowerCase();
let escapedName = escape(el.name);
let escapedValue = escape(el.value);
// add other form elements as parameters
switch (el.type) {
case "checkbox":
case "radio":
if (!el.checked) break;
case "text":
case "hidden":
case "textarea":
formData.push({ name: escapedName, value: escapedValue });
break;
case "select-one":
for (let option of el.options) {
if (option.selected) {
formData.push({ name: escapedName, value: escapedValue });
break;
}
}
}
}
// prompt user for name of search engine
let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine");
let title = { value: (aElement.ownerDocument.title || docURI.host) };
if (!Services.prompt.prompt(null, promptTitle, null, title, null, {}))
return;
// fetch the favicon for this page
let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
let mDBConn = Services.storage.openDatabase(dbFile);
let stmts = [];
stmts[0] = mDBConn.createStatement("SELECT favicon FROM history_with_favicons WHERE url = ?");
stmts[0].bindStringParameter(0, docURI.spec);
let favicon = null;
Services.search.init(function addEngine_cb(rv) {
if (!Components.isSuccessCode(rv)) {
Cu.reportError("Could not initialize search service, bailing out.");
return;
}
mDBConn.executeAsync(stmts, stmts.length, {
handleResult: function (results) {
let bytes = results.getNextRow().getResultByName("favicon");
if (bytes && bytes.length) {
favicon = "data:image/x-icon;base64," + btoa(String.fromCharCode.apply(null, bytes));
}
},
handleCompletion: function (reason) {
// if there's already an engine with this name, add a number to
// make the name unique (e.g., "Google" becomes "Google 2")
let name = title.value;
for (let i = 2; Services.search.getEngineByName(name); i++)
name = title.value + " " + i;
Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL);
let engine = Services.search.getEngineByName(name);
engine.wrappedJSObject._queryCharset = charset;
for (let i = 0; i < formData.length; ++i) {
let param = formData[i];
if (param.name && param.value)
engine.addParam(param.name, param.value, null);
}
}
});
});
}
};
var ActivityObserver = {
init: function ao_init() {
Services.obs.addObserver(this, "application-background", false);
Services.obs.addObserver(this, "application-foreground", false);
},
observe: function ao_observe(aSubject, aTopic, aData) {
let isForeground = false;
let tab = BrowserApp.selectedTab;
switch (aTopic) {
case "application-background" :
let doc = (tab ? tab.browser.contentDocument : null);
if (doc && doc.mozFullScreen) {
doc.mozCancelFullScreen();
}
isForeground = false;
break;
case "application-foreground" :
isForeground = true;
break;
}
if (tab && tab.getActive() != isForeground) {
tab.setActive(isForeground);
}
}
};
#ifndef MOZ_ANDROID_SYNTHAPKS
var WebappsUI = {
init: function init() {
Cu.import("resource://gre/modules/Webapps.jsm");
Cu.import("resource://gre/modules/AppsUtils.jsm");
DOMApplicationRegistry.allAppsLaunchable = true;
Services.obs.addObserver(this, "webapps-ask-install", false);
Services.obs.addObserver(this, "webapps-launch", false);
Services.obs.addObserver(this, "webapps-uninstall", false);
Services.obs.addObserver(this, "webapps-install-error", false);
},
uninit: function unint() {
Services.obs.removeObserver(this, "webapps-ask-install");
Services.obs.removeObserver(this, "webapps-launch");
Services.obs.removeObserver(this, "webapps-uninstall");
Services.obs.removeObserver(this, "webapps-install-error");
},
DEFAULT_ICON: "chrome://browser/skin/images/default-app-icon.png",
DEFAULT_PREFS_FILENAME: "default-prefs.js",
observe: function observe(aSubject, aTopic, aData) {
let data = {};
try {
data = JSON.parse(aData);
data.mm = aSubject;
} catch(ex) { }
switch (aTopic) {
case "webapps-install-error":
let msg = "";
switch (aData) {
case "INVALID_MANIFEST":
case "MANIFEST_PARSE_ERROR":
msg = Strings.browser.GetStringFromName("webapps.manifestInstallError");
break;
case "NETWORK_ERROR":
case "MANIFEST_URL_ERROR":
msg = Strings.browser.GetStringFromName("webapps.networkInstallError");
break;
default:
msg = Strings.browser.GetStringFromName("webapps.installError");
}
NativeWindow.toast.show(msg, "short");
console.log("Error installing app: " + aData);
break;
case "webapps-ask-install":
this.doInstall(data);
break;
case "webapps-launch":
this.openURL(data.manifestURL, data.origin);
break;
case "webapps-uninstall":
sendMessageToJava({
type: "Webapps:Uninstall",
origin: data.origin
});
break;
}
},
doInstall: function doInstall(aData) {
let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
if (Services.prompt.confirm(null, Strings.browser.GetStringFromName("webapps.installTitle"), manifest.name + "\n" + aData.app.origin)) {
// Get a profile for the app to be installed in. We'll download everything before creating the icons.
let origin = aData.app.origin;
sendMessageToJava({
type: "Webapps:Preinstall",
name: manifest.name,
manifestURL: aData.app.manifestURL,
origin: origin
}, (data) => {
let profilePath = data.profile;
if (!profilePath)
return;
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
file.initWithPath(profilePath);
let self = this;
DOMApplicationRegistry.confirmInstall(aData, file,
function (aManifest) {
let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
// the manifest argument is the manifest from within the zip file,
// TODO so now would be a good time to ask about permissions.
self.makeBase64Icon(localeManifest.biggestIconURL || this.DEFAULT_ICON,
function(scaledIcon, fullsizeIcon) {
// if java returned a profile path to us, try to use it to pre-populate the app cache
// also save the icon so that it can be used in the splash screen
try {
let iconFile = file.clone();
iconFile.append("logo.png");
let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
persist.persistFlags |= Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
let source = Services.io.newURI(fullsizeIcon, "UTF8", null);
persist.saveURI(source, null, null, null, null, iconFile, null);
// aData.app.origin may now point to the app: url that hosts this app
sendMessageToJava({
type: "Webapps:Postinstall",
name: localeManifest.name,
manifestURL: aData.app.manifestURL,
originalOrigin: origin,
origin: aData.app.origin,
iconURL: fullsizeIcon
});
if (!!aData.isPackage) {
// For packaged apps, put a notification in the notification bar.
let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", {
observe: function () {
self.openURL(aData.app.manifestURL, aData.app.origin);
}
}, "webapp");
}
// Create a system notification allowing the user to launch the app
let observer = {
observe: function (aSubject, aTopic) {
if (aTopic == "alertclickcallback") {
WebappsUI.openURL(aData.app.manifestURL, origin);
}
}
};
let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", observer, "webapp");
} catch(ex) {
console.log(ex);
}
self.writeDefaultPrefs(file, localeManifest);
}
);
}
);
});
} else {
DOMApplicationRegistry.denyInstall(aData);
}
},
writeDefaultPrefs: function webapps_writeDefaultPrefs(aProfile, aManifest) {
// build any app specific default prefs
let prefs = [];
if (aManifest.orientation) {
prefs.push({name:"app.orientation.default", value: aManifest.orientation.join(",") });
}
// write them into the app profile
let defaultPrefsFile = aProfile.clone();
defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME);
this._writeData(defaultPrefsFile, prefs);
},
_writeData: function(aFile, aPrefs) {
if (aPrefs.length > 0) {
let array = new TextEncoder().encode(JSON.stringify(aPrefs));
OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
console.log("Error writing default prefs: " + reason);
});
}
},
openURL: function openURL(aManifestURL, aOrigin) {
sendMessageToJava({
type: "Webapps:Open",
manifestURL: aManifestURL,
origin: aOrigin
});
},
get iconSize() {
let iconSize = 64;
try {
let jni = new JNI();
let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell");
let method = jni.getStaticMethodID(cls, "getPreferredIconSize", "()I");
iconSize = jni.callStaticIntMethod(cls, method);
jni.close();
} catch(ex) {
console.log(ex);
}
delete this.iconSize;
return this.iconSize = iconSize;
},
makeBase64Icon: function loadAndMakeBase64Icon(aIconURL, aCallbackFunction) {
let size = this.iconSize;
let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
canvas.width = canvas.height = size;
let ctx = canvas.getContext("2d");
let favicon = new Image();
favicon.onload = function() {
ctx.drawImage(favicon, 0, 0, size, size);
let scaledIcon = canvas.toDataURL("image/png", "");
canvas.width = favicon.width;
canvas.height = favicon.height;
ctx.drawImage(favicon, 0, 0, favicon.width, favicon.height);
let fullsizeIcon = canvas.toDataURL("image/png", "");
canvas = null;
aCallbackFunction.call(null, scaledIcon, fullsizeIcon);
};
favicon.onerror = function() {
Cu.reportError("CreateShortcut: favicon image load error");
// if the image failed to load, and it was not our default icon, attempt to
// use our default as a fallback
if (favicon.src != WebappsUI.DEFAULT_ICON) {
favicon.src = WebappsUI.DEFAULT_ICON;
}
};
favicon.src = aIconURL;
},
createShortcut: function createShortcut(aTitle, aURL, aIconURL, aType) {
this.makeBase64Icon(aIconURL, function _createShortcut(icon) {
try {
let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService);
shell.createShortcut(aTitle, aURL, icon, aType);
} catch(e) {
Cu.reportError(e);
}
});
}
}
#endif
var RemoteDebugger = {
init: function rd_init() {
Services.prefs.addObserver("devtools.debugger.", this, false);
if (this._isEnabled())
this._start();
},
observe: function rd_observe(aSubject, aTopic, aData) {
if (aTopic != "nsPref:changed")
return;
switch (aData) {
case "devtools.debugger.remote-enabled":
if (this._isEnabled())
this._start();
else
this._stop();
break;
case "devtools.debugger.remote-port":
if (this._isEnabled())
this._restart();
break;
}
},
uninit: function rd_uninit() {
Services.prefs.removeObserver("devtools.debugger.", this);
this._stop();
},
_getPort: function _rd_getPort() {
return Services.prefs.getIntPref("devtools.debugger.remote-port");
},
_isEnabled: function rd_isEnabled() {
return Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
},
/**
* Prompt the user to accept or decline the incoming connection.
* This is passed to DebuggerService.init as a callback.
*
* @return true if the connection should be permitted, false otherwise
*/
_showConnectionPrompt: function rd_showConnectionPrompt() {
let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
let msg = Strings.browser.GetStringFromName("remoteIncomingPromptMessage");
let disable = Strings.browser.GetStringFromName("remoteIncomingPromptDisable");
let cancel = Strings.browser.GetStringFromName("remoteIncomingPromptCancel");
let agree = Strings.browser.GetStringFromName("remoteIncomingPromptAccept");
// Make prompt. Note: button order is in reverse.
let prompt = new Prompt({
window: null,
hint: "remotedebug",
title: title,
message: msg,
buttons: [ agree, cancel, disable ],
priority: 1
});
// The debugger server expects a synchronous response, so spin on result since Prompt is async.
let result = null;
prompt.show(function(data) {
result = data.button;
});
// Spin this thread while we wait for a result.
let thread = Services.tm.currentThread;
while (result == null)
thread.processNextEvent(true);
if (result === 0)
return true;
if (result === 2) {
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
this._stop();
}
return false;
},
_restart: function rd_restart() {
this._stop();
this._start();
},
_start: function rd_start() {
try {
if (!DebuggerServer.initialized) {
DebuggerServer.init(this._showConnectionPrompt.bind(this));
DebuggerServer.addBrowserActors();
DebuggerServer.addActors("chrome://browser/content/dbg-browser-actors.js");
}
let port = this._getPort();
DebuggerServer.openListener(port);
dump("Remote debugger listening on port " + port);
} catch(e) {
dump("Remote debugger didn't start: " + e);
}
},
_stop: function rd_start() {
DebuggerServer.closeListener();
dump("Remote debugger stopped");
}
};
var Telemetry = {
SHARED_PREF_TELEMETRY_ENABLED: "datareporting.telemetry.enabled",
addData: function addData(aHistogramId, aValue) {
let histogram = Services.telemetry.getHistogramById(aHistogramId);
histogram.add(aValue);
},
};
let Reader = {
// Version of the cache database schema
DB_VERSION: 1,
DEBUG: 0,
READER_ADD_SUCCESS: 0,
READER_ADD_FAILED: 1,
READER_ADD_DUPLICATE: 2,
// Don't try to parse the page if it has too many elements (for memory and
// performance reasons)
MAX_ELEMS_TO_PARSE: 3000,
isEnabledForParseOnLoad: false,
init: function Reader_init() {
this.log("Init()");
this._requests = {};
this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
Services.obs.addObserver(this, "Reader:Add", false);
Services.obs.addObserver(this, "Reader:Remove", false);
Services.prefs.addObserver("reader.parse-on-load.", this, false);
},
pageAction: {
readerModeCallback: function(){
sendMessageToJava({
gecko: {
type: "Reader:Click",
}
});
},
readerModeActiveCallback: function(){
sendMessageToJava({
gecko: {
type: "Reader:LongClick",
}
});
},
},
updatePageAction: function(tab) {
if(this.pageAction.id) {
NativeWindow.pageactions.remove(this.pageAction.id);
delete this.pageAction.id;
}
if (tab.readerActive) {
this.pageAction.id = NativeWindow.pageactions.add({
title: Strings.browser.GetStringFromName("readerMode.exit"),
icon: "drawable://reader_active",
clickCallback: this.pageAction.readerModeCallback,
important: true
});
} else if (tab.readerEnabled) {
this.pageAction.id = NativeWindow.pageactions.add({
title: Strings.browser.GetStringFromName("readerMode.enter"),
icon: "drawable://reader",
clickCallback:this.pageAction.readerModeCallback,
longClickCallback: this.pageAction.readerModeActiveCallback,
important: true
});
}
},
observe: function(aMessage, aTopic, aData) {
switch(aTopic) {
case "Reader:Add": {
let args = JSON.parse(aData);
if ('fromAboutReader' in args) {
// Ignore adds initiated from aboutReader menu banner
break;
}
let tabID = null;
let url, urlWithoutRef;
if ('tabID' in args) {
tabID = args.tabID;
let tab = BrowserApp.getTabForId(tabID);
let currentURI = tab.browser.currentURI;
url = currentURI.spec;
urlWithoutRef = currentURI.specIgnoringRef;
} else if ('url' in args) {
let uri = Services.io.newURI(args.url, null, null);
url = uri.spec;
urlWithoutRef = uri.specIgnoringRef;
} else {
throw new Error("Reader:Add requires a tabID or an URL as argument");
}
let sendResult = function(result, article) {
article = article || {};
this.log("Reader:Add success=" + result + ", url=" + url + ", title=" + article.title + ", excerpt=" + article.excerpt);
sendMessageToJava({
type: "Reader:Added",
result: result,
title: article.title,
url: url,
length: article.length,
excerpt: article.excerpt
});
}.bind(this);
let handleArticle = function(article) {
if (!article) {
sendResult(this.READER_ADD_FAILED, null);
return;
}
this.storeArticleInCache(article, function(success) {
let result = (success ? this.READER_ADD_SUCCESS : this.READER_ADD_FAILED);
sendResult(result, article);
}.bind(this));
}.bind(this);
this.getArticleFromCache(urlWithoutRef, function (article) {
// If the article is already in reading list, bail
if (article) {
sendResult(this.READER_ADD_DUPLICATE, null);
return;
}
if (tabID != null) {
this.getArticleForTab(tabID, urlWithoutRef, handleArticle);
} else {
this.parseDocumentFromURL(urlWithoutRef, handleArticle);
}
}.bind(this));
break;
}
case "Reader:Remove": {
let url = aData;
this.removeArticleFromCache(url, function(success) {
this.log("Reader:Remove success=" + success + ", url=" + url);
if (success) {
sendMessageToJava({
type: "Reader:Removed",
url: url
});
}
}.bind(this));
break;
}
case "nsPref:changed": {
if (aData.startsWith("reader.parse-on-load.")) {
this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
}
break;
}
}
},
getStateForParseOnLoad: function Reader_getStateForParseOnLoad() {
let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
// For low-memory devices, don't allow reader mode since it takes up a lot of memory.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
return isForceEnabled || (isEnabled && !BrowserApp.isOnLowMemoryPlatform);
},
parseDocumentFromURL: function Reader_parseDocumentFromURL(url, callback) {
// If there's an on-going request for the same URL, simply append one
// more callback to it to be called when the request is done.
if (url in this._requests) {
let request = this._requests[url];
request.callbacks.push(callback);
return;
}
let request = { url: url, callbacks: [callback] };
this._requests[url] = request;
try {
this.log("parseDocumentFromURL: " + url);
// First, try to find a cached parsed article in the DB
this.getArticleFromCache(url, function(article) {
if (article) {
this.log("Page found in cache, return article immediately");
this._runCallbacksAndFinish(request, article);
return;
}
if (!this._requests) {
this.log("Reader has been destroyed, abort");
return;
}
// Article hasn't been found in the cache DB, we need to
// download the page and parse the article out of it.
this._downloadAndParseDocument(url, request);
}.bind(this));
} catch (e) {
this.log("Error parsing document from URL: " + e);
this._runCallbacksAndFinish(request, null);
}
},
getArticleForTab: function Reader_getArticleForTab(tabId, url, callback) {
let tab = BrowserApp.getTabForId(tabId);
if (tab) {
let article = tab.savedArticle;
if (article && article.url == url) {
this.log("Saved article found in tab");
callback(article);
return;
}
}
this.parseDocumentFromURL(url, callback);
},
parseDocumentFromTab: function(tabId, callback) {
try {
this.log("parseDocumentFromTab: " + tabId);
let tab = BrowserApp.getTabForId(tabId);
let url = tab.browser.contentWindow.location.href;
let uri = Services.io.newURI(url, null, null);
if (!this._shouldCheckUri(uri)) {
callback(null);
return;
}
// First, try to find a cached parsed article in the DB
this.getArticleFromCache(url, function(article) {
if (article) {
this.log("Page found in cache, return article immediately");
callback(article);
return;
}
let doc = tab.browser.contentWindow.document;
this._readerParse(uri, doc, function (article) {
if (!article) {
this.log("Failed to parse page");
callback(null);
return;
}
callback(article);
}.bind(this));
}.bind(this));
} catch (e) {
this.log("Error parsing document from tab: " + e);
callback(null);
}
},
getArticleFromCache: function Reader_getArticleFromCache(url, callback) {
this._getCacheDB(function(cacheDB) {
if (!cacheDB) {
callback(false);
return;
}
let transaction = cacheDB.transaction(cacheDB.objectStoreNames);
let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
let request = articles.get(url);
request.onerror = function(event) {
this.log("Error getting article from the cache DB: " + url);
callback(null);
}.bind(this);
request.onsuccess = function(event) {
this.log("Got article from the cache DB: " + event.target.result);
callback(event.target.result);
}.bind(this);
}.bind(this));
},
storeArticleInCache: function Reader_storeArticleInCache(article, callback) {
this._getCacheDB(function(cacheDB) {
if (!cacheDB) {
callback(false);
return;
}
let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
let request = articles.add(article);
request.onerror = function(event) {
this.log("Error storing article in the cache DB: " + article.url);
callback(false);
}.bind(this);
request.onsuccess = function(event) {
this.log("Stored article in the cache DB: " + article.url);
callback(true);
}.bind(this);
}.bind(this));
},
removeArticleFromCache: function Reader_removeArticleFromCache(url, callback) {
this._getCacheDB(function(cacheDB) {
if (!cacheDB) {
callback(false);
return;
}
let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
let request = articles.delete(url);
request.onerror = function(event) {
this.log("Error removing article from the cache DB: " + url);
callback(false);
}.bind(this);
request.onsuccess = function(event) {
this.log("Removed article from the cache DB: " + url);
callback(true);
}.bind(this);
}.bind(this));
},
uninit: function Reader_uninit() {
Services.prefs.removeObserver("reader.parse-on-load.", this);
Services.obs.removeObserver(this, "Reader:Add");
Services.obs.removeObserver(this, "Reader:Remove");
let requests = this._requests;
for (let url in requests) {
let request = requests[url];
if (request.browser) {
let browser = request.browser;
browser.parentNode.removeChild(browser);
}
}
delete this._requests;
if (this._cacheDB) {
this._cacheDB.close();
delete this._cacheDB;
}
},
log: function(msg) {
if (this.DEBUG)
dump("Reader: " + msg);
},
_shouldCheckUri: function Reader_shouldCheckUri(uri) {
if ((uri.prePath + "/") === uri.spec) {
this.log("Not parsing home page: " + uri.spec);
return false;
}
if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
this.log("Not parsing URI scheme: " + uri.scheme);
return false;
}
return true;
},
_readerParse: function Reader_readerParse(uri, doc, callback) {
let numTags = doc.getElementsByTagName("*").length;
if (numTags > this.MAX_ELEMS_TO_PARSE) {
this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
callback(null);
return;
}
let worker = new ChromeWorker("readerWorker.js");
worker.onmessage = function (evt) {
let article = evt.data;
// Append URL to the article data. specIgnoringRef will ignore any hash
// in the URL.
if (article) {
article.url = uri.specIgnoringRef;
let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
.convertToPlainText(article.title, flags, 0);
}
callback(article);
};
try {
worker.postMessage({
uri: {
spec: uri.spec,
host: uri.host,
prePath: uri.prePath,
scheme: uri.scheme,
pathBase: Services.io.newURI(".", null, uri).spec
},
doc: new XMLSerializer().serializeToString(doc)
});
} catch (e) {
dump("Reader: could not build Readability arguments: " + e);
callback(null);
}
},
_runCallbacksAndFinish: function Reader_runCallbacksAndFinish(request, result) {
delete this._requests[request.url];
request.callbacks.forEach(function(callback) {
callback(result);
});
},
_downloadDocument: function Reader_downloadDocument(url, callback) {
// We want to parse those arbitrary pages safely, outside the privileged
// context of chrome. We create a hidden browser element to fetch the
// loaded page's document object then discard the browser element.
let browser = document.createElement("browser");
browser.setAttribute("type", "content");
browser.setAttribute("collapsed", "true");
browser.setAttribute("disablehistory", "true");
document.documentElement.appendChild(browser);
browser.stop();
browser.webNavigation.allowAuth = false;
browser.webNavigation.allowImages = false;
browser.webNavigation.allowJavascript = false;
browser.webNavigation.allowMetaRedirects = true;
browser.webNavigation.allowPlugins = false;
browser.addEventListener("DOMContentLoaded", function (event) {
let doc = event.originalTarget;
// ignore on frames and other documents
if (doc != browser.contentDocument)
return;
this.log("Done loading: " + doc);
if (doc.location.href == "about:blank") {
callback(null);
// Request has finished with error, remove browser element
browser.parentNode.removeChild(browser);
return;
}
callback(doc);
}.bind(this));
browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
null, null, null);
return browser;
},
_downloadAndParseDocument: function Reader_downloadAndParseDocument(url, request) {
try {
this.log("Needs to fetch page, creating request: " + url);
request.browser = this._downloadDocument(url, function(doc) {
this.log("Finished loading page: " + doc);
if (!doc) {
this.log("Error loading page");
this._runCallbacksAndFinish(request, null);
return;
}
this.log("Parsing response with Readability");
let uri = Services.io.newURI(url, null, null);
this._readerParse(uri, doc, function (article) {
// Delete reference to the browser element as we've finished parsing.
let browser = request.browser;
if (browser) {
browser.parentNode.removeChild(browser);
delete request.browser;
}
if (!article) {
this.log("Failed to parse page");
this._runCallbacksAndFinish(request, null);
return;
}
this.log("Parsing has been successful");
this._runCallbacksAndFinish(request, article);
}.bind(this));
}.bind(this));
} catch (e) {
this.log("Error downloading and parsing document: " + e);
this._runCallbacksAndFinish(request, null);
}
},
_getCacheDB: function Reader_getCacheDB(callback) {
if (this._cacheDB) {
callback(this._cacheDB);
return;
}
let request = window.indexedDB.open("about:reader", this.DB_VERSION);
request.onerror = function(event) {
this.log("Error connecting to the cache DB");
this._cacheDB = null;
callback(null);
}.bind(this);
request.onsuccess = function(event) {
this.log("Successfully connected to the cache DB");
this._cacheDB = event.target.result;
callback(this._cacheDB);
}.bind(this);
request.onupgradeneeded = function(event) {
this.log("Database schema upgrade from " +
event.oldVersion + " to " + event.newVersion);
let cacheDB = event.target.result;
// Create the articles object store
this.log("Creating articles object store");
cacheDB.createObjectStore("articles", { keyPath: "url" });
this.log("Database upgrade done: " + this.DB_VERSION);
}.bind(this);
}
};
var ExternalApps = {
_contextMenuId: null,
// extend _getLink to pickup html5 media links.
_getMediaLink: function(aElement) {
let uri = NativeWindow.contextmenus._getLink(aElement);
if (uri == null && aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement)) {
try {
let mediaSrc = aElement.currentSrc || aElement.src;
uri = ContentAreaUtils.makeURI(mediaSrc, null, null);
} catch (e) {}
}
return uri;
},
init: function helper_init() {
this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) {
let uri = null;
var node = aElement;
while (node && !uri) {
uri = ExternalApps._getMediaLink(node);
node = node.parentNode;
}
let apps = [];
if (uri)
apps = HelperApps.getAppsForUri(uri);
return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) :
Strings.browser.GetStringFromName("helperapps.openWithList2");
}, this.filter, this.openExternal);
},
uninit: function helper_uninit() {
if (this._contextMenuId !== null) {
NativeWindow.contextmenus.remove(this._contextMenuId);
}
this._contextMenuId = null;
},
filter: {
matches: function(aElement) {
let uri = ExternalApps._getMediaLink(aElement);
let apps = [];
if (uri) {
apps = HelperApps.getAppsForUri(uri);
}
return apps.length > 0;
}
},
openExternal: function(aElement) {
let uri = ExternalApps._getMediaLink(aElement);
HelperApps.launchUri(uri);
},
shouldCheckUri: function(uri) {
if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
return false;
}
return true;
},
updatePageAction: function updatePageAction(uri) {
HelperApps.getAppsForUri(uri, { filterHttp: true }, (apps) => {
this.clearPageAction();
if (apps.length > 0)
this._setUriForPageAction(uri, apps);
});
},
updatePageActionUri: function updatePageActionUri(uri) {
this._pageActionUri = uri;
},
_setUriForPageAction: function setUriForPageAction(uri, apps) {
this.updatePageActionUri(uri);
// If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
if (this._pageActionId != undefined)
return;
this._pageActionId = NativeWindow.pageactions.add({
title: Strings.browser.GetStringFromName("openInApp.pageAction"),
icon: "drawable://icon_openinapp",
clickCallback: () => {
if (apps.length > 1) {
// Use the HelperApps prompt here to filter out any Http handlers
HelperApps.prompt(apps, {
title: Strings.browser.GetStringFromName("openInApp.pageAction"),
buttons: [
Strings.browser.GetStringFromName("openInApp.ok"),
Strings.browser.GetStringFromName("openInApp.cancel")
]
}, (result) => {
if (result.button != 0) {
return;
}
apps[result.icongrid0].launch(this._pageActionUri);
});
} else {
apps[0].launch(this._pageActionUri);
}
}
});
},
clearPageAction: function clearPageAction() {
if(!this._pageActionId)
return;
NativeWindow.pageactions.remove(this._pageActionId);
delete this._pageActionId;
},
};
var Distribution = {
// File used to store campaign data
_file: null,
init: function dc_init() {
Services.obs.addObserver(this, "Distribution:Set", false);
Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
Services.obs.addObserver(this, "Campaign:Set", false);
// Look for file outside the APK:
// /data/data/org.mozilla.xxx/distribution.json
this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
this._file.append("distribution.json");
this.readJSON(this._file, this.update);
},
uninit: function dc_uninit() {
Services.obs.removeObserver(this, "Distribution:Set");
Services.obs.removeObserver(this, "prefservice:after-app-defaults");
Services.obs.removeObserver(this, "Campaign:Set");
},
observe: function dc_observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "Distribution:Set":
// Reload the default prefs so we can observe "prefservice:after-app-defaults"
Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
break;
case "prefservice:after-app-defaults":
this.getPrefs();
break;
case "Campaign:Set": {
// Update the prefs for this session
try {
this.update(JSON.parse(aData));
} catch (ex) {
Cu.reportError("Distribution: Could not parse JSON: " + ex);
return;
}
// Asynchronously copy the data to the file.
let array = new TextEncoder().encode(aData);
OS.File.writeAtomic(this._file.path, array, { tmpPath: this._file.path + ".tmp" });
break;
}
}
},
update: function dc_update(aData) {
// Force the distribution preferences on the default branch
let defaults = Services.prefs.getDefaultBranch(null);
defaults.setCharPref("distribution.id", aData.id);
defaults.setCharPref("distribution.version", aData.version);
},
getPrefs: function dc_getPrefs() {
// Get the distribution directory, and bail if it doesn't exist.
let file = FileUtils.getDir("XREAppDist", [], false);
if (!file.exists())
return;
file.append("preferences.json");
this.readJSON(file, this.applyPrefs);
},
applyPrefs: function dc_applyPrefs(aData) {
// Check for required Global preferences
let global = aData["Global"];
if (!(global && global["id"] && global["version"] && global["about"])) {
Cu.reportError("Distribution: missing or incomplete Global preferences");
return;
}
// Force the distribution preferences on the default branch
let defaults = Services.prefs.getDefaultBranch(null);
defaults.setCharPref("distribution.id", global["id"]);
defaults.setCharPref("distribution.version", global["version"]);
let locale = Services.prefs.getCharPref("general.useragent.locale");
let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
aboutString.data = global["about." + locale] || global["about"];
defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString);
let prefs = aData["Preferences"];
for (let key in prefs) {
try {
let value = prefs[key];
switch (typeof value) {
case "boolean":
defaults.setBoolPref(key, value);
break;
case "number":
defaults.setIntPref(key, value);
break;
case "string":
case "undefined":
defaults.setCharPref(key, value);
break;
}
} catch (e) { /* ignore bad prefs and move on */ }
}
// Apply a lightweight theme if necessary
if (prefs["lightweightThemes.isThemeSelected"])
Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
let localizeablePrefs = aData["LocalizablePreferences"];
for (let key in localizeablePrefs) {
try {
let value = localizeablePrefs[key];
value = value.replace("%LOCALE%", locale, "g");
localizedString.data = "data:text/plain," + key + "=" + value;
defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
} catch (e) { /* ignore bad prefs and move on */ }
}
let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale];
for (let key in localizeablePrefsOverrides) {
try {
let value = localizeablePrefsOverrides[key];
localizedString.data = "data:text/plain," + key + "=" + value;
defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
} catch (e) { /* ignore bad prefs and move on */ }
}
sendMessageToJava({ type: "Distribution:Set:OK" });
},
// aFile is an nsIFile
// aCallback takes the parsed JSON object as a parameter
readJSON: function dc_readJSON(aFile, aCallback) {
Task.spawn(function() {
let bytes = yield OS.File.read(aFile.path);
let raw = new TextDecoder().decode(bytes) || "";
try {
aCallback(JSON.parse(raw));
} catch (e) {
Cu.reportError("Distribution: Could not parse JSON: " + e);
}
}).then(null, function onError(reason) {
if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
}
});
}
};
var Tabs = {
_enableTabExpiration: false,
_domains: new Set(),
init: function() {
// On low-memory platforms, always allow tab expiration. On high-mem
// platforms, allow it to be turned on once we hit a low-mem situation.
if (BrowserApp.isOnLowMemoryPlatform) {
this._enableTabExpiration = true;
} else {
Services.obs.addObserver(this, "memory-pressure", false);
}
Services.obs.addObserver(this, "Session:Prefetch", false);
BrowserApp.deck.addEventListener("pageshow", this, false);
BrowserApp.deck.addEventListener("TabOpen", this, false);
},
uninit: function() {
if (!this._enableTabExpiration) {
// If _enableTabExpiration is true then we won't have this
// observer registered any more.
Services.obs.removeObserver(this, "memory-pressure");
}
Services.obs.removeObserver(this, "Session:Prefetch");
BrowserApp.deck.removeEventListener("pageshow", this);
BrowserApp.deck.removeEventListener("TabOpen", this);
},
observe: function(aSubject, aTopic, aData) {
switch (aTopic) {
case "memory-pressure":
if (aData != "heap-minimize") {
// We received a low-memory related notification. This will enable
// expirations.
this._enableTabExpiration = true;
Services.obs.removeObserver(this, "memory-pressure");
} else {
// Use "heap-minimize" as a trigger to expire the most stale tab.
this.expireLruTab();
}
break;
case "Session:Prefetch":
if (aData) {
let uri = Services.io.newURI(aData, null, null);
if (uri && !this._domains.has(uri.host)) {
try {
Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
this._domains.add(uri.host);
} catch (e) {}
}
}
break;
}
},
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "pageshow":
// Clear the domain cache whenever a page get loaded into any browser.
this._domains.clear();
break;
case "TabOpen":
// Use opening a new tab as a trigger to expire the most stale tab.
this.expireLruTab();
break;
}
},
// Manage the most-recently-used list of tabs. Each tab has a timestamp
// associated with it that indicates when it was last touched.
expireLruTab: function() {
if (!this._enableTabExpiration) {
return false;
}
let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
if (expireTimeMs < 0) {
// This behaviour is disabled.
return false;
}
let tabs = BrowserApp.tabs;
let selected = BrowserApp.selectedTab;
let lruTab = null;
// Find the least recently used non-zombie tab.
for (let i = 0; i < tabs.length; i++) {
if (tabs[i] == selected || tabs[i].browser.__SS_restore) {
// This tab is selected or already a zombie, skip it.
continue;
}
if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
lruTab = tabs[i];
}
}
// If the tab was last touched more than browser.tabs.expireTime seconds ago,
// zombify it.
if (lruTab) {
let tabAgeMs = Date.now() - lruTab.lastTouchedAt;
if (tabAgeMs > expireTimeMs) {
MemoryObserver.zombify(lruTab);
Telemetry.addData("FENNEC_TAB_EXPIRED", tabAgeMs / 1000);
return true;
}
}
return false;
},
// For debugging
dump: function(aPrefix) {
let tabs = BrowserApp.tabs;
for (let i = 0; i < tabs.length; i++) {
dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore);
}
},
};
function ContextMenuItem(args) {
this.id = uuidgen.generateUUID().toString();
this.args = args;
}
ContextMenuItem.prototype = {
get order() {
return this.args.order || 0;
},
matches: function(elt, x, y) {
return this.args.selector.matches(elt, x, y);
},
callback: function(elt) {
this.args.callback(elt);
},
addVal: function(name, elt, defaultValue) {
if (!(name in this.args))
return defaultValue;
if (typeof this.args[name] == "function")
return this.args[name](elt);
return this.args[name];
},
getValue: function(elt) {
return {
id: this.id,
label: this.addVal("label", elt),
showAsActions: this.addVal("showAsActions", elt),
icon: this.addVal("icon", elt),
isGroup: this.addVal("isGroup", elt, false),
inGroup: this.addVal("inGroup", elt, false),
disabled: this.addVal("disabled", elt, false),
selected: this.addVal("selected", elt, false),
isParent: this.addVal("isParent", elt, false),
};
}
}
function HTMLContextMenuItem(elt, target) {
ContextMenuItem.call(this, { });
this.menuElementRef = Cu.getWeakReference(elt);
this.targetElementRef = Cu.getWeakReference(target);
}
HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
order: {
value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER
},
matches: {
value: function(target) {
let t = this.targetElementRef.get();
return t === target;
},
},
callback: {
value: function(target) {
let elt = this.menuElementRef.get();
if (!elt) {
return;
}
// If this is a menu item, show a new context menu with the submenu in it
if (elt instanceof Ci.nsIDOMHTMLMenuElement) {
try {
NativeWindow.contextmenus.menus = {};
let elt = this.menuElementRef.get();
let target = this.targetElementRef.get();
if (!elt) {
return;
}
var items = NativeWindow.contextmenus._getHTMLContextMenuItemsForMenu(elt, target);
if (items.length > 0) {
NativeWindow.contextmenus._addMenuItems(items, "link");
}
} catch(ex) {
Cu.reportError(ex);
}
} else {
// otherwise just click the menu item
elt.click();
}
},
},
getValue: {
value: function(target) {
let elt = this.menuElementRef.get();
if (!elt) {
return null;
}
if (elt.hasAttribute("hidden")) {
return null;
}
return {
id: this.id,
icon: elt.icon,
label: elt.label,
disabled: elt.disabled,
menu: elt instanceof Ci.nsIDOMHTMLMenuElement
};
}
},
});