diff options
Diffstat (limited to 'mobile/android/components')
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, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/\//g, "/"); + }, + + 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', +] |