summaryrefslogtreecommitdiffstats
path: root/toolkit/components/cloudstorage
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/cloudstorage
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/cloudstorage')
-rw-r--r--toolkit/components/cloudstorage/CloudStorage.jsm762
-rw-r--r--toolkit/components/cloudstorage/content/providers.json27
-rw-r--r--toolkit/components/cloudstorage/jar.mn7
-rw-r--r--toolkit/components/cloudstorage/moz.build18
-rw-r--r--toolkit/components/cloudstorage/tests/unit/cloud/info.json1
-rw-r--r--toolkit/components/cloudstorage/tests/unit/test_cloudstorage.js325
-rw-r--r--toolkit/components/cloudstorage/tests/unit/xpcshell.ini8
7 files changed, 1148 insertions, 0 deletions
diff --git a/toolkit/components/cloudstorage/CloudStorage.jsm b/toolkit/components/cloudstorage/CloudStorage.jsm
new file mode 100644
index 0000000000..577e802c0f
--- /dev/null
+++ b/toolkit/components/cloudstorage/CloudStorage.jsm
@@ -0,0 +1,762 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["CloudStorage"];
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Downloads",
+ "resource://gre/modules/Downloads.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileUtils",
+ "resource://gre/modules/FileUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(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.api.enabled - set to true to initialize and use Cloud Storage module
+ *
+ * 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.
+ */
+
+var 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 key of 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 metadata of provider opted-in by user to store downloaded files.
+ * Return preferred provider metadata without scanning by doing simple lookup
+ * inside storage providers metadata using preferred provider key
+ *
+ * @return {Object}
+ * Object with preferred provider metadata. Return null
+ * if user has not selected a preferred provider.
+ */
+ getPreferredProviderMetaData() {
+ return CloudStorageInternal.getPreferredProviderMetaData();
+ },
+
+ /**
+ * Get display name of a provider actively in use to store downloaded files
+ *
+ * @return {String}
+ * String with provider display name. Returns null if a provider
+ * is not in use.
+ */
+ getProviderIfInUse() {
+ return CloudStorageInternal.getProviderIfInUse();
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * Reset 'browser.download.folderList' cloud storage value '3' back
+ * to '2' or '1' depending on custom path or system default Downloads path
+ * in pref 'browser.download.dir'.
+ */
+ async resetFolderListPref() {
+ let folderListValue = Services.prefs.getIntPref(
+ "browser.download.folderList",
+ 0
+ );
+ if (folderListValue !== 3) {
+ return;
+ }
+
+ let downloadDirPath = null;
+ try {
+ let file = Services.prefs.getComplexValue(
+ "browser.download.dir",
+ Ci.nsIFile
+ );
+ downloadDirPath = file.path;
+ } catch (e) {}
+
+ if (
+ !downloadDirPath ||
+ downloadDirPath === (await Downloads.getSystemDownloadsDirectory())
+ ) {
+ // if downloadDirPath is the Downloads folder path or unspecified
+ folderListValue = 1;
+ } else if (
+ downloadDirPath === Services.dirsvc.get("Desk", Ci.nsIFile).path
+ ) {
+ // if downloadDirPath is the Desktop path
+ folderListValue = 0;
+ } else {
+ // otherwise
+ folderListValue = 2;
+ }
+ Services.prefs.setIntPref("browser.download.folderList", folderListValue);
+ },
+
+ /**
+ * Loads storage providers metadata asynchronously from providers.json.
+ *
+ * @returns {Promise} with resolved boolean value true if providers
+ * metadata is successfully initialized
+ */
+ async initProviders() {
+ // Cloud Storage API should continue initialization and load providers metadata
+ // only if a consumer add-on using API sets pref 'cloud.services.api.enabled' to true
+ // If API is not enabled, check and reset cloud storage value in folderList pref.
+ if (!this.isAPIEnabled) {
+ this.resetFolderListPref().catch(err => {
+ Cu.reportError("CloudStorage: Failed to reset folderList pref " + err);
+ });
+ return false;
+ }
+
+ 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
+ * Resolves null if a valid download folder is not found.
+ */
+ 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);
+ },
+
+ /**
+ * get access to preferred provider metadata by using preferred provider key
+ *
+ * @return {Object}
+ * Object with preferred provider metadata. Returns null if preferred provider is not set
+ */
+ getPreferredProviderMetaData() {
+ // Use preferred provider key to retrieve metadata from ProvidersMetaData
+ return this.providersMetaData.hasOwnProperty(this.preferredProviderKey)
+ ? this.providersMetaData[this.preferredProviderKey]
+ : null;
+ },
+
+ /**
+ * Get provider display name if cloud storage API is used by an add-on
+ * and user has set preferred provider and a valid download directory
+ * path exists on user desktop.
+ *
+ * @return {String}
+ * String with preferred provider display name. Returns null if provider is not in use.
+ */
+ async getProviderIfInUse() {
+ // Check if consumer add-on is present and user has set preferred provider key
+ // and a valid download path exist on user desktop
+ if (
+ this.isAPIEnabled &&
+ this.preferredProviderKey &&
+ (await this.getDownloadFolder())
+ ) {
+ let provider = this.getPreferredProviderMetaData();
+ return provider.displayName || null;
+ }
+ return null;
+ },
+};
+
+/**
+ * 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 */
+);
+
+/**
+ * generic pref that shows if cloud storage API is in use, by default set to false.
+ * Re-run CloudStorage init evertytime pref is set.
+ */
+XPCOMUtils.defineLazyPreferenceGetter(
+ CloudStorageInternal,
+ "isAPIEnabled",
+ CLOUD_SERVICES_PREF + "api.enabled",
+ false,
+ () => CloudStorage.init()
+);
+
+CloudStorageInternal.promiseInit = CloudStorage.init();
diff --git a/toolkit/components/cloudstorage/content/providers.json b/toolkit/components/cloudstorage/content/providers.json
new file mode 100644
index 0000000000..dd2ab6e252
--- /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"
+ }
+ }
+}
diff --git a/toolkit/components/cloudstorage/jar.mn b/toolkit/components/cloudstorage/jar.mn
new file mode 100644
index 0000000000..0b8a977c3a
--- /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/*)
diff --git a/toolkit/components/cloudstorage/moz.build b/toolkit/components/cloudstorage/moz.build
new file mode 100644
index 0000000000..fba9059000
--- /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")
diff --git a/toolkit/components/cloudstorage/tests/unit/cloud/info.json b/toolkit/components/cloudstorage/tests/unit/cloud/info.json
new file mode 100644
index 0000000000..0ba2e1f9f2
--- /dev/null
+++ b/toolkit/components/cloudstorage/tests/unit/cloud/info.json
@@ -0,0 +1 @@
+{"personal": {"path": "Test"}} \ No newline at end of file
diff --git a/toolkit/components/cloudstorage/tests/unit/test_cloudstorage.js b/toolkit/components/cloudstorage/tests/unit/test_cloudstorage.js
new file mode 100644
index 0000000000..1eede60976
--- /dev/null
+++ b/toolkit/components/cloudstorage/tests/unit/test_cloudstorage.js
@@ -0,0 +1,325 @@
+"use strict";
+
+// Globals
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "CloudStorage",
+ "resource://gre/modules/CloudStorage.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ 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() {
+ initPrefs();
+ registerFakePath("Home", do_get_file("cloud/"));
+ registerFakePath("LocalAppData", do_get_file("cloud/"));
+ registerCleanupFunction(() => {
+ cleanupPrefs();
+ });
+ run_next_test();
+}
+
+function initPrefs() {
+ Services.prefs.setBoolPref(CLOUD_SERVICES_PREF + "api.enabled", true);
+}
+
+/**
+ * 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);
+ registerCleanupFunction(() => {
+ 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);
+ }
+
+ registerCleanupFunction(() => {
+ 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);
+ }
+
+ registerCleanupFunction(() => {
+ 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.clearUserPref(CLOUD_SERVICES_PREF + "api.enabled");
+ 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 } = ChromeUtils.import(
+ "resource://gre/modules/CloudStorage.jsm",
+ null
+ );
+ 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();
+});
diff --git a/toolkit/components/cloudstorage/tests/unit/xpcshell.ini b/toolkit/components/cloudstorage/tests/unit/xpcshell.ini
new file mode 100644
index 0000000000..abba881a0b
--- /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]