new file mode 100644
--- /dev/null
+++ b/toolkit/components/cloudstorage/CloudStorage.jsm
@@ -0,0 +1,591 @@
+/* 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/. */
+
+/**
+ * Java Script module that helps consumers store data directly
+ * to cloud storage provider download folders.
+ *
+ * Takes cloud storage providers metadata as JSON input on Mac, Linux and Windows.
+ *
+ * Handles scan, prompt response save and exposes preferred storage provider.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["CloudStorage"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.importGlobalProperties(["fetch"]);
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+
+const CLOUD_SERVICES_PREF = "cloud.services.";
+const CLOUD_PROVIDERS_URI = "resource://cloudstorage/providers.json";
+
+/**
+ * Provider metadata JSON is loaded from resource://cloudstorage/providers.json
+ * Sample providers.json format
+ *
+ * {
+ * "Dropbox": {
+ * "displayName": "Dropbox",
+ * "relativeDownloadPath": ["homeDir", "Dropbox"],
+ * "relativeDiscoveryPath": {
+ * "linux": ["homeDir", ".dropbox", "info.json"],
+ * "macosx": ["homeDir", ".dropbox", "info.json"],
+ * "win": ["LocalAppData", "Dropbox", "info.json"]
+ * },
+ * "typeSpecificData": {
+ * "default": "Downloads",
+ * "screenshot": "Screenshots"
+ * }
+ * }
+ *
+ * Providers JSON is flat list of providers metdata with property as key in format @Provider
+ *
+ * @Provider - Unique cloud provider key, possible values: "Dropbox", "GDrive"
+ *
+ * @displayName - cloud storage name displayed in the prompt.
+ *
+ * @relativeDownloadPath - download path on user desktop for a cloud storage provider.
+ * By default downloadPath is a concatenation of home dir and name of dropbox folder.
+ * Example value: ["homeDir", "Dropbox"]
+ *
+ * @relativeDiscoveryPath - Lists discoveryPath by platform. Provider is not supported on a platform
+ * if its value doesn't exist in relativeDiscoveryPath. relativeDiscoveryPath by platform is stored
+ * as an array ofsubdirectories, which when concatenated, forms discovery path.
+ * During scan discoveryPath is checked for the existence of cloud storage provider on user desktop.
+ *
+ * @typeSpecificData - provides folder name for a cloud storage depending
+ * on type of data downloaded. Default folder is 'Downloads'. Other options are
+ * 'screenshot' depending on provider support.
+ */
+
+/**
+ *
+ * Internal cloud services prefs
+ *
+ * cloud.services.storage.key - set to string with preferred provider key
+ *
+ * cloud.services.lastPrompt - set to time when last prompt was shown
+ *
+ * cloud.services.interval.prompt - set to time interval in days after which prompt should be shown
+ *
+ * cloud.services.rejected.key - set to string with comma separated provider keys rejected
+ * by user when prompted to opt-in
+ *
+ * browser.download.folderList - set to int and indicates the location users wish to save downloaded files to.
+ * 0 - The desktop is the default download location.
+ * 1 - The system's downloads folder is the default download location.
+ * 2 - The default download location is elsewhere as specified in
+ * browser.download.dir.
+ * 3 - The default download location is elsewhere as specified by
+ * cloud storage API getDownloadFolder
+ *
+ * browser.download.dir - local file handle
+ * A local folder user may have selected for downloaded files to be
+ * saved. This folder is enabled when folderList equals 2.
+ */
+
+/**
+ * The external API exported by this module.
+ */
+
+this.CloudStorage = {
+ /**
+ * Init method to initialize providers metadata
+ */
+ async init() {
+ let isInitialized = null;
+ try {
+ // Invoke internal method asynchronously to read and
+ // parse providers metadata from JSON
+ isInitialized = await CloudStorageInternal.initProviders();
+ } catch (err) {
+ Cu.reportError(err);
+ }
+ return isInitialized;
+ },
+
+ /**
+ * Returns information to allow the consumer to decide whether showing
+ * a doorhanger prompt is appropriate. If a preferred provider is set
+ * on desktop, user is not prompted again and method returns null.
+ *
+ * @return {Promise} which resolves to an object with property name
+ * as 'key' and 'value'.
+ * 'key' property is provider key such as 'Dropbox', 'GDrive'.
+ * 'value' property contains metadata for respective provider.
+ * Resolves null if it's not appropriate to prompt.
+ */
+ promisePromptInfo() {
+ return CloudStorageInternal.promisePromptInfo();
+ },
+
+ /**
+ * Save user response from doorhanger prompt.
+ * If user confirms and checks 'always remember', update prefs
+ * cloud.services.storage.key and browser.download.folderList to pick
+ * download location from cloud storage API
+ * If user denies, save provider as rejected in cloud.services.rejected.key
+ *
+ * @param key
+ * cloud storage provider key from provider metadata
+ * @param remember
+ * bool value indicating whether user has asked to always remember
+ * the settings
+ * @param selected
+ * bool value by default set to false indicating if user has selected
+ * to save downloaded file with cloud provider
+ */
+ savePromptResponse(key, remember, selected = false) {
+ Services.prefs.setIntPref(CLOUD_SERVICES_PREF + "lastprompt",
+ Math.floor(Date.now() / 1000));
+ if (remember) {
+ if (selected) {
+ CloudStorageInternal.setCloudStoragePref(key);
+ } else {
+ // Store provider as rejected by setting cloud.services.rejected.key
+ // and not use in re-prompt
+ CloudStorageInternal.handleRejected(key);
+ }
+ }
+ },
+
+ /**
+ * Retrieve download folder of an opted-in storage provider
+ * by type specific data
+ * @param typeSpecificData
+ * type of data downloaded, options are 'default', 'screenshot'
+ * @return {Promise} which resolves to full path to provider download folder
+ */
+ getDownloadFolder(typeSpecificData) {
+ return CloudStorageInternal.getDownloadFolder(typeSpecificData);
+ },
+
+ /**
+ * Get provider opted-in by user to store downloaded files
+ *
+ * @return {String}
+ * Storage provider key from provider metadata. Return empty string
+ * if user has not selected a preferred provider.
+ */
+ getPreferredProvider() {
+ return CloudStorageInternal.preferredProviderKey;
+ },
+
+ /**
+ * Get providers found on user desktop. Used for unit tests
+ *
+ * @return {Promise}
+ * @resolves
+ * Map object with entries key set to storage provider key and values set to
+ * storage provider metadata
+ */
+ getStorageProviders() {
+ return CloudStorageInternal.getStorageProviders();
+ },
+};
+
+/**
+ * The internal API for the CloudStorage module.
+ */
+
+var CloudStorageInternal = {
+ /**
+ * promiseInit saves returned init method promise and is
+ * used to wait for initialization to complete.
+ */
+ promiseInit: null,
+
+ /**
+ * Internal property having storage providers data
+ */
+ providersMetaData: null,
+
+ async _downloadJSON(uri) {
+ let json = null;
+ try {
+ let response = await fetch(uri);
+ if (response.ok) {
+ json = await response.json();
+ }
+ } catch (e) {
+ Cu.reportError("Fetching " + uri + " results in error: " + e);
+ }
+ return json;
+ },
+
+ /**
+ * Loads storage providers metadata asynchronously from providers.json.
+ *
+ * @returns {Promise} with resolved boolean value true if providers
+ * metadata is successfully initialized
+ */
+ async initProviders() {
+ let response = await this._downloadJSON(CLOUD_PROVIDERS_URI);
+ this.providersMetaData = await this._parseProvidersJSON(response);
+
+ let providersCount = Object.keys(this.providersMetaData).length;
+ if (providersCount > 0) {
+ // Array of boolean results for each provider handled for custom downloadpath
+ let handledProviders = await this.initDownloadPathIfProvidersExist();
+ if (handledProviders.length === providersCount) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Load parsed metadata inside providers object
+ */
+ _parseProvidersJSON(providers) {
+ if (!providers) {
+ return {};
+ }
+
+ // Use relativeDiscoveryPath to filter providers object by platform.
+ // DownloadPath and discoveryPath are stored as
+ // array of subdirectories inside providers.json
+ // Update providers object discoveryPath and downloadPath
+ // property values by concatenating subdirectories and forming platform
+ // specific directory path
+
+ Object.getOwnPropertyNames(providers).forEach(key => {
+ if (providers[key].relativeDiscoveryPath.hasOwnProperty(AppConstants.platform)) {
+ providers[key].discoveryPath =
+ this._concatPath(providers[key].relativeDiscoveryPath[AppConstants.platform]);
+ providers[key].downloadPath =
+ this._concatPath(providers[key].relativeDownloadPath);
+ } else {
+ // delete key not supported on AppConstants.platform
+ delete providers[key];
+ }
+ });
+ return providers;
+ },
+
+ /**
+ * Concatenate subdir value inside array to form
+ * platform specific directory path
+ *
+ * @param arrDirs
+ * String Array containing sub directories name
+ * @returns Path of type String
+ */
+ _concatPath(arrDirs) {
+ let dirPath = "";
+ for (let subDir of arrDirs) {
+ switch (subDir) {
+ case "homeDir":
+ subDir = OS.Constants.Path.homeDir ? OS.Constants.Path.homeDir : "";
+ break;
+ case "LocalAppData":
+ if (OS.Constants.Win) {
+ let nsIFileLocal = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
+ subDir = nsIFileLocal && nsIFileLocal.path ? nsIFileLocal.path : "";
+ } else {
+ subDir = "";
+ }
+ break;
+ }
+ dirPath = OS.Path.join(dirPath, subDir);
+ }
+ return dirPath;
+ },
+
+ /**
+ * Check for custom download paths and override providers metadata
+ * downloadPath property
+ *
+ * For dropbox open config file ~/.dropbox/info.json
+ * and override downloadPath with path found
+ * See https://www.dropbox.com/en/help/desktop-web/find-folder-paths
+ *
+ * For all other providers we are using downloadpath from providers.json
+ *
+ * @returns {Promise} with array boolean values for respective provider. Value is true if a
+ * provider exist on user desktop and its downloadPath is updated. Promise returns with
+ * resolved array value when all providers in metadata are handled.
+ */
+ initDownloadPathIfProvidersExist() {
+ let providerKeys = Object.keys(this.providersMetaData);
+ let promises = providerKeys.map(key => {
+ return key === "Dropbox" ?
+ this._initDropbox(key) :
+ Promise.resolve(false);
+ });
+ return Promise.all(promises);
+ },
+
+ /**
+ * Read Dropbox info.json and override providers metadata
+ * downloadPath property
+ *
+ * @return {Promise}
+ * @resolves
+ * false if dropbox provider is not found. Returns true if dropbox service exist
+ * on user desktop and downloadPath in providermetadata is updated with
+ * value read from config file info.json
+ */
+ async _initDropbox(key) {
+ // Check if Dropbox provider exist on desktop before continuing
+ if (!await this._checkIfAssetExists(this.providersMetaData[key].discoveryPath)) {
+ return false;
+ }
+
+ // Check in cloud.services.rejected.key if Dropbox is previously rejected before continuing
+ let rejectedKeys = this.cloudStorageRejectedKeys.split(",");
+ if (rejectedKeys.includes(key)) {
+ return false;
+ }
+
+ let file = null;
+ try {
+ file = new FileUtils.File(this.providersMetaData[key].discoveryPath);
+ } catch (ex) {
+ return false;
+ }
+
+ let data = await this._downloadJSON(Services.io.newFileURI(file).spec);
+
+ if (!data) {
+ return false;
+ }
+
+ let path = data && data.personal && data.personal.path;
+ if (!path) {
+ return false;
+ }
+ let isUsable = await this._isUsableDirectory(path);
+ if (isUsable) {
+ this.providersMetaData.Dropbox.downloadPath = path;
+ }
+ return isUsable;
+ },
+
+ /**
+ * Determines if a given directory is valid and can be used to download files
+ *
+ * @param full absolute path to the directory
+ *
+ * @return {Promise} which resolves true if we can use the directory, false otherwise.
+ */
+ async _isUsableDirectory(path) {
+ let isUsable = false;
+ try {
+ let info = await OS.File.stat(path);
+ isUsable = info.isDir;
+ } catch (e) {
+ // Directory doesn't exist, so isUsable will still be false
+ }
+ return isUsable;
+ },
+
+ /**
+ * Retrieve download folder of preferred provider by type specific data
+ *
+ * @param dataType
+ * type of data downloaded, options are 'default', 'screenshot'
+ * default value is 'default'
+ * @return {Promise} which resolves to full path to download folder
+ */
+ async getDownloadFolder(dataType = "default") {
+ // Wait for cloudstorage to initialize if providers metadata is not available
+ if (!this.providersMetaData) {
+ let isInitialized = await this.promiseInit;
+ if (!isInitialized && !this.providersMetaData) {
+ Cu.reportError("CloudStorage: Failed to initialize and retrieve download folder ");
+ return null;
+ }
+ }
+
+ let key = this.preferredProviderKey;
+ if (!key || !this.providersMetaData.hasOwnProperty(key)) {
+ return null;
+ }
+
+ let provider = this.providersMetaData[key];
+ if (!provider.typeSpecificData[dataType]) {
+ return null;
+ }
+
+ let downloadDirPath = OS.Path.join(provider.downloadPath,
+ provider.typeSpecificData[dataType]);
+ if (!(await this._isUsableDirectory(downloadDirPath))) {
+ return null;
+ }
+ return downloadDirPath;
+ },
+
+ /**
+ * Return scanned provider info used by consumer inside doorhanger prompt.
+ * @return {Promise}
+ * which resolves to an object with property 'key' as found provider and
+ * property 'value' as provider metadata.
+ * Resolves null if no provider info is returned.
+ */
+ async promisePromptInfo() {
+ // Check if user has not previously opted-in for preferred provider download folder
+ // and if time elapsed since last prompt shown has exceeded maximum allowed interval
+ // in pref cloud.services.interval.prompt before continuing to scan for providers
+ if (!this.preferredProviderKey && this.shouldPrompt()) {
+ return this.scan();
+ }
+ return Promise.resolve(null);
+ },
+
+ /**
+ * Check if its time to prompt by reading lastprompt service pref.
+ * Return true if pref doesn't exist or last prompt time is
+ * more than prompt interval
+ */
+ shouldPrompt() {
+ let lastPrompt = this.lastPromptTime;
+ let now = Math.floor(Date.now() / 1000);
+ let interval = now - lastPrompt;
+
+ // Convert prompt interval to seconds
+ let maxAllow = this.promptInterval * 24 * 60 * 60;
+ return interval >= maxAllow;
+ },
+
+ /**
+ * Scans for local storage providers available on user desktop
+ *
+ * providers list is read in order as specified in providers.json.
+ * If a user has multiple cloud storage providers on desktop, return the first
+ * provider after filtering the rejected keys
+ *
+ * @return {Promise}
+ * which resolves to an object providerInfo with found provider key and value
+ * as provider metadata. Resolves null if no valid provider found
+ */
+ async scan() {
+ let providers = await this.getStorageProviders();
+ if (!providers.size) {
+ // No storage services installed on user desktop
+ return null;
+ }
+
+ // Filter the rejected providers in cloud.services.rejected.key
+ // from the providers map object
+ let rejectedKeys = this.cloudStorageRejectedKeys.split(",");
+ for (let rejectedKey of rejectedKeys) {
+ providers.delete(rejectedKey);
+ }
+
+ // Pick first storage provider from providers
+ let provider = providers.entries().next().value;
+ if (provider) {
+ return {key: provider[0], value: provider[1]};
+ }
+ return null;
+ },
+
+ /**
+ * Checks if the asset with input path exist on
+ * file system
+ * @return {Promise}
+ * @resolves
+ * boolean value of file existence check
+ */
+ _checkIfAssetExists(path) {
+ return OS.File.exists(path).catch(err => {
+ Cu.reportError(`Couldn't check existance of ${path}`, err);
+ return false;
+ });
+ },
+
+ /**
+ * get access to all local storage providers available on user desktop
+ *
+ * @return {Promise}
+ * @resolves
+ * Map object with entries key set to storage provider key and values set to
+ * storage provider metadata
+ */
+ async getStorageProviders() {
+ let providers = Object.entries(this.providersMetaData || {});
+
+ // Array of promises with boolean value exist for respective storage.
+ let promises = providers.map(([, provider]) => this._checkIfAssetExists(provider.discoveryPath));
+ let results = await Promise.all(promises);
+
+ // Filter providers array to remove provider with discoveryPath asset exist resolved value false
+ providers = providers.filter((_, idx) => results[idx]);
+ return new Map(providers);
+ },
+
+ /**
+ * Save the rejected provider in cloud.services.rejected.key. Pref
+ * stores rejected keys value as comma separated string.
+ *
+ * @param key
+ * Provider key to be saved in cloud.services.rejected.key pref
+ */
+ handleRejected(key) {
+ let rejected = this.cloudStorageRejectedKeys;
+
+ if (!rejected) {
+ Services.prefs.setCharPref(CLOUD_SERVICES_PREF + "rejected.key", key);
+ } else {
+ // Pref exists with previous rejected keys, append
+ // key at the end and update pref
+ let keys = rejected.split(",");
+ if (key) {
+ keys.push(key);
+ }
+ Services.prefs.setCharPref(CLOUD_SERVICES_PREF + "rejected.key", keys.join(","));
+ }
+ },
+
+ /**
+ *
+ * Sets pref cloud.services.storage.key. It updates download browser.download.folderList
+ * value to 3 indicating download location is stored elsewhere, as specified by
+ * cloud storage API getDownloadFolder
+ *
+ * @param key
+ * cloud storage provider key from provider metadata
+ */
+ setCloudStoragePref(key) {
+ Services.prefs.setCharPref(CLOUD_SERVICES_PREF + "storage.key", key);
+ Services.prefs.setIntPref("browser.download.folderList", 3);
+ },
+};
+
+/**
+ * Provider key retrieved from service pref cloud.services.storage.key
+ */
+XPCOMUtils.defineLazyPreferenceGetter(CloudStorageInternal, "preferredProviderKey",
+ CLOUD_SERVICES_PREF + "storage.key", "");
+
+/**
+ * Provider keys rejected by user for default download
+ */
+XPCOMUtils.defineLazyPreferenceGetter(CloudStorageInternal, "cloudStorageRejectedKeys",
+ CLOUD_SERVICES_PREF + "rejected.key", "");
+
+/**
+ * Lastprompt time in seconds, by default set to 0
+ */
+XPCOMUtils.defineLazyPreferenceGetter(CloudStorageInternal, "lastPromptTime",
+ CLOUD_SERVICES_PREF + "lastprompt", 0 /* 0 second */);
+
+/**
+ * show prompt interval in days, by default set to 0
+ */
+XPCOMUtils.defineLazyPreferenceGetter(CloudStorageInternal, "promptInterval",
+ CLOUD_SERVICES_PREF + "interval.prompt", 0 /* 0 days */);
+
+CloudStorageInternal.promiseInit = CloudStorage.init();
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cloudstorage/content/providers.json
@@ -0,0 +1,27 @@
+{
+ "Dropbox": {
+ "displayName": "Dropbox",
+ "relativeDownloadPath": ["homeDir", "Dropbox"],
+ "relativeDiscoveryPath": {
+ "linux": ["homeDir", ".dropbox", "info.json"],
+ "macosx": ["homeDir", ".dropbox", "info.json"],
+ "win": ["LocalAppData", "Dropbox", "info.json"]
+ },
+ "typeSpecificData": {
+ "default": "Downloads",
+ "screenshot": "Screenshots"
+ }
+ },
+
+ "GDrive": {
+ "displayName": "Google Drive",
+ "relativeDownloadPath": ["homeDir", "Google Drive"],
+ "relativeDiscoveryPath": {
+ "macosx": ["homeDir", "Library", "Application Support", "Google", "Drive"],
+ "win": ["LocalAppData", "Google", "Drive"]
+ },
+ "typeSpecificData": {
+ "default": "Downloads"
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cloudstorage/jar.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+toolkit.jar:
+% resource cloudstorage %content/
+ content/ (content/*)
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cloudstorage/moz.build
@@ -0,0 +1,18 @@
+# -*- 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/.
+
+XPCSHELL_TESTS_MANIFESTS += [
+ 'tests/unit/xpcshell.ini',
+]
+
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+ 'CloudStorage.jsm',
+]
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'General')
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cloudstorage/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "plugin:mozilla/xpcshell-test"
+ ]
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cloudstorage/tests/unit/cloud/info.json
@@ -0,0 +1,1 @@
+{"personal": {"path": "Test"}}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cloudstorage/tests/unit/test_cloudstorage.js
@@ -0,0 +1,278 @@
+"use strict";
+
+// Globals
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CloudStorage",
+ "resource://gre/modules/CloudStorage.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+const CLOUD_SERVICES_PREF = "cloud.services.";
+const DROPBOX_DOWNLOAD_FOLDER = "Dropbox";
+const GOOGLE_DRIVE_DOWNLOAD_FOLDER = "Google Drive";
+const DROPBOX_CONFIG_FOLDER = (AppConstants.platform === "win") ? "Dropbox" :
+ ".dropbox";
+const DROPBOX_KEY = "Dropbox";
+const GDRIVE_KEY = "GDrive";
+
+var nsIDropboxFile, nsIGDriveFile;
+
+function run_test() {
+ registerFakePath("Home", do_get_file("cloud/"));
+ registerFakePath("LocalAppData", do_get_file("cloud/"));
+ do_register_cleanup(() => {
+ cleanupPrefs();
+ });
+ run_next_test();
+}
+
+/**
+ * Replaces a directory service entry with a given nsIFile.
+ */
+function registerFakePath(key, file) {
+ let dirsvc = Services.dirsvc.QueryInterface(Ci.nsIProperties);
+ let originalFile;
+ try {
+ // If a file is already provided save it and undefine, otherwise set will
+ // throw for persistent entries (ones that are cached).
+ originalFile = dirsvc.get(key, Ci.nsIFile);
+ dirsvc.undefine(key);
+ } catch (e) {
+ // dirsvc.get will throw if nothing provides for the key and dirsvc.undefine
+ // will throw if it's not a persistent entry, in either case we don't want
+ // to set the original file in cleanup.
+ originalFile = undefined;
+ }
+
+ dirsvc.set(key, file);
+ do_register_cleanup(() => {
+ dirsvc.undefine(key);
+ if (originalFile) {
+ dirsvc.set(key, originalFile);
+ }
+ });
+}
+
+function mock_dropbox() {
+ let discoveryFolder = null;
+ if (AppConstants.platform === "win") {
+ discoveryFolder = FileUtils.getFile("LocalAppData", [DROPBOX_CONFIG_FOLDER]);
+ } else {
+ discoveryFolder = FileUtils.getFile("Home", [DROPBOX_CONFIG_FOLDER]);
+ }
+ discoveryFolder.append("info.json");
+ let fileDir = discoveryFolder.parent;
+ if (!fileDir.exists()) {
+ fileDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ do_get_file("cloud/info.json").copyTo(fileDir, "info.json");
+ let exist = fileDir.exists();
+ Assert.ok(exist, "file exists on desktop");
+
+ // Mock Dropbox Download folder in Home directory
+ let downloadFolder = FileUtils.getFile("Home", [DROPBOX_DOWNLOAD_FOLDER, "Downloads"]);
+ if (!downloadFolder.exists()) {
+ downloadFolder.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+
+ do_register_cleanup(() => {
+ if (discoveryFolder.exists()) {
+ discoveryFolder.remove(false);
+ }
+ if (downloadFolder.exists()) {
+ downloadFolder.remove(false);
+ }
+ });
+
+ return discoveryFolder;
+}
+
+function mock_gdrive() {
+ let discoveryFolder = null;
+ if (AppConstants.platform === "win") {
+ discoveryFolder = FileUtils.getFile("LocalAppData", ["Google", "Drive"]);
+ } else {
+ discoveryFolder = FileUtils.getFile("Home", ["Library", "Application Support", "Google", "Drive"]);
+ }
+ if (!discoveryFolder.exists()) {
+ discoveryFolder.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+ let exist = discoveryFolder.exists();
+ Assert.ok(exist, "file exists on desktop");
+
+ // Mock Google Drive Download folder in Home directory
+ let downloadFolder = FileUtils.getFile("Home", [GOOGLE_DRIVE_DOWNLOAD_FOLDER, "Downloads"]);
+ if (!downloadFolder.exists()) {
+ downloadFolder.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ }
+
+ do_register_cleanup(() => {
+ if (discoveryFolder.exists()) {
+ discoveryFolder.remove(false);
+ }
+ if (downloadFolder.exists()) {
+ downloadFolder.remove(false);
+ }
+ });
+
+ return discoveryFolder;
+}
+
+function cleanupPrefs() {
+ try {
+ Services.prefs.clearUserPref(CLOUD_SERVICES_PREF + "lastprompt");
+ Services.prefs.clearUserPref(CLOUD_SERVICES_PREF + "storage.key");
+ Services.prefs.clearUserPref(CLOUD_SERVICES_PREF + "rejected.key");
+ Services.prefs.clearUserPref(CLOUD_SERVICES_PREF + "interval.prompt");
+ Services.prefs.setIntPref("browser.download.folderList", 2);
+ } catch (e) {
+ do_throw("Failed to cleanup prefs: " + e);
+ }
+}
+
+function promiseGetStorageProviders() {
+ return CloudStorage.getStorageProviders();
+}
+
+function promisePromptInfo() {
+ return CloudStorage.promisePromptInfo();
+}
+
+async function checkScan(expectedKey) {
+ let metadata = await promiseGetStorageProviders();
+ let scanProvider = await promisePromptInfo();
+
+ if (!expectedKey) {
+ Assert.equal(metadata.size, 0, "Number of storage providers");
+ Assert.ok(!scanProvider, "No provider in scan results");
+ } else {
+ Assert.ok(metadata.size, "Number of storage providers");
+ Assert.equal(scanProvider.key, expectedKey, "Scanned provider key returned");
+ }
+ return metadata;
+}
+
+async function checkSavedPromptResponse(aKey, metadata, remember, selected = false) {
+ CloudStorage.savePromptResponse(aKey, remember, selected);
+
+ if (remember && selected) {
+ // Save prompt response with option to always remember the setting
+ // and provider with aKey selected as cloud storage provider
+ // Sets user download settings to always save to cloud
+
+ // Check preferred provider key, should be set to dropbox
+ let prefProvider = CloudStorage.getPreferredProvider();
+ Assert.equal(prefProvider, aKey, "Saved Response preferred provider key returned");
+ // Check browser.download.folderlist pref should be set to 3
+ Assert.equal(Services.prefs.getIntPref("browser.download.folderList"), 3,
+ "Default download location set to 3");
+
+ // Preferred download folder should be set to provider downloadPath from metadata
+ let path = await CloudStorage.getDownloadFolder();
+ let nsIDownloadFolder = new FileUtils.File(path);
+ Assert.ok(nsIDownloadFolder, "Download folder retrieved");
+ Assert.equal(nsIDownloadFolder.parent.path, metadata.get(aKey).downloadPath,
+ "Default download Folder Path");
+ } else if (remember && !selected) {
+ // Save prompt response with option to always remember the setting
+ // and provider with aKey rejected as cloud storage provider
+ // Sets cloud.services.rejected.key pref with provider key.
+ // Provider is ignored in next scan and never re-prompted again
+
+ let scanResult = await promisePromptInfo();
+ if (scanResult) {
+ Assert.notEqual(scanResult.key, DROPBOX_KEY, "Scanned provider key returned is not Dropbox");
+ } else {
+ Assert.ok(!scanResult, "No provider in scan results");
+ }
+ }
+}
+
+
+
+add_task(async function test_checkInit() {
+ let {CloudStorageInternal} = Cu.import("resource://gre/modules/CloudStorage.jsm", {});
+ let isInitialized = await CloudStorageInternal.promiseInit;
+ Assert.ok(isInitialized, "Providers Metadata successfully initialized");
+});
+
+add_task(async function test_noStorageProvider() {
+ await checkScan();
+ cleanupPrefs();
+});
+
+/**
+ * Check scan and save prompt response flow if only dropbox exists on desktop.
+ */
+add_task(async function test_dropboxStorageProvider() {
+ nsIDropboxFile = mock_dropbox();
+ let result = await checkScan(DROPBOX_KEY);
+
+ // Always save to cloud
+ await checkSavedPromptResponse(DROPBOX_KEY, result, true, true);
+ cleanupPrefs();
+
+ // Reject dropbox as cloud storage provider and never re-prompt again
+ await checkSavedPromptResponse(DROPBOX_KEY, result, true);
+
+ // Uninstall dropbox by removing discovery folder
+ nsIDropboxFile.remove(false);
+ cleanupPrefs();
+});
+
+
+/**
+ * Check scan and save prompt response flow if only gdrive exists on desktop.
+ */
+add_task(async function test_gDriveStorageProvider() {
+ nsIGDriveFile = mock_gdrive();
+ let result;
+ if (AppConstants.platform === "linux") {
+ result = await checkScan();
+ } else {
+ result = await checkScan(GDRIVE_KEY);
+ }
+
+ if (result.size || AppConstants.platform !== "linux") {
+ // Always save to cloud
+ await checkSavedPromptResponse(GDRIVE_KEY, result, true, true);
+ cleanupPrefs();
+
+ // Reject Google Drive as cloud storage provider and never re-prompt again
+ await checkSavedPromptResponse(GDRIVE_KEY, result, true);
+ }
+ // Uninstall gDrive by removing discovery folder /Home/Library/Application Support/Google/Drive
+ nsIGDriveFile.remove(false);
+ cleanupPrefs();
+});
+
+/**
+ * Check scan and save prompt response flow if multiple provider exists on desktop.
+ */
+add_task(async function test_multipleStorageProvider() {
+ nsIDropboxFile = mock_dropbox();
+ nsIGDriveFile = mock_gdrive();
+
+ // Dropbox picked by scan if multiple providers found
+ let result = await checkScan(DROPBOX_KEY);
+
+ // Always save to cloud
+ await checkSavedPromptResponse(DROPBOX_KEY, result, true, true);
+ cleanupPrefs();
+
+ // Reject dropbox as cloud storage provider and never re-prompt again
+ await checkSavedPromptResponse(DROPBOX_KEY, result, true);
+
+ // Uninstall dropbox and gdrive by removing discovery folder
+ nsIDropboxFile.remove(false);
+ nsIGDriveFile.remove(false);
+ cleanupPrefs();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/cloudstorage/tests/unit/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ cloud/**
+
+[test_cloudstorage.js]
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -39,16 +39,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "CloudStorage",
+ "resource://gre/modules/CloudStorage.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gDownloadPlatform",
"@mozilla.org/toolkit/download-platform;1",
"mozIDownloadPlatform");
XPCOMUtils.defineLazyServiceGetter(this, "gEnvironment",
"@mozilla.org/process/environment;1",
"nsIEnvironment");
XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
@@ -306,16 +308,24 @@ this.DownloadIntegration = {
Ci.nsIFile);
directoryPath = directory.path;
await OS.File.makeDir(directoryPath, { ignoreExisting: true });
} catch(ex) {
// Either the preference isn't set or the directory cannot be created.
directoryPath = await this.getSystemDownloadsDirectory();
}
break;
+ case 3: // Cloud Storage
+ try {
+ directoryPath = await CloudStorage.getDownloadFolder();
+ } catch(ex) {
+ // Either the preference isn't set or the directory cannot be created.
+ directoryPath = await this.getSystemDownloadsDirectory();
+ }
+ break;
default:
directoryPath = await this.getSystemDownloadsDirectory();
}
return directoryPath;
},
/**
* Returns the temporary downloads directory asynchronously.
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -16,16 +16,17 @@ DIRS += [
'aboutcheckerboard',
'aboutmemory',
'aboutperformance',
'addoncompat',
'alerts',
'apppicker',
'asyncshutdown',
'browser',
+ 'cloudstorage',
'commandlines',
'contentprefs',
'contextualidentity',
'crashmonitor',
'diskspacewatcher',
'downloads',
'extensions',
'exthelper',
--- a/toolkit/mozapps/downloads/nsHelperAppDlg.js
+++ b/toolkit/mozapps/downloads/nsHelperAppDlg.js
@@ -264,16 +264,19 @@ nsUnknownContentTypeDialog.prototype = {
}
catch (ex) {
// When the default download directory is write-protected,
// prompt the user for a different target file.
}
// Check to make sure we have a valid directory, otherwise, prompt
if (result) {
+ // Notifications for CloudStorage API consumers to show offer
+ // prompts while downloading. See Bug 1365129
+ Services.obs.notifyObservers(null, "cloudstorage-prompt-notification", result.path);
// This path is taken when we have a writable default download directory.
aLauncher.saveDestinationAvailable(result);
return;
}
}
}
// Use file picker to show dialog.