summaryrefslogtreecommitdiffstats
path: root/toolkit/components/jsdownloads/src
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/jsdownloads/src')
-rw-r--r--toolkit/components/jsdownloads/src/DownloadCore.jsm2871
-rw-r--r--toolkit/components/jsdownloads/src/DownloadImport.jsm193
-rw-r--r--toolkit/components/jsdownloads/src/DownloadIntegration.jsm1273
-rw-r--r--toolkit/components/jsdownloads/src/DownloadLegacy.js309
-rw-r--r--toolkit/components/jsdownloads/src/DownloadList.jsm559
-rw-r--r--toolkit/components/jsdownloads/src/DownloadPlatform.cpp275
-rw-r--r--toolkit/components/jsdownloads/src/DownloadPlatform.h34
-rw-r--r--toolkit/components/jsdownloads/src/DownloadStore.jsm203
-rw-r--r--toolkit/components/jsdownloads/src/DownloadUIHelper.jsm243
-rw-r--r--toolkit/components/jsdownloads/src/Downloads.jsm305
-rw-r--r--toolkit/components/jsdownloads/src/Downloads.manifest2
-rw-r--r--toolkit/components/jsdownloads/src/moz.build31
12 files changed, 6298 insertions, 0 deletions
diff --git a/toolkit/components/jsdownloads/src/DownloadCore.jsm b/toolkit/components/jsdownloads/src/DownloadCore.jsm
new file mode 100644
index 000000000..d89dd5805
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -0,0 +1,2871 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+/**
+ * This file includes the following constructors and global objects:
+ *
+ * Download
+ * Represents a single download, with associated state and actions. This object
+ * is transient, though it can be included in a DownloadList so that it can be
+ * managed by the user interface and persisted across sessions.
+ *
+ * DownloadSource
+ * Represents the source of a download, for example a document or an URI.
+ *
+ * DownloadTarget
+ * Represents the target of a download, for example a file in the global
+ * downloads directory, or a file in the system temporary directory.
+ *
+ * DownloadError
+ * Provides detailed information about a download failure.
+ *
+ * DownloadSaver
+ * Template for an object that actually transfers the data for the download.
+ *
+ * DownloadCopySaver
+ * Saver object that simply copies the entire source file to the target.
+ *
+ * DownloadLegacySaver
+ * Saver object that integrates with the legacy nsITransfer interface.
+ *
+ * DownloadPDFSaver
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "Download",
+ "DownloadSource",
+ "DownloadTarget",
+ "DownloadError",
+ "DownloadSaver",
+ "DownloadCopySaver",
+ "DownloadLegacySaver",
+ "DownloadPDFSaver",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm")
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
+ "@mozilla.org/browser/download-history;1",
+ Ci.nsIDownloadHistory);
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher",
+ "@mozilla.org/uriloader/external-helper-app-service;1",
+ Ci.nsPIExternalAppLauncher);
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
+ "@mozilla.org/uriloader/external-helper-app-service;1",
+ Ci.nsIExternalHelperAppService);
+XPCOMUtils.defineLazyServiceGetter(this, "gPrintSettingsService",
+ "@mozilla.org/gfx/printsettings-service;1",
+ Ci.nsIPrintSettingsService);
+
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm");
+
+const BackgroundFileSaverStreamListener = Components.Constructor(
+ "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
+ "nsIBackgroundFileSaver");
+
+/**
+ * Returns true if the given value is a primitive string or a String object.
+ */
+function isString(aValue) {
+ // We cannot use the "instanceof" operator reliably across module boundaries.
+ return (typeof aValue == "string") ||
+ (typeof aValue == "object" && "charAt" in aValue);
+}
+
+/**
+ * Serialize the unknown properties of aObject into aSerializable.
+ */
+function serializeUnknownProperties(aObject, aSerializable)
+{
+ if (aObject._unknownProperties) {
+ for (let property in aObject._unknownProperties) {
+ aSerializable[property] = aObject._unknownProperties[property];
+ }
+ }
+}
+
+/**
+ * Check for any unknown properties in aSerializable and preserve those in the
+ * _unknownProperties field of aObject. aFilterFn is called for each property
+ * name of aObject and should return true only for unknown properties.
+ */
+function deserializeUnknownProperties(aObject, aSerializable, aFilterFn)
+{
+ for (let property in aSerializable) {
+ if (aFilterFn(property)) {
+ if (!aObject._unknownProperties) {
+ aObject._unknownProperties = { };
+ }
+
+ aObject._unknownProperties[property] = aSerializable[property];
+ }
+ }
+}
+
+/**
+ * This determines the minimum time interval between updates to the number of
+ * bytes transferred, and is a limiting factor to the sequence of readings used
+ * in calculating the speed of the download.
+ */
+const kProgressUpdateIntervalMs = 400;
+
+// Download
+
+/**
+ * Represents a single download, with associated state and actions. This object
+ * is transient, though it can be included in a DownloadList so that it can be
+ * managed by the user interface and persisted across sessions.
+ */
+this.Download = function ()
+{
+ this._deferSucceeded = Promise.defer();
+}
+
+this.Download.prototype = {
+ /**
+ * DownloadSource object associated with this download.
+ */
+ source: null,
+
+ /**
+ * DownloadTarget object associated with this download.
+ */
+ target: null,
+
+ /**
+ * DownloadSaver object associated with this download.
+ */
+ saver: null,
+
+ /**
+ * Indicates that the download never started, has been completed successfully,
+ * failed, or has been canceled. This property becomes false when a download
+ * is started for the first time, or when a failed or canceled download is
+ * restarted.
+ */
+ stopped: true,
+
+ /**
+ * Indicates that the download has been completed successfully.
+ */
+ succeeded: false,
+
+ /**
+ * Indicates that the download has been canceled. This property can become
+ * true, then it can be reset to false when a canceled download is restarted.
+ *
+ * This property becomes true as soon as the "cancel" method is called, though
+ * the "stopped" property might remain false until the cancellation request
+ * has been processed. Temporary files or part files may still exist even if
+ * they are expected to be deleted, until the "stopped" property becomes true.
+ */
+ canceled: false,
+
+ /**
+ * When the download fails, this is set to a DownloadError instance indicating
+ * the cause of the failure. If the download has been completed successfully
+ * or has been canceled, this property is null. This property is reset to
+ * null when a failed download is restarted.
+ */
+ error: null,
+
+ /**
+ * Indicates the start time of the download. When the download starts,
+ * this property is set to a valid Date object. The default value is null
+ * before the download starts.
+ */
+ startTime: null,
+
+ /**
+ * Indicates whether this download's "progress" property is able to report
+ * partial progress while the download proceeds, and whether the value in
+ * totalBytes is relevant. This depends on the saver and the download source.
+ */
+ hasProgress: false,
+
+ /**
+ * Progress percent, from 0 to 100. Intermediate values are reported only if
+ * hasProgress is true.
+ *
+ * @note You shouldn't rely on this property being equal to 100 to determine
+ * whether the download is completed. You should use the individual
+ * state properties instead.
+ */
+ progress: 0,
+
+ /**
+ * When hasProgress is true, indicates the total number of bytes to be
+ * transferred before the download finishes, that can be zero for empty files.
+ *
+ * When hasProgress is false, this property is always zero.
+ *
+ * @note This property may be different than the final file size on disk for
+ * downloads that are encoded during the network transfer. You can use
+ * the "size" property of the DownloadTarget object to get the actual
+ * size on disk once the download succeeds.
+ */
+ totalBytes: 0,
+
+ /**
+ * Number of bytes currently transferred. This value starts at zero, and may
+ * be updated regardless of the value of hasProgress.
+ *
+ * @note You shouldn't rely on this property being equal to totalBytes to
+ * determine whether the download is completed. You should use the
+ * individual state properties instead. This property may not be
+ * updated during the last part of the download.
+ */
+ currentBytes: 0,
+
+ /**
+ * Fractional number representing the speed of the download, in bytes per
+ * second. This value is zero when the download is stopped, and may be
+ * updated regardless of the value of hasProgress.
+ */
+ speed: 0,
+
+ /**
+ * Indicates whether, at this time, there is any partially downloaded data
+ * that can be used when restarting a failed or canceled download.
+ *
+ * Even if the download has partial data on disk, hasPartialData will be false
+ * if that data cannot be used to restart the download. In order to determine
+ * if a part file is being used which contains partial data the
+ * Download.target.partFilePath should be checked.
+ *
+ * This property is relevant while the download is in progress, and also if it
+ * failed or has been canceled. If the download has been completed
+ * successfully, this property is always false.
+ *
+ * Whether partial data can actually be retained depends on the saver and the
+ * download source, and may not be known before the download is started.
+ */
+ hasPartialData: false,
+
+ /**
+ * Indicates whether, at this time, there is any data that has been blocked.
+ * Since reputation blocking takes place after the download has fully
+ * completed a value of true also indicates 100% of the data is present.
+ */
+ hasBlockedData: false,
+
+ /**
+ * This can be set to a function that is called after other properties change.
+ */
+ onchange: null,
+
+ /**
+ * This tells if the user has chosen to open/run the downloaded file after
+ * download has completed.
+ */
+ launchWhenSucceeded: false,
+
+ /**
+ * This represents the MIME type of the download.
+ */
+ contentType: null,
+
+ /**
+ * This indicates the path of the application to be used to launch the file,
+ * or null if the file should be launched with the default application.
+ */
+ launcherPath: null,
+
+ /**
+ * Raises the onchange notification.
+ */
+ _notifyChange: function D_notifyChange() {
+ try {
+ if (this.onchange) {
+ this.onchange();
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ },
+
+ /**
+ * The download may be stopped and restarted multiple times before it
+ * completes successfully. This may happen if any of the download attempts is
+ * canceled or fails.
+ *
+ * This property contains a promise that is linked to the current attempt, or
+ * null if the download is either stopped or in the process of being canceled.
+ * If the download restarts, this property is replaced with a new promise.
+ *
+ * The promise is resolved if the attempt it represents finishes successfully,
+ * and rejected if the attempt fails.
+ */
+ _currentAttempt: null,
+
+ /**
+ * Starts the download for the first time, or restarts a download that failed
+ * or has been canceled.
+ *
+ * Calling this method when the download has been completed successfully has
+ * no effect, and the method returns a resolved promise. If the download is
+ * in progress, the method returns the same promise as the previous call.
+ *
+ * If the "cancel" method was called but the cancellation process has not
+ * finished yet, this method waits for the cancellation to finish, then
+ * restarts the download immediately.
+ *
+ * @note If you need to start a new download from the same source, rather than
+ * restarting a failed or canceled one, you should create a separate
+ * Download object with the same source as the current one.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+ start: function D_start()
+ {
+ // If the download succeeded, it's the final state, we have nothing to do.
+ if (this.succeeded) {
+ return Promise.resolve();
+ }
+
+ // If the download already started and hasn't failed or hasn't been
+ // canceled, return the same promise as the previous call, allowing the
+ // caller to wait for the current attempt to finish.
+ if (this._currentAttempt) {
+ return this._currentAttempt;
+ }
+
+ // While shutting down or disposing of this object, we prevent the download
+ // from returning to be in progress.
+ if (this._finalized) {
+ return Promise.reject(new DownloadError({
+ message: "Cannot start after finalization."}));
+ }
+
+ if (this.error && this.error.becauseBlockedByReputationCheck) {
+ return Promise.reject(new DownloadError({
+ message: "Cannot start after being blocked " +
+ "by a reputation check."}));
+ }
+
+ // Initialize all the status properties for a new or restarted download.
+ this.stopped = false;
+ this.canceled = false;
+ this.error = null;
+ this.hasProgress = false;
+ this.hasBlockedData = false;
+ this.progress = 0;
+ this.totalBytes = 0;
+ this.currentBytes = 0;
+ this.startTime = new Date();
+
+ // Create a new deferred object and an associated promise before starting
+ // the actual download. We store it on the download as the current attempt.
+ let deferAttempt = Promise.defer();
+ let currentAttempt = deferAttempt.promise;
+ this._currentAttempt = currentAttempt;
+
+ // Restart the progress and speed calculations from scratch.
+ this._lastProgressTimeMs = 0;
+
+ // This function propagates progress from the DownloadSaver object, unless
+ // it comes in late from a download attempt that was replaced by a new one.
+ // If the cancellation process for the download has started, then the update
+ // is ignored.
+ function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData)
+ {
+ if (this._currentAttempt == currentAttempt) {
+ this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData);
+ }
+ }
+
+ // This function propagates download properties from the DownloadSaver
+ // object, unless it comes in late from a download attempt that was
+ // replaced by a new one. If the cancellation process for the download has
+ // started, then the update is ignored.
+ function DS_setProperties(aOptions)
+ {
+ if (this._currentAttempt != currentAttempt) {
+ return;
+ }
+
+ let changeMade = false;
+
+ for (let property of ["contentType", "progress", "hasPartialData",
+ "hasBlockedData"]) {
+ if (property in aOptions && this[property] != aOptions[property]) {
+ this[property] = aOptions[property];
+ changeMade = true;
+ }
+ }
+
+ if (changeMade) {
+ this._notifyChange();
+ }
+ }
+
+ // Now that we stored the promise in the download object, we can start the
+ // task that will actually execute the download.
+ deferAttempt.resolve(Task.spawn(function* task_D_start() {
+ // Wait upon any pending operation before restarting.
+ if (this._promiseCanceled) {
+ yield this._promiseCanceled;
+ }
+ if (this._promiseRemovePartialData) {
+ try {
+ yield this._promiseRemovePartialData;
+ } catch (ex) {
+ // Ignore any errors, which are already reported by the original
+ // caller of the removePartialData method.
+ }
+ }
+
+ // In case the download was restarted while cancellation was in progress,
+ // but the previous attempt actually succeeded before cancellation could
+ // be processed, it is possible that the download has already finished.
+ if (this.succeeded) {
+ return;
+ }
+
+ try {
+ // Disallow download if parental controls service restricts it.
+ if (yield DownloadIntegration.shouldBlockForParentalControls(this)) {
+ throw new DownloadError({ becauseBlockedByParentalControls: true });
+ }
+
+ // Disallow download if needed runtime permissions have not been granted
+ // by user.
+ if (yield DownloadIntegration.shouldBlockForRuntimePermissions()) {
+ throw new DownloadError({ becauseBlockedByRuntimePermissions: true });
+ }
+
+ // We should check if we have been canceled in the meantime, after all
+ // the previous asynchronous operations have been executed and just
+ // before we call the "execute" method of the saver.
+ if (this._promiseCanceled) {
+ // The exception will become a cancellation in the "catch" block.
+ throw undefined;
+ }
+
+ // Execute the actual download through the saver object.
+ this._saverExecuting = true;
+ yield this.saver.execute(DS_setProgressBytes.bind(this),
+ DS_setProperties.bind(this));
+
+ // Now that the actual saving finished, read the actual file size on
+ // disk, that may be different from the amount of data transferred.
+ yield this.target.refresh();
+
+ // Check for the last time if the download has been canceled. This must
+ // be done right before setting the "stopped" property of the download,
+ // without any asynchronous operations in the middle, so that another
+ // cancellation request cannot start in the meantime and stay unhandled.
+ if (this._promiseCanceled) {
+ try {
+ yield OS.File.remove(this.target.path);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ this.target.exists = false;
+ this.target.size = 0;
+
+ // Cancellation exceptions will be changed in the catch block below.
+ throw new DownloadError();
+ }
+
+ // Update the status properties for a successful download.
+ this.progress = 100;
+ this.succeeded = true;
+ this.hasPartialData = false;
+ } catch (originalEx) {
+ // We may choose a different exception to propagate in the code below,
+ // or wrap the original one. We do this mutation in a different variable
+ // because of the "no-ex-assign" ESLint rule.
+ let ex = originalEx;
+
+ // Fail with a generic status code on cancellation, so that the caller
+ // is forced to actually check the status properties to see if the
+ // download was canceled or failed because of other reasons.
+ if (this._promiseCanceled) {
+ throw new DownloadError({ message: "Download canceled." });
+ }
+
+ // An HTTP 450 error code is used by Windows to indicate that a uri is
+ // blocked by parental controls. This will prevent the download from
+ // occuring, so an error needs to be raised. This is not performed
+ // during the parental controls check above as it requires the request
+ // to start.
+ if (this._blockedByParentalControls) {
+ ex = new DownloadError({ becauseBlockedByParentalControls: true });
+ }
+
+ // Update the download error, unless a new attempt already started. The
+ // change in the status property is notified in the finally block.
+ if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
+ if (!(ex instanceof DownloadError)) {
+ let properties = {innerException: ex};
+
+ if (ex.message) {
+ properties.message = ex.message;
+ }
+
+ ex = new DownloadError(properties);
+ }
+
+ this.error = ex;
+ }
+ throw ex;
+ } finally {
+ // Any cancellation request has now been processed.
+ this._saverExecuting = false;
+ this._promiseCanceled = null;
+
+ // Update the status properties, unless a new attempt already started.
+ if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
+ this._currentAttempt = null;
+ this.stopped = true;
+ this.speed = 0;
+ this._notifyChange();
+ if (this.succeeded) {
+ yield this._succeed();
+ }
+ }
+ }
+ }.bind(this)));
+
+ // Notify the new download state before returning.
+ this._notifyChange();
+ return currentAttempt;
+ },
+
+ /**
+ * Perform the actions necessary when a Download succeeds.
+ *
+ * @return {Promise}
+ * @resolves When the steps to take after success have completed.
+ * @rejects JavaScript exception if any of the operations failed.
+ */
+ _succeed: Task.async(function* () {
+ yield DownloadIntegration.downloadDone(this);
+
+ this._deferSucceeded.resolve();
+
+ if (this.launchWhenSucceeded) {
+ this.launch().then(null, Cu.reportError);
+
+ // Always schedule files to be deleted at the end of the private browsing
+ // mode, regardless of the value of the pref.
+ if (this.source.isPrivate) {
+ gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
+ new FileUtils.File(this.target.path));
+ } else if (Services.prefs.getBoolPref(
+ "browser.helperApps.deleteTempFileOnExit")) {
+ gExternalAppLauncher.deleteTemporaryFileOnExit(
+ new FileUtils.File(this.target.path));
+ }
+ }
+ }),
+
+ /**
+ * When a request to unblock the download is received, contains a promise
+ * that will be resolved when the unblock request is completed. This property
+ * will then continue to hold the promise indefinitely.
+ */
+ _promiseUnblock: null,
+
+ /**
+ * When a request to confirm the block of the download is received, contains
+ * a promise that will be resolved when cleaning up the download has
+ * completed. This property will then continue to hold the promise
+ * indefinitely.
+ */
+ _promiseConfirmBlock: null,
+
+ /**
+ * Unblocks a download which had been blocked by reputation.
+ *
+ * The file will be moved out of quarantine and the download will be
+ * marked as succeeded.
+ *
+ * @return {Promise}
+ * @resolves When the Download has been unblocked and succeeded.
+ * @rejects JavaScript exception if any of the operations failed.
+ */
+ unblock: function() {
+ if (this._promiseUnblock) {
+ return this._promiseUnblock;
+ }
+
+ if (this._promiseConfirmBlock) {
+ return Promise.reject(new Error(
+ "Download block has been confirmed, cannot unblock."));
+ }
+
+ if (!this.hasBlockedData) {
+ return Promise.reject(new Error(
+ "unblock may only be called on Downloads with blocked data."));
+ }
+
+ this._promiseUnblock = Task.spawn(function* () {
+ try {
+ yield OS.File.move(this.target.partFilePath, this.target.path);
+ yield this.target.refresh();
+ } catch (ex) {
+ yield this.refresh();
+ this._promiseUnblock = null;
+ throw ex;
+ }
+
+ this.succeeded = true;
+ this.hasBlockedData = false;
+ this._notifyChange();
+ yield this._succeed();
+ }.bind(this));
+
+ return this._promiseUnblock;
+ },
+
+ /**
+ * Confirms that a blocked download should be cleaned up.
+ *
+ * If a download was blocked but retained on disk this method can be used
+ * to remove the file.
+ *
+ * @return {Promise}
+ * @resolves When the Download's data has been removed.
+ * @rejects JavaScript exception if any of the operations failed.
+ */
+ confirmBlock: function() {
+ if (this._promiseConfirmBlock) {
+ return this._promiseConfirmBlock;
+ }
+
+ if (this._promiseUnblock) {
+ return Promise.reject(new Error(
+ "Download is being unblocked, cannot confirmBlock."));
+ }
+
+ if (!this.hasBlockedData) {
+ return Promise.reject(new Error(
+ "confirmBlock may only be called on Downloads with blocked data."));
+ }
+
+ this._promiseConfirmBlock = Task.spawn(function* () {
+ try {
+ yield OS.File.remove(this.target.partFilePath);
+ } catch (ex) {
+ yield this.refresh();
+ this._promiseConfirmBlock = null;
+ throw ex;
+ }
+
+ this.hasBlockedData = false;
+ this._notifyChange();
+ }.bind(this));
+
+ return this._promiseConfirmBlock;
+ },
+
+ /*
+ * Launches the file after download has completed. This can open
+ * the file with the default application for the target MIME type
+ * or file extension, or with a custom application if launcherPath
+ * is set.
+ *
+ * @return {Promise}
+ * @resolves When the instruction to launch the file has been
+ * successfully given to the operating system. Note that
+ * the OS might still take a while until the file is actually
+ * launched.
+ * @rejects JavaScript exception if there was an error trying to launch
+ * the file.
+ */
+ launch: function () {
+ if (!this.succeeded) {
+ return Promise.reject(
+ new Error("launch can only be called if the download succeeded")
+ );
+ }
+
+ return DownloadIntegration.launchDownload(this);
+ },
+
+ /*
+ * Shows the folder containing the target file, or where the target file
+ * will be saved. This may be called at any time, even if the download
+ * failed or is currently in progress.
+ *
+ * @return {Promise}
+ * @resolves When the instruction to open the containing folder has been
+ * successfully given to the operating system. Note that
+ * the OS might still take a while until the folder is actually
+ * opened.
+ * @rejects JavaScript exception if there was an error trying to open
+ * the containing folder.
+ */
+ showContainingDirectory: function D_showContainingDirectory() {
+ return DownloadIntegration.showContainingDirectory(this.target.path);
+ },
+
+ /**
+ * When a request to cancel the download is received, contains a promise that
+ * will be resolved when the cancellation request is processed. When the
+ * request is processed, this property becomes null again.
+ */
+ _promiseCanceled: null,
+
+ /**
+ * True between the call to the "execute" method of the saver and the
+ * completion of the current download attempt.
+ */
+ _saverExecuting: false,
+
+ /**
+ * Cancels the download.
+ *
+ * The cancellation request is asynchronous. Until the cancellation process
+ * finishes, temporary files or part files may still exist even if they are
+ * expected to be deleted.
+ *
+ * In case the download completes successfully before the cancellation request
+ * could be processed, this method has no effect, and it returns a resolved
+ * promise. You should check the properties of the download at the time the
+ * returned promise is resolved to determine if the download was cancelled.
+ *
+ * Calling this method when the download has been completed successfully,
+ * failed, or has been canceled has no effect, and the method returns a
+ * resolved promise. This behavior is designed for the case where the call
+ * to "cancel" happens asynchronously, and is consistent with the case where
+ * the cancellation request could not be processed in time.
+ *
+ * @return {Promise}
+ * @resolves When the cancellation process has finished.
+ * @rejects Never.
+ */
+ cancel: function D_cancel()
+ {
+ // If the download is currently stopped, we have nothing to do.
+ if (this.stopped) {
+ return Promise.resolve();
+ }
+
+ if (!this._promiseCanceled) {
+ // Start a new cancellation request.
+ let deferCanceled = Promise.defer();
+ this._currentAttempt.then(() => deferCanceled.resolve(),
+ () => deferCanceled.resolve());
+ this._promiseCanceled = deferCanceled.promise;
+
+ // The download can already be restarted.
+ this._currentAttempt = null;
+
+ // Notify that the cancellation request was received.
+ this.canceled = true;
+ this._notifyChange();
+
+ // Execute the actual cancellation through the saver object, in case it
+ // has already started. Otherwise, the cancellation will be handled just
+ // before the saver is started.
+ if (this._saverExecuting) {
+ this.saver.cancel();
+ }
+ }
+
+ return this._promiseCanceled;
+ },
+
+ /**
+ * Indicates whether any partially downloaded data should be retained, to use
+ * when restarting a failed or canceled download. The default is false.
+ *
+ * Whether partial data can actually be retained depends on the saver and the
+ * download source, and may not be known before the download is started.
+ *
+ * To have any effect, this property must be set before starting the download.
+ * Resetting this property to false after the download has already started
+ * will not remove any partial data.
+ *
+ * If this property is set to true, care should be taken that partial data is
+ * removed before the reference to the download is discarded. This can be
+ * done using the removePartialData or the "finalize" methods.
+ */
+ tryToKeepPartialData: false,
+
+ /**
+ * When a request to remove partially downloaded data is received, contains a
+ * promise that will be resolved when the removal request is processed. When
+ * the request is processed, this property becomes null again.
+ */
+ _promiseRemovePartialData: null,
+
+ /**
+ * Removes any partial data kept as part of a canceled or failed download.
+ *
+ * If the download is not canceled or failed, this method has no effect, and
+ * it returns a resolved promise. If the "cancel" method was called but the
+ * cancellation process has not finished yet, this method waits for the
+ * cancellation to finish, then removes the partial data.
+ *
+ * After this method has been called, if the tryToKeepPartialData property is
+ * still true when the download is restarted, partial data will be retained
+ * during the new download attempt.
+ *
+ * @return {Promise}
+ * @resolves When the partial data has been successfully removed.
+ * @rejects JavaScript exception if the operation could not be completed.
+ */
+ removePartialData: function ()
+ {
+ if (!this.canceled && !this.error) {
+ return Promise.resolve();
+ }
+
+ let promiseRemovePartialData = this._promiseRemovePartialData;
+
+ if (!promiseRemovePartialData) {
+ let deferRemovePartialData = Promise.defer();
+ promiseRemovePartialData = deferRemovePartialData.promise;
+ this._promiseRemovePartialData = promiseRemovePartialData;
+
+ deferRemovePartialData.resolve(
+ Task.spawn(function* task_D_removePartialData() {
+ try {
+ // Wait upon any pending cancellation request.
+ if (this._promiseCanceled) {
+ yield this._promiseCanceled;
+ }
+ // Ask the saver object to remove any partial data.
+ yield this.saver.removePartialData();
+ // For completeness, clear the number of bytes transferred.
+ if (this.currentBytes != 0 || this.hasPartialData) {
+ this.currentBytes = 0;
+ this.hasPartialData = false;
+ this._notifyChange();
+ }
+ } finally {
+ this._promiseRemovePartialData = null;
+ }
+ }.bind(this)));
+ }
+
+ return promiseRemovePartialData;
+ },
+
+ /**
+ * This deferred object contains a promise that is resolved as soon as this
+ * download finishes successfully, and is never rejected. This property is
+ * initialized when the download is created, and never changes.
+ */
+ _deferSucceeded: null,
+
+ /**
+ * Returns a promise that is resolved as soon as this download finishes
+ * successfully, even if the download was stopped and restarted meanwhile.
+ *
+ * You can use this property for scheduling download completion actions in the
+ * current session, for downloads that are controlled interactively. If the
+ * download is not controlled interactively, you should use the promise
+ * returned by the "start" method instead, to check for success or failure.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects Never.
+ */
+ whenSucceeded: function D_whenSucceeded()
+ {
+ return this._deferSucceeded.promise;
+ },
+
+ /**
+ * Updates the state of a finished, failed, or canceled download based on the
+ * current state in the file system. If the download is in progress or it has
+ * been finalized, this method has no effect, and it returns a resolved
+ * promise.
+ *
+ * This allows the properties of the download to be updated in case the user
+ * moved or deleted the target file or its associated ".part" file.
+ *
+ * @return {Promise}
+ * @resolves When the operation has completed.
+ * @rejects Never.
+ */
+ refresh: function ()
+ {
+ return Task.spawn(function* () {
+ if (!this.stopped || this._finalized) {
+ return;
+ }
+
+ if (this.succeeded) {
+ let oldExists = this.target.exists;
+ let oldSize = this.target.size;
+ yield this.target.refresh();
+ if (oldExists != this.target.exists || oldSize != this.target.size) {
+ this._notifyChange();
+ }
+ return;
+ }
+
+ // Update the current progress from disk if we retained partial data.
+ if ((this.hasPartialData || this.hasBlockedData) &&
+ this.target.partFilePath) {
+
+ try {
+ let stat = yield OS.File.stat(this.target.partFilePath);
+
+ // Ignore the result if the state has changed meanwhile.
+ if (!this.stopped || this._finalized) {
+ return;
+ }
+
+ // Update the bytes transferred and the related progress properties.
+ this.currentBytes = stat.size;
+ if (this.totalBytes > 0) {
+ this.hasProgress = true;
+ this.progress = Math.floor(this.currentBytes /
+ this.totalBytes * 100);
+ }
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ // Ignore the result if the state has changed meanwhile.
+ if (!this.stopped || this._finalized) {
+ return;
+ }
+
+ this.hasBlockedData = false;
+ this.hasPartialData = false;
+ }
+
+ this._notifyChange();
+ }
+ }.bind(this)).then(null, Cu.reportError);
+ },
+
+ /**
+ * True if the "finalize" method has been called. This prevents the download
+ * from starting again after having been stopped.
+ */
+ _finalized: false,
+
+ /**
+ * Ensures that the download is stopped, and optionally removes any partial
+ * data kept as part of a canceled or failed download. After this method has
+ * been called, the download cannot be started again.
+ *
+ * This method should be used in place of "cancel" and removePartialData while
+ * shutting down or disposing of the download object, to prevent other callers
+ * from interfering with the operation. This is required because cancellation
+ * and other operations are asynchronous.
+ *
+ * @param aRemovePartialData
+ * Whether any partially downloaded data should be removed after the
+ * download has been stopped.
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished successfully.
+ * @rejects JavaScript exception if an error occurred while removing the
+ * partially downloaded data.
+ */
+ finalize: function (aRemovePartialData)
+ {
+ // Prevents the download from starting again after having been stopped.
+ this._finalized = true;
+
+ if (aRemovePartialData) {
+ // Cancel the download, in case it is currently in progress, then remove
+ // any partially downloaded data. The removal operation waits for
+ // cancellation to be completed before resolving the promise it returns.
+ this.cancel();
+ return this.removePartialData();
+ }
+ // Just cancel the download, in case it is currently in progress.
+ return this.cancel();
+ },
+
+ /**
+ * Indicates the time of the last progress notification, expressed as the
+ * number of milliseconds since January 1, 1970, 00:00:00 UTC. This is zero
+ * until some bytes have actually been transferred.
+ */
+ _lastProgressTimeMs: 0,
+
+ /**
+ * Updates progress notifications based on the number of bytes transferred.
+ *
+ * The number of bytes transferred is not updated unless enough time passed
+ * since this function was last called. This limits the computation load, in
+ * particular when the listeners update the user interface in response.
+ *
+ * @param aCurrentBytes
+ * Number of bytes transferred until now.
+ * @param aTotalBytes
+ * Total number of bytes to be transferred, or -1 if unknown.
+ * @param aHasPartialData
+ * Indicates whether the partially downloaded data can be used when
+ * restarting the download if it fails or is canceled.
+ */
+ _setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
+ let changeMade = (this.hasPartialData != aHasPartialData);
+ this.hasPartialData = aHasPartialData;
+
+ // Unless aTotalBytes is -1, we can report partial download progress. In
+ // this case, notify when the related properties changed since last time.
+ if (aTotalBytes != -1 && (!this.hasProgress ||
+ this.totalBytes != aTotalBytes)) {
+ this.hasProgress = true;
+ this.totalBytes = aTotalBytes;
+ changeMade = true;
+ }
+
+ // Updating the progress and computing the speed require that enough time
+ // passed since the last update, or that we haven't started throttling yet.
+ let currentTimeMs = Date.now();
+ let intervalMs = currentTimeMs - this._lastProgressTimeMs;
+ if (intervalMs >= kProgressUpdateIntervalMs) {
+ // Don't compute the speed unless we started throttling notifications.
+ if (this._lastProgressTimeMs != 0) {
+ // Calculate the speed in bytes per second.
+ let rawSpeed = (aCurrentBytes - this.currentBytes) / intervalMs * 1000;
+ if (this.speed == 0) {
+ // When the previous speed is exactly zero instead of a fractional
+ // number, this can be considered the first element of the series.
+ this.speed = rawSpeed;
+ } else {
+ // Apply exponential smoothing, with a smoothing factor of 0.1.
+ this.speed = rawSpeed * 0.1 + this.speed * 0.9;
+ }
+ }
+
+ // Start throttling notifications only when we have actually received some
+ // bytes for the first time. The timing of the first part of the download
+ // is not reliable, due to possible latency in the initial notifications.
+ // This also allows automated tests to receive and verify the number of
+ // bytes initially transferred.
+ if (aCurrentBytes > 0) {
+ this._lastProgressTimeMs = currentTimeMs;
+
+ // Update the progress now that we don't need its previous value.
+ this.currentBytes = aCurrentBytes;
+ if (this.totalBytes > 0) {
+ this.progress = Math.floor(this.currentBytes / this.totalBytes * 100);
+ }
+ changeMade = true;
+ }
+ }
+
+ if (changeMade) {
+ this._notifyChange();
+ }
+ },
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ let serializable = {
+ source: this.source.toSerializable(),
+ target: this.target.toSerializable(),
+ };
+
+ let saver = this.saver.toSerializable();
+ if (!serializable.source || !saver) {
+ // If we are unable to serialize either the source or the saver,
+ // we won't persist the download.
+ return null;
+ }
+
+ // Simplify the representation for the most common saver type. If the saver
+ // is an object instead of a simple string, we can't simplify it because we
+ // need to persist all its properties, not only "type". This may happen for
+ // savers of type "copy" as well as other types.
+ if (saver !== "copy") {
+ serializable.saver = saver;
+ }
+
+ if (this.error) {
+ serializable.errorObj = this.error.toSerializable();
+ }
+
+ if (this.startTime) {
+ serializable.startTime = this.startTime.toJSON();
+ }
+
+ // These are serialized unless they are false, null, or empty strings.
+ for (let property of kPlainSerializableDownloadProperties) {
+ if (this[property]) {
+ serializable[property] = this[property];
+ }
+ }
+
+ serializeUnknownProperties(this, serializable);
+
+ return serializable;
+ },
+
+ /**
+ * Returns a value that changes only when one of the properties of a Download
+ * object that should be saved into a file also change. This excludes
+ * properties whose value doesn't usually change during the download lifetime.
+ *
+ * This function is used to determine whether the download should be
+ * serialized after a property change notification has been received.
+ *
+ * @return String representing the relevant download state.
+ */
+ getSerializationHash: function ()
+ {
+ // The "succeeded", "canceled", "error", and startTime properties are not
+ // taken into account because they all change before the "stopped" property
+ // changes, and are not altered in other cases.
+ return this.stopped + "," + this.totalBytes + "," + this.hasPartialData +
+ "," + this.contentType;
+ },
+};
+
+/**
+ * Defines which properties of the Download object are serializable.
+ */
+const kPlainSerializableDownloadProperties = [
+ "succeeded",
+ "canceled",
+ "totalBytes",
+ "hasPartialData",
+ "hasBlockedData",
+ "tryToKeepPartialData",
+ "launcherPath",
+ "launchWhenSucceeded",
+ "contentType",
+];
+
+/**
+ * Creates a new Download object from a serializable representation. This
+ * function is used by the createDownload method of Downloads.jsm when a new
+ * Download object is requested, thus some properties may refer to live objects
+ * in place of their serializable representations.
+ *
+ * @param aSerializable
+ * An object with the following fields:
+ * {
+ * source: DownloadSource object, or its serializable representation.
+ * See DownloadSource.fromSerializable for details.
+ * target: DownloadTarget object, or its serializable representation.
+ * See DownloadTarget.fromSerializable for details.
+ * saver: Serializable representation of a DownloadSaver object. See
+ * DownloadSaver.fromSerializable for details. If omitted,
+ * defaults to "copy".
+ * }
+ *
+ * @return The newly created Download object.
+ */
+Download.fromSerializable = function (aSerializable) {
+ let download = new Download();
+ if (aSerializable.source instanceof DownloadSource) {
+ download.source = aSerializable.source;
+ } else {
+ download.source = DownloadSource.fromSerializable(aSerializable.source);
+ }
+ if (aSerializable.target instanceof DownloadTarget) {
+ download.target = aSerializable.target;
+ } else {
+ download.target = DownloadTarget.fromSerializable(aSerializable.target);
+ }
+ if ("saver" in aSerializable) {
+ download.saver = DownloadSaver.fromSerializable(aSerializable.saver);
+ } else {
+ download.saver = DownloadSaver.fromSerializable("copy");
+ }
+ download.saver.download = download;
+
+ if ("startTime" in aSerializable) {
+ let time = aSerializable.startTime.getTime
+ ? aSerializable.startTime.getTime()
+ : aSerializable.startTime;
+ download.startTime = new Date(time);
+ }
+
+ // If 'errorObj' is present it will take precedence over the 'error' property.
+ // 'error' is a legacy property only containing message, which is insufficient
+ // to represent all of the error information.
+ //
+ // Instead of just replacing 'error' we use a new 'errorObj' so that previous
+ // versions will keep it as an unknown property.
+ if ("errorObj" in aSerializable) {
+ download.error = DownloadError.fromSerializable(aSerializable.errorObj);
+ } else if ("error" in aSerializable) {
+ download.error = aSerializable.error;
+ }
+
+ for (let property of kPlainSerializableDownloadProperties) {
+ if (property in aSerializable) {
+ download[property] = aSerializable[property];
+ }
+ }
+
+ deserializeUnknownProperties(download, aSerializable, property =>
+ kPlainSerializableDownloadProperties.indexOf(property) == -1 &&
+ property != "startTime" &&
+ property != "source" &&
+ property != "target" &&
+ property != "error" &&
+ property != "saver");
+
+ return download;
+};
+
+// DownloadSource
+
+/**
+ * Represents the source of a download, for example a document or an URI.
+ */
+this.DownloadSource = function () {}
+
+this.DownloadSource.prototype = {
+ /**
+ * String containing the URI for the download source.
+ */
+ url: null,
+
+ /**
+ * Indicates whether the download originated from a private window. This
+ * determines the context of the network request that is made to retrieve the
+ * resource.
+ */
+ isPrivate: false,
+
+ /**
+ * String containing the referrer URI of the download source, or null if no
+ * referrer should be sent or the download source is not HTTP.
+ */
+ referrer: null,
+
+ /**
+ * For downloads handled by the (default) DownloadCopySaver, this function
+ * can adjust the network channel before it is opened, for example to change
+ * the HTTP headers or to upload a stream as POST data.
+ *
+ * @note If this is defined this object will not be serializable, thus the
+ * Download object will not be persisted across sessions.
+ *
+ * @param aChannel
+ * The nsIChannel to be adjusted.
+ *
+ * @return {Promise}
+ * @resolves When the channel has been adjusted and can be opened.
+ * @rejects JavaScript exception that will cause the download to fail.
+ */
+ adjustChannel: null,
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ if (this.adjustChannel) {
+ // If the callback was used, we can't reproduce this across sessions.
+ return null;
+ }
+
+ // Simplify the representation if we don't have other details.
+ if (!this.isPrivate && !this.referrer && !this._unknownProperties) {
+ return this.url;
+ }
+
+ let serializable = { url: this.url };
+ if (this.isPrivate) {
+ serializable.isPrivate = true;
+ }
+ if (this.referrer) {
+ serializable.referrer = this.referrer;
+ }
+
+ serializeUnknownProperties(this, serializable);
+ return serializable;
+ },
+};
+
+/**
+ * Creates a new DownloadSource object from its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadSource object. This may be a
+ * string containing the URI for the download source, an nsIURI, or an
+ * object with the following properties:
+ * {
+ * url: String containing the URI for the download source.
+ * isPrivate: Indicates whether the download originated from a private
+ * window. If omitted, the download is public.
+ * referrer: String containing the referrer URI of the download source.
+ * Can be omitted or null if no referrer should be sent or
+ * the download source is not HTTP.
+ * adjustChannel: For downloads handled by (default) DownloadCopySaver,
+ * this function can adjust the network channel before
+ * it is opened, for example to change the HTTP headers
+ * or to upload a stream as POST data. Optional.
+ * }
+ *
+ * @return The newly created DownloadSource object.
+ */
+this.DownloadSource.fromSerializable = function (aSerializable) {
+ let source = new DownloadSource();
+ if (isString(aSerializable)) {
+ // Convert String objects to primitive strings at this point.
+ source.url = aSerializable.toString();
+ } else if (aSerializable instanceof Ci.nsIURI) {
+ source.url = aSerializable.spec;
+ } else if (aSerializable instanceof Ci.nsIDOMWindow) {
+ source.url = aSerializable.location.href;
+ source.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(aSerializable);
+ source.windowRef = Cu.getWeakReference(aSerializable);
+ } else {
+ // Convert String objects to primitive strings at this point.
+ source.url = aSerializable.url.toString();
+ if ("isPrivate" in aSerializable) {
+ source.isPrivate = aSerializable.isPrivate;
+ }
+ if ("referrer" in aSerializable) {
+ source.referrer = aSerializable.referrer;
+ }
+ if ("adjustChannel" in aSerializable) {
+ source.adjustChannel = aSerializable.adjustChannel;
+ }
+
+ deserializeUnknownProperties(source, aSerializable, property =>
+ property != "url" && property != "isPrivate" && property != "referrer");
+ }
+
+ return source;
+};
+
+// DownloadTarget
+
+/**
+ * Represents the target of a download, for example a file in the global
+ * downloads directory, or a file in the system temporary directory.
+ */
+this.DownloadTarget = function () {}
+
+this.DownloadTarget.prototype = {
+ /**
+ * String containing the path of the target file.
+ */
+ path: null,
+
+ /**
+ * String containing the path of the ".part" file containing the data
+ * downloaded so far, or null to disable the use of a ".part" file to keep
+ * partially downloaded data.
+ */
+ partFilePath: null,
+
+ /**
+ * Indicates whether the target file exists.
+ *
+ * This is a dynamic property updated when the download finishes or when the
+ * "refresh" method of the Download object is called. It can be used by the
+ * front-end to reduce I/O compared to checking the target file directly.
+ */
+ exists: false,
+
+ /**
+ * Size in bytes of the target file, or zero if the download has not finished.
+ *
+ * Even if the target file does not exist anymore, this property may still
+ * have a value taken from the download metadata. If the metadata has never
+ * been available in this session and the size cannot be obtained from the
+ * file because it has already been deleted, this property will be zero.
+ *
+ * For single-file downloads, this property will always match the actual file
+ * size on disk, while the totalBytes property of the Download object, when
+ * available, may represent the size of the encoded data instead.
+ *
+ * For downloads involving multiple files, like complete web pages saved to
+ * disk, the meaning of this value is undefined. It currently matches the size
+ * of the main file only rather than the sum of all the written data.
+ *
+ * This is a dynamic property updated when the download finishes or when the
+ * "refresh" method of the Download object is called. It can be used by the
+ * front-end to reduce I/O compared to checking the target file directly.
+ */
+ size: 0,
+
+ /**
+ * Sets the "exists" and "size" properties based on the actual file on disk.
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished successfully.
+ * @rejects JavaScript exception.
+ */
+ refresh: Task.async(function* () {
+ try {
+ this.size = (yield OS.File.stat(this.path)).size;
+ this.exists = true;
+ } catch (ex) {
+ // Report any error not caused by the file not being there. In any case,
+ // the size of the download is not updated and the known value is kept.
+ if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
+ Cu.reportError(ex);
+ }
+ this.exists = false;
+ }
+ }),
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ // Simplify the representation if we don't have other details.
+ if (!this.partFilePath && !this._unknownProperties) {
+ return this.path;
+ }
+
+ let serializable = { path: this.path,
+ partFilePath: this.partFilePath };
+ serializeUnknownProperties(this, serializable);
+ return serializable;
+ },
+};
+
+/**
+ * Creates a new DownloadTarget object from its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadTarget object. This may be a
+ * string containing the path of the target file, an nsIFile, or an
+ * object with the following properties:
+ * {
+ * path: String containing the path of the target file.
+ * partFilePath: optional string containing the part file path.
+ * }
+ *
+ * @return The newly created DownloadTarget object.
+ */
+this.DownloadTarget.fromSerializable = function (aSerializable) {
+ let target = new DownloadTarget();
+ if (isString(aSerializable)) {
+ // Convert String objects to primitive strings at this point.
+ target.path = aSerializable.toString();
+ } else if (aSerializable instanceof Ci.nsIFile) {
+ // Read the "path" property of nsIFile after checking the object type.
+ target.path = aSerializable.path;
+ } else {
+ // Read the "path" property of the serializable DownloadTarget
+ // representation, converting String objects to primitive strings.
+ target.path = aSerializable.path.toString();
+ if ("partFilePath" in aSerializable) {
+ target.partFilePath = aSerializable.partFilePath;
+ }
+
+ deserializeUnknownProperties(target, aSerializable, property =>
+ property != "path" && property != "partFilePath");
+ }
+ return target;
+};
+
+// DownloadError
+
+/**
+ * Provides detailed information about a download failure.
+ *
+ * @param aProperties
+ * Object which may contain any of the following properties:
+ * {
+ * result: Result error code, defaulting to Cr.NS_ERROR_FAILURE
+ * message: String error message to be displayed, or null to use the
+ * message associated with the result code.
+ * inferCause: If true, attempts to determine if the cause of the
+ * download is a network failure or a local file failure,
+ * based on a set of known values of the result code.
+ * This is useful when the error is received by a
+ * component that handles both aspects of the download.
+ * }
+ * The properties object may also contain any of the DownloadError's
+ * because properties, which will be set accordingly in the error object.
+ */
+this.DownloadError = function (aProperties)
+{
+ const NS_ERROR_MODULE_BASE_OFFSET = 0x45;
+ const NS_ERROR_MODULE_NETWORK = 6;
+ const NS_ERROR_MODULE_FILES = 13;
+
+ // Set the error name used by the Error object prototype first.
+ this.name = "DownloadError";
+ this.result = aProperties.result || Cr.NS_ERROR_FAILURE;
+ if (aProperties.message) {
+ this.message = aProperties.message;
+ } else if (aProperties.becauseBlocked ||
+ aProperties.becauseBlockedByParentalControls ||
+ aProperties.becauseBlockedByReputationCheck ||
+ aProperties.becauseBlockedByRuntimePermissions) {
+ this.message = "Download blocked.";
+ } else {
+ let exception = new Components.Exception("", this.result);
+ this.message = exception.toString();
+ }
+ if (aProperties.inferCause) {
+ let module = ((this.result & 0x7FFF0000) >> 16) -
+ NS_ERROR_MODULE_BASE_OFFSET;
+ this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK);
+ this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES);
+ }
+ else {
+ if (aProperties.becauseSourceFailed) {
+ this.becauseSourceFailed = true;
+ }
+ if (aProperties.becauseTargetFailed) {
+ this.becauseTargetFailed = true;
+ }
+ }
+
+ if (aProperties.becauseBlockedByParentalControls) {
+ this.becauseBlocked = true;
+ this.becauseBlockedByParentalControls = true;
+ } else if (aProperties.becauseBlockedByReputationCheck) {
+ this.becauseBlocked = true;
+ this.becauseBlockedByReputationCheck = true;
+ this.reputationCheckVerdict = aProperties.reputationCheckVerdict || "";
+ } else if (aProperties.becauseBlockedByRuntimePermissions) {
+ this.becauseBlocked = true;
+ this.becauseBlockedByRuntimePermissions = true;
+ } else if (aProperties.becauseBlocked) {
+ this.becauseBlocked = true;
+ }
+
+ if (aProperties.innerException) {
+ this.innerException = aProperties.innerException;
+ }
+
+ this.stack = new Error().stack;
+}
+
+/**
+ * These constants are used by the reputationCheckVerdict property and indicate
+ * the detailed reason why a download is blocked.
+ *
+ * @note These values should not be changed because they can be serialized.
+ */
+this.DownloadError.BLOCK_VERDICT_MALWARE = "Malware";
+this.DownloadError.BLOCK_VERDICT_POTENTIALLY_UNWANTED = "PotentiallyUnwanted";
+this.DownloadError.BLOCK_VERDICT_UNCOMMON = "Uncommon";
+
+this.DownloadError.prototype = {
+ __proto__: Error.prototype,
+
+ /**
+ * The result code associated with this error.
+ */
+ result: false,
+
+ /**
+ * Indicates an error occurred while reading from the remote location.
+ */
+ becauseSourceFailed: false,
+
+ /**
+ * Indicates an error occurred while writing to the local target.
+ */
+ becauseTargetFailed: false,
+
+ /**
+ * Indicates the download failed because it was blocked. If the reason for
+ * blocking is known, the corresponding property will be also set.
+ */
+ becauseBlocked: false,
+
+ /**
+ * Indicates the download was blocked because downloads are globally
+ * disallowed by the Parental Controls or Family Safety features on Windows.
+ */
+ becauseBlockedByParentalControls: false,
+
+ /**
+ * Indicates the download was blocked because it failed the reputation check
+ * and may be malware.
+ */
+ becauseBlockedByReputationCheck: false,
+
+ /**
+ * Indicates the download was blocked because a runtime permission required to
+ * download files was not granted.
+ *
+ * This does not apply to all systems. On Android this flag is set to true if
+ * a needed runtime permission (storage) has not been granted by the user.
+ */
+ becauseBlockedByRuntimePermissions: false,
+
+ /**
+ * If becauseBlockedByReputationCheck is true, indicates the detailed reason
+ * why the download was blocked, according to the "BLOCK_VERDICT_" constants.
+ *
+ * If the download was not blocked or the reason for the block is unknown,
+ * this will be an empty string.
+ */
+ reputationCheckVerdict: "",
+
+ /**
+ * If this DownloadError was caused by an exception this property will
+ * contain the original exception. This will not be serialized when saving
+ * to the store.
+ */
+ innerException: null,
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ let serializable = {
+ result: this.result,
+ message: this.message,
+ becauseSourceFailed: this.becauseSourceFailed,
+ becauseTargetFailed: this.becauseTargetFailed,
+ becauseBlocked: this.becauseBlocked,
+ becauseBlockedByParentalControls: this.becauseBlockedByParentalControls,
+ becauseBlockedByReputationCheck: this.becauseBlockedByReputationCheck,
+ becauseBlockedByRuntimePermissions: this.becauseBlockedByRuntimePermissions,
+ reputationCheckVerdict: this.reputationCheckVerdict,
+ };
+
+ serializeUnknownProperties(this, serializable);
+ return serializable;
+ },
+};
+
+/**
+ * Creates a new DownloadError object from its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadError object.
+ *
+ * @return The newly created DownloadError object.
+ */
+this.DownloadError.fromSerializable = function (aSerializable) {
+ let e = new DownloadError(aSerializable);
+ deserializeUnknownProperties(e, aSerializable, property =>
+ property != "result" &&
+ property != "message" &&
+ property != "becauseSourceFailed" &&
+ property != "becauseTargetFailed" &&
+ property != "becauseBlocked" &&
+ property != "becauseBlockedByParentalControls" &&
+ property != "becauseBlockedByReputationCheck" &&
+ property != "becauseBlockedByRuntimePermissions" &&
+ property != "reputationCheckVerdict");
+
+ return e;
+};
+
+// DownloadSaver
+
+/**
+ * Template for an object that actually transfers the data for the download.
+ */
+this.DownloadSaver = function () {}
+
+this.DownloadSaver.prototype = {
+ /**
+ * Download object for raising notifications and reading properties.
+ *
+ * If the tryToKeepPartialData property of the download object is false, the
+ * saver should never try to keep partially downloaded data if the download
+ * fails.
+ */
+ download: null,
+
+ /**
+ * Executes the download.
+ *
+ * @param aSetProgressBytesFn
+ * This function may be called by the saver to report progress. It
+ * takes three arguments: the first is the number of bytes transferred
+ * until now, the second is the total number of bytes to be
+ * transferred (or -1 if unknown), the third indicates whether the
+ * partially downloaded data can be used when restarting the download
+ * if it fails or is canceled.
+ * @param aSetPropertiesFn
+ * This function may be called by the saver to report information
+ * about new download properties discovered by the saver during the
+ * download process. It takes an object where the keys represents
+ * the names of the properties to set, and the value represents the
+ * value to set.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+ execute: function DS_execute(aSetProgressBytesFn, aSetPropertiesFn)
+ {
+ throw new Error("Not implemented.");
+ },
+
+ /**
+ * Cancels the download.
+ */
+ cancel: function DS_cancel()
+ {
+ throw new Error("Not implemented.");
+ },
+
+ /**
+ * Removes any partial data kept as part of a canceled or failed download.
+ *
+ * This method is never called until the promise returned by "execute" is
+ * either resolved or rejected, and the "execute" method is not called again
+ * until the promise returned by this method is resolved or rejected.
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished successfully.
+ * @rejects JavaScript exception.
+ */
+ removePartialData: function DS_removePartialData()
+ {
+ return Promise.resolve();
+ },
+
+ /**
+ * This can be called by the saver implementation when the download is already
+ * started, to add it to the browsing history. This method has no effect if
+ * the download is private.
+ */
+ addToHistory: function ()
+ {
+ if (this.download.source.isPrivate) {
+ return;
+ }
+
+ let sourceUri = NetUtil.newURI(this.download.source.url);
+ let referrer = this.download.source.referrer;
+ let referrerUri = referrer ? NetUtil.newURI(referrer) : null;
+ let targetUri = NetUtil.newURI(new FileUtils.File(
+ this.download.target.path));
+
+ // The start time is always available when we reach this point.
+ let startPRTime = this.download.startTime.getTime() * 1000;
+
+ try {
+ gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime,
+ targetUri);
+ }
+ catch (ex) {
+ if (!(ex instanceof Components.Exception) ||
+ ex.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw ex;
+ }
+ //
+ // Under normal operation the download history service may not
+ // be available. We don't want all downloads that are public to fail
+ // when this happens so we'll ignore this error and this error only!
+ //
+ }
+ },
+
+ /**
+ * Returns a static representation of the current object state.
+ *
+ * @return A JavaScript object that can be serialized to JSON.
+ */
+ toSerializable: function ()
+ {
+ throw new Error("Not implemented.");
+ },
+
+ /**
+ * Returns the SHA-256 hash of the downloaded file, if it exists.
+ */
+ getSha256Hash: function ()
+ {
+ throw new Error("Not implemented.");
+ },
+
+ getSignatureInfo: function ()
+ {
+ throw new Error("Not implemented.");
+ },
+}; // DownloadSaver
+
+/**
+ * Creates a new DownloadSaver object from its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadSaver object. If no initial
+ * state information for the saver object is needed, can be a string
+ * representing the class of the download operation, for example "copy".
+ *
+ * @return The newly created DownloadSaver object.
+ */
+this.DownloadSaver.fromSerializable = function (aSerializable) {
+ let serializable = isString(aSerializable) ? { type: aSerializable }
+ : aSerializable;
+ let saver;
+ switch (serializable.type) {
+ case "copy":
+ saver = DownloadCopySaver.fromSerializable(serializable);
+ break;
+ case "legacy":
+ saver = DownloadLegacySaver.fromSerializable(serializable);
+ break;
+ case "pdf":
+ saver = DownloadPDFSaver.fromSerializable(serializable);
+ break;
+ default:
+ throw new Error("Unrecoginzed download saver type.");
+ }
+ return saver;
+};
+
+// DownloadCopySaver
+
+/**
+ * Saver object that simply copies the entire source file to the target.
+ */
+this.DownloadCopySaver = function () {}
+
+this.DownloadCopySaver.prototype = {
+ __proto__: DownloadSaver.prototype,
+
+ /**
+ * BackgroundFileSaver object currently handling the download.
+ */
+ _backgroundFileSaver: null,
+
+ /**
+ * Indicates whether the "cancel" method has been called. This is used to
+ * prevent the request from starting in case the operation is canceled before
+ * the BackgroundFileSaver instance has been created.
+ */
+ _canceled: false,
+
+ /**
+ * Save the SHA-256 hash in raw bytes of the downloaded file. This is null
+ * unless BackgroundFileSaver has successfully completed saving the file.
+ */
+ _sha256Hash: null,
+
+ /**
+ * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
+ * if the file is signed. This is empty if the file is unsigned, and null
+ * unless BackgroundFileSaver has successfully completed saving the file.
+ */
+ _signatureInfo: null,
+
+ /**
+ * Save the redirects chain as an nsIArray of nsIPrincipal.
+ */
+ _redirects: null,
+
+ /**
+ * True if the associated download has already been added to browsing history.
+ */
+ alreadyAddedToHistory: false,
+
+ /**
+ * String corresponding to the entityID property of the nsIResumableChannel
+ * used to execute the download, or null if the channel was not resumable or
+ * the saver was instructed not to keep partially downloaded data.
+ */
+ entityID: null,
+
+ /**
+ * Implements "DownloadSaver.execute".
+ */
+ execute: function DCS_execute(aSetProgressBytesFn, aSetPropertiesFn)
+ {
+ let copySaver = this;
+
+ this._canceled = false;
+
+ let download = this.download;
+ let targetPath = download.target.path;
+ let partFilePath = download.target.partFilePath;
+ let keepPartialData = download.tryToKeepPartialData;
+
+ return Task.spawn(function* task_DCS_execute() {
+ // Add the download to history the first time it is started in this
+ // session. If the download is restarted in a different session, a new
+ // history visit will be added. We do this just to avoid the complexity
+ // of serializing this state between sessions, since adding a new visit
+ // does not have any noticeable side effect.
+ if (!this.alreadyAddedToHistory) {
+ this.addToHistory();
+ this.alreadyAddedToHistory = true;
+ }
+
+ // To reduce the chance that other downloads reuse the same final target
+ // file name, we should create a placeholder as soon as possible, before
+ // starting the network request. The placeholder is also required in case
+ // we are using a ".part" file instead of the final target while the
+ // download is in progress.
+ try {
+ // If the file already exists, don't delete its contents yet.
+ let file = yield OS.File.open(targetPath, { write: true });
+ yield file.close();
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error)) {
+ throw ex;
+ }
+ // Throw a DownloadError indicating that the operation failed because of
+ // the target file. We cannot translate this into a specific result
+ // code, but we preserve the original message using the toString method.
+ let error = new DownloadError({ message: ex.toString() });
+ error.becauseTargetFailed = true;
+ throw error;
+ }
+
+ try {
+ let deferSaveComplete = Promise.defer();
+
+ if (this._canceled) {
+ // Don't create the BackgroundFileSaver object if we have been
+ // canceled meanwhile.
+ throw new DownloadError({ message: "Saver canceled." });
+ }
+
+ // Create the object that will save the file in a background thread.
+ let backgroundFileSaver = new BackgroundFileSaverStreamListener();
+ try {
+ // When the operation completes, reflect the status in the promise
+ // returned by this download execution function.
+ backgroundFileSaver.observer = {
+ onTargetChange: function () { },
+ onSaveComplete: (aSaver, aStatus) => {
+ // Send notifications now that we can restart if needed.
+ if (Components.isSuccessCode(aStatus)) {
+ // Save the hash before freeing backgroundFileSaver.
+ this._sha256Hash = aSaver.sha256Hash;
+ this._signatureInfo = aSaver.signatureInfo;
+ this._redirects = aSaver.redirects;
+ deferSaveComplete.resolve();
+ } else {
+ // Infer the origin of the error from the failure code, because
+ // BackgroundFileSaver does not provide more specific data.
+ let properties = { result: aStatus, inferCause: true };
+ deferSaveComplete.reject(new DownloadError(properties));
+ }
+ // Free the reference cycle, to release resources earlier.
+ backgroundFileSaver.observer = null;
+ this._backgroundFileSaver = null;
+ },
+ };
+
+ // Create a channel from the source, and listen to progress
+ // notifications.
+ let channel = NetUtil.newChannel({
+ uri: download.source.url,
+ loadUsingSystemPrincipal: true,
+ });
+ if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
+ channel.setPrivate(download.source.isPrivate);
+ }
+ if (channel instanceof Ci.nsIHttpChannel &&
+ download.source.referrer) {
+ channel.referrer = NetUtil.newURI(download.source.referrer);
+ }
+
+ // If we have data that we can use to resume the download from where
+ // it stopped, try to use it.
+ let resumeAttempted = false;
+ let resumeFromBytes = 0;
+ if (channel instanceof Ci.nsIResumableChannel && this.entityID &&
+ partFilePath && keepPartialData) {
+ try {
+ let stat = yield OS.File.stat(partFilePath);
+ channel.resumeAt(stat.size, this.entityID);
+ resumeAttempted = true;
+ resumeFromBytes = stat.size;
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ }
+ }
+
+ channel.notificationCallbacks = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
+ getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]),
+ onProgress: function DCSE_onProgress(aRequest, aContext, aProgress,
+ aProgressMax)
+ {
+ let currentBytes = resumeFromBytes + aProgress;
+ let totalBytes = aProgressMax == -1 ? -1 : (resumeFromBytes +
+ aProgressMax);
+ aSetProgressBytesFn(currentBytes, totalBytes, aProgress > 0 &&
+ partFilePath && keepPartialData);
+ },
+ onStatus: function () { },
+ };
+
+ // If the callback was set, handle it now before opening the channel.
+ if (download.source.adjustChannel) {
+ yield download.source.adjustChannel(channel);
+ }
+
+ // Open the channel, directing output to the background file saver.
+ backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
+ channel.asyncOpen2({
+ onStartRequest: function (aRequest, aContext) {
+ backgroundFileSaver.onStartRequest(aRequest, aContext);
+
+ // Check if the request's response has been blocked by Windows
+ // Parental Controls with an HTTP 450 error code.
+ if (aRequest instanceof Ci.nsIHttpChannel &&
+ aRequest.responseStatus == 450) {
+ // Set a flag that can be retrieved later when handling the
+ // cancellation so that the proper error can be thrown.
+ this.download._blockedByParentalControls = true;
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ return;
+ }
+
+ aSetPropertiesFn({ contentType: channel.contentType });
+
+ // Ensure we report the value of "Content-Length", if available,
+ // even if the download doesn't generate any progress events
+ // later.
+ if (channel.contentLength >= 0) {
+ aSetProgressBytesFn(0, channel.contentLength);
+ }
+
+ // If the URL we are downloading from includes a file extension
+ // that matches the "Content-Encoding" header, for example ".gz"
+ // with a "gzip" encoding, we should save the file in its encoded
+ // form. In all other cases, we decode the body while saving.
+ if (channel instanceof Ci.nsIEncodedChannel &&
+ channel.contentEncodings) {
+ let uri = channel.URI;
+ if (uri instanceof Ci.nsIURL && uri.fileExtension) {
+ // Only the first, outermost encoding is considered.
+ let encoding = channel.contentEncodings.getNext();
+ if (encoding) {
+ channel.applyConversion =
+ gExternalHelperAppService.applyDecodingForExtension(
+ uri.fileExtension, encoding);
+ }
+ }
+ }
+
+ if (keepPartialData) {
+ // If the source is not resumable, don't keep partial data even
+ // if we were asked to try and do it.
+ if (aRequest instanceof Ci.nsIResumableChannel) {
+ try {
+ // If reading the ID succeeds, the source is resumable.
+ this.entityID = aRequest.entityID;
+ } catch (ex) {
+ if (!(ex instanceof Components.Exception) ||
+ ex.result != Cr.NS_ERROR_NOT_RESUMABLE) {
+ throw ex;
+ }
+ keepPartialData = false;
+ }
+ } else {
+ keepPartialData = false;
+ }
+ }
+
+ // Enable hashing and signature verification before setting the
+ // target.
+ backgroundFileSaver.enableSha256();
+ backgroundFileSaver.enableSignatureInfo();
+ if (partFilePath) {
+ // If we actually resumed a request, append to the partial data.
+ if (resumeAttempted) {
+ // TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED
+ backgroundFileSaver.enableAppend();
+ }
+
+ // Use a part file, determining if we should keep it on failure.
+ backgroundFileSaver.setTarget(new FileUtils.File(partFilePath),
+ keepPartialData);
+ } else {
+ // Set the final target file, and delete it on failure.
+ backgroundFileSaver.setTarget(new FileUtils.File(targetPath),
+ false);
+ }
+ }.bind(copySaver),
+
+ onStopRequest: function (aRequest, aContext, aStatusCode) {
+ try {
+ backgroundFileSaver.onStopRequest(aRequest, aContext,
+ aStatusCode);
+ } finally {
+ // If the data transfer completed successfully, indicate to the
+ // background file saver that the operation can finish. If the
+ // data transfer failed, the saver has been already stopped.
+ if (Components.isSuccessCode(aStatusCode)) {
+ backgroundFileSaver.finish(Cr.NS_OK);
+ }
+ }
+ }.bind(copySaver),
+
+ onDataAvailable: function (aRequest, aContext, aInputStream,
+ aOffset, aCount) {
+ backgroundFileSaver.onDataAvailable(aRequest, aContext,
+ aInputStream, aOffset,
+ aCount);
+ }.bind(copySaver),
+ });
+
+ // We should check if we have been canceled in the meantime, after
+ // all the previous asynchronous operations have been executed and
+ // just before we set the _backgroundFileSaver property.
+ if (this._canceled) {
+ throw new DownloadError({ message: "Saver canceled." });
+ }
+
+ // If the operation succeeded, store the object to allow cancellation.
+ this._backgroundFileSaver = backgroundFileSaver;
+ } catch (ex) {
+ // In case an error occurs while setting up the chain of objects for
+ // the download, ensure that we release the resources of the saver.
+ backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
+ // Since we're not going to handle deferSaveComplete.promise below,
+ // we need to make sure that the rejection is handled.
+ deferSaveComplete.promise.catch(() => {});
+ throw ex;
+ }
+
+ // We will wait on this promise in case no error occurred while setting
+ // up the chain of objects for the download.
+ yield deferSaveComplete.promise;
+
+ yield this._checkReputationAndMove(aSetPropertiesFn);
+ } catch (ex) {
+ // Ensure we always remove the placeholder for the final target file on
+ // failure, independently of which code path failed. In some cases, the
+ // background file saver may have already removed the file.
+ try {
+ yield OS.File.remove(targetPath);
+ } catch (e2) {
+ // If we failed during the operation, we report the error but use the
+ // original one as the failure reason of the download. Note that on
+ // Windows we may get an access denied error instead of a no such file
+ // error if the file existed before, and was recently deleted.
+ if (!(e2 instanceof OS.File.Error &&
+ (e2.becauseNoSuchFile || e2.becauseAccessDenied))) {
+ Cu.reportError(e2);
+ }
+ }
+ throw ex;
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Perform the reputation check and cleanup the downloaded data if required.
+ * If the download passes the reputation check and is using a part file we
+ * will move it to the target path since reputation checking is the final
+ * step in the saver.
+ *
+ * @param aSetPropertiesFn
+ * Function provided to the "execute" method.
+ *
+ * @return {Promise}
+ * @resolves When the reputation check and cleanup is complete.
+ * @rejects DownloadError if the download should be blocked.
+ */
+ _checkReputationAndMove: Task.async(function* (aSetPropertiesFn) {
+ let download = this.download;
+ let targetPath = this.download.target.path;
+ let partFilePath = this.download.target.partFilePath;
+
+ let { shouldBlock, verdict } =
+ yield DownloadIntegration.shouldBlockForReputationCheck(download);
+ if (shouldBlock) {
+ let newProperties = { progress: 100, hasPartialData: false };
+
+ // We will remove the potentially dangerous file if instructed by
+ // DownloadIntegration. We will always remove the file when the
+ // download did not use a partial file path, meaning it
+ // currently has its final filename.
+ if (!DownloadIntegration.shouldKeepBlockedData() || !partFilePath) {
+ try {
+ yield OS.File.remove(partFilePath || targetPath);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ } else {
+ newProperties.hasBlockedData = true;
+ }
+
+ aSetPropertiesFn(newProperties);
+
+ throw new DownloadError({
+ becauseBlockedByReputationCheck: true,
+ reputationCheckVerdict: verdict,
+ });
+ }
+
+ if (partFilePath) {
+ yield OS.File.move(partFilePath, targetPath);
+ }
+ }),
+
+ /**
+ * Implements "DownloadSaver.cancel".
+ */
+ cancel: function DCS_cancel()
+ {
+ this._canceled = true;
+ if (this._backgroundFileSaver) {
+ this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
+ this._backgroundFileSaver = null;
+ }
+ },
+
+ /**
+ * Implements "DownloadSaver.removePartialData".
+ */
+ removePartialData: function ()
+ {
+ return Task.spawn(function* task_DCS_removePartialData() {
+ if (this.download.target.partFilePath) {
+ try {
+ yield OS.File.remove(this.download.target.partFilePath);
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ }
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Implements "DownloadSaver.toSerializable".
+ */
+ toSerializable: function ()
+ {
+ // Simplify the representation if we don't have other details.
+ if (!this.entityID && !this._unknownProperties) {
+ return "copy";
+ }
+
+ let serializable = { type: "copy",
+ entityID: this.entityID };
+ serializeUnknownProperties(this, serializable);
+ return serializable;
+ },
+
+ /**
+ * Implements "DownloadSaver.getSha256Hash"
+ */
+ getSha256Hash: function ()
+ {
+ return this._sha256Hash;
+ },
+
+ /*
+ * Implements DownloadSaver.getSignatureInfo.
+ */
+ getSignatureInfo: function ()
+ {
+ return this._signatureInfo;
+ },
+
+ /*
+ * Implements DownloadSaver.getRedirects.
+ */
+ getRedirects: function ()
+ {
+ return this._redirects;
+ }
+};
+
+/**
+ * Creates a new DownloadCopySaver object, with its initial state derived from
+ * its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadCopySaver object.
+ *
+ * @return The newly created DownloadCopySaver object.
+ */
+this.DownloadCopySaver.fromSerializable = function (aSerializable) {
+ let saver = new DownloadCopySaver();
+ if ("entityID" in aSerializable) {
+ saver.entityID = aSerializable.entityID;
+ }
+
+ deserializeUnknownProperties(saver, aSerializable, property =>
+ property != "entityID" && property != "type");
+
+ return saver;
+};
+
+// DownloadLegacySaver
+
+/**
+ * Saver object that integrates with the legacy nsITransfer interface.
+ *
+ * For more background on the process, see the DownloadLegacyTransfer object.
+ */
+this.DownloadLegacySaver = function ()
+{
+ this.deferExecuted = Promise.defer();
+ this.deferCanceled = Promise.defer();
+}
+
+this.DownloadLegacySaver.prototype = {
+ __proto__: DownloadSaver.prototype,
+
+ /**
+ * Save the SHA-256 hash in raw bytes of the downloaded file. This may be
+ * null when nsExternalHelperAppService (and thus BackgroundFileSaver) is not
+ * invoked.
+ */
+ _sha256Hash: null,
+
+ /**
+ * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
+ * if the file is signed. This is empty if the file is unsigned, and null
+ * unless BackgroundFileSaver has successfully completed saving the file.
+ */
+ _signatureInfo: null,
+
+ /**
+ * Save the redirect chain as an nsIArray of nsIPrincipal.
+ */
+ _redirects: null,
+
+ /**
+ * nsIRequest object associated to the status and progress updates we
+ * received. This object is null before we receive the first status and
+ * progress update, and is also reset to null when the download is stopped.
+ */
+ request: null,
+
+ /**
+ * This deferred object contains a promise that is resolved as soon as this
+ * download finishes successfully, and is rejected in case the download is
+ * canceled or receives a failure notification through nsITransfer.
+ */
+ deferExecuted: null,
+
+ /**
+ * This deferred object contains a promise that is resolved if the download
+ * receives a cancellation request through the "cancel" method, and is never
+ * rejected. The nsITransfer implementation will register a handler that
+ * actually causes the download cancellation.
+ */
+ deferCanceled: null,
+
+ /**
+ * This is populated with the value of the aSetProgressBytesFn argument of the
+ * "execute" method, and is null before the method is called.
+ */
+ setProgressBytesFn: null,
+
+ /**
+ * Called by the nsITransfer implementation while the download progresses.
+ *
+ * @param aCurrentBytes
+ * Number of bytes transferred until now.
+ * @param aTotalBytes
+ * Total number of bytes to be transferred, or -1 if unknown.
+ */
+ onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes)
+ {
+ this.progressWasNotified = true;
+
+ // Ignore progress notifications until we are ready to process them.
+ if (!this.setProgressBytesFn) {
+ // Keep the data from the last progress notification that was received.
+ this.currentBytes = aCurrentBytes;
+ this.totalBytes = aTotalBytes;
+ return;
+ }
+
+ let hasPartFile = !!this.download.target.partFilePath;
+
+ this.setProgressBytesFn(aCurrentBytes, aTotalBytes,
+ aCurrentBytes > 0 && hasPartFile);
+ },
+
+ /**
+ * Whether the onProgressBytes function has been called at least once.
+ */
+ progressWasNotified: false,
+
+ /**
+ * Called by the nsITransfer implementation when the request has started.
+ *
+ * @param aRequest
+ * nsIRequest associated to the status update.
+ * @param aAlreadyAddedToHistory
+ * Indicates that the nsIExternalHelperAppService component already
+ * added the download to the browsing history, unless it was started
+ * from a private browsing window. When this parameter is false, the
+ * download is added to the browsing history here. Private downloads
+ * are never added to history even if this parameter is false.
+ */
+ onTransferStarted: function (aRequest, aAlreadyAddedToHistory)
+ {
+ // Store the entity ID to use for resuming if required.
+ if (this.download.tryToKeepPartialData &&
+ aRequest instanceof Ci.nsIResumableChannel) {
+ try {
+ // If reading the ID succeeds, the source is resumable.
+ this.entityID = aRequest.entityID;
+ } catch (ex) {
+ if (!(ex instanceof Components.Exception) ||
+ ex.result != Cr.NS_ERROR_NOT_RESUMABLE) {
+ throw ex;
+ }
+ }
+ }
+
+ // For legacy downloads, we must update the referrer at this time.
+ if (aRequest instanceof Ci.nsIHttpChannel && aRequest.referrer) {
+ this.download.source.referrer = aRequest.referrer.spec;
+ }
+
+ if (!aAlreadyAddedToHistory) {
+ this.addToHistory();
+ }
+ },
+
+ /**
+ * Called by the nsITransfer implementation when the request has finished.
+ *
+ * @param aRequest
+ * nsIRequest associated to the status update.
+ * @param aStatus
+ * Status code received by the nsITransfer implementation.
+ */
+ onTransferFinished: function DLS_onTransferFinished(aRequest, aStatus)
+ {
+ // Store a reference to the request, used when handling completion.
+ this.request = aRequest;
+
+ if (Components.isSuccessCode(aStatus)) {
+ this.deferExecuted.resolve();
+ } else {
+ // Infer the origin of the error from the failure code, because more
+ // specific data is not available through the nsITransfer implementation.
+ let properties = { result: aStatus, inferCause: true };
+ this.deferExecuted.reject(new DownloadError(properties));
+ }
+ },
+
+ /**
+ * When the first execution of the download finished, it can be restarted by
+ * using a DownloadCopySaver object instead of the original legacy component
+ * that executed the download.
+ */
+ firstExecutionFinished: false,
+
+ /**
+ * In case the download is restarted after the first execution finished, this
+ * property contains a reference to the DownloadCopySaver that is executing
+ * the new download attempt.
+ */
+ copySaver: null,
+
+ /**
+ * String corresponding to the entityID property of the nsIResumableChannel
+ * used to execute the download, or null if the channel was not resumable or
+ * the saver was instructed not to keep partially downloaded data.
+ */
+ entityID: null,
+
+ /**
+ * Implements "DownloadSaver.execute".
+ */
+ execute: function DLS_execute(aSetProgressBytesFn, aSetPropertiesFn)
+ {
+ // Check if this is not the first execution of the download. The Download
+ // object guarantees that this function is not re-entered during execution.
+ if (this.firstExecutionFinished) {
+ if (!this.copySaver) {
+ this.copySaver = new DownloadCopySaver();
+ this.copySaver.download = this.download;
+ this.copySaver.entityID = this.entityID;
+ this.copySaver.alreadyAddedToHistory = true;
+ }
+ return this.copySaver.execute.apply(this.copySaver, arguments);
+ }
+
+ this.setProgressBytesFn = aSetProgressBytesFn;
+ if (this.progressWasNotified) {
+ this.onProgressBytes(this.currentBytes, this.totalBytes);
+ }
+
+ return Task.spawn(function* task_DLS_execute() {
+ try {
+ // Wait for the component that executes the download to finish.
+ yield this.deferExecuted.promise;
+
+ // At this point, the "request" property has been populated. Ensure we
+ // report the value of "Content-Length", if available, even if the
+ // download didn't generate any progress events.
+ if (!this.progressWasNotified &&
+ this.request instanceof Ci.nsIChannel &&
+ this.request.contentLength >= 0) {
+ aSetProgressBytesFn(0, this.request.contentLength);
+ }
+
+ // If the component executing the download provides the path of a
+ // ".part" file, it means that it expects the listener to move the file
+ // to its final target path when the download succeeds. In this case,
+ // an empty ".part" file is created even if no data was received from
+ // the source.
+ //
+ // When no ".part" file path is provided the download implementation may
+ // not have created the target file (if no data was received from the
+ // source). In this case, ensure that an empty file is created as
+ // expected.
+ if (!this.download.target.partFilePath) {
+ try {
+ // This atomic operation is more efficient than an existence check.
+ let file = yield OS.File.open(this.download.target.path,
+ { create: true });
+ yield file.close();
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseExists) {
+ throw ex;
+ }
+ }
+ }
+
+ yield this._checkReputationAndMove(aSetPropertiesFn);
+
+ } catch (ex) {
+ // Ensure we always remove the final target file on failure,
+ // independently of which code path failed. In some cases, the
+ // component executing the download may have already removed the file.
+ try {
+ yield OS.File.remove(this.download.target.path);
+ } catch (e2) {
+ // If we failed during the operation, we report the error but use the
+ // original one as the failure reason of the download. Note that on
+ // Windows we may get an access denied error instead of a no such file
+ // error if the file existed before, and was recently deleted.
+ if (!(e2 instanceof OS.File.Error &&
+ (e2.becauseNoSuchFile || e2.becauseAccessDenied))) {
+ Cu.reportError(e2);
+ }
+ }
+ // In case the operation failed, ensure we stop downloading data. Since
+ // we never re-enter this function, deferCanceled is always available.
+ this.deferCanceled.resolve();
+ throw ex;
+ } finally {
+ // We don't need the reference to the request anymore. We must also set
+ // deferCanceled to null in order to free any indirect references it
+ // may hold to the request.
+ this.request = null;
+ this.deferCanceled = null;
+ // Allow the download to restart through a DownloadCopySaver.
+ this.firstExecutionFinished = true;
+ }
+ }.bind(this));
+ },
+
+ _checkReputationAndMove: function () {
+ return DownloadCopySaver.prototype._checkReputationAndMove
+ .apply(this, arguments);
+ },
+
+ /**
+ * Implements "DownloadSaver.cancel".
+ */
+ cancel: function DLS_cancel()
+ {
+ // We may be using a DownloadCopySaver to handle resuming.
+ if (this.copySaver) {
+ return this.copySaver.cancel.apply(this.copySaver, arguments);
+ }
+
+ // If the download hasn't stopped already, resolve deferCanceled so that the
+ // operation is canceled as soon as a cancellation handler is registered.
+ // Note that the handler might not have been registered yet.
+ if (this.deferCanceled) {
+ this.deferCanceled.resolve();
+ }
+ },
+
+ /**
+ * Implements "DownloadSaver.removePartialData".
+ */
+ removePartialData: function ()
+ {
+ // DownloadCopySaver and DownloadLeagcySaver use the same logic for removing
+ // partially downloaded data, though this implementation isn't shared by
+ // other saver types, thus it isn't found on their shared prototype.
+ return DownloadCopySaver.prototype.removePartialData.call(this);
+ },
+
+ /**
+ * Implements "DownloadSaver.toSerializable".
+ */
+ toSerializable: function ()
+ {
+ // This object depends on legacy components that are created externally,
+ // thus it cannot be rebuilt during deserialization. To support resuming
+ // across different browser sessions, this object is transformed into a
+ // DownloadCopySaver for the purpose of serialization.
+ return DownloadCopySaver.prototype.toSerializable.call(this);
+ },
+
+ /**
+ * Implements "DownloadSaver.getSha256Hash".
+ */
+ getSha256Hash: function ()
+ {
+ if (this.copySaver) {
+ return this.copySaver.getSha256Hash();
+ }
+ return this._sha256Hash;
+ },
+
+ /**
+ * Called by the nsITransfer implementation when the hash is available.
+ */
+ setSha256Hash: function (hash)
+ {
+ this._sha256Hash = hash;
+ },
+
+ /**
+ * Implements "DownloadSaver.getSignatureInfo".
+ */
+ getSignatureInfo: function ()
+ {
+ if (this.copySaver) {
+ return this.copySaver.getSignatureInfo();
+ }
+ return this._signatureInfo;
+ },
+
+ /**
+ * Called by the nsITransfer implementation when the hash is available.
+ */
+ setSignatureInfo: function (signatureInfo)
+ {
+ this._signatureInfo = signatureInfo;
+ },
+
+ /**
+ * Implements "DownloadSaver.getRedirects".
+ */
+ getRedirects: function ()
+ {
+ if (this.copySaver) {
+ return this.copySaver.getRedirects();
+ }
+ return this._redirects;
+ },
+
+ /**
+ * Called by the nsITransfer implementation when the redirect chain is
+ * available.
+ */
+ setRedirects: function (redirects)
+ {
+ this._redirects = redirects;
+ },
+};
+
+/**
+ * Returns a new DownloadLegacySaver object. This saver type has a
+ * deserializable form only when creating a new object in memory, because it
+ * cannot be serialized to disk.
+ */
+this.DownloadLegacySaver.fromSerializable = function () {
+ return new DownloadLegacySaver();
+};
+
+// DownloadPDFSaver
+
+/**
+ * This DownloadSaver type creates a PDF file from the current document in a
+ * given window, specified using the windowRef property of the DownloadSource
+ * object associated with the download.
+ *
+ * In order to prevent the download from saving a different document than the one
+ * originally loaded in the window, any attempt to restart the download will fail.
+ *
+ * Since this DownloadSaver type requires a live document as a source, it cannot
+ * be persisted across sessions, unless the download already succeeded.
+ */
+this.DownloadPDFSaver = function () {
+}
+
+this.DownloadPDFSaver.prototype = {
+ __proto__: DownloadSaver.prototype,
+
+ /**
+ * An nsIWebBrowserPrint instance for printing this page.
+ * This is null when saving has not started or has completed,
+ * or while the operation is being canceled.
+ */
+ _webBrowserPrint: null,
+
+ /**
+ * Implements "DownloadSaver.execute".
+ */
+ execute: function (aSetProgressBytesFn, aSetPropertiesFn)
+ {
+ return Task.spawn(function* task_DCS_execute() {
+ if (!this.download.source.windowRef) {
+ throw new DownloadError({
+ message: "PDF saver must be passed an open window, and cannot be restarted.",
+ becauseSourceFailed: true,
+ });
+ }
+
+ let win = this.download.source.windowRef.get();
+
+ // Set windowRef to null to avoid re-trying.
+ this.download.source.windowRef = null;
+
+ if (!win) {
+ throw new DownloadError({
+ message: "PDF saver can't save a window that has been closed.",
+ becauseSourceFailed: true,
+ });
+ }
+
+ this.addToHistory();
+
+ let targetPath = this.download.target.path;
+
+ // An empty target file must exist for the PDF printer to work correctly.
+ let file = yield OS.File.open(targetPath, { truncate: true });
+ yield file.close();
+
+ let printSettings = gPrintSettingsService.newPrintSettings;
+
+ printSettings.printToFile = true;
+ printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
+ printSettings.toFileName = targetPath;
+
+ printSettings.printSilent = true;
+ printSettings.showPrintProgress = false;
+
+ printSettings.printBGImages = true;
+ printSettings.printBGColors = true;
+ printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
+ printSettings.headerStrCenter = "";
+ printSettings.headerStrLeft = "";
+ printSettings.headerStrRight = "";
+ printSettings.footerStrCenter = "";
+ printSettings.footerStrLeft = "";
+ printSettings.footerStrRight = "";
+
+ this._webBrowserPrint = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebBrowserPrint);
+
+ try {
+ yield new Promise((resolve, reject) => {
+ this._webBrowserPrint.print(printSettings, {
+ onStateChange: function (webProgress, request, stateFlags, status) {
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ if (!Components.isSuccessCode(status)) {
+ reject(new DownloadError({ result: status,
+ inferCause: true }));
+ } else {
+ resolve();
+ }
+ }
+ },
+ onProgressChange: function (webProgress, request, curSelfProgress,
+ maxSelfProgress, curTotalProgress,
+ maxTotalProgress) {
+ aSetProgressBytesFn(curTotalProgress, maxTotalProgress, false);
+ },
+ onLocationChange: function () {},
+ onStatusChange: function () {},
+ onSecurityChange: function () {},
+ });
+ });
+ } finally {
+ // Remove the print object to avoid leaks
+ this._webBrowserPrint = null;
+ }
+
+ let fileInfo = yield OS.File.stat(targetPath);
+ aSetProgressBytesFn(fileInfo.size, fileInfo.size, false);
+ }.bind(this));
+ },
+
+ /**
+ * Implements "DownloadSaver.cancel".
+ */
+ cancel: function DCS_cancel()
+ {
+ if (this._webBrowserPrint) {
+ this._webBrowserPrint.cancel();
+ this._webBrowserPrint = null;
+ }
+ },
+
+ /**
+ * Implements "DownloadSaver.toSerializable".
+ */
+ toSerializable: function ()
+ {
+ if (this.download.succeeded) {
+ return DownloadCopySaver.prototype.toSerializable.call(this);
+ }
+
+ // This object needs a window to recreate itself. If it didn't succeded
+ // it will not be possible to restart. Returning null here will
+ // prevent us from serializing it at all.
+ return null;
+ },
+};
+
+/**
+ * Creates a new DownloadPDFSaver object, with its initial state derived from
+ * its serializable representation.
+ *
+ * @param aSerializable
+ * Serializable representation of a DownloadPDFSaver object.
+ *
+ * @return The newly created DownloadPDFSaver object.
+ */
+this.DownloadPDFSaver.fromSerializable = function (aSerializable) {
+ return new DownloadPDFSaver();
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadImport.jsm b/toolkit/components/jsdownloads/src/DownloadImport.jsm
new file mode 100644
index 000000000..5fb7fd0c7
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadImport.jsm
@@ -0,0 +1,193 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadImport",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm")
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+/**
+ * These values come from the previous interface
+ * nsIDownloadManager, which has now been deprecated.
+ * These are the only types of download states that
+ * we will import.
+ */
+const DOWNLOAD_NOTSTARTED = -1;
+const DOWNLOAD_DOWNLOADING = 0;
+const DOWNLOAD_PAUSED = 4;
+const DOWNLOAD_QUEUED = 5;
+
+// DownloadImport
+
+/**
+ * Provides an object that has a method to import downloads
+ * from the previous SQLite storage format.
+ *
+ * @param aList A DownloadList where each successfully
+ * imported download will be added.
+ * @param aPath The path to the database file.
+ */
+this.DownloadImport = function (aList, aPath)
+{
+ this.list = aList;
+ this.path = aPath;
+}
+
+this.DownloadImport.prototype = {
+ /**
+ * Imports unfinished downloads from the previous SQLite storage
+ * format (supporting schemas 7 and up), to the new Download object
+ * format. Each imported download will be added to the DownloadList
+ *
+ * @return {Promise}
+ * @resolves When the operation has completed (i.e., every download
+ * from the previous database has been read and added to
+ * the DownloadList)
+ */
+ import: function () {
+ return Task.spawn(function* task_DI_import() {
+ let connection = yield Sqlite.openConnection({ path: this.path });
+
+ try {
+ let schemaVersion = yield connection.getSchemaVersion();
+ // We don't support schemas older than version 7 (from 2007)
+ // - Version 7 added the columns mimeType, preferredApplication
+ // and preferredAction in 2007
+ // - Version 8 added the column autoResume in 2007
+ // (if we encounter version 7 we will treat autoResume = false)
+ // - Version 9 is the last known version, which added a unique
+ // GUID text column that is not used here
+ if (schemaVersion < 7) {
+ throw new Error("Unable to import in-progress downloads because "
+ + "the existing profile is too old.");
+ }
+
+ let rows = yield connection.execute("SELECT * FROM moz_downloads");
+
+ for (let row of rows) {
+ try {
+ // Get the DB row data
+ let source = row.getResultByName("source");
+ let target = row.getResultByName("target");
+ let tempPath = row.getResultByName("tempPath");
+ let startTime = row.getResultByName("startTime");
+ let state = row.getResultByName("state");
+ let referrer = row.getResultByName("referrer");
+ let maxBytes = row.getResultByName("maxBytes");
+ let mimeType = row.getResultByName("mimeType");
+ let preferredApplication = row.getResultByName("preferredApplication");
+ let preferredAction = row.getResultByName("preferredAction");
+ let entityID = row.getResultByName("entityID");
+
+ let autoResume = false;
+ try {
+ autoResume = (row.getResultByName("autoResume") == 1);
+ } catch (ex) {
+ // autoResume wasn't present in schema version 7
+ }
+
+ if (!source) {
+ throw new Error("Attempted to import a row with an empty " +
+ "source column.");
+ }
+
+ let resumeDownload = false;
+
+ switch (state) {
+ case DOWNLOAD_NOTSTARTED:
+ case DOWNLOAD_QUEUED:
+ case DOWNLOAD_DOWNLOADING:
+ resumeDownload = true;
+ break;
+
+ case DOWNLOAD_PAUSED:
+ resumeDownload = autoResume;
+ break;
+
+ default:
+ // We won't import downloads in other states
+ continue;
+ }
+
+ // Transform the data
+ let targetPath = NetUtil.newURI(target)
+ .QueryInterface(Ci.nsIFileURL).file.path;
+
+ let launchWhenSucceeded = (preferredAction != Ci.nsIMIMEInfo.saveToDisk);
+
+ let downloadOptions = {
+ source: {
+ url: source,
+ referrer: referrer
+ },
+ target: {
+ path: targetPath,
+ partFilePath: tempPath,
+ },
+ saver: {
+ type: "copy",
+ entityID: entityID
+ },
+ startTime: new Date(startTime / 1000),
+ totalBytes: maxBytes,
+ hasPartialData: !!tempPath,
+ tryToKeepPartialData: true,
+ launchWhenSucceeded: launchWhenSucceeded,
+ contentType: mimeType,
+ launcherPath: preferredApplication
+ };
+
+ // Paused downloads that should not be auto-resumed are considered
+ // in a "canceled" state.
+ if (!resumeDownload) {
+ downloadOptions.canceled = true;
+ }
+
+ let download = yield Downloads.createDownload(downloadOptions);
+
+ yield this.list.add(download);
+
+ if (resumeDownload) {
+ download.start().catch(() => {});
+ } else {
+ yield download.refresh();
+ }
+
+ } catch (ex) {
+ Cu.reportError("Error importing download: " + ex);
+ }
+ }
+
+ } catch (ex) {
+ Cu.reportError(ex);
+ } finally {
+ yield connection.close();
+ }
+ }.bind(this));
+ }
+}
+
diff --git a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
new file mode 100644
index 000000000..5fed9212a
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -0,0 +1,1273 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+/**
+ * Provides functions to integrate with the host application, handling for
+ * example the global prompts on shutdown.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadIntegration",
+];
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+ "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+ "resource://gre/modules/DeferredTask.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadStore",
+ "resource://gre/modules/DownloadStore.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
+ "resource://gre/modules/DownloadImport.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
+ "resource://gre/modules/DownloadUIHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+#ifdef MOZ_PLACES
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+#endif
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.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",
+ "@mozilla.org/mime;1",
+ "nsIMIMEService");
+XPCOMUtils.defineLazyServiceGetter(this, "gExternalProtocolService",
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ "nsIExternalProtocolService");
+#ifdef MOZ_WIDGET_ANDROID
+XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions",
+ "resource://gre/modules/RuntimePermissions.jsm");
+#endif
+
+XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
+ if ("@mozilla.org/parental-controls-service;1" in Cc) {
+ return Cc["@mozilla.org/parental-controls-service;1"]
+ .createInstance(Ci.nsIParentalControlsService);
+ }
+ return null;
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "gApplicationReputationService",
+ "@mozilla.org/downloads/application-reputation-service;1",
+ Ci.nsIApplicationReputationService);
+
+XPCOMUtils.defineLazyServiceGetter(this, "volumeService",
+ "@mozilla.org/telephony/volume-service;1",
+ "nsIVolumeService");
+
+// We have to use the gCombinedDownloadIntegration identifier because, in this
+// module only, the DownloadIntegration identifier refers to the base version.
+Integration.downloads.defineModuleGetter(this, "gCombinedDownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm",
+ "DownloadIntegration");
+
+const Timer = Components.Constructor("@mozilla.org/timer;1", "nsITimer",
+ "initWithCallback");
+
+/**
+ * Indicates the delay between a change to the downloads data and the related
+ * save operation.
+ *
+ * For best efficiency, this value should be high enough that the input/output
+ * for opening or closing the target file does not overlap with the one for
+ * saving the list of downloads.
+ */
+const kSaveDelayMs = 1500;
+
+/**
+ * This pref indicates if we have already imported (or attempted to import)
+ * the downloads database from the previous SQLite storage.
+ */
+const kPrefImportedFromSqlite = "browser.download.importedFromSqlite";
+
+/**
+ * List of observers to listen against
+ */
+const kObserverTopics = [
+ "quit-application-requested",
+ "offline-requested",
+ "last-pb-context-exiting",
+ "last-pb-context-exited",
+ "sleep_notification",
+ "suspend_process_notification",
+ "wake_notification",
+ "resume_process_notification",
+ "network:offline-about-to-go-offline",
+ "network:offline-status-changed",
+ "xpcom-will-shutdown",
+];
+
+/**
+ * Maps nsIApplicationReputationService verdicts with the DownloadError ones.
+ */
+const kVerdictMap = {
+ [Ci.nsIApplicationReputationService.VERDICT_DANGEROUS]:
+ Downloads.Error.BLOCK_VERDICT_MALWARE,
+ [Ci.nsIApplicationReputationService.VERDICT_UNCOMMON]:
+ Downloads.Error.BLOCK_VERDICT_UNCOMMON,
+ [Ci.nsIApplicationReputationService.VERDICT_POTENTIALLY_UNWANTED]:
+ Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED,
+ [Ci.nsIApplicationReputationService.VERDICT_DANGEROUS_HOST]:
+ Downloads.Error.BLOCK_VERDICT_MALWARE,
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadIntegration
+
+/**
+ * Provides functions to integrate with the host application, handling for
+ * example the global prompts on shutdown.
+ */
+this.DownloadIntegration = {
+ /**
+ * Main DownloadStore object for loading and saving the list of persistent
+ * downloads, or null if the download list was never requested and thus it
+ * doesn't need to be persisted.
+ */
+ _store: null,
+
+ /**
+ * Returns whether data for blocked downloads should be kept on disk.
+ * Implementations which support unblocking downloads may return true to
+ * keep the blocked download on disk until its fate is decided.
+ *
+ * If a download is blocked and the partial data is kept the Download's
+ * 'hasBlockedData' property will be true. In this state Download.unblock()
+ * or Download.confirmBlock() may be used to either unblock the download or
+ * remove the downloaded data respectively.
+ *
+ * Even if shouldKeepBlockedData returns true, if the download did not use a
+ * partFile the blocked data will be removed - preventing the complete
+ * download from existing on disk with its final filename.
+ *
+ * @return boolean True if data should be kept.
+ */
+ shouldKeepBlockedData() {
+ const FIREFOX_ID = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+ return Services.appinfo.ID == FIREFOX_ID;
+ },
+
+ /**
+ * Performs initialization of the list of persistent downloads, before its
+ * first use by the host application. This function may be called only once
+ * during the entire lifetime of the application.
+ *
+ * @param list
+ * DownloadList object to be initialized.
+ *
+ * @return {Promise}
+ * @resolves When the list has been initialized.
+ * @rejects JavaScript exception.
+ */
+ initializePublicDownloadList: Task.async(function* (list) {
+ try {
+ yield this.loadPublicDownloadListFromStore(list);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ // After the list of persistent downloads has been loaded, we can add the
+ // history observers, even if the load operation failed. This object is kept
+ // alive by the history service.
+ new DownloadHistoryObserver(list);
+ }),
+
+ /**
+ * Called by initializePublicDownloadList to load the list of persistent
+ * downloads, before its first use by the host application. This function may
+ * be called only once during the entire lifetime of the application.
+ *
+ * @param list
+ * DownloadList object to be populated with the download objects
+ * serialized from the previous session. This list will be persisted
+ * to disk during the session lifetime.
+ *
+ * @return {Promise}
+ * @resolves When the list has been populated.
+ * @rejects JavaScript exception.
+ */
+ loadPublicDownloadListFromStore: Task.async(function* (list) {
+ if (this._store) {
+ throw new Error("Initialization may be performed only once.");
+ }
+
+ this._store = new DownloadStore(list, OS.Path.join(
+ OS.Constants.Path.profileDir,
+ "downloads.json"));
+ this._store.onsaveitem = this.shouldPersistDownload.bind(this);
+
+ try {
+ if (this._importedFromSqlite) {
+ yield this._store.load();
+ } else {
+ let sqliteDBpath = OS.Path.join(OS.Constants.Path.profileDir,
+ "downloads.sqlite");
+
+ if (yield OS.File.exists(sqliteDBpath)) {
+ let sqliteImport = new DownloadImport(list, sqliteDBpath);
+ yield sqliteImport.import();
+
+ let importCount = (yield list.getAll()).length;
+ if (importCount > 0) {
+ try {
+ yield this._store.save();
+ } catch (ex) { }
+ }
+
+ // No need to wait for the file removal.
+ OS.File.remove(sqliteDBpath).then(null, Cu.reportError);
+ }
+
+ Services.prefs.setBoolPref(kPrefImportedFromSqlite, true);
+
+ // Don't even report error here because this file is pre Firefox 3
+ // and most likely doesn't exist.
+ OS.File.remove(OS.Path.join(OS.Constants.Path.profileDir,
+ "downloads.rdf")).catch(() => {});
+
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ // Add the view used for detecting changes to downloads to be persisted.
+ // We must do this after the list of persistent downloads has been loaded,
+ // even if the load operation failed. We wait for a complete initialization
+ // so other callers cannot modify the list without being detected. The
+ // DownloadAutoSaveView is kept alive by the underlying DownloadList.
+ yield new DownloadAutoSaveView(list, this._store).initialize();
+ }),
+
+ /**
+ * Determines if a Download object from the list of persistent downloads
+ * should be saved into a file, so that it can be restored across sessions.
+ *
+ * This function allows filtering out downloads that the host application is
+ * not interested in persisting across sessions, for example downloads that
+ * finished successfully.
+ *
+ * @param aDownload
+ * The Download object to be inspected. This is originally taken from
+ * the global DownloadList object for downloads that were not started
+ * from a private browsing window. The item may have been removed
+ * from the list since the save operation started, though in this case
+ * the save operation will be repeated later.
+ *
+ * @return True to save the download, false otherwise.
+ */
+ shouldPersistDownload(aDownload) {
+ // On all platforms, we save all the downloads currently in progress, as
+ // well as stopped downloads for which we retained partially downloaded
+ // data or we have blocked data.
+ if (!aDownload.stopped || aDownload.hasPartialData ||
+ aDownload.hasBlockedData) {
+ return true;
+ }
+#ifdef MOZ_B2G
+ // On B2G we keep a few days of history.
+ let maxTime = Date.now() -
+ Services.prefs.getIntPref("dom.downloads.max_retention_days") * 24 * 60 * 60 * 1000;
+ return aDownload.startTime > maxTime;
+#elif defined(MOZ_WIDGET_ANDROID)
+ // On Android we store all history.
+ return true;
+#else
+ // On Desktop, stopped downloads for which we don't need to track the
+ // presence of a ".part" file are only retained in the browser history.
+ return false;
+#endif
+ },
+
+ /**
+ * Returns the system downloads directory asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getSystemDownloadsDirectory: Task.async(function* () {
+ if (this._downloadsDirectory) {
+ return this._downloadsDirectory;
+ }
+
+ let directoryPath = null;
+#ifdef XP_MACOSX
+ directoryPath = this._getDirectory("DfltDwnld");
+#elifdef XP_WIN
+ // For XP/2K, use My Documents/Downloads. Other version uses
+ // the default Downloads directory.
+ let version = parseFloat(Services.sysinfo.getProperty("version"));
+ if (version < 6) {
+ directoryPath = yield this._createDownloadsDirectory("Pers");
+ } else {
+ directoryPath = this._getDirectory("DfltDwnld");
+ }
+#elifdef XP_UNIX
+#ifdef MOZ_WIDGET_ANDROID
+ // Android doesn't have a $HOME directory, and by default we only have
+ // write access to /data/data/org.mozilla.{$APP} and /sdcard
+ directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
+ if (!directoryPath) {
+ throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
+ Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
+ }
+#else
+ // For Linux, use XDG download dir, with a fallback to Home/Downloads
+ // if the XDG user dirs are disabled.
+ try {
+ directoryPath = this._getDirectory("DfltDwnld");
+ } catch(e) {
+ directoryPath = yield this._createDownloadsDirectory("Home");
+ }
+#endif
+#else
+ directoryPath = yield this._createDownloadsDirectory("Home");
+#endif
+
+ this._downloadsDirectory = directoryPath;
+ return this._downloadsDirectory;
+ }),
+ _downloadsDirectory: null,
+
+ /**
+ * Returns the user downloads directory asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getPreferredDownloadsDirectory: Task.async(function* () {
+ let directoryPath = null;
+ let prefValue = 1;
+
+ try {
+ prefValue = Services.prefs.getIntPref("browser.download.folderList");
+ } catch(e) {}
+
+ switch(prefValue) {
+ case 0: // Desktop
+ directoryPath = this._getDirectory("Desk");
+ break;
+ case 1: // Downloads
+ directoryPath = yield this.getSystemDownloadsDirectory();
+ break;
+ case 2: // Custom
+ try {
+ let directory = Services.prefs.getComplexValue("browser.download.dir",
+ Ci.nsIFile);
+ directoryPath = directory.path;
+ yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
+ } catch(ex) {
+ // Either the preference isn't set or the directory cannot be created.
+ directoryPath = yield this.getSystemDownloadsDirectory();
+ }
+ break;
+ default:
+ directoryPath = yield this.getSystemDownloadsDirectory();
+ }
+ return directoryPath;
+ }),
+
+ /**
+ * Returns the temporary downloads directory asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getTemporaryDownloadsDirectory: Task.async(function* () {
+ let directoryPath = null;
+#ifdef XP_MACOSX
+ directoryPath = yield this.getPreferredDownloadsDirectory();
+#elifdef MOZ_WIDGET_ANDROID
+ directoryPath = yield this.getSystemDownloadsDirectory();
+#elifdef MOZ_WIDGET_GONK
+ directoryPath = yield this.getSystemDownloadsDirectory();
+#else
+ directoryPath = this._getDirectory("TmpD");
+#endif
+ return directoryPath;
+ }),
+
+ /**
+ * Checks to determine whether to block downloads for parental controls.
+ *
+ * aParam aDownload
+ * The download object.
+ *
+ * @return {Promise}
+ * @resolves The boolean indicates to block downloads or not.
+ */
+ shouldBlockForParentalControls(aDownload) {
+ let isEnabled = gParentalControlsService &&
+ gParentalControlsService.parentalControlsEnabled;
+ let shouldBlock = isEnabled &&
+ gParentalControlsService.blockFileDownloadsEnabled;
+
+ // Log the event if required by parental controls settings.
+ if (isEnabled && gParentalControlsService.loggingEnabled) {
+ gParentalControlsService.log(gParentalControlsService.ePCLog_FileDownload,
+ shouldBlock,
+ NetUtil.newURI(aDownload.source.url), null);
+ }
+
+ return Promise.resolve(shouldBlock);
+ },
+
+ /**
+ * Checks to determine whether to block downloads for not granted runtime permissions.
+ *
+ * @return {Promise}
+ * @resolves The boolean indicates to block downloads or not.
+ */
+ shouldBlockForRuntimePermissions() {
+#ifdef MOZ_WIDGET_ANDROID
+ return RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE)
+ .then(permissionGranted => !permissionGranted);
+#else
+ return Promise.resolve(false);
+#endif
+ },
+
+ /**
+ * Checks to determine whether to block downloads because they might be
+ * malware, based on application reputation checks.
+ *
+ * aParam aDownload
+ * The download object.
+ *
+ * @return {Promise}
+ * @resolves Object with the following properties:
+ * {
+ * shouldBlock: Whether the download should be blocked.
+ * verdict: Detailed reason for the block, according to the
+ * "Downloads.Error.BLOCK_VERDICT_" constants, or empty
+ * string if the reason is unknown.
+ * }
+ */
+ shouldBlockForReputationCheck(aDownload) {
+ let hash;
+ let sigInfo;
+ let channelRedirects;
+ try {
+ hash = aDownload.saver.getSha256Hash();
+ sigInfo = aDownload.saver.getSignatureInfo();
+ channelRedirects = aDownload.saver.getRedirects();
+ } catch (ex) {
+ // Bail if DownloadSaver doesn't have a hash or signature info.
+ return Promise.resolve({
+ shouldBlock: false,
+ verdict: "",
+ });
+ }
+ if (!hash || !sigInfo) {
+ return Promise.resolve({
+ shouldBlock: false,
+ verdict: "",
+ });
+ }
+ let deferred = Promise.defer();
+ let aReferrer = null;
+ if (aDownload.source.referrer) {
+ aReferrer = NetUtil.newURI(aDownload.source.referrer);
+ }
+ gApplicationReputationService.queryReputation({
+ sourceURI: NetUtil.newURI(aDownload.source.url),
+ referrerURI: aReferrer,
+ fileSize: aDownload.currentBytes,
+ sha256Hash: hash,
+ suggestedFileName: OS.Path.basename(aDownload.target.path),
+ signatureInfo: sigInfo,
+ redirects: channelRedirects },
+ function onComplete(aShouldBlock, aRv, aVerdict) {
+ deferred.resolve({
+ shouldBlock: aShouldBlock,
+ verdict: (aShouldBlock && kVerdictMap[aVerdict]) || "",
+ });
+ });
+ return deferred.promise;
+ },
+
+#ifdef XP_WIN
+ /**
+ * Checks whether downloaded files should be marked as coming from
+ * Internet Zone.
+ *
+ * @return true if files should be marked
+ */
+ _shouldSaveZoneInformation() {
+ let key = Cc["@mozilla.org/windows-registry-key;1"]
+ .createInstance(Ci.nsIWindowsRegKey);
+ try {
+ key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Attachments",
+ Ci.nsIWindowsRegKey.ACCESS_QUERY_VALUE);
+ try {
+ return key.readIntValue("SaveZoneInformation") != 1;
+ } finally {
+ key.close();
+ }
+ } catch (ex) {
+ // If the key is not present, files should be marked by default.
+ return true;
+ }
+ },
+#endif
+
+ /**
+ * Performs platform-specific operations when a download is done.
+ *
+ * aParam aDownload
+ * The Download object.
+ *
+ * @return {Promise}
+ * @resolves When all the operations completed successfully.
+ * @rejects JavaScript exception if any of the operations failed.
+ */
+ downloadDone: Task.async(function* (aDownload) {
+#ifdef XP_WIN
+ // On Windows, we mark any file saved to the NTFS file system as coming
+ // from the Internet security zone unless Group Policy disables the
+ // feature. We do this by writing to the "Zone.Identifier" Alternate
+ // Data Stream directly, because the Save method of the
+ // IAttachmentExecute interface would trigger operations that may cause
+ // the application to hang, or other performance issues.
+ // The stream created in this way is forward-compatible with all the
+ // current and future versions of Windows.
+ if (this._shouldSaveZoneInformation()) {
+ let zone;
+ try {
+ zone = gDownloadPlatform.mapUrlToZone(aDownload.source.url);
+ } catch (e) {
+ // Default to Internet Zone if mapUrlToZone failed for
+ // whatever reason.
+ zone = Ci.mozIDownloadPlatform.ZONE_INTERNET;
+ }
+ try {
+ // Don't write zone IDs for Local, Intranet, or Trusted sites
+ // to match Windows behavior.
+ if (zone >= Ci.mozIDownloadPlatform.ZONE_INTERNET) {
+ let streamPath = aDownload.target.path + ":Zone.Identifier";
+ let stream = yield OS.File.open(
+ streamPath,
+ { create: true },
+ { winAllowLengthBeyondMaxPathWithCaveats: true }
+ );
+ try {
+ yield stream.write(new TextEncoder().encode("[ZoneTransfer]\r\nZoneId=" + zone + "\r\n"));
+ } finally {
+ yield stream.close();
+ }
+ }
+ } catch (ex) {
+ // If writing to the stream fails, we ignore the error and continue.
+ // The Windows API error 123 (ERROR_INVALID_NAME) is expected to
+ // occur when working on a file system that does not support
+ // Alternate Data Streams, like FAT32, thus we don't report this
+ // specific error.
+ if (!(ex instanceof OS.File.Error) || ex.winLastError != 123) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+#endif
+
+ // The file with the partially downloaded data has restrictive permissions
+ // that don't allow other users on the system to access it. Now that the
+ // download is completed, we need to adjust permissions based on whether
+ // this is a permanently downloaded file or a temporary download to be
+ // opened read-only with an external application.
+ try {
+ // The following logic to determine whether this is a temporary download
+ // is due to the fact that "deleteTempFileOnExit" is false on Mac, where
+ // downloads to be opened with external applications are preserved in
+ // the "Downloads" folder like normal downloads.
+ let isTemporaryDownload =
+ aDownload.launchWhenSucceeded && (aDownload.source.isPrivate ||
+ Services.prefs.getBoolPref("browser.helperApps.deleteTempFileOnExit"));
+ // Permanently downloaded files are made accessible by other users on
+ // this system, while temporary downloads are marked as read-only.
+ let options = {};
+ if (isTemporaryDownload) {
+ options.unixMode = 0o400;
+ options.winAttributes = {readOnly: true};
+ } else {
+ options.unixMode = 0o666;
+ }
+ // On Unix, the umask of the process is respected.
+ yield OS.File.setPermissions(aDownload.target.path, options);
+ } catch (ex) {
+ // We should report errors with making the permissions less restrictive
+ // or marking the file as read-only on Unix and Mac, but this should not
+ // prevent the download from completing.
+ // The setPermissions API error EPERM is expected to occur when working
+ // on a file system that does not support file permissions, like FAT32,
+ // thus we don't report this error.
+ if (!(ex instanceof OS.File.Error) || ex.unixErrno != OS.Constants.libc.EPERM) {
+ Cu.reportError(ex);
+ }
+ }
+
+ let aReferrer = null;
+ if (aDownload.source.referrer) {
+ aReferrer = NetUtil.newURI(aDownload.source.referrer);
+ }
+
+ gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),
+ aReferrer,
+ new FileUtils.File(aDownload.target.path),
+ aDownload.contentType,
+ aDownload.source.isPrivate);
+ }),
+
+ /**
+ * Launches a file represented by the target of a download. This can
+ * open the file with the default application for the target MIME type
+ * or file extension, or with a custom application if
+ * aDownload.launcherPath is set.
+ *
+ * @param aDownload
+ * A Download object that contains the necessary information
+ * to launch the file. The relevant properties are: the target
+ * file, the contentType and the custom application chosen
+ * to launch it.
+ *
+ * @return {Promise}
+ * @resolves When the instruction to launch the file has been
+ * successfully given to the operating system. Note that
+ * the OS might still take a while until the file is actually
+ * launched.
+ * @rejects JavaScript exception if there was an error trying to launch
+ * the file.
+ */
+ launchDownload: Task.async(function* (aDownload) {
+ let file = new FileUtils.File(aDownload.target.path);
+
+#ifndef XP_WIN
+ // Ask for confirmation if the file is executable, except on Windows where
+ // the operating system will show the prompt based on the security zone.
+ // We do this here, instead of letting the caller handle the prompt
+ // separately in the user interface layer, for two reasons. The first is
+ // because of its security nature, so that add-ons cannot forget to do
+ // this check. The second is that the system-level security prompt would
+ // be displayed at launch time in any case.
+ if (file.isExecutable() &&
+ !(yield this.confirmLaunchExecutable(file.path))) {
+ return;
+ }
+#endif
+
+ // In case of a double extension, like ".tar.gz", we only
+ // consider the last one, because the MIME service cannot
+ // handle multiple extensions.
+ let fileExtension = null, mimeInfo = null;
+ let match = file.leafName.match(/\.([^.]+)$/);
+ if (match) {
+ fileExtension = match[1];
+ }
+
+ try {
+ // The MIME service might throw if contentType == "" and it can't find
+ // a MIME type for the given extension, so we'll treat this case as
+ // an unknown mimetype.
+ mimeInfo = gMIMEService.getFromTypeAndExtension(aDownload.contentType,
+ fileExtension);
+ } catch (e) { }
+
+ if (aDownload.launcherPath) {
+ if (!mimeInfo) {
+ // This should not happen on normal circumstances because launcherPath
+ // is only set when we had an instance of nsIMIMEInfo to retrieve
+ // the custom application chosen by the user.
+ throw new Error(
+ "Unable to create nsIMIMEInfo to launch a custom application");
+ }
+
+ // Custom application chosen
+ let localHandlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]
+ .createInstance(Ci.nsILocalHandlerApp);
+ localHandlerApp.executable = new FileUtils.File(aDownload.launcherPath);
+
+ mimeInfo.preferredApplicationHandler = localHandlerApp;
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useHelperApp;
+
+ this.launchFile(file, mimeInfo);
+ return;
+ }
+
+ // No custom application chosen, let's launch the file with the default
+ // handler. First, let's try to launch it through the MIME service.
+ if (mimeInfo) {
+ mimeInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
+
+ try {
+ this.launchFile(file, mimeInfo);
+ return;
+ } catch (ex) { }
+ }
+
+ // If it didn't work or if there was no MIME info available,
+ // let's try to directly launch the file.
+ try {
+ this.launchFile(file);
+ return;
+ } catch (ex) { }
+
+ // If our previous attempts failed, try sending it through
+ // the system's external "file:" URL handler.
+ gExternalProtocolService.loadUrl(NetUtil.newURI(file));
+ }),
+
+ /**
+ * Asks for confirmation for launching the specified executable file. This
+ * can be overridden by regression tests to avoid the interactive prompt.
+ */
+ confirmLaunchExecutable: Task.async(function* (path) {
+ // We don't anchor the prompt to a specific window intentionally, not
+ // only because this is the same behavior as the system-level prompt,
+ // but also because the most recently active window is the right choice
+ // in basically all cases.
+ return yield DownloadUIHelper.getPrompter().confirmLaunchExecutable(path);
+ }),
+
+ /**
+ * Launches the specified file, unless overridden by regression tests.
+ */
+ launchFile(file, mimeInfo) {
+ if (mimeInfo) {
+ mimeInfo.launchWithFile(file);
+ } else {
+ file.launch();
+ }
+ },
+
+ /**
+ * Shows the containing folder of a file.
+ *
+ * @param aFilePath
+ * The path to the file.
+ *
+ * @return {Promise}
+ * @resolves When the instruction to open the containing folder has been
+ * successfully given to the operating system. Note that
+ * the OS might still take a while until the folder is actually
+ * opened.
+ * @rejects JavaScript exception if there was an error trying to open
+ * the containing folder.
+ */
+ showContainingDirectory: Task.async(function* (aFilePath) {
+ let file = new FileUtils.File(aFilePath);
+
+ try {
+ // Show the directory containing the file and select the file.
+ file.reveal();
+ return;
+ } catch (ex) { }
+
+ // If reveal fails for some reason (e.g., it's not implemented on unix
+ // or the file doesn't exist), try using the parent if we have it.
+ let parent = file.parent;
+ if (!parent) {
+ throw new Error(
+ "Unexpected reference to a top-level directory instead of a file");
+ }
+
+ try {
+ // Open the parent directory to show where the file should be.
+ parent.launch();
+ return;
+ } catch (ex) { }
+
+ // If launch also fails (probably because it's not implemented), let
+ // the OS handler try to open the parent.
+ gExternalProtocolService.loadUrl(NetUtil.newURI(parent));
+ }),
+
+ /**
+ * Calls the directory service, create a downloads directory and returns an
+ * nsIFile for the downloads directory.
+ *
+ * @return {Promise}
+ * @resolves The directory string path.
+ */
+ _createDownloadsDirectory(aName) {
+ // We read the name of the directory from the list of translated strings
+ // that is kept by the UI helper module, even if this string is not strictly
+ // displayed in the user interface.
+ let directoryPath = OS.Path.join(this._getDirectory(aName),
+ DownloadUIHelper.strings.downloadsFolder);
+
+ // Create the Downloads folder and ignore if it already exists.
+ return OS.File.makeDir(directoryPath, { ignoreExisting: true })
+ .then(() => directoryPath);
+ },
+
+ /**
+ * Returns the string path for the given directory service location name. This
+ * can be overridden by regression tests to return the path of the system
+ * temporary directory in all cases.
+ */
+ _getDirectory(name) {
+ return Services.dirsvc.get(name, Ci.nsIFile).path;
+ },
+
+ /**
+ * Register the downloads interruption observers.
+ *
+ * @param aList
+ * The public or private downloads list.
+ * @param aIsPrivate
+ * True if the list is private, false otherwise.
+ *
+ * @return {Promise}
+ * @resolves When the views and observers are added.
+ */
+ addListObservers(aList, aIsPrivate) {
+ DownloadObserver.registerView(aList, aIsPrivate);
+ if (!DownloadObserver.observersAdded) {
+ DownloadObserver.observersAdded = true;
+ for (let topic of kObserverTopics) {
+ Services.obs.addObserver(DownloadObserver, topic, false);
+ }
+ }
+ return Promise.resolve();
+ },
+
+ /**
+ * Force a save on _store if it exists. Used to ensure downloads do not
+ * persist after being sanitized on Android.
+ *
+ * @return {Promise}
+ * @resolves When _store.save() completes.
+ */
+ forceSave() {
+ if (this._store) {
+ return this._store.save();
+ }
+ return Promise.resolve();
+ },
+
+ /**
+ * Checks if we have already imported (or attempted to import)
+ * the downloads database from the previous SQLite storage.
+ *
+ * @return boolean True if we the previous DB was imported.
+ */
+ get _importedFromSqlite() {
+ try {
+ return Services.prefs.getBoolPref(kPrefImportedFromSqlite);
+ } catch (ex) {
+ return false;
+ }
+ },
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadObserver
+
+this.DownloadObserver = {
+ /**
+ * Flag to determine if the observers have been added previously.
+ */
+ observersAdded: false,
+
+ /**
+ * Timer used to delay restarting canceled downloads upon waking and returning
+ * online.
+ */
+ _wakeTimer: null,
+
+ /**
+ * Set that contains the in progress publics downloads.
+ * It's kept updated when a public download is added, removed or changes its
+ * properties.
+ */
+ _publicInProgressDownloads: new Set(),
+
+ /**
+ * Set that contains the in progress private downloads.
+ * It's kept updated when a private download is added, removed or changes its
+ * properties.
+ */
+ _privateInProgressDownloads: new Set(),
+
+ /**
+ * Set that contains the downloads that have been canceled when going offline
+ * or to sleep. These are started again when returning online or waking. This
+ * list is not persisted so when exiting and restarting, the downloads will not
+ * be started again.
+ */
+ _canceledOfflineDownloads: new Set(),
+
+ /**
+ * Registers a view that updates the corresponding downloads state set, based
+ * on the aIsPrivate argument. The set is updated when a download is added,
+ * removed or changes its properties.
+ *
+ * @param aList
+ * The public or private downloads list.
+ * @param aIsPrivate
+ * True if the list is private, false otherwise.
+ */
+ registerView: function DO_registerView(aList, aIsPrivate) {
+ let downloadsSet = aIsPrivate ? this._privateInProgressDownloads
+ : this._publicInProgressDownloads;
+ let downloadsView = {
+ onDownloadAdded: aDownload => {
+ if (!aDownload.stopped) {
+ downloadsSet.add(aDownload);
+ }
+ },
+ onDownloadChanged: aDownload => {
+ if (aDownload.stopped) {
+ downloadsSet.delete(aDownload);
+ } else {
+ downloadsSet.add(aDownload);
+ }
+ },
+ onDownloadRemoved: aDownload => {
+ downloadsSet.delete(aDownload);
+ // The download must also be removed from the canceled when offline set.
+ this._canceledOfflineDownloads.delete(aDownload);
+ }
+ };
+
+ // We register the view asynchronously.
+ aList.addView(downloadsView).then(null, Cu.reportError);
+ },
+
+ /**
+ * Wrapper that handles the test mode before calling the prompt that display
+ * a warning message box that informs that there are active downloads,
+ * and asks whether the user wants to cancel them or not.
+ *
+ * @param aCancel
+ * The observer notification subject.
+ * @param aDownloadsCount
+ * The current downloads count.
+ * @param aPrompter
+ * The prompter object that shows the confirm dialog.
+ * @param aPromptType
+ * The type of prompt notification depending on the observer.
+ */
+ _confirmCancelDownloads: function DO_confirmCancelDownload(
+ aCancel, aDownloadsCount, aPrompter, aPromptType) {
+ // If user has already dismissed the request, then do nothing.
+ if ((aCancel instanceof Ci.nsISupportsPRBool) && aCancel.data) {
+ return;
+ }
+ // Handle test mode
+ if (gCombinedDownloadIntegration._testPromptDownloads) {
+ gCombinedDownloadIntegration._testPromptDownloads = aDownloadsCount;
+ return;
+ }
+
+ aCancel.data = aPrompter.confirmCancelDownloads(aDownloadsCount, aPromptType);
+ },
+
+ /**
+ * Resume all downloads that were paused when going offline, used when waking
+ * from sleep or returning from being offline.
+ */
+ _resumeOfflineDownloads: function DO_resumeOfflineDownloads() {
+ this._wakeTimer = null;
+
+ for (let download of this._canceledOfflineDownloads) {
+ download.start().catch(() => {});
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsIObserver
+
+ observe: function DO_observe(aSubject, aTopic, aData) {
+ let downloadsCount;
+ let p = DownloadUIHelper.getPrompter();
+ switch (aTopic) {
+ case "quit-application-requested":
+ downloadsCount = this._publicInProgressDownloads.size +
+ this._privateInProgressDownloads.size;
+ this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_QUIT);
+ break;
+ case "offline-requested":
+ downloadsCount = this._publicInProgressDownloads.size +
+ this._privateInProgressDownloads.size;
+ this._confirmCancelDownloads(aSubject, downloadsCount, p, p.ON_OFFLINE);
+ break;
+ case "last-pb-context-exiting":
+ downloadsCount = this._privateInProgressDownloads.size;
+ this._confirmCancelDownloads(aSubject, downloadsCount, p,
+ p.ON_LEAVE_PRIVATE_BROWSING);
+ break;
+ case "last-pb-context-exited":
+ let promise = Task.spawn(function() {
+ let list = yield Downloads.getList(Downloads.PRIVATE);
+ let downloads = yield list.getAll();
+
+ // We can remove the downloads and finalize them in parallel.
+ for (let download of downloads) {
+ list.remove(download).then(null, Cu.reportError);
+ download.finalize(true).then(null, Cu.reportError);
+ }
+ });
+ // Handle test mode
+ if (gCombinedDownloadIntegration._testResolveClearPrivateList) {
+ gCombinedDownloadIntegration._testResolveClearPrivateList(promise);
+ } else {
+ promise.catch(ex => Cu.reportError(ex));
+ }
+ break;
+ case "sleep_notification":
+ case "suspend_process_notification":
+ case "network:offline-about-to-go-offline":
+ for (let download of this._publicInProgressDownloads) {
+ download.cancel();
+ this._canceledOfflineDownloads.add(download);
+ }
+ for (let download of this._privateInProgressDownloads) {
+ download.cancel();
+ this._canceledOfflineDownloads.add(download);
+ }
+ break;
+ case "wake_notification":
+ case "resume_process_notification":
+ let wakeDelay = 10000;
+ try {
+ wakeDelay = Services.prefs.getIntPref("browser.download.manager.resumeOnWakeDelay");
+ } catch(e) {}
+
+ if (wakeDelay >= 0) {
+ this._wakeTimer = new Timer(this._resumeOfflineDownloads.bind(this), wakeDelay,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ break;
+ case "network:offline-status-changed":
+ if (aData == "online") {
+ this._resumeOfflineDownloads();
+ }
+ break;
+ // We need to unregister observers explicitly before we reach the
+ // "xpcom-shutdown" phase, otherwise observers may be notified when some
+ // required services are not available anymore. We can't unregister
+ // observers on "quit-application", because this module is also loaded
+ // during "make package" automation, and the quit notification is not sent
+ // in that execution environment (bug 973637).
+ case "xpcom-will-shutdown":
+ for (let topic of kObserverTopics) {
+ Services.obs.removeObserver(this, topic);
+ }
+ break;
+ }
+ },
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadHistoryObserver
+
+#ifdef MOZ_PLACES
+/**
+ * Registers a Places observer so that operations on download history are
+ * reflected on the provided list of downloads.
+ *
+ * You do not need to keep a reference to this object in order to keep it alive,
+ * because the history service already keeps a strong reference to it.
+ *
+ * @param aList
+ * DownloadList object linked to this observer.
+ */
+this.DownloadHistoryObserver = function (aList)
+{
+ this._list = aList;
+ PlacesUtils.history.addObserver(this, false);
+}
+
+this.DownloadHistoryObserver.prototype = {
+ /**
+ * DownloadList object linked to this observer.
+ */
+ _list: null,
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver]),
+
+ ////////////////////////////////////////////////////////////////////////////
+ //// nsINavHistoryObserver
+
+ onDeleteURI: function DL_onDeleteURI(aURI, aGUID) {
+ this._list.removeFinished(download => aURI.equals(NetUtil.newURI(
+ download.source.url)));
+ },
+
+ onClearHistory: function DL_onClearHistory() {
+ this._list.removeFinished();
+ },
+
+ onTitleChanged: function () {},
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+};
+#else
+/**
+ * Empty implementation when we have no Places support, for example on B2G.
+ */
+this.DownloadHistoryObserver = function (aList) {}
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+//// DownloadAutoSaveView
+
+/**
+ * This view can be added to a DownloadList object to trigger a save operation
+ * in the given DownloadStore object when a relevant change occurs. You should
+ * call the "initialize" method in order to register the view and load the
+ * current state from disk.
+ *
+ * You do not need to keep a reference to this object in order to keep it alive,
+ * because the DownloadList object already keeps a strong reference to it.
+ *
+ * @param aList
+ * The DownloadList object on which the view should be registered.
+ * @param aStore
+ * The DownloadStore object used for saving.
+ */
+this.DownloadAutoSaveView = function (aList, aStore)
+{
+ this._list = aList;
+ this._store = aStore;
+ this._downloadsMap = new Map();
+ this._writer = new DeferredTask(() => this._store.save(), kSaveDelayMs);
+ AsyncShutdown.profileBeforeChange.addBlocker("DownloadAutoSaveView: writing data",
+ () => this._writer.finalize());
+}
+
+this.DownloadAutoSaveView.prototype = {
+ /**
+ * DownloadList object linked to this view.
+ */
+ _list: null,
+
+ /**
+ * The DownloadStore object used for saving.
+ */
+ _store: null,
+
+ /**
+ * True when the initial state of the downloads has been loaded.
+ */
+ _initialized: false,
+
+ /**
+ * Registers the view and loads the current state from disk.
+ *
+ * @return {Promise}
+ * @resolves When the view has been registered.
+ * @rejects JavaScript exception.
+ */
+ initialize: function ()
+ {
+ // We set _initialized to true after adding the view, so that
+ // onDownloadAdded doesn't cause a save to occur.
+ return this._list.addView(this).then(() => this._initialized = true);
+ },
+
+ /**
+ * This map contains only Download objects that should be saved to disk, and
+ * associates them with the result of their getSerializationHash function, for
+ * the purpose of detecting changes to the relevant properties.
+ */
+ _downloadsMap: null,
+
+ /**
+ * DeferredTask for the save operation.
+ */
+ _writer: null,
+
+ /**
+ * Called when the list of downloads changed, this triggers the asynchronous
+ * serialization of the list of downloads.
+ */
+ saveSoon: function ()
+ {
+ this._writer.arm();
+ },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// DownloadList view
+
+ onDownloadAdded: function (aDownload)
+ {
+ if (gCombinedDownloadIntegration.shouldPersistDownload(aDownload)) {
+ this._downloadsMap.set(aDownload, aDownload.getSerializationHash());
+ if (this._initialized) {
+ this.saveSoon();
+ }
+ }
+ },
+
+ onDownloadChanged: function (aDownload)
+ {
+ if (!gCombinedDownloadIntegration.shouldPersistDownload(aDownload)) {
+ if (this._downloadsMap.has(aDownload)) {
+ this._downloadsMap.delete(aDownload);
+ this.saveSoon();
+ }
+ return;
+ }
+
+ let hash = aDownload.getSerializationHash();
+ if (this._downloadsMap.get(aDownload) != hash) {
+ this._downloadsMap.set(aDownload, hash);
+ this.saveSoon();
+ }
+ },
+
+ onDownloadRemoved: function (aDownload)
+ {
+ if (this._downloadsMap.has(aDownload)) {
+ this._downloadsMap.delete(aDownload);
+ this.saveSoon();
+ }
+ },
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadLegacy.js b/toolkit/components/jsdownloads/src/DownloadLegacy.js
new file mode 100644
index 000000000..fc9fb35d2
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js
@@ -0,0 +1,309 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This component implements the XPCOM interfaces required for integration with
+ * the legacy download components.
+ *
+ * New code is expected to use the "Downloads.jsm" module directly, without
+ * going through the interfaces implemented in this XPCOM component. These
+ * interfaces are only maintained for backwards compatibility with components
+ * that still work synchronously on the main thread.
+ */
+
+"use strict";
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+// DownloadLegacyTransfer
+
+/**
+ * nsITransfer implementation that provides a bridge to a Download object.
+ *
+ * Legacy downloads work differently than the JavaScript implementation. In the
+ * latter, the caller only provides the properties for the Download object and
+ * the entire process is handled by the "start" method. In the legacy
+ * implementation, the caller must create a separate object to execute the
+ * download, and then make the download visible to the user by hooking it up to
+ * an nsITransfer instance.
+ *
+ * Since nsITransfer instances may be created before the download system is
+ * initialized, and initialization as well as other operations are asynchronous,
+ * this implementation is able to delay all progress and status notifications it
+ * receives until the associated Download object is finally created.
+ *
+ * Conversely, the DownloadLegacySaver object can also receive execution and
+ * cancellation requests asynchronously, before or after it is connected to
+ * this nsITransfer instance. For that reason, those requests are communicated
+ * in a potentially deferred way, using promise objects.
+ *
+ * The component that executes the download implements nsICancelable to receive
+ * cancellation requests, but after cancellation it cannot be reused again.
+ *
+ * Since the components that execute the download may be different and they
+ * don't always give consistent results, this bridge takes care of enforcing the
+ * expectations, for example by ensuring the target file exists when the
+ * download is successful, even if the source has a size of zero bytes.
+ */
+function DownloadLegacyTransfer()
+{
+ this._deferDownload = Promise.defer();
+}
+
+DownloadLegacyTransfer.prototype = {
+ classID: Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"),
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsIWebProgressListener2,
+ Ci.nsITransfer]),
+
+ // nsIWebProgressListener
+
+ onStateChange: function DLT_onStateChange(aWebProgress, aRequest, aStateFlags,
+ aStatus)
+ {
+ if (!Components.isSuccessCode(aStatus)) {
+ this._componentFailed = true;
+ }
+
+ if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
+ (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
+
+ let blockedByParentalControls = false;
+ // If it is a failed download, aRequest.responseStatus doesn't exist.
+ // (missing file on the server, network failure to download)
+ try {
+ // If the request's response has been blocked by Windows Parental Controls
+ // with an HTTP 450 error code, we must cancel the request synchronously.
+ blockedByParentalControls = aRequest instanceof Ci.nsIHttpChannel &&
+ aRequest.responseStatus == 450;
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ }
+
+ if (blockedByParentalControls) {
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+ }
+
+ // The main request has just started. Wait for the associated Download
+ // object to be available before notifying.
+ this._deferDownload.promise.then(download => {
+ // If the request was blocked, now that we have the download object we
+ // should set a flag that can be retrieved later when handling the
+ // cancellation so that the proper error can be thrown.
+ if (blockedByParentalControls) {
+ download._blockedByParentalControls = true;
+ }
+
+ download.saver.onTransferStarted(
+ aRequest,
+ this._cancelable instanceof Ci.nsIHelperAppLauncher);
+
+ // To handle asynchronous cancellation properly, we should hook up the
+ // handler only after we have been notified that the main request
+ // started. We will wait until the main request stopped before
+ // notifying that the download has been canceled. Since the request has
+ // not completed yet, deferCanceled is guaranteed to be set.
+ return download.saver.deferCanceled.promise.then(() => {
+ // Only cancel if the object executing the download is still running.
+ if (this._cancelable && !this._componentFailed) {
+ this._cancelable.cancel(Cr.NS_ERROR_ABORT);
+ if (this._cancelable instanceof Ci.nsIWebBrowserPersist) {
+ // This component will not send the STATE_STOP notification.
+ download.saver.onTransferFinished(aRequest, Cr.NS_ERROR_ABORT);
+ this._cancelable = null;
+ }
+ }
+ });
+ }).then(null, Cu.reportError);
+ } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
+ (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
+ // The last file has been received, or the download failed. Wait for the
+ // associated Download object to be available before notifying.
+ this._deferDownload.promise.then(download => {
+ // At this point, the hash has been set and we need to copy it to the
+ // DownloadSaver.
+ if (Components.isSuccessCode(aStatus)) {
+ download.saver.setSha256Hash(this._sha256Hash);
+ download.saver.setSignatureInfo(this._signatureInfo);
+ download.saver.setRedirects(this._redirects);
+ }
+ download.saver.onTransferFinished(aRequest, aStatus);
+ }).then(null, Cu.reportError);
+
+ // Release the reference to the component executing the download.
+ this._cancelable = null;
+ }
+ },
+
+ onProgressChange: function DLT_onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress)
+ {
+ this.onProgressChange64(aWebProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress);
+ },
+
+ onLocationChange: function () { },
+
+ onStatusChange: function DLT_onStatusChange(aWebProgress, aRequest, aStatus,
+ aMessage)
+ {
+ // The status change may optionally be received in addition to the state
+ // change, but if no network request actually started, it is possible that
+ // we only receive a status change with an error status code.
+ if (!Components.isSuccessCode(aStatus)) {
+ this._componentFailed = true;
+
+ // Wait for the associated Download object to be available.
+ this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) {
+ aDownload.saver.onTransferFinished(aRequest, aStatus);
+ }).then(null, Cu.reportError);
+ }
+ },
+
+ onSecurityChange: function () { },
+
+ // nsIWebProgressListener2
+
+ onProgressChange64: function DLT_onProgressChange64(aWebProgress, aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress)
+ {
+ // Wait for the associated Download object to be available.
+ this._deferDownload.promise.then(function DLT_OPC64_onDownload(aDownload) {
+ aDownload.saver.onProgressBytes(aCurTotalProgress, aMaxTotalProgress);
+ }).then(null, Cu.reportError);
+ },
+
+ onRefreshAttempted: function DLT_onRefreshAttempted(aWebProgress, aRefreshURI,
+ aMillis, aSameURI)
+ {
+ // Indicate that refreshes and redirects are allowed by default. However,
+ // note that download components don't usually call this method at all.
+ return true;
+ },
+
+ // nsITransfer
+
+ init: function DLT_init(aSource, aTarget, aDisplayName, aMIMEInfo, aStartTime,
+ aTempFile, aCancelable, aIsPrivate)
+ {
+ this._cancelable = aCancelable;
+
+ let launchWhenSucceeded = false, contentType = null, launcherPath = null;
+
+ if (aMIMEInfo instanceof Ci.nsIMIMEInfo) {
+ launchWhenSucceeded =
+ aMIMEInfo.preferredAction != Ci.nsIMIMEInfo.saveToDisk;
+ contentType = aMIMEInfo.type;
+
+ let appHandler = aMIMEInfo.preferredApplicationHandler;
+ if (aMIMEInfo.preferredAction == Ci.nsIMIMEInfo.useHelperApp &&
+ appHandler instanceof Ci.nsILocalHandlerApp) {
+ launcherPath = appHandler.executable.path;
+ }
+ }
+
+ // Create a new Download object associated to a DownloadLegacySaver, and
+ // wait for it to be available. This operation may cause the entire
+ // download system to initialize before the object is created.
+ Downloads.createDownload({
+ source: { url: aSource.spec, isPrivate: aIsPrivate },
+ target: { path: aTarget.QueryInterface(Ci.nsIFileURL).file.path,
+ partFilePath: aTempFile && aTempFile.path },
+ saver: "legacy",
+ launchWhenSucceeded: launchWhenSucceeded,
+ contentType: contentType,
+ launcherPath: launcherPath
+ }).then(function DLT_I_onDownload(aDownload) {
+ // Legacy components keep partial data when they use a ".part" file.
+ if (aTempFile) {
+ aDownload.tryToKeepPartialData = true;
+ }
+
+ // Start the download before allowing it to be controlled. Ignore errors.
+ aDownload.start().catch(() => {});
+
+ // Start processing all the other events received through nsITransfer.
+ this._deferDownload.resolve(aDownload);
+
+ // Add the download to the list, allowing it to be seen and canceled.
+ return Downloads.getList(Downloads.ALL).then(list => list.add(aDownload));
+ }.bind(this)).then(null, Cu.reportError);
+ },
+
+ setSha256Hash: function (hash)
+ {
+ this._sha256Hash = hash;
+ },
+
+ setSignatureInfo: function (signatureInfo)
+ {
+ this._signatureInfo = signatureInfo;
+ },
+
+ setRedirects: function (redirects)
+ {
+ this._redirects = redirects;
+ },
+
+ // Private methods and properties
+
+ /**
+ * This deferred object contains a promise that is resolved with the Download
+ * object associated with this nsITransfer instance, when it is available.
+ */
+ _deferDownload: null,
+
+ /**
+ * Reference to the component that is executing the download. This component
+ * allows cancellation through its nsICancelable interface.
+ */
+ _cancelable: null,
+
+ /**
+ * Indicates that the component that executes the download has notified a
+ * failure condition. In this case, we should never use the component methods
+ * that cancel the download.
+ */
+ _componentFailed: false,
+
+ /**
+ * Save the SHA-256 hash in raw bytes of the downloaded file.
+ */
+ _sha256Hash: null,
+
+ /**
+ * Save the signature info in a serialized protobuf of the downloaded file.
+ */
+ _signatureInfo: null,
+};
+
+// Module
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadLegacyTransfer]);
diff --git a/toolkit/components/jsdownloads/src/DownloadList.jsm b/toolkit/components/jsdownloads/src/DownloadList.jsm
new file mode 100644
index 000000000..f725bd3de
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadList.jsm
@@ -0,0 +1,559 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+/**
+ * This file includes the following constructors and global objects:
+ *
+ * DownloadList
+ * Represents a collection of Download objects that can be viewed and managed by
+ * the user interface, and persisted across sessions.
+ *
+ * DownloadCombinedList
+ * Provides a unified, unordered list combining public and private downloads.
+ *
+ * DownloadSummary
+ * Provides an aggregated view on the contents of a DownloadList.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadList",
+ "DownloadCombinedList",
+ "DownloadSummary",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+// DownloadList
+
+/**
+ * Represents a collection of Download objects that can be viewed and managed by
+ * the user interface, and persisted across sessions.
+ */
+this.DownloadList = function ()
+{
+ this._downloads = [];
+ this._views = new Set();
+}
+
+this.DownloadList.prototype = {
+ /**
+ * Array of Download objects currently in the list.
+ */
+ _downloads: null,
+
+ /**
+ * Retrieves a snapshot of the downloads that are currently in the list. The
+ * returned array does not change when downloads are added or removed, though
+ * the Download objects it contains are still updated in real time.
+ *
+ * @return {Promise}
+ * @resolves An array of Download objects.
+ * @rejects JavaScript exception.
+ */
+ getAll: function DL_getAll() {
+ return Promise.resolve(Array.slice(this._downloads, 0));
+ },
+
+ /**
+ * Adds a new download to the end of the items list.
+ *
+ * @note When a download is added to the list, its "onchange" event is
+ * registered by the list, thus it cannot be used to monitor the
+ * download. To receive change notifications for downloads that are
+ * added to the list, use the addView method to register for
+ * onDownloadChanged notifications.
+ *
+ * @param aDownload
+ * The Download object to add.
+ *
+ * @return {Promise}
+ * @resolves When the download has been added.
+ * @rejects JavaScript exception.
+ */
+ add: function DL_add(aDownload) {
+ this._downloads.push(aDownload);
+ aDownload.onchange = this._change.bind(this, aDownload);
+ this._notifyAllViews("onDownloadAdded", aDownload);
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Removes a download from the list. If the download was already removed,
+ * this method has no effect.
+ *
+ * This method does not change the state of the download, to allow adding it
+ * to another list, or control it directly. If you want to dispose of the
+ * download object, you should cancel it afterwards, and remove any partially
+ * downloaded data if needed.
+ *
+ * @param aDownload
+ * The Download object to remove.
+ *
+ * @return {Promise}
+ * @resolves When the download has been removed.
+ * @rejects JavaScript exception.
+ */
+ remove: function DL_remove(aDownload) {
+ let index = this._downloads.indexOf(aDownload);
+ if (index != -1) {
+ this._downloads.splice(index, 1);
+ aDownload.onchange = null;
+ this._notifyAllViews("onDownloadRemoved", aDownload);
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * This function is called when "onchange" events of downloads occur.
+ *
+ * @param aDownload
+ * The Download object that changed.
+ */
+ _change: function DL_change(aDownload) {
+ this._notifyAllViews("onDownloadChanged", aDownload);
+ },
+
+ /**
+ * Set of currently registered views.
+ */
+ _views: null,
+
+ /**
+ * Adds a view that will be notified of changes to downloads. The newly added
+ * view will receive onDownloadAdded notifications for all the downloads that
+ * are already in the list.
+ *
+ * @param aView
+ * The view object to add. The following methods may be defined:
+ * {
+ * onDownloadAdded: function (aDownload) {
+ * // Called after aDownload is added to the end of the list.
+ * },
+ * onDownloadChanged: function (aDownload) {
+ * // Called after the properties of aDownload change.
+ * },
+ * onDownloadRemoved: function (aDownload) {
+ * // Called after aDownload is removed from the list.
+ * },
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the view has been registered and all the onDownloadAdded
+ * notifications for the existing downloads have been sent.
+ * @rejects JavaScript exception.
+ */
+ addView: function DL_addView(aView)
+ {
+ this._views.add(aView);
+
+ if ("onDownloadAdded" in aView) {
+ for (let download of this._downloads) {
+ try {
+ aView.onDownloadAdded(download);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Removes a view that was previously added using addView.
+ *
+ * @param aView
+ * The view object to remove.
+ *
+ * @return {Promise}
+ * @resolves When the view has been removed. At this point, the removed view
+ * will not receive any more notifications.
+ * @rejects JavaScript exception.
+ */
+ removeView: function DL_removeView(aView)
+ {
+ this._views.delete(aView);
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Notifies all the views of a download addition, change, or removal.
+ *
+ * @param aMethodName
+ * String containing the name of the method to call on the view.
+ * @param aDownload
+ * The Download object that changed.
+ */
+ _notifyAllViews: function (aMethodName, aDownload) {
+ for (let view of this._views) {
+ try {
+ if (aMethodName in view) {
+ view[aMethodName](aDownload);
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ /**
+ * Removes downloads from the list that have finished, have failed, or have
+ * been canceled without keeping partial data. A filter function may be
+ * specified to remove only a subset of those downloads.
+ *
+ * This method finalizes each removed download, ensuring that any partially
+ * downloaded data associated with it is also removed.
+ *
+ * @param aFilterFn
+ * The filter function is called with each download as its only
+ * argument, and should return true to remove the download and false
+ * to keep it. This parameter may be null or omitted to have no
+ * additional filter.
+ */
+ removeFinished: function DL_removeFinished(aFilterFn) {
+ Task.spawn(function* () {
+ let list = yield this.getAll();
+ for (let download of list) {
+ // Remove downloads that have been canceled, even if the cancellation
+ // operation hasn't completed yet so we don't check "stopped" here.
+ // Failed downloads with partial data are also removed.
+ if (download.stopped && (!download.hasPartialData || download.error) &&
+ (!aFilterFn || aFilterFn(download))) {
+ // Remove the download first, so that the views don't get the change
+ // notifications that may occur during finalization.
+ yield this.remove(download);
+ // Ensure that the download is stopped and no partial data is kept.
+ // This works even if the download state has changed meanwhile. We
+ // don't need to wait for the procedure to be complete before
+ // processing the other downloads in the list.
+ download.finalize(true).then(null, Cu.reportError);
+ }
+ }
+ }.bind(this)).then(null, Cu.reportError);
+ },
+};
+
+// DownloadCombinedList
+
+/**
+ * Provides a unified, unordered list combining public and private downloads.
+ *
+ * Download objects added to this list are also added to one of the two
+ * underlying lists, based on their "source.isPrivate" property. Views on this
+ * list will receive notifications for both public and private downloads.
+ *
+ * @param aPublicList
+ * Underlying DownloadList containing public downloads.
+ * @param aPrivateList
+ * Underlying DownloadList containing private downloads.
+ */
+this.DownloadCombinedList = function (aPublicList, aPrivateList)
+{
+ DownloadList.call(this);
+ this._publicList = aPublicList;
+ this._privateList = aPrivateList;
+ aPublicList.addView(this).then(null, Cu.reportError);
+ aPrivateList.addView(this).then(null, Cu.reportError);
+}
+
+this.DownloadCombinedList.prototype = {
+ __proto__: DownloadList.prototype,
+
+ /**
+ * Underlying DownloadList containing public downloads.
+ */
+ _publicList: null,
+
+ /**
+ * Underlying DownloadList containing private downloads.
+ */
+ _privateList: null,
+
+ /**
+ * Adds a new download to the end of the items list.
+ *
+ * @note When a download is added to the list, its "onchange" event is
+ * registered by the list, thus it cannot be used to monitor the
+ * download. To receive change notifications for downloads that are
+ * added to the list, use the addView method to register for
+ * onDownloadChanged notifications.
+ *
+ * @param aDownload
+ * The Download object to add.
+ *
+ * @return {Promise}
+ * @resolves When the download has been added.
+ * @rejects JavaScript exception.
+ */
+ add: function (aDownload)
+ {
+ if (aDownload.source.isPrivate) {
+ return this._privateList.add(aDownload);
+ }
+ return this._publicList.add(aDownload);
+ },
+
+ /**
+ * Removes a download from the list. If the download was already removed,
+ * this method has no effect.
+ *
+ * This method does not change the state of the download, to allow adding it
+ * to another list, or control it directly. If you want to dispose of the
+ * download object, you should cancel it afterwards, and remove any partially
+ * downloaded data if needed.
+ *
+ * @param aDownload
+ * The Download object to remove.
+ *
+ * @return {Promise}
+ * @resolves When the download has been removed.
+ * @rejects JavaScript exception.
+ */
+ remove: function (aDownload)
+ {
+ if (aDownload.source.isPrivate) {
+ return this._privateList.remove(aDownload);
+ }
+ return this._publicList.remove(aDownload);
+ },
+
+ // DownloadList view
+
+ onDownloadAdded: function (aDownload)
+ {
+ this._downloads.push(aDownload);
+ this._notifyAllViews("onDownloadAdded", aDownload);
+ },
+
+ onDownloadChanged: function (aDownload)
+ {
+ this._notifyAllViews("onDownloadChanged", aDownload);
+ },
+
+ onDownloadRemoved: function (aDownload)
+ {
+ let index = this._downloads.indexOf(aDownload);
+ if (index != -1) {
+ this._downloads.splice(index, 1);
+ }
+ this._notifyAllViews("onDownloadRemoved", aDownload);
+ },
+};
+
+// DownloadSummary
+
+/**
+ * Provides an aggregated view on the contents of a DownloadList.
+ */
+this.DownloadSummary = function ()
+{
+ this._downloads = [];
+ this._views = new Set();
+}
+
+this.DownloadSummary.prototype = {
+ /**
+ * Array of Download objects that are currently part of the summary.
+ */
+ _downloads: null,
+
+ /**
+ * Underlying DownloadList whose contents should be summarized.
+ */
+ _list: null,
+
+ /**
+ * This method may be called once to bind this object to a DownloadList.
+ *
+ * Views on the summarized data can be registered before this object is bound
+ * to an actual list. This allows the summary to be used without requiring
+ * the initialization of the DownloadList first.
+ *
+ * @param aList
+ * Underlying DownloadList whose contents should be summarized.
+ *
+ * @return {Promise}
+ * @resolves When the view on the underlying list has been registered.
+ * @rejects JavaScript exception.
+ */
+ bindToList: function (aList)
+ {
+ if (this._list) {
+ throw new Error("bindToList may be called only once.");
+ }
+
+ return aList.addView(this).then(() => {
+ // Set the list reference only after addView has returned, so that we don't
+ // send a notification to our views for each download that is added.
+ this._list = aList;
+ this._onListChanged();
+ });
+ },
+
+ /**
+ * Set of currently registered views.
+ */
+ _views: null,
+
+ /**
+ * Adds a view that will be notified of changes to the summary. The newly
+ * added view will receive an initial onSummaryChanged notification.
+ *
+ * @param aView
+ * The view object to add. The following methods may be defined:
+ * {
+ * onSummaryChanged: function () {
+ * // Called after any property of the summary has changed.
+ * },
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the view has been registered and the onSummaryChanged
+ * notification has been sent.
+ * @rejects JavaScript exception.
+ */
+ addView: function (aView)
+ {
+ this._views.add(aView);
+
+ if ("onSummaryChanged" in aView) {
+ try {
+ aView.onSummaryChanged();
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Removes a view that was previously added using addView.
+ *
+ * @param aView
+ * The view object to remove.
+ *
+ * @return {Promise}
+ * @resolves When the view has been removed. At this point, the removed view
+ * will not receive any more notifications.
+ * @rejects JavaScript exception.
+ */
+ removeView: function (aView)
+ {
+ this._views.delete(aView);
+
+ return Promise.resolve();
+ },
+
+ /**
+ * Indicates whether all the downloads are currently stopped.
+ */
+ allHaveStopped: true,
+
+ /**
+ * Indicates the total number of bytes to be transferred before completing all
+ * the downloads that are currently in progress.
+ *
+ * For downloads that do not have a known final size, the number of bytes
+ * currently transferred is reported as part of this property.
+ *
+ * This is zero if no downloads are currently in progress.
+ */
+ progressTotalBytes: 0,
+
+ /**
+ * Number of bytes currently transferred as part of all the downloads that are
+ * currently in progress.
+ *
+ * This is zero if no downloads are currently in progress.
+ */
+ progressCurrentBytes: 0,
+
+ /**
+ * This function is called when any change in the list of downloads occurs,
+ * and will recalculate the summary and notify the views in case the
+ * aggregated properties are different.
+ */
+ _onListChanged: function () {
+ let allHaveStopped = true;
+ let progressTotalBytes = 0;
+ let progressCurrentBytes = 0;
+
+ // Recalculate the aggregated state. See the description of the individual
+ // properties for an explanation of the summarization logic.
+ for (let download of this._downloads) {
+ if (!download.stopped) {
+ allHaveStopped = false;
+ progressTotalBytes += download.hasProgress ? download.totalBytes
+ : download.currentBytes;
+ progressCurrentBytes += download.currentBytes;
+ }
+ }
+
+ // Exit now if the properties did not change.
+ if (this.allHaveStopped == allHaveStopped &&
+ this.progressTotalBytes == progressTotalBytes &&
+ this.progressCurrentBytes == progressCurrentBytes) {
+ return;
+ }
+
+ this.allHaveStopped = allHaveStopped;
+ this.progressTotalBytes = progressTotalBytes;
+ this.progressCurrentBytes = progressCurrentBytes;
+
+ // Notify all the views that our properties changed.
+ for (let view of this._views) {
+ try {
+ if ("onSummaryChanged" in view) {
+ view.onSummaryChanged();
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ },
+
+ // DownloadList view
+
+ onDownloadAdded: function (aDownload)
+ {
+ this._downloads.push(aDownload);
+ if (this._list) {
+ this._onListChanged();
+ }
+ },
+
+ onDownloadChanged: function (aDownload)
+ {
+ this._onListChanged();
+ },
+
+ onDownloadRemoved: function (aDownload)
+ {
+ let index = this._downloads.indexOf(aDownload);
+ if (index != -1) {
+ this._downloads.splice(index, 1);
+ }
+ this._onListChanged();
+ },
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadPlatform.cpp b/toolkit/components/jsdownloads/src/DownloadPlatform.cpp
new file mode 100644
index 000000000..1506b7c30
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadPlatform.cpp
@@ -0,0 +1,275 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DownloadPlatform.h"
+#include "nsAutoPtr.h"
+#include "nsNetUtil.h"
+#include "nsString.h"
+#include "nsINestedURI.h"
+#include "nsIProtocolHandler.h"
+#include "nsIURI.h"
+#include "nsIFile.h"
+#include "nsIObserverService.h"
+#include "nsISupportsPrimitives.h"
+#include "nsDirectoryServiceDefs.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+
+#define PREF_BDM_ADDTORECENTDOCS "browser.download.manager.addToRecentDocs"
+
+#ifdef XP_WIN
+#include <shlobj.h>
+#include <urlmon.h>
+#include "nsILocalFileWin.h"
+#endif
+
+#ifdef XP_MACOSX
+#include <CoreFoundation/CoreFoundation.h>
+#include "../../../../xpcom/io/CocoaFileUtils.h"
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+#include "FennecJNIWrappers.h"
+#endif
+
+#ifdef MOZ_WIDGET_GTK
+#include <gtk/gtk.h>
+#endif
+
+using namespace mozilla;
+
+DownloadPlatform *DownloadPlatform::gDownloadPlatformService = nullptr;
+
+NS_IMPL_ISUPPORTS(DownloadPlatform, mozIDownloadPlatform);
+
+DownloadPlatform* DownloadPlatform::GetDownloadPlatform()
+{
+ if (!gDownloadPlatformService) {
+ gDownloadPlatformService = new DownloadPlatform();
+ }
+
+ NS_ADDREF(gDownloadPlatformService);
+
+#if defined(MOZ_WIDGET_GTK)
+ g_type_init();
+#endif
+
+ return gDownloadPlatformService;
+}
+
+#ifdef MOZ_ENABLE_GIO
+static void gio_set_metadata_done(GObject *source_obj, GAsyncResult *res, gpointer user_data)
+{
+ GError *err = nullptr;
+ g_file_set_attributes_finish(G_FILE(source_obj), res, nullptr, &err);
+ if (err) {
+#ifdef DEBUG
+ NS_DebugBreak(NS_DEBUG_WARNING, "Set file metadata failed: ", err->message, __FILE__, __LINE__);
+#endif
+ g_error_free(err);
+ }
+}
+#endif
+
+#ifdef XP_MACOSX
+// Caller is responsible for freeing any result (CF Create Rule)
+CFURLRef CreateCFURLFromNSIURI(nsIURI *aURI) {
+ nsAutoCString spec;
+ if (aURI) {
+ aURI->GetSpec(spec);
+ }
+
+ CFStringRef urlStr = ::CFStringCreateWithCString(kCFAllocatorDefault,
+ spec.get(),
+ kCFStringEncodingUTF8);
+ if (!urlStr) {
+ return NULL;
+ }
+
+ CFURLRef url = ::CFURLCreateWithString(kCFAllocatorDefault,
+ urlStr,
+ NULL);
+
+ ::CFRelease(urlStr);
+
+ return url;
+}
+#endif
+
+nsresult DownloadPlatform::DownloadDone(nsIURI* aSource, nsIURI* aReferrer, nsIFile* aTarget,
+ const nsACString& aContentType, bool aIsPrivate)
+{
+#if defined(XP_WIN) || defined(XP_MACOSX) || defined(MOZ_WIDGET_ANDROID) \
+ || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_GONK)
+
+ nsAutoString path;
+ if (aTarget && NS_SUCCEEDED(aTarget->GetPath(path))) {
+#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_ANDROID)
+ // On Windows and Gtk, add the download to the system's "recent documents"
+ // list, with a pref to disable.
+ {
+ bool addToRecentDocs = Preferences::GetBool(PREF_BDM_ADDTORECENTDOCS);
+#ifdef MOZ_WIDGET_ANDROID
+ if (jni::IsFennec() && addToRecentDocs) {
+ java::DownloadsIntegration::ScanMedia(path, aContentType);
+ }
+#else
+ if (addToRecentDocs && !aIsPrivate) {
+#ifdef XP_WIN
+ ::SHAddToRecentDocs(SHARD_PATHW, path.get());
+#elif defined(MOZ_WIDGET_GTK)
+ GtkRecentManager* manager = gtk_recent_manager_get_default();
+
+ gchar* uri = g_filename_to_uri(NS_ConvertUTF16toUTF8(path).get(),
+ nullptr, nullptr);
+ if (uri) {
+ gtk_recent_manager_add_item(manager, uri);
+ g_free(uri);
+ }
+#endif
+ }
+#endif
+#ifdef MOZ_ENABLE_GIO
+ // Use GIO to store the source URI for later display in the file manager.
+ GFile* gio_file = g_file_new_for_path(NS_ConvertUTF16toUTF8(path).get());
+ nsCString source_uri;
+ nsresult rv = aSource->GetSpec(source_uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ GFileInfo *file_info = g_file_info_new();
+ g_file_info_set_attribute_string(file_info, "metadata::download-uri", source_uri.get());
+ g_file_set_attributes_async(gio_file,
+ file_info,
+ G_FILE_QUERY_INFO_NONE,
+ G_PRIORITY_DEFAULT,
+ nullptr, gio_set_metadata_done, nullptr);
+ g_object_unref(file_info);
+ g_object_unref(gio_file);
+#endif
+ }
+#endif
+
+#ifdef XP_MACOSX
+ // On OS X, make the downloads stack bounce.
+ CFStringRef observedObject = ::CFStringCreateWithCString(kCFAllocatorDefault,
+ NS_ConvertUTF16toUTF8(path).get(),
+ kCFStringEncodingUTF8);
+ CFNotificationCenterRef center = ::CFNotificationCenterGetDistributedCenter();
+ ::CFNotificationCenterPostNotification(center, CFSTR("com.apple.DownloadFileFinished"),
+ observedObject, nullptr, TRUE);
+ ::CFRelease(observedObject);
+
+ // Add OS X origin and referrer file metadata
+ CFStringRef pathCFStr = NULL;
+ if (!path.IsEmpty()) {
+ pathCFStr = ::CFStringCreateWithCharacters(kCFAllocatorDefault,
+ (const UniChar*)path.get(),
+ path.Length());
+ }
+ if (pathCFStr) {
+ bool isFromWeb = IsURLPossiblyFromWeb(aSource);
+
+ CFURLRef sourceCFURL = CreateCFURLFromNSIURI(aSource);
+ CFURLRef referrerCFURL = CreateCFURLFromNSIURI(aReferrer);
+
+ CocoaFileUtils::AddOriginMetadataToFile(pathCFStr,
+ sourceCFURL,
+ referrerCFURL);
+ CocoaFileUtils::AddQuarantineMetadataToFile(pathCFStr,
+ sourceCFURL,
+ referrerCFURL,
+ isFromWeb);
+
+ ::CFRelease(pathCFStr);
+ if (sourceCFURL) {
+ ::CFRelease(sourceCFURL);
+ }
+ if (referrerCFURL) {
+ ::CFRelease(referrerCFURL);
+ }
+ }
+#endif
+ }
+
+#endif
+
+ return NS_OK;
+}
+
+nsresult DownloadPlatform::MapUrlToZone(const nsAString& aURL,
+ uint32_t* aZone)
+{
+#ifdef XP_WIN
+ RefPtr<IInternetSecurityManager> inetSecMgr;
+ if (FAILED(CoCreateInstance(CLSID_InternetSecurityManager, NULL,
+ CLSCTX_ALL, IID_IInternetSecurityManager,
+ getter_AddRefs(inetSecMgr)))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ DWORD zone;
+ if (inetSecMgr->MapUrlToZone(PromiseFlatString(aURL).get(),
+ &zone, 0) != S_OK) {
+ return NS_ERROR_UNEXPECTED;
+ } else {
+ *aZone = zone;
+ }
+
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+// Check if a URI is likely to be web-based, by checking its URI flags.
+// If in doubt (e.g. if anything fails during the check) claims things
+// are from the web.
+bool DownloadPlatform::IsURLPossiblyFromWeb(nsIURI* aURI)
+{
+ nsCOMPtr<nsIIOService> ios = do_GetIOService();
+ nsCOMPtr<nsIURI> uri = aURI;
+ if (!ios) {
+ return true;
+ }
+
+ while (uri) {
+ // We're not using nsIIOService::ProtocolHasFlags because it doesn't
+ // take per-URI flags into account. We're also not using
+ // NS_URIChainHasFlags because we're checking for *any* of 3 flags
+ // to be present on *all* of the nested URIs, which it can't do.
+ nsAutoCString scheme;
+ nsresult rv = uri->GetScheme(scheme);
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ nsCOMPtr<nsIProtocolHandler> ph;
+ rv = ios->GetProtocolHandler(scheme.get(), getter_AddRefs(ph));
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ uint32_t flags;
+ rv = ph->DoGetProtocolFlags(uri, &flags);
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ // If not dangerous to load, not a UI resource and not a local file,
+ // assume this is from the web:
+ if (!(flags & nsIProtocolHandler::URI_DANGEROUS_TO_LOAD) &&
+ !(flags & nsIProtocolHandler::URI_IS_UI_RESOURCE) &&
+ !(flags & nsIProtocolHandler::URI_IS_LOCAL_FILE)) {
+ return true;
+ }
+ // Otherwise, check if the URI is nested, and if so go through
+ // the loop again:
+ nsCOMPtr<nsINestedURI> nestedURI = do_QueryInterface(uri);
+ uri = nullptr;
+ if (nestedURI) {
+ rv = nestedURI->GetInnerURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
diff --git a/toolkit/components/jsdownloads/src/DownloadPlatform.h b/toolkit/components/jsdownloads/src/DownloadPlatform.h
new file mode 100644
index 000000000..ef3c7554f
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadPlatform.h
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __DownloadPlatform_h__
+#define __DownloadPlatform_h__
+
+#include "mozIDownloadPlatform.h"
+
+#include "nsCOMPtr.h"
+class nsIURI;
+
+class DownloadPlatform : public mozIDownloadPlatform
+{
+protected:
+
+ virtual ~DownloadPlatform() { }
+
+public:
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_MOZIDOWNLOADPLATFORM
+
+ DownloadPlatform() { }
+
+ static DownloadPlatform *gDownloadPlatformService;
+
+ static DownloadPlatform* GetDownloadPlatform();
+
+private:
+ static bool IsURLPossiblyFromWeb(nsIURI* aURI);
+};
+
+#endif
diff --git a/toolkit/components/jsdownloads/src/DownloadStore.jsm b/toolkit/components/jsdownloads/src/DownloadStore.jsm
new file mode 100644
index 000000000..765a45c5a
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadStore.jsm
@@ -0,0 +1,203 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+/**
+ * Handles serialization of Download objects and persistence into a file, so
+ * that the state of downloads can be restored across sessions.
+ *
+ * The file is stored in JSON format, without indentation. With indentation
+ * applied, the file would look like this:
+ *
+ * {
+ * "list": [
+ * {
+ * "source": "http://www.example.com/download.txt",
+ * "target": "/home/user/Downloads/download.txt"
+ * },
+ * {
+ * "source": {
+ * "url": "http://www.example.com/download.txt",
+ * "referrer": "http://www.example.com/referrer.html"
+ * },
+ * "target": "/home/user/Downloads/download-2.txt"
+ * }
+ * ]
+ * }
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadStore",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
+ "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm")
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gTextDecoder", function () {
+ return new TextDecoder();
+});
+
+XPCOMUtils.defineLazyGetter(this, "gTextEncoder", function () {
+ return new TextEncoder();
+});
+
+// DownloadStore
+
+/**
+ * Handles serialization of Download objects and persistence into a file, so
+ * that the state of downloads can be restored across sessions.
+ *
+ * @param aList
+ * DownloadList object to be populated or serialized.
+ * @param aPath
+ * String containing the file path where data should be saved.
+ */
+this.DownloadStore = function (aList, aPath)
+{
+ this.list = aList;
+ this.path = aPath;
+}
+
+this.DownloadStore.prototype = {
+ /**
+ * DownloadList object to be populated or serialized.
+ */
+ list: null,
+
+ /**
+ * String containing the file path where data should be saved.
+ */
+ path: "",
+
+ /**
+ * This function is called with a Download object as its first argument, and
+ * should return true if the item should be saved.
+ */
+ onsaveitem: () => true,
+
+ /**
+ * Loads persistent downloads from the file to the list.
+ *
+ * @return {Promise}
+ * @resolves When the operation finished successfully.
+ * @rejects JavaScript exception.
+ */
+ load: function DS_load()
+ {
+ return Task.spawn(function* task_DS_load() {
+ let bytes;
+ try {
+ bytes = yield OS.File.read(this.path);
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) || !ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ // If the file does not exist, there are no downloads to load.
+ return;
+ }
+
+ let storeData = JSON.parse(gTextDecoder.decode(bytes));
+
+ // Create live downloads based on the static snapshot.
+ for (let downloadData of storeData.list) {
+ try {
+ let download = yield Downloads.createDownload(downloadData);
+ try {
+ if (!download.succeeded && !download.canceled && !download.error) {
+ // Try to restart the download if it was in progress during the
+ // previous session. Ignore errors.
+ download.start().catch(() => {});
+ } else {
+ // If the download was not in progress, try to update the current
+ // progress from disk. This is relevant in case we retained
+ // partially downloaded data.
+ yield download.refresh();
+ }
+ } finally {
+ // Add the download to the list if we succeeded in creating it,
+ // after we have updated its initial state.
+ yield this.list.add(download);
+ }
+ } catch (ex) {
+ // If an item is unrecognized, don't prevent others from being loaded.
+ Cu.reportError(ex);
+ }
+ }
+ }.bind(this));
+ },
+
+ /**
+ * Saves persistent downloads from the list to the file.
+ *
+ * If an error occurs, the previous file is not deleted.
+ *
+ * @return {Promise}
+ * @resolves When the operation finished successfully.
+ * @rejects JavaScript exception.
+ */
+ save: function DS_save()
+ {
+ return Task.spawn(function* task_DS_save() {
+ let downloads = yield this.list.getAll();
+
+ // Take a static snapshot of the current state of all the downloads.
+ let storeData = { list: [] };
+ let atLeastOneDownload = false;
+ for (let download of downloads) {
+ try {
+ if (!this.onsaveitem(download)) {
+ continue;
+ }
+
+ let serializable = download.toSerializable();
+ if (!serializable) {
+ // This item cannot be persisted across sessions.
+ continue;
+ }
+ storeData.list.push(serializable);
+ atLeastOneDownload = true;
+ } catch (ex) {
+ // If an item cannot be converted to a serializable form, don't
+ // prevent others from being saved.
+ Cu.reportError(ex);
+ }
+ }
+
+ if (atLeastOneDownload) {
+ // Create or overwrite the file if there are downloads to save.
+ let bytes = gTextEncoder.encode(JSON.stringify(storeData));
+ yield OS.File.writeAtomic(this.path, bytes,
+ { tmpPath: this.path + ".tmp" });
+ } else {
+ // Remove the file if there are no downloads to save at all.
+ try {
+ yield OS.File.remove(this.path);
+ } catch (ex) {
+ if (!(ex instanceof OS.File.Error) ||
+ !(ex.becauseNoSuchFile || ex.becauseAccessDenied)) {
+ throw ex;
+ }
+ // On Windows, we may get an access denied error instead of a no such
+ // file error if the file existed before, and was recently deleted.
+ }
+ }
+ }.bind(this));
+ },
+};
diff --git a/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
new file mode 100644
index 000000000..f5102b4a8
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/DownloadUIHelper.jsm
@@ -0,0 +1,243 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+/**
+ * Provides functions to handle status and messages in the user interface.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "DownloadUIHelper",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+const kStringBundleUrl =
+ "chrome://mozapps/locale/downloads/downloads.properties";
+
+const kStringsRequiringFormatting = {
+ fileExecutableSecurityWarning: true,
+ cancelDownloadsOKTextMultiple: true,
+ quitCancelDownloadsAlertMsgMultiple: true,
+ quitCancelDownloadsAlertMsgMacMultiple: true,
+ offlineCancelDownloadsAlertMsgMultiple: true,
+ leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple2: true
+};
+
+// DownloadUIHelper
+
+/**
+ * Provides functions to handle status and messages in the user interface.
+ */
+this.DownloadUIHelper = {
+ /**
+ * Returns an object that can be used to display prompts related to downloads.
+ *
+ * The prompts may be either anchored to a specified window, or anchored to
+ * the most recently active window, for example if the prompt is displayed in
+ * response to global notifications that are not associated with any window.
+ *
+ * @param aParent
+ * If specified, should reference the nsIDOMWindow to which the prompts
+ * should be attached. If omitted, the prompts will be attached to the
+ * most recently active window.
+ *
+ * @return A DownloadPrompter object.
+ */
+ getPrompter: function (aParent)
+ {
+ return new DownloadPrompter(aParent || null);
+ },
+};
+
+/**
+ * Returns an object whose keys are the string names from the downloads string
+ * bundle, and whose values are either the translated strings or functions
+ * returning formatted strings.
+ */
+XPCOMUtils.defineLazyGetter(DownloadUIHelper, "strings", function () {
+ let strings = {};
+ let sb = Services.strings.createBundle(kStringBundleUrl);
+ let enumerator = sb.getSimpleEnumeration();
+ while (enumerator.hasMoreElements()) {
+ let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+ let stringName = string.key;
+ if (stringName in kStringsRequiringFormatting) {
+ strings[stringName] = function () {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ return sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ };
+ } else {
+ strings[stringName] = string.value;
+ }
+ }
+ return strings;
+});
+
+// DownloadPrompter
+
+/**
+ * Allows displaying prompts related to downloads.
+ *
+ * @param aParent
+ * The nsIDOMWindow to which prompts should be attached, or null to
+ * attach prompts to the most recently active window.
+ */
+this.DownloadPrompter = function (aParent)
+{
+ if (AppConstants.MOZ_B2G) {
+ // On B2G there is no prompter implementation.
+ this._prompter = null;
+ } else {
+ this._prompter = Services.ww.getNewPrompter(aParent);
+ }
+}
+
+this.DownloadPrompter.prototype = {
+ /**
+ * Constants with the different type of prompts.
+ */
+ ON_QUIT: "prompt-on-quit",
+ ON_OFFLINE: "prompt-on-offline",
+ ON_LEAVE_PRIVATE_BROWSING: "prompt-on-leave-private-browsing",
+
+ /**
+ * nsIPrompt instance for displaying messages.
+ */
+ _prompter: null,
+
+ /**
+ * Displays a warning message box that informs that the specified file is
+ * executable, and asks whether the user wants to launch it. The user is
+ * given the option of disabling future instances of this warning.
+ *
+ * @param aPath
+ * String containing the full path to the file to be opened.
+ *
+ * @return {Promise}
+ * @resolves Boolean indicating whether the launch operation can continue.
+ * @rejects JavaScript exception.
+ */
+ confirmLaunchExecutable: function (aPath)
+ {
+ const kPrefAlertOnEXEOpen = "browser.download.manager.alertOnEXEOpen";
+
+ try {
+ // Always launch in case we have no prompter implementation.
+ if (!this._prompter) {
+ return Promise.resolve(true);
+ }
+
+ try {
+ if (!Services.prefs.getBoolPref(kPrefAlertOnEXEOpen)) {
+ return Promise.resolve(true);
+ }
+ } catch (ex) {
+ // If the preference does not exist, continue with the prompt.
+ }
+
+ let leafName = OS.Path.basename(aPath);
+
+ let s = DownloadUIHelper.strings;
+ let checkState = { value: false };
+ let shouldLaunch = this._prompter.confirmCheck(
+ s.fileExecutableSecurityWarningTitle,
+ s.fileExecutableSecurityWarning(leafName, leafName),
+ s.fileExecutableSecurityWarningDontAsk,
+ checkState);
+
+ if (shouldLaunch) {
+ Services.prefs.setBoolPref(kPrefAlertOnEXEOpen, !checkState.value);
+ }
+
+ return Promise.resolve(shouldLaunch);
+ } catch (ex) {
+ return Promise.reject(ex);
+ }
+ },
+
+ /**
+ * Displays a warning message box that informs that there are active
+ * downloads, and asks whether the user wants to cancel them or not.
+ *
+ * @param aDownloadsCount
+ * The current downloads count.
+ * @param aPromptType
+ * The type of prompt notification depending on the observer.
+ *
+ * @return False to cancel the downloads and continue, true to abort the
+ * operation.
+ */
+ confirmCancelDownloads: function DP_confirmCancelDownload(aDownloadsCount,
+ aPromptType)
+ {
+ // Always continue in case we have no prompter implementation, or if there
+ // are no active downloads.
+ if (!this._prompter || aDownloadsCount <= 0) {
+ return false;
+ }
+
+ let s = DownloadUIHelper.strings;
+ let buttonFlags = (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1);
+ let okButton = aDownloadsCount > 1 ? s.cancelDownloadsOKTextMultiple(aDownloadsCount)
+ : s.cancelDownloadsOKText;
+ let title, message, cancelButton;
+
+ switch (aPromptType) {
+ case this.ON_QUIT:
+ title = s.quitCancelDownloadsAlertTitle;
+ if (AppConstants.platform != "macosx") {
+ message = aDownloadsCount > 1
+ ? s.quitCancelDownloadsAlertMsgMultiple(aDownloadsCount)
+ : s.quitCancelDownloadsAlertMsg;
+ cancelButton = s.dontQuitButtonWin;
+ } else {
+ message = aDownloadsCount > 1
+ ? s.quitCancelDownloadsAlertMsgMacMultiple(aDownloadsCount)
+ : s.quitCancelDownloadsAlertMsgMac;
+ cancelButton = s.dontQuitButtonMac;
+ }
+ break;
+ case this.ON_OFFLINE:
+ title = s.offlineCancelDownloadsAlertTitle;
+ message = aDownloadsCount > 1
+ ? s.offlineCancelDownloadsAlertMsgMultiple(aDownloadsCount)
+ : s.offlineCancelDownloadsAlertMsg;
+ cancelButton = s.dontGoOfflineButton;
+ break;
+ case this.ON_LEAVE_PRIVATE_BROWSING:
+ title = s.leavePrivateBrowsingCancelDownloadsAlertTitle;
+ message = aDownloadsCount > 1
+ ? s.leavePrivateBrowsingWindowsCancelDownloadsAlertMsgMultiple2(aDownloadsCount)
+ : s.leavePrivateBrowsingWindowsCancelDownloadsAlertMsg2;
+ cancelButton = s.dontLeavePrivateBrowsingButton2;
+ break;
+ }
+
+ let rv = this._prompter.confirmEx(title, message, buttonFlags, okButton,
+ cancelButton, null, null, {});
+ return (rv == 1);
+ }
+};
diff --git a/toolkit/components/jsdownloads/src/Downloads.jsm b/toolkit/components/jsdownloads/src/Downloads.jsm
new file mode 100644
index 000000000..9511dc4ca
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/Downloads.jsm
@@ -0,0 +1,305 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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/. */
+
+/**
+ * Main entry point to get references to all the back-end objects.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "Downloads",
+];
+
+// Globals
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/Integration.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/DownloadCore.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadCombinedList",
+ "resource://gre/modules/DownloadList.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadList",
+ "resource://gre/modules/DownloadList.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadSummary",
+ "resource://gre/modules/DownloadList.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
+ "resource://gre/modules/DownloadUIHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+Integration.downloads.defineModuleGetter(this, "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm");
+
+// Downloads
+
+/**
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides the only entry point to get references to back-end objects.
+ */
+this.Downloads = {
+ /**
+ * Work on downloads that were not started from a private browsing window.
+ */
+ get PUBLIC() {
+ return "{Downloads.PUBLIC}";
+ },
+ /**
+ * Work on downloads that were started from a private browsing window.
+ */
+ get PRIVATE() {
+ return "{Downloads.PRIVATE}";
+ },
+ /**
+ * Work on both Downloads.PRIVATE and Downloads.PUBLIC downloads.
+ */
+ get ALL() {
+ return "{Downloads.ALL}";
+ },
+
+ /**
+ * Creates a new Download object.
+ *
+ * @param aProperties
+ * Provides the initial properties for the newly created download.
+ * This matches the serializable representation of a Download object.
+ * Some of the most common properties in this object include:
+ * {
+ * source: String containing the URI for the download source.
+ * Alternatively, may be an nsIURI, a DownloadSource object,
+ * or an object with the following properties:
+ * {
+ * url: String containing the URI for the download source.
+ * isPrivate: Indicates whether the download originated from a
+ * private window. If omitted, the download is public.
+ * referrer: String containing the referrer URI of the download
+ * source. Can be omitted or null if no referrer should
+ * be sent or the download source is not HTTP.
+ * },
+ * target: String containing the path of the target file.
+ * Alternatively, may be an nsIFile, a DownloadTarget object,
+ * or an object with the following properties:
+ * {
+ * path: String containing the path of the target file.
+ * },
+ * saver: String representing the class of the download operation.
+ * If omitted, defaults to "copy". Alternatively, may be the
+ * serializable representation of a DownloadSaver object.
+ * }
+ *
+ * @return {Promise}
+ * @resolves The newly created Download object.
+ * @rejects JavaScript exception.
+ */
+ createDownload: function D_createDownload(aProperties)
+ {
+ try {
+ return Promise.resolve(Download.fromSerializable(aProperties));
+ } catch (ex) {
+ return Promise.reject(ex);
+ }
+ },
+
+ /**
+ * Downloads data from a remote network location to a local file.
+ *
+ * This download method does not provide user interface, or the ability to
+ * cancel or restart the download programmatically. For that, you should
+ * obtain a reference to a Download object using the createDownload function.
+ *
+ * Since the download cannot be restarted, any partially downloaded data will
+ * not be kept in case the download fails.
+ *
+ * @param aSource
+ * String containing the URI for the download source. Alternatively,
+ * may be an nsIURI or a DownloadSource object.
+ * @param aTarget
+ * String containing the path of the target file. Alternatively, may
+ * be an nsIFile or a DownloadTarget object.
+ * @param aOptions
+ * An optional object used to control the behavior of this function.
+ * You may pass an object with a subset of the following fields:
+ * {
+ * isPrivate: Indicates whether the download originated from a
+ * private window.
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+ fetch: function (aSource, aTarget, aOptions) {
+ return this.createDownload({
+ source: aSource,
+ target: aTarget,
+ }).then(function D_SD_onSuccess(aDownload) {
+ if (aOptions && ("isPrivate" in aOptions)) {
+ aDownload.source.isPrivate = aOptions.isPrivate;
+ }
+ return aDownload.start();
+ });
+ },
+
+ /**
+ * Retrieves the specified type of DownloadList object. There is one download
+ * list for each type, and this method always retrieves a reference to the
+ * same download list when called with the same argument.
+ *
+ * Calling this function may cause the list of public downloads to be reloaded
+ * from the previous session, if it wasn't loaded already.
+ *
+ * @param aType
+ * This can be Downloads.PUBLIC, Downloads.PRIVATE, or Downloads.ALL.
+ * Downloads added to the Downloads.PUBLIC and Downloads.PRIVATE lists
+ * are reflected in the Downloads.ALL list, and downloads added to the
+ * Downloads.ALL list are also added to either the Downloads.PUBLIC or
+ * the Downloads.PRIVATE list based on their properties.
+ *
+ * @return {Promise}
+ * @resolves The requested DownloadList or DownloadCombinedList object.
+ * @rejects JavaScript exception.
+ */
+ getList: function (aType)
+ {
+ if (!this._promiseListsInitialized) {
+ this._promiseListsInitialized = Task.spawn(function* () {
+ let publicList = new DownloadList();
+ let privateList = new DownloadList();
+ let combinedList = new DownloadCombinedList(publicList, privateList);
+
+ try {
+ yield DownloadIntegration.addListObservers(publicList, false);
+ yield DownloadIntegration.addListObservers(privateList, true);
+ yield DownloadIntegration.initializePublicDownloadList(publicList);
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+
+ let publicSummary = yield this.getSummary(Downloads.PUBLIC);
+ let privateSummary = yield this.getSummary(Downloads.PRIVATE);
+ let combinedSummary = yield this.getSummary(Downloads.ALL);
+
+ yield publicSummary.bindToList(publicList);
+ yield privateSummary.bindToList(privateList);
+ yield combinedSummary.bindToList(combinedList);
+
+ this._lists[Downloads.PUBLIC] = publicList;
+ this._lists[Downloads.PRIVATE] = privateList;
+ this._lists[Downloads.ALL] = combinedList;
+ }.bind(this));
+ }
+
+ return this._promiseListsInitialized.then(() => this._lists[aType]);
+ },
+
+ /**
+ * Promise resolved when the initialization of the download lists has
+ * completed, or null if initialization has never been requested.
+ */
+ _promiseListsInitialized: null,
+
+ /**
+ * After initialization, this object is populated with one key for each type
+ * of download list that can be returned (Downloads.PUBLIC, Downloads.PRIVATE,
+ * or Downloads.ALL). The values are the DownloadList objects.
+ */
+ _lists: {},
+
+ /**
+ * Retrieves the specified type of DownloadSummary object. There is one
+ * download summary for each type, and this method always retrieves a
+ * reference to the same download summary when called with the same argument.
+ *
+ * Calling this function does not cause the list of public downloads to be
+ * reloaded from the previous session. The summary will behave as if no
+ * downloads are present until the getList method is called.
+ *
+ * @param aType
+ * This can be Downloads.PUBLIC, Downloads.PRIVATE, or Downloads.ALL.
+ *
+ * @return {Promise}
+ * @resolves The requested DownloadList or DownloadCombinedList object.
+ * @rejects JavaScript exception.
+ */
+ getSummary: function (aType)
+ {
+ if (aType != Downloads.PUBLIC && aType != Downloads.PRIVATE &&
+ aType != Downloads.ALL) {
+ throw new Error("Invalid aType argument.");
+ }
+
+ if (!(aType in this._summaries)) {
+ this._summaries[aType] = new DownloadSummary();
+ }
+
+ return Promise.resolve(this._summaries[aType]);
+ },
+
+ /**
+ * This object is populated by the getSummary method with one key for each
+ * type of object that can be returned (Downloads.PUBLIC, Downloads.PRIVATE,
+ * or Downloads.ALL). The values are the DownloadSummary objects.
+ */
+ _summaries: {},
+
+ /**
+ * Returns the system downloads directory asynchronously.
+ * Mac OSX:
+ * User downloads directory
+ * XP/2K:
+ * My Documents/Downloads
+ * Vista and others:
+ * User downloads directory
+ * Linux:
+ * XDG user dir spec, with a fallback to Home/Downloads
+ * Android:
+ * standard downloads directory i.e. /sdcard
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getSystemDownloadsDirectory: function D_getSystemDownloadsDirectory() {
+ return DownloadIntegration.getSystemDownloadsDirectory();
+ },
+
+ /**
+ * Returns the preferred downloads directory based on the user preferences
+ * in the current profile asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getPreferredDownloadsDirectory: function D_getPreferredDownloadsDirectory() {
+ return DownloadIntegration.getPreferredDownloadsDirectory();
+ },
+
+ /**
+ * Returns the temporary directory where downloads are placed before the
+ * final location is chosen, or while the document is opened temporarily
+ * with an external application. This may or may not be the system temporary
+ * directory, based on the platform asynchronously.
+ *
+ * @return {Promise}
+ * @resolves The downloads directory string path.
+ */
+ getTemporaryDownloadsDirectory: function D_getTemporaryDownloadsDirectory() {
+ return DownloadIntegration.getTemporaryDownloadsDirectory();
+ },
+
+ /**
+ * Constructor for a DownloadError object. When you catch an exception during
+ * a download, you can use this to verify if "ex instanceof Downloads.Error",
+ * before reading the exception properties with the error details.
+ */
+ Error: DownloadError,
+};
diff --git a/toolkit/components/jsdownloads/src/Downloads.manifest b/toolkit/components/jsdownloads/src/Downloads.manifest
new file mode 100644
index 000000000..03d4ed4a6
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/Downloads.manifest
@@ -0,0 +1,2 @@
+component {1b4c85df-cbdd-4bb6-b04e-613caece083c} DownloadLegacy.js
+contract @mozilla.org/transfer;1 {1b4c85df-cbdd-4bb6-b04e-613caece083c}
diff --git a/toolkit/components/jsdownloads/src/moz.build b/toolkit/components/jsdownloads/src/moz.build
new file mode 100644
index 000000000..87abed62e
--- /dev/null
+++ b/toolkit/components/jsdownloads/src/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SOURCES += [
+ 'DownloadPlatform.cpp',
+]
+
+EXTRA_COMPONENTS += [
+ 'DownloadLegacy.js',
+ 'Downloads.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'DownloadCore.jsm',
+ 'DownloadImport.jsm',
+ 'DownloadList.jsm',
+ 'Downloads.jsm',
+ 'DownloadStore.jsm',
+ 'DownloadUIHelper.jsm',
+]
+
+EXTRA_PP_JS_MODULES += [
+ 'DownloadIntegration.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
+
+CXXFLAGS += CONFIG['TK_CFLAGS']