1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,2179 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +/** 1.11 + * This file includes the following constructors and global objects: 1.12 + * 1.13 + * Download 1.14 + * Represents a single download, with associated state and actions. This object 1.15 + * is transient, though it can be included in a DownloadList so that it can be 1.16 + * managed by the user interface and persisted across sessions. 1.17 + * 1.18 + * DownloadSource 1.19 + * Represents the source of a download, for example a document or an URI. 1.20 + * 1.21 + * DownloadTarget 1.22 + * Represents the target of a download, for example a file in the global 1.23 + * downloads directory, or a file in the system temporary directory. 1.24 + * 1.25 + * DownloadError 1.26 + * Provides detailed information about a download failure. 1.27 + * 1.28 + * DownloadSaver 1.29 + * Template for an object that actually transfers the data for the download. 1.30 + * 1.31 + * DownloadCopySaver 1.32 + * Saver object that simply copies the entire source file to the target. 1.33 + * 1.34 + * DownloadLegacySaver 1.35 + * Saver object that integrates with the legacy nsITransfer interface. 1.36 + */ 1.37 + 1.38 +"use strict"; 1.39 + 1.40 +this.EXPORTED_SYMBOLS = [ 1.41 + "Download", 1.42 + "DownloadSource", 1.43 + "DownloadTarget", 1.44 + "DownloadError", 1.45 + "DownloadSaver", 1.46 + "DownloadCopySaver", 1.47 + "DownloadLegacySaver", 1.48 +]; 1.49 + 1.50 +//////////////////////////////////////////////////////////////////////////////// 1.51 +//// Globals 1.52 + 1.53 +const Cc = Components.classes; 1.54 +const Ci = Components.interfaces; 1.55 +const Cu = Components.utils; 1.56 +const Cr = Components.results; 1.57 + 1.58 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.59 + 1.60 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration", 1.61 + "resource://gre/modules/DownloadIntegration.jsm"); 1.62 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", 1.63 + "resource://gre/modules/FileUtils.jsm"); 1.64 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.65 + "resource://gre/modules/NetUtil.jsm"); 1.66 +XPCOMUtils.defineLazyModuleGetter(this, "OS", 1.67 + "resource://gre/modules/osfile.jsm") 1.68 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.69 + "resource://gre/modules/Promise.jsm"); 1.70 +XPCOMUtils.defineLazyModuleGetter(this, "Services", 1.71 + "resource://gre/modules/Services.jsm"); 1.72 +XPCOMUtils.defineLazyModuleGetter(this, "Task", 1.73 + "resource://gre/modules/Task.jsm"); 1.74 + 1.75 +XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory", 1.76 + "@mozilla.org/browser/download-history;1", 1.77 + Ci.nsIDownloadHistory); 1.78 +XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher", 1.79 + "@mozilla.org/uriloader/external-helper-app-service;1", 1.80 + Ci.nsPIExternalAppLauncher); 1.81 +XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService", 1.82 + "@mozilla.org/uriloader/external-helper-app-service;1", 1.83 + Ci.nsIExternalHelperAppService); 1.84 + 1.85 +const BackgroundFileSaverStreamListener = Components.Constructor( 1.86 + "@mozilla.org/network/background-file-saver;1?mode=streamlistener", 1.87 + "nsIBackgroundFileSaver"); 1.88 + 1.89 +/** 1.90 + * Returns true if the given value is a primitive string or a String object. 1.91 + */ 1.92 +function isString(aValue) { 1.93 + // We cannot use the "instanceof" operator reliably across module boundaries. 1.94 + return (typeof aValue == "string") || 1.95 + (typeof aValue == "object" && "charAt" in aValue); 1.96 +} 1.97 + 1.98 +/** 1.99 + * Serialize the unknown properties of aObject into aSerializable. 1.100 + */ 1.101 +function serializeUnknownProperties(aObject, aSerializable) 1.102 +{ 1.103 + if (aObject._unknownProperties) { 1.104 + for (let property in aObject._unknownProperties) { 1.105 + aSerializable[property] = aObject._unknownProperties[property]; 1.106 + } 1.107 + } 1.108 +} 1.109 + 1.110 +/** 1.111 + * Check for any unknown properties in aSerializable and preserve those in the 1.112 + * _unknownProperties field of aObject. aFilterFn is called for each property 1.113 + * name of aObject and should return true only for unknown properties. 1.114 + */ 1.115 +function deserializeUnknownProperties(aObject, aSerializable, aFilterFn) 1.116 +{ 1.117 + for (let property in aSerializable) { 1.118 + if (aFilterFn(property)) { 1.119 + if (!aObject._unknownProperties) { 1.120 + aObject._unknownProperties = { }; 1.121 + } 1.122 + 1.123 + aObject._unknownProperties[property] = aSerializable[property]; 1.124 + } 1.125 + } 1.126 +} 1.127 + 1.128 +/** 1.129 + * This determines the minimum time interval between updates to the number of 1.130 + * bytes transferred, and is a limiting factor to the sequence of readings used 1.131 + * in calculating the speed of the download. 1.132 + */ 1.133 +const kProgressUpdateIntervalMs = 400; 1.134 + 1.135 +//////////////////////////////////////////////////////////////////////////////// 1.136 +//// Download 1.137 + 1.138 +/** 1.139 + * Represents a single download, with associated state and actions. This object 1.140 + * is transient, though it can be included in a DownloadList so that it can be 1.141 + * managed by the user interface and persisted across sessions. 1.142 + */ 1.143 +this.Download = function () 1.144 +{ 1.145 + this._deferSucceeded = Promise.defer(); 1.146 +} 1.147 + 1.148 +this.Download.prototype = { 1.149 + /** 1.150 + * DownloadSource object associated with this download. 1.151 + */ 1.152 + source: null, 1.153 + 1.154 + /** 1.155 + * DownloadTarget object associated with this download. 1.156 + */ 1.157 + target: null, 1.158 + 1.159 + /** 1.160 + * DownloadSaver object associated with this download. 1.161 + */ 1.162 + saver: null, 1.163 + 1.164 + /** 1.165 + * Indicates that the download never started, has been completed successfully, 1.166 + * failed, or has been canceled. This property becomes false when a download 1.167 + * is started for the first time, or when a failed or canceled download is 1.168 + * restarted. 1.169 + */ 1.170 + stopped: true, 1.171 + 1.172 + /** 1.173 + * Indicates that the download has been completed successfully. 1.174 + */ 1.175 + succeeded: false, 1.176 + 1.177 + /** 1.178 + * Indicates that the download has been canceled. This property can become 1.179 + * true, then it can be reset to false when a canceled download is restarted. 1.180 + * 1.181 + * This property becomes true as soon as the "cancel" method is called, though 1.182 + * the "stopped" property might remain false until the cancellation request 1.183 + * has been processed. Temporary files or part files may still exist even if 1.184 + * they are expected to be deleted, until the "stopped" property becomes true. 1.185 + */ 1.186 + canceled: false, 1.187 + 1.188 + /** 1.189 + * When the download fails, this is set to a DownloadError instance indicating 1.190 + * the cause of the failure. If the download has been completed successfully 1.191 + * or has been canceled, this property is null. This property is reset to 1.192 + * null when a failed download is restarted. 1.193 + */ 1.194 + error: null, 1.195 + 1.196 + /** 1.197 + * Indicates the start time of the download. When the download starts, 1.198 + * this property is set to a valid Date object. The default value is null 1.199 + * before the download starts. 1.200 + */ 1.201 + startTime: null, 1.202 + 1.203 + /** 1.204 + * Indicates whether this download's "progress" property is able to report 1.205 + * partial progress while the download proceeds, and whether the value in 1.206 + * totalBytes is relevant. This depends on the saver and the download source. 1.207 + */ 1.208 + hasProgress: false, 1.209 + 1.210 + /** 1.211 + * Progress percent, from 0 to 100. Intermediate values are reported only if 1.212 + * hasProgress is true. 1.213 + * 1.214 + * @note You shouldn't rely on this property being equal to 100 to determine 1.215 + * whether the download is completed. You should use the individual 1.216 + * state properties instead. 1.217 + */ 1.218 + progress: 0, 1.219 + 1.220 + /** 1.221 + * When hasProgress is true, indicates the total number of bytes to be 1.222 + * transferred before the download finishes, that can be zero for empty files. 1.223 + * 1.224 + * When hasProgress is false, this property is always zero. 1.225 + */ 1.226 + totalBytes: 0, 1.227 + 1.228 + /** 1.229 + * Number of bytes currently transferred. This value starts at zero, and may 1.230 + * be updated regardless of the value of hasProgress. 1.231 + * 1.232 + * @note You shouldn't rely on this property being equal to totalBytes to 1.233 + * determine whether the download is completed. You should use the 1.234 + * individual state properties instead. 1.235 + */ 1.236 + currentBytes: 0, 1.237 + 1.238 + /** 1.239 + * Fractional number representing the speed of the download, in bytes per 1.240 + * second. This value is zero when the download is stopped, and may be 1.241 + * updated regardless of the value of hasProgress. 1.242 + */ 1.243 + speed: 0, 1.244 + 1.245 + /** 1.246 + * Indicates whether, at this time, there is any partially downloaded data 1.247 + * that can be used when restarting a failed or canceled download. 1.248 + * 1.249 + * This property is relevant while the download is in progress, and also if it 1.250 + * failed or has been canceled. If the download has been completed 1.251 + * successfully, this property is always false. 1.252 + * 1.253 + * Whether partial data can actually be retained depends on the saver and the 1.254 + * download source, and may not be known before the download is started. 1.255 + */ 1.256 + hasPartialData: false, 1.257 + 1.258 + /** 1.259 + * This can be set to a function that is called after other properties change. 1.260 + */ 1.261 + onchange: null, 1.262 + 1.263 + /** 1.264 + * This tells if the user has chosen to open/run the downloaded file after 1.265 + * download has completed. 1.266 + */ 1.267 + launchWhenSucceeded: false, 1.268 + 1.269 + /** 1.270 + * This represents the MIME type of the download. 1.271 + */ 1.272 + contentType: null, 1.273 + 1.274 + /** 1.275 + * This indicates the path of the application to be used to launch the file, 1.276 + * or null if the file should be launched with the default application. 1.277 + */ 1.278 + launcherPath: null, 1.279 + 1.280 + /** 1.281 + * Raises the onchange notification. 1.282 + */ 1.283 + _notifyChange: function D_notifyChange() { 1.284 + try { 1.285 + if (this.onchange) { 1.286 + this.onchange(); 1.287 + } 1.288 + } catch (ex) { 1.289 + Cu.reportError(ex); 1.290 + } 1.291 + }, 1.292 + 1.293 + /** 1.294 + * The download may be stopped and restarted multiple times before it 1.295 + * completes successfully. This may happen if any of the download attempts is 1.296 + * canceled or fails. 1.297 + * 1.298 + * This property contains a promise that is linked to the current attempt, or 1.299 + * null if the download is either stopped or in the process of being canceled. 1.300 + * If the download restarts, this property is replaced with a new promise. 1.301 + * 1.302 + * The promise is resolved if the attempt it represents finishes successfully, 1.303 + * and rejected if the attempt fails. 1.304 + */ 1.305 + _currentAttempt: null, 1.306 + 1.307 + /** 1.308 + * Starts the download for the first time, or restarts a download that failed 1.309 + * or has been canceled. 1.310 + * 1.311 + * Calling this method when the download has been completed successfully has 1.312 + * no effect, and the method returns a resolved promise. If the download is 1.313 + * in progress, the method returns the same promise as the previous call. 1.314 + * 1.315 + * If the "cancel" method was called but the cancellation process has not 1.316 + * finished yet, this method waits for the cancellation to finish, then 1.317 + * restarts the download immediately. 1.318 + * 1.319 + * @note If you need to start a new download from the same source, rather than 1.320 + * restarting a failed or canceled one, you should create a separate 1.321 + * Download object with the same source as the current one. 1.322 + * 1.323 + * @return {Promise} 1.324 + * @resolves When the download has finished successfully. 1.325 + * @rejects JavaScript exception if the download failed. 1.326 + */ 1.327 + start: function D_start() 1.328 + { 1.329 + // If the download succeeded, it's the final state, we have nothing to do. 1.330 + if (this.succeeded) { 1.331 + return Promise.resolve(); 1.332 + } 1.333 + 1.334 + // If the download already started and hasn't failed or hasn't been 1.335 + // canceled, return the same promise as the previous call, allowing the 1.336 + // caller to wait for the current attempt to finish. 1.337 + if (this._currentAttempt) { 1.338 + return this._currentAttempt; 1.339 + } 1.340 + 1.341 + // While shutting down or disposing of this object, we prevent the download 1.342 + // from returning to be in progress. 1.343 + if (this._finalized) { 1.344 + return Promise.reject(new DownloadError({ 1.345 + message: "Cannot start after finalization."})); 1.346 + } 1.347 + 1.348 + // Initialize all the status properties for a new or restarted download. 1.349 + this.stopped = false; 1.350 + this.canceled = false; 1.351 + this.error = null; 1.352 + this.hasProgress = false; 1.353 + this.progress = 0; 1.354 + this.totalBytes = 0; 1.355 + this.currentBytes = 0; 1.356 + this.startTime = new Date(); 1.357 + 1.358 + // Create a new deferred object and an associated promise before starting 1.359 + // the actual download. We store it on the download as the current attempt. 1.360 + let deferAttempt = Promise.defer(); 1.361 + let currentAttempt = deferAttempt.promise; 1.362 + this._currentAttempt = currentAttempt; 1.363 + 1.364 + // Restart the progress and speed calculations from scratch. 1.365 + this._lastProgressTimeMs = 0; 1.366 + 1.367 + // This function propagates progress from the DownloadSaver object, unless 1.368 + // it comes in late from a download attempt that was replaced by a new one. 1.369 + // If the cancellation process for the download has started, then the update 1.370 + // is ignored. 1.371 + function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData) 1.372 + { 1.373 + if (this._currentAttempt == currentAttempt) { 1.374 + this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData); 1.375 + } 1.376 + } 1.377 + 1.378 + // This function propagates download properties from the DownloadSaver 1.379 + // object, unless it comes in late from a download attempt that was 1.380 + // replaced by a new one. If the cancellation process for the download has 1.381 + // started, then the update is ignored. 1.382 + function DS_setProperties(aOptions) 1.383 + { 1.384 + if (this._currentAttempt != currentAttempt) { 1.385 + return; 1.386 + } 1.387 + 1.388 + let changeMade = false; 1.389 + 1.390 + if ("contentType" in aOptions && 1.391 + this.contentType != aOptions.contentType) { 1.392 + this.contentType = aOptions.contentType; 1.393 + changeMade = true; 1.394 + } 1.395 + 1.396 + if (changeMade) { 1.397 + this._notifyChange(); 1.398 + } 1.399 + } 1.400 + 1.401 + // Now that we stored the promise in the download object, we can start the 1.402 + // task that will actually execute the download. 1.403 + deferAttempt.resolve(Task.spawn(function task_D_start() { 1.404 + // Wait upon any pending operation before restarting. 1.405 + if (this._promiseCanceled) { 1.406 + yield this._promiseCanceled; 1.407 + } 1.408 + if (this._promiseRemovePartialData) { 1.409 + try { 1.410 + yield this._promiseRemovePartialData; 1.411 + } catch (ex) { 1.412 + // Ignore any errors, which are already reported by the original 1.413 + // caller of the removePartialData method. 1.414 + } 1.415 + } 1.416 + 1.417 + // In case the download was restarted while cancellation was in progress, 1.418 + // but the previous attempt actually succeeded before cancellation could 1.419 + // be processed, it is possible that the download has already finished. 1.420 + if (this.succeeded) { 1.421 + return; 1.422 + } 1.423 + 1.424 + try { 1.425 + // Disallow download if parental controls service restricts it. 1.426 + if (yield DownloadIntegration.shouldBlockForParentalControls(this)) { 1.427 + throw new DownloadError({ becauseBlockedByParentalControls: true }); 1.428 + } 1.429 + 1.430 + // We should check if we have been canceled in the meantime, after all 1.431 + // the previous asynchronous operations have been executed and just 1.432 + // before we call the "execute" method of the saver. 1.433 + if (this._promiseCanceled) { 1.434 + // The exception will become a cancellation in the "catch" block. 1.435 + throw undefined; 1.436 + } 1.437 + 1.438 + // Execute the actual download through the saver object. 1.439 + this._saverExecuting = true; 1.440 + yield this.saver.execute(DS_setProgressBytes.bind(this), 1.441 + DS_setProperties.bind(this)); 1.442 + 1.443 + // Check for application reputation, which requires the entire file to 1.444 + // be downloaded. After that, check for the last time if the download 1.445 + // has been canceled. Both cases require the target file to be deleted, 1.446 + // thus we process both in the same block of code. 1.447 + if ((yield DownloadIntegration.shouldBlockForReputationCheck(this)) || 1.448 + this._promiseCanceled) { 1.449 + try { 1.450 + yield OS.File.remove(this.target.path); 1.451 + } catch (ex) { 1.452 + Cu.reportError(ex); 1.453 + } 1.454 + // If this is actually a cancellation, this exception will be changed 1.455 + // in the catch block below. 1.456 + throw new DownloadError({ becauseBlockedByReputationCheck: true }); 1.457 + } 1.458 + 1.459 + // Update the status properties for a successful download. 1.460 + this.progress = 100; 1.461 + this.succeeded = true; 1.462 + this.hasPartialData = false; 1.463 + } catch (ex) { 1.464 + // Fail with a generic status code on cancellation, so that the caller 1.465 + // is forced to actually check the status properties to see if the 1.466 + // download was canceled or failed because of other reasons. 1.467 + if (this._promiseCanceled) { 1.468 + throw new DownloadError({ message: "Download canceled." }); 1.469 + } 1.470 + 1.471 + // An HTTP 450 error code is used by Windows to indicate that a uri is 1.472 + // blocked by parental controls. This will prevent the download from 1.473 + // occuring, so an error needs to be raised. This is not performed 1.474 + // during the parental controls check above as it requires the request 1.475 + // to start. 1.476 + if (this._blockedByParentalControls) { 1.477 + ex = new DownloadError({ becauseBlockedByParentalControls: true }); 1.478 + } 1.479 + 1.480 + // Update the download error, unless a new attempt already started. The 1.481 + // change in the status property is notified in the finally block. 1.482 + if (this._currentAttempt == currentAttempt || !this._currentAttempt) { 1.483 + this.error = ex; 1.484 + } 1.485 + throw ex; 1.486 + } finally { 1.487 + // Any cancellation request has now been processed. 1.488 + this._saverExecuting = false; 1.489 + this._promiseCanceled = null; 1.490 + 1.491 + // Update the status properties, unless a new attempt already started. 1.492 + if (this._currentAttempt == currentAttempt || !this._currentAttempt) { 1.493 + this._currentAttempt = null; 1.494 + this.stopped = true; 1.495 + this.speed = 0; 1.496 + this._notifyChange(); 1.497 + if (this.succeeded) { 1.498 + yield DownloadIntegration.downloadDone(this); 1.499 + 1.500 + this._deferSucceeded.resolve(); 1.501 + 1.502 + if (this.launchWhenSucceeded) { 1.503 + this.launch().then(null, Cu.reportError); 1.504 + 1.505 + // Always schedule files to be deleted at the end of the private browsing 1.506 + // mode, regardless of the value of the pref. 1.507 + if (this.source.isPrivate) { 1.508 + gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible( 1.509 + new FileUtils.File(this.target.path)); 1.510 + } else if (Services.prefs.getBoolPref( 1.511 + "browser.helperApps.deleteTempFileOnExit")) { 1.512 + gExternalAppLauncher.deleteTemporaryFileOnExit( 1.513 + new FileUtils.File(this.target.path)); 1.514 + } 1.515 + } 1.516 + } 1.517 + } 1.518 + } 1.519 + }.bind(this))); 1.520 + 1.521 + // Notify the new download state before returning. 1.522 + this._notifyChange(); 1.523 + return currentAttempt; 1.524 + }, 1.525 + 1.526 + /* 1.527 + * Launches the file after download has completed. This can open 1.528 + * the file with the default application for the target MIME type 1.529 + * or file extension, or with a custom application if launcherPath 1.530 + * is set. 1.531 + * 1.532 + * @return {Promise} 1.533 + * @resolves When the instruction to launch the file has been 1.534 + * successfully given to the operating system. Note that 1.535 + * the OS might still take a while until the file is actually 1.536 + * launched. 1.537 + * @rejects JavaScript exception if there was an error trying to launch 1.538 + * the file. 1.539 + */ 1.540 + launch: function() { 1.541 + if (!this.succeeded) { 1.542 + return Promise.reject( 1.543 + new Error("launch can only be called if the download succeeded") 1.544 + ); 1.545 + } 1.546 + 1.547 + return DownloadIntegration.launchDownload(this); 1.548 + }, 1.549 + 1.550 + /* 1.551 + * Shows the folder containing the target file, or where the target file 1.552 + * will be saved. This may be called at any time, even if the download 1.553 + * failed or is currently in progress. 1.554 + * 1.555 + * @return {Promise} 1.556 + * @resolves When the instruction to open the containing folder has been 1.557 + * successfully given to the operating system. Note that 1.558 + * the OS might still take a while until the folder is actually 1.559 + * opened. 1.560 + * @rejects JavaScript exception if there was an error trying to open 1.561 + * the containing folder. 1.562 + */ 1.563 + showContainingDirectory: function D_showContainingDirectory() { 1.564 + return DownloadIntegration.showContainingDirectory(this.target.path); 1.565 + }, 1.566 + 1.567 + /** 1.568 + * When a request to cancel the download is received, contains a promise that 1.569 + * will be resolved when the cancellation request is processed. When the 1.570 + * request is processed, this property becomes null again. 1.571 + */ 1.572 + _promiseCanceled: null, 1.573 + 1.574 + /** 1.575 + * True between the call to the "execute" method of the saver and the 1.576 + * completion of the current download attempt. 1.577 + */ 1.578 + _saverExecuting: false, 1.579 + 1.580 + /** 1.581 + * Cancels the download. 1.582 + * 1.583 + * The cancellation request is asynchronous. Until the cancellation process 1.584 + * finishes, temporary files or part files may still exist even if they are 1.585 + * expected to be deleted. 1.586 + * 1.587 + * In case the download completes successfully before the cancellation request 1.588 + * could be processed, this method has no effect, and it returns a resolved 1.589 + * promise. You should check the properties of the download at the time the 1.590 + * returned promise is resolved to determine if the download was cancelled. 1.591 + * 1.592 + * Calling this method when the download has been completed successfully, 1.593 + * failed, or has been canceled has no effect, and the method returns a 1.594 + * resolved promise. This behavior is designed for the case where the call 1.595 + * to "cancel" happens asynchronously, and is consistent with the case where 1.596 + * the cancellation request could not be processed in time. 1.597 + * 1.598 + * @return {Promise} 1.599 + * @resolves When the cancellation process has finished. 1.600 + * @rejects Never. 1.601 + */ 1.602 + cancel: function D_cancel() 1.603 + { 1.604 + // If the download is currently stopped, we have nothing to do. 1.605 + if (this.stopped) { 1.606 + return Promise.resolve(); 1.607 + } 1.608 + 1.609 + if (!this._promiseCanceled) { 1.610 + // Start a new cancellation request. 1.611 + let deferCanceled = Promise.defer(); 1.612 + this._currentAttempt.then(function () deferCanceled.resolve(), 1.613 + function () deferCanceled.resolve()); 1.614 + this._promiseCanceled = deferCanceled.promise; 1.615 + 1.616 + // The download can already be restarted. 1.617 + this._currentAttempt = null; 1.618 + 1.619 + // Notify that the cancellation request was received. 1.620 + this.canceled = true; 1.621 + this._notifyChange(); 1.622 + 1.623 + // Execute the actual cancellation through the saver object, in case it 1.624 + // has already started. Otherwise, the cancellation will be handled just 1.625 + // before the saver is started. 1.626 + if (this._saverExecuting) { 1.627 + this.saver.cancel(); 1.628 + } 1.629 + } 1.630 + 1.631 + return this._promiseCanceled; 1.632 + }, 1.633 + 1.634 + /** 1.635 + * Indicates whether any partially downloaded data should be retained, to use 1.636 + * when restarting a failed or canceled download. The default is false. 1.637 + * 1.638 + * Whether partial data can actually be retained depends on the saver and the 1.639 + * download source, and may not be known before the download is started. 1.640 + * 1.641 + * To have any effect, this property must be set before starting the download. 1.642 + * Resetting this property to false after the download has already started 1.643 + * will not remove any partial data. 1.644 + * 1.645 + * If this property is set to true, care should be taken that partial data is 1.646 + * removed before the reference to the download is discarded. This can be 1.647 + * done using the removePartialData or the "finalize" methods. 1.648 + */ 1.649 + tryToKeepPartialData: false, 1.650 + 1.651 + /** 1.652 + * When a request to remove partially downloaded data is received, contains a 1.653 + * promise that will be resolved when the removal request is processed. When 1.654 + * the request is processed, this property becomes null again. 1.655 + */ 1.656 + _promiseRemovePartialData: null, 1.657 + 1.658 + /** 1.659 + * Removes any partial data kept as part of a canceled or failed download. 1.660 + * 1.661 + * If the download is not canceled or failed, this method has no effect, and 1.662 + * it returns a resolved promise. If the "cancel" method was called but the 1.663 + * cancellation process has not finished yet, this method waits for the 1.664 + * cancellation to finish, then removes the partial data. 1.665 + * 1.666 + * After this method has been called, if the tryToKeepPartialData property is 1.667 + * still true when the download is restarted, partial data will be retained 1.668 + * during the new download attempt. 1.669 + * 1.670 + * @return {Promise} 1.671 + * @resolves When the partial data has been successfully removed. 1.672 + * @rejects JavaScript exception if the operation could not be completed. 1.673 + */ 1.674 + removePartialData: function () 1.675 + { 1.676 + if (!this.canceled && !this.error) { 1.677 + return Promise.resolve(); 1.678 + } 1.679 + 1.680 + let promiseRemovePartialData = this._promiseRemovePartialData; 1.681 + 1.682 + if (!promiseRemovePartialData) { 1.683 + let deferRemovePartialData = Promise.defer(); 1.684 + promiseRemovePartialData = deferRemovePartialData.promise; 1.685 + this._promiseRemovePartialData = promiseRemovePartialData; 1.686 + 1.687 + deferRemovePartialData.resolve( 1.688 + Task.spawn(function task_D_removePartialData() { 1.689 + try { 1.690 + // Wait upon any pending cancellation request. 1.691 + if (this._promiseCanceled) { 1.692 + yield this._promiseCanceled; 1.693 + } 1.694 + // Ask the saver object to remove any partial data. 1.695 + yield this.saver.removePartialData(); 1.696 + // For completeness, clear the number of bytes transferred. 1.697 + if (this.currentBytes != 0 || this.hasPartialData) { 1.698 + this.currentBytes = 0; 1.699 + this.hasPartialData = false; 1.700 + this._notifyChange(); 1.701 + } 1.702 + } finally { 1.703 + this._promiseRemovePartialData = null; 1.704 + } 1.705 + }.bind(this))); 1.706 + } 1.707 + 1.708 + return promiseRemovePartialData; 1.709 + }, 1.710 + 1.711 + /** 1.712 + * This deferred object contains a promise that is resolved as soon as this 1.713 + * download finishes successfully, and is never rejected. This property is 1.714 + * initialized when the download is created, and never changes. 1.715 + */ 1.716 + _deferSucceeded: null, 1.717 + 1.718 + /** 1.719 + * Returns a promise that is resolved as soon as this download finishes 1.720 + * successfully, even if the download was stopped and restarted meanwhile. 1.721 + * 1.722 + * You can use this property for scheduling download completion actions in the 1.723 + * current session, for downloads that are controlled interactively. If the 1.724 + * download is not controlled interactively, you should use the promise 1.725 + * returned by the "start" method instead, to check for success or failure. 1.726 + * 1.727 + * @return {Promise} 1.728 + * @resolves When the download has finished successfully. 1.729 + * @rejects Never. 1.730 + */ 1.731 + whenSucceeded: function D_whenSucceeded() 1.732 + { 1.733 + return this._deferSucceeded.promise; 1.734 + }, 1.735 + 1.736 + /** 1.737 + * Updates the state of a finished, failed, or canceled download based on the 1.738 + * current state in the file system. If the download is in progress or it has 1.739 + * been finalized, this method has no effect, and it returns a resolved 1.740 + * promise. 1.741 + * 1.742 + * This allows the properties of the download to be updated in case the user 1.743 + * moved or deleted the target file or its associated ".part" file. 1.744 + * 1.745 + * @return {Promise} 1.746 + * @resolves When the operation has completed. 1.747 + * @rejects Never. 1.748 + */ 1.749 + refresh: function () 1.750 + { 1.751 + return Task.spawn(function () { 1.752 + if (!this.stopped || this._finalized) { 1.753 + return; 1.754 + } 1.755 + 1.756 + // Update the current progress from disk if we retained partial data. 1.757 + if (this.hasPartialData && this.target.partFilePath) { 1.758 + let stat = yield OS.File.stat(this.target.partFilePath); 1.759 + 1.760 + // Ignore the result if the state has changed meanwhile. 1.761 + if (!this.stopped || this._finalized) { 1.762 + return; 1.763 + } 1.764 + 1.765 + // Update the bytes transferred and the related progress properties. 1.766 + this.currentBytes = stat.size; 1.767 + if (this.totalBytes > 0) { 1.768 + this.hasProgress = true; 1.769 + this.progress = Math.floor(this.currentBytes / 1.770 + this.totalBytes * 100); 1.771 + } 1.772 + this._notifyChange(); 1.773 + } 1.774 + }.bind(this)).then(null, Cu.reportError); 1.775 + }, 1.776 + 1.777 + /** 1.778 + * True if the "finalize" method has been called. This prevents the download 1.779 + * from starting again after having been stopped. 1.780 + */ 1.781 + _finalized: false, 1.782 + 1.783 + /** 1.784 + * Ensures that the download is stopped, and optionally removes any partial 1.785 + * data kept as part of a canceled or failed download. After this method has 1.786 + * been called, the download cannot be started again. 1.787 + * 1.788 + * This method should be used in place of "cancel" and removePartialData while 1.789 + * shutting down or disposing of the download object, to prevent other callers 1.790 + * from interfering with the operation. This is required because cancellation 1.791 + * and other operations are asynchronous. 1.792 + * 1.793 + * @param aRemovePartialData 1.794 + * Whether any partially downloaded data should be removed after the 1.795 + * download has been stopped. 1.796 + * 1.797 + * @return {Promise} 1.798 + * @resolves When the operation has finished successfully. 1.799 + * @rejects JavaScript exception if an error occurred while removing the 1.800 + * partially downloaded data. 1.801 + */ 1.802 + finalize: function (aRemovePartialData) 1.803 + { 1.804 + // Prevents the download from starting again after having been stopped. 1.805 + this._finalized = true; 1.806 + 1.807 + if (aRemovePartialData) { 1.808 + // Cancel the download, in case it is currently in progress, then remove 1.809 + // any partially downloaded data. The removal operation waits for 1.810 + // cancellation to be completed before resolving the promise it returns. 1.811 + this.cancel(); 1.812 + return this.removePartialData(); 1.813 + } else { 1.814 + // Just cancel the download, in case it is currently in progress. 1.815 + return this.cancel(); 1.816 + } 1.817 + }, 1.818 + 1.819 + /** 1.820 + * Indicates the time of the last progress notification, expressed as the 1.821 + * number of milliseconds since January 1, 1970, 00:00:00 UTC. This is zero 1.822 + * until some bytes have actually been transferred. 1.823 + */ 1.824 + _lastProgressTimeMs: 0, 1.825 + 1.826 + /** 1.827 + * Updates progress notifications based on the number of bytes transferred. 1.828 + * 1.829 + * The number of bytes transferred is not updated unless enough time passed 1.830 + * since this function was last called. This limits the computation load, in 1.831 + * particular when the listeners update the user interface in response. 1.832 + * 1.833 + * @param aCurrentBytes 1.834 + * Number of bytes transferred until now. 1.835 + * @param aTotalBytes 1.836 + * Total number of bytes to be transferred, or -1 if unknown. 1.837 + * @param aHasPartialData 1.838 + * Indicates whether the partially downloaded data can be used when 1.839 + * restarting the download if it fails or is canceled. 1.840 + */ 1.841 + _setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) { 1.842 + let changeMade = (this.hasPartialData != aHasPartialData); 1.843 + this.hasPartialData = aHasPartialData; 1.844 + 1.845 + // Unless aTotalBytes is -1, we can report partial download progress. In 1.846 + // this case, notify when the related properties changed since last time. 1.847 + if (aTotalBytes != -1 && (!this.hasProgress || 1.848 + this.totalBytes != aTotalBytes)) { 1.849 + this.hasProgress = true; 1.850 + this.totalBytes = aTotalBytes; 1.851 + changeMade = true; 1.852 + } 1.853 + 1.854 + // Updating the progress and computing the speed require that enough time 1.855 + // passed since the last update, or that we haven't started throttling yet. 1.856 + let currentTimeMs = Date.now(); 1.857 + let intervalMs = currentTimeMs - this._lastProgressTimeMs; 1.858 + if (intervalMs >= kProgressUpdateIntervalMs) { 1.859 + // Don't compute the speed unless we started throttling notifications. 1.860 + if (this._lastProgressTimeMs != 0) { 1.861 + // Calculate the speed in bytes per second. 1.862 + let rawSpeed = (aCurrentBytes - this.currentBytes) / intervalMs * 1000; 1.863 + if (this.speed == 0) { 1.864 + // When the previous speed is exactly zero instead of a fractional 1.865 + // number, this can be considered the first element of the series. 1.866 + this.speed = rawSpeed; 1.867 + } else { 1.868 + // Apply exponential smoothing, with a smoothing factor of 0.1. 1.869 + this.speed = rawSpeed * 0.1 + this.speed * 0.9; 1.870 + } 1.871 + } 1.872 + 1.873 + // Start throttling notifications only when we have actually received some 1.874 + // bytes for the first time. The timing of the first part of the download 1.875 + // is not reliable, due to possible latency in the initial notifications. 1.876 + // This also allows automated tests to receive and verify the number of 1.877 + // bytes initially transferred. 1.878 + if (aCurrentBytes > 0) { 1.879 + this._lastProgressTimeMs = currentTimeMs; 1.880 + 1.881 + // Update the progress now that we don't need its previous value. 1.882 + this.currentBytes = aCurrentBytes; 1.883 + if (this.totalBytes > 0) { 1.884 + this.progress = Math.floor(this.currentBytes / this.totalBytes * 100); 1.885 + } 1.886 + changeMade = true; 1.887 + } 1.888 + } 1.889 + 1.890 + if (changeMade) { 1.891 + this._notifyChange(); 1.892 + } 1.893 + }, 1.894 + 1.895 + /** 1.896 + * Returns a static representation of the current object state. 1.897 + * 1.898 + * @return A JavaScript object that can be serialized to JSON. 1.899 + */ 1.900 + toSerializable: function () 1.901 + { 1.902 + let serializable = { 1.903 + source: this.source.toSerializable(), 1.904 + target: this.target.toSerializable(), 1.905 + }; 1.906 + 1.907 + // Simplify the representation for the most common saver type. If the saver 1.908 + // is an object instead of a simple string, we can't simplify it because we 1.909 + // need to persist all its properties, not only "type". This may happen for 1.910 + // savers of type "copy" as well as other types. 1.911 + let saver = this.saver.toSerializable(); 1.912 + if (saver !== "copy") { 1.913 + serializable.saver = saver; 1.914 + } 1.915 + 1.916 + if (this.error && ("message" in this.error)) { 1.917 + serializable.error = { message: this.error.message }; 1.918 + } 1.919 + 1.920 + if (this.startTime) { 1.921 + serializable.startTime = this.startTime.toJSON(); 1.922 + } 1.923 + 1.924 + // These are serialized unless they are false, null, or empty strings. 1.925 + for (let property of kSerializableDownloadProperties) { 1.926 + if (property != "error" && property != "startTime" && this[property]) { 1.927 + serializable[property] = this[property]; 1.928 + } 1.929 + } 1.930 + 1.931 + serializeUnknownProperties(this, serializable); 1.932 + 1.933 + return serializable; 1.934 + }, 1.935 + 1.936 + /** 1.937 + * Returns a value that changes only when one of the properties of a Download 1.938 + * object that should be saved into a file also change. This excludes 1.939 + * properties whose value doesn't usually change during the download lifetime. 1.940 + * 1.941 + * This function is used to determine whether the download should be 1.942 + * serialized after a property change notification has been received. 1.943 + * 1.944 + * @return String representing the relevant download state. 1.945 + */ 1.946 + getSerializationHash: function () 1.947 + { 1.948 + // The "succeeded", "canceled", "error", and startTime properties are not 1.949 + // taken into account because they all change before the "stopped" property 1.950 + // changes, and are not altered in other cases. 1.951 + return this.stopped + "," + this.totalBytes + "," + this.hasPartialData + 1.952 + "," + this.contentType; 1.953 + }, 1.954 +}; 1.955 + 1.956 +/** 1.957 + * Defines which properties of the Download object are serializable. 1.958 + */ 1.959 +const kSerializableDownloadProperties = [ 1.960 + "succeeded", 1.961 + "canceled", 1.962 + "error", 1.963 + "totalBytes", 1.964 + "hasPartialData", 1.965 + "tryToKeepPartialData", 1.966 + "launcherPath", 1.967 + "launchWhenSucceeded", 1.968 + "contentType", 1.969 +]; 1.970 + 1.971 +/** 1.972 + * Creates a new Download object from a serializable representation. This 1.973 + * function is used by the createDownload method of Downloads.jsm when a new 1.974 + * Download object is requested, thus some properties may refer to live objects 1.975 + * in place of their serializable representations. 1.976 + * 1.977 + * @param aSerializable 1.978 + * An object with the following fields: 1.979 + * { 1.980 + * source: DownloadSource object, or its serializable representation. 1.981 + * See DownloadSource.fromSerializable for details. 1.982 + * target: DownloadTarget object, or its serializable representation. 1.983 + * See DownloadTarget.fromSerializable for details. 1.984 + * saver: Serializable representation of a DownloadSaver object. See 1.985 + * DownloadSaver.fromSerializable for details. If omitted, 1.986 + * defaults to "copy". 1.987 + * } 1.988 + * 1.989 + * @return The newly created Download object. 1.990 + */ 1.991 +Download.fromSerializable = function (aSerializable) { 1.992 + let download = new Download(); 1.993 + if (aSerializable.source instanceof DownloadSource) { 1.994 + download.source = aSerializable.source; 1.995 + } else { 1.996 + download.source = DownloadSource.fromSerializable(aSerializable.source); 1.997 + } 1.998 + if (aSerializable.target instanceof DownloadTarget) { 1.999 + download.target = aSerializable.target; 1.1000 + } else { 1.1001 + download.target = DownloadTarget.fromSerializable(aSerializable.target); 1.1002 + } 1.1003 + if ("saver" in aSerializable) { 1.1004 + download.saver = DownloadSaver.fromSerializable(aSerializable.saver); 1.1005 + } else { 1.1006 + download.saver = DownloadSaver.fromSerializable("copy"); 1.1007 + } 1.1008 + download.saver.download = download; 1.1009 + 1.1010 + if ("startTime" in aSerializable) { 1.1011 + let time = aSerializable.startTime.getTime 1.1012 + ? aSerializable.startTime.getTime() 1.1013 + : aSerializable.startTime; 1.1014 + download.startTime = new Date(time); 1.1015 + } 1.1016 + 1.1017 + for (let property of kSerializableDownloadProperties) { 1.1018 + if (property in aSerializable) { 1.1019 + download[property] = aSerializable[property]; 1.1020 + } 1.1021 + } 1.1022 + 1.1023 + deserializeUnknownProperties(download, aSerializable, property => 1.1024 + kSerializableDownloadProperties.indexOf(property) == -1 && 1.1025 + property != "startTime" && 1.1026 + property != "source" && 1.1027 + property != "target" && 1.1028 + property != "saver"); 1.1029 + 1.1030 + return download; 1.1031 +}; 1.1032 + 1.1033 +//////////////////////////////////////////////////////////////////////////////// 1.1034 +//// DownloadSource 1.1035 + 1.1036 +/** 1.1037 + * Represents the source of a download, for example a document or an URI. 1.1038 + */ 1.1039 +this.DownloadSource = function () {} 1.1040 + 1.1041 +this.DownloadSource.prototype = { 1.1042 + /** 1.1043 + * String containing the URI for the download source. 1.1044 + */ 1.1045 + url: null, 1.1046 + 1.1047 + /** 1.1048 + * Indicates whether the download originated from a private window. This 1.1049 + * determines the context of the network request that is made to retrieve the 1.1050 + * resource. 1.1051 + */ 1.1052 + isPrivate: false, 1.1053 + 1.1054 + /** 1.1055 + * String containing the referrer URI of the download source, or null if no 1.1056 + * referrer should be sent or the download source is not HTTP. 1.1057 + */ 1.1058 + referrer: null, 1.1059 + 1.1060 + /** 1.1061 + * Returns a static representation of the current object state. 1.1062 + * 1.1063 + * @return A JavaScript object that can be serialized to JSON. 1.1064 + */ 1.1065 + toSerializable: function () 1.1066 + { 1.1067 + // Simplify the representation if we don't have other details. 1.1068 + if (!this.isPrivate && !this.referrer && !this._unknownProperties) { 1.1069 + return this.url; 1.1070 + } 1.1071 + 1.1072 + let serializable = { url: this.url }; 1.1073 + if (this.isPrivate) { 1.1074 + serializable.isPrivate = true; 1.1075 + } 1.1076 + if (this.referrer) { 1.1077 + serializable.referrer = this.referrer; 1.1078 + } 1.1079 + 1.1080 + serializeUnknownProperties(this, serializable); 1.1081 + return serializable; 1.1082 + }, 1.1083 +}; 1.1084 + 1.1085 +/** 1.1086 + * Creates a new DownloadSource object from its serializable representation. 1.1087 + * 1.1088 + * @param aSerializable 1.1089 + * Serializable representation of a DownloadSource object. This may be a 1.1090 + * string containing the URI for the download source, an nsIURI, or an 1.1091 + * object with the following properties: 1.1092 + * { 1.1093 + * url: String containing the URI for the download source. 1.1094 + * isPrivate: Indicates whether the download originated from a private 1.1095 + * window. If omitted, the download is public. 1.1096 + * referrer: String containing the referrer URI of the download source. 1.1097 + * Can be omitted or null if no referrer should be sent or 1.1098 + * the download source is not HTTP. 1.1099 + * } 1.1100 + * 1.1101 + * @return The newly created DownloadSource object. 1.1102 + */ 1.1103 +this.DownloadSource.fromSerializable = function (aSerializable) { 1.1104 + let source = new DownloadSource(); 1.1105 + if (isString(aSerializable)) { 1.1106 + // Convert String objects to primitive strings at this point. 1.1107 + source.url = aSerializable.toString(); 1.1108 + } else if (aSerializable instanceof Ci.nsIURI) { 1.1109 + source.url = aSerializable.spec; 1.1110 + } else { 1.1111 + // Convert String objects to primitive strings at this point. 1.1112 + source.url = aSerializable.url.toString(); 1.1113 + if ("isPrivate" in aSerializable) { 1.1114 + source.isPrivate = aSerializable.isPrivate; 1.1115 + } 1.1116 + if ("referrer" in aSerializable) { 1.1117 + source.referrer = aSerializable.referrer; 1.1118 + } 1.1119 + 1.1120 + deserializeUnknownProperties(source, aSerializable, property => 1.1121 + property != "url" && property != "isPrivate" && property != "referrer"); 1.1122 + } 1.1123 + 1.1124 + return source; 1.1125 +}; 1.1126 + 1.1127 +//////////////////////////////////////////////////////////////////////////////// 1.1128 +//// DownloadTarget 1.1129 + 1.1130 +/** 1.1131 + * Represents the target of a download, for example a file in the global 1.1132 + * downloads directory, or a file in the system temporary directory. 1.1133 + */ 1.1134 +this.DownloadTarget = function () {} 1.1135 + 1.1136 +this.DownloadTarget.prototype = { 1.1137 + /** 1.1138 + * String containing the path of the target file. 1.1139 + */ 1.1140 + path: null, 1.1141 + 1.1142 + /** 1.1143 + * String containing the path of the ".part" file containing the data 1.1144 + * downloaded so far, or null to disable the use of a ".part" file to keep 1.1145 + * partially downloaded data. 1.1146 + */ 1.1147 + partFilePath: null, 1.1148 + 1.1149 + /** 1.1150 + * Returns a static representation of the current object state. 1.1151 + * 1.1152 + * @return A JavaScript object that can be serialized to JSON. 1.1153 + */ 1.1154 + toSerializable: function () 1.1155 + { 1.1156 + // Simplify the representation if we don't have other details. 1.1157 + if (!this.partFilePath && !this._unknownProperties) { 1.1158 + return this.path; 1.1159 + } 1.1160 + 1.1161 + let serializable = { path: this.path, 1.1162 + partFilePath: this.partFilePath }; 1.1163 + serializeUnknownProperties(this, serializable); 1.1164 + return serializable; 1.1165 + }, 1.1166 +}; 1.1167 + 1.1168 +/** 1.1169 + * Creates a new DownloadTarget object from its serializable representation. 1.1170 + * 1.1171 + * @param aSerializable 1.1172 + * Serializable representation of a DownloadTarget object. This may be a 1.1173 + * string containing the path of the target file, an nsIFile, or an 1.1174 + * object with the following properties: 1.1175 + * { 1.1176 + * path: String containing the path of the target file. 1.1177 + * partFilePath: optional string containing the part file path. 1.1178 + * } 1.1179 + * 1.1180 + * @return The newly created DownloadTarget object. 1.1181 + */ 1.1182 +this.DownloadTarget.fromSerializable = function (aSerializable) { 1.1183 + let target = new DownloadTarget(); 1.1184 + if (isString(aSerializable)) { 1.1185 + // Convert String objects to primitive strings at this point. 1.1186 + target.path = aSerializable.toString(); 1.1187 + } else if (aSerializable instanceof Ci.nsIFile) { 1.1188 + // Read the "path" property of nsIFile after checking the object type. 1.1189 + target.path = aSerializable.path; 1.1190 + } else { 1.1191 + // Read the "path" property of the serializable DownloadTarget 1.1192 + // representation, converting String objects to primitive strings. 1.1193 + target.path = aSerializable.path.toString(); 1.1194 + if ("partFilePath" in aSerializable) { 1.1195 + target.partFilePath = aSerializable.partFilePath; 1.1196 + } 1.1197 + 1.1198 + deserializeUnknownProperties(target, aSerializable, property => 1.1199 + property != "path" && property != "partFilePath"); 1.1200 + } 1.1201 + return target; 1.1202 +}; 1.1203 + 1.1204 +//////////////////////////////////////////////////////////////////////////////// 1.1205 +//// DownloadError 1.1206 + 1.1207 +/** 1.1208 + * Provides detailed information about a download failure. 1.1209 + * 1.1210 + * @param aProperties 1.1211 + * Object which may contain any of the following properties: 1.1212 + * { 1.1213 + * result: Result error code, defaulting to Cr.NS_ERROR_FAILURE 1.1214 + * message: String error message to be displayed, or null to use the 1.1215 + * message associated with the result code. 1.1216 + * inferCause: If true, attempts to determine if the cause of the 1.1217 + * download is a network failure or a local file failure, 1.1218 + * based on a set of known values of the result code. 1.1219 + * This is useful when the error is received by a 1.1220 + * component that handles both aspects of the download. 1.1221 + * } 1.1222 + * The properties object may also contain any of the DownloadError's 1.1223 + * because properties, which will be set accordingly in the error object. 1.1224 + */ 1.1225 +this.DownloadError = function (aProperties) 1.1226 +{ 1.1227 + const NS_ERROR_MODULE_BASE_OFFSET = 0x45; 1.1228 + const NS_ERROR_MODULE_NETWORK = 6; 1.1229 + const NS_ERROR_MODULE_FILES = 13; 1.1230 + 1.1231 + // Set the error name used by the Error object prototype first. 1.1232 + this.name = "DownloadError"; 1.1233 + this.result = aProperties.result || Cr.NS_ERROR_FAILURE; 1.1234 + if (aProperties.message) { 1.1235 + this.message = aProperties.message; 1.1236 + } else if (aProperties.becauseBlocked || 1.1237 + aProperties.becauseBlockedByParentalControls || 1.1238 + aProperties.becauseBlockedByReputationCheck) { 1.1239 + this.message = "Download blocked."; 1.1240 + } else { 1.1241 + let exception = new Components.Exception("", this.result); 1.1242 + this.message = exception.toString(); 1.1243 + } 1.1244 + if (aProperties.inferCause) { 1.1245 + let module = ((this.result & 0x7FFF0000) >> 16) - 1.1246 + NS_ERROR_MODULE_BASE_OFFSET; 1.1247 + this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK); 1.1248 + this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES); 1.1249 + } 1.1250 + else { 1.1251 + if (aProperties.becauseSourceFailed) { 1.1252 + this.becauseSourceFailed = true; 1.1253 + } 1.1254 + if (aProperties.becauseTargetFailed) { 1.1255 + this.becauseTargetFailed = true; 1.1256 + } 1.1257 + } 1.1258 + 1.1259 + if (aProperties.becauseBlockedByParentalControls) { 1.1260 + this.becauseBlocked = true; 1.1261 + this.becauseBlockedByParentalControls = true; 1.1262 + } else if (aProperties.becauseBlockedByReputationCheck) { 1.1263 + this.becauseBlocked = true; 1.1264 + this.becauseBlockedByReputationCheck = true; 1.1265 + } else if (aProperties.becauseBlocked) { 1.1266 + this.becauseBlocked = true; 1.1267 + } 1.1268 + 1.1269 + this.stack = new Error().stack; 1.1270 +} 1.1271 + 1.1272 +this.DownloadError.prototype = { 1.1273 + __proto__: Error.prototype, 1.1274 + 1.1275 + /** 1.1276 + * The result code associated with this error. 1.1277 + */ 1.1278 + result: false, 1.1279 + 1.1280 + /** 1.1281 + * Indicates an error occurred while reading from the remote location. 1.1282 + */ 1.1283 + becauseSourceFailed: false, 1.1284 + 1.1285 + /** 1.1286 + * Indicates an error occurred while writing to the local target. 1.1287 + */ 1.1288 + becauseTargetFailed: false, 1.1289 + 1.1290 + /** 1.1291 + * Indicates the download failed because it was blocked. If the reason for 1.1292 + * blocking is known, the corresponding property will be also set. 1.1293 + */ 1.1294 + becauseBlocked: false, 1.1295 + 1.1296 + /** 1.1297 + * Indicates the download was blocked because downloads are globally 1.1298 + * disallowed by the Parental Controls or Family Safety features on Windows. 1.1299 + */ 1.1300 + becauseBlockedByParentalControls: false, 1.1301 + 1.1302 + /** 1.1303 + * Indicates the download was blocked because it failed the reputation check 1.1304 + * and may be malware. 1.1305 + */ 1.1306 + becauseBlockedByReputationCheck: false, 1.1307 +}; 1.1308 + 1.1309 +//////////////////////////////////////////////////////////////////////////////// 1.1310 +//// DownloadSaver 1.1311 + 1.1312 +/** 1.1313 + * Template for an object that actually transfers the data for the download. 1.1314 + */ 1.1315 +this.DownloadSaver = function () {} 1.1316 + 1.1317 +this.DownloadSaver.prototype = { 1.1318 + /** 1.1319 + * Download object for raising notifications and reading properties. 1.1320 + * 1.1321 + * If the tryToKeepPartialData property of the download object is false, the 1.1322 + * saver should never try to keep partially downloaded data if the download 1.1323 + * fails. 1.1324 + */ 1.1325 + download: null, 1.1326 + 1.1327 + /** 1.1328 + * Executes the download. 1.1329 + * 1.1330 + * @param aSetProgressBytesFn 1.1331 + * This function may be called by the saver to report progress. It 1.1332 + * takes three arguments: the first is the number of bytes transferred 1.1333 + * until now, the second is the total number of bytes to be 1.1334 + * transferred (or -1 if unknown), the third indicates whether the 1.1335 + * partially downloaded data can be used when restarting the download 1.1336 + * if it fails or is canceled. 1.1337 + * @param aSetPropertiesFn 1.1338 + * This function may be called by the saver to report information 1.1339 + * about new download properties discovered by the saver during the 1.1340 + * download process. It takes an object where the keys represents 1.1341 + * the names of the properties to set, and the value represents the 1.1342 + * value to set. 1.1343 + * 1.1344 + * @return {Promise} 1.1345 + * @resolves When the download has finished successfully. 1.1346 + * @rejects JavaScript exception if the download failed. 1.1347 + */ 1.1348 + execute: function DS_execute(aSetProgressBytesFn, aSetPropertiesFn) 1.1349 + { 1.1350 + throw new Error("Not implemented."); 1.1351 + }, 1.1352 + 1.1353 + /** 1.1354 + * Cancels the download. 1.1355 + */ 1.1356 + cancel: function DS_cancel() 1.1357 + { 1.1358 + throw new Error("Not implemented."); 1.1359 + }, 1.1360 + 1.1361 + /** 1.1362 + * Removes any partial data kept as part of a canceled or failed download. 1.1363 + * 1.1364 + * This method is never called until the promise returned by "execute" is 1.1365 + * either resolved or rejected, and the "execute" method is not called again 1.1366 + * until the promise returned by this method is resolved or rejected. 1.1367 + * 1.1368 + * @return {Promise} 1.1369 + * @resolves When the operation has finished successfully. 1.1370 + * @rejects JavaScript exception. 1.1371 + */ 1.1372 + removePartialData: function DS_removePartialData() 1.1373 + { 1.1374 + return Promise.resolve(); 1.1375 + }, 1.1376 + 1.1377 + /** 1.1378 + * This can be called by the saver implementation when the download is already 1.1379 + * started, to add it to the browsing history. This method has no effect if 1.1380 + * the download is private. 1.1381 + */ 1.1382 + addToHistory: function () 1.1383 + { 1.1384 + if (this.download.source.isPrivate) { 1.1385 + return; 1.1386 + } 1.1387 + 1.1388 + let sourceUri = NetUtil.newURI(this.download.source.url); 1.1389 + let referrer = this.download.source.referrer; 1.1390 + let referrerUri = referrer ? NetUtil.newURI(referrer) : null; 1.1391 + let targetUri = NetUtil.newURI(new FileUtils.File( 1.1392 + this.download.target.path)); 1.1393 + 1.1394 + // The start time is always available when we reach this point. 1.1395 + let startPRTime = this.download.startTime.getTime() * 1000; 1.1396 + 1.1397 + gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime, 1.1398 + targetUri); 1.1399 + }, 1.1400 + 1.1401 + /** 1.1402 + * Returns a static representation of the current object state. 1.1403 + * 1.1404 + * @return A JavaScript object that can be serialized to JSON. 1.1405 + */ 1.1406 + toSerializable: function () 1.1407 + { 1.1408 + throw new Error("Not implemented."); 1.1409 + }, 1.1410 + 1.1411 + /** 1.1412 + * Returns the SHA-256 hash of the downloaded file, if it exists. 1.1413 + */ 1.1414 + getSha256Hash: function () 1.1415 + { 1.1416 + throw new Error("Not implemented."); 1.1417 + }, 1.1418 + 1.1419 + getSignatureInfo: function () 1.1420 + { 1.1421 + throw new Error("Not implemented."); 1.1422 + }, 1.1423 +}; // DownloadSaver 1.1424 + 1.1425 +/** 1.1426 + * Creates a new DownloadSaver object from its serializable representation. 1.1427 + * 1.1428 + * @param aSerializable 1.1429 + * Serializable representation of a DownloadSaver object. If no initial 1.1430 + * state information for the saver object is needed, can be a string 1.1431 + * representing the class of the download operation, for example "copy". 1.1432 + * 1.1433 + * @return The newly created DownloadSaver object. 1.1434 + */ 1.1435 +this.DownloadSaver.fromSerializable = function (aSerializable) { 1.1436 + let serializable = isString(aSerializable) ? { type: aSerializable } 1.1437 + : aSerializable; 1.1438 + let saver; 1.1439 + switch (serializable.type) { 1.1440 + case "copy": 1.1441 + saver = DownloadCopySaver.fromSerializable(serializable); 1.1442 + break; 1.1443 + case "legacy": 1.1444 + saver = DownloadLegacySaver.fromSerializable(serializable); 1.1445 + break; 1.1446 + default: 1.1447 + throw new Error("Unrecoginzed download saver type."); 1.1448 + } 1.1449 + return saver; 1.1450 +}; 1.1451 + 1.1452 +//////////////////////////////////////////////////////////////////////////////// 1.1453 +//// DownloadCopySaver 1.1454 + 1.1455 +/** 1.1456 + * Saver object that simply copies the entire source file to the target. 1.1457 + */ 1.1458 +this.DownloadCopySaver = function () {} 1.1459 + 1.1460 +this.DownloadCopySaver.prototype = { 1.1461 + __proto__: DownloadSaver.prototype, 1.1462 + 1.1463 + /** 1.1464 + * BackgroundFileSaver object currently handling the download. 1.1465 + */ 1.1466 + _backgroundFileSaver: null, 1.1467 + 1.1468 + /** 1.1469 + * Indicates whether the "cancel" method has been called. This is used to 1.1470 + * prevent the request from starting in case the operation is canceled before 1.1471 + * the BackgroundFileSaver instance has been created. 1.1472 + */ 1.1473 + _canceled: false, 1.1474 + 1.1475 + /** 1.1476 + * Save the SHA-256 hash in raw bytes of the downloaded file. This is null 1.1477 + * unless BackgroundFileSaver has successfully completed saving the file. 1.1478 + */ 1.1479 + _sha256Hash: null, 1.1480 + 1.1481 + /** 1.1482 + * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert 1.1483 + * if the file is signed. This is empty if the file is unsigned, and null 1.1484 + * unless BackgroundFileSaver has successfully completed saving the file. 1.1485 + */ 1.1486 + _signatureInfo: null, 1.1487 + 1.1488 + /** 1.1489 + * True if the associated download has already been added to browsing history. 1.1490 + */ 1.1491 + alreadyAddedToHistory: false, 1.1492 + 1.1493 + /** 1.1494 + * String corresponding to the entityID property of the nsIResumableChannel 1.1495 + * used to execute the download, or null if the channel was not resumable or 1.1496 + * the saver was instructed not to keep partially downloaded data. 1.1497 + */ 1.1498 + entityID: null, 1.1499 + 1.1500 + /** 1.1501 + * Implements "DownloadSaver.execute". 1.1502 + */ 1.1503 + execute: function DCS_execute(aSetProgressBytesFn, aSetPropertiesFn) 1.1504 + { 1.1505 + let copySaver = this; 1.1506 + 1.1507 + this._canceled = false; 1.1508 + 1.1509 + let download = this.download; 1.1510 + let targetPath = download.target.path; 1.1511 + let partFilePath = download.target.partFilePath; 1.1512 + let keepPartialData = download.tryToKeepPartialData; 1.1513 + 1.1514 + return Task.spawn(function task_DCS_execute() { 1.1515 + // Add the download to history the first time it is started in this 1.1516 + // session. If the download is restarted in a different session, a new 1.1517 + // history visit will be added. We do this just to avoid the complexity 1.1518 + // of serializing this state between sessions, since adding a new visit 1.1519 + // does not have any noticeable side effect. 1.1520 + if (!this.alreadyAddedToHistory) { 1.1521 + this.addToHistory(); 1.1522 + this.alreadyAddedToHistory = true; 1.1523 + } 1.1524 + 1.1525 + // To reduce the chance that other downloads reuse the same final target 1.1526 + // file name, we should create a placeholder as soon as possible, before 1.1527 + // starting the network request. The placeholder is also required in case 1.1528 + // we are using a ".part" file instead of the final target while the 1.1529 + // download is in progress. 1.1530 + try { 1.1531 + // If the file already exists, don't delete its contents yet. 1.1532 + let file = yield OS.File.open(targetPath, { write: true }); 1.1533 + yield file.close(); 1.1534 + } catch (ex if ex instanceof OS.File.Error) { 1.1535 + // Throw a DownloadError indicating that the operation failed because of 1.1536 + // the target file. We cannot translate this into a specific result 1.1537 + // code, but we preserve the original message using the toString method. 1.1538 + let error = new DownloadError({ message: ex.toString() }); 1.1539 + error.becauseTargetFailed = true; 1.1540 + throw error; 1.1541 + } 1.1542 + 1.1543 + try { 1.1544 + let deferSaveComplete = Promise.defer(); 1.1545 + 1.1546 + if (this._canceled) { 1.1547 + // Don't create the BackgroundFileSaver object if we have been 1.1548 + // canceled meanwhile. 1.1549 + throw new DownloadError({ message: "Saver canceled." }); 1.1550 + } 1.1551 + 1.1552 + // Create the object that will save the file in a background thread. 1.1553 + let backgroundFileSaver = new BackgroundFileSaverStreamListener(); 1.1554 + try { 1.1555 + // When the operation completes, reflect the status in the promise 1.1556 + // returned by this download execution function. 1.1557 + backgroundFileSaver.observer = { 1.1558 + onTargetChange: function () { }, 1.1559 + onSaveComplete: (aSaver, aStatus) => { 1.1560 + // Send notifications now that we can restart if needed. 1.1561 + if (Components.isSuccessCode(aStatus)) { 1.1562 + // Save the hash before freeing backgroundFileSaver. 1.1563 + this._sha256Hash = aSaver.sha256Hash; 1.1564 + this._signatureInfo = aSaver.signatureInfo; 1.1565 + deferSaveComplete.resolve(); 1.1566 + } else { 1.1567 + // Infer the origin of the error from the failure code, because 1.1568 + // BackgroundFileSaver does not provide more specific data. 1.1569 + let properties = { result: aStatus, inferCause: true }; 1.1570 + deferSaveComplete.reject(new DownloadError(properties)); 1.1571 + } 1.1572 + // Free the reference cycle, to release resources earlier. 1.1573 + backgroundFileSaver.observer = null; 1.1574 + this._backgroundFileSaver = null; 1.1575 + }, 1.1576 + }; 1.1577 + 1.1578 + // Create a channel from the source, and listen to progress 1.1579 + // notifications. 1.1580 + let channel = NetUtil.newChannel(NetUtil.newURI(download.source.url)); 1.1581 + if (channel instanceof Ci.nsIPrivateBrowsingChannel) { 1.1582 + channel.setPrivate(download.source.isPrivate); 1.1583 + } 1.1584 + if (channel instanceof Ci.nsIHttpChannel && 1.1585 + download.source.referrer) { 1.1586 + channel.referrer = NetUtil.newURI(download.source.referrer); 1.1587 + } 1.1588 + 1.1589 + // If we have data that we can use to resume the download from where 1.1590 + // it stopped, try to use it. 1.1591 + let resumeAttempted = false; 1.1592 + let resumeFromBytes = 0; 1.1593 + if (channel instanceof Ci.nsIResumableChannel && this.entityID && 1.1594 + partFilePath && keepPartialData) { 1.1595 + try { 1.1596 + let stat = yield OS.File.stat(partFilePath); 1.1597 + channel.resumeAt(stat.size, this.entityID); 1.1598 + resumeAttempted = true; 1.1599 + resumeFromBytes = stat.size; 1.1600 + } catch (ex if ex instanceof OS.File.Error && 1.1601 + ex.becauseNoSuchFile) { } 1.1602 + } 1.1603 + 1.1604 + channel.notificationCallbacks = { 1.1605 + QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]), 1.1606 + getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]), 1.1607 + onProgress: function DCSE_onProgress(aRequest, aContext, aProgress, 1.1608 + aProgressMax) 1.1609 + { 1.1610 + let currentBytes = resumeFromBytes + aProgress; 1.1611 + let totalBytes = aProgressMax == -1 ? -1 : (resumeFromBytes + 1.1612 + aProgressMax); 1.1613 + aSetProgressBytesFn(currentBytes, totalBytes, aProgress > 0 && 1.1614 + partFilePath && keepPartialData); 1.1615 + }, 1.1616 + onStatus: function () { }, 1.1617 + }; 1.1618 + 1.1619 + // Open the channel, directing output to the background file saver. 1.1620 + backgroundFileSaver.QueryInterface(Ci.nsIStreamListener); 1.1621 + channel.asyncOpen({ 1.1622 + onStartRequest: function (aRequest, aContext) { 1.1623 + backgroundFileSaver.onStartRequest(aRequest, aContext); 1.1624 + 1.1625 + // Check if the request's response has been blocked by Windows 1.1626 + // Parental Controls with an HTTP 450 error code. 1.1627 + if (aRequest instanceof Ci.nsIHttpChannel && 1.1628 + aRequest.responseStatus == 450) { 1.1629 + // Set a flag that can be retrieved later when handling the 1.1630 + // cancellation so that the proper error can be thrown. 1.1631 + this.download._blockedByParentalControls = true; 1.1632 + aRequest.cancel(Cr.NS_BINDING_ABORTED); 1.1633 + return; 1.1634 + } 1.1635 + 1.1636 + aSetPropertiesFn({ contentType: channel.contentType }); 1.1637 + 1.1638 + // Ensure we report the value of "Content-Length", if available, 1.1639 + // even if the download doesn't generate any progress events 1.1640 + // later. 1.1641 + if (channel.contentLength >= 0) { 1.1642 + aSetProgressBytesFn(0, channel.contentLength); 1.1643 + } 1.1644 + 1.1645 + // If the URL we are downloading from includes a file extension 1.1646 + // that matches the "Content-Encoding" header, for example ".gz" 1.1647 + // with a "gzip" encoding, we should save the file in its encoded 1.1648 + // form. In all other cases, we decode the body while saving. 1.1649 + if (channel instanceof Ci.nsIEncodedChannel && 1.1650 + channel.contentEncodings) { 1.1651 + let uri = channel.URI; 1.1652 + if (uri instanceof Ci.nsIURL && uri.fileExtension) { 1.1653 + // Only the first, outermost encoding is considered. 1.1654 + let encoding = channel.contentEncodings.getNext(); 1.1655 + if (encoding) { 1.1656 + channel.applyConversion = 1.1657 + gExternalHelperAppService.applyDecodingForExtension( 1.1658 + uri.fileExtension, encoding); 1.1659 + } 1.1660 + } 1.1661 + } 1.1662 + 1.1663 + if (keepPartialData) { 1.1664 + // If the source is not resumable, don't keep partial data even 1.1665 + // if we were asked to try and do it. 1.1666 + if (aRequest instanceof Ci.nsIResumableChannel) { 1.1667 + try { 1.1668 + // If reading the ID succeeds, the source is resumable. 1.1669 + this.entityID = aRequest.entityID; 1.1670 + } catch (ex if ex instanceof Components.Exception && 1.1671 + ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { 1.1672 + keepPartialData = false; 1.1673 + } 1.1674 + } else { 1.1675 + keepPartialData = false; 1.1676 + } 1.1677 + } 1.1678 + 1.1679 + // Enable hashing and signature verification before setting the 1.1680 + // target. 1.1681 + backgroundFileSaver.enableSha256(); 1.1682 + backgroundFileSaver.enableSignatureInfo(); 1.1683 + if (partFilePath) { 1.1684 + // If we actually resumed a request, append to the partial data. 1.1685 + if (resumeAttempted) { 1.1686 + // TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED 1.1687 + backgroundFileSaver.enableAppend(); 1.1688 + } 1.1689 + 1.1690 + // Use a part file, determining if we should keep it on failure. 1.1691 + backgroundFileSaver.setTarget(new FileUtils.File(partFilePath), 1.1692 + keepPartialData); 1.1693 + } else { 1.1694 + // Set the final target file, and delete it on failure. 1.1695 + backgroundFileSaver.setTarget(new FileUtils.File(targetPath), 1.1696 + false); 1.1697 + } 1.1698 + }.bind(copySaver), 1.1699 + 1.1700 + onStopRequest: function (aRequest, aContext, aStatusCode) { 1.1701 + try { 1.1702 + backgroundFileSaver.onStopRequest(aRequest, aContext, 1.1703 + aStatusCode); 1.1704 + } finally { 1.1705 + // If the data transfer completed successfully, indicate to the 1.1706 + // background file saver that the operation can finish. If the 1.1707 + // data transfer failed, the saver has been already stopped. 1.1708 + if (Components.isSuccessCode(aStatusCode)) { 1.1709 + if (partFilePath) { 1.1710 + // Move to the final target if we were using a part file. 1.1711 + backgroundFileSaver.setTarget( 1.1712 + new FileUtils.File(targetPath), false); 1.1713 + } 1.1714 + backgroundFileSaver.finish(Cr.NS_OK); 1.1715 + } 1.1716 + } 1.1717 + }.bind(copySaver), 1.1718 + 1.1719 + onDataAvailable: function (aRequest, aContext, aInputStream, 1.1720 + aOffset, aCount) { 1.1721 + backgroundFileSaver.onDataAvailable(aRequest, aContext, 1.1722 + aInputStream, aOffset, 1.1723 + aCount); 1.1724 + }.bind(copySaver), 1.1725 + }, null); 1.1726 + 1.1727 + // We should check if we have been canceled in the meantime, after 1.1728 + // all the previous asynchronous operations have been executed and 1.1729 + // just before we set the _backgroundFileSaver property. 1.1730 + if (this._canceled) { 1.1731 + throw new DownloadError({ message: "Saver canceled." }); 1.1732 + } 1.1733 + 1.1734 + // If the operation succeeded, store the object to allow cancellation. 1.1735 + this._backgroundFileSaver = backgroundFileSaver; 1.1736 + } catch (ex) { 1.1737 + // In case an error occurs while setting up the chain of objects for 1.1738 + // the download, ensure that we release the resources of the saver. 1.1739 + backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE); 1.1740 + throw ex; 1.1741 + } 1.1742 + 1.1743 + // We will wait on this promise in case no error occurred while setting 1.1744 + // up the chain of objects for the download. 1.1745 + yield deferSaveComplete.promise; 1.1746 + } catch (ex) { 1.1747 + // Ensure we always remove the placeholder for the final target file on 1.1748 + // failure, independently of which code path failed. In some cases, the 1.1749 + // background file saver may have already removed the file. 1.1750 + try { 1.1751 + yield OS.File.remove(targetPath); 1.1752 + } catch (e2) { 1.1753 + // If we failed during the operation, we report the error but use the 1.1754 + // original one as the failure reason of the download. Note that on 1.1755 + // Windows we may get an access denied error instead of a no such file 1.1756 + // error if the file existed before, and was recently deleted. 1.1757 + if (!(e2 instanceof OS.File.Error && 1.1758 + (e2.becauseNoSuchFile || e2.becauseAccessDenied))) { 1.1759 + Cu.reportError(e2); 1.1760 + } 1.1761 + } 1.1762 + throw ex; 1.1763 + } 1.1764 + }.bind(this)); 1.1765 + }, 1.1766 + 1.1767 + /** 1.1768 + * Implements "DownloadSaver.cancel". 1.1769 + */ 1.1770 + cancel: function DCS_cancel() 1.1771 + { 1.1772 + this._canceled = true; 1.1773 + if (this._backgroundFileSaver) { 1.1774 + this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE); 1.1775 + this._backgroundFileSaver = null; 1.1776 + } 1.1777 + }, 1.1778 + 1.1779 + /** 1.1780 + * Implements "DownloadSaver.removePartialData". 1.1781 + */ 1.1782 + removePartialData: function () 1.1783 + { 1.1784 + return Task.spawn(function task_DCS_removePartialData() { 1.1785 + if (this.download.target.partFilePath) { 1.1786 + try { 1.1787 + yield OS.File.remove(this.download.target.partFilePath); 1.1788 + } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { } 1.1789 + } 1.1790 + }.bind(this)); 1.1791 + }, 1.1792 + 1.1793 + /** 1.1794 + * Implements "DownloadSaver.toSerializable". 1.1795 + */ 1.1796 + toSerializable: function () 1.1797 + { 1.1798 + // Simplify the representation if we don't have other details. 1.1799 + if (!this.entityID && !this._unknownProperties) { 1.1800 + return "copy"; 1.1801 + } 1.1802 + 1.1803 + let serializable = { type: "copy", 1.1804 + entityID: this.entityID }; 1.1805 + serializeUnknownProperties(this, serializable); 1.1806 + return serializable; 1.1807 + }, 1.1808 + 1.1809 + /** 1.1810 + * Implements "DownloadSaver.getSha256Hash" 1.1811 + */ 1.1812 + getSha256Hash: function () 1.1813 + { 1.1814 + return this._sha256Hash; 1.1815 + }, 1.1816 + 1.1817 + /* 1.1818 + * Implements DownloadSaver.getSignatureInfo. 1.1819 + */ 1.1820 + getSignatureInfo: function () 1.1821 + { 1.1822 + return this._signatureInfo; 1.1823 + } 1.1824 +}; 1.1825 + 1.1826 +/** 1.1827 + * Creates a new DownloadCopySaver object, with its initial state derived from 1.1828 + * its serializable representation. 1.1829 + * 1.1830 + * @param aSerializable 1.1831 + * Serializable representation of a DownloadCopySaver object. 1.1832 + * 1.1833 + * @return The newly created DownloadCopySaver object. 1.1834 + */ 1.1835 +this.DownloadCopySaver.fromSerializable = function (aSerializable) { 1.1836 + let saver = new DownloadCopySaver(); 1.1837 + if ("entityID" in aSerializable) { 1.1838 + saver.entityID = aSerializable.entityID; 1.1839 + } 1.1840 + 1.1841 + deserializeUnknownProperties(saver, aSerializable, property => 1.1842 + property != "entityID" && property != "type"); 1.1843 + 1.1844 + return saver; 1.1845 +}; 1.1846 + 1.1847 +//////////////////////////////////////////////////////////////////////////////// 1.1848 +//// DownloadLegacySaver 1.1849 + 1.1850 +/** 1.1851 + * Saver object that integrates with the legacy nsITransfer interface. 1.1852 + * 1.1853 + * For more background on the process, see the DownloadLegacyTransfer object. 1.1854 + */ 1.1855 +this.DownloadLegacySaver = function() 1.1856 +{ 1.1857 + this.deferExecuted = Promise.defer(); 1.1858 + this.deferCanceled = Promise.defer(); 1.1859 +} 1.1860 + 1.1861 +this.DownloadLegacySaver.prototype = { 1.1862 + __proto__: DownloadSaver.prototype, 1.1863 + 1.1864 + /** 1.1865 + * Save the SHA-256 hash in raw bytes of the downloaded file. This may be 1.1866 + * null when nsExternalHelperAppService (and thus BackgroundFileSaver) is not 1.1867 + * invoked. 1.1868 + */ 1.1869 + _sha256Hash: null, 1.1870 + 1.1871 + /** 1.1872 + * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert 1.1873 + * if the file is signed. This is empty if the file is unsigned, and null 1.1874 + * unless BackgroundFileSaver has successfully completed saving the file. 1.1875 + */ 1.1876 + _signatureInfo: null, 1.1877 + 1.1878 + /** 1.1879 + * nsIRequest object associated to the status and progress updates we 1.1880 + * received. This object is null before we receive the first status and 1.1881 + * progress update, and is also reset to null when the download is stopped. 1.1882 + */ 1.1883 + request: null, 1.1884 + 1.1885 + /** 1.1886 + * This deferred object contains a promise that is resolved as soon as this 1.1887 + * download finishes successfully, and is rejected in case the download is 1.1888 + * canceled or receives a failure notification through nsITransfer. 1.1889 + */ 1.1890 + deferExecuted: null, 1.1891 + 1.1892 + /** 1.1893 + * This deferred object contains a promise that is resolved if the download 1.1894 + * receives a cancellation request through the "cancel" method, and is never 1.1895 + * rejected. The nsITransfer implementation will register a handler that 1.1896 + * actually causes the download cancellation. 1.1897 + */ 1.1898 + deferCanceled: null, 1.1899 + 1.1900 + /** 1.1901 + * This is populated with the value of the aSetProgressBytesFn argument of the 1.1902 + * "execute" method, and is null before the method is called. 1.1903 + */ 1.1904 + setProgressBytesFn: null, 1.1905 + 1.1906 + /** 1.1907 + * Called by the nsITransfer implementation while the download progresses. 1.1908 + * 1.1909 + * @param aCurrentBytes 1.1910 + * Number of bytes transferred until now. 1.1911 + * @param aTotalBytes 1.1912 + * Total number of bytes to be transferred, or -1 if unknown. 1.1913 + */ 1.1914 + onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes) 1.1915 + { 1.1916 + // Ignore progress notifications until we are ready to process them. 1.1917 + if (!this.setProgressBytesFn) { 1.1918 + return; 1.1919 + } 1.1920 + 1.1921 + let hasPartFile = !!this.download.target.partFilePath; 1.1922 + 1.1923 + this.progressWasNotified = true; 1.1924 + this.setProgressBytesFn(aCurrentBytes, aTotalBytes, 1.1925 + aCurrentBytes > 0 && hasPartFile); 1.1926 + }, 1.1927 + 1.1928 + /** 1.1929 + * Whether the onProgressBytes function has been called at least once. 1.1930 + */ 1.1931 + progressWasNotified: false, 1.1932 + 1.1933 + /** 1.1934 + * Called by the nsITransfer implementation when the request has started. 1.1935 + * 1.1936 + * @param aRequest 1.1937 + * nsIRequest associated to the status update. 1.1938 + * @param aAlreadyAddedToHistory 1.1939 + * Indicates that the nsIExternalHelperAppService component already 1.1940 + * added the download to the browsing history, unless it was started 1.1941 + * from a private browsing window. When this parameter is false, the 1.1942 + * download is added to the browsing history here. Private downloads 1.1943 + * are never added to history even if this parameter is false. 1.1944 + */ 1.1945 + onTransferStarted: function (aRequest, aAlreadyAddedToHistory) 1.1946 + { 1.1947 + // Store the entity ID to use for resuming if required. 1.1948 + if (this.download.tryToKeepPartialData && 1.1949 + aRequest instanceof Ci.nsIResumableChannel) { 1.1950 + try { 1.1951 + // If reading the ID succeeds, the source is resumable. 1.1952 + this.entityID = aRequest.entityID; 1.1953 + } catch (ex if ex instanceof Components.Exception && 1.1954 + ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { } 1.1955 + } 1.1956 + 1.1957 + // For legacy downloads, we must update the referrer at this time. 1.1958 + if (aRequest instanceof Ci.nsIHttpChannel && aRequest.referrer) { 1.1959 + this.download.source.referrer = aRequest.referrer.spec; 1.1960 + } 1.1961 + 1.1962 + if (!aAlreadyAddedToHistory) { 1.1963 + this.addToHistory(); 1.1964 + } 1.1965 + }, 1.1966 + 1.1967 + /** 1.1968 + * Called by the nsITransfer implementation when the request has finished. 1.1969 + * 1.1970 + * @param aRequest 1.1971 + * nsIRequest associated to the status update. 1.1972 + * @param aStatus 1.1973 + * Status code received by the nsITransfer implementation. 1.1974 + */ 1.1975 + onTransferFinished: function DLS_onTransferFinished(aRequest, aStatus) 1.1976 + { 1.1977 + // Store a reference to the request, used when handling completion. 1.1978 + this.request = aRequest; 1.1979 + 1.1980 + if (Components.isSuccessCode(aStatus)) { 1.1981 + this.deferExecuted.resolve(); 1.1982 + } else { 1.1983 + // Infer the origin of the error from the failure code, because more 1.1984 + // specific data is not available through the nsITransfer implementation. 1.1985 + let properties = { result: aStatus, inferCause: true }; 1.1986 + this.deferExecuted.reject(new DownloadError(properties)); 1.1987 + } 1.1988 + }, 1.1989 + 1.1990 + /** 1.1991 + * When the first execution of the download finished, it can be restarted by 1.1992 + * using a DownloadCopySaver object instead of the original legacy component 1.1993 + * that executed the download. 1.1994 + */ 1.1995 + firstExecutionFinished: false, 1.1996 + 1.1997 + /** 1.1998 + * In case the download is restarted after the first execution finished, this 1.1999 + * property contains a reference to the DownloadCopySaver that is executing 1.2000 + * the new download attempt. 1.2001 + */ 1.2002 + copySaver: null, 1.2003 + 1.2004 + /** 1.2005 + * String corresponding to the entityID property of the nsIResumableChannel 1.2006 + * used to execute the download, or null if the channel was not resumable or 1.2007 + * the saver was instructed not to keep partially downloaded data. 1.2008 + */ 1.2009 + entityID: null, 1.2010 + 1.2011 + /** 1.2012 + * Implements "DownloadSaver.execute". 1.2013 + */ 1.2014 + execute: function DLS_execute(aSetProgressBytesFn) 1.2015 + { 1.2016 + // Check if this is not the first execution of the download. The Download 1.2017 + // object guarantees that this function is not re-entered during execution. 1.2018 + if (this.firstExecutionFinished) { 1.2019 + if (!this.copySaver) { 1.2020 + this.copySaver = new DownloadCopySaver(); 1.2021 + this.copySaver.download = this.download; 1.2022 + this.copySaver.entityID = this.entityID; 1.2023 + this.copySaver.alreadyAddedToHistory = true; 1.2024 + } 1.2025 + return this.copySaver.execute.apply(this.copySaver, arguments); 1.2026 + } 1.2027 + 1.2028 + this.setProgressBytesFn = aSetProgressBytesFn; 1.2029 + 1.2030 + return Task.spawn(function task_DLS_execute() { 1.2031 + try { 1.2032 + // Wait for the component that executes the download to finish. 1.2033 + yield this.deferExecuted.promise; 1.2034 + 1.2035 + // At this point, the "request" property has been populated. Ensure we 1.2036 + // report the value of "Content-Length", if available, even if the 1.2037 + // download didn't generate any progress events. 1.2038 + if (!this.progressWasNotified && 1.2039 + this.request instanceof Ci.nsIChannel && 1.2040 + this.request.contentLength >= 0) { 1.2041 + aSetProgressBytesFn(0, this.request.contentLength); 1.2042 + } 1.2043 + 1.2044 + // If the component executing the download provides the path of a 1.2045 + // ".part" file, it means that it expects the listener to move the file 1.2046 + // to its final target path when the download succeeds. In this case, 1.2047 + // an empty ".part" file is created even if no data was received from 1.2048 + // the source. 1.2049 + if (this.download.target.partFilePath) { 1.2050 + yield OS.File.move(this.download.target.partFilePath, 1.2051 + this.download.target.path); 1.2052 + } else { 1.2053 + // The download implementation may not have created the target file if 1.2054 + // no data was received from the source. In this case, ensure that an 1.2055 + // empty file is created as expected. 1.2056 + try { 1.2057 + // This atomic operation is more efficient than an existence check. 1.2058 + let file = yield OS.File.open(this.download.target.path, 1.2059 + { create: true }); 1.2060 + yield file.close(); 1.2061 + } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { } 1.2062 + } 1.2063 + } catch (ex) { 1.2064 + // Ensure we always remove the final target file on failure, 1.2065 + // independently of which code path failed. In some cases, the 1.2066 + // component executing the download may have already removed the file. 1.2067 + try { 1.2068 + yield OS.File.remove(this.download.target.path); 1.2069 + } catch (e2) { 1.2070 + // If we failed during the operation, we report the error but use the 1.2071 + // original one as the failure reason of the download. Note that on 1.2072 + // Windows we may get an access denied error instead of a no such file 1.2073 + // error if the file existed before, and was recently deleted. 1.2074 + if (!(e2 instanceof OS.File.Error && 1.2075 + (e2.becauseNoSuchFile || e2.becauseAccessDenied))) { 1.2076 + Cu.reportError(e2); 1.2077 + } 1.2078 + } 1.2079 + // In case the operation failed, ensure we stop downloading data. Since 1.2080 + // we never re-enter this function, deferCanceled is always available. 1.2081 + this.deferCanceled.resolve(); 1.2082 + throw ex; 1.2083 + } finally { 1.2084 + // We don't need the reference to the request anymore. We must also set 1.2085 + // deferCanceled to null in order to free any indirect references it 1.2086 + // may hold to the request. 1.2087 + this.request = null; 1.2088 + this.deferCanceled = null; 1.2089 + // Allow the download to restart through a DownloadCopySaver. 1.2090 + this.firstExecutionFinished = true; 1.2091 + } 1.2092 + }.bind(this)); 1.2093 + }, 1.2094 + 1.2095 + /** 1.2096 + * Implements "DownloadSaver.cancel". 1.2097 + */ 1.2098 + cancel: function DLS_cancel() 1.2099 + { 1.2100 + // We may be using a DownloadCopySaver to handle resuming. 1.2101 + if (this.copySaver) { 1.2102 + return this.copySaver.cancel.apply(this.copySaver, arguments); 1.2103 + } 1.2104 + 1.2105 + // If the download hasn't stopped already, resolve deferCanceled so that the 1.2106 + // operation is canceled as soon as a cancellation handler is registered. 1.2107 + // Note that the handler might not have been registered yet. 1.2108 + if (this.deferCanceled) { 1.2109 + this.deferCanceled.resolve(); 1.2110 + } 1.2111 + }, 1.2112 + 1.2113 + /** 1.2114 + * Implements "DownloadSaver.removePartialData". 1.2115 + */ 1.2116 + removePartialData: function () 1.2117 + { 1.2118 + // DownloadCopySaver and DownloadLeagcySaver use the same logic for removing 1.2119 + // partially downloaded data, though this implementation isn't shared by 1.2120 + // other saver types, thus it isn't found on their shared prototype. 1.2121 + return DownloadCopySaver.prototype.removePartialData.call(this); 1.2122 + }, 1.2123 + 1.2124 + /** 1.2125 + * Implements "DownloadSaver.toSerializable". 1.2126 + */ 1.2127 + toSerializable: function () 1.2128 + { 1.2129 + // This object depends on legacy components that are created externally, 1.2130 + // thus it cannot be rebuilt during deserialization. To support resuming 1.2131 + // across different browser sessions, this object is transformed into a 1.2132 + // DownloadCopySaver for the purpose of serialization. 1.2133 + return DownloadCopySaver.prototype.toSerializable.call(this); 1.2134 + }, 1.2135 + 1.2136 + /** 1.2137 + * Implements "DownloadSaver.getSha256Hash". 1.2138 + */ 1.2139 + getSha256Hash: function () 1.2140 + { 1.2141 + if (this.copySaver) { 1.2142 + return this.copySaver.getSha256Hash(); 1.2143 + } 1.2144 + return this._sha256Hash; 1.2145 + }, 1.2146 + 1.2147 + /** 1.2148 + * Called by the nsITransfer implementation when the hash is available. 1.2149 + */ 1.2150 + setSha256Hash: function (hash) 1.2151 + { 1.2152 + this._sha256Hash = hash; 1.2153 + }, 1.2154 + 1.2155 + /** 1.2156 + * Implements "DownloadSaver.getSignatureInfo". 1.2157 + */ 1.2158 + getSignatureInfo: function () 1.2159 + { 1.2160 + if (this.copySaver) { 1.2161 + return this.copySaver.getSignatureInfo(); 1.2162 + } 1.2163 + return this._signatureInfo; 1.2164 + }, 1.2165 + 1.2166 + /** 1.2167 + * Called by the nsITransfer implementation when the hash is available. 1.2168 + */ 1.2169 + setSignatureInfo: function (signatureInfo) 1.2170 + { 1.2171 + this._signatureInfo = signatureInfo; 1.2172 + }, 1.2173 +}; 1.2174 + 1.2175 +/** 1.2176 + * Returns a new DownloadLegacySaver object. This saver type has a 1.2177 + * deserializable form only when creating a new object in memory, because it 1.2178 + * cannot be serialized to disk. 1.2179 + */ 1.2180 +this.DownloadLegacySaver.fromSerializable = function () { 1.2181 + return new DownloadLegacySaver(); 1.2182 +};