diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /toolkit/components/cloudstorage | |
parent | Initial commit. (diff) | |
download | firefox-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')
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] |