/* 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 implementation of the Downloads API objects. Consumers should get * references to these objects through the "Downloads.sys.mjs" module. */ import { Integration } from "resource://gre/modules/Integration.sys.mjs"; import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { DownloadHistory: "resource://gre/modules/DownloadHistory.sys.mjs", DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs", E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", FileUtils: "resource://gre/modules/FileUtils.sys.mjs", PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", }); XPCOMUtils.defineLazyModuleGetters(lazy, { NetUtil: "resource://gre/modules/NetUtil.jsm", }); XPCOMUtils.defineLazyServiceGetter( lazy, "gExternalAppLauncher", "@mozilla.org/uriloader/external-helper-app-service;1", Ci.nsPIExternalAppLauncher ); XPCOMUtils.defineLazyServiceGetter( lazy, "gExternalHelperAppService", "@mozilla.org/uriloader/external-helper-app-service;1", Ci.nsIExternalHelperAppService ); Integration.downloads.defineESModuleGetter( lazy, "DownloadIntegration", "resource://gre/modules/DownloadIntegration.sys.mjs" ); 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]; } } } /** * Check if the file is a placeholder. * * @return {Promise} * @resolves {boolean} * @rejects Never. */ async function isPlaceholder(path) { try { if ((await IOUtils.stat(path)).size == 0) { return true; } } catch (ex) { // Canceling the download may have removed the placeholder already. if (ex.name != "NotFoundError") { console.error(ex); } } return false; } /** * 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; /** * 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. */ export var Download = function () { this._deferSucceeded = lazy.PromiseUtils.defer(); }; 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, /** * Downloaded files can be deleted from within Firefox, e.g. via the context * menu. Currently Firefox does not track file moves (see bug 1746386), so if * a download's target file stops existing we have to assume it's "moved or * missing." To distinguish files intentionally deleted within Firefox from * files that are moved/missing, we mark them as "deleted" with this property. */ deleted: 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, /** * When a download starts, we typically want to automatically open the * downloads panel if the pref browser.download.alwaysOpenPanel is enabled. * However, there are conditions where we want to prevent this. For example, a * false value can prevent the downloads panel from opening when an add-on * creates a download without user input as part of some background operation. */ openDownloadsListOnStart: true, /** * 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) { console.error(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, /** * The download was launched to open from the Downloads Panel. */ _launchedFromPanel: false, /** * 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; // Avoid serializing the previous error, or it would be restored on the next // startup, even if the download was restarted. delete this._unknownProperties?.errorObj; 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 = lazy.PromiseUtils.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( (async () => { // Wait upon any pending operation before restarting. if (this._promiseCanceled) { await this._promiseCanceled; } if (this._promiseRemovePartialData) { try { await 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 { if (this.downloadingToSameFile()) { throw new DownloadError({ message: "Can't overwrite the source file.", becauseTargetFailed: true, }); } // Disallow download if parental controls service restricts it. if ( await lazy.DownloadIntegration.shouldBlockForParentalControls(this) ) { throw new DownloadError({ becauseBlockedByParentalControls: 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 new Error(undefined); } // Execute the actual download through the saver object. this._saverExecuting = true; try { await this.saver.execute( DS_setProgressBytes.bind(this), DS_setProperties.bind(this) ); } catch (ex) { // Remove the target file placeholder and all partial data when // needed, independently of which code path failed. In some cases, the // component executing the download may have already removed the file. if (!this.hasPartialData && !this.hasBlockedData) { await this.saver.removeData(true); } throw ex; } // Now that the actual saving finished, read the actual file size on // disk, that may be different from the amount of data transferred. await 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) { // To keep the internal state of the Download object consistent, we // just delete the target and effectively cancel the download. Since // the DownloadSaver succeeded, we already renamed the ".part" file to // the final name, and this results in all the data being deleted. await this.saver.removeData(true); // 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); } // Don't store an error if it's an abort caused by shutdown, so the // download can be retried automatically at the next startup. if ( originalEx.result != Cr.NS_ERROR_ABORT || !Services.startup.isInOrBeyondShutdownPhase( Ci.nsIAppStartup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED ) ) { 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) { await this._succeed(); } } } })() ); // 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. */ async _succeed() { await lazy.DownloadIntegration.downloadDone(this); this._deferSucceeded.resolve(); if (this.launchWhenSucceeded) { this.launch().catch(console.error); // 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) { lazy.gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible( new lazy.FileUtils.File(this.target.path) ); } else if ( Services.prefs.getBoolPref("browser.helperApps.deleteTempFileOnExit") && Services.prefs.getBoolPref( "browser.download.start_downloads_in_tmp_dir", false ) ) { lazy.gExternalAppLauncher.deleteTemporaryFileOnExit( new lazy.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() { if (this._promiseUnblock) { return this._promiseUnblock; } if (this._promiseConfirmBlock) { return Promise.reject( new Error("Download block has been confirmed, cannot unblock.") ); } if (this.error?.becauseBlockedByReputationCheck) { Services.telemetry .getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD") .add(this.error.reputationCheckVerdict, 2); // unblock } if ( this.error?.reputationCheckVerdict == DownloadError.BLOCK_VERDICT_INSECURE ) { // In this Error case, the download was actually canceled before it was // passed to the Download UI. So we need to start the download here. this.error = null; this.succeeded = false; this.hasBlockedData = false; // This ensures the verdict will not get set again after the browser // restarts and the download gets serialized and de-serialized again. delete this._unknownProperties?.errorObj; this.start() .catch(err => { if (err.becauseTargetFailed) { // In case we cannot write to the target file // retry with a new unique name let uniquePath = lazy.DownloadPaths.createNiceUniqueFile( new lazy.FileUtils.File(this.target.path) ).path; this.target.path = uniquePath; return this.start(); } return Promise.reject(err); }) .catch(err => { if (!this.canceled) { console.error(err); } this._notifyChange(); }); this._notifyChange(); this._promiseUnblock = lazy.DownloadIntegration.downloadDone(this); return this._promiseUnblock; } if (!this.hasBlockedData) { return Promise.reject( new Error("unblock may only be called on Downloads with blocked data.") ); } this._promiseUnblock = (async () => { try { await IOUtils.move(this.target.partFilePath, this.target.path); await this.target.refresh(); } catch (ex) { await this.refresh(); this._promiseUnblock = null; throw ex; } this.succeeded = true; this.hasBlockedData = false; this._notifyChange(); await this._succeed(); })(); 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() { if (this._promiseConfirmBlock) { return this._promiseConfirmBlock; } if (this._promiseUnblock) { return Promise.reject( new Error("Download is being unblocked, cannot confirmBlock.") ); } if (this.error?.becauseBlockedByReputationCheck) { // We have to record the telemetry in both DownloadsCommon.deleteDownload // and confirmBlock here. The former is for cases where users click // "Remove file" in the download panel and the latter is when // users click "X" button in about:downloads. Services.telemetry .getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD") .add(this.error.reputationCheckVerdict, 1); // confirm block } if (!this.hasBlockedData) { return Promise.reject( new Error( "confirmBlock may only be called on Downloads with blocked data." ) ); } this._promiseConfirmBlock = (async () => { // This call never throws exceptions. If the removal fails, the blocked // data remains stored on disk in the ".part" file. await this.saver.removeData(); this.hasBlockedData = false; this._notifyChange(); })(); 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. * * @param options.openWhere Optional string indicating how to open when handling * download by opening the target file URI. * One of "window", "tab", "tabshifted" * @param options.useSystemDefault * Optional value indicating how to handle launching this download, * this time only. Will override the associated mimeInfo.preferredAction * @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(options = {}) { if (!this.succeeded) { return Promise.reject( new Error("launch can only be called if the download succeeded") ); } if (this._launchedFromPanel) { Services.telemetry.scalarAdd("downloads.file_opened", 1); } return lazy.DownloadIntegration.launchDownload(this, options); }, /* * 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 lazy.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. this._promiseCanceled = new Promise(resolve => { this._currentAttempt.then(resolve, resolve); }); // 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() { if (!this.canceled && !this.error) { return Promise.resolve(); } if (!this._promiseRemovePartialData) { this._promiseRemovePartialData = (async () => { try { // Wait upon any pending cancellation request. if (this._promiseCanceled) { await this._promiseCanceled; } // Ask the saver object to remove any partial data. await this.saver.removeData(); // For completeness, clear the number of bytes transferred. if (this.currentBytes != 0 || this.hasPartialData) { this.currentBytes = 0; this.hasPartialData = false; this.target.refreshPartFileState(); this._notifyChange(); } } finally { this._promiseRemovePartialData = null; } })(); } return this._promiseRemovePartialData; }, /** * Returns true if the download source is the same as the target file. */ downloadingToSameFile() { if (!this.source.url || !this.source.url.startsWith("file:")) { return false; } try { let sourceUri = lazy.NetUtil.newURI(this.source.url); let targetUri = lazy.NetUtil.newURI( new lazy.FileUtils.File(this.target.path) ); return sourceUri.equals(targetUri); } catch (ex) { return false; } }, /** * 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() { return (async () => { if (!this.stopped || this._finalized) { return; } if (this.succeeded) { let oldExists = this.target.exists; let oldSize = this.target.size; await 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 = await IOUtils.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.name != "NotFoundError") { throw ex; } // Ignore the result if the state has changed meanwhile. if (!this.stopped || this._finalized) { return; } // In case we've blocked the Download becasue its // insecure, we should not set hasBlockedData to // false as its required to show the Unblock option. if ( this.error.reputationCheckVerdict == DownloadError.BLOCK_VERDICT_INSECURE ) { return; } this.hasBlockedData = false; this.hasPartialData = false; } this._notifyChange(); } })().catch(console.error); }, /** * True if the "finalize" method has been called. This prevents the download * from starting again after having been stopped. */ _finalized: false, /** * True if the "finalize" has been called and fully finished it's execution. */ _finalizeExecuted: 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(aRemovePartialData) { // Prevents the download from starting again after having been stopped. this._finalized = true; let promise; 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(); promise = this.removePartialData(); } else { // Just cancel the download, in case it is currently in progress. promise = this.cancel(); } promise.then(() => { // At this point, either removing data / just cancelling the download should be done. this._finalizeExecuted = true; }); return promise; }, /** * Deletes all file data associated with a download, preserving the download * object itself and updating it for download views. */ async manuallyRemoveData() { let { path } = this.target; if (this.succeeded) { // Temp files are made "read-only" by DownloadIntegration.downloadDone, so // reset the permission bits to read/write. This won't be necessary after // bug 1733587 since Downloads won't ever be temporary. await IOUtils.setPermissions(path, 0o660); await IOUtils.remove(path, { ignoreAbsent: true }); } this.deleted = true; await this.cancel(); await this.removePartialData(); // We need to guarantee that the UI is refreshed irrespective of what state // the download is in when this is called, to ensure the download doesn't // wind up stuck displaying as if it exists when it actually doesn't. And // that means updating this.target.partFileExists no matter what. await this.target.refreshPartFileState(); await this.refresh(); // The above methods will sometimes call _notifyChange, but not always. It // depends on whether the download is `succeeded`, `stopped`, `canceled`, // etc. Since this method needs to update the UI and can be invoked on any // download as long as its target has some file on the system, we need to // call _notifyChange no matter what state the download is in. this._notifyChange(); }, /** * 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 (this.hasProgress && this.target && !this.target.partFileExists) { this.target.refreshPartFileState(); } } if (changeMade) { this._notifyChange(); } }, /** * Returns a static representation of the current object state. * * @return A JavaScript object that can be serialized to JSON. */ toSerializable() { 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() { // 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", "handleInternally", "openDownloadsListOnStart", ]; /** * Creates a new Download object from a serializable representation. This * function is used by the createDownload method of Downloads.sys.mjs 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.includes(property) && property != "startTime" && property != "source" && property != "target" && property != "error" && property != "saver" ); return download; }; /** * Represents the source of a download, for example a document or an URI. */ export var DownloadSource = function () {}; DownloadSource.prototype = { /** * String containing the URI for the download source. */ url: null, /** * String containing the original URL for the download source. */ originalUrl: 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, /** * Represents the referrerInfo of the download source, could be null for * example if the download source is not HTTP. */ referrerInfo: 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, /** * For downloads handled by the (default) DownloadCopySaver, this function * will determine, if provided, if a download can progress or has to be * cancelled based on the HTTP status code of the network channel. * * @note If this is defined this object will not be serializable, thus the * Download object will not be persisted across sessions. * * @param aDownload * The download asking. * @param aStatus * The HTTP status in question * * @return {Boolean} Download can progress */ allowHttpStatus: null, /** * Represents the loadingPrincipal of the download source, * could be null, in which case the system principal is used instead. */ loadingPrincipal: null, /** * Represents the cookieJarSettings of the download source, could be null if * the download source is not from a document. */ cookieJarSettings: null, /** * Represents the authentication header of the download source, could be null if * the download source had no authentication header. */ authHeader: null, /** * Returns a static representation of the current object state. * * @return A JavaScript object that can be serialized to JSON. */ toSerializable() { if (this.adjustChannel) { // If the callback was used, we can't reproduce this across sessions. return null; } if (this.allowHttpStatus) { // If the callback was used, we can't reproduce this across sessions. return null; } let serializable = { url: this.url }; if (this.isPrivate) { serializable.isPrivate = true; } if (this.referrerInfo && isString(this.referrerInfo)) { serializable.referrerInfo = this.referrerInfo; } else if (this.referrerInfo) { serializable.referrerInfo = lazy.E10SUtils.serializeReferrerInfo( this.referrerInfo ); } if (this.loadingPrincipal) { serializable.loadingPrincipal = isString(this.loadingPrincipal) ? this.loadingPrincipal : lazy.E10SUtils.serializePrincipal(this.loadingPrincipal); } if (this.cookieJarSettings) { serializable.cookieJarSettings = isString(this.cookieJarSettings) ? this.cookieJarSettings : lazy.E10SUtils.serializeCookieJarSettings(this.cookieJarSettings); } serializeUnknownProperties(this, serializable); // Simplify the representation if we don't have other details. if (Object.keys(serializable).length === 1) { // serializable's only key is "url", just return the URL as a string. return this.url; } 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. * referrerInfo: represents the referrerInfo of the download source. * Can be omitted or null for example if the download * source is not HTTP. * cookieJarSettings: represents the cookieJarSettings of the download * source. Can be omitted or null if the download * source is not from a document. * 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. * allowHttpStatus: For downloads handled by the (default) * DownloadCopySaver, this function will determine, if * provided, if a download can progress or has to be * cancelled based on the HTTP status code of the * network channel. * } * * @return The newly created DownloadSource object. */ 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 { // Convert String objects to primitive strings at this point. source.url = aSerializable.url.toString(); for (let propName of ["isPrivate", "userContextId", "browsingContextId"]) { if (propName in aSerializable) { source[propName] = aSerializable[propName]; } } if ("originalUrl" in aSerializable) { source.originalUrl = aSerializable.originalUrl; } if ("referrerInfo" in aSerializable) { // Quick pass, pass directly nsIReferrerInfo, we don't need to serialize // and deserialize if (aSerializable.referrerInfo instanceof Ci.nsIReferrerInfo) { source.referrerInfo = aSerializable.referrerInfo; } else { source.referrerInfo = lazy.E10SUtils.deserializeReferrerInfo( aSerializable.referrerInfo ); } } if ("loadingPrincipal" in aSerializable) { // Quick pass, pass directly nsIPrincipal, we don't need to serialize // and deserialize if (aSerializable.loadingPrincipal instanceof Ci.nsIPrincipal) { source.loadingPrincipal = aSerializable.loadingPrincipal; } else { source.loadingPrincipal = lazy.E10SUtils.deserializePrincipal( aSerializable.loadingPrincipal ); } } if ("adjustChannel" in aSerializable) { source.adjustChannel = aSerializable.adjustChannel; } if ("allowHttpStatus" in aSerializable) { source.allowHttpStatus = aSerializable.allowHttpStatus; } if ("cookieJarSettings" in aSerializable) { if (aSerializable.cookieJarSettings instanceof Ci.nsICookieJarSettings) { source.cookieJarSettings = aSerializable.cookieJarSettings; } else { source.cookieJarSettings = lazy.E10SUtils.deserializeCookieJarSettings( aSerializable.cookieJarSettings ); } } if ("authHeader" in aSerializable) { source.authHeader = aSerializable.authHeader; } deserializeUnknownProperties( source, aSerializable, property => property != "url" && property != "originalUrl" && property != "isPrivate" && property != "referrerInfo" && property != "cookieJarSettings" && property != "authHeader" ); } return source; }; /** * Represents the target of a download, for example a file in the global * downloads directory, or a file in the system temporary directory. */ export var DownloadTarget = function () {}; 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, /** * Indicates whether the part file exists. Like `exists`, this is updated * dynamically to reduce I/O compared to checking the target file directly. */ partFileExists: 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. */ async refresh() { try { this.size = (await IOUtils.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.name != "NotFoundError") { console.error(ex); } this.exists = false; } this.refreshPartFileState(); }, async refreshPartFileState() { if (!this.partFilePath) { this.partFileExists = false; return; } try { this.partFileExists = (await IOUtils.stat(this.partFilePath)).size > 0; } catch (ex) { if (ex.name != "NotFoundError") { console.error(ex); } this.partFileExists = false; } }, /** * Returns a static representation of the current object state. * * @return A JavaScript object that can be serialized to JSON. */ toSerializable() { // 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. */ 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; }; /** * 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. */ export var 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 ) { 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.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. */ DownloadError.BLOCK_VERDICT_MALWARE = "Malware"; DownloadError.BLOCK_VERDICT_POTENTIALLY_UNWANTED = "PotentiallyUnwanted"; DownloadError.BLOCK_VERDICT_INSECURE = "Insecure"; DownloadError.BLOCK_VERDICT_UNCOMMON = "Uncommon"; DownloadError.BLOCK_VERDICT_DOWNLOAD_SPAM = "DownloadSpam"; DownloadError.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, /** * 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() { let serializable = { result: this.result, message: this.message, becauseSourceFailed: this.becauseSourceFailed, becauseTargetFailed: this.becauseTargetFailed, becauseBlocked: this.becauseBlocked, becauseBlockedByParentalControls: this.becauseBlockedByParentalControls, becauseBlockedByReputationCheck: this.becauseBlockedByReputationCheck, reputationCheckVerdict: this.reputationCheckVerdict, }; serializeUnknownProperties(this, serializable); return serializable; }, }; Object.setPrototypeOf(DownloadError.prototype, Error.prototype); /** * Creates a new DownloadError object from its serializable representation. * * @param aSerializable * Serializable representation of a DownloadError object. * * @return The newly created DownloadError object. */ 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 != "reputationCheckVerdict" ); return e; }; /** * Template for an object that actually transfers the data for the download. */ export var DownloadSaver = function () {}; 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. */ async execute(aSetProgressBytesFn, aSetPropertiesFn) { throw new Error("Not implemented."); }, /** * Cancels the download. */ cancel: function DS_cancel() { throw new Error("Not implemented."); }, /** * Removes any target file placeholder and any partial data kept as part of a * canceled, failed, or temporarily blocked 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. * * @param canRemoveFinalTarget * True if can remove target file regardless of it being a placeholder. * @return {Promise} * @resolves When the operation has finished successfully. * @rejects Never. */ async removeData(canRemoveFinalTarget) {}, /** * 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() { if (AppConstants.MOZ_PLACES) { lazy.DownloadHistory.addDownloadToHistory(this.download).catch( console.error ); } }, /** * Returns a static representation of the current object state. * * @return A JavaScript object that can be serialized to JSON. */ toSerializable() { throw new Error("Not implemented."); }, /** * Returns the SHA-256 hash of the downloaded file, if it exists. */ getSha256Hash() { throw new Error("Not implemented."); }, getSignatureInfo() { 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. */ 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; default: throw new Error("Unrecoginzed download saver type."); } return saver; }; /** * Saver object that simply copies the entire source file to the target. */ export var DownloadCopySaver = function () {}; DownloadCopySaver.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 Array of Array of raw bytes 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". */ async execute(aSetProgressBytesFn, aSetPropertiesFn) { this._canceled = false; let download = this.download; let targetPath = download.target.path; let partFilePath = download.target.partFilePath; let keepPartialData = download.tryToKeepPartialData; // 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. await IOUtils.writeUTF8(targetPath, "", { mode: "appendOrCreate" }); } catch (ex) { if (!DOMException.isInstance(ex)) { 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. let error = new DownloadError({ message: ex.message }); error.becauseTargetFailed = true; throw error; } let deferSaveComplete = lazy.PromiseUtils.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(); backgroundFileSaver.QueryInterface(Ci.nsIStreamListener); try { // When the operation completes, reflect the status in the promise // returned by this download execution function. backgroundFileSaver.observer = { onTargetChange() {}, 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; }, }; // 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; const notificationCallbacks = { QueryInterface: ChromeUtils.generateQI(["nsIInterfaceRequestor"]), getInterface: ChromeUtils.generateQI(["nsIProgressEventSink"]), onProgress: function DCSE_onProgress( aRequest, aProgress, aProgressMax ) { let currentBytes = resumeFromBytes + aProgress; let totalBytes = aProgressMax == -1 ? -1 : resumeFromBytes + aProgressMax; aSetProgressBytesFn( currentBytes, totalBytes, aProgress > 0 && partFilePath && keepPartialData ); }, onStatus() {}, }; const streamListener = { onStartRequest: function (aRequest) { backgroundFileSaver.onStartRequest(aRequest); if (aRequest instanceof Ci.nsIHttpChannel) { // Check if the request's response has been blocked by Windows // Parental Controls with an HTTP 450 error code. if (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; } // Check back with the initiator if we should allow a certain // HTTP code. By default, we'll just save error pages too, // however a consumer down the line, such as the WebExtensions // downloads API might want to handle this differently. if ( download.source.allowHttpStatus && !download.source.allowHttpStatus( download, aRequest.responseStatus ) ) { aRequest.cancel(Cr.NS_BINDING_ABORTED); return; } } if (aRequest instanceof Ci.nsIChannel) { aSetPropertiesFn({ contentType: aRequest.contentType }); // Ensure we report the value of "Content-Length", if available, // even if the download doesn't generate any progress events // later. if (aRequest.contentLength >= 0) { aSetProgressBytesFn(0, aRequest.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 ( aRequest instanceof Ci.nsIEncodedChannel && aRequest.contentEncodings ) { let uri = aRequest.URI; if (uri instanceof Ci.nsIURL && uri.fileExtension) { // Only the first, outermost encoding is considered. let encoding = aRequest.contentEncodings.getNext(); if (encoding) { aRequest.applyConversion = lazy.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 lazy.FileUtils.File(partFilePath), keepPartialData ); } else { // Set the final target file, and delete it on failure. backgroundFileSaver.setTarget( new lazy.FileUtils.File(targetPath), false ); } }.bind(this), onStopRequest(aRequest, aStatusCode) { try { backgroundFileSaver.onStopRequest(aRequest, 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); } } }, onDataAvailable: (aRequest, aInputStream, aOffset, aCount) => { // Check if the download have been canceled in the mean time, // and close the channel and return earlier, BackgroundFileSaver // methods shouldn't be called anymore after `finish` was called // on download cancellation. if (this._canceled) { aRequest.cancel(Cr.NS_BINDING_ABORTED); return; } backgroundFileSaver.onDataAvailable( aRequest, aInputStream, aOffset, aCount ); }, }; // Wrap the channel creation, to prevent the listener code from // accidentally using the wrong channel. // The channel that is created here is not necessarily the same channel // that will eventually perform the actual download. // When a HTTP redirect happens, the http backend will create a new // channel, this initial channel will be abandoned, and its properties // will either return incorrect data, or worse, will throw exceptions // upon access. const open = async () => { // Create a channel from the source, and listen to progress // notifications. let channel; if (download.source.loadingPrincipal) { channel = lazy.NetUtil.newChannel({ uri: download.source.url, contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD, loadingPrincipal: download.source.loadingPrincipal, // triggeringPrincipal must be the system principal to prevent the // request from being mistaken as a third-party request. triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, }); } else { channel = lazy.NetUtil.newChannel({ uri: download.source.url, contentPolicyType: Ci.nsIContentPolicy.TYPE_SAVEAS_DOWNLOAD, loadUsingSystemPrincipal: true, }); } if (channel instanceof Ci.nsIPrivateBrowsingChannel) { channel.setPrivate(download.source.isPrivate); } if ( channel instanceof Ci.nsIHttpChannel && download.source.referrerInfo ) { channel.referrerInfo = download.source.referrerInfo; // Stored computed referrerInfo; download.source.referrerInfo = channel.referrerInfo; } if ( channel instanceof Ci.nsIHttpChannel && download.source.cookieJarSettings ) { channel.loadInfo.cookieJarSettings = download.source.cookieJarSettings; } if ( channel instanceof Ci.nsIHttpChannel && download.source.authHeader ) { try { channel.setRequestHeader( "Authorization", download.source.authHeader, true ); } catch (e) {} } if (download.source.userContextId) { // Getters and setters only exist on originAttributes, // so it has to be cloned, changed, and re-set channel.loadInfo.originAttributes = { ...channel.loadInfo.originAttributes, userContextId: download.source.userContextId, }; } // This makes the channel be corretly throttled during page loads // and also prevents its caching. if (channel instanceof Ci.nsIHttpChannelInternal) { channel.channelIsForDownload = true; // Include cookies even if cookieBehavior is BEHAVIOR_REJECT_FOREIGN. channel.forceAllowThirdPartyCookie = true; } if ( channel instanceof Ci.nsIResumableChannel && this.entityID && partFilePath && keepPartialData ) { try { let stat = await IOUtils.stat(partFilePath); channel.resumeAt(stat.size, this.entityID); resumeAttempted = true; resumeFromBytes = stat.size; } catch (ex) { if (ex.name != "NotFoundError") { throw ex; } } } channel.notificationCallbacks = notificationCallbacks; // If the callback was set, handle it now before opening the channel. if (download.source.adjustChannel) { await download.source.adjustChannel(channel); } channel.asyncOpen(streamListener); }; // Kick off the download, creating and opening the channel. await open(); // 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. await deferSaveComplete.promise; await this._checkReputationAndMove(aSetPropertiesFn); }, /** * 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. */ async _checkReputationAndMove(aSetPropertiesFn) { let download = this.download; let targetPath = this.download.target.path; let partFilePath = this.download.target.partFilePath; let { shouldBlock, verdict } = await lazy.DownloadIntegration.shouldBlockForReputationCheck(download); if (shouldBlock) { Services.telemetry .getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD") .add(verdict, 0); 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 (!lazy.DownloadIntegration.shouldKeepBlockedData() || !partFilePath) { await this.removeData(!partFilePath); } else { newProperties.hasBlockedData = true; } aSetPropertiesFn(newProperties); throw new DownloadError({ becauseBlockedByReputationCheck: true, reputationCheckVerdict: verdict, }); } if (partFilePath) { try { await IOUtils.move(partFilePath, targetPath); } catch (e) { if (e.name === "NotAllowedError") { // In case we cannot write to the target file // retry with a new unique name let uniquePath = lazy.DownloadPaths.createNiceUniqueFile( new lazy.FileUtils.File(targetPath) ).path; await IOUtils.move(partFilePath, uniquePath); this.download.target.path = uniquePath; } else { throw e; } } } }, /** * 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.removeData". */ async removeData(canRemoveFinalTarget = false) { // Defined inline so removeData can be shared with DownloadLegacySaver. async function _tryToRemoveFile(path) { try { await IOUtils.remove(path); } catch (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. This // is likely to happen when the component that executed the download has // just deleted the target file itself. if (!["NotFoundError", "NotAllowedError"].includes(ex.name)) { console.error(ex); } } } if (this.download.target.partFilePath) { await _tryToRemoveFile(this.download.target.partFilePath); } if (this.download.target.path) { if ( canRemoveFinalTarget || (await isPlaceholder(this.download.target.path)) ) { await _tryToRemoveFile(this.download.target.path); } this.download.target.exists = false; this.download.target.size = 0; } }, /** * Implements "DownloadSaver.toSerializable". */ toSerializable() { // 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() { return this._sha256Hash; }, /* * Implements DownloadSaver.getSignatureInfo. */ getSignatureInfo() { return this._signatureInfo; }, /* * Implements DownloadSaver.getRedirects. */ getRedirects() { return this._redirects; }, }; Object.setPrototypeOf(DownloadCopySaver.prototype, DownloadSaver.prototype); /** * 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. */ 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; }; /** * Saver object that integrates with the legacy nsITransfer interface. * * For more background on the process, see the DownloadLegacyTransfer object. */ export var DownloadLegacySaver = function () { this.deferExecuted = lazy.PromiseUtils.defer(); this.deferCanceled = lazy.PromiseUtils.defer(); }; DownloadLegacySaver.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 Array of Array of raw bytes 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. */ onTransferStarted(aRequest) { // Store a reference to the request, used in some cases when handling // completion, and also checked during the download by unit tests. this.request = aRequest; // 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 referrerInfo at this time. if (aRequest instanceof Ci.nsIHttpChannel) { this.download.source.referrerInfo = aRequest.referrerInfo; } // Don't open the download panel when the user initiated to save a // link or document. if ( aRequest instanceof Ci.nsIChannel && aRequest.loadInfo.isUserTriggeredSave ) { this.download.openDownloadsListOnStart = false; } this.addToHistory(); }, /** * Called by the nsITransfer implementation when the request has finished. * * @param aStatus * Status code received by the nsITransfer implementation. */ onTransferFinished: function DLS_onTransferFinished(aStatus) { 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". */ async 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; } await this.copySaver.execute.apply(this.copySaver, arguments); return; } this.setProgressBytesFn = aSetProgressBytesFn; if (this.progressWasNotified) { this.onProgressBytes(this.currentBytes, this.totalBytes); } try { // Wait for the component that executes the download to finish. await 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. await IOUtils.writeUTF8(this.download.target.path, "", { mode: "create", }); } catch (ex) { if ( !DOMException.isInstance(ex) || ex.name !== "NoModificationAllowedError" ) { throw ex; } } } await this._checkReputationAndMove(aSetPropertiesFn); } catch (ex) { // 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; } }, _checkReputationAndMove() { 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.removeData". */ removeData(canRemoveFinalTarget) { // DownloadCopySaver and DownloadLegacySaver 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.removeData.call( this, canRemoveFinalTarget ); }, /** * Implements "DownloadSaver.toSerializable". */ toSerializable() { // 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() { if (this.copySaver) { return this.copySaver.getSha256Hash(); } return this._sha256Hash; }, /** * Called by the nsITransfer implementation when the hash is available. */ setSha256Hash(hash) { this._sha256Hash = hash; }, /** * Implements "DownloadSaver.getSignatureInfo". */ getSignatureInfo() { if (this.copySaver) { return this.copySaver.getSignatureInfo(); } return this._signatureInfo; }, /** * Called by the nsITransfer implementation when the hash is available. */ setSignatureInfo(signatureInfo) { this._signatureInfo = signatureInfo; }, /** * Implements "DownloadSaver.getRedirects". */ getRedirects() { if (this.copySaver) { return this.copySaver.getRedirects(); } return this._redirects; }, /** * Called by the nsITransfer implementation when the redirect chain is * available. */ setRedirects(redirects) { this._redirects = redirects; }, }; Object.setPrototypeOf(DownloadLegacySaver.prototype, DownloadSaver.prototype); /** * 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. */ DownloadLegacySaver.fromSerializable = function () { return new DownloadLegacySaver(); };