summaryrefslogtreecommitdiffstats
path: root/mobile/android/components
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /mobile/android/components
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/components')
-rw-r--r--mobile/android/components/AboutRedirector.js132
-rw-r--r--mobile/android/components/AddonUpdateService.js75
-rw-r--r--mobile/android/components/BlocklistPrompt.js61
-rw-r--r--mobile/android/components/BrowserCLH.js47
-rw-r--r--mobile/android/components/ColorPicker.js55
-rw-r--r--mobile/android/components/ContentDispatchChooser.js83
-rw-r--r--mobile/android/components/ContentPermissionPrompt.js146
-rw-r--r--mobile/android/components/DirectoryProvider.js214
-rw-r--r--mobile/android/components/FilePicker.js302
-rw-r--r--mobile/android/components/FxAccountsPush.js164
-rw-r--r--mobile/android/components/HelperAppDialog.js373
-rw-r--r--mobile/android/components/ImageBlockingPolicy.js125
-rw-r--r--mobile/android/components/LoginManagerPrompter.js413
-rw-r--r--mobile/android/components/MobileComponents.manifest126
-rw-r--r--mobile/android/components/NSSDialogService.js276
-rw-r--r--mobile/android/components/PersistentNotificationHandler.js78
-rw-r--r--mobile/android/components/PresentationDevicePrompt.js134
-rw-r--r--mobile/android/components/PresentationRequestUIGlue.js86
-rw-r--r--mobile/android/components/PromptService.js878
-rw-r--r--mobile/android/components/SessionStore.idl86
-rw-r--r--mobile/android/components/SessionStore.js1794
-rw-r--r--mobile/android/components/SiteSpecificUserAgent.js33
-rw-r--r--mobile/android/components/Snippets.js446
-rw-r--r--mobile/android/components/TabSource.js91
-rw-r--r--mobile/android/components/XPIDialogService.js49
-rw-r--r--mobile/android/components/build/moz.build31
-rw-r--r--mobile/android/components/build/nsAndroidHistory.cpp395
-rw-r--r--mobile/android/components/build/nsAndroidHistory.h97
-rw-r--r--mobile/android/components/build/nsBrowserComponents.h7
-rw-r--r--mobile/android/components/build/nsBrowserModule.cpp47
-rw-r--r--mobile/android/components/build/nsIShellService.idl26
-rw-r--r--mobile/android/components/build/nsShellService.cpp30
-rw-r--r--mobile/android/components/build/nsShellService.h29
-rw-r--r--mobile/android/components/extensions/.eslintrc.js5
-rw-r--r--mobile/android/components/extensions/ext-pageAction.js169
-rw-r--r--mobile/android/components/extensions/extensions-mobile.manifest5
-rw-r--r--mobile/android/components/extensions/jar.mn6
-rw-r--r--mobile/android/components/extensions/moz.build16
-rw-r--r--mobile/android/components/extensions/schemas/jar.mn6
-rw-r--r--mobile/android/components/extensions/schemas/moz.build7
-rw-r--r--mobile/android/components/extensions/schemas/page_action.json239
-rw-r--r--mobile/android/components/extensions/test/mochitest/.eslintrc.js10
-rw-r--r--mobile/android/components/extensions/test/mochitest/chrome.ini7
-rw-r--r--mobile/android/components/extensions/test/mochitest/head.js15
-rw-r--r--mobile/android/components/extensions/test/mochitest/mochitest.ini6
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html23
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html99
-rw-r--r--mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html169
-rw-r--r--mobile/android/components/moz.build48
49 files changed, 7759 insertions, 0 deletions
diff --git a/mobile/android/components/AboutRedirector.js b/mobile/android/components/AboutRedirector.js
new file mode 100644
index 000000000..df50864dd
--- /dev/null
+++ b/mobile/android/components/AboutRedirector.js
@@ -0,0 +1,132 @@
+/* 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/. */
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var modules = {
+ // about:
+ "": {
+ uri: "chrome://browser/content/about.xhtml",
+ privileged: true
+ },
+
+ // about:fennec and about:firefox are aliases for about:,
+ // but hidden from about:about
+ fennec: {
+ uri: "chrome://browser/content/about.xhtml",
+ privileged: true,
+ hide: true
+ },
+ get firefox() {
+ return this.fennec
+ },
+
+ // about:blank has some bad loading behavior we can avoid, if we use an alias
+ empty: {
+ uri: "about:blank",
+ privileged: false,
+ hide: true
+ },
+
+ rights: {
+ uri: "chrome://browser/content/aboutRights.xhtml",
+ privileged: false
+ },
+ blocked: {
+ uri: "chrome://browser/content/blockedSite.xhtml",
+ privileged: false,
+ hide: true
+ },
+ certerror: {
+ uri: "chrome://browser/content/aboutCertError.xhtml",
+ privileged: false,
+ hide: true
+ },
+ home: {
+ uri: "chrome://browser/content/aboutHome.xhtml",
+ privileged: false
+ },
+ downloads: {
+ uri: "chrome://browser/content/aboutDownloads.xhtml",
+ privileged: true
+ },
+ reader: {
+ uri: "chrome://global/content/reader/aboutReader.html",
+ privileged: false,
+ hide: true
+ },
+ feedback: {
+ uri: "chrome://browser/content/aboutFeedback.xhtml",
+ privileged: true
+ },
+ privatebrowsing: {
+ uri: "chrome://browser/content/aboutPrivateBrowsing.xhtml",
+ privileged: true
+ },
+ logins: {
+ uri: "chrome://browser/content/aboutLogins.xhtml",
+ privileged: true
+ },
+ accounts: {
+ uri: "chrome://browser/content/aboutAccounts.xhtml",
+ privileged: true
+ },
+};
+
+if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
+ modules['healthreport'] = {
+ uri: "chrome://browser/content/aboutHealthReport.xhtml",
+ privileged: true
+ };
+}
+
+function AboutRedirector() {}
+AboutRedirector.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+ classID: Components.ID("{322ba47e-7047-4f71-aebf-cb7d69325cd9}"),
+
+ _getModuleInfo: function (aURI) {
+ let moduleName = aURI.path.replace(/[?#].*/, "").toLowerCase();
+ return modules[moduleName];
+ },
+
+ // nsIAboutModule
+ getURIFlags: function(aURI) {
+ let flags;
+ let moduleInfo = this._getModuleInfo(aURI);
+ if (moduleInfo.hide)
+ flags = Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
+
+ return flags | Ci.nsIAboutModule.ALLOW_SCRIPT;
+ },
+
+ newChannel: function(aURI, aLoadInfo) {
+ let moduleInfo = this._getModuleInfo(aURI);
+
+ var ios = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+
+ var newURI = ios.newURI(moduleInfo.uri, null, null);
+
+ var channel = ios.newChannelFromURIWithLoadInfo(newURI, aLoadInfo);
+
+ if (!moduleInfo.privileged) {
+ // Setting the owner to null means that we'll go through the normal
+ // path in GetChannelPrincipal and create a codebase principal based
+ // on the channel's originalURI
+ channel.owner = null;
+ }
+
+ channel.originalURI = aURI;
+
+ return channel;
+ }
+};
+
+const components = [AboutRedirector];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/mobile/android/components/AddonUpdateService.js b/mobile/android/components/AddonUpdateService.js
new file mode 100644
index 000000000..2505e2796
--- /dev/null
+++ b/mobile/android/components/AddonUpdateService.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate",
+ "resource://gre/modules/AddonManager.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "GMPInstallManager",
+ "resource://gre/modules/GMPInstallManager.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+function getPref(func, preference, defaultValue) {
+ try {
+ return Services.prefs[func](preference);
+ }
+ catch (e) {}
+ return defaultValue;
+}
+
+// -----------------------------------------------------------------------
+// Add-on auto-update management service
+// -----------------------------------------------------------------------
+
+const PREF_ADDON_UPDATE_ENABLED = "extensions.autoupdate.enabled";
+const PREF_ADDON_UPDATE_INTERVAL = "extensions.autoupdate.interval";
+
+var gNeedsRestart = false;
+
+function AddonUpdateService() {}
+
+AddonUpdateService.prototype = {
+ classDescription: "Add-on auto-update management",
+ classID: Components.ID("{93c8824c-9b87-45ae-bc90-5b82a1e4d877}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback]),
+
+ notify: function aus_notify(aTimer) {
+ if (aTimer && !getPref("getBoolPref", PREF_ADDON_UPDATE_ENABLED, true))
+ return;
+
+ // If we already auto-upgraded and installed new versions, ignore this check
+ if (gNeedsRestart)
+ return;
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+
+ let gmp = new GMPInstallManager();
+ gmp.simpleCheckAndInstall().then(null, () => {});
+
+ let interval = 1000 * getPref("getIntPref", PREF_ADDON_UPDATE_INTERVAL, 86400);
+ Messaging.sendRequest({
+ type: "Gecko:ScheduleRun",
+ action: "update-addons",
+ trigger: interval,
+ interval: interval,
+ });
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonUpdateService]);
+
diff --git a/mobile/android/components/BlocklistPrompt.js b/mobile/android/components/BlocklistPrompt.js
new file mode 100644
index 000000000..ce7b8e011
--- /dev/null
+++ b/mobile/android/components/BlocklistPrompt.js
@@ -0,0 +1,61 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// -----------------------------------------------------------------------
+// BlocklistPrompt Service
+// -----------------------------------------------------------------------
+
+
+function BlocklistPrompt() { }
+
+BlocklistPrompt.prototype = {
+ prompt: function(aAddons, aCount) {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (win.ExtensionsView.visible) {
+ win.ExtensionsView.showRestart("blocked");
+ } else {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let notifyBox = win.getNotificationBox();
+ let restartCallback = function(aNotification, aDescription) {
+ // Notify all windows that an application quit has been requested
+ var 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 buttons = [{accessKey: null,
+ label: bundle.GetStringFromName("notificationRestart.button"),
+ callback: restartCallback}];
+ notifyBox.appendNotification(bundle.GetStringFromName("notificationRestart.blocked"),
+ "blocked-add-on",
+ "",
+ "PRIORITY_CRITICAL_HIGH",
+ buttons);
+ }
+ // Disable softblocked items automatically
+ for (let i = 0; i < aAddons.length; i++) {
+ if (aAddons[i].item instanceof Ci.nsIPluginTag)
+ aAddons[i].item.disabled = true;
+ else
+ aAddons[i].item.userDisabled = true;
+ }
+ },
+ classID: Components.ID("{4e6ea350-b09a-11df-94e2-0800200c9a66}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIBlocklistPrompt])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([BlocklistPrompt]);
+
diff --git a/mobile/android/components/BrowserCLH.js b/mobile/android/components/BrowserCLH.js
new file mode 100644
index 000000000..4cbf03554
--- /dev/null
+++ b/mobile/android/components/BrowserCLH.js
@@ -0,0 +1,47 @@
+/* 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/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function BrowserCLH() {}
+
+BrowserCLH.prototype = {
+ /**
+ * Register resource://android as the APK root.
+ *
+ * Consumers can access Android assets using resource://android/assets/FILENAME.
+ */
+ setResourceSubstitutions: function () {
+ let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci["nsIChromeRegistry"]);
+ // Like jar:jar:file:///data/app/org.mozilla.fennec-2.apk!/assets/omni.ja!/chrome/chrome/content/aboutHome.xhtml
+ let url = registry.convertChromeURL(Services.io.newURI("chrome://browser/content/aboutHome.xhtml", null, null)).spec;
+ // Like jar:file:///data/app/org.mozilla.fennec-2.apk!/
+ url = url.substring(4, url.indexOf("!/") + 2);
+
+ let protocolHandler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ protocolHandler.setSubstitution("android", Services.io.newURI(url, null, null));
+ },
+
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "app-startup":
+ this.setResourceSubstitutions();
+ break;
+ }
+ },
+
+ // QI
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ // XPCOMUtils factory
+ classID: Components.ID("{be623d20-d305-11de-8a39-0800200c9a66}")
+};
+
+var components = [ BrowserCLH ];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/mobile/android/components/ColorPicker.js b/mobile/android/components/ColorPicker.js
new file mode 100644
index 000000000..7d478da80
--- /dev/null
+++ b/mobile/android/components/ColorPicker.js
@@ -0,0 +1,55 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+function ColorPicker() {
+}
+
+ColorPicker.prototype = {
+ _initial: 0,
+ _domWin: null,
+ _title: "",
+
+ get strings() {
+ if (!this._strings) {
+ this._strings = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ }
+ return this._strings;
+ },
+
+ init: function(aParent, aTitle, aInitial) {
+ this._domWin = aParent;
+ this._initial = aInitial;
+ this._title = aTitle;
+ },
+
+ open: function(aCallback) {
+ let p = new Prompt({ title: this._title,
+ buttons: [
+ this.strings.GetStringFromName("inputWidgetHelper.set"),
+ this.strings.GetStringFromName("inputWidgetHelper.cancel")
+ ] })
+ .addColorPicker({ value: this._initial })
+ .show((data) => {
+ if (data.button == 0)
+ aCallback.done(data.color0);
+ else
+ aCallback.done(this._initial);
+ });
+ },
+
+ classID: Components.ID("{430b987f-bb9f-46a3-99a5-241749220b29}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIColorPicker])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ColorPicker]);
diff --git a/mobile/android/components/ContentDispatchChooser.js b/mobile/android/components/ContentDispatchChooser.js
new file mode 100644
index 000000000..b28e356e0
--- /dev/null
+++ b/mobile/android/components/ContentDispatchChooser.js
@@ -0,0 +1,83 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+function ContentDispatchChooser() {}
+
+ContentDispatchChooser.prototype =
+{
+ classID: Components.ID("5a072a22-1e66-4100-afc1-07aed8b62fc5"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentDispatchChooser]),
+
+ get protoSvc() {
+ if (!this._protoSvc) {
+ this._protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService);
+ }
+ return this._protoSvc;
+ },
+
+ _getChromeWin: function getChromeWin() {
+ try {
+ return Services.wm.getMostRecentWindow("navigator:browser");
+ } catch (e) {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+ },
+
+ ask: function ask(aHandler, aWindowContext, aURI, aReason) {
+ let window = null;
+ try {
+ if (aWindowContext)
+ window = aWindowContext.getInterface(Ci.nsIDOMWindow);
+ } catch (e) { /* it's OK to not have a window */ }
+
+ // The current list is based purely on the scheme. Redo the query using the url to get more
+ // specific results.
+ aHandler = this.protoSvc.getProtocolHandlerInfoFromOS(aURI.spec, {});
+
+ // The first handler in the set is the Android Application Chooser (which will fall back to a default if one is set)
+ // If we have more than one option, let the OS handle showing a list (if needed).
+ if (aHandler.possibleApplicationHandlers.length > 1) {
+ aHandler.launchWithURI(aURI, aWindowContext);
+ } else {
+ // xpcshell tests do not have an Android Bridge but we require Android
+ // Bridge when using Messaging so we guard against this case. xpcshell
+ // tests also do not have a window, so we use this state to guard.
+ let win = this._getChromeWin();
+ if (!win) {
+ return;
+ }
+
+ let msg = {
+ type: "Intent:OpenNoHandler",
+ uri: aURI.spec,
+ };
+
+ Messaging.sendRequestForResult(msg).then(() => {
+ // Java opens an app on success: take no action.
+ }, (uri) => {
+ // We couldn't open this. If this was from a click, it's likely that we just
+ // want this to fail silently. If the user entered this on the address bar, though,
+ // we want to show the neterror page.
+
+ let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ let millis = dwu.millisSinceLastUserInput;
+ if (millis > 0 && millis >= 1000) {
+ window.location.href = uri;
+ }
+ });
+ }
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentDispatchChooser]);
diff --git a/mobile/android/components/ContentPermissionPrompt.js b/mobile/android/components/ContentPermissionPrompt.js
new file mode 100644
index 000000000..fd13ce26b
--- /dev/null
+++ b/mobile/android/components/ContentPermissionPrompt.js
@@ -0,0 +1,146 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const kEntities = {
+ "contacts": "contacts",
+ "desktop-notification": "desktopNotification2",
+ "geolocation": "geolocation",
+ "flyweb-publish-server": "flyWebPublishServer",
+};
+
+// For these types, prompt for permission if action is unknown.
+const PROMPT_FOR_UNKNOWN = [
+ "desktop-notification",
+ "geolocation",
+ "flyweb-publish-server",
+];
+
+function ContentPermissionPrompt() {}
+
+ContentPermissionPrompt.prototype = {
+ classID: Components.ID("{C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
+
+ handleExistingPermission: function handleExistingPermission(request, type, denyUnknown) {
+ let result = Services.perms.testExactPermissionFromPrincipal(request.principal, type);
+ if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
+ request.allow();
+ return true;
+ }
+
+ if (result == Ci.nsIPermissionManager.DENY_ACTION) {
+ request.cancel();
+ return true;
+ }
+
+ if (denyUnknown && result == Ci.nsIPermissionManager.UNKNOWN_ACTION) {
+ request.cancel();
+ return true;
+ }
+
+ return false;
+ },
+
+ getChromeWindow: function getChromeWindow(aWindow) {
+ let chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+ return chromeWin;
+ },
+
+ getChromeForRequest: function getChromeForRequest(request) {
+ if (request.window) {
+ let requestingWindow = request.window.top;
+ return this.getChromeWindow(requestingWindow).wrappedJSObject;
+ }
+ return request.element.ownerDocument.defaultView;
+ },
+
+ prompt: function(request) {
+ let isApp = request.principal.appId !== Ci.nsIScriptSecurityManager.NO_APP_ID && request.principal.appId !== Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID;
+
+ // Only allow exactly one permission rquest here.
+ let types = request.types.QueryInterface(Ci.nsIArray);
+ if (types.length != 1) {
+ request.cancel();
+ return;
+ }
+ let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
+
+ // Returns true if the request was handled
+ let access = (perm.access && perm.access !== "unused") ?
+ (perm.type + "-" + perm.access) : perm.type;
+ if (this.handleExistingPermission(request, access,
+ /* denyUnknown */ isApp || PROMPT_FOR_UNKNOWN.indexOf(perm.type) < 0))
+ return;
+
+ let chromeWin = this.getChromeForRequest(request);
+ let tab = chromeWin.BrowserApp.getTabForWindow(request.window.top);
+ if (!tab)
+ return;
+
+ let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let entityName = kEntities[perm.type];
+
+ let buttons = [{
+ label: browserBundle.GetStringFromName(entityName + ".dontAllow"),
+ callback: function(aChecked) {
+ // If the user checked "Don't ask again" or this is a desktopNotification, make a permanent exception
+ if (aChecked || entityName == "desktopNotification2")
+ Services.perms.addFromPrincipal(request.principal, access, Ci.nsIPermissionManager.DENY_ACTION);
+
+ request.cancel();
+ }
+ },
+ {
+ label: browserBundle.GetStringFromName(entityName + ".allow"),
+ callback: function(aChecked) {
+ // If the user checked "Don't ask again" or this is a desktopNotification, make a permanent exception
+ if (aChecked || entityName == "desktopNotification2") {
+ Services.perms.addFromPrincipal(request.principal, access, Ci.nsIPermissionManager.ALLOW_ACTION);
+ } else if (isApp) {
+ // Otherwise allow the permission for the current session if the request comes from an app
+ Services.perms.addFromPrincipal(request.principal, access, Ci.nsIPermissionManager.ALLOW_ACTION, Ci.nsIPermissionManager.EXPIRE_SESSION);
+ }
+
+ request.allow();
+ },
+ positive: true
+ }];
+
+ let requestor = chromeWin.BrowserApp.manifest ? "'" + chromeWin.BrowserApp.manifest.name + "'" : request.principal.URI.host;
+ let message = browserBundle.formatStringFromName(entityName + ".ask", [requestor], 1);
+ // desktopNotification doesn't have a checkbox
+ let options;
+ if (entityName == "desktopNotification2") {
+ options = {
+ link: {
+ label: browserBundle.GetStringFromName("doorhanger.learnMore"),
+ url: "https://www.mozilla.org/firefox/push/"
+ }
+ };
+ } else {
+ options = { checkbox: browserBundle.GetStringFromName(entityName + ".dontAskAgain") };
+ }
+
+ chromeWin.NativeWindow.doorhanger.show(message, entityName + request.principal.URI.host, buttons, tab.id, options, entityName.toUpperCase());
+ }
+};
+
+
+//module initialization
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);
diff --git a/mobile/android/components/DirectoryProvider.js b/mobile/android/components/DirectoryProvider.js
new file mode 100644
index 000000000..5d0f7974c
--- /dev/null
+++ b/mobile/android/components/DirectoryProvider.js
@@ -0,0 +1,214 @@
+/* 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/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JNI", "resource://gre/modules/JNI.jsm");
+
+// -----------------------------------------------------------------------
+// Directory Provider for special browser folders and files
+// -----------------------------------------------------------------------
+
+const NS_APP_CACHE_PARENT_DIR = "cachePDir";
+const NS_APP_SEARCH_DIR = "SrchPlugns";
+const NS_APP_SEARCH_DIR_LIST = "SrchPluginsDL";
+const NS_APP_DISTRIBUTION_SEARCH_DIR_LIST = "SrchPluginsDistDL";
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
+const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
+const XRE_UPDATE_ROOT_DIR = "UpdRootD";
+const ENVVAR_UPDATE_DIR = "UPDATES_DIRECTORY";
+const WEBAPPS_DIR = "webappsDir";
+
+const SYSTEM_DIST_PATH = `/system/${AppConstants.ANDROID_PACKAGE_NAME}/distribution`;
+
+function DirectoryProvider() {}
+
+DirectoryProvider.prototype = {
+ classID: Components.ID("{ef0f7a87-c1ee-45a8-8d67-26f586e46a4b}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider,
+ Ci.nsIDirectoryServiceProvider2]),
+
+ getFile: function(prop, persistent) {
+ if (prop == NS_APP_CACHE_PARENT_DIR) {
+ let dirsvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+ let profile = dirsvc.get("ProfD", Ci.nsIFile);
+ return profile;
+ } else if (prop == WEBAPPS_DIR) {
+ // returns the folder that should hold the webapps database file
+ // For fennec we will store that in the root profile folder so that all
+ // webapps can easily access it
+ let dirsvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+ let profile = dirsvc.get("ProfD", Ci.nsIFile);
+ return profile.parent;
+ } else if (prop == XRE_APP_DISTRIBUTION_DIR) {
+ let distributionDirectories = this._getDistributionDirectories();
+ for (let i = 0; i < distributionDirectories.length; i++) {
+ if (distributionDirectories[i].exists()) {
+ return distributionDirectories[i];
+ }
+ }
+ // Fallback: Return default data distribution directory
+ return FileUtils.getDir(NS_XPCOM_CURRENT_PROCESS_DIR, ["distribution"], false);
+ } else if (prop == XRE_UPDATE_ROOT_DIR) {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ if (env.exists(ENVVAR_UPDATE_DIR)) {
+ let path = env.get(ENVVAR_UPDATE_DIR);
+ if (path) {
+ return new FileUtils.File(path);
+ }
+ }
+ return new FileUtils.File(env.get("DOWNLOADS_DIRECTORY"));
+ }
+
+ // We are retuning null to show failure instead for throwing an error. The
+ // interface is called quite a bit and throwing an error is noisy. Returning
+ // null works with the way the interface is called [see bug 529077]
+ return null;
+ },
+
+ /**
+ * Appends the distribution-specific search engine directories to the array.
+ * The distribution directory structure is as follows:
+ *
+ * \- distribution/
+ * \- searchplugins/
+ * |- common/
+ * \- locale/
+ * |- <locale 1>/
+ * ...
+ * \- <locale N>/
+ *
+ * Common engines are loaded for all locales. If there is no locale directory for
+ * the current locale, there is a pref: "distribution.searchplugins.defaultLocale",
+ * which specifies a default locale to use.
+ */
+ _appendDistroSearchDirs: function(array) {
+ let distro = this.getFile(XRE_APP_DISTRIBUTION_DIR);
+ if (!distro.exists())
+ return;
+
+ let searchPlugins = distro.clone();
+ searchPlugins.append("searchplugins");
+ if (!searchPlugins.exists())
+ return;
+
+ let commonPlugins = searchPlugins.clone();
+ commonPlugins.append("common");
+ if (commonPlugins.exists())
+ array.push(commonPlugins);
+
+ let localePlugins = searchPlugins.clone();
+ localePlugins.append("locale");
+ if (!localePlugins.exists())
+ return;
+
+ let curLocale = "";
+ try {
+ curLocale = Services.prefs.getComplexValue("general.useragent.locale", Ci.nsIPrefLocalizedString).data;
+ } catch (e) {
+ try {
+ curLocale = Services.prefs.getCharPref("general.useragent.locale");
+ } catch (ee) {
+ }
+ }
+
+ if (curLocale) {
+ let curLocalePlugins = localePlugins.clone();
+ curLocalePlugins.append(curLocale);
+ if (curLocalePlugins.exists()) {
+ array.push(curLocalePlugins);
+ return;
+ }
+ }
+
+ // We didn't append the locale dir - try the default one.
+ try {
+ let defLocale = Services.prefs.getCharPref("distribution.searchplugins.defaultLocale");
+ let defLocalePlugins = localePlugins.clone();
+ defLocalePlugins.append(defLocale);
+ if (defLocalePlugins.exists())
+ array.push(defLocalePlugins);
+ } catch(e) {
+ }
+ },
+
+ getFiles: function(prop) {
+ if (prop != NS_APP_SEARCH_DIR_LIST &&
+ prop != NS_APP_DISTRIBUTION_SEARCH_DIR_LIST)
+ return null;
+
+ let result = [];
+
+ if (prop == NS_APP_DISTRIBUTION_SEARCH_DIR_LIST) {
+ this._appendDistroSearchDirs(result);
+ }
+ else {
+ /**
+ * We want to preserve the following order, since the search service
+ * loads engines in first-loaded-wins order.
+ * - distro search plugin locations (loaded separately by the search
+ * service)
+ * - user search plugin locations (profile)
+ * - app search plugin location (shipped engines)
+ */
+ let appUserSearchDir = FileUtils.getDir(NS_APP_USER_SEARCH_DIR, [], false);
+ if (appUserSearchDir.exists())
+ result.push(appUserSearchDir);
+
+ let appSearchDir = FileUtils.getDir(NS_APP_SEARCH_DIR, [], false);
+ if (appSearchDir.exists())
+ result.push(appSearchDir);
+ }
+
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),
+ hasMoreElements: function() {
+ return result.length > 0;
+ },
+ getNext: function() {
+ return result.shift();
+ }
+ };
+ },
+
+ _getDistributionDirectories: function() {
+ let directories = [];
+ let jenv = null;
+
+ try {
+ jenv = JNI.GetForThread();
+
+ let jDistribution = JNI.LoadClass(jenv, "org.mozilla.gecko.distribution.Distribution", {
+ static_methods: [
+ { name: "getDistributionDirectories", sig: "()[Ljava/lang/String;" }
+ ],
+ });
+
+ let jDirectories = jDistribution.getDistributionDirectories();
+
+ for (let i = 0; i < jDirectories.length; i++) {
+ directories.push(new FileUtils.File(
+ JNI.ReadString(jenv, jDirectories.get(i))
+ ));
+ }
+ } finally {
+ if (jenv) {
+ JNI.UnloadClasses(jenv);
+ }
+ }
+
+ return directories;
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DirectoryProvider]);
diff --git a/mobile/android/components/FilePicker.js b/mobile/android/components/FilePicker.js
new file mode 100644
index 000000000..2de81ca46
--- /dev/null
+++ b/mobile/android/components/FilePicker.js
@@ -0,0 +1,302 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+Cu.importGlobalProperties(['File']);
+
+function FilePicker() {
+}
+
+FilePicker.prototype = {
+ _mimeTypeFilter: 0,
+ _extensionsFilter: "",
+ _defaultString: "",
+ _domWin: null,
+ _defaultExtension: null,
+ _displayDirectory: null,
+ _filePath: null,
+ _promptActive: false,
+ _filterIndex: 0,
+ _addToRecentDocs: false,
+ _title: "",
+
+ init: function(aParent, aTitle, aMode) {
+ this._domWin = aParent;
+ this._mode = aMode;
+ this._title = aTitle;
+ Services.obs.addObserver(this, "FilePicker:Result", false);
+
+ let idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ this.guid = idService.generateUUID().toString();
+
+ if (aMode != Ci.nsIFilePicker.modeOpen && aMode != Ci.nsIFilePicker.modeOpenMultiple)
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ appendFilters: function(aFilterMask) {
+ if (aFilterMask & Ci.nsIFilePicker.filterAudio) {
+ this._mimeTypeFilter = "audio/*";
+ return;
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.filterImages) {
+ this._mimeTypeFilter = "image/*";
+ return;
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.filterVideo) {
+ this._mimeTypeFilter = "video/*";
+ return;
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.filterAll) {
+ this._mimeTypeFilter = "*/*";
+ return;
+ }
+
+ /* From BaseFilePicker.cpp */
+ if (aFilterMask & Ci.nsIFilePicker.filterHTML) {
+ this.appendFilter("*.html; *.htm; *.shtml; *.xhtml");
+ }
+ if (aFilterMask & Ci.nsIFilePicker.filterText) {
+ this.appendFilter("*.txt; *.text");
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.filterXML) {
+ this.appendFilter("*.xml");
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.xulFilter) {
+ this.appendFilter("*.xul");
+ }
+
+ if (aFilterMask & Ci.nsIFilePicker.xulFilter) {
+ this.appendFilter("..apps");
+ }
+ },
+
+ appendFilter: function(title, filter) {
+ if (this._extensionsFilter)
+ this._extensionsFilter += ", ";
+ this._extensionsFilter += filter;
+ },
+
+ get defaultString() {
+ return this._defaultString;
+ },
+
+ set defaultString(defaultString) {
+ this._defaultString = defaultString;
+ },
+
+ get defaultExtension() {
+ return this._defaultExtension;
+ },
+
+ set defaultExtension(defaultExtension) {
+ this._defaultExtension = defaultExtension;
+ },
+
+ get filterIndex() {
+ return this._filterIndex;
+ },
+
+ set filterIndex(val) {
+ this._filterIndex = val;
+ },
+
+ get displayDirectory() {
+ return this._displayDirectory;
+ },
+
+ set displayDirectory(dir) {
+ this._displayDirectory = dir;
+ },
+
+ get file() {
+ if (!this._filePath) {
+ return null;
+ }
+
+ return new FileUtils.File(this._filePath);
+ },
+
+ get fileURL() {
+ let file = this.getFile();
+ return Services.io.newFileURI(file);
+ },
+
+ get files() {
+ return this.getEnumerator([this.file], function(file) {
+ return file;
+ });
+ },
+
+ // We don't support directory selection yet.
+ get domFileOrDirectory() {
+ let f = this.file;
+ if (!f) {
+ return null;
+ }
+
+ let win = this._domWin;
+ if (win) {
+ let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ return utils.wrapDOMFile(f);
+ }
+
+ return File.createFromNsIFile(f);
+ },
+
+ get domFileOrDirectoryEnumerator() {
+ let win = this._domWin;
+ return this.getEnumerator([this.file], function(file) {
+ if (win) {
+ let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ return utils.wrapDOMFile(file);
+ }
+
+ return File.createFromNsIFile(file);
+ });
+ },
+
+ get addToRecentDocs() {
+ return this._addToRecentDocs;
+ },
+
+ set addToRecentDocs(val) {
+ this._addToRecentDocs = val;
+ },
+
+ get mode() {
+ return this._mode;
+ },
+
+ show: function() {
+ if (this._domWin) {
+ this.fireDialogEvent(this._domWin, "DOMWillOpenModalDialog");
+ let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.enterModalState();
+ }
+
+ this._promptActive = true;
+ this._sendMessage();
+
+ let thread = Services.tm.currentThread;
+ while (this._promptActive)
+ thread.processNextEvent(true);
+ delete this._promptActive;
+
+ if (this._domWin) {
+ let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.leaveModalState();
+ this.fireDialogEvent(this._domWin, "DOMModalDialogClosed");
+ }
+
+ if (this._filePath)
+ return Ci.nsIFilePicker.returnOK;
+
+ return Ci.nsIFilePicker.returnCancel;
+ },
+
+ open: function(callback) {
+ this._callback = callback;
+ this._sendMessage();
+ },
+
+ _sendMessage: function() {
+ let msg = {
+ type: "FilePicker:Show",
+ guid: this.guid,
+ title: this._title,
+ };
+
+ // Knowing the window lets us destroy any temp files when the tab is closed
+ // Other consumers of the file picker may have to either wait for Android
+ // to clean up the temp dir (not guaranteed) or clean up after themselves.
+ let win = Services.wm.getMostRecentWindow('navigator:browser');
+ let tab = win.BrowserApp.getTabForWindow(this._domWin.top)
+ if (tab) {
+ msg.tabId = tab.id;
+ }
+
+ if (!this._extensionsFilter && !this._mimeTypeFilter) {
+ // If neither filters is set show anything we can.
+ msg.mode = "mimeType";
+ msg.mimeType = "*/*";
+ } else if (this._extensionsFilter) {
+ msg.mode = "extension";
+ msg.extensions = this._extensionsFilter;
+ } else {
+ msg.mode = "mimeType";
+ msg.mimeType = this._mimeTypeFilter;
+ }
+
+ this.sendMessageToJava(msg);
+ },
+
+ sendMessageToJava: function(aMsg) {
+ Services.androidBridge.handleGeckoMessage(aMsg);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let data = JSON.parse(aData);
+ if (data.guid != this.guid)
+ return;
+
+ this._filePath = null;
+ if (data.file)
+ this._filePath = data.file;
+
+ this._promptActive = false;
+
+ if (this._callback) {
+ this._callback.done(this._filePath ? Ci.nsIFilePicker.returnOK : Ci.nsIFilePicker.returnCancel);
+ }
+ delete this._callback;
+ },
+
+ getEnumerator: function(files, mapFunction) {
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),
+ mFiles: files,
+ mIndex: 0,
+ hasMoreElements: function() {
+ return (this.mIndex < this.mFiles.length);
+ },
+ getNext: function() {
+ if (this.mIndex >= this.mFiles.length) {
+ throw Components.results.NS_ERROR_FAILURE;
+ }
+ return mapFunction(this.mFiles[this.mIndex++]);
+ }
+ };
+ },
+
+ fireDialogEvent: function(aDomWin, aEventName) {
+ // accessing the document object can throw if this window no longer exists. See bug 789888.
+ try {
+ if (!aDomWin.document)
+ return;
+ let event = aDomWin.document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ let winUtils = aDomWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.dispatchEventToChromeOnly(aDomWin, event);
+ } catch(ex) {
+ }
+ },
+
+ classID: Components.ID("{18a4e042-7c7c-424b-a583-354e68553a7f}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFilePicker, Ci.nsIObserver])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FilePicker]);
diff --git a/mobile/android/components/FxAccountsPush.js b/mobile/android/components/FxAccountsPush.js
new file mode 100644
index 000000000..e6054a2de
--- /dev/null
+++ b/mobile/android/components/FxAccountsPush.js
@@ -0,0 +1,164 @@
+/* jshint moz: true, esnext: true */
+/* 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/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+const {
+ PushCrypto,
+ getCryptoParams,
+} = Cu.import("resource://gre/modules/PushCrypto.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "PushService",
+ "@mozilla.org/push/Service;1", "nsIPushService");
+XPCOMUtils.defineLazyGetter(this, "_decoder", () => new TextDecoder());
+
+const FXA_PUSH_SCOPE = "chrome://fxa-push";
+const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccountsPush");
+
+function FxAccountsPush() {
+ Services.obs.addObserver(this, "FxAccountsPush:ReceivedPushMessageToDecode", false);
+
+ Messaging.sendRequestForResult({
+ type: "FxAccountsPush:Initialized"
+ });
+}
+
+FxAccountsPush.prototype = {
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "android-push-service":
+ if (data === "android-fxa-subscribe") {
+ this._subscribe();
+ } else if (data === "android-fxa-unsubscribe") {
+ this._unsubscribe();
+ }
+ break;
+ case "FxAccountsPush:ReceivedPushMessageToDecode":
+ this._decodePushMessage(data);
+ break;
+ }
+ },
+
+ _subscribe() {
+ Log.i("FxAccountsPush _subscribe");
+ return new Promise((resolve, reject) => {
+ PushService.subscribe(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (Components.isSuccessCode(result)) {
+ Log.d("FxAccountsPush got subscription");
+ resolve(subscription);
+ } else {
+ Log.w("FxAccountsPush failed to subscribe", result);
+ reject(new Error("FxAccountsPush failed to subscribe"));
+ }
+ });
+ })
+ .then(subscription => {
+ Messaging.sendRequest({
+ type: "FxAccountsPush:Subscribe:Response",
+ subscription: {
+ pushCallback: subscription.endpoint,
+ pushPublicKey: urlsafeBase64Encode(subscription.getKey('p256dh')),
+ pushAuthKey: urlsafeBase64Encode(subscription.getKey('auth'))
+ }
+ });
+ })
+ .catch(err => {
+ Log.i("Error when registering FxA push endpoint " + err);
+ });
+ },
+
+ _unsubscribe() {
+ Log.i("FxAccountsPush _unsubscribe");
+ return new Promise((resolve) => {
+ PushService.unsubscribe(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, ok) => {
+ if (Components.isSuccessCode(result)) {
+ if (ok === true) {
+ Log.d("FxAccountsPush unsubscribed");
+ } else {
+ Log.d("FxAccountsPush had no subscription to unsubscribe");
+ }
+ } else {
+ Log.w("FxAccountsPush failed to unsubscribe", result);
+ }
+ return resolve(ok);
+ });
+ }).catch(err => {
+ Log.e("Error during unsubscribe", err);
+ });
+ },
+
+ _decodePushMessage(data) {
+ Log.i("FxAccountsPush _decodePushMessage");
+ data = JSON.parse(data);
+ let { headers, message } = this._messageAndHeaders(data);
+ return new Promise((resolve, reject) => {
+ PushService.getSubscription(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (!subscription) {
+ return reject(new Error("No subscription found"));
+ }
+ return resolve(subscription);
+ });
+ }).then(subscription => {
+ return PushCrypto.decrypt(subscription.p256dhPrivateKey,
+ new Uint8Array(subscription.getKey("p256dh")),
+ new Uint8Array(subscription.getKey("auth")),
+ headers, message);
+ })
+ .then(plaintext => {
+ let decryptedMessage = plaintext ? _decoder.decode(plaintext) : "";
+ Messaging.sendRequestForResult({
+ type: "FxAccountsPush:ReceivedPushMessageToDecode:Response",
+ message: decryptedMessage
+ });
+ })
+ .catch(err => {
+ Log.d("Error while decoding incoming message : " + err);
+ });
+ },
+
+ // Copied from PushServiceAndroidGCM
+ _messageAndHeaders(data) {
+ // Default is no data (and no encryption).
+ let message = null;
+ let headers = null;
+
+ if (data.message && data.enc && (data.enckey || data.cryptokey)) {
+ headers = {
+ encryption_key: data.enckey,
+ crypto_key: data.cryptokey,
+ encryption: data.enc,
+ encoding: data.con,
+ };
+ // Ciphertext is (urlsafe) Base 64 encoded.
+ message = ChromeUtils.base64URLDecode(data.message, {
+ // The Push server may append padding.
+ padding: "ignore",
+ });
+ }
+ return { headers, message };
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ classID: Components.ID("{d1bbb0fd-1d47-4134-9c12-d7b1be20b721}")
+};
+
+function urlsafeBase64Encode(key) {
+ return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
+}
+
+var components = [ FxAccountsPush ];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/mobile/android/components/HelperAppDialog.js b/mobile/android/components/HelperAppDialog.js
new file mode 100644
index 000000000..f127fb0b3
--- /dev/null
+++ b/mobile/android/components/HelperAppDialog.js
@@ -0,0 +1,373 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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/. */
+
+/*globals ContentAreaUtils */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const APK_MIME_TYPE = "application/vnd.android.package-archive";
+
+const OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE = "application/vnd.oma.dd+xml";
+const OMA_DRM_MESSAGE_MIME = "application/vnd.oma.drm.message";
+const OMA_DRM_CONTENT_MIME = "application/vnd.oma.drm.content";
+const OMA_DRM_RIGHTS_MIME = "application/vnd.oma.drm.rights+wbxml";
+
+const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
+const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download";
+
+Cu.import("resource://gre/modules/Downloads.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/HelperApps.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+// -----------------------------------------------------------------------
+// HelperApp Launcher Dialog
+// -----------------------------------------------------------------------
+
+XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
+ let ContentAreaUtils = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
+ return ContentAreaUtils;
+});
+
+function HelperAppLauncherDialog() { }
+
+HelperAppLauncherDialog.prototype = {
+ classID: Components.ID("{e9d277a0-268a-4ec2-bb8c-10fdf3e44611}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
+
+ /**
+ * Returns false if `url` represents a local or special URL that we don't
+ * wish to ever download.
+ *
+ * Returns true otherwise.
+ */
+ _canDownload: function (url, alreadyResolved=false) {
+ // The common case.
+ if (url.schemeIs("http") ||
+ url.schemeIs("https") ||
+ url.schemeIs("ftp")) {
+ return true;
+ }
+
+ // The less-common opposite case.
+ if (url.schemeIs("chrome") ||
+ url.schemeIs("jar") ||
+ url.schemeIs("resource") ||
+ url.schemeIs("wyciwyg") ||
+ url.schemeIs("file")) {
+ return false;
+ }
+
+ // For all other URIs, try to resolve them to an inner URI, and check that.
+ if (!alreadyResolved) {
+ let innerURI = NetUtil.newChannel({
+ uri: url,
+ loadUsingSystemPrincipal: true
+ }).URI;
+
+ if (!url.equals(innerURI)) {
+ return this._canDownload(innerURI, true);
+ }
+ }
+
+ // Anything else is fine to download.
+ return true;
+ },
+
+ /**
+ * Returns true if `launcher` represents a download for which we wish
+ * to prompt.
+ */
+ _shouldPrompt: function (launcher) {
+ let mimeType = this._getMimeTypeFromLauncher(launcher);
+
+ // Straight equality: nsIMIMEInfo normalizes.
+ return APK_MIME_TYPE == mimeType || OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE == mimeType;
+ },
+
+ /**
+ * Returns true if `launcher` represents a download for which we wish to
+ * offer a "Save to disk" option.
+ */
+ _shouldAddSaveToDiskIntent: function(launcher) {
+ let mimeType = this._getMimeTypeFromLauncher(launcher);
+
+ // We can't handle OMA downloads. So don't even try. (Bug 1219078)
+ return mimeType != OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE;
+ },
+
+ /**
+ * Returns true if `launcher`represents a download that should not be handled by Firefox
+ * or a third-party app and instead be forwarded to Android's download manager.
+ */
+ _shouldForwardToAndroidDownloadManager: function(aLauncher) {
+ let forwardDownload = Services.prefs.getBoolPref('browser.download.forward_oma_android_download_manager');
+ if (!forwardDownload) {
+ return false;
+ }
+
+ let mimeType = aLauncher.MIMEInfo.MIMEType;
+ if (!mimeType) {
+ mimeType = ContentAreaUtils.getMIMETypeForURI(aLauncher.source) || "";
+ }
+
+ return [
+ OMA_DOWNLOAD_DESCRIPTOR_MIME_TYPE,
+ OMA_DRM_MESSAGE_MIME,
+ OMA_DRM_CONTENT_MIME,
+ OMA_DRM_RIGHTS_MIME
+ ].indexOf(mimeType) != -1;
+ },
+
+ show: function hald_show(aLauncher, aContext, aReason) {
+ if (!this._canDownload(aLauncher.source)) {
+ this._refuseDownload(aLauncher);
+ return;
+ }
+
+ if (this._shouldForwardToAndroidDownloadManager(aLauncher)) {
+ Task.spawn(function* () {
+ try {
+ let hasPermission = yield RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE);
+ if (hasPermission) {
+ this._downloadWithAndroidDownloadManager(aLauncher);
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ } finally {
+ }
+ }.bind(this)).catch(Cu.reportError);
+ return;
+ }
+
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ let defaultHandler = new Object();
+ let apps = HelperApps.getAppsForUri(aLauncher.source, {
+ mimeType: aLauncher.MIMEInfo.MIMEType,
+ });
+
+ if (this._shouldAddSaveToDiskIntent(aLauncher)) {
+ // Add a fake intent for save to disk at the top of the list.
+ apps.unshift({
+ name: bundle.GetStringFromName("helperapps.saveToDisk"),
+ packageName: "org.mozilla.gecko.Download",
+ iconUri: "drawable://icon",
+ selected: true, // Default to download for files
+ launch: function() {
+ // Reset the preferredAction here.
+ aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk;
+ aLauncher.saveToDisk(null, false);
+ return true;
+ }
+ });
+ }
+
+ // We do not handle this download and there are no apps that want to do it
+ if (apps.length === 0) {
+ this._refuseDownload(aLauncher);
+ return;
+ }
+
+ let callback = function(app) {
+ aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+ if (!app.launch(aLauncher.source)) {
+ // Once the app is done we need to get rid of the temp file. This shouldn't
+ // get run in the saveToDisk case.
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ }
+
+ // See if the user already marked something as the default for this mimetype,
+ // and if that app is still installed.
+ let preferredApp = this._getPreferredApp(aLauncher);
+ if (preferredApp) {
+ let pref = apps.filter(function(app) {
+ return app.packageName === preferredApp;
+ });
+
+ if (pref.length > 0) {
+ callback(pref[0]);
+ return;
+ }
+ }
+
+ // If there's only one choice, and we don't want to prompt, go right ahead
+ // and choose that app automatically.
+ if (!this._shouldPrompt(aLauncher) && (apps.length === 1)) {
+ callback(apps[0]);
+ return;
+ }
+
+ // Otherwise, let's go through the prompt.
+ HelperApps.prompt(apps, {
+ title: bundle.GetStringFromName("helperapps.pick"),
+ buttons: [
+ bundle.GetStringFromName("helperapps.alwaysUse"),
+ bundle.GetStringFromName("helperapps.useJustOnce")
+ ],
+ // Tapping an app twice should choose "Just once".
+ doubleTapButton: 1
+ }, (data) => {
+ if (data.button < 0) {
+ return;
+ }
+
+ callback(apps[data.icongrid0]);
+
+ if (data.button === 0) {
+ this._setPreferredApp(aLauncher, apps[data.icongrid0]);
+ }
+ });
+ },
+
+ _refuseDownload: function(aLauncher) {
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+
+ Services.console.logStringMessage("Refusing download of non-downloadable file.");
+
+ let bundle = Services.strings.createBundle("chrome://browser/locale/handling.properties");
+ let failedText = bundle.GetStringFromName("download.blocked");
+
+ Snackbars.show(failedText, Snackbars.LENGTH_LONG);
+ },
+
+ _downloadWithAndroidDownloadManager(aLauncher) {
+ let mimeType = aLauncher.MIMEInfo.MIMEType;
+ if (!mimeType) {
+ mimeType = ContentAreaUtils.getMIMETypeForURI(aLauncher.source) || "";
+ }
+
+ Messaging.sendRequest({
+ 'type': 'Download:AndroidDownloadManager',
+ 'uri': aLauncher.source.spec,
+ 'mimeType': mimeType,
+ 'filename': aLauncher.suggestedFileName
+ });
+ },
+
+ _getPrefName: function getPrefName(mimetype) {
+ return "browser.download.preferred." + mimetype.replace("\\", ".");
+ },
+
+ _getMimeTypeFromLauncher: function (launcher) {
+ let mime = launcher.MIMEInfo.MIMEType;
+ if (!mime)
+ mime = ContentAreaUtils.getMIMETypeForURI(launcher.source) || "";
+ return mime;
+ },
+
+ _getPreferredApp: function getPreferredApp(launcher) {
+ let mime = this._getMimeTypeFromLauncher(launcher);
+ if (!mime)
+ return;
+
+ try {
+ return Services.prefs.getCharPref(this._getPrefName(mime));
+ } catch(ex) {
+ Services.console.logStringMessage("Error getting pref for " + mime + ".");
+ }
+ return null;
+ },
+
+ _setPreferredApp: function setPreferredApp(launcher, app) {
+ let mime = this._getMimeTypeFromLauncher(launcher);
+ if (!mime)
+ return;
+
+ if (app)
+ Services.prefs.setCharPref(this._getPrefName(mime), app.packageName);
+ else
+ Services.prefs.clearUserPref(this._getPrefName(mime));
+ },
+
+ promptForSaveToFileAsync: function (aLauncher, aContext, aDefaultFile,
+ aSuggestedFileExt, aForcePrompt) {
+ Task.spawn(function* () {
+ let file = null;
+ try {
+ let hasPermission = yield RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE);
+ if (hasPermission) {
+ // If we do have the STORAGE permission then pick the public downloads directory as destination
+ // for this file. Without the permission saveDestinationAvailable(null) will be called which
+ // will effectively cancel the download.
+ let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+ file = this.validateLeafName(new FileUtils.File(preferredDir),
+ aDefaultFile, aSuggestedFileExt);
+ }
+ } finally {
+ // The file argument will be null in case any exception occurred.
+ aLauncher.saveDestinationAvailable(file);
+ }
+ }.bind(this)).catch(Cu.reportError);
+ },
+
+ validateLeafName: function hald_validateLeafName(aLocalFile, aLeafName, aFileExt) {
+ if (!(aLocalFile && this.isUsableDirectory(aLocalFile)))
+ return null;
+
+ // Remove any leading periods, since we don't want to save hidden files
+ // automatically.
+ aLeafName = aLeafName.replace(/^\.+/, "");
+
+ if (aLeafName == "")
+ aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
+ aLocalFile.append(aLeafName);
+
+ this.makeFileUnique(aLocalFile);
+ return aLocalFile;
+ },
+
+ makeFileUnique: function hald_makeFileUnique(aLocalFile) {
+ try {
+ // Note - this code is identical to that in
+ // toolkit/content/contentAreaUtils.js.
+ // If you are updating this code, update that code too! We can't share code
+ // here since this is called in a js component.
+ let collisionCount = 0;
+ while (aLocalFile.exists()) {
+ collisionCount++;
+ if (collisionCount == 1) {
+ // Append "(2)" before the last dot in (or at the end of) the filename
+ // special case .ext.gz etc files so we don't wind up with .tar(2).gz
+ if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
+ aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
+ else
+ aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
+ }
+ else {
+ // replace the last (n) in the filename with (n+1)
+ aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")");
+ }
+ }
+ aLocalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ }
+ catch (e) {
+ dump("*** exception in validateLeafName: " + e + "\n");
+
+ if (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED)
+ throw e;
+
+ if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) {
+ aLocalFile.append("unnamed");
+ if (aLocalFile.exists())
+ aLocalFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600);
+ }
+ }
+ },
+
+ isUsableDirectory: function hald_isUsableDirectory(aDirectory) {
+ return aDirectory.exists() && aDirectory.isDirectory() && aDirectory.isWritable();
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]);
diff --git a/mobile/android/components/ImageBlockingPolicy.js b/mobile/android/components/ImageBlockingPolicy.js
new file mode 100644
index 000000000..2444bda06
--- /dev/null
+++ b/mobile/android/components/ImageBlockingPolicy.js
@@ -0,0 +1,125 @@
+/* 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/. */
+
+const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Constants
+
+//// SVG placeholder image for blocked image content
+const PLACEHOLDER_IMG = "chrome://browser/skin/images/placeholder_image.svg";
+
+//// Telemetry
+const TELEMETRY_TAP_TO_LOAD_ENABLED = "TAP_TO_LOAD_ENABLED";
+const TELEMETRY_SHOW_IMAGE_SIZE = "TAP_TO_LOAD_IMAGE_SIZE";
+const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
+
+//// Gecko preference
+const PREF_IMAGEBLOCKING = "browser.image_blocking";
+
+//// Enabled options
+const OPTION_NEVER = 0;
+const OPTION_ALWAYS = 1;
+const OPTION_WIFI_ONLY = 2;
+
+
+/**
+ * Content policy for blocking images
+ */
+function ImageBlockingPolicy() {
+ Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY, false);
+}
+
+ImageBlockingPolicy.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIObserver]),
+ classDescription: "Click-To-Play Image",
+ classID: Components.ID("{f55f77f9-d33d-4759-82fc-60db3ee0bb91}"),
+ contractID: "@mozilla.org/browser/blockimages-policy;1",
+ xpcom_categories: [{category: "content-policy", service: true}],
+
+ // nsIContentPolicy interface implementation
+ shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) {
+ // When enabled or when on cellular, and option for cellular-only is selected
+ if (this._enabled() == OPTION_NEVER || (this._enabled() == OPTION_WIFI_ONLY && this._usingCellular())) {
+ if (contentType === Ci.nsIContentPolicy.TYPE_IMAGE || contentType === Ci.nsIContentPolicy.TYPE_IMAGESET) {
+ // Accept any non-http(s) image URLs
+ if (!contentLocation.schemeIs("http") && !contentLocation.schemeIs("https")) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+
+ if (node instanceof Ci.nsIDOMHTMLImageElement) {
+ // Accept if the user has asked to view the image
+ if (node.getAttribute("data-ctv-show") == "true") {
+ sendImageSizeTelemetry(node.getAttribute("data-ctv-src"));
+ return Ci.nsIContentPolicy.ACCEPT;
+ }
+
+ setTimeout(() => {
+ // Cache the original image URL and swap in our placeholder
+ node.setAttribute("data-ctv-src", contentLocation.spec);
+ node.setAttribute("src", PLACEHOLDER_IMG);
+
+ // For imageset (img + srcset) the "srcset" is used even after we reset the "src" causing a loop.
+ // We are given the final image URL anyway, so it's OK to just remove the "srcset" value.
+ node.removeAttribute("srcset");
+ }, 0);
+ }
+
+ // Reject any image that is not associated with a DOM element
+ return Ci.nsIContentPolicy.REJECT;
+ }
+ }
+
+ // Accept all other content types
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ shouldProcess: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) {
+ return Ci.nsIContentPolicy.ACCEPT;
+ },
+
+ _usingCellular: function() {
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ return !(network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_UNKNOWN ||
+ network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET ||
+ network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_USB ||
+ network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_WIFI);
+ },
+
+ _enabled: function() {
+ return Services.prefs.getIntPref(PREF_IMAGEBLOCKING);
+ },
+
+ observe : function (subject, topic, data) {
+ if (topic == TOPIC_GATHER_TELEMETRY) {
+ Services.telemetry.getHistogramById(TELEMETRY_TAP_TO_LOAD_ENABLED).add(this._enabled());
+ }
+ },
+};
+
+function sendImageSizeTelemetry(imageURL) {
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open("HEAD", imageURL, true);
+ xhr.onreadystatechange = function (e) {
+ if (xhr.readyState != 4) {
+ return;
+ }
+ if (xhr.status != 200) {
+ return;
+ }
+ let contentLength = xhr.getResponseHeader("Content-Length");
+ if (!contentLength) {
+ return;
+ }
+ let imageSize = contentLength / 1024;
+ Services.telemetry.getHistogramById(TELEMETRY_SHOW_IMAGE_SIZE).add(imageSize);
+ };
+ xhr.send(null);
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ImageBlockingPolicy]);
diff --git a/mobile/android/components/LoginManagerPrompter.js b/mobile/android/components/LoginManagerPrompter.js
new file mode 100644
index 000000000..e70afbe14
--- /dev/null
+++ b/mobile/android/components/LoginManagerPrompter.js
@@ -0,0 +1,413 @@
+/* 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/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/* Constants for password prompt telemetry.
+* Mirrored in nsLoginManagerPrompter.js */
+const PROMPT_DISPLAYED = 0;
+
+const PROMPT_ADD = 1;
+const PROMPT_NOTNOW = 2;
+const PROMPT_NEVER = 3;
+
+const PROMPT_UPDATE = 1;
+
+/* ==================== LoginManagerPrompter ==================== */
+/*
+ * LoginManagerPrompter
+ *
+ * Implements interfaces for prompting the user to enter/save/change auth info.
+ *
+ * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
+ * found in HTML forms.
+ */
+function LoginManagerPrompter() {
+}
+
+LoginManagerPrompter.prototype = {
+ classID : Components.ID("97d12931-abe2-11df-94e2-0800200c9a66"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerPrompter]),
+
+ _factory : null,
+ _window : null,
+ _debug : false, // mirrors signon.debug
+
+ __pwmgr : null, // Password Manager service
+ get _pwmgr() {
+ if (!this.__pwmgr)
+ this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ return this.__pwmgr;
+ },
+
+ __promptService : null, // Prompt service for user interaction
+ get _promptService() {
+ if (!this.__promptService)
+ this.__promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService2);
+ return this.__promptService;
+ },
+
+ __strBundle : null, // String bundle for L10N
+ get _strBundle() {
+ if (!this.__strBundle) {
+ let bunService = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ this.__strBundle = {
+ pwmgr : bunService.createBundle("chrome://passwordmgr/locale/passwordmgr.properties"),
+ brand : bunService.createBundle("chrome://branding/locale/brand.properties")
+ };
+
+ if (!this.__strBundle)
+ throw "String bundle for Login Manager not present!";
+ }
+
+ return this.__strBundle;
+ },
+
+ __ellipsis : null,
+ get _ellipsis() {
+ if (!this.__ellipsis) {
+ this.__ellipsis = "\u2026";
+ try {
+ this.__ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
+ } catch (e) { }
+ }
+ return this.__ellipsis;
+ },
+
+ /*
+ * log
+ *
+ * Internal function for logging debug messages to the Error Console window.
+ */
+ log : function (message) {
+ if (!this._debug)
+ return;
+
+ dump("Pwmgr Prompter: " + message + "\n");
+ Services.console.logStringMessage("Pwmgr Prompter: " + message);
+ },
+
+ /* ---------- nsILoginManagerPrompter prompts ---------- */
+
+ /*
+ * init
+ *
+ */
+ init : function (aWindow, aFactory) {
+ this._chromeWindow = this._getChromeWindow(aWindow).wrappedJSObject;
+ this._factory = aFactory || null;
+ this._browser = null;
+
+ var prefBranch = Services.prefs.getBranch("signon.");
+ this._debug = prefBranch.getBoolPref("debug");
+ this.log("===== initialized =====");
+ },
+
+ set browser(aBrowser) {
+ this._browser = aBrowser;
+ },
+
+ // setting this attribute is ignored because Android does not consider
+ // opener windows when displaying login notifications
+ set opener(aOpener) { },
+
+ /*
+ * promptToSavePassword
+ *
+ */
+ promptToSavePassword : function (aLogin) {
+ this._showSaveLoginNotification(aLogin);
+ Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED);
+ Services.obs.notifyObservers(aLogin, "passwordmgr-prompt-save", null);
+ },
+
+ /*
+ * _showLoginNotification
+ *
+ * Displays a notification doorhanger.
+ * @param aBody
+ * String message to be displayed in the doorhanger
+ * @param aButtons
+ * Buttons to display with the doorhanger
+ * @param aUsername
+ * Username string used in creating a doorhanger action
+ * @param aPassword
+ * Password string used in creating a doorhanger action
+ */
+ _showLoginNotification : function (aBody, aButtons, aUsername, aPassword) {
+ let tabID = this._chromeWindow.BrowserApp.getTabForBrowser(this._browser).id;
+
+ let actionText = {
+ text: aUsername,
+ type: "EDIT",
+ bundle: { username: aUsername,
+ password: aPassword }
+ };
+
+ // The page we're going to hasn't loaded yet, so we want to persist
+ // across the first location change.
+
+ // Sites like Gmail perform a funky redirect dance before you end up
+ // at the post-authentication page. I don't see a good way to
+ // heuristically determine when to ignore such location changes, so
+ // we'll try ignoring location changes based on a time interval.
+ let options = {
+ persistWhileVisible: true,
+ timeout: Date.now() + 10000,
+ actionText: actionText
+ }
+
+ var nativeWindow = this._getNativeWindow();
+ if (nativeWindow)
+ nativeWindow.doorhanger.show(aBody, "password", aButtons, tabID, options, "LOGIN");
+ },
+
+ /*
+ * _showSaveLoginNotification
+ *
+ * Displays a notification doorhanger (rather than a popup), to allow the user to
+ * save the specified login. This allows the user to see the results of
+ * their login, and only save a login which they know worked.
+ *
+ */
+ _showSaveLoginNotification : function (aLogin) {
+ let brandShortName = this._strBundle.brand.GetStringFromName("brandShortName");
+ let notificationText = this._getLocalizedString("saveLogin", [brandShortName]);
+
+ let username = aLogin.username ? this._sanitizeUsername(aLogin.username) : "";
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var pwmgr = this._pwmgr;
+ let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION");
+
+ var buttons = [
+ {
+ label: this._getLocalizedString("neverButton"),
+ callback: function() {
+ promptHistogram.add(PROMPT_NEVER);
+ pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
+ }
+ },
+ {
+ label: this._getLocalizedString("rememberButton"),
+ callback: function(checked, response) {
+ if (response) {
+ aLogin.username = response["username"] || aLogin.username;
+ aLogin.password = response["password"] || aLogin.password;
+ }
+ pwmgr.addLogin(aLogin);
+ promptHistogram.add(PROMPT_ADD);
+ },
+ positive: true
+ }
+ ];
+
+ this._showLoginNotification(notificationText, buttons, aLogin.username, aLogin.password);
+ },
+
+ /*
+ * promptToChangePassword
+ *
+ * Called when we think we detect a password change for an existing
+ * login, when the form being submitted contains multiple password
+ * fields.
+ *
+ */
+ promptToChangePassword : function (aOldLogin, aNewLogin) {
+ this._showChangeLoginNotification(aOldLogin, aNewLogin.password);
+ Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION").add(PROMPT_DISPLAYED);
+ let oldGUID = aOldLogin.QueryInterface(Ci.nsILoginMetaInfo).guid;
+ Services.obs.notifyObservers(aNewLogin, "passwordmgr-prompt-change", oldGUID);
+ },
+
+ /*
+ * _showChangeLoginNotification
+ *
+ * Shows the Change Password notification doorhanger.
+ *
+ */
+ _showChangeLoginNotification : function (aOldLogin, aNewPassword) {
+ var notificationText;
+ if (aOldLogin.username) {
+ let displayUser = this._sanitizeUsername(aOldLogin.username);
+ notificationText = this._getLocalizedString("updatePassword", [displayUser]);
+ } else {
+ notificationText = this._getLocalizedString("updatePasswordNoUser");
+ }
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var self = this;
+ let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION");
+
+ var buttons = [
+ {
+ label: this._getLocalizedString("dontUpdateButton"),
+ callback: function() {
+ promptHistogram.add(PROMPT_NOTNOW);
+ // do nothing
+ }
+ },
+ {
+ label: this._getLocalizedString("updateButton"),
+ callback: function(checked, response) {
+ let password = response ? response["password"] : aNewPassword;
+ self._updateLogin(aOldLogin, password);
+
+ promptHistogram.add(PROMPT_UPDATE);
+ },
+ positive: true
+ }
+ ];
+
+ this._showLoginNotification(notificationText, buttons, aOldLogin.username, aNewPassword);
+ },
+
+ /*
+ * promptToChangePasswordWithUsernames
+ *
+ * Called when we detect a password change in a form submission, but we
+ * don't know which existing login (username) it's for. Asks the user
+ * to select a username and confirm the password change.
+ *
+ * Note: The caller doesn't know the username for aNewLogin, so this
+ * function fills in .username and .usernameField with the values
+ * from the login selected by the user.
+ *
+ * Note; XPCOM stupidity: |count| is just |logins.length|.
+ */
+ promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
+ const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
+
+ var usernames = logins.map(l => l.username);
+ var dialogText = this._getLocalizedString("userSelectText");
+ var dialogTitle = this._getLocalizedString("passwordChangeTitle");
+ var selectedIndex = { value: null };
+
+ // If user selects ok, outparam.value is set to the index
+ // of the selected username.
+ var ok = this._promptService.select(null,
+ dialogTitle, dialogText,
+ usernames.length, usernames,
+ selectedIndex);
+ if (ok) {
+ // Now that we know which login to use, modify its password.
+ let selectedLogin = logins[selectedIndex.value];
+ this.log("Updating password for user " + selectedLogin.username);
+ this._updateLogin(selectedLogin, aNewLogin.password);
+ }
+ },
+
+ /* ---------- Internal Methods ---------- */
+
+ /*
+ * _updateLogin
+ */
+ _updateLogin : function (login, newPassword) {
+ var now = Date.now();
+ var propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ if (newPassword) {
+ propBag.setProperty("password", newPassword);
+ // Explicitly set the password change time here (even though it would
+ // be changed automatically), to ensure that it's exactly the same
+ // value as timeLastUsed.
+ propBag.setProperty("timePasswordChanged", now);
+ }
+ propBag.setProperty("timeLastUsed", now);
+ propBag.setProperty("timesUsedIncrement", 1);
+ this._pwmgr.modifyLogin(login, propBag);
+ },
+
+ /*
+ * _getChromeWindow
+ *
+ * Given a content DOM window, returns the chrome window it's in.
+ */
+ _getChromeWindow: function (aWindow) {
+ if (aWindow instanceof Ci.nsIDOMChromeWindow)
+ return aWindow;
+ var chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler.ownerDocument.defaultView;
+ return chromeWin;
+ },
+
+ /*
+ * _getNativeWindow
+ *
+ * Returns the NativeWindow to this prompter, or null if there isn't
+ * a NativeWindow available (w/ error sent to logcat).
+ */
+ _getNativeWindow : function () {
+ let nativeWindow = null;
+ try {
+ let chromeWin = this._chromeWindow;
+ if (chromeWin.NativeWindow) {
+ nativeWindow = chromeWin.NativeWindow;
+ } else {
+ Cu.reportError("NativeWindow not available on window");
+ }
+
+ } catch (e) {
+ // If any errors happen, just assume no native window helper.
+ Cu.reportError("No NativeWindow available: " + e);
+ }
+ return nativeWindow;
+ },
+
+ /*
+ * _getLocalizedString
+ *
+ * Can be called as:
+ * _getLocalizedString("key1");
+ * _getLocalizedString("key2", ["arg1"]);
+ * _getLocalizedString("key3", ["arg1", "arg2"]);
+ * (etc)
+ *
+ * Returns the localized string for the specified key,
+ * formatted if required.
+ *
+ */
+ _getLocalizedString : function (key, formatArgs) {
+ if (formatArgs)
+ return this._strBundle.pwmgr.formatStringFromName(
+ key, formatArgs, formatArgs.length);
+ else
+ return this._strBundle.pwmgr.GetStringFromName(key);
+ },
+
+ /*
+ * _sanitizeUsername
+ *
+ * Sanitizes the specified username, by stripping quotes and truncating if
+ * it's too long. This helps prevent an evil site from messing with the
+ * "save password?" prompt too much.
+ */
+ _sanitizeUsername : function (username) {
+ if (username.length > 30) {
+ username = username.substring(0, 30);
+ username += this._ellipsis;
+ }
+ return username.replace(/['"]/g, "");
+ },
+}; // end of LoginManagerPrompter implementation
+
+
+var component = [LoginManagerPrompter];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
diff --git a/mobile/android/components/MobileComponents.manifest b/mobile/android/components/MobileComponents.manifest
new file mode 100644
index 000000000..8cf8f9a27
--- /dev/null
+++ b/mobile/android/components/MobileComponents.manifest
@@ -0,0 +1,126 @@
+# AboutRedirector.js
+component {322ba47e-7047-4f71-aebf-cb7d69325cd9} AboutRedirector.js
+contract @mozilla.org/network/protocol/about;1?what= {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=fennec {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=firefox {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=empty {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=rights {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=certerror {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=home {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=downloads {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=reader {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=feedback {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=privatebrowsing {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+#ifdef MOZ_SERVICES_HEALTHREPORT
+contract @mozilla.org/network/protocol/about;1?what=healthreport {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+#endif
+contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=accounts {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+contract @mozilla.org/network/protocol/about;1?what=logins {322ba47e-7047-4f71-aebf-cb7d69325cd9}
+
+# DirectoryProvider.js
+component {ef0f7a87-c1ee-45a8-8d67-26f586e46a4b} DirectoryProvider.js
+contract @mozilla.org/browser/directory-provider;1 {ef0f7a87-c1ee-45a8-8d67-26f586e46a4b}
+category xpcom-directory-providers browser-directory-provider @mozilla.org/browser/directory-provider;1
+
+# stylesheets
+category agent-style-sheets browser-content-stylesheet chrome://browser/skin/content.css
+
+# SessionStore.js
+component {8c1f07d6-cba3-4226-a315-8bd43d67d032} SessionStore.js
+contract @mozilla.org/browser/sessionstore;1 {8c1f07d6-cba3-4226-a315-8bd43d67d032}
+category app-startup SessionStore service,@mozilla.org/browser/sessionstore;1
+
+# ContentPermissionPrompt.js
+component {C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5} ContentPermissionPrompt.js
+contract @mozilla.org/content-permission/prompt;1 {C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}
+
+# PromptService.js
+component {9a61149b-2276-4a0a-b79c-be994ad106cf} PromptService.js
+contract @mozilla.org/prompter;1 {9a61149b-2276-4a0a-b79c-be994ad106cf}
+contract @mozilla.org/embedcomp/prompt-service;1 {9a61149b-2276-4a0a-b79c-be994ad106cf}
+component {80dae1e9-e0d2-4974-915f-f97050fa8068} PromptService.js
+contract @mozilla.org/network/authprompt-adapter-factory;1 {80dae1e9-e0d2-4974-915f-f97050fa8068}
+
+# PresentationDevicePrompt.js
+component {388bd149-c919-4a43-b646-d7ec57877689} PresentationDevicePrompt.js
+contract @mozilla.org/presentation-device/prompt;1 {388bd149-c919-4a43-b646-d7ec57877689}
+
+# PresentationRequestUIGlue.js
+component {9c550ef7-3ff6-4bd1-9ad1-5a3735b90d21} PresentationRequestUIGlue.js
+contract @mozilla.org/presentation/requestuiglue;1 {9c550ef7-3ff6-4bd1-9ad1-5a3735b90d21}
+
+# ImageBlockingPolicy.js
+component {f55f77f9-d33d-4759-82fc-60db3ee0bb91} ImageBlockingPolicy.js
+contract @mozilla.org/browser/blockimages-policy;1 {f55f77f9-d33d-4759-82fc-60db3ee0bb91}
+category content-policy ImageBlockingPolicy @mozilla.org/browser/blockimages-policy;1
+
+# XPIDialogService.js
+component {c1242012-27d8-477e-a0f1-0b098ffc329b} XPIDialogService.js
+contract @mozilla.org/addons/web-install-prompt;1 {c1242012-27d8-477e-a0f1-0b098ffc329b}
+
+# HelperAppDialog.js
+component {e9d277a0-268a-4ec2-bb8c-10fdf3e44611} HelperAppDialog.js
+contract @mozilla.org/helperapplauncherdialog;1 {e9d277a0-268a-4ec2-bb8c-10fdf3e44611}
+
+# BrowserCLH.js
+component {be623d20-d305-11de-8a39-0800200c9a66} BrowserCLH.js application={aa3c5121-dab2-40e2-81ca-7ea25febc110}
+contract @mozilla.org/browser/browser-clh;1 {be623d20-d305-11de-8a39-0800200c9a66}
+category app-startup BrowserCLH @mozilla.org/browser/browser-clh;1
+
+# ContentDispatchChooser.js
+component {5a072a22-1e66-4100-afc1-07aed8b62fc5} ContentDispatchChooser.js
+contract @mozilla.org/content-dispatch-chooser;1 {5a072a22-1e66-4100-afc1-07aed8b62fc5}
+
+# AddonUpdateService.js
+component {93c8824c-9b87-45ae-bc90-5b82a1e4d877} AddonUpdateService.js
+contract @mozilla.org/browser/addon-update-service;1 {93c8824c-9b87-45ae-bc90-5b82a1e4d877}
+category update-timer AddonUpdateService @mozilla.org/browser/addon-update-service;1,getService,auto-addon-background-update-timer,extensions.autoupdate.interval,86400
+
+# LoginManagerPrompter.js
+component {97d12931-abe2-11df-94e2-0800200c9a66} LoginManagerPrompter.js
+contract @mozilla.org/login-manager/prompter;1 {97d12931-abe2-11df-94e2-0800200c9a66}
+
+# BlocklistPrompt.js
+component {4e6ea350-b09a-11df-94e2-0800200c9a66} BlocklistPrompt.js
+contract @mozilla.org/addons/blocklist-prompt;1 {4e6ea350-b09a-11df-94e2-0800200c9a66}
+
+# NSSDialogService.js
+component {cbc08081-49b6-4561-9c18-a7707a50bda1} NSSDialogService.js
+contract @mozilla.org/nsCertificateDialogs;1 {cbc08081-49b6-4561-9c18-a7707a50bda1}
+contract @mozilla.org/nsClientAuthDialogs;1 {cbc08081-49b6-4561-9c18-a7707a50bda1}
+
+# SiteSpecificUserAgent.js
+component {d5234c9d-0ee2-4b3c-9da3-18be9e5cf7e6} SiteSpecificUserAgent.js
+contract @mozilla.org/dom/site-specific-user-agent;1 {d5234c9d-0ee2-4b3c-9da3-18be9e5cf7e6}
+
+# FilePicker.js
+component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js
+contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f}
+
+# FxAccountsPush.js
+component {d1bbb0fd-1d47-4134-9c12-d7b1be20b721} FxAccountsPush.js
+contract @mozilla.org/fxa-push;1 {d1bbb0fd-1d47-4134-9c12-d7b1be20b721}
+category android-push-service FxAccountsPush @mozilla.org/fxa-push;1
+
+#ifndef RELEASE_OR_BETA
+# TabSource.js
+component {5850c76e-b916-4218-b99a-31f004e0a7e7} TabSource.js
+contract @mozilla.org/tab-source-service;1 {5850c76e-b916-4218-b99a-31f004e0a7e7}
+#endif
+
+# Snippets.js
+component {a78d7e59-b558-4321-a3d6-dffe2f1e76dd} Snippets.js
+contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
+category browser-delayed-startup-finished Snippets @mozilla.org/snippets;1
+category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400
+
+# ColorPicker.js
+component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
+contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}
+
+# PersistentNotificationHandler.js
+component {75390fe7-f8a3-423a-b3b1-258d7eabed40} PersistentNotificationHandler.js
+contract @mozilla.org/persistent-notification-handler;1 {75390fe7-f8a3-423a-b3b1-258d7eabed40}
+category persistent-notification-click PersistentNotificationHandler @mozilla.org/persistent-notification-handler;1
+category persistent-notification-close PersistentNotificationHandler @mozilla.org/persistent-notification-handler;1
diff --git a/mobile/android/components/NSSDialogService.js b/mobile/android/components/NSSDialogService.js
new file mode 100644
index 000000000..671cc8c35
--- /dev/null
+++ b/mobile/android/components/NSSDialogService.js
@@ -0,0 +1,276 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cc = Components.classes;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+// -----------------------------------------------------------------------
+// NSS Dialog Service
+// -----------------------------------------------------------------------
+
+function NSSDialogs() { }
+
+NSSDialogs.prototype = {
+ classID: Components.ID("{cbc08081-49b6-4561-9c18-a7707a50bda1}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICertificateDialogs, Ci.nsIClientAuthDialogs]),
+
+ /**
+ * Escapes the given input via HTML entity encoding. Used to prevent HTML
+ * injection when the input is to be placed inside an HTML body, but not in
+ * any other context.
+ *
+ * @param {String} input The input to interpret as a plain string.
+ * @returns {String} The escaped input.
+ */
+ escapeHTML: function(input) {
+ return input.replace(/&/g, "&amp;")
+ .replace(/</g, "&lt;")
+ .replace(/>/g, "&gt;")
+ .replace(/"/g, "&quot;")
+ .replace(/'/g, "&#x27;")
+ .replace(/\//g, "&#x2F;");
+ },
+
+ getString: function(aName) {
+ if (!this.bundle) {
+ this.bundle = Services.strings.createBundle("chrome://browser/locale/pippki.properties");
+ }
+ return this.bundle.GetStringFromName(aName);
+ },
+
+ formatString: function(aName, argList) {
+ if (!this.bundle) {
+ this.bundle =
+ Services.strings.createBundle("chrome://browser/locale/pippki.properties");
+ }
+ let escapedArgList = Array.from(argList, x => this.escapeHTML(x));
+ return this.bundle.formatStringFromName(aName, escapedArgList,
+ escapedArgList.length);
+ },
+
+ getPrompt: function(aTitle, aText, aButtons) {
+ return new Prompt({
+ title: aTitle,
+ text: aText,
+ buttons: aButtons,
+ });
+ },
+
+ showPrompt: function(aPrompt) {
+ let response = null;
+ aPrompt.show(function(data) {
+ response = data;
+ });
+
+ // Spin this thread while we wait for a result
+ let thread = Services.tm.currentThread;
+ while (response === null)
+ thread.processNextEvent(true);
+
+ return response;
+ },
+
+ confirmDownloadCACert: function(aCtx, aCert, aTrust) {
+ while (true) {
+ let prompt = this.getPrompt(this.getString("downloadCert.title"),
+ this.getString("downloadCert.message1"),
+ [ this.getString("nssdialogs.ok.label"),
+ this.getString("downloadCert.viewCert.label"),
+ this.getString("nssdialogs.cancel.label")
+ ]);
+
+ prompt.addCheckbox({ id: "trustSSL", label: this.getString("downloadCert.trustSSL"), checked: false })
+ .addCheckbox({ id: "trustEmail", label: this.getString("downloadCert.trustEmail"), checked: false })
+ .addCheckbox({ id: "trustSign", label: this.getString("downloadCert.trustObjSign"), checked: false });
+ let response = this.showPrompt(prompt);
+
+ // they hit the "view cert" button, so show the cert and try again
+ if (response.button == 1) {
+ this.viewCert(aCtx, aCert);
+ continue;
+ } else if (response.button != 0) {
+ return false;
+ }
+
+ aTrust.value = Ci.nsIX509CertDB.UNTRUSTED;
+ if (response.trustSSL) aTrust.value |= Ci.nsIX509CertDB.TRUSTED_SSL;
+ if (response.trustEmail) aTrust.value |= Ci.nsIX509CertDB.TRUSTED_EMAIL;
+ if (response.trustSign) aTrust.value |= Ci.nsIX509CertDB.TRUSTED_OBJSIGN;
+ return true;
+ }
+ },
+
+ setPKCS12FilePassword: function(aCtx, aPassword) {
+ // this dialog is never shown in Fennec; in Desktop it is shown while backing up a personal
+ // certificate to a file via Preferences->Advanced->Encryption->View Certificates->Your Certificates
+ throw "Unimplemented";
+ },
+
+ getPKCS12FilePassword: function(aCtx, aPassword) {
+ let prompt = this.getPrompt(this.getString("pkcs12.getpassword.title"),
+ this.getString("pkcs12.getpassword.message"),
+ [ this.getString("nssdialogs.ok.label"),
+ this.getString("nssdialogs.cancel.label")
+ ]).addPassword({id: "pw"});
+ let response = this.showPrompt(prompt);
+ if (response.button != 0) {
+ return false;
+ }
+
+ aPassword.value = response.pw;
+ return true;
+ },
+
+ certInfoSection: function(aHeading, aDataPairs, aTrailingNewline = true) {
+ let certInfoStrings = [
+ "<big>" + this.getString(aHeading) + "</big>",
+ ];
+
+ for (let i = 0; i < aDataPairs.length; i += 2) {
+ let key = aDataPairs[i];
+ let value = aDataPairs[i + 1];
+ certInfoStrings.push(this.formatString(key, [value]));
+ }
+
+ if (aTrailingNewline) {
+ certInfoStrings.push("<br/>");
+ }
+
+ return certInfoStrings.join("<br/>");
+ },
+
+ viewCert: function(aCtx, aCert) {
+ let p = this.getPrompt(this.getString("certmgr.title"), "", [
+ this.getString("nssdialogs.ok.label"),
+ ]);
+ p.addLabel({ label: this.certInfoSection("certmgr.subjectinfo.label",
+ ["certdetail.cn", aCert.commonName,
+ "certdetail.o", aCert.organization,
+ "certdetail.ou", aCert.organizationalUnit,
+ "certdetail.serialnumber", aCert.serialNumber])})
+ .addLabel({ label: this.certInfoSection("certmgr.issuerinfo.label",
+ ["certdetail.cn", aCert.issuerCommonName,
+ "certdetail.o", aCert.issuerOrganization,
+ "certdetail.ou", aCert.issuerOrganizationUnit])})
+ .addLabel({ label: this.certInfoSection("certmgr.periodofvalidity.label",
+ ["certdetail.notBefore", aCert.validity.notBeforeLocalDay,
+ "certdetail.notAfter", aCert.validity.notAfterLocalDay])})
+ .addLabel({ label: this.certInfoSection("certmgr.fingerprints.label",
+ ["certdetail.sha256fingerprint", aCert.sha256Fingerprint,
+ "certdetail.sha1fingerprint", aCert.sha1Fingerprint],
+ false) });
+ this.showPrompt(p);
+ },
+
+ /**
+ * Returns a list of details of the given cert relevant for TLS client
+ * authentication.
+ *
+ * @param {nsIX509Cert} cert Cert to get the details of.
+ * @returns {String} <br/> delimited list of details.
+ */
+ getCertDetails: function(cert) {
+ let detailLines = [
+ this.formatString("clientAuthAsk.issuedTo", [cert.subjectName]),
+ this.formatString("clientAuthAsk.serial", [cert.serialNumber]),
+ this.formatString("clientAuthAsk.validityPeriod",
+ [cert.validity.notBeforeLocalTime,
+ cert.validity.notAfterLocalTime]),
+ ];
+ let keyUsages = cert.keyUsages;
+ if (keyUsages) {
+ detailLines.push(this.formatString("clientAuthAsk.keyUsages",
+ [keyUsages]));
+ }
+ let emailAddresses = cert.getEmailAddresses({});
+ if (emailAddresses.length > 0) {
+ let joinedAddresses = emailAddresses.join(", ");
+ detailLines.push(this.formatString("clientAuthAsk.emailAddresses",
+ [joinedAddresses]));
+ }
+ detailLines.push(this.formatString("clientAuthAsk.issuedBy",
+ [cert.issuerName]));
+ detailLines.push(this.formatString("clientAuthAsk.storedOn",
+ [cert.tokenName]));
+
+ return detailLines.join("<br/>");
+ },
+
+ viewCertDetails: function(details) {
+ let p = this.getPrompt(this.getString("clientAuthAsk.message3"),
+ '',
+ [ this.getString("nssdialogs.ok.label") ]);
+ p.addLabel({ label: details });
+ this.showPrompt(p);
+ },
+
+ chooseCertificate: function(ctx, hostname, port, organization, issuerOrg,
+ certList, selectedIndex) {
+ let rememberSetting =
+ Services.prefs.getBoolPref("security.remember_cert_checkbox_default_setting");
+
+ let serverRequestedDetails = [
+ this.formatString("clientAuthAsk.hostnameAndPort",
+ [hostname, port.toString()]),
+ this.formatString("clientAuthAsk.organization", [organization]),
+ this.formatString("clientAuthAsk.issuer", [issuerOrg]),
+ ].join("<br/>");
+
+ let certNickList = [];
+ let certDetailsList = [];
+ for (let i = 0; i < certList.length; i++) {
+ let cert = certList.queryElementAt(i, Ci.nsIX509Cert);
+ certNickList.push(this.formatString("clientAuthAsk.nickAndSerial",
+ [cert.nickname, cert.serialNumber]));
+ certDetailsList.push(this.getCertDetails(cert));
+ }
+
+ selectedIndex.value = 0;
+ while (true) {
+ let buttons = [
+ this.getString("nssdialogs.ok.label"),
+ this.getString("clientAuthAsk.viewCert.label"),
+ this.getString("nssdialogs.cancel.label"),
+ ];
+ let prompt = this.getPrompt(this.getString("clientAuthAsk.title"),
+ this.getString("clientAuthAsk.message1"),
+ buttons)
+ .addLabel({ id: "requestedDetails", label: serverRequestedDetails } )
+ .addMenulist({
+ id: "nicknames",
+ label: this.getString("clientAuthAsk.message2"),
+ values: certNickList,
+ selected: selectedIndex.value,
+ }).addCheckbox({
+ id: "rememberBox",
+ label: this.getString("clientAuthAsk.remember.label"),
+ checked: rememberSetting
+ });
+ let response = this.showPrompt(prompt);
+ selectedIndex.value = response.nicknames;
+ if (response.button == 1 /* buttons[1] */) {
+ this.viewCertDetails(certDetailsList[selectedIndex.value]);
+ continue;
+ } else if (response.button == 0 /* buttons[0] */) {
+ if (response.rememberBox == true) {
+ let caud = ctx.QueryInterface(Ci.nsIClientAuthUserDecision);
+ if (caud) {
+ caud.rememberClientAuthCertificate = true;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NSSDialogs]);
diff --git a/mobile/android/components/PersistentNotificationHandler.js b/mobile/android/components/PersistentNotificationHandler.js
new file mode 100644
index 000000000..2a3529f5f
--- /dev/null
+++ b/mobile/android/components/PersistentNotificationHandler.js
@@ -0,0 +1,78 @@
+/* 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";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, 'Services', // jshint ignore:line
+ 'resource://gre/modules/Services.jsm');
+XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage",
+ "@mozilla.org/notificationStorage;1",
+ "nsINotificationStorage");
+XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+ "@mozilla.org/serviceworkers/manager;1",
+ "nsIServiceWorkerManager");
+
+function PersistentNotificationHandler() {
+}
+
+PersistentNotificationHandler.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+ classID: Components.ID("{75390fe7-f8a3-423a-b3b1-258d7eabed40}"),
+
+ observe(subject, topic, data) {
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ Cu.import("resource://gre/modules/NotificationDB.jsm");
+ }
+ const persistentInfo = JSON.parse(data);
+
+ if (topic === 'persistent-notification-click') {
+ notificationStorage.getByID(persistentInfo.origin, persistentInfo.id, {
+ handle(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationScope) {
+ serviceWorkerManager.sendNotificationClickEvent(
+ persistentInfo.originSuffix,
+ serviceWorkerRegistrationScope,
+ id,
+ title,
+ dir,
+ lang,
+ body,
+ tag,
+ icon,
+ data,
+ behavior
+ );
+ notificationStorage.delete(persistentInfo.origin, persistentInfo.id);
+ }
+ });
+ } else if (topic === 'persistent-notification-close') {
+ notificationStorage.getByID(persistentInfo.origin, persistentInfo.id, {
+ handle(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationScope) {
+ serviceWorkerManager.sendNotificationCloseEvent(
+ persistentInfo.originSuffix,
+ serviceWorkerRegistrationScope,
+ id,
+ title,
+ dir,
+ lang,
+ body,
+ tag,
+ icon,
+ data,
+ behavior
+ );
+ notificationStorage.delete(persistentInfo.origin, persistentInfo.id);
+ }
+ });
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
+ PersistentNotificationHandler
+]);
diff --git a/mobile/android/components/PresentationDevicePrompt.js b/mobile/android/components/PresentationDevicePrompt.js
new file mode 100644
index 000000000..e3e063373
--- /dev/null
+++ b/mobile/android/components/PresentationDevicePrompt.js
@@ -0,0 +1,134 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
+ "resource://gre/modules/UITelemetry.jsm");
+
+const kPRESENTATIONDEVICEPROMPT_CONTRACTID = "@mozilla.org/presentation-device/prompt;1";
+const kPRESENTATIONDEVICEPROMPT_CID = Components.ID("{388bd149-c919-4a43-b646-d7ec57877689}");
+
+function debug(aMsg) {
+ // dump("-*- PresentationDevicePrompt: " + aMsg + "\n");
+}
+
+// nsIPresentationDevicePrompt
+function PresentationDevicePrompt() {
+ debug("PresentationDevicePrompt init");
+}
+
+PresentationDevicePrompt.prototype = {
+ classID: kPRESENTATIONDEVICEPROMPT_CID,
+ contractID: kPRESENTATIONDEVICEPROMPT_CONTRACTID,
+ classDescription: "Fennec Presentation Device Prompt",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt]),
+
+ _devices: [], // Store all available presentation devices
+ _request: null, // Store the request from presentation api
+
+ _getString: function(aName) {
+ debug("_getString");
+
+ if (!this.bundle) {
+ this.bundle = Services.strings.createBundle("chrome://browser/locale/devicePrompt.properties");
+ }
+ return this.bundle.GetStringFromName(aName);
+ },
+
+ _loadDevices: function(requestURLs) {
+ debug("_loadDevices");
+
+ let deviceManager = Cc["@mozilla.org/presentation-device/manager;1"]
+ .getService(Ci.nsIPresentationDeviceManager);
+ let devices = deviceManager.getAvailableDevices(requestURLs).QueryInterface(Ci.nsIArray);
+
+ // Re-load the available devices
+ this._devices = [];
+ for (let i = 0; i < devices.length; i++) {
+ let device = devices.queryElementAt(i, Ci.nsIPresentationDevice);
+ this._devices.push(device);
+ }
+ },
+
+ _getPromptMenu: function(aDevices) {
+ debug("_getPromptMenu");
+
+ return aDevices.map(function(device) {
+ return { label: device.name };
+ });
+ },
+
+ _getPrompt: function(aTitle, aMenu) {
+ debug("_getPrompt");
+
+ let p = new Prompt({
+ title: aTitle,
+ });
+
+ p.setSingleChoiceItems(aMenu);
+
+ return p;
+ },
+
+ _showPrompt: function(aPrompt, aCallback) {
+ debug("_showPrompt");
+
+ aPrompt.show(function(data) {
+ let buttonIndex = data.button;
+ aCallback(buttonIndex);
+ });
+ },
+
+ _selectDevice: function(aIndex) {
+ debug("_selectDevice");
+
+ if (!this._request) {
+ return;
+ }
+
+ if (aIndex < 0) { // Cancel request if no selected device,
+ this._request.cancel(Cr.NS_ERROR_DOM_NOT_ALLOWED_ERR);
+ return;
+ } else if (!this._devices.length) { // or there is no available devices
+ this._request.cancel(Cr.NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ this._request.select(this._devices[aIndex]);
+ },
+
+ // This will be fired when window.PresentationRequest(URL).start() is called
+ promptDeviceSelection: function(aRequest) {
+ debug("promptDeviceSelection");
+
+ // Load available presentation devices into this._devices
+ this._loadDevices(aRequest.requestURLs);
+
+ if (!this._devices.length) { // Cancel request if no available device
+ aRequest.cancel(Cr.NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ this._request = aRequest;
+
+ let prompt = this._getPrompt(this._getString("deviceMenu.title"),
+ this._getPromptMenu(this._devices));
+
+ this._showPrompt(prompt, this._selectDevice.bind(this));
+
+ UITelemetry.addEvent("show.1", "dialog", null, "prompt_device_selection");
+ },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationDevicePrompt]);
diff --git a/mobile/android/components/PresentationRequestUIGlue.js b/mobile/android/components/PresentationRequestUIGlue.js
new file mode 100644
index 000000000..af252c875
--- /dev/null
+++ b/mobile/android/components/PresentationRequestUIGlue.js
@@ -0,0 +1,86 @@
+/* -*- Mode: tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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"
+
+const { interfaces: Ci, utils: Cu, classes: Cc } = Components;
+
+const TOPIC_PRESENTATION_RECEIVER_LAUNCH = "presentation-receiver:launch";
+const TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE = "presentation-receiver:launch:response";
+
+// globals XPCOMUtils
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+// globals Services
+Cu.import("resource://gre/modules/Services.jsm");
+
+function log(str) {
+ // dump("-*- PresentationRequestUIGlue.js -*-: " + str + "\n");
+}
+
+function PresentationRequestUIGlue() { }
+
+PresentationRequestUIGlue.prototype = {
+ sendRequest: function sendRequest(aURL, aSessionId, aDevice) {
+ log("PresentationRequestUIGlue - sendRequest aURL=" + aURL +
+ " aSessionId=" + aSessionId);
+
+ let localDevice;
+ try {
+ localDevice = aDevice.QueryInterface(Ci.nsIPresentationLocalDevice);
+ } catch (e) {
+ /* XXX: Currently, Fennec only support 1-UA devices. Remove this
+ * Promise.reject() when it starts to support 2-UA devices.
+ */
+ log("Not an 1-UA device.")
+ return new Promise.reject();
+ }
+
+ return new Promise((aResolve, aReject) => {
+
+ let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator);
+ let requestId = uuidGenerator.generateUUID().toString();
+
+ let handleObserve = (aSubject, aTopic, aData) => {
+ log("Got observe: aTopic=" + aTopic);
+
+ let data = JSON.parse(aData);
+ if (data.requestId != requestId) {
+ return;
+ }
+
+ Services.obs.removeObserver(handleObserve,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE);
+ switch(data.result) {
+ case "success":
+ aResolve(aSubject);
+ break;
+ case "error":
+ aReject();
+ break;
+ };
+ };
+
+ Services.obs.addObserver(handleObserve,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH_RESPONSE,
+ false);
+
+ let data = {
+ url: aURL,
+ windowId: localDevice.windowId,
+ requestId: requestId
+ };
+ Services.obs.notifyObservers(null,
+ TOPIC_PRESENTATION_RECEIVER_LAUNCH,
+ JSON.stringify(data));
+ })
+ },
+
+ classID: Components.ID("9c550ef7-3ff6-4bd1-9ad1-5a3735b90d21"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationRequestUIGlue]);
diff --git a/mobile/android/components/PromptService.js b/mobile/android/components/PromptService.js
new file mode 100644
index 000000000..93aff67ee
--- /dev/null
+++ b/mobile/android/components/PromptService.js
@@ -0,0 +1,878 @@
+/* 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/. */
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+var gPromptService = null;
+
+function PromptService() {
+ gPromptService = this;
+}
+
+PromptService.prototype = {
+ classID: Components.ID("{9a61149b-2276-4a0a-b79c-be994ad106cf}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIPromptService, Ci.nsIPromptService2]),
+
+ /* ---------- nsIPromptFactory ---------- */
+ // XXX Copied from nsPrompter.js.
+ getPrompt: function getPrompt(domWin, iid) {
+ // This is still kind of dumb; the C++ code delegated to login manager
+ // here, which in turn calls back into us via nsIPromptService2.
+ if (iid.equals(Ci.nsIAuthPrompt2) || iid.equals(Ci.nsIAuthPrompt)) {
+ try {
+ let pwmgr = Cc["@mozilla.org/passwordmanager/authpromptfactory;1"].getService(Ci.nsIPromptFactory);
+ return pwmgr.getPrompt(domWin, iid);
+ } catch (e) {
+ Cu.reportError("nsPrompter: Delegation to password manager failed: " + e);
+ }
+ }
+
+ let p = new InternalPrompt(domWin);
+ p.QueryInterface(iid);
+ return p;
+ },
+
+ /* ---------- private memebers ---------- */
+
+ // nsIPromptService and nsIPromptService2 methods proxy to our Prompt class
+ callProxy: function(aMethod, aArguments) {
+ let prompt;
+ let domWin = aArguments[0];
+ prompt = new InternalPrompt(domWin);
+ return prompt[aMethod].apply(prompt, Array.prototype.slice.call(aArguments, 1));
+ },
+
+ /* ---------- nsIPromptService ---------- */
+
+ alert: function() {
+ return this.callProxy("alert", arguments);
+ },
+ alertCheck: function() {
+ return this.callProxy("alertCheck", arguments);
+ },
+ confirm: function() {
+ return this.callProxy("confirm", arguments);
+ },
+ confirmCheck: function() {
+ return this.callProxy("confirmCheck", arguments);
+ },
+ confirmEx: function() {
+ return this.callProxy("confirmEx", arguments);
+ },
+ prompt: function() {
+ return this.callProxy("prompt", arguments);
+ },
+ promptUsernameAndPassword: function() {
+ return this.callProxy("promptUsernameAndPassword", arguments);
+ },
+ promptPassword: function() {
+ return this.callProxy("promptPassword", arguments);
+ },
+ select: function() {
+ return this.callProxy("select", arguments);
+ },
+
+ /* ---------- nsIPromptService2 ---------- */
+ promptAuth: function() {
+ return this.callProxy("promptAuth", arguments);
+ },
+ asyncPromptAuth: function() {
+ return this.callProxy("asyncPromptAuth", arguments);
+ }
+};
+
+function InternalPrompt(aDomWin) {
+ this._domWin = aDomWin;
+}
+
+InternalPrompt.prototype = {
+ _domWin: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt, Ci.nsIAuthPrompt, Ci.nsIAuthPrompt2]),
+
+ /* ---------- internal methods ---------- */
+ _getPrompt: function _getPrompt(aTitle, aText, aButtons, aCheckMsg, aCheckState) {
+ let p = new Prompt({
+ window: this._domWin,
+ title: aTitle,
+ message: aText,
+ buttons: aButtons || [
+ PromptUtils.getLocaleString("OK"),
+ PromptUtils.getLocaleString("Cancel")
+ ]
+ });
+ return p;
+ },
+
+ addCheckbox: function addCheckbox(aPrompt, aCheckMsg, aCheckState) {
+ // Don't bother to check for aCheckSate. For nsIPomptService interfaces, aCheckState is an
+ // out param and is required to be defined. If we've gotten here without it, something
+ // has probably gone wrong and we should fail
+ if (aCheckMsg) {
+ aPrompt.addCheckbox({
+ label: PromptUtils.cleanUpLabel(aCheckMsg),
+ checked: aCheckState.value
+ });
+ }
+
+ return aPrompt;
+ },
+
+ addTextbox: function(prompt, value, autofocus, hint) {
+ prompt.addTextbox({
+ value: (value !== null) ? value : "",
+ autofocus: autofocus,
+ hint: hint
+ });
+ },
+
+ addPassword: function(prompt, value, autofocus, hint) {
+ prompt.addPassword({
+ value: (value !== null) ? value : "",
+ autofocus: autofocus,
+ hint: hint
+ });
+ },
+
+ /* Shows a native prompt, and then spins the event loop for this thread while we wait
+ * for a response
+ */
+ showPrompt: function showPrompt(aPrompt) {
+ if (this._domWin) {
+ PromptUtils.fireDialogEvent(this._domWin, "DOMWillOpenModalDialog");
+ let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.enterModalState();
+ }
+
+ let retval = null;
+ aPrompt.show(function(data) {
+ retval = data;
+ });
+
+ // Spin this thread while we wait for a result
+ let thread = Services.tm.currentThread;
+ while (retval == null)
+ thread.processNextEvent(true);
+
+ if (this._domWin) {
+ let winUtils = this._domWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.leaveModalState();
+ PromptUtils.fireDialogEvent(this._domWin, "DOMModalDialogClosed");
+ }
+
+ return retval;
+ },
+
+ /*
+ * ---------- interface disambiguation ----------
+ *
+ * XXX Copied from nsPrompter.js.
+ *
+ * nsIPrompt and nsIAuthPrompt share 3 method names with slightly
+ * different arguments. All but prompt() have the same number of
+ * arguments, so look at the arg types to figure out how we're being
+ * called. :-(
+ */
+ prompt: function prompt() {
+ if (gPromptService.inContentProcess)
+ return gPromptService.callProxy("prompt", [null].concat(Array.prototype.slice.call(arguments)));
+
+ // also, the nsIPrompt flavor has 5 args instead of 6.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_prompt.apply(this, arguments);
+ else
+ return this.nsIAuthPrompt_prompt.apply(this, arguments);
+ },
+
+ promptUsernameAndPassword: function promptUsernameAndPassword() {
+ // Both have 6 args, so use types.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_promptUsernameAndPassword.apply(this, arguments);
+ else
+ return this.nsIAuthPrompt_promptUsernameAndPassword.apply(this, arguments);
+ },
+
+ promptPassword: function promptPassword() {
+ // Both have 5 args, so use types.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_promptPassword.apply(this, arguments);
+ else
+ return this.nsIAuthPrompt_promptPassword.apply(this, arguments);
+ },
+
+ /* ---------- nsIPrompt ---------- */
+
+ alert: function alert(aTitle, aText) {
+ let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ]);
+ p.setHint("alert");
+ this.showPrompt(p);
+ },
+
+ alertCheck: function alertCheck(aTitle, aText, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ]);
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ },
+
+ confirm: function confirm(aTitle, aText) {
+ let p = this._getPrompt(aTitle, aText);
+ p.setHint("confirm");
+ let data = this.showPrompt(p);
+ return (data.button == 0);
+ },
+
+ confirmCheck: function confirmCheck(aTitle, aText, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, null);
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+ let ok = data.button == 0;
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ return ok;
+ },
+
+ confirmEx: function confirmEx(aTitle, aText, aButtonFlags, aButton0,
+ aButton1, aButton2, aCheckMsg, aCheckState) {
+ let buttons = [];
+ let titles = [aButton0, aButton1, aButton2];
+ for (let i = 0; i < 3; i++) {
+ let bTitle = null;
+ switch (aButtonFlags & 0xff) {
+ case Ci.nsIPromptService.BUTTON_TITLE_OK :
+ bTitle = PromptUtils.getLocaleString("OK");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_CANCEL :
+ bTitle = PromptUtils.getLocaleString("Cancel");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_YES :
+ bTitle = PromptUtils.getLocaleString("Yes");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_NO :
+ bTitle = PromptUtils.getLocaleString("No");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_SAVE :
+ bTitle = PromptUtils.getLocaleString("Save");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_DONT_SAVE :
+ bTitle = PromptUtils.getLocaleString("DontSave");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_REVERT :
+ bTitle = PromptUtils.getLocaleString("Revert");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_IS_STRING :
+ bTitle = PromptUtils.cleanUpLabel(titles[i]);
+ break;
+ }
+
+ if (bTitle)
+ buttons.push(bTitle);
+
+ aButtonFlags >>= 8;
+ }
+
+ let p = this._getPrompt(aTitle, aText, buttons);
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ return data.button;
+ },
+
+ nsIPrompt_prompt: function nsIPrompt_prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, null, aCheckMsg, aCheckState);
+ p.setHint("prompt");
+ this.addTextbox(p, aValue.value, true);
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+
+ let ok = data.button == 0;
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ if (ok)
+ aValue.value = data.textbox0;
+ return ok;
+ },
+
+ nsIPrompt_promptPassword: function nsIPrompt_promptPassword(
+ aTitle, aText, aPassword, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, null);
+ this.addPassword(p, aPassword.value, true, PromptUtils.getLocaleString("password", "passwdmgr"));
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+
+ let ok = data.button == 0;
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+ if (ok)
+ aPassword.value = data.password0;
+ return ok;
+ },
+
+ nsIPrompt_promptUsernameAndPassword: function nsIPrompt_promptUsernameAndPassword(
+ aTitle, aText, aUsername, aPassword, aCheckMsg, aCheckState) {
+ let p = this._getPrompt(aTitle, aText, null);
+ this.addTextbox(p, aUsername.value, true, PromptUtils.getLocaleString("username", "passwdmgr"));
+ this.addPassword(p, aPassword.value, false, PromptUtils.getLocaleString("password", "passwdmgr"));
+ this.addCheckbox(p, aCheckMsg, aCheckState);
+ let data = this.showPrompt(p);
+
+ let ok = data.button == 0;
+ if (aCheckState && data.button > -1)
+ aCheckState.value = data.checkbox0;
+
+ if (ok) {
+ aUsername.value = data.textbox0;
+ aPassword.value = data.password0;
+ }
+ return ok;
+ },
+
+ select: function select(aTitle, aText, aCount, aSelectList, aOutSelection) {
+ let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ]);
+ p.addMenulist({ values: aSelectList });
+ let data = this.showPrompt(p);
+
+ let ok = data.button == 0;
+ if (ok)
+ aOutSelection.value = data.menulist0;
+
+ return ok;
+ },
+
+ /* ---------- nsIAuthPrompt ---------- */
+
+ nsIAuthPrompt_prompt : function (title, text, passwordRealm, savePassword, defaultText, result) {
+ // TODO: Port functions from nsLoginManagerPrompter.js to here
+ if (defaultText)
+ result.value = defaultText;
+ return this.nsIPrompt_prompt(title, text, result, null, {});
+ },
+
+ nsIAuthPrompt_promptUsernameAndPassword : function(aTitle, aText, aPasswordRealm, aSavePassword, aUser, aPass) {
+ return this.nsIAuthPrompt_loginPrompt(aTitle, aText, aPasswordRealm, aSavePassword, aUser, aPass);
+ },
+
+ nsIAuthPrompt_promptPassword : function(aTitle, aText, aPasswordRealm, aSavePassword, aPass) {
+ return this.nsIAuthPrompt_loginPrompt(aTitle, aText, aPasswordRealm, aSavePassword, null, aPass);
+ },
+
+ nsIAuthPrompt_loginPrompt: function(aTitle, aPasswordRealm, aSavePassword, aUser, aPass) {
+ let checkMsg = null;
+ let check = { value: false };
+ let hostname, realm;
+ [hostname, realm, aUser] = PromptUtils.getHostnameAndRealm(aPasswordRealm);
+
+ let canSave = PromptUtils.canSaveLogin(hostname, aSavePassword);
+ if (canSave) {
+ // Look for existing logins.
+ let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, realm);
+ [checkMsg, check] = PromptUtils.getUsernameAndPassword(foundLogins, aUser, aPass);
+ }
+
+ // (eslint-disable: see bug 1177904)
+ let ok = false;
+ if (aUser)
+ ok = this.nsIPrompt_promptUsernameAndPassword(aTitle, aText, aUser, aPass, checkMsg, check); // eslint-disable-line no-undef
+ else
+ ok = this.nsIPrompt_promptPassword(aTitle, aText, aPass, checkMsg, check); // eslint-disable-line no-undef
+
+ if (ok && canSave && check.value)
+ PromptUtils.savePassword(hostname, realm, aUser, aPass);
+
+ return ok;
+ },
+
+ /* ---------- nsIAuthPrompt2 ---------- */
+
+ promptAuth: function promptAuth(aChannel, aLevel, aAuthInfo) {
+ let checkMsg = null;
+ let check = { value: false };
+ let message = PromptUtils.makeDialogText(aChannel, aAuthInfo);
+ let [username, password] = PromptUtils.getAuthInfo(aAuthInfo);
+ let [hostname, httpRealm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo);
+ let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, httpRealm);
+
+ let canSave = PromptUtils.canSaveLogin(hostname, null);
+ if (canSave)
+ [checkMsg, check] = PromptUtils.getUsernameAndPassword(foundLogins, username, password);
+
+ if (username.value && password.value) {
+ PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value);
+ }
+
+ let canAutologin = false;
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
+ !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
+ Services.prefs.getBoolPref("signon.autologin.proxy"))
+ canAutologin = true;
+
+ let ok = canAutologin;
+ if (!ok && aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD)
+ ok = this.nsIPrompt_promptPassword(null, message, password, checkMsg, check);
+ else if (!ok)
+ ok = this.nsIPrompt_promptUsernameAndPassword(null, message, username, password, checkMsg, check);
+
+ PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value);
+
+ if (ok && canSave && check.value)
+ PromptUtils.savePassword(foundLogins, username, password, hostname, httpRealm);
+
+ return ok;
+ },
+
+ _asyncPrompts: {},
+ _asyncPromptInProgress: false,
+
+ _doAsyncPrompt : function() {
+ if (this._asyncPromptInProgress)
+ return;
+
+ // Find the first prompt key we have in the queue
+ let hashKey = null;
+ for (hashKey in this._asyncPrompts)
+ break;
+
+ if (!hashKey)
+ return;
+
+ // If login manger has logins for this host, defer prompting if we're
+ // already waiting on a master password entry.
+ let prompt = this._asyncPrompts[hashKey];
+ let prompter = prompt.prompter;
+ let [hostname, httpRealm] = PromptUtils.getAuthTarget(prompt.channel, prompt.authInfo);
+ let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, httpRealm);
+ if (foundLogins.length > 0 && PromptUtils.pwmgr.uiBusy)
+ return;
+
+ this._asyncPromptInProgress = true;
+ prompt.inProgress = true;
+
+ let self = this;
+
+ let runnable = {
+ run: function() {
+ let ok = false;
+ try {
+ ok = prompter.promptAuth(prompt.channel, prompt.level, prompt.authInfo);
+ } catch (e) {
+ Cu.reportError("_doAsyncPrompt:run: " + e + "\n");
+ }
+
+ delete self._asyncPrompts[hashKey];
+ prompt.inProgress = false;
+ self._asyncPromptInProgress = false;
+
+ for (let consumer of prompt.consumers) {
+ if (!consumer.callback)
+ // Not having a callback means that consumer didn't provide it
+ // or canceled the notification
+ continue;
+
+ try {
+ if (ok)
+ consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
+ else
+ consumer.callback.onAuthCancelled(consumer.context, true);
+ } catch (e) { /* Throw away exceptions caused by callback */ }
+ }
+ self._doAsyncPrompt();
+ }
+ }
+
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ asyncPromptAuth: function asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) {
+ let cancelable = null;
+ try {
+ // If the user submits a login but it fails, we need to remove the
+ // notification bar that was displayed. Conveniently, the user will
+ // be prompted for authentication again, which brings us here.
+ //this._removeLoginNotifications();
+
+ cancelable = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ callback: aCallback,
+ context: aContext,
+ cancel: function() {
+ this.callback.onAuthCancelled(this.context, false);
+ this.callback = null;
+ this.context = null;
+ }
+ };
+ let [hostname, httpRealm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo);
+ let hashKey = aLevel + "|" + hostname + "|" + httpRealm;
+ let asyncPrompt = this._asyncPrompts[hashKey];
+ if (asyncPrompt) {
+ asyncPrompt.consumers.push(cancelable);
+ return cancelable;
+ }
+
+ asyncPrompt = {
+ consumers: [cancelable],
+ channel: aChannel,
+ authInfo: aAuthInfo,
+ level: aLevel,
+ inProgress : false,
+ prompter: this
+ }
+
+ this._asyncPrompts[hashKey] = asyncPrompt;
+ this._doAsyncPrompt();
+ } catch (e) {
+ Cu.reportError("PromptService: " + e + "\n");
+ throw e;
+ }
+ return cancelable;
+ }
+};
+
+var PromptUtils = {
+ getLocaleString: function pu_getLocaleString(aKey, aService) {
+ if (aService == "passwdmgr")
+ return this.cleanUpLabel(this.passwdBundle.GetStringFromName(aKey));
+
+ return this.cleanUpLabel(this.bundle.GetStringFromName(aKey));
+ },
+
+ //
+ // Copied from chrome://global/content/commonDialog.js
+ //
+ cleanUpLabel: function cleanUpLabel(aLabel) {
+ // This is for labels which may contain embedded access keys.
+ // If we end in (&X) where X represents the access key, optionally preceded
+ // by spaces and/or followed by the ':' character,
+ // remove the access key placeholder + leading spaces from the label.
+ // Otherwise a character preceded by one but not two &s is the access key.
+
+ // Note that if you change the following code, see the comment of
+ // nsTextBoxFrame::UpdateAccessTitle.
+ if (!aLabel)
+ return "";
+
+ if (/ *\(\&([^&])\)(:?)$/.test(aLabel)) {
+ aLabel = RegExp.leftContext + RegExp.$2;
+ } else if (/^([^&]*)\&(([^&]).*$)/.test(aLabel)) {
+ aLabel = RegExp.$1 + RegExp.$2;
+ }
+
+ // Special code for using that & symbol
+ aLabel = aLabel.replace(/\&\&/g, "&");
+
+ return aLabel;
+ },
+
+ get pwmgr() {
+ delete this.pwmgr;
+ return this.pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+ },
+
+ getHostnameAndRealm: function pu_getHostnameAndRealm(aRealmString) {
+ let httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString))
+ return [null, null, null];
+
+ let uri = Services.io.newURI(aRealmString, null, null);
+ let pathname = "";
+
+ if (uri.path != "/")
+ pathname = uri.path;
+
+ let formattedHostname = this._getFormattedHostname(uri);
+ return [formattedHostname, formattedHostname + pathname, uri.username];
+ },
+
+ canSaveLogin: function pu_canSaveLogin(aHostname, aSavePassword) {
+ let canSave = !this._inPrivateBrowsing && this.pwmgr.getLoginSavingEnabled(aHostname)
+ if (aSavePassword)
+ canSave = canSave && (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY)
+ return canSave;
+ },
+
+ getUsernameAndPassword: function pu_getUsernameAndPassword(aFoundLogins, aUser, aPass) {
+ let checkLabel = null;
+ let check = { value: false };
+ let selectedLogin;
+
+ checkLabel = this.getLocaleString("rememberButton", "passwdmgr");
+
+ // XXX Like the original code, we can't deal with multiple
+ // account selection. (bug 227632)
+ if (aFoundLogins.length > 0) {
+ selectedLogin = aFoundLogins[0];
+
+ // If the caller provided a username, try to use it. If they
+ // provided only a password, this will try to find a password-only
+ // login (or return null if none exists).
+ if (aUser.value)
+ selectedLogin = this.findLogin(aFoundLogins, "username", aUser.value);
+
+ if (selectedLogin) {
+ check.value = true;
+ aUser.value = selectedLogin.username;
+ // If the caller provided a password, prefer it.
+ if (!aPass.value)
+ aPass.value = selectedLogin.password;
+ }
+ }
+
+ return [checkLabel, check];
+ },
+
+ findLogin: function pu_findLogin(aLogins, aName, aValue) {
+ for (let i = 0; i < aLogins.length; i++)
+ if (aLogins[i][aName] == aValue)
+ return aLogins[i];
+ return null;
+ },
+
+ savePassword: function pu_savePassword(aLogins, aUser, aPass, aHostname, aRealm) {
+ let selectedLogin = this.findLogin(aLogins, "username", aUser.value);
+
+ // If we didn't find an existing login, or if the username
+ // changed, save as a new login.
+ if (!selectedLogin) {
+ // add as new
+ var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ newLogin.init(aHostname, null, aRealm, aUser.value, aPass.value, "", "");
+ this.pwmgr.addLogin(newLogin);
+ } else if (aPass.value != selectedLogin.password) {
+ // update password
+ this.updateLogin(selectedLogin, aPass.value);
+ } else {
+ this.updateLogin(selectedLogin);
+ }
+ },
+
+ updateLogin: function pu_updateLogin(aLogin, aPassword) {
+ let now = Date.now();
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag);
+ if (aPassword) {
+ propBag.setProperty("password", aPassword);
+ // Explicitly set the password change time here (even though it would
+ // be changed automatically), to ensure that it's exactly the same
+ // value as timeLastUsed.
+ propBag.setProperty("timePasswordChanged", now);
+ }
+ propBag.setProperty("timeLastUsed", now);
+ propBag.setProperty("timesUsedIncrement", 1);
+
+ this.pwmgr.modifyLogin(aLogin, propBag);
+ },
+
+ // JS port of http://mxr.mozilla.org/mozilla-central/source/embedding/components/windowwatcher/nsPrompt.cpp#388
+ makeDialogText: function pu_makeDialogText(aChannel, aAuthInfo) {
+ let isProxy = (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY);
+ let isPassOnly = (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD);
+ let isCrossOrig = (aAuthInfo.flags &
+ Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE);
+
+ let username = aAuthInfo.username;
+ let [displayHost, realm] = this.getAuthTarget(aChannel, aAuthInfo);
+
+ // Suppress "the site says: $realm" when we synthesized a missing realm.
+ if (!aAuthInfo.realm && !isProxy)
+ realm = "";
+
+ // Trim obnoxiously long realms.
+ if (realm.length > 150) {
+ realm = realm.substring(0, 150);
+ // Append "..." (or localized equivalent).
+ realm += this.ellipsis;
+ }
+
+ let text;
+ if (isProxy) {
+ text = this.bundle.formatStringFromName("EnterLoginForProxy3", [realm, displayHost], 2);
+ } else if (isPassOnly) {
+ text = this.bundle.formatStringFromName("EnterPasswordFor", [username, displayHost], 2);
+ } else if (isCrossOrig) {
+ text = this.bundle.formatStringFromName("EnterUserPasswordForCrossOrigin2", [displayHost], 1);
+ } else if (!realm) {
+ text = this.bundle.formatStringFromName("EnterUserPasswordFor2", [displayHost], 1);
+ } else {
+ text = this.bundle.formatStringFromName("EnterLoginForRealm3", [realm, displayHost], 2);
+ }
+
+ return text;
+ },
+
+ // JS port of http://mxr.mozilla.org/mozilla-central/source/embedding/components/windowwatcher/nsPromptUtils.h#89
+ getAuthHostPort: function pu_getAuthHostPort(aChannel, aAuthInfo) {
+ let uri = aChannel.URI;
+ let res = { host: null, port: -1 };
+ if (aAuthInfo.flags & aAuthInfo.AUTH_PROXY) {
+ let proxy = aChannel.QueryInterface(Ci.nsIProxiedChannel);
+ res.host = proxy.proxyInfo.host;
+ res.port = proxy.proxyInfo.port;
+ } else {
+ res.host = uri.host;
+ res.port = uri.port;
+ }
+ return res;
+ },
+
+ getAuthTarget : function pu_getAuthTarget(aChannel, aAuthInfo) {
+ let hostname, realm;
+ // If our proxy is demanding authentication, don't use the
+ // channel's actual destination.
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
+ if (!(aChannel instanceof Ci.nsIProxiedChannel))
+ throw "proxy auth needs nsIProxiedChannel";
+
+ let info = aChannel.proxyInfo;
+ if (!info)
+ throw "proxy auth needs nsIProxyInfo";
+
+ // Proxies don't have a scheme, but we'll use "moz-proxy://"
+ // so that it's more obvious what the login is for.
+ let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
+ hostname = "moz-proxy://" + idnService.convertUTF8toACE(info.host) + ":" + info.port;
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ }
+ hostname = this.getFormattedHostname(aChannel.URI);
+
+ // If a HTTP WWW-Authenticate header specified a realm, that value
+ // will be available here. If it wasn't set or wasn't HTTP, we'll use
+ // the formatted hostname instead.
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ },
+
+ getAuthInfo : function pu_getAuthInfo(aAuthInfo) {
+ let flags = aAuthInfo.flags;
+ let username = {value: ""};
+ let password = {value: ""};
+
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain)
+ username.value = aAuthInfo.domain + "\\" + aAuthInfo.username;
+ else
+ username.value = aAuthInfo.username;
+
+ password.value = aAuthInfo.password
+
+ return [username, password];
+ },
+
+ setAuthInfo : function (aAuthInfo, username, password) {
+ var flags = aAuthInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
+ // Domain is separated from username by a backslash
+ var idx = username.indexOf("\\");
+ if (idx == -1) {
+ aAuthInfo.username = username;
+ } else {
+ aAuthInfo.domain = username.substring(0, idx);
+ aAuthInfo.username = username.substring(idx+1);
+ }
+ } else {
+ aAuthInfo.username = username;
+ }
+ aAuthInfo.password = password;
+ },
+
+ /**
+ * Strip out things like userPass and path for display.
+ */
+ getFormattedHostname : function pu_getFormattedHostname(uri) {
+ return uri.scheme + "://" + uri.hostPort;
+ },
+
+ fireDialogEvent: function(aDomWin, aEventName) {
+ // accessing the document object can throw if this window no longer exists. See bug 789888.
+ try {
+ if (!aDomWin.document)
+ return;
+ let event = aDomWin.document.createEvent("Events");
+ event.initEvent(aEventName, true, true);
+ let winUtils = aDomWin.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.dispatchEventToChromeOnly(aDomWin, event);
+ } catch(ex) {
+ }
+ }
+};
+
+XPCOMUtils.defineLazyGetter(PromptUtils, "passwdBundle", function () {
+ return Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+});
+
+XPCOMUtils.defineLazyGetter(PromptUtils, "bundle", function () {
+ return Services.strings.createBundle("chrome://global/locale/commonDialogs.properties");
+});
+
+
+// Factory for wrapping nsIAuthPrompt interfaces to make them usable via an nsIAuthPrompt2 interface.
+// XXX Copied from nsPrompter.js.
+function AuthPromptAdapterFactory() {
+}
+
+AuthPromptAdapterFactory.prototype = {
+ classID: Components.ID("{80dae1e9-e0d2-4974-915f-f97050fa8068}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPromptAdapterFactory]),
+
+ /* ---------- nsIAuthPromptAdapterFactory ---------- */
+
+ createAdapter: function(aPrompt) {
+ return new AuthPromptAdapter(aPrompt);
+ }
+};
+
+
+// Takes an nsIAuthPrompt implementation, wraps it with a nsIAuthPrompt2 shell.
+// XXX Copied from nsPrompter.js.
+function AuthPromptAdapter(aPrompt) {
+ this.prompt = aPrompt;
+}
+
+AuthPromptAdapter.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAuthPrompt2]),
+ prompt: null,
+
+ /* ---------- nsIAuthPrompt2 ---------- */
+
+ promptAuth: function(aChannel, aLevel, aAuthInfo, aCheckLabel, aCheckValue) {
+ let message = PromptUtils.makeDialogText(aChannel, aAuthInfo);
+
+ let [username, password] = PromptUtils.getAuthInfo(aAuthInfo);
+ let [host, realm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo);
+ let authTarget = host + " (" + realm + ")";
+
+ let ok;
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) {
+ ok = this.prompt.promptPassword(null, message, authTarget, Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, password);
+ } else {
+ ok = this.prompt.promptUsernameAndPassword(null, message, authTarget, Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, username, password);
+ }
+
+ if (ok) {
+ PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value);
+ }
+ return ok;
+ },
+
+ asyncPromptAuth: function(aChannel, aCallback, aContext, aLevel, aAuthInfo, aCheckLabel, aCheckValue) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PromptService, AuthPromptAdapterFactory]);
diff --git a/mobile/android/components/SessionStore.idl b/mobile/android/components/SessionStore.idl
new file mode 100644
index 000000000..14ddd5834
--- /dev/null
+++ b/mobile/android/components/SessionStore.idl
@@ -0,0 +1,86 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMWindow;
+interface nsIDOMNode;
+
+/**
+ * nsISessionStore keeps track of the current browsing state.
+ *
+ * The nsISessionStore API operates mostly on browser windows and the browser
+ * tabs contained in them.
+ */
+
+[scriptable, uuid(da9ffc70-d444-47d4-b4ab-df3fb0fd24d0)]
+interface nsISessionStore : nsISupports
+{
+ /**
+ * Get the current browsing state.
+ * @returns a JSON string representing the session state.
+ */
+ AString getBrowserState();
+
+ /**
+ * Get the number of restore-able tabs for a browser window
+ */
+ unsigned long getClosedTabCount(in nsIDOMWindow aWindow);
+
+ /**
+ * Get closed tab data
+ *
+ * @param aWindow is the browser window for which to get closed tab data
+ * @returns a JS array of closed tabs.
+ */
+ jsval getClosedTabs(in nsIDOMWindow aWindow);
+
+ /**
+ * @param aWindow is the browser window to reopen a closed tab in.
+ * @param aCloseTabData is the data of the tab to be restored.
+ * @returns a reference to the reopened tab.
+ */
+ nsIDOMNode undoCloseTab(in nsIDOMWindow aWindow, in jsval aCloseTabData);
+
+ /**
+ * @param aWindow is the browser window associated with the closed tab.
+ * @param aIndex is the index of the closed tab to be removed (FIFO ordered).
+ */
+ nsIDOMNode forgetClosedTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
+
+ /**
+ * @param aTab is the browser tab to get the value for.
+ * @param aKey is the value's name.
+ *
+ * @returns A string value or an empty string if none is set.
+ */
+ AString getTabValue(in jsval aTab, in AString aKey);
+
+ /**
+ * @param aTab is the browser tab to set the value for.
+ * @param aKey is the value's name.
+ * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects).
+ */
+ void setTabValue(in jsval aTab, in AString aKey, in AString aStringValue);
+
+ /**
+ * @param aTab is the browser tab to get the value for.
+ * @param aKey is the value's name.
+ */
+ void deleteTabValue(in jsval aTab, in AString aKey);
+
+ /**
+ * Restores the previous browser session using a fast, lightweight strategy
+ * @param aSessionString The session string to restore from. If null, the
+ * backup session file is read from.
+ */
+ void restoreLastSession(in AString aSessionString);
+
+ /**
+ * Removes a window from the current session history. Data from this window
+ * won't be saved when its closed.
+ * @param aWindow The window to remove
+ */
+ void removeWindow(in nsIDOMWindow aWindow);
+};
diff --git a/mobile/android/components/SessionStore.js b/mobile/android/components/SessionStore.js
new file mode 100644
index 000000000..18ac6bf94
--- /dev/null
+++ b/mobile/android/components/SessionStore.js
@@ -0,0 +1,1794 @@
+/* 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/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormData", "resource://gre/modules/FormData.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", "resource://gre/modules/ScrollPosition.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
+XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences", "resource://gre/modules/SharedPreferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Utils", "resource://gre/modules/sessionstore/Utils.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "serializationHelper",
+ "@mozilla.org/network/serialization-helper;1",
+ "nsISerializationHelper");
+
+function dump(a) {
+ Services.console.logStringMessage(a);
+}
+
+let loggingEnabled = false;
+
+function log(a) {
+ if (!loggingEnabled) {
+ return;
+ }
+ Log.d("SessionStore", a);
+}
+
+// -----------------------------------------------------------------------
+// Session Store
+// -----------------------------------------------------------------------
+
+const STATE_STOPPED = 0;
+const STATE_RUNNING = 1;
+const STATE_QUITTING = -1;
+const STATE_QUITTING_FLUSHED = -2;
+
+const PRIVACY_NONE = 0;
+const PRIVACY_ENCRYPTED = 1;
+const PRIVACY_FULL = 2;
+
+const PREFS_RESTORE_FROM_CRASH = "browser.sessionstore.resume_from_crash";
+const PREFS_MAX_CRASH_RESUMES = "browser.sessionstore.max_resumed_crashes";
+
+const MINIMUM_SAVE_DELAY = 2000;
+// We reduce the delay in background because we could be killed at any moment,
+// however we don't set it to 0 in order to allow for multiple events arriving
+// one after the other to be batched together in one write operation.
+const MINIMUM_SAVE_DELAY_BACKGROUND = 200;
+
+function SessionStore() { }
+
+SessionStore.prototype = {
+ classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
+ Ci.nsIDOMEventListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ _windows: {},
+ _lastSaveTime: 0,
+ _lastBackupTime: 0,
+ _interval: 10000,
+ _backupInterval: 120000, // 2 minutes
+ _minSaveDelay: MINIMUM_SAVE_DELAY,
+ _maxTabsUndo: 5,
+ _pendingWrite: 0,
+ _scrollSavePending: null,
+ _writeInProgress: false,
+
+ // We only want to start doing backups if we've successfully
+ // written the session data at least once.
+ _sessionDataIsGood: false,
+
+ // The index where the most recently closed tab was in the tabs array
+ // when it was closed.
+ _lastClosedTabIndex: -1,
+
+ // Whether or not to send notifications for changes to the closed tabs.
+ _notifyClosedTabs: false,
+
+ // If we're simultaneously closing both a tab and Firefox, we don't want
+ // to bother reloading the newly selected tab if it is zombified.
+ // The Java UI will tell us which tab to watch out for.
+ _keepAsZombieTabId: -1,
+
+ init: function ss_init() {
+ loggingEnabled = Services.prefs.getBoolPref("browser.sessionstore.debug_logging");
+
+ // Get file references
+ this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ this._sessionFileBackup = this._sessionFile.clone();
+ this._sessionFilePrevious = this._sessionFile.clone();
+ this._sessionFileTemp = this._sessionFile.clone();
+ this._sessionFile.append("sessionstore.js"); // The main session store save file.
+ this._sessionFileBackup.append("sessionstore.bak"); // A backup copy to guard against interrupted writes.
+ this._sessionFilePrevious.append("sessionstore.old"); // The previous session's file, used for what used to be the "Tabs from last time".
+ this._sessionFileTemp.append(this._sessionFile.leafName + ".tmp"); // Temporary file for writing changes to disk.
+
+ this._loadState = STATE_STOPPED;
+ this._startupRestoreFinished = false;
+
+ this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
+ this._backupInterval = Services.prefs.getIntPref("browser.sessionstore.backupInterval");
+ this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+
+ // Copy changes in Gecko settings to their Java counterparts,
+ // so the startup code can access them
+ Services.prefs.addObserver(PREFS_RESTORE_FROM_CRASH, function() {
+ SharedPreferences.forApp().setBoolPref(PREFS_RESTORE_FROM_CRASH,
+ Services.prefs.getBoolPref(PREFS_RESTORE_FROM_CRASH));
+ }, false);
+ Services.prefs.addObserver(PREFS_MAX_CRASH_RESUMES, function() {
+ SharedPreferences.forApp().setIntPref(PREFS_MAX_CRASH_RESUMES,
+ Services.prefs.getIntPref(PREFS_MAX_CRASH_RESUMES));
+ }, false);
+ },
+
+ _clearDisk: function ss_clearDisk() {
+ this._sessionDataIsGood = false;
+
+ if (this._loadState > STATE_QUITTING) {
+ OS.File.remove(this._sessionFile.path);
+ OS.File.remove(this._sessionFileBackup.path);
+ OS.File.remove(this._sessionFilePrevious.path);
+ OS.File.remove(this._sessionFileTemp.path);
+ } else { // We're shutting down and must delete synchronously
+ if (this._sessionFile.exists()) { this._sessionFile.remove(false); }
+ if (this._sessionFileBackup.exists()) { this._sessionFileBackup.remove(false); }
+ if (this._sessionFileBackup.exists()) { this._sessionFilePrevious.remove(false); }
+ if (this._sessionFileBackup.exists()) { this._sessionFileTemp.remove(false); }
+ }
+ },
+
+ observe: function ss_observe(aSubject, aTopic, aData) {
+ let self = this;
+ let observerService = Services.obs;
+ switch (aTopic) {
+ case "app-startup":
+ observerService.addObserver(this, "final-ui-startup", true);
+ observerService.addObserver(this, "domwindowopened", true);
+ observerService.addObserver(this, "domwindowclosed", true);
+ observerService.addObserver(this, "browser:purge-session-history", true);
+ observerService.addObserver(this, "quit-application-requested", true);
+ observerService.addObserver(this, "quit-application-proceeding", true);
+ observerService.addObserver(this, "quit-application", true);
+ observerService.addObserver(this, "Session:Restore", true);
+ observerService.addObserver(this, "Session:NotifyLocationChange", true);
+ observerService.addObserver(this, "Tab:KeepZombified", true);
+ observerService.addObserver(this, "application-background", true);
+ observerService.addObserver(this, "application-foreground", true);
+ observerService.addObserver(this, "ClosedTabs:StartNotifications", true);
+ observerService.addObserver(this, "ClosedTabs:StopNotifications", true);
+ observerService.addObserver(this, "last-pb-context-exited", true);
+ observerService.addObserver(this, "Session:RestoreRecentTabs", true);
+ observerService.addObserver(this, "Tabs:OpenMultiple", true);
+ break;
+ case "final-ui-startup":
+ observerService.removeObserver(this, "final-ui-startup");
+ this.init();
+ break;
+ case "domwindowopened": {
+ let window = aSubject;
+ window.addEventListener("load", function() {
+ self.onWindowOpen(window);
+ window.removeEventListener("load", arguments.callee, false);
+ }, false);
+ break;
+ }
+ case "domwindowclosed": // catch closed windows
+ this.onWindowClose(aSubject);
+ break;
+ case "quit-application-requested":
+ log("quit-application-requested");
+ // Get a current snapshot of all windows
+ if (this._pendingWrite) {
+ this._forEachBrowserWindow(function(aWindow) {
+ self._collectWindowData(aWindow);
+ });
+ }
+ break;
+ case "quit-application-proceeding":
+ log("quit-application-proceeding");
+ // Freeze the data at what we've got (ignoring closing windows)
+ this._loadState = STATE_QUITTING;
+ break;
+ case "quit-application":
+ log("quit-application");
+ observerService.removeObserver(this, "domwindowopened");
+ observerService.removeObserver(this, "domwindowclosed");
+ observerService.removeObserver(this, "quit-application-requested");
+ observerService.removeObserver(this, "quit-application-proceeding");
+ observerService.removeObserver(this, "quit-application");
+
+ // Flush all pending writes to disk now
+ this.flushPendingState();
+ this._loadState = STATE_QUITTING_FLUSHED;
+
+ break;
+ case "browser:purge-session-history": // catch sanitization
+ log("browser:purge-session-history");
+ this._clearDisk();
+
+ // Clear all data about closed tabs
+ for (let [ssid, win] of Object.entries(this._windows))
+ win.closedTabs = [];
+
+ this._lastClosedTabIndex = -1;
+
+ if (this._loadState == STATE_RUNNING) {
+ // Save the purged state immediately
+ this.saveState();
+ } else if (this._loadState <= STATE_QUITTING) {
+ this.saveStateDelayed();
+ if (this._loadState == STATE_QUITTING_FLUSHED) {
+ this.flushPendingState();
+ }
+ }
+
+ Services.obs.notifyObservers(null, "sessionstore-state-purge-complete", "");
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(Services.wm.getMostRecentWindow("navigator:browser"));
+ }
+ break;
+ case "timer-callback":
+ if (this._loadState == STATE_RUNNING) {
+ // Timer call back for delayed saving
+ this._saveTimer = null;
+ log("timer-callback, pendingWrite = " + this._pendingWrite);
+ if (this._pendingWrite) {
+ this.saveState();
+ }
+ }
+ break;
+ case "Session:Restore": {
+ Services.obs.removeObserver(this, "Session:Restore");
+ if (aData) {
+ // Be ready to handle any restore failures by making sure we have a valid tab opened
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ let restoreCleanup = {
+ observe: function (aSubject, aTopic, aData) {
+ Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored");
+
+ if (window.BrowserApp.tabs.length == 0) {
+ window.BrowserApp.addTab("about:home", {
+ selected: true
+ });
+ }
+ // Normally, _restoreWindow() will have set this to true already,
+ // but we want to make sure it's set even in case of a restore failure.
+ this._startupRestoreFinished = true;
+ log("startupRestoreFinished = true (through notification)");
+ }.bind(this)
+ };
+ Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false);
+
+ // Do a restore, triggered by Java
+ let data = JSON.parse(aData);
+ this.restoreLastSession(data.sessionString);
+ } else {
+ // Not doing a restore; just send restore message
+ this._startupRestoreFinished = true;
+ log("startupRestoreFinished = true");
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
+ }
+ break;
+ }
+ case "Session:NotifyLocationChange": {
+ let browser = aSubject;
+
+ if (browser.__SS_restoreReloadPending && this._startupRestoreFinished) {
+ delete browser.__SS_restoreReloadPending;
+ log("remove restoreReloadPending");
+ }
+
+ if (browser.__SS_restoreDataOnLocationChange) {
+ delete browser.__SS_restoreDataOnLocationChange;
+ this._restoreZoom(browser.__SS_data.scrolldata, browser);
+ }
+ break;
+ }
+ case "Tabs:OpenMultiple": {
+ let data = JSON.parse(aData);
+
+ this._openTabs(data);
+
+ if (data.shouldNotifyTabsOpenedToJava) {
+ Messaging.sendRequest({
+ type: "Tabs:TabsOpened"
+ });
+ }
+ break;
+ }
+ case "Tab:KeepZombified": {
+ if (aData >= 0) {
+ this._keepAsZombieTabId = aData;
+ log("Tab:KeepZombified " + aData);
+ }
+ break;
+ }
+ case "application-background":
+ // We receive this notification when Android's onPause callback is
+ // executed. After onPause, the application may be terminated at any
+ // point without notice; therefore, we must synchronously write out any
+ // pending save state to ensure that this data does not get lost.
+ log("application-background");
+ // Tab events dispatched immediately before the application was backgrounded
+ // might actually arrive after this point, therefore save them without delay.
+ if (this._loadState == STATE_RUNNING) {
+ this._interval = 0;
+ this._minSaveDelay = MINIMUM_SAVE_DELAY_BACKGROUND; // A small delay allows successive tab events to be batched together.
+ this.flushPendingState();
+ }
+ break;
+ case "application-foreground":
+ // Reset minimum interval between session store writes back to default.
+ log("application-foreground");
+ this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
+ this._minSaveDelay = MINIMUM_SAVE_DELAY;
+
+ // If we skipped restoring a zombified tab before backgrounding,
+ // we might have to do it now instead.
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ if (window) { // Might not yet be ready during a cold startup.
+ let tab = window.BrowserApp.selectedTab;
+ if (tab.browser.__SS_restore) {
+ this._restoreZombieTab(tab.browser, tab.id);
+ }
+ }
+ break;
+ case "ClosedTabs:StartNotifications":
+ this._notifyClosedTabs = true;
+ log("ClosedTabs:StartNotifications");
+ this._sendClosedTabsToJava(Services.wm.getMostRecentWindow("navigator:browser"));
+ break;
+ case "ClosedTabs:StopNotifications":
+ this._notifyClosedTabs = false;
+ log("ClosedTabs:StopNotifications");
+ break;
+ case "last-pb-context-exited":
+ // Clear private closed tab data when we leave private browsing.
+ for (let window of Object.values(this._windows)) {
+ window.closedTabs = window.closedTabs.filter(tab => !tab.isPrivate);
+ }
+ this._lastClosedTabIndex = -1;
+ break;
+ case "Session:RestoreRecentTabs": {
+ let data = JSON.parse(aData);
+ this._restoreTabs(data);
+ break;
+ }
+ }
+ },
+
+ handleEvent: function ss_handleEvent(aEvent) {
+ let window = aEvent.currentTarget.ownerDocument.defaultView;
+ switch (aEvent.type) {
+ case "TabOpen": {
+ let browser = aEvent.target;
+ log("TabOpen for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabAdd(window, browser);
+ break;
+ }
+ case "TabClose": {
+ let browser = aEvent.target;
+ log("TabClose for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabClose(window, browser, aEvent.detail);
+ this.onTabRemove(window, browser);
+ break;
+ }
+ case "TabPreZombify": {
+ let browser = aEvent.target;
+ log("TabPreZombify for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabRemove(window, browser, true);
+ break;
+ }
+ case "TabPostZombify": {
+ let browser = aEvent.target;
+ log("TabPostZombify for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabAdd(window, browser, true);
+ break;
+ }
+ case "TabSelect": {
+ let browser = aEvent.target;
+ log("TabSelect for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabSelect(window, browser);
+ break;
+ }
+ case "DOMTitleChanged": {
+ // Use DOMTitleChanged to detect page loads over alternatives.
+ // onLocationChange happens too early, so we don't have the page title
+ // yet; pageshow happens too late, so we could lose session data if the
+ // browser were killed.
+ let browser = aEvent.currentTarget;
+ log("DOMTitleChanged for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabLoad(window, browser);
+ break;
+ }
+ case "load": {
+ let browser = aEvent.currentTarget;
+
+ // Skip subframe loads.
+ if (browser.contentDocument !== aEvent.originalTarget) {
+ return;
+ }
+
+ // Handle restoring the text data into the content and frames.
+ // We wait until the main content and all frames are loaded
+ // before trying to restore this data.
+ log("load for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ if (browser.__SS_restoreDataOnLoad) {
+ delete browser.__SS_restoreDataOnLoad;
+ this._restoreTextData(browser.__SS_data.formdata, browser);
+ }
+ break;
+ }
+ case "pageshow":
+ case "AboutReaderContentReady": {
+ let browser = aEvent.currentTarget;
+
+ // Skip subframe pageshows.
+ if (browser.contentDocument !== aEvent.originalTarget) {
+ return;
+ }
+
+ if (browser.currentURI.spec.startsWith("about:reader") &&
+ !browser.contentDocument.body.classList.contains("loaded")) {
+ // Don't restore the scroll position of an about:reader page at this point;
+ // wait for the custom event dispatched from AboutReader.jsm instead.
+ return;
+ }
+
+ // Restoring the scroll position needs to happen after the zoom level has been
+ // restored, which is done by the MobileViewportManager either on first paint
+ // or on load, whichever comes first.
+ // In the latter case, our load handler runs before the MVM's one, which is the
+ // wrong way around, so we have to use a later event instead.
+ log(aEvent.type + " for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ if (browser.__SS_restoreDataOnPageshow) {
+ delete browser.__SS_restoreDataOnPageshow;
+ this._restoreScrollPosition(browser.__SS_data.scrolldata, browser);
+ } else {
+ // We're not restoring, capture the initial scroll position on pageshow.
+ this.onTabScroll(window, browser);
+ }
+ break;
+ }
+ case "change":
+ case "input":
+ case "DOMAutoComplete": {
+ let browser = aEvent.currentTarget;
+ log("TabInput for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ this.onTabInput(window, browser);
+ break;
+ }
+ case "resize":
+ case "scroll": {
+ let browser = aEvent.currentTarget;
+ // Duplicated logging check to avoid calling getTabForBrowser on each scroll event.
+ if (loggingEnabled) {
+ log(aEvent.type + " for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+ }
+ if (!this._scrollSavePending) {
+ this._scrollSavePending =
+ window.setTimeout(() => {
+ this._scrollSavePending = null;
+ this.onTabScroll(window, browser);
+ }, 500);
+ }
+ break;
+ }
+ }
+ },
+
+ onWindowOpen: function ss_onWindowOpen(aWindow) {
+ // Return if window has already been initialized
+ if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) {
+ return;
+ }
+
+ // Ignore non-browser windows and windows opened while shutting down
+ if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState <= STATE_QUITTING) {
+ return;
+ }
+
+ // Assign it a unique identifier (timestamp) and create its data object
+ aWindow.__SSID = "window" + Date.now();
+ this._windows[aWindow.__SSID] = { tabs: [], selected: 0, closedTabs: [] };
+
+ // Perform additional initialization when the first window is loading
+ if (this._loadState == STATE_STOPPED) {
+ this._loadState = STATE_RUNNING;
+ this._lastSaveTime = Date.now();
+ }
+
+ // Add tab change listeners to all already existing tabs
+ let tabs = aWindow.BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++)
+ this.onTabAdd(aWindow, tabs[i].browser, true);
+
+ // Notification of tab add/remove/selection/zombification
+ let browsers = aWindow.document.getElementById("browsers");
+ browsers.addEventListener("TabOpen", this, true);
+ browsers.addEventListener("TabClose", this, true);
+ browsers.addEventListener("TabSelect", this, true);
+ browsers.addEventListener("TabPreZombify", this, true);
+ browsers.addEventListener("TabPostZombify", this, true);
+ },
+
+ onWindowClose: function ss_onWindowClose(aWindow) {
+ // Ignore windows not tracked by SessionStore
+ if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) {
+ return;
+ }
+
+ let browsers = aWindow.document.getElementById("browsers");
+ browsers.removeEventListener("TabOpen", this, true);
+ browsers.removeEventListener("TabClose", this, true);
+ browsers.removeEventListener("TabSelect", this, true);
+ browsers.removeEventListener("TabPreZombify", this, true);
+ browsers.removeEventListener("TabPostZombify", this, true);
+
+ if (this._loadState == STATE_RUNNING) {
+ // Update all window data for a last time
+ this._collectWindowData(aWindow);
+
+ // Clear this window from the list
+ delete this._windows[aWindow.__SSID];
+
+ // Save the state without this window to disk
+ this.saveStateDelayed();
+ }
+
+ let tabs = aWindow.BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++)
+ this.onTabRemove(aWindow, tabs[i].browser, true);
+
+ delete aWindow.__SSID;
+ },
+
+ onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) {
+ // Use DOMTitleChange to catch the initial load and restore history
+ aBrowser.addEventListener("DOMTitleChanged", this, true);
+
+ // Use load to restore text data
+ aBrowser.addEventListener("load", this, true);
+
+ // Gecko might set the initial zoom level after the JS "load" event,
+ // so we have to restore zoom and scroll position after that.
+ aBrowser.addEventListener("pageshow", this, true);
+ aBrowser.addEventListener("AboutReaderContentReady", this, true);
+
+ // Use a combination of events to watch for text data changes
+ aBrowser.addEventListener("change", this, true);
+ aBrowser.addEventListener("input", this, true);
+ aBrowser.addEventListener("DOMAutoComplete", this, true);
+
+ // Record the current scroll position and zoom level.
+ aBrowser.addEventListener("scroll", this, true);
+ aBrowser.addEventListener("resize", this, true);
+
+ log("onTabAdd() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
+ ", aNoNotification = " + aNoNotification);
+ if (!aNoNotification) {
+ this.saveStateDelayed();
+ }
+ this._updateCrashReportURL(aWindow);
+ },
+
+ onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
+ // Cleanup event listeners
+ aBrowser.removeEventListener("DOMTitleChanged", this, true);
+ aBrowser.removeEventListener("load", this, true);
+ aBrowser.removeEventListener("pageshow", this, true);
+ aBrowser.removeEventListener("AboutReaderContentReady", this, true);
+ aBrowser.removeEventListener("change", this, true);
+ aBrowser.removeEventListener("input", this, true);
+ aBrowser.removeEventListener("DOMAutoComplete", this, true);
+ aBrowser.removeEventListener("scroll", this, true);
+ aBrowser.removeEventListener("resize", this, true);
+
+ delete aBrowser.__SS_data;
+
+ log("onTabRemove() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
+ ", aNoNotification = " + aNoNotification);
+ if (!aNoNotification) {
+ this.saveStateDelayed();
+ }
+ },
+
+ onTabClose: function ss_onTabClose(aWindow, aBrowser, aTabIndex) {
+ if (this._maxTabsUndo == 0) {
+ return;
+ }
+
+ if (aWindow.BrowserApp.tabs.length > 0) {
+ // Bundle this browser's data and extra data and save in the closedTabs
+ // window property
+ let data = aBrowser.__SS_data || {};
+ data.extData = aBrowser.__SS_extdata || {};
+
+ this._windows[aWindow.__SSID].closedTabs.unshift(data);
+ let length = this._windows[aWindow.__SSID].closedTabs.length;
+ if (length > this._maxTabsUndo) {
+ this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo);
+ }
+
+ this._lastClosedTabIndex = aTabIndex;
+
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(aWindow);
+ }
+
+ log("onTabClose() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+ let evt = new Event("SSTabCloseProcessed", {"bubbles":true, "cancelable":false});
+ aBrowser.dispatchEvent(evt);
+ }
+ },
+
+ onTabLoad: function ss_onTabLoad(aWindow, aBrowser) {
+ // If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
+ // skip any session save activity.
+ if (aBrowser.__SS_restore || !this._startupRestoreFinished || aBrowser.__SS_restoreReloadPending) {
+ return;
+ }
+
+ // Ignore a transient "about:blank"
+ if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank") {
+ return;
+ }
+
+ let history = aBrowser.sessionHistory;
+
+ // Serialize the tab data
+ let entries = [];
+ let index = history.index + 1;
+ for (let i = 0; i < history.count; i++) {
+ let historyEntry = history.getEntryAtIndex(i, false);
+ // Don't try to restore wyciwyg URLs
+ if (historyEntry.URI.schemeIs("wyciwyg")) {
+ // Adjust the index to account for skipped history entries
+ if (i <= history.index) {
+ index--;
+ }
+ continue;
+ }
+ let entry = this._serializeHistoryEntry(historyEntry);
+ entries.push(entry);
+ }
+ let data = { entries: entries, index: index };
+
+ let formdata;
+ let scrolldata;
+ if (aBrowser.__SS_data) {
+ formdata = aBrowser.__SS_data.formdata;
+ scrolldata = aBrowser.__SS_data.scrolldata;
+ }
+ delete aBrowser.__SS_data;
+
+ this._collectTabData(aWindow, aBrowser, data);
+ if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
+ // If the tab has been freshly restored and the "load" or "pageshow"
+ // events haven't yet fired, we need to preserve any form data and
+ // scroll positions that might have been present.
+ aBrowser.__SS_data.formdata = formdata;
+ aBrowser.__SS_data.scrolldata = scrolldata;
+ } else {
+ // When navigating via the forward/back buttons, Gecko restores
+ // the form data all by itself and doesn't invoke any input events.
+ // As _collectTabData() doesn't save any form data, we need to manually
+ // capture it to bridge the time until the next input event arrives.
+ this.onTabInput(aWindow, aBrowser);
+ }
+
+ log("onTabLoad() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+ let evt = new Event("SSTabDataUpdated", {"bubbles":true, "cancelable":false});
+ aBrowser.dispatchEvent(evt);
+ this.saveStateDelayed();
+
+ this._updateCrashReportURL(aWindow);
+ },
+
+ onTabSelect: function ss_onTabSelect(aWindow, aBrowser) {
+ if (this._loadState != STATE_RUNNING) {
+ return;
+ }
+
+ let browsers = aWindow.document.getElementById("browsers");
+ let index = browsers.selectedIndex;
+ this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based
+
+ let tabId = aWindow.BrowserApp.getTabForBrowser(aBrowser).id;
+
+ // Restore the resurrected browser
+ if (aBrowser.__SS_restore) {
+ if (tabId != this._keepAsZombieTabId) {
+ this._restoreZombieTab(aBrowser, tabId);
+ } else {
+ log("keeping as zombie tab " + tabId);
+ }
+ }
+ // The tab id passed through Tab:KeepZombified is valid for one TabSelect only.
+ this._keepAsZombieTabId = -1;
+
+ log("onTabSelect() ran for tab " + tabId);
+ this.saveStateDelayed();
+ this._updateCrashReportURL(aWindow);
+
+ // If the selected tab has changed while listening for closed tab
+ // notifications, we may have switched between different private browsing
+ // modes.
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(aWindow);
+ }
+ },
+
+ _restoreZombieTab: function ss_restoreZombieTab(aBrowser, aTabId) {
+ let data = aBrowser.__SS_data;
+ this._restoreTab(data, aBrowser);
+
+ delete aBrowser.__SS_restore;
+ aBrowser.removeAttribute("pending");
+ log("restoring zombie tab " + aTabId);
+ },
+
+ onTabInput: function ss_onTabInput(aWindow, aBrowser) {
+ // If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
+ // skip any session save activity.
+ if (aBrowser.__SS_restore || !this._startupRestoreFinished || aBrowser.__SS_restoreReloadPending) {
+ return;
+ }
+
+ // Don't bother trying to save text data if we don't have history yet
+ let data = aBrowser.__SS_data;
+ if (!data || data.entries.length == 0) {
+ return;
+ }
+
+ // Start with storing the main content
+ let content = aBrowser.contentWindow;
+
+ // If the main content document has an associated URL that we are not
+ // allowed to store data for, bail out. We explicitly discard data for any
+ // children as well even if storing data for those frames would be allowed.
+ if (!this.checkPrivacyLevel(content.document.documentURI)) {
+ return;
+ }
+
+ // Store the main content
+ let formdata = FormData.collect(content) || {};
+
+ // Loop over direct child frames, and store the text data
+ let children = [];
+ for (let i = 0; i < content.frames.length; i++) {
+ let frame = content.frames[i];
+ if (!this.checkPrivacyLevel(frame.document.documentURI)) {
+ continue;
+ }
+
+ let result = FormData.collect(frame);
+ if (result && Object.keys(result).length) {
+ children[i] = result;
+ }
+ }
+
+ // If any frame had text data, add it to the main form data
+ if (children.length) {
+ formdata.children = children;
+ }
+
+ // If we found any form data, main content or frames, let's save it
+ if (Object.keys(formdata).length) {
+ data.formdata = formdata;
+ log("onTabInput() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+ this.saveStateDelayed();
+ }
+ },
+
+ onTabScroll: function ss_onTabScroll(aWindow, aBrowser) {
+ // If we've been called directly, cancel any pending timeouts.
+ if (this._scrollSavePending) {
+ aWindow.clearTimeout(this._scrollSavePending);
+ this._scrollSavePending = null;
+ log("onTabScroll() clearing pending timeout");
+ }
+
+ // If this browser belongs to a zombie tab or the initial restore hasn't yet finished,
+ // skip any session save activity.
+ if (aBrowser.__SS_restore || !this._startupRestoreFinished || aBrowser.__SS_restoreReloadPending) {
+ return;
+ }
+
+ // Don't bother trying to save scroll positions if we don't have history yet.
+ let data = aBrowser.__SS_data;
+ if (!data || data.entries.length == 0) {
+ return;
+ }
+
+ // Neither bother if we're yet to restore the previous scroll position.
+ if (aBrowser.__SS_restoreDataOnLoad || aBrowser.__SS_restoreDataOnPageshow) {
+ return;
+ }
+
+ // Start with storing the main content.
+ let content = aBrowser.contentWindow;
+
+ // Store the main content.
+ let scrolldata = ScrollPosition.collect(content) || {};
+
+ // Loop over direct child frames, and store the scroll positions.
+ let children = [];
+ for (let i = 0; i < content.frames.length; i++) {
+ let frame = content.frames[i];
+
+ let result = ScrollPosition.collect(frame);
+ if (result && Object.keys(result).length) {
+ children[i] = result;
+ }
+ }
+
+ // If any frame had scroll positions, add them to the main scroll data.
+ if (children.length) {
+ scrolldata.children = children;
+ }
+
+ // Save the current document resolution.
+ let zoom = { value: 1 };
+ content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
+ Ci.nsIDOMWindowUtils).getResolution(zoom);
+ scrolldata.zoom = {};
+ scrolldata.zoom.resolution = zoom.value;
+ log("onTabScroll() zoom level: " + zoom.value);
+
+ // Save some data that'll help in adjusting the zoom level
+ // when restoring in a different screen orientation.
+ scrolldata.zoom.displaySize = this._getContentViewerSize(content);
+ log("onTabScroll() displayWidth: " + scrolldata.zoom.displaySize.width);
+
+ // Save zoom and scroll data.
+ data.scrolldata = scrolldata;
+ log("onTabScroll() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+ let evt = new Event("SSTabScrollCaptured", {"bubbles":true, "cancelable":false});
+ aBrowser.dispatchEvent(evt);
+ this.saveStateDelayed();
+ },
+
+ _getContentViewerSize: function ss_getContentViewerSize(aWindow) {
+ let displaySize = {};
+ let width = {}, height = {};
+ aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(
+ Ci.nsIDOMWindowUtils).getContentViewerSize(width, height);
+
+ displaySize.width = width.value;
+ displaySize.height = height.value;
+
+ return displaySize;
+ },
+
+ saveStateDelayed: function ss_saveStateDelayed() {
+ if (!this._saveTimer) {
+ // Interval until the next disk operation is allowed
+ let currentDelay = this._lastSaveTime + this._interval - Date.now();
+
+ // If we have to wait, set a timer, otherwise saveState directly
+ let delay = Math.max(currentDelay, this._minSaveDelay);
+ if (delay > 0) {
+ this._pendingWrite++;
+ this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ log("saveStateDelayed() timer delay = " + delay +
+ ", incrementing _pendingWrite to " + this._pendingWrite);
+ } else {
+ log("saveStateDelayed() no delay");
+ this.saveState();
+ }
+ } else {
+ log("saveStateDelayed() timer already running, taking no action");
+ }
+ },
+
+ saveState: function ss_saveState() {
+ this._pendingWrite++;
+ log("saveState(), incrementing _pendingWrite to " + this._pendingWrite);
+ this._saveState(true);
+ },
+
+ // Immediately and synchronously writes any pending state to disk.
+ flushPendingState: function ss_flushPendingState() {
+ log("flushPendingState(), _pendingWrite = " + this._pendingWrite);
+ if (this._pendingWrite) {
+ this._saveState(false);
+ }
+ },
+
+ _saveState: function ss_saveState(aAsync) {
+ log("_saveState(aAsync = " + aAsync + ")");
+ // Kill any queued timer and save immediately
+ if (this._saveTimer) {
+ this._saveTimer.cancel();
+ this._saveTimer = null;
+ log("_saveState() killed queued timer");
+ }
+
+ // Periodically save a "known good" copy of the session store data.
+ if (!this._writeInProgress && Date.now() - this._lastBackupTime > this._backupInterval &&
+ this._sessionDataIsGood && this._sessionFile.exists()) {
+ if (this._sessionFileBackup.exists()) {
+ this._sessionFileBackup.remove(false);
+ }
+
+ log("_saveState() backing up session data");
+ this._sessionFile.copyTo(null, this._sessionFileBackup.leafName);
+ this._lastBackupTime = Date.now();
+ }
+
+ let data = this._getCurrentState();
+ let normalData = { windows: [] };
+ let privateData = { windows: [] };
+ log("_saveState() current state collected");
+
+ for (let winIndex = 0; winIndex < data.windows.length; ++winIndex) {
+ let win = data.windows[winIndex];
+ let normalWin = {};
+ for (let prop in win) {
+ normalWin[prop] = data[prop];
+ }
+ normalWin.tabs = [];
+
+ // Save normal closed tabs. Forget about private closed tabs.
+ normalWin.closedTabs = win.closedTabs.filter(tab => !tab.isPrivate);
+
+ normalData.windows.push(normalWin);
+ privateData.windows.push({ tabs: [] });
+
+ // Split the session data into private and non-private data objects.
+ // Non-private session data will be saved to disk, and private session
+ // data will be sent to Java for Android to hold it in memory.
+ for (let i = 0; i < win.tabs.length; ++i) {
+ let tab = win.tabs[i];
+ let savedWin = tab.isPrivate ? privateData.windows[winIndex] : normalData.windows[winIndex];
+ savedWin.tabs.push(tab);
+ if (win.selected == i + 1) {
+ savedWin.selected = savedWin.tabs.length;
+ }
+ }
+ }
+
+ // Write only non-private data to disk
+ if (normalData.windows[0] && normalData.windows[0].tabs) {
+ log("_saveState() writing normal data, " +
+ normalData.windows[0].tabs.length + " tabs in window[0]");
+ } else {
+ log("_saveState() writing empty normal data");
+ }
+ this._writeFile(this._sessionFile, this._sessionFileTemp, normalData, aAsync);
+
+ // If we have private data, send it to Java; otherwise, send null to
+ // indicate that there is no private data
+ Messaging.sendRequest({
+ type: "PrivateBrowsing:Data",
+ session: (privateData.windows.length > 0 && privateData.windows[0].tabs.length > 0) ? JSON.stringify(privateData) : null
+ });
+
+ this._lastSaveTime = Date.now();
+ },
+
+ _getCurrentState: function ss_getCurrentState() {
+ let self = this;
+ this._forEachBrowserWindow(function(aWindow) {
+ self._collectWindowData(aWindow);
+ });
+
+ let data = { windows: [] };
+ for (let index in this._windows) {
+ data.windows.push(this._windows[index]);
+ }
+
+ return data;
+ },
+
+ _collectTabData: function ss__collectTabData(aWindow, aBrowser, aHistory) {
+ // If this browser is being restored, skip any session save activity
+ if (aBrowser.__SS_restore) {
+ return;
+ }
+
+ aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 };
+
+ let tabData = {};
+ tabData.entries = aHistory.entries;
+ tabData.index = aHistory.index;
+ tabData.attributes = { image: aBrowser.mIconURL };
+ tabData.desktopMode = aWindow.BrowserApp.getTabForBrowser(aBrowser).desktopMode;
+ tabData.isPrivate = aBrowser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing;
+
+ aBrowser.__SS_data = tabData;
+ },
+
+ _collectWindowData: function ss__collectWindowData(aWindow) {
+ // Ignore windows not tracked by SessionStore
+ if (!aWindow.__SSID || !this._windows[aWindow.__SSID]) {
+ return;
+ }
+
+ let winData = this._windows[aWindow.__SSID];
+ winData.tabs = [];
+
+ let browsers = aWindow.document.getElementById("browsers");
+ let index = browsers.selectedIndex;
+ winData.selected = parseInt(index) + 1; // 1-based
+
+ let tabs = aWindow.BrowserApp.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ let browser = tabs[i].browser;
+ if (browser.__SS_data) {
+ let tabData = browser.__SS_data;
+ if (browser.__SS_extdata) {
+ tabData.extData = browser.__SS_extdata;
+ }
+ winData.tabs.push(tabData);
+ }
+ }
+ },
+
+ _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) {
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ while (windowsEnum.hasMoreElements()) {
+ let window = windowsEnum.getNext();
+ if (window.__SSID && !window.closed) {
+ aFunc.call(this, window);
+ }
+ }
+ },
+
+ /**
+ * Writes the session state to a disk file, while doing some telemetry and notification
+ * bookkeeping.
+ * @param aFile nsIFile used for saving the session
+ * @param aFileTemp nsIFile used as a temporary file in writing the data
+ * @param aData JSON session state
+ * @param aAsync boolelan used to determine the method of saving the state
+ */
+ _writeFile: function ss_writeFile(aFile, aFileTemp, aData, aAsync) {
+ TelemetryStopwatch.start("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
+ let state = JSON.stringify(aData);
+ TelemetryStopwatch.finish("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
+
+ // Convert data string to a utf-8 encoded array buffer
+ let buffer = new TextEncoder().encode(state);
+ Services.telemetry.getHistogramById("FX_SESSION_RESTORE_FILE_SIZE_BYTES").add(buffer.byteLength);
+
+ Services.obs.notifyObservers(null, "sessionstore-state-write", "");
+ let startWriteMs = Cu.now();
+
+ log("_writeFile(aAsync = " + aAsync + "), _pendingWrite = " + this._pendingWrite);
+ this._writeInProgress = true;
+ let pendingWrite = this._pendingWrite;
+ this._write(aFile, aFileTemp, buffer, aAsync).then(() => {
+ let stopWriteMs = Cu.now();
+
+ // Make sure this._pendingWrite is the same value it was before we
+ // fired off the async write. If the count is different, another write
+ // is pending, so we shouldn't reset this._pendingWrite yet.
+ if (pendingWrite === this._pendingWrite) {
+ this._pendingWrite = 0;
+ this._writeInProgress = false;
+ }
+
+ log("_writeFile() _write() returned, _pendingWrite = " + this._pendingWrite);
+
+ // We don't use a stopwatch here since the calls are async and stopwatches can only manage
+ // a single timer per histogram.
+ Services.telemetry.getHistogramById("FX_SESSION_RESTORE_WRITE_FILE_MS").add(Math.round(stopWriteMs - startWriteMs));
+ Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
+ this._sessionDataIsGood = true;
+ });
+ },
+
+ /**
+ * Writes the session state to a disk file, using async or sync methods
+ * @param aFile nsIFile used for saving the session
+ * @param aFileTemp nsIFile used as a temporary file in writing the data
+ * @param aBuffer UTF-8 encoded ArrayBuffer of the session state
+ * @param aAsync boolelan used to determine the method of saving the state
+ * @return Promise that resolves when the file has been written
+ */
+ _write: function ss_write(aFile, aFileTemp, aBuffer, aAsync) {
+ // Use async file writer and just return it's promise
+ if (aAsync) {
+ log("_write() writing asynchronously");
+ return OS.File.writeAtomic(aFile.path, aBuffer, { tmpPath: aFileTemp.path });
+ }
+
+ // Convert buffer to an encoded string and sync write to disk
+ let bytes = String.fromCharCode.apply(null, new Uint16Array(aBuffer));
+ let stream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+ stream.init(aFileTemp, 0x02 | 0x08 | 0x20, 0o666, 0);
+ stream.write(bytes, bytes.length);
+ stream.close();
+ // Mimic writeAtomic behaviour when tmpPath is set and write
+ // to a temp file which is then renamed at the end.
+ aFileTemp.renameTo(null, aFile.leafName);
+ log("_write() writing synchronously");
+
+ // Return a resolved promise to make the caller happy
+ return Promise.resolve();
+ },
+
+ _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) {
+ let crashReporterBuilt = "nsICrashReporter" in Ci && Services.appinfo instanceof Ci.nsICrashReporter;
+ if (!crashReporterBuilt) {
+ return;
+ }
+
+ if (!aWindow.BrowserApp.selectedBrowser) {
+ return;
+ }
+
+ try {
+ let currentURI = aWindow.BrowserApp.selectedBrowser.currentURI.clone();
+ // if the current URI contains a username/password, remove it
+ try {
+ currentURI.userPass = "";
+ } catch (ex) { } // ignore failures on about: URIs
+
+ Services.appinfo.annotateCrashReport("URL", currentURI.spec);
+ } catch (ex) {
+ // don't make noise when crashreporter is built but not enabled
+ if (ex.result != Cr.NS_ERROR_NOT_INITIALIZED) {
+ Cu.reportError("SessionStore:" + ex);
+ }
+ }
+ },
+
+ /**
+ * Determines whether a given session history entry has been added dynamically.
+ */
+ isDynamic: function(aEntry) {
+ // aEntry.isDynamicallyAdded() is true for dynamically added
+ // <iframe> and <frameset>, but also for <html> (the root of the
+ // document) so we use aEntry.parent to ensure that we're not looking
+ // at the root of the document
+ return aEntry.parent && aEntry.isDynamicallyAdded();
+ },
+
+ /**
+ * Get an object that is a serialized representation of a History entry.
+ */
+ _serializeHistoryEntry: function _serializeHistoryEntry(aEntry) {
+ let entry = { url: aEntry.URI.spec };
+
+ if (aEntry.title && aEntry.title != entry.url) {
+ entry.title = aEntry.title;
+ }
+
+ if (!(aEntry instanceof Ci.nsISHEntry)) {
+ return entry;
+ }
+
+ let cacheKey = aEntry.cacheKey;
+ if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0) {
+ entry.cacheKey = cacheKey.data;
+ }
+
+ entry.ID = aEntry.ID;
+ entry.docshellID = aEntry.docshellID;
+
+ if (aEntry.referrerURI) {
+ entry.referrer = aEntry.referrerURI.spec;
+ }
+
+ if (aEntry.originalURI) {
+ entry.originalURI = aEntry.originalURI.spec;
+ }
+
+ if (aEntry.loadReplace) {
+ entry.loadReplace = aEntry.loadReplace;
+ }
+
+ if (aEntry.contentType) {
+ entry.contentType = aEntry.contentType;
+ }
+
+ if (aEntry.scrollRestorationIsManual) {
+ entry.scrollRestorationIsManual = true;
+ } else {
+ let x = {}, y = {};
+ aEntry.getScrollPosition(x, y);
+ if (x.value != 0 || y.value != 0) {
+ entry.scroll = x.value + "," + y.value;
+ }
+ }
+
+ // Collect triggeringPrincipal data for the current history entry.
+ // Please note that before Bug 1297338 there was no concept of a
+ // principalToInherit. To remain backward/forward compatible we
+ // serialize the principalToInherit as triggeringPrincipal_b64.
+ // Once principalToInherit is well established (within FF55)
+ // we can update this code, remove triggeringPrincipal_b64 and
+ // just keep triggeringPrincipal_base64 as well as
+ // principalToInherit_base64; see Bug 1301666.
+ if (aEntry.principalToInherit) {
+ try {
+ let principalToInherit = Utils.serializePrincipal(aEntry.principalToInherit);
+ if (principalToInherit) {
+ entry.triggeringPrincipal_b64 = principalToInherit;
+ entry.principalToInherit_base64 = principalToInherit;
+ }
+ } catch (e) {
+ dump(e);
+ }
+ }
+
+ if (aEntry.triggeringPrincipal) {
+ try {
+ let triggeringPrincipal = Utils.serializePrincipal(aEntry.triggeringPrincipal);
+ if (triggeringPrincipal) {
+ entry.triggeringPrincipal_base64 = triggeringPrincipal;
+ }
+ } catch (e) {
+ dump(e);
+ }
+ }
+
+ entry.docIdentifier = aEntry.BFCacheEntry.ID;
+
+ if (aEntry.stateData != null) {
+ entry.structuredCloneState = aEntry.stateData.getDataAsBase64();
+ entry.structuredCloneVersion = aEntry.stateData.formatVersion;
+ }
+
+ if (!(aEntry instanceof Ci.nsISHContainer)) {
+ return entry;
+ }
+
+ if (aEntry.childCount > 0) {
+ let children = [];
+ for (let i = 0; i < aEntry.childCount; i++) {
+ let child = aEntry.GetChildAt(i);
+
+ if (child && !this.isDynamic(child)) {
+ // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
+ if (child.URI.schemeIs("wyciwyg")) {
+ children = [];
+ break;
+ }
+ children.push(this._serializeHistoryEntry(child));
+ }
+ }
+
+ if (children.length) {
+ entry.children = children;
+ }
+ }
+
+ return entry;
+ },
+
+ _deserializeHistoryEntry: function _deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) {
+ let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].createInstance(Ci.nsISHEntry);
+
+ shEntry.setURI(Services.io.newURI(aEntry.url, null, null));
+ shEntry.setTitle(aEntry.title || aEntry.url);
+ if (aEntry.subframe) {
+ shEntry.setIsSubFrame(aEntry.subframe || false);
+ }
+ shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
+ if (aEntry.contentType) {
+ shEntry.contentType = aEntry.contentType;
+ }
+ if (aEntry.referrer) {
+ shEntry.referrerURI = Services.io.newURI(aEntry.referrer, null, null);
+ }
+
+ if (aEntry.originalURI) {
+ shEntry.originalURI = Services.io.newURI(aEntry.originalURI, null, null);
+ }
+
+ if (aEntry.loadReplace) {
+ shEntry.loadReplace = aEntry.loadReplace;
+ }
+
+ if (aEntry.cacheKey) {
+ let cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].createInstance(Ci.nsISupportsPRUint32);
+ cacheKey.data = aEntry.cacheKey;
+ shEntry.cacheKey = cacheKey;
+ }
+
+ if (aEntry.ID) {
+ // get a new unique ID for this frame (since the one from the last
+ // start might already be in use)
+ let id = aIdMap[aEntry.ID] || 0;
+ if (!id) {
+ for (id = Date.now(); id in aIdMap.used; id++);
+ aIdMap[aEntry.ID] = id;
+ aIdMap.used[id] = true;
+ }
+ shEntry.ID = id;
+ }
+
+ if (aEntry.docshellID) {
+ shEntry.docshellID = aEntry.docshellID;
+ }
+
+ if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) {
+ shEntry.stateData =
+ Cc["@mozilla.org/docshell/structured-clone-container;1"].
+ createInstance(Ci.nsIStructuredCloneContainer);
+
+ shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion);
+ }
+
+ if (aEntry.scrollRestorationIsManual) {
+ shEntry.scrollRestorationIsManual = true;
+ } else if (aEntry.scroll) {
+ let scrollPos = aEntry.scroll.split(",");
+ scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
+ shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
+ }
+
+ let childDocIdents = {};
+ if (aEntry.docIdentifier) {
+ // If we have a serialized document identifier, try to find an SHEntry
+ // which matches that doc identifier and adopt that SHEntry's
+ // BFCacheEntry. If we don't find a match, insert shEntry as the match
+ // for the document identifier.
+ let matchingEntry = aDocIdentMap[aEntry.docIdentifier];
+ if (!matchingEntry) {
+ matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
+ aDocIdentMap[aEntry.docIdentifier] = matchingEntry;
+ } else {
+ shEntry.adoptBFCacheEntry(matchingEntry.shEntry);
+ childDocIdents = matchingEntry.childDocIdents;
+ }
+ }
+
+ // The field aEntry.owner_b64 got renamed to aEntry.triggeringPricipal_b64 in
+ // Bug 1286472. To remain backward compatible we still have to support that
+ // field for a few cycles before we can remove it within Bug 1289785.
+ if (aEntry.owner_b64) {
+ aEntry.triggeringPricipal_b64 = aEntry.owner_b64;
+ delete aEntry.owner_b64;
+ }
+
+ // Before introducing the concept of principalToInherit we only had
+ // a triggeringPrincipal within every entry which basically is the
+ // equivalent of the new principalToInherit. To avoid compatibility
+ // issues, we first check if the entry has entries for
+ // triggeringPrincipal_base64 and principalToInherit_base64. If not
+ // we fall back to using the principalToInherit (which is stored
+ // as triggeringPrincipal_b64) as the triggeringPrincipal and
+ // the principalToInherit.
+ // FF55 will remove the triggeringPrincipal_b64, see Bug 1301666.
+ if (aEntry.triggeringPrincipal_base64 || aEntry.principalToInherit_base64) {
+ if (aEntry.triggeringPrincipal_base64) {
+ shEntry.triggeringPrincipal =
+ Utils.deserializePrincipal(aEntry.triggeringPrincipal_base64);
+ }
+ if (aEntry.principalToInherit_base64) {
+ shEntry.principalToInherit =
+ Utils.deserializePrincipal(aEntry.principalToInherit_base64);
+ }
+ } else if (aEntry.triggeringPrincipal_b64) {
+ shEntry.triggeringPrincipal = Utils.deserializePrincipal(aEntry.triggeringPrincipal_b64);
+ shEntry.principalToInherit = shEntry.triggeringPrincipal;
+ }
+
+ if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
+ for (let i = 0; i < aEntry.children.length; i++) {
+ if (!aEntry.children[i].url) {
+ continue;
+ }
+
+ // We're getting sessionrestore.js files with a cycle in the
+ // doc-identifier graph, likely due to bug 698656. (That is, we have
+ // an entry where doc identifier A is an ancestor of doc identifier B,
+ // and another entry where doc identifier B is an ancestor of A.)
+ //
+ // If we were to respect these doc identifiers, we'd create a cycle in
+ // the SHEntries themselves, which causes the docshell to loop forever
+ // when it looks for the root SHEntry.
+ //
+ // So as a hack to fix this, we restrict the scope of a doc identifier
+ // to be a node's siblings and cousins, and pass childDocIdents, not
+ // aDocIdents, to _deserializeHistoryEntry. That is, we say that two
+ // SHEntries with the same doc identifier have the same document iff
+ // they have the same parent or their parents have the same document.
+
+ shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i);
+ }
+ }
+
+ return shEntry;
+ },
+
+ // This function iterates through a list of urls opening a new tab for each.
+ _openTabs: function ss_openTabs(aData) {
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ for (let i = 0; i < aData.urls.length; i++) {
+ let url = aData.urls[i];
+ let params = {
+ selected: (i == aData.urls.length - 1),
+ isPrivate: false,
+ desktopMode: false,
+ };
+
+ let tab = window.BrowserApp.addTab(url, params);
+ }
+ },
+
+ // This function iterates through a list of tab data restoring session for each of them.
+ _restoreTabs: function ss_restoreTabs(aData) {
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ for (let i = 0; i < aData.tabs.length; i++) {
+ let tabData = JSON.parse(aData.tabs[i]);
+ let isSelectedTab = (i == aData.tabs.length - 1);
+ let params = {
+ selected: isSelectedTab,
+ isPrivate: tabData.isPrivate,
+ desktopMode: tabData.desktopMode,
+ cancelEditMode: isSelectedTab
+ };
+
+ let tab = window.BrowserApp.addTab(tabData.entries[tabData.index - 1].url, params);
+ tab.browser.__SS_data = tabData;
+ tab.browser.__SS_extdata = tabData.extData;
+ this._restoreTab(tabData, tab.browser);
+ }
+ },
+
+ /**
+ * Don't save sensitive data if the user doesn't want to
+ * (distinguishes between encrypted and non-encrypted sites)
+ */
+ checkPrivacyLevel: function ss_checkPrivacyLevel(aURL) {
+ let isHTTPS = aURL.startsWith("https:");
+ let pref = "browser.sessionstore.privacy_level";
+ return Services.prefs.getIntPref(pref) < (isHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL);
+ },
+
+ /**
+ * Starts the restoration process for a browser. History is restored at this
+ * point, but text data must be delayed until the content loads.
+ */
+ _restoreTab: function ss_restoreTab(aTabData, aBrowser) {
+ // aTabData shouldn't be empty here, but if it is,
+ // _restoreHistory() will crash otherwise.
+ if (!aTabData || aTabData.entries.length == 0) {
+ Cu.reportError("SessionStore.js: Error trying to restore tab with empty tabdata");
+ return;
+ }
+ this._restoreHistory(aTabData, aBrowser.sessionHistory);
+
+ // Various bits of state can only be restored if page loading has progressed far enough:
+ // The MobileViewportManager needs to be told as early as possible about
+ // our desired zoom level so it can take it into account during the
+ // initial document resolution calculation.
+ aBrowser.__SS_restoreDataOnLocationChange = true;
+ // Restoring saved form data requires the input fields to be available,
+ // so we have to wait for the content to load.
+ aBrowser.__SS_restoreDataOnLoad = true;
+ // Restoring the scroll position depends on the document resolution having been set,
+ // which is only guaranteed to have happened *after* we receive the load event.
+ aBrowser.__SS_restoreDataOnPageshow = true;
+ },
+
+ /**
+ * Takes serialized history data and create news entries into the given
+ * nsISessionHistory object.
+ */
+ _restoreHistory: function ss_restoreHistory(aTabData, aHistory) {
+ if (aHistory.count > 0) {
+ aHistory.PurgeHistory(aHistory.count);
+ }
+ aHistory.QueryInterface(Ci.nsISHistoryInternal);
+
+ // Helper hashes for ensuring unique frame IDs and unique document
+ // identifiers.
+ let idMap = { used: {} };
+ let docIdentMap = {};
+
+ for (let i = 0; i < aTabData.entries.length; i++) {
+ if (!aTabData.entries[i].url) {
+ continue;
+ }
+ aHistory.addEntry(this._deserializeHistoryEntry(aTabData.entries[i], idMap, docIdentMap), true);
+ }
+
+ // We need to force set the active history item and cause it to reload since
+ // we stop the load above
+ let activeIndex = (aTabData.index || aTabData.entries.length) - 1;
+ aHistory.getEntryAtIndex(activeIndex, true);
+
+ try {
+ aHistory.QueryInterface(Ci.nsISHistory).reloadCurrentEntry();
+ } catch (e) {
+ // This will throw if the current entry is an error page.
+ }
+ },
+
+ /**
+ * Takes serialized form text data and restores it into the given browser.
+ */
+ _restoreTextData: function ss_restoreTextData(aFormData, aBrowser) {
+ if (aFormData) {
+ log("_restoreTextData()");
+ FormData.restoreTree(aBrowser.contentWindow, aFormData);
+ }
+ },
+
+ /**
+ * Restores the zoom level of the window. This needs to be called before
+ * first paint/load (whichever comes first) to take any effect.
+ */
+ _restoreZoom: function ss_restoreZoom(aScrollData, aBrowser) {
+ if (aScrollData && aScrollData.zoom && aScrollData.zoom.displaySize) {
+ log("_restoreZoom(), resolution: " + aScrollData.zoom.resolution +
+ ", old displayWidth: " + aScrollData.zoom.displaySize.width);
+
+ let utils = aBrowser.contentWindow.QueryInterface(
+ Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ // Restore zoom level.
+ utils.setRestoreResolution(aScrollData.zoom.resolution,
+ aScrollData.zoom.displaySize.width,
+ aScrollData.zoom.displaySize.height);
+ }
+ },
+
+ /**
+ * Takes serialized scroll positions and restores them into the given browser.
+ */
+ _restoreScrollPosition: function ss_restoreScrollPosition(aScrollData, aBrowser) {
+ if (aScrollData) {
+ log("_restoreScrollPosition()");
+ ScrollPosition.restoreTree(aBrowser.contentWindow, aScrollData);
+ }
+ },
+
+ getBrowserState: function ss_getBrowserState() {
+ return this._getCurrentState();
+ },
+
+ _restoreWindow: function ss_restoreWindow(aData) {
+ let state;
+ try {
+ state = JSON.parse(aData);
+ } catch (e) {
+ throw "Invalid session JSON: " + aData;
+ }
+
+ // To do a restore, we must have at least one window with one tab
+ if (!state || state.windows.length == 0 || !state.windows[0].tabs || state.windows[0].tabs.length == 0) {
+ throw "Invalid session JSON: " + aData;
+ }
+
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+
+ let tabs = state.windows[0].tabs;
+ let selected = state.windows[0].selected;
+ log("_restoreWindow() selected tab in aData is " + selected + " of " + tabs.length)
+ if (selected == null || selected > tabs.length) { // Clamp the selected index if it's bogus
+ log("_restoreWindow() resetting selected tab");
+ selected = 1;
+ }
+ log("restoreWindow() window.BrowserApp.selectedTab is " + window.BrowserApp.selectedTab.id);
+
+ for (let i = 0; i < tabs.length; i++) {
+ let tabData = tabs[i];
+ let entry = tabData.entries[tabData.index - 1];
+
+ // Use stubbed tab if we've already created it; otherwise, make a new tab
+ let tab;
+ if (tabData.tabId == null) {
+ let params = {
+ selected: (selected == i+1),
+ delayLoad: true,
+ title: entry.title,
+ desktopMode: (tabData.desktopMode == true),
+ isPrivate: (tabData.isPrivate == true)
+ };
+ tab = window.BrowserApp.addTab(entry.url, params);
+ } else {
+ tab = window.BrowserApp.getTabForId(tabData.tabId);
+ delete tabData.tabId;
+
+ // Don't restore tab if user has closed it
+ if (tab == null) {
+ continue;
+ }
+ }
+
+ tab.browser.__SS_data = tabData;
+ tab.browser.__SS_extdata = tabData.extData;
+
+ if (window.BrowserApp.selectedTab == tab) {
+ this._restoreTab(tabData, tab.browser);
+
+ // We can now lift the general ban on tab data capturing,
+ // but we still need to protect the foreground tab until we're
+ // sure it's actually reloading after history restoring has finished.
+ tab.browser.__SS_restoreReloadPending = true;
+ this._startupRestoreFinished = true;
+ log("startupRestoreFinished = true");
+
+ delete tab.browser.__SS_restore;
+ tab.browser.removeAttribute("pending");
+ } else {
+ // Mark the browser for delay loading
+ tab.browser.__SS_restore = true;
+ tab.browser.setAttribute("pending", "true");
+ }
+ }
+
+ // Restore the closed tabs array on the current window.
+ if (state.windows[0].closedTabs) {
+ this._windows[window.__SSID].closedTabs = state.windows[0].closedTabs;
+ log("_restoreWindow() loaded " + state.windows[0].closedTabs.length + " closed tabs");
+ }
+ },
+
+ getClosedTabCount: function ss_getClosedTabCount(aWindow) {
+ if (!aWindow || !aWindow.__SSID || !this._windows[aWindow.__SSID]) {
+ return 0; // not a browser window, or not otherwise tracked by SS.
+ }
+
+ return this._windows[aWindow.__SSID].closedTabs.length;
+ },
+
+ getClosedTabs: function ss_getClosedTabs(aWindow) {
+ if (!aWindow.__SSID) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ return this._windows[aWindow.__SSID].closedTabs;
+ },
+
+ undoCloseTab: function ss_undoCloseTab(aWindow, aCloseTabData) {
+ if (!aWindow.__SSID) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let closedTabs = this._windows[aWindow.__SSID].closedTabs;
+ if (!closedTabs) {
+ return null;
+ }
+
+ // If the tab data is in the closedTabs array, remove it.
+ closedTabs.find(function (tabData, i) {
+ if (tabData == aCloseTabData) {
+ closedTabs.splice(i, 1);
+ return true;
+ }
+ });
+
+ // create a new tab and bring to front
+ let params = {
+ selected: true,
+ isPrivate: aCloseTabData.isPrivate,
+ desktopMode: aCloseTabData.desktopMode,
+ tabIndex: this._lastClosedTabIndex
+ };
+ let tab = aWindow.BrowserApp.addTab(aCloseTabData.entries[aCloseTabData.index - 1].url, params);
+ tab.browser.__SS_data = aCloseTabData;
+ tab.browser.__SS_extdata = aCloseTabData.extData;
+ this._restoreTab(aCloseTabData, tab.browser);
+
+ this._lastClosedTabIndex = -1;
+
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(aWindow);
+ }
+
+ return tab.browser;
+ },
+
+ forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
+ if (!aWindow.__SSID) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let closedTabs = this._windows[aWindow.__SSID].closedTabs;
+
+ // default to the most-recently closed tab
+ aIndex = aIndex || 0;
+ if (!(aIndex in closedTabs)) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // remove closed tab from the array
+ closedTabs.splice(aIndex, 1);
+
+ // Forget the last closed tab index if we're forgetting the last closed tab.
+ if (aIndex == 0) {
+ this._lastClosedTabIndex = -1;
+ }
+ if (this._notifyClosedTabs) {
+ this._sendClosedTabsToJava(aWindow);
+ }
+ },
+
+ _sendClosedTabsToJava: function ss_sendClosedTabsToJava(aWindow) {
+ if (!aWindow.__SSID) {
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let closedTabs = this._windows[aWindow.__SSID].closedTabs;
+ let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aWindow.BrowserApp.selectedBrowser);
+
+ let tabs = closedTabs
+ .filter(tab => tab.isPrivate == isPrivate)
+ .map(function (tab) {
+ // Get the url and title for the last entry in the session history.
+ let lastEntry = tab.entries[tab.entries.length - 1];
+ return {
+ url: lastEntry.url,
+ title: lastEntry.title || "",
+ data: tab
+ };
+ });
+
+ log("sending " + tabs.length + " closed tabs to Java");
+ Messaging.sendRequest({
+ type: "ClosedTabs:Data",
+ tabs: tabs
+ });
+ },
+
+ getTabValue: function ss_getTabValue(aTab, aKey) {
+ let browser = aTab.browser;
+ let data = browser.__SS_extdata || {};
+ return data[aKey] || "";
+ },
+
+ setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
+ let browser = aTab.browser;
+ if (!browser.__SS_extdata) {
+ browser.__SS_extdata = {};
+ }
+ browser.__SS_extdata[aKey] = aStringValue;
+ this.saveStateDelayed();
+ },
+
+ deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
+ let browser = aTab.browser;
+ if (browser.__SS_extdata && aKey in browser.__SS_extdata) {
+ delete browser.__SS_extdata[aKey];
+ this.saveStateDelayed();
+ }
+ },
+
+ restoreLastSession: Task.async(function* (aSessionString) {
+ let notifyMessage = "";
+
+ try {
+ this._restoreWindow(aSessionString);
+ } catch (e) {
+ Cu.reportError("SessionStore: " + e);
+ notifyMessage = "fail";
+ }
+
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", notifyMessage);
+ }),
+
+ removeWindow: function ss_removeWindow(aWindow) {
+ if (!aWindow || !aWindow.__SSID || !this._windows[aWindow.__SSID]) {
+ return;
+ }
+
+ delete this._windows[aWindow.__SSID];
+ delete aWindow.__SSID;
+
+ if (this._loadState == STATE_RUNNING) {
+ // Save the purged state immediately
+ this.saveState();
+ } else if (this._loadState <= STATE_QUITTING) {
+ this.saveStateDelayed();
+ }
+ }
+
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SessionStore]);
diff --git a/mobile/android/components/SiteSpecificUserAgent.js b/mobile/android/components/SiteSpecificUserAgent.js
new file mode 100644
index 000000000..f95d7ab16
--- /dev/null
+++ b/mobile/android/components/SiteSpecificUserAgent.js
@@ -0,0 +1,33 @@
+/* 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/. */
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/UserAgentOverrides.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const DEFAULT_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
+ .getService(Ci.nsIHttpProtocolHandler)
+ .userAgent;
+
+function SiteSpecificUserAgent() {}
+
+SiteSpecificUserAgent.prototype = {
+ getUserAgentForURIAndWindow: function ssua_getUserAgentForURIAndWindow(aURI, aWindow) {
+ let UA;
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ if (win && win.DesktopUserAgent) {
+ UA = win.DesktopUserAgent.getUserAgentForWindow(aWindow);
+ }
+ return UA || UserAgentOverrides.getOverrideForURI(aURI) || DEFAULT_UA;
+ },
+
+ classID: Components.ID("{d5234c9d-0ee2-4b3c-9da3-18be9e5cf7e6}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISiteSpecificUserAgent])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SiteSpecificUserAgent]);
diff --git a/mobile/android/components/Snippets.js b/mobile/android/components/Snippets.js
new file mode 100644
index 000000000..92639236f
--- /dev/null
+++ b/mobile/android/components/Snippets.js
@@ -0,0 +1,446 @@
+/* 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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Accounts.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Home", "resource://gre/modules/Home.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+
+
+XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { return new gChromeWin.TextEncoder(); });
+XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { return new gChromeWin.TextDecoder(); });
+
+// URL to fetch snippets, in the urlFormatter service format.
+const SNIPPETS_UPDATE_URL_PREF = "browser.snippets.updateUrl";
+
+// URL to send stats data to metrics.
+const SNIPPETS_STATS_URL_PREF = "browser.snippets.statsUrl";
+
+// URL to fetch country code, a value that's cached and refreshed once per month.
+const SNIPPETS_GEO_URL_PREF = "browser.snippets.geoUrl";
+
+// Timestamp when we last updated the user's country code.
+const SNIPPETS_GEO_LAST_UPDATE_PREF = "browser.snippets.geoLastUpdate";
+
+// Pref where we'll cache the user's country.
+const SNIPPETS_COUNTRY_CODE_PREF = "browser.snippets.countryCode";
+
+// Pref where we store an array IDs of snippets that should not be shown again
+const SNIPPETS_REMOVED_IDS_PREF = "browser.snippets.removedIds";
+
+// How frequently we update the user's country code from the server (30 days).
+const SNIPPETS_GEO_UPDATE_INTERVAL_MS = 86400000*30;
+
+// Should be bumped up if the snippets content format changes.
+const SNIPPETS_VERSION = 1;
+
+XPCOMUtils.defineLazyGetter(this, "gSnippetsURL", function() {
+ let updateURL = Services.prefs.getCharPref(SNIPPETS_UPDATE_URL_PREF).replace("%SNIPPETS_VERSION%", SNIPPETS_VERSION);
+ return Services.urlFormatter.formatURL(updateURL);
+});
+
+// Where we cache snippets data
+XPCOMUtils.defineLazyGetter(this, "gSnippetsPath", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gStatsURL", function() {
+ return Services.prefs.getCharPref(SNIPPETS_STATS_URL_PREF);
+});
+
+// Where we store stats about which snippets have been shown
+XPCOMUtils.defineLazyGetter(this, "gStatsPath", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "snippets-stats.txt");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gGeoURL", function() {
+ return Services.prefs.getCharPref(SNIPPETS_GEO_URL_PREF);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gCountryCode", function() {
+ try {
+ return Services.prefs.getCharPref(SNIPPETS_COUNTRY_CODE_PREF);
+ } catch (e) {
+ // Return an empty string if the country code pref isn't set yet.
+ return "";
+ }
+});
+
+XPCOMUtils.defineLazyGetter(this, "gChromeWin", function() {
+ return Services.wm.getMostRecentWindow("navigator:browser");
+});
+
+/**
+ * Updates snippet data and country code (if necessary).
+ */
+function update() {
+ // Check to see if we should update the user's country code from the geo server.
+ let lastUpdate = 0;
+ try {
+ lastUpdate = parseFloat(Services.prefs.getCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF));
+ } catch (e) {}
+
+ if (Date.now() - lastUpdate > SNIPPETS_GEO_UPDATE_INTERVAL_MS) {
+ // We should update the snippets after updating the country code,
+ // so that we can filter snippets to add to the banner.
+ updateCountryCode(updateSnippets);
+ } else {
+ updateSnippets();
+ }
+}
+
+/**
+ * Fetches the user's country code from the geo server and stores the value in a pref.
+ *
+ * @param callback function called once country code is updated
+ */
+function updateCountryCode(callback) {
+ _httpGetRequest(gGeoURL, function(responseText) {
+ // Store the country code in a pref.
+ let data = JSON.parse(responseText);
+ Services.prefs.setCharPref(SNIPPETS_COUNTRY_CODE_PREF, data.country_code);
+
+ // Set last update time.
+ Services.prefs.setCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF, Date.now());
+
+ callback();
+ });
+}
+
+/**
+ * Loads snippets from snippets server, caches the response, and
+ * updates the home banner with the new set of snippets.
+ */
+function updateSnippets() {
+ _httpGetRequest(gSnippetsURL, function(responseText) {
+ try {
+ let messages = JSON.parse(responseText);
+ updateBanner(messages);
+
+ // Only cache the response if it is valid JSON.
+ cacheSnippets(responseText);
+ } catch (e) {
+ Cu.reportError("Error parsing snippets responseText: " + e);
+ }
+ });
+}
+
+/**
+ * Caches snippets server response text to `snippets.json` in profile directory.
+ *
+ * @param response responseText returned from snippets server
+ */
+function cacheSnippets(response) {
+ let data = gEncoder.encode(response);
+ let promise = OS.File.writeAtomic(gSnippetsPath, data, { tmpPath: gSnippetsPath + ".tmp" });
+ promise.then(null, e => Cu.reportError("Error caching snippets: " + e));
+}
+
+/**
+ * Loads snippets from cached `snippets.json`.
+ */
+function loadSnippetsFromCache() {
+ let promise = OS.File.read(gSnippetsPath);
+ promise.then(array => {
+ let messages = JSON.parse(gDecoder.decode(array));
+ updateBanner(messages);
+ }, e => {
+ if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ Services.console.logStringMessage("Couldn't show snippets because cache does not exist yet.");
+ } else {
+ Cu.reportError("Error loading snippets from cache: " + e);
+ }
+ });
+}
+
+// Array of the message ids added to the home banner, used to remove
+// older set of snippets when new ones are available.
+var gMessageIds = [];
+
+/**
+ * Updates set of snippets in the home banner message rotation.
+ *
+ * @param messages JSON array of message data JSON objects.
+ * Each message object should have the following properties:
+ * - id (?): Unique identifier for this snippets message
+ * - text (string): Text to show as banner message
+ * - url (string): URL to open when banner is clicked
+ * - icon (data URI): Icon to appear in banner
+ * - countries (list of strings): Country codes for where this message should be shown (e.g. ["US", "GR"])
+ */
+function updateBanner(messages) {
+ // Remove the current messages, if there are any.
+ gMessageIds.forEach(function(id) {
+ Home.banner.remove(id);
+ })
+ gMessageIds = [];
+
+ try {
+ let removedSnippetIds = JSON.parse(Services.prefs.getCharPref(SNIPPETS_REMOVED_IDS_PREF));
+ messages = messages.filter(function(message) {
+ // Only include the snippet if it has not been previously removed.
+ return removedSnippetIds.indexOf(message.id) === -1;
+ });
+ } catch (e) {
+ // If the pref doesn't exist, there aren't any snippets to filter out.
+ }
+
+ messages.forEach(function(message) {
+ // Don't add this message to the banner if it's not supposed to be shown in this country.
+ if ("countries" in message && message.countries.indexOf(gCountryCode) === -1) {
+ return;
+ }
+
+ let id = Home.banner.add({
+ text: message.text,
+ icon: message.icon,
+ weight: message.weight,
+ onclick: function() {
+ gChromeWin.BrowserApp.loadURI(message.url);
+ removeSnippet(id, message.id);
+ UITelemetry.addEvent("action.1", "banner", null, message.id);
+ },
+ ondismiss: function() {
+ removeSnippet(id, message.id);
+ UITelemetry.addEvent("cancel.1", "banner", null, message.id);
+ },
+ onshown: function() {
+ // 10% of the time, record the snippet id and a timestamp
+ if (Math.random() < .1) {
+ writeStat(message.id, new Date().toISOString());
+ }
+ }
+ });
+ // Keep track of the message we added so that we can remove it later.
+ gMessageIds.push(id);
+ });
+}
+
+/**
+ * Removes a snippet message from the home banner rotation, and stores its
+ * snippet id in a pref so we'll never show it again.
+ *
+ * @param messageId unique id for home banner message, returned from Home.banner API
+ * @param snippetId unique id for snippet, sent from snippets server
+ */
+function removeSnippet(messageId, snippetId) {
+ // Remove the message from the home banner rotation.
+ Home.banner.remove(messageId);
+
+ // Remove the message from the stored message ids.
+ gMessageIds.splice(gMessageIds.indexOf(messageId), 1);
+
+ let removedSnippetIds;
+ try {
+ removedSnippetIds = JSON.parse(Services.prefs.getCharPref(SNIPPETS_REMOVED_IDS_PREF));
+ } catch (e) {
+ removedSnippetIds = [];
+ }
+
+ removedSnippetIds.push(snippetId);
+ Services.prefs.setCharPref(SNIPPETS_REMOVED_IDS_PREF, JSON.stringify(removedSnippetIds));
+}
+
+/**
+ * Appends snippet id and timestamp to the end of `snippets-stats.txt`.
+ *
+ * @param snippetId unique id for snippet, sent from snippets server
+ * @param timestamp in ISO8601
+ */
+function writeStat(snippetId, timestamp) {
+ let data = gEncoder.encode(snippetId + "," + timestamp + ";");
+
+ Task.spawn(function() {
+ try {
+ let file = yield OS.File.open(gStatsPath, { append: true, write: true });
+ try {
+ yield file.write(data);
+ } finally {
+ yield file.close();
+ }
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ // If the file doesn't exist yet, create it.
+ yield OS.File.writeAtomic(gStatsPath, data, { tmpPath: gStatsPath + ".tmp" });
+ }
+ }).then(null, e => Cu.reportError("Error writing snippets stats: " + e));
+}
+
+/**
+ * Reads snippets stats data from `snippets-stats.txt` and sends the data to metrics.
+ */
+function sendStats() {
+ let promise = OS.File.read(gStatsPath);
+ promise.then(array => sendStatsRequest(gDecoder.decode(array)), e => {
+ if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
+ // If the file doesn't exist, there aren't any stats to send.
+ } else {
+ Cu.reportError("Error eading snippets stats: " + e);
+ }
+ });
+}
+
+/**
+ * Sends stats to metrics about which snippets have been shown.
+ * Appends snippet ids and timestamps as parameters to a GET request.
+ * e.g. https://snippets-stats.mozilla.org/mobile?s1=3825&t1=2013-11-17T18:27Z&s2=6326&t2=2013-11-18T18:27Z
+ *
+ * @param data contents of stats data file
+ */
+function sendStatsRequest(data) {
+ let params = [];
+ let stats = data.split(";");
+
+ // The last item in the array will be an empty string, so stop before then.
+ for (let i = 0; i < stats.length - 1; i++) {
+ let stat = stats[i].split(",");
+ params.push("s" + i + "=" + encodeURIComponent(stat[0]));
+ params.push("t" + i + "=" + encodeURIComponent(stat[1]));
+ }
+
+ let url = gStatsURL + "?" + params.join("&");
+
+ // Remove the file after succesfully sending the data.
+ _httpGetRequest(url, removeStats);
+}
+
+/**
+ * Removes text file where we store snippets stats.
+ */
+function removeStats() {
+ let promise = OS.File.remove(gStatsPath);
+ promise.then(null, e => Cu.reportError("Error removing snippets stats: " + e));
+}
+
+/**
+ * Helper function to make HTTP GET requests.
+ *
+ * @param url where we send the request
+ * @param callback function that is called with the xhr responseText
+ */
+function _httpGetRequest(url, callback) {
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ try {
+ xhr.open("GET", url, true);
+ } catch (e) {
+ Cu.reportError("Error opening request to " + url + ": " + e);
+ return;
+ }
+ xhr.onerror = function onerror(e) {
+ Cu.reportError("Error making request to " + url + ": " + e.error);
+ }
+ xhr.onload = function onload(event) {
+ if (xhr.status !== 200) {
+ Cu.reportError("Request to " + url + " returned status " + xhr.status);
+ return;
+ }
+ if (callback) {
+ callback(xhr.responseText);
+ }
+ }
+ xhr.send(null);
+}
+
+function loadSyncPromoBanner() {
+ Accounts.anySyncAccountsExist().then(
+ (exist) => {
+ // Don't show the banner if sync accounts exist.
+ if (exist) {
+ return;
+ }
+
+ let stringBundle = Services.strings.createBundle("chrome://browser/locale/sync.properties");
+ let text = stringBundle.GetStringFromName("promoBanner.message.text");
+ let link = stringBundle.GetStringFromName("promoBanner.message.link");
+
+ let id = Home.banner.add({
+ text: text + "<a href=\"#\">" + link + "</a>",
+ icon: "drawable://sync_promo",
+ onclick: function() {
+ // Remove the message, so that it won't show again for the rest of the app lifetime.
+ Home.banner.remove(id);
+ Accounts.launchSetup();
+
+ UITelemetry.addEvent("action.1", "banner", null, "syncpromo");
+ },
+ ondismiss: function() {
+ // Remove the sync promo message from the banner and never try to show it again.
+ Home.banner.remove(id);
+ Services.prefs.setBoolPref("browser.snippets.syncPromo.enabled", false);
+
+ UITelemetry.addEvent("cancel.1", "banner", null, "syncpromo");
+ }
+ });
+ },
+ (err) => {
+ Cu.reportError("Error checking whether sync account exists: " + err);
+ }
+ );
+}
+
+function loadHomePanelsBanner() {
+ let stringBundle = Services.strings.createBundle("chrome://browser/locale/aboutHome.properties");
+ let text = stringBundle.GetStringFromName("banner.firstrunHomepage.text");
+
+ let id = Home.banner.add({
+ text: text,
+ icon: "drawable://homepage_banner_firstrun",
+ onclick: function() {
+ // Remove the message, so that it won't show again for the rest of the app lifetime.
+ Home.banner.remove(id);
+ // User has interacted with this snippet so don't show it again.
+ Services.prefs.setBoolPref("browser.snippets.firstrunHomepage.enabled", false);
+
+ UITelemetry.addEvent("action.1", "banner", null, "firstrun-homepage");
+ },
+ ondismiss: function() {
+ Home.banner.remove(id);
+ Services.prefs.setBoolPref("browser.snippets.firstrunHomepage.enabled", false);
+
+ UITelemetry.addEvent("cancel.1", "banner", null, "firstrun-homepage");
+ }
+ });
+}
+
+function Snippets() {}
+
+Snippets.prototype = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsITimerCallback]),
+ classID: Components.ID("{a78d7e59-b558-4321-a3d6-dffe2f1e76dd}"),
+
+ observe: function(subject, topic, data) {
+ switch(topic) {
+ case "browser-delayed-startup-finished":
+ // Add snippets to be cycled through.
+ if (Services.prefs.getBoolPref("browser.snippets.firstrunHomepage.enabled")) {
+ loadHomePanelsBanner();
+ }
+
+ if (Services.prefs.getBoolPref("browser.snippets.syncPromo.enabled")) {
+ loadSyncPromoBanner();
+ }
+
+ if (Services.prefs.getBoolPref("browser.snippets.enabled")) {
+ loadSnippetsFromCache();
+ }
+ break;
+ }
+ },
+
+ // By default, this timer fires once every 24 hours. See the "browser.snippets.updateInterval" pref.
+ notify: function(timer) {
+ if (!Services.prefs.getBoolPref("browser.snippets.enabled")) {
+ return;
+ }
+ update();
+ sendStats();
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Snippets]);
diff --git a/mobile/android/components/TabSource.js b/mobile/android/components/TabSource.js
new file mode 100644
index 000000000..c35a54438
--- /dev/null
+++ b/mobile/android/components/TabSource.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * 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"
+
+const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm");
+
+function TabSource() {
+}
+
+TabSource.prototype = {
+ classID: Components.ID("{5850c76e-b916-4218-b99a-31f004e0a7e7}"),
+ classDescription: "Fennec Tab Source",
+ contractID: "@mozilla.org/tab-source-service;1",
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITabSource]),
+
+ getTabToStream: function() {
+ let app = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ let tabs = app.tabs;
+ if (tabs == null || tabs.length == 0) {
+ Services.console.logStringMessage("ERROR: No tabs");
+ return null;
+ }
+
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let title = bundle.GetStringFromName("tabshare.title")
+
+ let prompt = new Prompt({
+ title: title,
+ window: null
+ }).setSingleChoiceItems(tabs.map(function(tab) {
+ let label;
+ if (tab.browser.contentTitle)
+ label = tab.browser.contentTitle;
+ else if (tab.browser.contentURI)
+ label = tab.browser.contentURI.spec;
+ else
+ label = tab.originalURI.spec;
+ return { label: label,
+ icon: "thumbnail:" + tab.id }
+ }));
+
+ 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 == -1) {
+ return null;
+ }
+ return tabs[result].browser.contentWindow;
+ },
+
+ notifyStreamStart: function(window) {
+ let app = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ let tabs = app.tabs;
+ for (var i in tabs) {
+ if (tabs[i].browser.contentWindow == window) {
+ Messaging.sendRequest({ type: "Tab:StreamStart", tabID: tabs[i].id });
+ }
+ }
+ },
+
+ notifyStreamStop: function(window) {
+ let app = Services.wm.getMostRecentWindow("navigator:browser").BrowserApp;
+ let tabs = app.tabs;
+ for (let i in tabs) {
+ if (tabs[i].browser.contentWindow == window) {
+ Messaging.sendRequest({ type: "Tab:StreamStop", tabID: tabs[i].id });
+ }
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TabSource]);
diff --git a/mobile/android/components/XPIDialogService.js b/mobile/android/components/XPIDialogService.js
new file mode 100644
index 000000000..2a33d4ddf
--- /dev/null
+++ b/mobile/android/components/XPIDialogService.js
@@ -0,0 +1,49 @@
+/* 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/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+
+// -----------------------------------------------------------------------
+// Web Install Prompt service
+// -----------------------------------------------------------------------
+
+function WebInstallPrompt() { }
+
+WebInstallPrompt.prototype = {
+ classID: Components.ID("{c1242012-27d8-477e-a0f1-0b098ffc329b}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.amIWebInstallPrompt]),
+
+ confirm: function(aBrowser, aURL, aInstalls) {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ let prompt = Services.prompt;
+ let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_IS_STRING + prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL;
+ let title = bundle.GetStringFromName("addonsConfirmInstall.title");
+ let button = bundle.GetStringFromName("addonsConfirmInstall.install");
+
+ aInstalls.forEach(function(install) {
+ let message;
+ if (install.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+ title = bundle.GetStringFromName("addonsConfirmInstallUnsigned.title")
+ message = bundle.GetStringFromName("addonsConfirmInstallUnsigned.message") + "\n\n" + install.name;
+ } else {
+ message = install.name;
+ }
+
+ let result = (prompt.confirmEx(aBrowser.contentWindow, title, message, flags, button, null, null, null, {value: false}) == 0);
+ if (result)
+ install.install();
+ else
+ install.cancel();
+ });
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebInstallPrompt]);
diff --git a/mobile/android/components/build/moz.build b/mobile/android/components/build/moz.build
new file mode 100644
index 000000000..7a5c439e7
--- /dev/null
+++ b/mobile/android/components/build/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ 'nsIShellService.idl',
+]
+
+XPIDL_MODULE = 'browsercomps'
+
+EXPORTS += [
+ 'nsBrowserComponents.h',
+]
+
+SOURCES += [
+ 'nsBrowserModule.cpp',
+ 'nsShellService.cpp',
+]
+
+if CONFIG['MOZ_ANDROID_HISTORY']:
+ SOURCES += [
+ 'nsAndroidHistory.cpp',
+ ]
+ LOCAL_INCLUDES += [
+ '/docshell/base',
+ '/dom/base',
+ ]
+
+FINAL_LIBRARY = 'xul'
diff --git a/mobile/android/components/build/nsAndroidHistory.cpp b/mobile/android/components/build/nsAndroidHistory.cpp
new file mode 100644
index 000000000..2610781c0
--- /dev/null
+++ b/mobile/android/components/build/nsAndroidHistory.cpp
@@ -0,0 +1,395 @@
+/* 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/. */
+
+#include "nsThreadUtils.h"
+#include "nsAndroidHistory.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIURI.h"
+#include "nsIObserverService.h"
+#include "GeneratedJNIWrappers.h"
+#include "Link.h"
+
+#include "mozilla/Services.h"
+#include "mozilla/Preferences.h"
+
+#define NS_LINK_VISITED_EVENT_TOPIC "link-visited"
+
+// We copy Places here.
+// Note that we don't yet observe this pref at runtime.
+#define PREF_HISTORY_ENABLED "places.history.enabled"
+
+// Time we wait to see if a pending visit is really a redirect
+#define PENDING_REDIRECT_TIMEOUT 3000
+
+using namespace mozilla;
+using mozilla::dom::Link;
+
+NS_IMPL_ISUPPORTS(nsAndroidHistory, IHistory, nsIRunnable, nsITimerCallback)
+
+nsAndroidHistory* nsAndroidHistory::sHistory = nullptr;
+
+/*static*/
+nsAndroidHistory*
+nsAndroidHistory::GetSingleton()
+{
+ if (!sHistory) {
+ sHistory = new nsAndroidHistory();
+ NS_ENSURE_TRUE(sHistory, nullptr);
+ }
+
+ NS_ADDREF(sHistory);
+ return sHistory;
+}
+
+nsAndroidHistory::nsAndroidHistory()
+ : mHistoryEnabled(true)
+{
+ LoadPrefs();
+
+ mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::RegisterVisitedCallback(nsIURI *aURI, Link *aContent)
+{
+ if (!aContent || !aURI)
+ return NS_OK;
+
+ // Silently return if URI is something we would never add to DB.
+ bool canAdd;
+ nsresult rv = CanAddURI(aURI, &canAdd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canAdd) {
+ return NS_OK;
+ }
+
+ nsAutoCString uri;
+ rv = aURI->GetSpec(uri);
+ if (NS_FAILED(rv)) return rv;
+ NS_ConvertUTF8toUTF16 uriString(uri);
+
+ nsTArray<Link*>* list = mListeners.Get(uriString);
+ if (! list) {
+ list = new nsTArray<Link*>();
+ mListeners.Put(uriString, list);
+ }
+ list->AppendElement(aContent);
+
+ if (jni::IsAvailable()) {
+ java::GeckoAppShell::CheckURIVisited(uriString);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::UnregisterVisitedCallback(nsIURI *aURI, Link *aContent)
+{
+ if (!aContent || !aURI)
+ return NS_OK;
+
+ nsAutoCString uri;
+ nsresult rv = aURI->GetSpec(uri);
+ if (NS_FAILED(rv)) return rv;
+ NS_ConvertUTF8toUTF16 uriString(uri);
+
+ nsTArray<Link*>* list = mListeners.Get(uriString);
+ if (! list)
+ return NS_OK;
+
+ list->RemoveElement(aContent);
+ if (list->IsEmpty()) {
+ mListeners.Remove(uriString);
+ delete list;
+ }
+ return NS_OK;
+}
+
+void
+nsAndroidHistory::AppendToRecentlyVisitedURIs(nsIURI* aURI) {
+ if (mRecentlyVisitedURIs.Length() < RECENTLY_VISITED_URI_SIZE) {
+ // Append a new element while the array is not full.
+ mRecentlyVisitedURIs.AppendElement(aURI);
+ } else {
+ // Otherwise, replace the oldest member.
+ mRecentlyVisitedURIsNextIndex %= RECENTLY_VISITED_URI_SIZE;
+ mRecentlyVisitedURIs.ElementAt(mRecentlyVisitedURIsNextIndex) = aURI;
+ mRecentlyVisitedURIsNextIndex++;
+ }
+}
+
+bool
+nsAndroidHistory::ShouldRecordHistory() {
+ return mHistoryEnabled;
+}
+
+void
+nsAndroidHistory::LoadPrefs() {
+ mHistoryEnabled = Preferences::GetBool(PREF_HISTORY_ENABLED, true);
+}
+
+inline bool
+nsAndroidHistory::IsRecentlyVisitedURI(nsIURI* aURI) {
+ bool equals = false;
+ RecentlyVisitedArray::index_type i;
+ RecentlyVisitedArray::size_type length = mRecentlyVisitedURIs.Length();
+ for (i = 0; i < length && !equals; ++i) {
+ aURI->Equals(mRecentlyVisitedURIs.ElementAt(i), &equals);
+ }
+ return equals;
+}
+
+void
+nsAndroidHistory::AppendToEmbedURIs(nsIURI* aURI) {
+ if (mEmbedURIs.Length() < EMBED_URI_SIZE) {
+ // Append a new element while the array is not full.
+ mEmbedURIs.AppendElement(aURI);
+ } else {
+ // Otherwise, replace the oldest member.
+ mEmbedURIsNextIndex %= EMBED_URI_SIZE;
+ mEmbedURIs.ElementAt(mEmbedURIsNextIndex) = aURI;
+ mEmbedURIsNextIndex++;
+ }
+}
+
+inline bool
+nsAndroidHistory::IsEmbedURI(nsIURI* aURI) {
+ bool equals = false;
+ EmbedArray::index_type i;
+ EmbedArray::size_type length = mEmbedURIs.Length();
+ for (i = 0; i < length && !equals; ++i) {
+ aURI->Equals(mEmbedURIs.ElementAt(i), &equals);
+ }
+ return equals;
+}
+
+inline bool
+nsAndroidHistory::RemovePendingVisitURI(nsIURI* aURI) {
+ // Remove the first pending URI that matches. Return a boolean to
+ // let the caller know if we removed a URI or not.
+ bool equals = false;
+ PendingVisitArray::index_type i;
+ for (i = 0; i < mPendingVisitURIs.Length(); ++i) {
+ aURI->Equals(mPendingVisitURIs.ElementAt(i), &equals);
+ if (equals) {
+ mPendingVisitURIs.RemoveElementAt(i);
+ return true;
+ }
+ }
+ return false;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::Notify(nsITimer *timer)
+{
+ // Any pending visits left in the queue have exceeded our threshold for
+ // redirects, so save them
+ PendingVisitArray::index_type i;
+ for (i = 0; i < mPendingVisitURIs.Length(); ++i) {
+ SaveVisitURI(mPendingVisitURIs.ElementAt(i));
+ }
+ mPendingVisitURIs.Clear();
+
+ return NS_OK;
+}
+
+void
+nsAndroidHistory::SaveVisitURI(nsIURI* aURI) {
+ // Add the URI to our cache so we can take a fast path later
+ AppendToRecentlyVisitedURIs(aURI);
+
+ if (jni::IsAvailable()) {
+ // Save this URI in our history
+ nsAutoCString spec;
+ (void)aURI->GetSpec(spec);
+ java::GeckoAppShell::MarkURIVisited(NS_ConvertUTF8toUTF16(spec));
+ }
+
+ // Finally, notify that we've been visited.
+ nsCOMPtr<nsIObserverService> obsService = mozilla::services::GetObserverService();
+ if (obsService) {
+ obsService->NotifyObservers(aURI, NS_LINK_VISITED_EVENT_TOPIC, nullptr);
+ }
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::VisitURI(nsIURI *aURI, nsIURI *aLastVisitedURI, uint32_t aFlags)
+{
+ if (!aURI) {
+ return NS_OK;
+ }
+
+ if (!(aFlags & VisitFlags::TOP_LEVEL)) {
+ return NS_OK;
+ }
+
+ if (aFlags & VisitFlags::UNRECOVERABLE_ERROR) {
+ return NS_OK;
+ }
+
+ // Silently return if URI is something we shouldn't add to DB.
+ bool canAdd;
+ nsresult rv = CanAddURI(aURI, &canAdd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canAdd) {
+ return NS_OK;
+ }
+
+ if (aLastVisitedURI) {
+ if (aFlags & VisitFlags::REDIRECT_SOURCE ||
+ aFlags & VisitFlags::REDIRECT_PERMANENT ||
+ aFlags & VisitFlags::REDIRECT_TEMPORARY) {
+ // aLastVisitedURI redirected to aURI. We want to ignore aLastVisitedURI,
+ // so remove the pending visit. We want to give aURI a chance to be saved,
+ // so don't return early.
+ RemovePendingVisitURI(aLastVisitedURI);
+ }
+
+ bool same;
+ rv = aURI->Equals(aLastVisitedURI, &same);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (same && IsRecentlyVisitedURI(aURI)) {
+ // Do not save refresh visits if we have visited this URI recently.
+ return NS_OK;
+ }
+
+ // Since we have a last visited URI and we were not redirected, it is
+ // safe to save the visit if it's still pending.
+ if (RemovePendingVisitURI(aLastVisitedURI)) {
+ SaveVisitURI(aLastVisitedURI);
+ }
+ }
+
+ // Let's wait and see if this visit is not a redirect.
+ mPendingVisitURIs.AppendElement(aURI);
+ mTimer->InitWithCallback(this, PENDING_REDIRECT_TIMEOUT, nsITimer::TYPE_ONE_SHOT);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::SetURITitle(nsIURI *aURI, const nsAString& aTitle)
+{
+ // Silently return if URI is something we shouldn't add to DB.
+ bool canAdd;
+ nsresult rv = CanAddURI(aURI, &canAdd);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canAdd) {
+ return NS_OK;
+ }
+
+ if (IsEmbedURI(aURI)) {
+ return NS_OK;
+ }
+
+ if (jni::IsAvailable()) {
+ nsAutoCString uri;
+ nsresult rv = aURI->GetSpec(uri);
+ if (NS_FAILED(rv)) return rv;
+ if (RemovePendingVisitURI(aURI)) {
+ // We have a title, so aURI isn't a redirect, so save the visit now before setting the title.
+ SaveVisitURI(aURI);
+ }
+ NS_ConvertUTF8toUTF16 uriString(uri);
+ java::GeckoAppShell::SetURITitle(uriString, aTitle);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::NotifyVisited(nsIURI *aURI)
+{
+ if (aURI && sHistory) {
+ nsAutoCString spec;
+ (void)aURI->GetSpec(spec);
+ sHistory->mPendingLinkURIs.Push(NS_ConvertUTF8toUTF16(spec));
+ NS_DispatchToMainThread(sHistory);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsAndroidHistory::Run()
+{
+ while (! mPendingLinkURIs.IsEmpty()) {
+ nsString uriString = mPendingLinkURIs.Pop();
+ nsTArray<Link*>* list = sHistory->mListeners.Get(uriString);
+ if (list) {
+ for (unsigned int i = 0; i < list->Length(); i++) {
+ list->ElementAt(i)->SetLinkState(eLinkState_Visited);
+ }
+ // as per the IHistory interface contract, remove the
+ // Link pointers once they have been notified
+ mListeners.Remove(uriString);
+ delete list;
+ }
+ }
+ return NS_OK;
+}
+
+// Filter out unwanted URIs such as "chrome:", "mailbox:", etc.
+//
+// The model is if we don't know differently then add which basically means
+// we are suppose to try all the things we know not to allow in and then if
+// we don't bail go on and allow it in.
+//
+// Logic ported from nsNavHistory::CanAddURI.
+
+NS_IMETHODIMP
+nsAndroidHistory::CanAddURI(nsIURI* aURI, bool* canAdd)
+{
+ NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
+ NS_ENSURE_ARG(aURI);
+ NS_ENSURE_ARG_POINTER(canAdd);
+
+ // See if we're disabled.
+ if (!ShouldRecordHistory()) {
+ *canAdd = false;
+ return NS_OK;
+ }
+
+ nsAutoCString scheme;
+ nsresult rv = aURI->GetScheme(scheme);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // first check the most common cases (HTTP, HTTPS) to allow in to avoid most
+ // of the work
+ if (scheme.EqualsLiteral("http")) {
+ *canAdd = true;
+ return NS_OK;
+ }
+ if (scheme.EqualsLiteral("https")) {
+ *canAdd = true;
+ return NS_OK;
+ }
+ if (scheme.EqualsLiteral("about")) {
+ nsAutoCString path;
+ rv = aURI->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (StringBeginsWith(path, NS_LITERAL_CSTRING("reader"))) {
+ *canAdd = true;
+ return NS_OK;
+ }
+ }
+
+ // now check for all bad things
+ if (scheme.EqualsLiteral("about") ||
+ scheme.EqualsLiteral("imap") ||
+ scheme.EqualsLiteral("news") ||
+ scheme.EqualsLiteral("mailbox") ||
+ scheme.EqualsLiteral("moz-anno") ||
+ scheme.EqualsLiteral("view-source") ||
+ scheme.EqualsLiteral("chrome") ||
+ scheme.EqualsLiteral("resource") ||
+ scheme.EqualsLiteral("data") ||
+ scheme.EqualsLiteral("wyciwyg") ||
+ scheme.EqualsLiteral("javascript") ||
+ scheme.EqualsLiteral("blob")) {
+ *canAdd = false;
+ return NS_OK;
+ }
+ *canAdd = true;
+ return NS_OK;
+}
diff --git a/mobile/android/components/build/nsAndroidHistory.h b/mobile/android/components/build/nsAndroidHistory.h
new file mode 100644
index 000000000..382fbcd2e
--- /dev/null
+++ b/mobile/android/components/build/nsAndroidHistory.h
@@ -0,0 +1,97 @@
+/* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+#ifndef NS_ANDROIDHISTORY_H
+#define NS_ANDROIDHISTORY_H
+
+#include "IHistory.h"
+#include "nsDataHashtable.h"
+#include "nsTPriorityQueue.h"
+#include "nsIRunnable.h"
+#include "nsIURI.h"
+#include "nsITimer.h"
+
+
+#define NS_ANDROIDHISTORY_CID \
+ {0xCCAA4880, 0x44DD, 0x40A7, {0xA1, 0x3F, 0x61, 0x56, 0xFC, 0x88, 0x2C, 0x0B}}
+
+// Max size of History::mRecentlyVisitedURIs
+#define RECENTLY_VISITED_URI_SIZE 8
+
+// Max size of History::mEmbedURIs
+#define EMBED_URI_SIZE 128
+
+class nsAndroidHistory final : public mozilla::IHistory,
+ public nsIRunnable,
+ public nsITimerCallback
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_IHISTORY
+ NS_DECL_NSIRUNNABLE
+ NS_DECL_NSITIMERCALLBACK
+
+ /**
+ * Obtains a pointer that has had AddRef called on it. Used by the service
+ * manager only.
+ */
+ static nsAndroidHistory* GetSingleton();
+
+ nsAndroidHistory();
+
+private:
+ ~nsAndroidHistory() {}
+
+ static nsAndroidHistory* sHistory;
+
+ // Will mimic the value of the places.history.enabled preference.
+ bool mHistoryEnabled;
+
+ void LoadPrefs();
+ bool ShouldRecordHistory();
+ nsresult CanAddURI(nsIURI* aURI, bool* canAdd);
+
+ /**
+ * We need to manage data used to determine a:visited status.
+ */
+ nsDataHashtable<nsStringHashKey, nsTArray<mozilla::dom::Link *> *> mListeners;
+ nsTPriorityQueue<nsString> mPendingLinkURIs;
+
+ /**
+ * Redirection (temporary and permanent) flags are sent with the redirected
+ * URI, not the original URI. Since we want to ignore the original URI, we
+ * need to cache the pending visit and make sure it doesn't redirect.
+ */
+ RefPtr<nsITimer> mTimer;
+ typedef AutoTArray<nsCOMPtr<nsIURI>, RECENTLY_VISITED_URI_SIZE> PendingVisitArray;
+ PendingVisitArray mPendingVisitURIs;
+
+ bool RemovePendingVisitURI(nsIURI* aURI);
+ void SaveVisitURI(nsIURI* aURI);
+
+ /**
+ * mRecentlyVisitedURIs remembers URIs which are recently added to the DB,
+ * to avoid saving these locations repeatedly in a short period.
+ */
+ typedef AutoTArray<nsCOMPtr<nsIURI>, RECENTLY_VISITED_URI_SIZE> RecentlyVisitedArray;
+ RecentlyVisitedArray mRecentlyVisitedURIs;
+ RecentlyVisitedArray::index_type mRecentlyVisitedURIsNextIndex;
+
+ void AppendToRecentlyVisitedURIs(nsIURI* aURI);
+ bool IsRecentlyVisitedURI(nsIURI* aURI);
+
+ /**
+ * mEmbedURIs remembers URIs which are explicitly not added to the DB,
+ * to avoid wasting time on these locations.
+ */
+ typedef AutoTArray<nsCOMPtr<nsIURI>, EMBED_URI_SIZE> EmbedArray;
+ EmbedArray::index_type mEmbedURIsNextIndex;
+ EmbedArray mEmbedURIs;
+
+ void AppendToEmbedURIs(nsIURI* aURI);
+ bool IsEmbedURI(nsIURI* aURI);
+};
+
+#endif
diff --git a/mobile/android/components/build/nsBrowserComponents.h b/mobile/android/components/build/nsBrowserComponents.h
new file mode 100644
index 000000000..c9830d9c5
--- /dev/null
+++ b/mobile/android/components/build/nsBrowserComponents.h
@@ -0,0 +1,7 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+// Needed for building our components as part of libxul
+#define APP_COMPONENT_MODULES MODULE(nsBrowserCompsModule)
diff --git a/mobile/android/components/build/nsBrowserModule.cpp b/mobile/android/components/build/nsBrowserModule.cpp
new file mode 100644
index 000000000..6f9fe67bf
--- /dev/null
+++ b/mobile/android/components/build/nsBrowserModule.cpp
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "mozilla/ModuleUtils.h"
+
+#include "nsShellService.h"
+
+#ifdef MOZ_ANDROID_HISTORY
+#include "nsDocShellCID.h"
+#include "nsAndroidHistory.h"
+#define NS_ANDROIDHISTORY_CID \
+ {0xCCAA4880, 0x44DD, 0x40A7, {0xA1, 0x3F, 0x61, 0x56, 0xFC, 0x88, 0x2C, 0x0B}}
+#endif
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsShellService)
+NS_DEFINE_NAMED_CID(nsShellService_CID);
+
+#ifdef MOZ_ANDROID_HISTORY
+NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(nsAndroidHistory, nsAndroidHistory::GetSingleton)
+NS_DEFINE_NAMED_CID(NS_ANDROIDHISTORY_CID);
+#endif
+
+static const mozilla::Module::CIDEntry kBrowserCIDs[] = {
+ { &knsShellService_CID, false, nullptr, nsShellServiceConstructor },
+#ifdef MOZ_ANDROID_HISTORY
+ { &kNS_ANDROIDHISTORY_CID, false, nullptr, nsAndroidHistoryConstructor },
+#endif
+ { nullptr }
+};
+
+static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
+ { nsShellService_ContractID, &knsShellService_CID },
+#ifdef MOZ_ANDROID_HISTORY
+ { NS_IHISTORY_CONTRACTID, &kNS_ANDROIDHISTORY_CID },
+#endif
+ { nullptr }
+};
+
+static const mozilla::Module kBrowserModule = {
+ mozilla::Module::kVersion,
+ kBrowserCIDs,
+ kBrowserContracts
+};
+
+NSMODULE_DEFN(nsBrowserCompsModule) = &kBrowserModule;
diff --git a/mobile/android/components/build/nsIShellService.idl b/mobile/android/components/build/nsIShellService.idl
new file mode 100644
index 000000000..e7f8d9277
--- /dev/null
+++ b/mobile/android/components/build/nsIShellService.idl
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(fd2450a3-966b-44a9-a8eb-316256bb80b4)]
+interface nsIShellService : nsISupports
+{
+ /**
+ * This method displays a UI to switch to (or launch) a different task
+ */
+ void switchTask();
+
+ /**
+ * This method creates a shortcut on a desktop or homescreen that opens in
+ * the our application.
+ *
+ * @param aTitle the user-friendly name of the shortcut.
+ * @param aURI the URI to open.
+ * @param aIconData obsolete and ignored, but remains for backward compatibility; pass an empty string
+ * @param aIntent obsolete and ignored, but remains for backward compatibility; pass an empty string
+ */
+ void createShortcut(in AString aTitle, in AString aURI, in AString aIconData, in AString aIntent);
+};
diff --git a/mobile/android/components/build/nsShellService.cpp b/mobile/android/components/build/nsShellService.cpp
new file mode 100644
index 000000000..86cac86b4
--- /dev/null
+++ b/mobile/android/components/build/nsShellService.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#include "nsShellService.h"
+#include "nsString.h"
+
+#include "GeneratedJNIWrappers.h"
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS(nsShellService, nsIShellService)
+
+NS_IMETHODIMP
+nsShellService::SwitchTask()
+{
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsShellService::CreateShortcut(const nsAString& aTitle, const nsAString& aURI,
+ const nsAString& aIcondata, const nsAString& aIntent)
+{
+ if (!aTitle.Length() || !aURI.Length())
+ return NS_ERROR_FAILURE;
+
+ java::GeckoAppShell::CreateShortcut(aTitle, aURI);
+ return NS_OK;
+}
diff --git a/mobile/android/components/build/nsShellService.h b/mobile/android/components/build/nsShellService.h
new file mode 100644
index 000000000..ba56cbcae
--- /dev/null
+++ b/mobile/android/components/build/nsShellService.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef __NS_SHELLSERVICE_H__
+#define __NS_SHELLSERVICE_H__
+
+#include "nsIShellService.h"
+
+class nsShellService final : public nsIShellService
+{
+public:
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISHELLSERVICE
+
+ nsShellService() {}
+
+private:
+ ~nsShellService() {}
+};
+
+#define nsShellService_CID \
+{0xae9ebe1c, 0x61e9, 0x45fa, {0x8f, 0x34, 0xc1, 0x07, 0x80, 0x3a, 0x5b, 0x44}}
+
+#define nsShellService_ContractID "@mozilla.org/browser/shell-service;1"
+
+#endif
diff --git a/mobile/android/components/extensions/.eslintrc.js b/mobile/android/components/extensions/.eslintrc.js
new file mode 100644
index 000000000..4b67e27b8
--- /dev/null
+++ b/mobile/android/components/extensions/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ "extends": "../../../../toolkit/components/extensions/.eslintrc.js",
+};
diff --git a/mobile/android/components/extensions/ext-pageAction.js b/mobile/android/components/extensions/ext-pageAction.js
new file mode 100644
index 000000000..fb1c3a3f3
--- /dev/null
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -0,0 +1,169 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+ "resource://devtools/shared/event-emitter.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+// Import the android PageActions module.
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+ "resource://gre/modules/PageActions.jsm");
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+ IconDetails,
+ SingletonEventManager,
+} = ExtensionUtils;
+
+// WeakMap[Extension -> PageAction]
+var pageActionMap = new WeakMap();
+
+function PageAction(options, extension) {
+ this.id = null;
+
+ this.extension = extension;
+ this.icons = IconDetails.normalize({path: options.default_icon}, extension);
+
+ this.popupUrl = options.default_popup;
+
+ this.options = {
+ title: options.default_title || extension.name,
+ id: `{${extension.uuid}}`,
+ clickCallback: () => {
+ if (this.popupUrl) {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ win.BrowserApp.addTab(this.popupUrl, {
+ selected: true,
+ parentId: win.BrowserApp.selectedTab.id,
+ });
+ } else {
+ this.emit("click");
+ }
+ },
+ };
+
+ this.shouldShow = false;
+
+ EventEmitter.decorate(this);
+}
+
+PageAction.prototype = {
+ show(tabId, context) {
+ if (this.id) {
+ return Promise.resolve();
+ }
+
+ if (this.options.icon) {
+ this.id = PageActions.add(this.options);
+ return Promise.resolve();
+ }
+
+ this.shouldShow = true;
+
+ // TODO(robwu): Remove dependency on contentWindow from this file. It should
+ // be put in a separate file called ext-c-pageAction.js.
+ // Note: Fennec is not going to be multi-process for the foreseaable future,
+ // so this layering violation has no immediate impact. However, it is should
+ // be done at some point.
+ let {contentWindow} = context.xulBrowser;
+
+ // TODO(robwu): Why is this contentWindow.devicePixelRatio, while
+ // convertImageURLToDataURL uses browserWindow.devicePixelRatio?
+ let {icon} = IconDetails.getPreferredIcon(this.icons, this.extension,
+ 18 * contentWindow.devicePixelRatio);
+
+ let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ return IconDetails.convertImageURLToDataURL(icon, contentWindow, browserWindow).then(dataURI => {
+ if (this.shouldShow) {
+ this.options.icon = dataURI;
+ this.id = PageActions.add(this.options);
+ }
+ }).catch(() => {
+ return Promise.reject({
+ message: "Failed to load PageAction icon",
+ });
+ });
+ },
+
+ hide(tabId) {
+ this.shouldShow = false;
+ if (this.id) {
+ PageActions.remove(this.id);
+ this.id = null;
+ }
+ },
+
+ setPopup(tab, url) {
+ // TODO: Only set the popup for the specified tab once we have Tabs API support.
+ this.popupUrl = url;
+ },
+
+ getPopup(tab) {
+ // TODO: Only return the popup for the specified tab once we have Tabs API support.
+ return this.popupUrl;
+ },
+
+ shutdown() {
+ this.hide();
+ },
+};
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
+ let pageAction = new PageAction(manifest.page_action, extension);
+ pageActionMap.set(extension, pageAction);
+});
+
+extensions.on("shutdown", (type, extension) => {
+ if (pageActionMap.has(extension)) {
+ pageActionMap.get(extension).shutdown();
+ pageActionMap.delete(extension);
+ }
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("pageAction", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ pageAction: {
+ onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
+ let listener = (event) => {
+ fire();
+ };
+ pageActionMap.get(extension).on("click", listener);
+ return () => {
+ pageActionMap.get(extension).off("click", listener);
+ };
+ }).api(),
+
+ show(tabId) {
+ return pageActionMap.get(extension)
+ .show(tabId, context)
+ .then(() => {});
+ },
+
+ hide(tabId) {
+ pageActionMap.get(extension).hide(tabId);
+ return Promise.resolve();
+ },
+
+ setPopup(details) {
+ // TODO: Use the Tabs API to get the tab from details.tabId.
+ let tab = null;
+ let url = details.popup && context.uri.resolve(details.popup);
+ pageActionMap.get(extension).setPopup(tab, url);
+ },
+
+ getPopup(details) {
+ // TODO: Use the Tabs API to get the tab from details.tabId.
+ let tab = null;
+ let popup = pageActionMap.get(extension).getPopup(tab);
+ return Promise.resolve(popup);
+ },
+ },
+ };
+});
diff --git a/mobile/android/components/extensions/extensions-mobile.manifest b/mobile/android/components/extensions/extensions-mobile.manifest
new file mode 100644
index 000000000..f15540d62
--- /dev/null
+++ b/mobile/android/components/extensions/extensions-mobile.manifest
@@ -0,0 +1,5 @@
+# scripts
+category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
+
+# schemas
+category webextension-schemas page_action chrome://browser/content/schemas/page_action.json \ No newline at end of file
diff --git a/mobile/android/components/extensions/jar.mn b/mobile/android/components/extensions/jar.mn
new file mode 100644
index 000000000..a3d2b8de8
--- /dev/null
+++ b/mobile/android/components/extensions/jar.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+chrome.jar:
+ content/ext-pageAction.js \ No newline at end of file
diff --git a/mobile/android/components/extensions/moz.build b/mobile/android/components/extensions/moz.build
new file mode 100644
index 000000000..0953fcefc
--- /dev/null
+++ b/mobile/android/components/extensions/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_COMPONENTS += [
+ 'extensions-mobile.manifest',
+]
+
+DIRS += ['schemas']
+
+MOCHITEST_MANIFESTS += ['test/mochitest/mochitest.ini']
+MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
diff --git a/mobile/android/components/extensions/schemas/jar.mn b/mobile/android/components/extensions/schemas/jar.mn
new file mode 100644
index 000000000..1a587ce20
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/jar.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+chrome.jar:
+ content/schemas/page_action.json \ No newline at end of file
diff --git a/mobile/android/components/extensions/schemas/moz.build b/mobile/android/components/extensions/schemas/moz.build
new file mode 100644
index 000000000..eb4454d28
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/mobile/android/components/extensions/schemas/page_action.json b/mobile/android/components/extensions/schemas/page_action.json
new file mode 100644
index 000000000..5e9280922
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/page_action.json
@@ -0,0 +1,239 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "WebExtensionManifest",
+ "properties": {
+ "page_action": {
+ "type": "object",
+ "additionalProperties": { "$ref": "UnrecognizedProperty" },
+ "properties": {
+ "default_title": {
+ "type": "string",
+ "optional": true,
+ "preprocess": "localize"
+ },
+ "default_icon": {
+ "$ref": "IconPath",
+ "optional": true
+ },
+ "default_popup": {
+ "type": "string",
+ "format": "relativeUrl",
+ "optional": true,
+ "preprocess": "localize"
+ },
+ "browser_style": {
+ "type": "boolean",
+ "optional": true
+ }
+ },
+ "optional": true
+ }
+ }
+ }
+ ]
+ },
+ {
+ "namespace": "pageAction",
+ "description": "Use the <code>browser.pageAction</code> API to put icons inside the address bar. Page actions represent actions that can be taken on the current page, but that aren't applicable to all pages.",
+ "permissions": ["manifest:page_action"],
+ "types": [
+ {
+ "id": "ImageDataType",
+ "type": "object",
+ "isInstanceOf": "ImageData",
+ "additionalProperties": { "type": "any" },
+ "description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
+ }
+ ],
+ "functions": [
+ {
+ "name": "show",
+ "type": "function",
+ "description": "Shows the page action. The page action is shown whenever the tab is selected.",
+ "async": "callback",
+ "parameters": [
+ {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "hide",
+ "type": "function",
+ "description": "Hides the page action.",
+ "async": "callback",
+ "parameters": [
+ {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "setTitle",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sets the title of the page action. This is displayed in a tooltip over the page action.",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ "title": {"type": "string", "description": "The tooltip string."}
+ }
+ }
+ ]
+ },
+ {
+ "name": "getTitle",
+ "unsupported": true,
+ "type": "function",
+ "description": "Gets the title of the page action.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {
+ "type": "integer",
+ "description": "Specify the tab to get the title from."
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "parameters": [
+ {
+ "name": "result",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setIcon",
+ "unsupported": true,
+ "type": "function",
+ "description": "Sets the icon for the page action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ "imageData": {
+ "choices": [
+ { "$ref": "ImageDataType" },
+ {
+ "type": "object",
+ "additionalProperties": {"$ref": "ImageDataType"}
+ }
+ ],
+ "optional": true,
+ "description": "Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'19': foo}'"
+ },
+ "path": {
+ "choices": [
+ { "type": "string" },
+ {
+ "type": "object",
+ "additionalProperties": {"type": "string"}
+ }
+ ],
+ "optional": true,
+ "description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "setPopup",
+ "type": "function",
+ "async": "callback",
+ "description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
+ "popup": {
+ "type": "string",
+ "description": "The html file to show in a popup. If set to the empty string (''), no popup is shown."
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "name": "getPopup",
+ "type": "function",
+ "description": "Gets the html document set as the popup for this page action.",
+ "async": "callback",
+ "parameters": [
+ {
+ "name": "details",
+ "type": "object",
+ "properties": {
+ "tabId": {
+ "type": "integer",
+ "description": "Specify the tab to get the popup from."
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onClicked",
+ "type": "function",
+ "description": "Fired when a page action icon is clicked. This event will not fire if the page action has a popup.",
+ "parameters": [
+ {
+ "name": "tab",
+ "$ref": "tabs.Tab"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/mobile/android/components/extensions/test/mochitest/.eslintrc.js b/mobile/android/components/extensions/test/mochitest/.eslintrc.js
new file mode 100644
index 000000000..5f9059e18
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/.eslintrc.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = {
+ "extends": "../../../../../../toolkit/components/extensions/test/mochitest/.eslintrc.js",
+
+ "globals": {
+ "isPageActionShown": true,
+ "clickPageAction": true,
+ },
+};
diff --git a/mobile/android/components/extensions/test/mochitest/chrome.ini b/mobile/android/components/extensions/test/mochitest/chrome.ini
new file mode 100644
index 000000000..e19ddf393
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+ head.js
+tags = webextensions
+
+[test_ext_pageAction.html]
+[test_ext_pageAction_popup.html]
diff --git a/mobile/android/components/extensions/test/mochitest/head.js b/mobile/android/components/extensions/test/mochitest/head.js
new file mode 100644
index 000000000..be9683682
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/head.js
@@ -0,0 +1,15 @@
+"use strict";
+
+/* exported isPageActionShown clickPageAction */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/PageActions.jsm");
+
+function isPageActionShown(uuid) {
+ return PageActions.isShown(uuid);
+}
+
+function clickPageAction(uuid) {
+ PageActions.synthesizeClick(uuid);
+}
diff --git a/mobile/android/components/extensions/test/mochitest/mochitest.ini b/mobile/android/components/extensions/test/mochitest/mochitest.ini
new file mode 100644
index 000000000..59ef4bd20
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/mochitest.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+ ../../../../../../toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
+tags = webextensions
+
+[test_ext_all_apis.html]
diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html
new file mode 100644
index 000000000..aec3eb7c1
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebExtension test</title>
+ <meta charset="utf-8">
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<script>
+"use strict";
+/* exported expectedContentApisTargetSpecific, expectedBackgroundApisTargetSpecific */
+let expectedContentApisTargetSpecific = [
+];
+
+let expectedBackgroundApisTargetSpecific = [
+];
+</script>
+<script src="test_ext_all_apis.js"></script>
+</body>
+</html>
diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
new file mode 100644
index 000000000..b13c551bd
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>PageAction Test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+
+let image = atob(dataURI);
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
+function background() {
+ browser.test.assertTrue("pageAction" in browser, "Namespace 'pageAction' exists in browser");
+ browser.test.assertTrue("show" in browser.pageAction, "API method 'show' exists in browser.pageAction");
+
+ // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
+ let tabId = 1;
+ browser.test.onMessage.addListener(msg => {
+ if (msg === "pageAction-show") {
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
+ } else if (msg === "pageAction-hide") {
+ browser.pageAction.hide(tabId).then(() => {
+ browser.test.sendMessage("page-action-hidden");
+ });
+ }
+ });
+
+ browser.pageAction.onClicked.addListener(tab => {
+ // TODO: Make sure we get the correct tab once basic tabs support is added.
+ browser.test.sendMessage("page-action-clicked");
+ });
+
+ let extensionInfo = {
+ // Extract the assigned uuid from the background page url.
+ uuid: `{${window.location.hostname}}`,
+ };
+
+ browser.test.sendMessage("ready", extensionInfo);
+}
+
+add_task(function* test_pageAction() {
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ "name": "PageAction Extension",
+ "page_action": {
+ "default_title": "Page Action",
+ "default_icon": {
+ "18": "extension.png",
+ },
+ },
+ "applications": {
+ "gecko": {
+ "id": "foo@bar.com",
+ },
+ },
+ },
+ files: {
+ "extension.png": IMAGE_ARRAYBUFFER,
+ },
+ });
+
+ yield extension.startup();
+ let {uuid} = yield extension.awaitMessage("ready");
+
+ extension.sendMessage("pageAction-show");
+ yield extension.awaitMessage("page-action-shown");
+ ok(isPageActionShown(uuid), "The PageAction should be shown");
+
+ extension.sendMessage("pageAction-hide");
+ yield extension.awaitMessage("page-action-hidden");
+ ok(!isPageActionShown(uuid), "The PageAction should be hidden");
+
+ extension.sendMessage("pageAction-show");
+ yield extension.awaitMessage("page-action-shown");
+ ok(isPageActionShown(uuid), "The PageAction should be shown");
+
+ clickPageAction(uuid);
+ yield extension.awaitMessage("page-action-clicked");
+ ok(isPageActionShown(uuid), "The PageAction should still be shown after being clicked");
+
+ yield extension.unload();
+ ok(!isPageActionShown(uuid), "The PageAction should be removed after unload");
+});
+</script>
+
+</body>
+</html>
diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
new file mode 100644
index 000000000..89edc7c29
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>PageAction Test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+ <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+
+let image = atob(dataURI);
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
+add_task(function* test_contentscript() {
+ function background() {
+ // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
+ let tabId = 1;
+ let onClickedListenerEnabled = false;
+
+ browser.test.onMessage.addListener((msg, details) => {
+ if (msg === "page-action-show") {
+ // TODO: switch to using .show(tabId).then(...) once bug 1270742 lands.
+ browser.pageAction.show(tabId).then(() => {
+ browser.test.sendMessage("page-action-shown");
+ });
+ } else if (msg == "page-action-set-popup") {
+ browser.pageAction.setPopup({popup: details.name, tabId: tabId}).then(() => {
+ browser.test.sendMessage("page-action-popup-set");
+ });
+ } else if (msg == "page-action-get-popup") {
+ browser.pageAction.getPopup({tabId: tabId}).then(url => {
+ browser.test.sendMessage("page-action-got-popup", url);
+ });
+ } else if (msg == "page-action-enable-onClicked-listener") {
+ onClickedListenerEnabled = true;
+ browser.test.sendMessage("page-action-onClicked-listener-enabled");
+ } else if (msg == "page-action-disable-onClicked-listener") {
+ onClickedListenerEnabled = false;
+ browser.test.sendMessage("page-action-onClicked-listener-disabled");
+ }
+ });
+
+ browser.pageAction.onClicked.addListener(tab => {
+ browser.test.assertTrue(onClickedListenerEnabled, "The onClicked listener should only fire when it is enabled.");
+ browser.test.sendMessage("page-action-onClicked-fired");
+ });
+
+ let extensionInfo = {
+ // Extract the assigned uuid from the background page url.
+ uuid: `{${window.location.hostname}}`,
+ };
+
+ browser.test.sendMessage("ready", extensionInfo);
+ }
+
+ function popupScript() {
+ window.onload = () => {
+ browser.test.sendMessage("page-action-from-popup", location.href);
+ };
+ browser.test.onMessage.addListener((msg, details) => {
+ if (msg == "page-action-close-popup") {
+ if (details.location == location.href) {
+ window.close();
+ }
+ }
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ "name": "PageAction Extension",
+ "page_action": {
+ "default_title": "Page Action",
+ "default_popup": "default.html",
+ "default_icon": {
+ "18": "extension.png",
+ },
+ },
+ },
+ files: {
+ "default.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
+ "extension.png": IMAGE_ARRAYBUFFER,
+ "a.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
+ "b.html": `<html><head><meta charset="utf-8"><script src="popup.js"><\/script></head></html>`,
+ "popup.js": popupScript,
+ },
+ });
+
+ let tabClosedPromise = () => {
+ return new Promise(resolve => {
+ let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let BrowserApp = chromeWin.BrowserApp;
+
+ let tabCloseListener = (event) => {
+ BrowserApp.deck.removeEventListener("TabClose", tabCloseListener, false);
+ let browser = event.target;
+ let url = browser.currentURI.spec;
+ resolve(url);
+ };
+
+ BrowserApp.deck.addEventListener("TabClose", tabCloseListener, false);
+ });
+ };
+
+ function* testPopup(name, uuid) {
+ // We don't need to set the popup when testing default_popup.
+ if (name != "default.html") {
+ extension.sendMessage("page-action-set-popup", {name});
+ yield extension.awaitMessage("page-action-popup-set");
+ }
+
+ extension.sendMessage("page-action-get-popup");
+ let url = yield extension.awaitMessage("page-action-got-popup");
+
+ if (name == "") {
+ ok(url == name, "Calling pageAction.getPopup should return an empty string when the popup is not set.");
+
+ // The onClicked listener should get called when the popup is set to an empty string.
+ extension.sendMessage("page-action-enable-onClicked-listener");
+ yield extension.awaitMessage("page-action-onClicked-listener-enabled");
+
+ clickPageAction(uuid);
+ yield extension.awaitMessage("page-action-onClicked-fired");
+
+ extension.sendMessage("page-action-disable-onClicked-listener");
+ yield extension.awaitMessage("page-action-onClicked-listener-disabled");
+ } else {
+ ok(url.includes(name), "Calling pageAction.getPopup should return the correct popup URL when the popup is set.");
+
+ clickPageAction(uuid);
+ let location = yield extension.awaitMessage("page-action-from-popup");
+ ok(location.includes(name), "The popup with the correct URL should be shown.");
+
+ extension.sendMessage("page-action-close-popup", {location});
+
+ url = yield tabClosedPromise();
+ ok(url.includes(name), "The tab for the popup should be closed.");
+ }
+ }
+
+ yield extension.startup();
+ let {uuid} = yield extension.awaitMessage("ready");
+
+ extension.sendMessage("page-action-show");
+ yield extension.awaitMessage("page-action-shown");
+ ok(isPageActionShown(uuid), "The PageAction should be shown.");
+
+ yield testPopup("default.html", uuid);
+ yield testPopup("a.html", uuid);
+ yield testPopup("", uuid);
+ yield testPopup("b.html", uuid);
+
+ yield extension.unload();
+ ok(!isPageActionShown(uuid), "The PageAction should be removed after unload.");
+});
+</script>
+
+</body>
+</html>
diff --git a/mobile/android/components/moz.build b/mobile/android/components/moz.build
new file mode 100644
index 000000000..cac34b603
--- /dev/null
+++ b/mobile/android/components/moz.build
@@ -0,0 +1,48 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+XPIDL_SOURCES += [
+ 'SessionStore.idl',
+]
+
+XPIDL_MODULE = 'MobileComponents'
+
+EXTRA_COMPONENTS += [
+ 'AboutRedirector.js',
+ 'AddonUpdateService.js',
+ 'BlocklistPrompt.js',
+ 'BrowserCLH.js',
+ 'ColorPicker.js',
+ 'ContentDispatchChooser.js',
+ 'ContentPermissionPrompt.js',
+ 'DirectoryProvider.js',
+ 'FilePicker.js',
+ 'FxAccountsPush.js',
+ 'HelperAppDialog.js',
+ 'ImageBlockingPolicy.js',
+ 'LoginManagerPrompter.js',
+ 'NSSDialogService.js',
+ 'PersistentNotificationHandler.js',
+ 'PresentationDevicePrompt.js',
+ 'PresentationRequestUIGlue.js',
+ 'PromptService.js',
+ 'SessionStore.js',
+ 'SiteSpecificUserAgent.js',
+ 'Snippets.js',
+ 'TabSource.js',
+ 'XPIDialogService.js',
+]
+
+# Keep it this way if at all possible. If you need preprocessing,
+# consider adding fields to AppConstants.jsm.
+EXTRA_PP_COMPONENTS += [
+ 'MobileComponents.manifest',
+]
+
+DIRS += [
+ 'extensions',
+ 'build',
+]