toolkit/components/jsdownloads/src/DownloadCore.jsm

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 /**
michael@0 8 * This file includes the following constructors and global objects:
michael@0 9 *
michael@0 10 * Download
michael@0 11 * Represents a single download, with associated state and actions. This object
michael@0 12 * is transient, though it can be included in a DownloadList so that it can be
michael@0 13 * managed by the user interface and persisted across sessions.
michael@0 14 *
michael@0 15 * DownloadSource
michael@0 16 * Represents the source of a download, for example a document or an URI.
michael@0 17 *
michael@0 18 * DownloadTarget
michael@0 19 * Represents the target of a download, for example a file in the global
michael@0 20 * downloads directory, or a file in the system temporary directory.
michael@0 21 *
michael@0 22 * DownloadError
michael@0 23 * Provides detailed information about a download failure.
michael@0 24 *
michael@0 25 * DownloadSaver
michael@0 26 * Template for an object that actually transfers the data for the download.
michael@0 27 *
michael@0 28 * DownloadCopySaver
michael@0 29 * Saver object that simply copies the entire source file to the target.
michael@0 30 *
michael@0 31 * DownloadLegacySaver
michael@0 32 * Saver object that integrates with the legacy nsITransfer interface.
michael@0 33 */
michael@0 34
michael@0 35 "use strict";
michael@0 36
michael@0 37 this.EXPORTED_SYMBOLS = [
michael@0 38 "Download",
michael@0 39 "DownloadSource",
michael@0 40 "DownloadTarget",
michael@0 41 "DownloadError",
michael@0 42 "DownloadSaver",
michael@0 43 "DownloadCopySaver",
michael@0 44 "DownloadLegacySaver",
michael@0 45 ];
michael@0 46
michael@0 47 ////////////////////////////////////////////////////////////////////////////////
michael@0 48 //// Globals
michael@0 49
michael@0 50 const Cc = Components.classes;
michael@0 51 const Ci = Components.interfaces;
michael@0 52 const Cu = Components.utils;
michael@0 53 const Cr = Components.results;
michael@0 54
michael@0 55 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 56
michael@0 57 XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
michael@0 58 "resource://gre/modules/DownloadIntegration.jsm");
michael@0 59 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
michael@0 60 "resource://gre/modules/FileUtils.jsm");
michael@0 61 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 62 "resource://gre/modules/NetUtil.jsm");
michael@0 63 XPCOMUtils.defineLazyModuleGetter(this, "OS",
michael@0 64 "resource://gre/modules/osfile.jsm")
michael@0 65 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
michael@0 66 "resource://gre/modules/Promise.jsm");
michael@0 67 XPCOMUtils.defineLazyModuleGetter(this, "Services",
michael@0 68 "resource://gre/modules/Services.jsm");
michael@0 69 XPCOMUtils.defineLazyModuleGetter(this, "Task",
michael@0 70 "resource://gre/modules/Task.jsm");
michael@0 71
michael@0 72 XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
michael@0 73 "@mozilla.org/browser/download-history;1",
michael@0 74 Ci.nsIDownloadHistory);
michael@0 75 XPCOMUtils.defineLazyServiceGetter(this, "gExternalAppLauncher",
michael@0 76 "@mozilla.org/uriloader/external-helper-app-service;1",
michael@0 77 Ci.nsPIExternalAppLauncher);
michael@0 78 XPCOMUtils.defineLazyServiceGetter(this, "gExternalHelperAppService",
michael@0 79 "@mozilla.org/uriloader/external-helper-app-service;1",
michael@0 80 Ci.nsIExternalHelperAppService);
michael@0 81
michael@0 82 const BackgroundFileSaverStreamListener = Components.Constructor(
michael@0 83 "@mozilla.org/network/background-file-saver;1?mode=streamlistener",
michael@0 84 "nsIBackgroundFileSaver");
michael@0 85
michael@0 86 /**
michael@0 87 * Returns true if the given value is a primitive string or a String object.
michael@0 88 */
michael@0 89 function isString(aValue) {
michael@0 90 // We cannot use the "instanceof" operator reliably across module boundaries.
michael@0 91 return (typeof aValue == "string") ||
michael@0 92 (typeof aValue == "object" && "charAt" in aValue);
michael@0 93 }
michael@0 94
michael@0 95 /**
michael@0 96 * Serialize the unknown properties of aObject into aSerializable.
michael@0 97 */
michael@0 98 function serializeUnknownProperties(aObject, aSerializable)
michael@0 99 {
michael@0 100 if (aObject._unknownProperties) {
michael@0 101 for (let property in aObject._unknownProperties) {
michael@0 102 aSerializable[property] = aObject._unknownProperties[property];
michael@0 103 }
michael@0 104 }
michael@0 105 }
michael@0 106
michael@0 107 /**
michael@0 108 * Check for any unknown properties in aSerializable and preserve those in the
michael@0 109 * _unknownProperties field of aObject. aFilterFn is called for each property
michael@0 110 * name of aObject and should return true only for unknown properties.
michael@0 111 */
michael@0 112 function deserializeUnknownProperties(aObject, aSerializable, aFilterFn)
michael@0 113 {
michael@0 114 for (let property in aSerializable) {
michael@0 115 if (aFilterFn(property)) {
michael@0 116 if (!aObject._unknownProperties) {
michael@0 117 aObject._unknownProperties = { };
michael@0 118 }
michael@0 119
michael@0 120 aObject._unknownProperties[property] = aSerializable[property];
michael@0 121 }
michael@0 122 }
michael@0 123 }
michael@0 124
michael@0 125 /**
michael@0 126 * This determines the minimum time interval between updates to the number of
michael@0 127 * bytes transferred, and is a limiting factor to the sequence of readings used
michael@0 128 * in calculating the speed of the download.
michael@0 129 */
michael@0 130 const kProgressUpdateIntervalMs = 400;
michael@0 131
michael@0 132 ////////////////////////////////////////////////////////////////////////////////
michael@0 133 //// Download
michael@0 134
michael@0 135 /**
michael@0 136 * Represents a single download, with associated state and actions. This object
michael@0 137 * is transient, though it can be included in a DownloadList so that it can be
michael@0 138 * managed by the user interface and persisted across sessions.
michael@0 139 */
michael@0 140 this.Download = function ()
michael@0 141 {
michael@0 142 this._deferSucceeded = Promise.defer();
michael@0 143 }
michael@0 144
michael@0 145 this.Download.prototype = {
michael@0 146 /**
michael@0 147 * DownloadSource object associated with this download.
michael@0 148 */
michael@0 149 source: null,
michael@0 150
michael@0 151 /**
michael@0 152 * DownloadTarget object associated with this download.
michael@0 153 */
michael@0 154 target: null,
michael@0 155
michael@0 156 /**
michael@0 157 * DownloadSaver object associated with this download.
michael@0 158 */
michael@0 159 saver: null,
michael@0 160
michael@0 161 /**
michael@0 162 * Indicates that the download never started, has been completed successfully,
michael@0 163 * failed, or has been canceled. This property becomes false when a download
michael@0 164 * is started for the first time, or when a failed or canceled download is
michael@0 165 * restarted.
michael@0 166 */
michael@0 167 stopped: true,
michael@0 168
michael@0 169 /**
michael@0 170 * Indicates that the download has been completed successfully.
michael@0 171 */
michael@0 172 succeeded: false,
michael@0 173
michael@0 174 /**
michael@0 175 * Indicates that the download has been canceled. This property can become
michael@0 176 * true, then it can be reset to false when a canceled download is restarted.
michael@0 177 *
michael@0 178 * This property becomes true as soon as the "cancel" method is called, though
michael@0 179 * the "stopped" property might remain false until the cancellation request
michael@0 180 * has been processed. Temporary files or part files may still exist even if
michael@0 181 * they are expected to be deleted, until the "stopped" property becomes true.
michael@0 182 */
michael@0 183 canceled: false,
michael@0 184
michael@0 185 /**
michael@0 186 * When the download fails, this is set to a DownloadError instance indicating
michael@0 187 * the cause of the failure. If the download has been completed successfully
michael@0 188 * or has been canceled, this property is null. This property is reset to
michael@0 189 * null when a failed download is restarted.
michael@0 190 */
michael@0 191 error: null,
michael@0 192
michael@0 193 /**
michael@0 194 * Indicates the start time of the download. When the download starts,
michael@0 195 * this property is set to a valid Date object. The default value is null
michael@0 196 * before the download starts.
michael@0 197 */
michael@0 198 startTime: null,
michael@0 199
michael@0 200 /**
michael@0 201 * Indicates whether this download's "progress" property is able to report
michael@0 202 * partial progress while the download proceeds, and whether the value in
michael@0 203 * totalBytes is relevant. This depends on the saver and the download source.
michael@0 204 */
michael@0 205 hasProgress: false,
michael@0 206
michael@0 207 /**
michael@0 208 * Progress percent, from 0 to 100. Intermediate values are reported only if
michael@0 209 * hasProgress is true.
michael@0 210 *
michael@0 211 * @note You shouldn't rely on this property being equal to 100 to determine
michael@0 212 * whether the download is completed. You should use the individual
michael@0 213 * state properties instead.
michael@0 214 */
michael@0 215 progress: 0,
michael@0 216
michael@0 217 /**
michael@0 218 * When hasProgress is true, indicates the total number of bytes to be
michael@0 219 * transferred before the download finishes, that can be zero for empty files.
michael@0 220 *
michael@0 221 * When hasProgress is false, this property is always zero.
michael@0 222 */
michael@0 223 totalBytes: 0,
michael@0 224
michael@0 225 /**
michael@0 226 * Number of bytes currently transferred. This value starts at zero, and may
michael@0 227 * be updated regardless of the value of hasProgress.
michael@0 228 *
michael@0 229 * @note You shouldn't rely on this property being equal to totalBytes to
michael@0 230 * determine whether the download is completed. You should use the
michael@0 231 * individual state properties instead.
michael@0 232 */
michael@0 233 currentBytes: 0,
michael@0 234
michael@0 235 /**
michael@0 236 * Fractional number representing the speed of the download, in bytes per
michael@0 237 * second. This value is zero when the download is stopped, and may be
michael@0 238 * updated regardless of the value of hasProgress.
michael@0 239 */
michael@0 240 speed: 0,
michael@0 241
michael@0 242 /**
michael@0 243 * Indicates whether, at this time, there is any partially downloaded data
michael@0 244 * that can be used when restarting a failed or canceled download.
michael@0 245 *
michael@0 246 * This property is relevant while the download is in progress, and also if it
michael@0 247 * failed or has been canceled. If the download has been completed
michael@0 248 * successfully, this property is always false.
michael@0 249 *
michael@0 250 * Whether partial data can actually be retained depends on the saver and the
michael@0 251 * download source, and may not be known before the download is started.
michael@0 252 */
michael@0 253 hasPartialData: false,
michael@0 254
michael@0 255 /**
michael@0 256 * This can be set to a function that is called after other properties change.
michael@0 257 */
michael@0 258 onchange: null,
michael@0 259
michael@0 260 /**
michael@0 261 * This tells if the user has chosen to open/run the downloaded file after
michael@0 262 * download has completed.
michael@0 263 */
michael@0 264 launchWhenSucceeded: false,
michael@0 265
michael@0 266 /**
michael@0 267 * This represents the MIME type of the download.
michael@0 268 */
michael@0 269 contentType: null,
michael@0 270
michael@0 271 /**
michael@0 272 * This indicates the path of the application to be used to launch the file,
michael@0 273 * or null if the file should be launched with the default application.
michael@0 274 */
michael@0 275 launcherPath: null,
michael@0 276
michael@0 277 /**
michael@0 278 * Raises the onchange notification.
michael@0 279 */
michael@0 280 _notifyChange: function D_notifyChange() {
michael@0 281 try {
michael@0 282 if (this.onchange) {
michael@0 283 this.onchange();
michael@0 284 }
michael@0 285 } catch (ex) {
michael@0 286 Cu.reportError(ex);
michael@0 287 }
michael@0 288 },
michael@0 289
michael@0 290 /**
michael@0 291 * The download may be stopped and restarted multiple times before it
michael@0 292 * completes successfully. This may happen if any of the download attempts is
michael@0 293 * canceled or fails.
michael@0 294 *
michael@0 295 * This property contains a promise that is linked to the current attempt, or
michael@0 296 * null if the download is either stopped or in the process of being canceled.
michael@0 297 * If the download restarts, this property is replaced with a new promise.
michael@0 298 *
michael@0 299 * The promise is resolved if the attempt it represents finishes successfully,
michael@0 300 * and rejected if the attempt fails.
michael@0 301 */
michael@0 302 _currentAttempt: null,
michael@0 303
michael@0 304 /**
michael@0 305 * Starts the download for the first time, or restarts a download that failed
michael@0 306 * or has been canceled.
michael@0 307 *
michael@0 308 * Calling this method when the download has been completed successfully has
michael@0 309 * no effect, and the method returns a resolved promise. If the download is
michael@0 310 * in progress, the method returns the same promise as the previous call.
michael@0 311 *
michael@0 312 * If the "cancel" method was called but the cancellation process has not
michael@0 313 * finished yet, this method waits for the cancellation to finish, then
michael@0 314 * restarts the download immediately.
michael@0 315 *
michael@0 316 * @note If you need to start a new download from the same source, rather than
michael@0 317 * restarting a failed or canceled one, you should create a separate
michael@0 318 * Download object with the same source as the current one.
michael@0 319 *
michael@0 320 * @return {Promise}
michael@0 321 * @resolves When the download has finished successfully.
michael@0 322 * @rejects JavaScript exception if the download failed.
michael@0 323 */
michael@0 324 start: function D_start()
michael@0 325 {
michael@0 326 // If the download succeeded, it's the final state, we have nothing to do.
michael@0 327 if (this.succeeded) {
michael@0 328 return Promise.resolve();
michael@0 329 }
michael@0 330
michael@0 331 // If the download already started and hasn't failed or hasn't been
michael@0 332 // canceled, return the same promise as the previous call, allowing the
michael@0 333 // caller to wait for the current attempt to finish.
michael@0 334 if (this._currentAttempt) {
michael@0 335 return this._currentAttempt;
michael@0 336 }
michael@0 337
michael@0 338 // While shutting down or disposing of this object, we prevent the download
michael@0 339 // from returning to be in progress.
michael@0 340 if (this._finalized) {
michael@0 341 return Promise.reject(new DownloadError({
michael@0 342 message: "Cannot start after finalization."}));
michael@0 343 }
michael@0 344
michael@0 345 // Initialize all the status properties for a new or restarted download.
michael@0 346 this.stopped = false;
michael@0 347 this.canceled = false;
michael@0 348 this.error = null;
michael@0 349 this.hasProgress = false;
michael@0 350 this.progress = 0;
michael@0 351 this.totalBytes = 0;
michael@0 352 this.currentBytes = 0;
michael@0 353 this.startTime = new Date();
michael@0 354
michael@0 355 // Create a new deferred object and an associated promise before starting
michael@0 356 // the actual download. We store it on the download as the current attempt.
michael@0 357 let deferAttempt = Promise.defer();
michael@0 358 let currentAttempt = deferAttempt.promise;
michael@0 359 this._currentAttempt = currentAttempt;
michael@0 360
michael@0 361 // Restart the progress and speed calculations from scratch.
michael@0 362 this._lastProgressTimeMs = 0;
michael@0 363
michael@0 364 // This function propagates progress from the DownloadSaver object, unless
michael@0 365 // it comes in late from a download attempt that was replaced by a new one.
michael@0 366 // If the cancellation process for the download has started, then the update
michael@0 367 // is ignored.
michael@0 368 function DS_setProgressBytes(aCurrentBytes, aTotalBytes, aHasPartialData)
michael@0 369 {
michael@0 370 if (this._currentAttempt == currentAttempt) {
michael@0 371 this._setBytes(aCurrentBytes, aTotalBytes, aHasPartialData);
michael@0 372 }
michael@0 373 }
michael@0 374
michael@0 375 // This function propagates download properties from the DownloadSaver
michael@0 376 // object, unless it comes in late from a download attempt that was
michael@0 377 // replaced by a new one. If the cancellation process for the download has
michael@0 378 // started, then the update is ignored.
michael@0 379 function DS_setProperties(aOptions)
michael@0 380 {
michael@0 381 if (this._currentAttempt != currentAttempt) {
michael@0 382 return;
michael@0 383 }
michael@0 384
michael@0 385 let changeMade = false;
michael@0 386
michael@0 387 if ("contentType" in aOptions &&
michael@0 388 this.contentType != aOptions.contentType) {
michael@0 389 this.contentType = aOptions.contentType;
michael@0 390 changeMade = true;
michael@0 391 }
michael@0 392
michael@0 393 if (changeMade) {
michael@0 394 this._notifyChange();
michael@0 395 }
michael@0 396 }
michael@0 397
michael@0 398 // Now that we stored the promise in the download object, we can start the
michael@0 399 // task that will actually execute the download.
michael@0 400 deferAttempt.resolve(Task.spawn(function task_D_start() {
michael@0 401 // Wait upon any pending operation before restarting.
michael@0 402 if (this._promiseCanceled) {
michael@0 403 yield this._promiseCanceled;
michael@0 404 }
michael@0 405 if (this._promiseRemovePartialData) {
michael@0 406 try {
michael@0 407 yield this._promiseRemovePartialData;
michael@0 408 } catch (ex) {
michael@0 409 // Ignore any errors, which are already reported by the original
michael@0 410 // caller of the removePartialData method.
michael@0 411 }
michael@0 412 }
michael@0 413
michael@0 414 // In case the download was restarted while cancellation was in progress,
michael@0 415 // but the previous attempt actually succeeded before cancellation could
michael@0 416 // be processed, it is possible that the download has already finished.
michael@0 417 if (this.succeeded) {
michael@0 418 return;
michael@0 419 }
michael@0 420
michael@0 421 try {
michael@0 422 // Disallow download if parental controls service restricts it.
michael@0 423 if (yield DownloadIntegration.shouldBlockForParentalControls(this)) {
michael@0 424 throw new DownloadError({ becauseBlockedByParentalControls: true });
michael@0 425 }
michael@0 426
michael@0 427 // We should check if we have been canceled in the meantime, after all
michael@0 428 // the previous asynchronous operations have been executed and just
michael@0 429 // before we call the "execute" method of the saver.
michael@0 430 if (this._promiseCanceled) {
michael@0 431 // The exception will become a cancellation in the "catch" block.
michael@0 432 throw undefined;
michael@0 433 }
michael@0 434
michael@0 435 // Execute the actual download through the saver object.
michael@0 436 this._saverExecuting = true;
michael@0 437 yield this.saver.execute(DS_setProgressBytes.bind(this),
michael@0 438 DS_setProperties.bind(this));
michael@0 439
michael@0 440 // Check for application reputation, which requires the entire file to
michael@0 441 // be downloaded. After that, check for the last time if the download
michael@0 442 // has been canceled. Both cases require the target file to be deleted,
michael@0 443 // thus we process both in the same block of code.
michael@0 444 if ((yield DownloadIntegration.shouldBlockForReputationCheck(this)) ||
michael@0 445 this._promiseCanceled) {
michael@0 446 try {
michael@0 447 yield OS.File.remove(this.target.path);
michael@0 448 } catch (ex) {
michael@0 449 Cu.reportError(ex);
michael@0 450 }
michael@0 451 // If this is actually a cancellation, this exception will be changed
michael@0 452 // in the catch block below.
michael@0 453 throw new DownloadError({ becauseBlockedByReputationCheck: true });
michael@0 454 }
michael@0 455
michael@0 456 // Update the status properties for a successful download.
michael@0 457 this.progress = 100;
michael@0 458 this.succeeded = true;
michael@0 459 this.hasPartialData = false;
michael@0 460 } catch (ex) {
michael@0 461 // Fail with a generic status code on cancellation, so that the caller
michael@0 462 // is forced to actually check the status properties to see if the
michael@0 463 // download was canceled or failed because of other reasons.
michael@0 464 if (this._promiseCanceled) {
michael@0 465 throw new DownloadError({ message: "Download canceled." });
michael@0 466 }
michael@0 467
michael@0 468 // An HTTP 450 error code is used by Windows to indicate that a uri is
michael@0 469 // blocked by parental controls. This will prevent the download from
michael@0 470 // occuring, so an error needs to be raised. This is not performed
michael@0 471 // during the parental controls check above as it requires the request
michael@0 472 // to start.
michael@0 473 if (this._blockedByParentalControls) {
michael@0 474 ex = new DownloadError({ becauseBlockedByParentalControls: true });
michael@0 475 }
michael@0 476
michael@0 477 // Update the download error, unless a new attempt already started. The
michael@0 478 // change in the status property is notified in the finally block.
michael@0 479 if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
michael@0 480 this.error = ex;
michael@0 481 }
michael@0 482 throw ex;
michael@0 483 } finally {
michael@0 484 // Any cancellation request has now been processed.
michael@0 485 this._saverExecuting = false;
michael@0 486 this._promiseCanceled = null;
michael@0 487
michael@0 488 // Update the status properties, unless a new attempt already started.
michael@0 489 if (this._currentAttempt == currentAttempt || !this._currentAttempt) {
michael@0 490 this._currentAttempt = null;
michael@0 491 this.stopped = true;
michael@0 492 this.speed = 0;
michael@0 493 this._notifyChange();
michael@0 494 if (this.succeeded) {
michael@0 495 yield DownloadIntegration.downloadDone(this);
michael@0 496
michael@0 497 this._deferSucceeded.resolve();
michael@0 498
michael@0 499 if (this.launchWhenSucceeded) {
michael@0 500 this.launch().then(null, Cu.reportError);
michael@0 501
michael@0 502 // Always schedule files to be deleted at the end of the private browsing
michael@0 503 // mode, regardless of the value of the pref.
michael@0 504 if (this.source.isPrivate) {
michael@0 505 gExternalAppLauncher.deleteTemporaryPrivateFileWhenPossible(
michael@0 506 new FileUtils.File(this.target.path));
michael@0 507 } else if (Services.prefs.getBoolPref(
michael@0 508 "browser.helperApps.deleteTempFileOnExit")) {
michael@0 509 gExternalAppLauncher.deleteTemporaryFileOnExit(
michael@0 510 new FileUtils.File(this.target.path));
michael@0 511 }
michael@0 512 }
michael@0 513 }
michael@0 514 }
michael@0 515 }
michael@0 516 }.bind(this)));
michael@0 517
michael@0 518 // Notify the new download state before returning.
michael@0 519 this._notifyChange();
michael@0 520 return currentAttempt;
michael@0 521 },
michael@0 522
michael@0 523 /*
michael@0 524 * Launches the file after download has completed. This can open
michael@0 525 * the file with the default application for the target MIME type
michael@0 526 * or file extension, or with a custom application if launcherPath
michael@0 527 * is set.
michael@0 528 *
michael@0 529 * @return {Promise}
michael@0 530 * @resolves When the instruction to launch the file has been
michael@0 531 * successfully given to the operating system. Note that
michael@0 532 * the OS might still take a while until the file is actually
michael@0 533 * launched.
michael@0 534 * @rejects JavaScript exception if there was an error trying to launch
michael@0 535 * the file.
michael@0 536 */
michael@0 537 launch: function() {
michael@0 538 if (!this.succeeded) {
michael@0 539 return Promise.reject(
michael@0 540 new Error("launch can only be called if the download succeeded")
michael@0 541 );
michael@0 542 }
michael@0 543
michael@0 544 return DownloadIntegration.launchDownload(this);
michael@0 545 },
michael@0 546
michael@0 547 /*
michael@0 548 * Shows the folder containing the target file, or where the target file
michael@0 549 * will be saved. This may be called at any time, even if the download
michael@0 550 * failed or is currently in progress.
michael@0 551 *
michael@0 552 * @return {Promise}
michael@0 553 * @resolves When the instruction to open the containing folder has been
michael@0 554 * successfully given to the operating system. Note that
michael@0 555 * the OS might still take a while until the folder is actually
michael@0 556 * opened.
michael@0 557 * @rejects JavaScript exception if there was an error trying to open
michael@0 558 * the containing folder.
michael@0 559 */
michael@0 560 showContainingDirectory: function D_showContainingDirectory() {
michael@0 561 return DownloadIntegration.showContainingDirectory(this.target.path);
michael@0 562 },
michael@0 563
michael@0 564 /**
michael@0 565 * When a request to cancel the download is received, contains a promise that
michael@0 566 * will be resolved when the cancellation request is processed. When the
michael@0 567 * request is processed, this property becomes null again.
michael@0 568 */
michael@0 569 _promiseCanceled: null,
michael@0 570
michael@0 571 /**
michael@0 572 * True between the call to the "execute" method of the saver and the
michael@0 573 * completion of the current download attempt.
michael@0 574 */
michael@0 575 _saverExecuting: false,
michael@0 576
michael@0 577 /**
michael@0 578 * Cancels the download.
michael@0 579 *
michael@0 580 * The cancellation request is asynchronous. Until the cancellation process
michael@0 581 * finishes, temporary files or part files may still exist even if they are
michael@0 582 * expected to be deleted.
michael@0 583 *
michael@0 584 * In case the download completes successfully before the cancellation request
michael@0 585 * could be processed, this method has no effect, and it returns a resolved
michael@0 586 * promise. You should check the properties of the download at the time the
michael@0 587 * returned promise is resolved to determine if the download was cancelled.
michael@0 588 *
michael@0 589 * Calling this method when the download has been completed successfully,
michael@0 590 * failed, or has been canceled has no effect, and the method returns a
michael@0 591 * resolved promise. This behavior is designed for the case where the call
michael@0 592 * to "cancel" happens asynchronously, and is consistent with the case where
michael@0 593 * the cancellation request could not be processed in time.
michael@0 594 *
michael@0 595 * @return {Promise}
michael@0 596 * @resolves When the cancellation process has finished.
michael@0 597 * @rejects Never.
michael@0 598 */
michael@0 599 cancel: function D_cancel()
michael@0 600 {
michael@0 601 // If the download is currently stopped, we have nothing to do.
michael@0 602 if (this.stopped) {
michael@0 603 return Promise.resolve();
michael@0 604 }
michael@0 605
michael@0 606 if (!this._promiseCanceled) {
michael@0 607 // Start a new cancellation request.
michael@0 608 let deferCanceled = Promise.defer();
michael@0 609 this._currentAttempt.then(function () deferCanceled.resolve(),
michael@0 610 function () deferCanceled.resolve());
michael@0 611 this._promiseCanceled = deferCanceled.promise;
michael@0 612
michael@0 613 // The download can already be restarted.
michael@0 614 this._currentAttempt = null;
michael@0 615
michael@0 616 // Notify that the cancellation request was received.
michael@0 617 this.canceled = true;
michael@0 618 this._notifyChange();
michael@0 619
michael@0 620 // Execute the actual cancellation through the saver object, in case it
michael@0 621 // has already started. Otherwise, the cancellation will be handled just
michael@0 622 // before the saver is started.
michael@0 623 if (this._saverExecuting) {
michael@0 624 this.saver.cancel();
michael@0 625 }
michael@0 626 }
michael@0 627
michael@0 628 return this._promiseCanceled;
michael@0 629 },
michael@0 630
michael@0 631 /**
michael@0 632 * Indicates whether any partially downloaded data should be retained, to use
michael@0 633 * when restarting a failed or canceled download. The default is false.
michael@0 634 *
michael@0 635 * Whether partial data can actually be retained depends on the saver and the
michael@0 636 * download source, and may not be known before the download is started.
michael@0 637 *
michael@0 638 * To have any effect, this property must be set before starting the download.
michael@0 639 * Resetting this property to false after the download has already started
michael@0 640 * will not remove any partial data.
michael@0 641 *
michael@0 642 * If this property is set to true, care should be taken that partial data is
michael@0 643 * removed before the reference to the download is discarded. This can be
michael@0 644 * done using the removePartialData or the "finalize" methods.
michael@0 645 */
michael@0 646 tryToKeepPartialData: false,
michael@0 647
michael@0 648 /**
michael@0 649 * When a request to remove partially downloaded data is received, contains a
michael@0 650 * promise that will be resolved when the removal request is processed. When
michael@0 651 * the request is processed, this property becomes null again.
michael@0 652 */
michael@0 653 _promiseRemovePartialData: null,
michael@0 654
michael@0 655 /**
michael@0 656 * Removes any partial data kept as part of a canceled or failed download.
michael@0 657 *
michael@0 658 * If the download is not canceled or failed, this method has no effect, and
michael@0 659 * it returns a resolved promise. If the "cancel" method was called but the
michael@0 660 * cancellation process has not finished yet, this method waits for the
michael@0 661 * cancellation to finish, then removes the partial data.
michael@0 662 *
michael@0 663 * After this method has been called, if the tryToKeepPartialData property is
michael@0 664 * still true when the download is restarted, partial data will be retained
michael@0 665 * during the new download attempt.
michael@0 666 *
michael@0 667 * @return {Promise}
michael@0 668 * @resolves When the partial data has been successfully removed.
michael@0 669 * @rejects JavaScript exception if the operation could not be completed.
michael@0 670 */
michael@0 671 removePartialData: function ()
michael@0 672 {
michael@0 673 if (!this.canceled && !this.error) {
michael@0 674 return Promise.resolve();
michael@0 675 }
michael@0 676
michael@0 677 let promiseRemovePartialData = this._promiseRemovePartialData;
michael@0 678
michael@0 679 if (!promiseRemovePartialData) {
michael@0 680 let deferRemovePartialData = Promise.defer();
michael@0 681 promiseRemovePartialData = deferRemovePartialData.promise;
michael@0 682 this._promiseRemovePartialData = promiseRemovePartialData;
michael@0 683
michael@0 684 deferRemovePartialData.resolve(
michael@0 685 Task.spawn(function task_D_removePartialData() {
michael@0 686 try {
michael@0 687 // Wait upon any pending cancellation request.
michael@0 688 if (this._promiseCanceled) {
michael@0 689 yield this._promiseCanceled;
michael@0 690 }
michael@0 691 // Ask the saver object to remove any partial data.
michael@0 692 yield this.saver.removePartialData();
michael@0 693 // For completeness, clear the number of bytes transferred.
michael@0 694 if (this.currentBytes != 0 || this.hasPartialData) {
michael@0 695 this.currentBytes = 0;
michael@0 696 this.hasPartialData = false;
michael@0 697 this._notifyChange();
michael@0 698 }
michael@0 699 } finally {
michael@0 700 this._promiseRemovePartialData = null;
michael@0 701 }
michael@0 702 }.bind(this)));
michael@0 703 }
michael@0 704
michael@0 705 return promiseRemovePartialData;
michael@0 706 },
michael@0 707
michael@0 708 /**
michael@0 709 * This deferred object contains a promise that is resolved as soon as this
michael@0 710 * download finishes successfully, and is never rejected. This property is
michael@0 711 * initialized when the download is created, and never changes.
michael@0 712 */
michael@0 713 _deferSucceeded: null,
michael@0 714
michael@0 715 /**
michael@0 716 * Returns a promise that is resolved as soon as this download finishes
michael@0 717 * successfully, even if the download was stopped and restarted meanwhile.
michael@0 718 *
michael@0 719 * You can use this property for scheduling download completion actions in the
michael@0 720 * current session, for downloads that are controlled interactively. If the
michael@0 721 * download is not controlled interactively, you should use the promise
michael@0 722 * returned by the "start" method instead, to check for success or failure.
michael@0 723 *
michael@0 724 * @return {Promise}
michael@0 725 * @resolves When the download has finished successfully.
michael@0 726 * @rejects Never.
michael@0 727 */
michael@0 728 whenSucceeded: function D_whenSucceeded()
michael@0 729 {
michael@0 730 return this._deferSucceeded.promise;
michael@0 731 },
michael@0 732
michael@0 733 /**
michael@0 734 * Updates the state of a finished, failed, or canceled download based on the
michael@0 735 * current state in the file system. If the download is in progress or it has
michael@0 736 * been finalized, this method has no effect, and it returns a resolved
michael@0 737 * promise.
michael@0 738 *
michael@0 739 * This allows the properties of the download to be updated in case the user
michael@0 740 * moved or deleted the target file or its associated ".part" file.
michael@0 741 *
michael@0 742 * @return {Promise}
michael@0 743 * @resolves When the operation has completed.
michael@0 744 * @rejects Never.
michael@0 745 */
michael@0 746 refresh: function ()
michael@0 747 {
michael@0 748 return Task.spawn(function () {
michael@0 749 if (!this.stopped || this._finalized) {
michael@0 750 return;
michael@0 751 }
michael@0 752
michael@0 753 // Update the current progress from disk if we retained partial data.
michael@0 754 if (this.hasPartialData && this.target.partFilePath) {
michael@0 755 let stat = yield OS.File.stat(this.target.partFilePath);
michael@0 756
michael@0 757 // Ignore the result if the state has changed meanwhile.
michael@0 758 if (!this.stopped || this._finalized) {
michael@0 759 return;
michael@0 760 }
michael@0 761
michael@0 762 // Update the bytes transferred and the related progress properties.
michael@0 763 this.currentBytes = stat.size;
michael@0 764 if (this.totalBytes > 0) {
michael@0 765 this.hasProgress = true;
michael@0 766 this.progress = Math.floor(this.currentBytes /
michael@0 767 this.totalBytes * 100);
michael@0 768 }
michael@0 769 this._notifyChange();
michael@0 770 }
michael@0 771 }.bind(this)).then(null, Cu.reportError);
michael@0 772 },
michael@0 773
michael@0 774 /**
michael@0 775 * True if the "finalize" method has been called. This prevents the download
michael@0 776 * from starting again after having been stopped.
michael@0 777 */
michael@0 778 _finalized: false,
michael@0 779
michael@0 780 /**
michael@0 781 * Ensures that the download is stopped, and optionally removes any partial
michael@0 782 * data kept as part of a canceled or failed download. After this method has
michael@0 783 * been called, the download cannot be started again.
michael@0 784 *
michael@0 785 * This method should be used in place of "cancel" and removePartialData while
michael@0 786 * shutting down or disposing of the download object, to prevent other callers
michael@0 787 * from interfering with the operation. This is required because cancellation
michael@0 788 * and other operations are asynchronous.
michael@0 789 *
michael@0 790 * @param aRemovePartialData
michael@0 791 * Whether any partially downloaded data should be removed after the
michael@0 792 * download has been stopped.
michael@0 793 *
michael@0 794 * @return {Promise}
michael@0 795 * @resolves When the operation has finished successfully.
michael@0 796 * @rejects JavaScript exception if an error occurred while removing the
michael@0 797 * partially downloaded data.
michael@0 798 */
michael@0 799 finalize: function (aRemovePartialData)
michael@0 800 {
michael@0 801 // Prevents the download from starting again after having been stopped.
michael@0 802 this._finalized = true;
michael@0 803
michael@0 804 if (aRemovePartialData) {
michael@0 805 // Cancel the download, in case it is currently in progress, then remove
michael@0 806 // any partially downloaded data. The removal operation waits for
michael@0 807 // cancellation to be completed before resolving the promise it returns.
michael@0 808 this.cancel();
michael@0 809 return this.removePartialData();
michael@0 810 } else {
michael@0 811 // Just cancel the download, in case it is currently in progress.
michael@0 812 return this.cancel();
michael@0 813 }
michael@0 814 },
michael@0 815
michael@0 816 /**
michael@0 817 * Indicates the time of the last progress notification, expressed as the
michael@0 818 * number of milliseconds since January 1, 1970, 00:00:00 UTC. This is zero
michael@0 819 * until some bytes have actually been transferred.
michael@0 820 */
michael@0 821 _lastProgressTimeMs: 0,
michael@0 822
michael@0 823 /**
michael@0 824 * Updates progress notifications based on the number of bytes transferred.
michael@0 825 *
michael@0 826 * The number of bytes transferred is not updated unless enough time passed
michael@0 827 * since this function was last called. This limits the computation load, in
michael@0 828 * particular when the listeners update the user interface in response.
michael@0 829 *
michael@0 830 * @param aCurrentBytes
michael@0 831 * Number of bytes transferred until now.
michael@0 832 * @param aTotalBytes
michael@0 833 * Total number of bytes to be transferred, or -1 if unknown.
michael@0 834 * @param aHasPartialData
michael@0 835 * Indicates whether the partially downloaded data can be used when
michael@0 836 * restarting the download if it fails or is canceled.
michael@0 837 */
michael@0 838 _setBytes: function D_setBytes(aCurrentBytes, aTotalBytes, aHasPartialData) {
michael@0 839 let changeMade = (this.hasPartialData != aHasPartialData);
michael@0 840 this.hasPartialData = aHasPartialData;
michael@0 841
michael@0 842 // Unless aTotalBytes is -1, we can report partial download progress. In
michael@0 843 // this case, notify when the related properties changed since last time.
michael@0 844 if (aTotalBytes != -1 && (!this.hasProgress ||
michael@0 845 this.totalBytes != aTotalBytes)) {
michael@0 846 this.hasProgress = true;
michael@0 847 this.totalBytes = aTotalBytes;
michael@0 848 changeMade = true;
michael@0 849 }
michael@0 850
michael@0 851 // Updating the progress and computing the speed require that enough time
michael@0 852 // passed since the last update, or that we haven't started throttling yet.
michael@0 853 let currentTimeMs = Date.now();
michael@0 854 let intervalMs = currentTimeMs - this._lastProgressTimeMs;
michael@0 855 if (intervalMs >= kProgressUpdateIntervalMs) {
michael@0 856 // Don't compute the speed unless we started throttling notifications.
michael@0 857 if (this._lastProgressTimeMs != 0) {
michael@0 858 // Calculate the speed in bytes per second.
michael@0 859 let rawSpeed = (aCurrentBytes - this.currentBytes) / intervalMs * 1000;
michael@0 860 if (this.speed == 0) {
michael@0 861 // When the previous speed is exactly zero instead of a fractional
michael@0 862 // number, this can be considered the first element of the series.
michael@0 863 this.speed = rawSpeed;
michael@0 864 } else {
michael@0 865 // Apply exponential smoothing, with a smoothing factor of 0.1.
michael@0 866 this.speed = rawSpeed * 0.1 + this.speed * 0.9;
michael@0 867 }
michael@0 868 }
michael@0 869
michael@0 870 // Start throttling notifications only when we have actually received some
michael@0 871 // bytes for the first time. The timing of the first part of the download
michael@0 872 // is not reliable, due to possible latency in the initial notifications.
michael@0 873 // This also allows automated tests to receive and verify the number of
michael@0 874 // bytes initially transferred.
michael@0 875 if (aCurrentBytes > 0) {
michael@0 876 this._lastProgressTimeMs = currentTimeMs;
michael@0 877
michael@0 878 // Update the progress now that we don't need its previous value.
michael@0 879 this.currentBytes = aCurrentBytes;
michael@0 880 if (this.totalBytes > 0) {
michael@0 881 this.progress = Math.floor(this.currentBytes / this.totalBytes * 100);
michael@0 882 }
michael@0 883 changeMade = true;
michael@0 884 }
michael@0 885 }
michael@0 886
michael@0 887 if (changeMade) {
michael@0 888 this._notifyChange();
michael@0 889 }
michael@0 890 },
michael@0 891
michael@0 892 /**
michael@0 893 * Returns a static representation of the current object state.
michael@0 894 *
michael@0 895 * @return A JavaScript object that can be serialized to JSON.
michael@0 896 */
michael@0 897 toSerializable: function ()
michael@0 898 {
michael@0 899 let serializable = {
michael@0 900 source: this.source.toSerializable(),
michael@0 901 target: this.target.toSerializable(),
michael@0 902 };
michael@0 903
michael@0 904 // Simplify the representation for the most common saver type. If the saver
michael@0 905 // is an object instead of a simple string, we can't simplify it because we
michael@0 906 // need to persist all its properties, not only "type". This may happen for
michael@0 907 // savers of type "copy" as well as other types.
michael@0 908 let saver = this.saver.toSerializable();
michael@0 909 if (saver !== "copy") {
michael@0 910 serializable.saver = saver;
michael@0 911 }
michael@0 912
michael@0 913 if (this.error && ("message" in this.error)) {
michael@0 914 serializable.error = { message: this.error.message };
michael@0 915 }
michael@0 916
michael@0 917 if (this.startTime) {
michael@0 918 serializable.startTime = this.startTime.toJSON();
michael@0 919 }
michael@0 920
michael@0 921 // These are serialized unless they are false, null, or empty strings.
michael@0 922 for (let property of kSerializableDownloadProperties) {
michael@0 923 if (property != "error" && property != "startTime" && this[property]) {
michael@0 924 serializable[property] = this[property];
michael@0 925 }
michael@0 926 }
michael@0 927
michael@0 928 serializeUnknownProperties(this, serializable);
michael@0 929
michael@0 930 return serializable;
michael@0 931 },
michael@0 932
michael@0 933 /**
michael@0 934 * Returns a value that changes only when one of the properties of a Download
michael@0 935 * object that should be saved into a file also change. This excludes
michael@0 936 * properties whose value doesn't usually change during the download lifetime.
michael@0 937 *
michael@0 938 * This function is used to determine whether the download should be
michael@0 939 * serialized after a property change notification has been received.
michael@0 940 *
michael@0 941 * @return String representing the relevant download state.
michael@0 942 */
michael@0 943 getSerializationHash: function ()
michael@0 944 {
michael@0 945 // The "succeeded", "canceled", "error", and startTime properties are not
michael@0 946 // taken into account because they all change before the "stopped" property
michael@0 947 // changes, and are not altered in other cases.
michael@0 948 return this.stopped + "," + this.totalBytes + "," + this.hasPartialData +
michael@0 949 "," + this.contentType;
michael@0 950 },
michael@0 951 };
michael@0 952
michael@0 953 /**
michael@0 954 * Defines which properties of the Download object are serializable.
michael@0 955 */
michael@0 956 const kSerializableDownloadProperties = [
michael@0 957 "succeeded",
michael@0 958 "canceled",
michael@0 959 "error",
michael@0 960 "totalBytes",
michael@0 961 "hasPartialData",
michael@0 962 "tryToKeepPartialData",
michael@0 963 "launcherPath",
michael@0 964 "launchWhenSucceeded",
michael@0 965 "contentType",
michael@0 966 ];
michael@0 967
michael@0 968 /**
michael@0 969 * Creates a new Download object from a serializable representation. This
michael@0 970 * function is used by the createDownload method of Downloads.jsm when a new
michael@0 971 * Download object is requested, thus some properties may refer to live objects
michael@0 972 * in place of their serializable representations.
michael@0 973 *
michael@0 974 * @param aSerializable
michael@0 975 * An object with the following fields:
michael@0 976 * {
michael@0 977 * source: DownloadSource object, or its serializable representation.
michael@0 978 * See DownloadSource.fromSerializable for details.
michael@0 979 * target: DownloadTarget object, or its serializable representation.
michael@0 980 * See DownloadTarget.fromSerializable for details.
michael@0 981 * saver: Serializable representation of a DownloadSaver object. See
michael@0 982 * DownloadSaver.fromSerializable for details. If omitted,
michael@0 983 * defaults to "copy".
michael@0 984 * }
michael@0 985 *
michael@0 986 * @return The newly created Download object.
michael@0 987 */
michael@0 988 Download.fromSerializable = function (aSerializable) {
michael@0 989 let download = new Download();
michael@0 990 if (aSerializable.source instanceof DownloadSource) {
michael@0 991 download.source = aSerializable.source;
michael@0 992 } else {
michael@0 993 download.source = DownloadSource.fromSerializable(aSerializable.source);
michael@0 994 }
michael@0 995 if (aSerializable.target instanceof DownloadTarget) {
michael@0 996 download.target = aSerializable.target;
michael@0 997 } else {
michael@0 998 download.target = DownloadTarget.fromSerializable(aSerializable.target);
michael@0 999 }
michael@0 1000 if ("saver" in aSerializable) {
michael@0 1001 download.saver = DownloadSaver.fromSerializable(aSerializable.saver);
michael@0 1002 } else {
michael@0 1003 download.saver = DownloadSaver.fromSerializable("copy");
michael@0 1004 }
michael@0 1005 download.saver.download = download;
michael@0 1006
michael@0 1007 if ("startTime" in aSerializable) {
michael@0 1008 let time = aSerializable.startTime.getTime
michael@0 1009 ? aSerializable.startTime.getTime()
michael@0 1010 : aSerializable.startTime;
michael@0 1011 download.startTime = new Date(time);
michael@0 1012 }
michael@0 1013
michael@0 1014 for (let property of kSerializableDownloadProperties) {
michael@0 1015 if (property in aSerializable) {
michael@0 1016 download[property] = aSerializable[property];
michael@0 1017 }
michael@0 1018 }
michael@0 1019
michael@0 1020 deserializeUnknownProperties(download, aSerializable, property =>
michael@0 1021 kSerializableDownloadProperties.indexOf(property) == -1 &&
michael@0 1022 property != "startTime" &&
michael@0 1023 property != "source" &&
michael@0 1024 property != "target" &&
michael@0 1025 property != "saver");
michael@0 1026
michael@0 1027 return download;
michael@0 1028 };
michael@0 1029
michael@0 1030 ////////////////////////////////////////////////////////////////////////////////
michael@0 1031 //// DownloadSource
michael@0 1032
michael@0 1033 /**
michael@0 1034 * Represents the source of a download, for example a document or an URI.
michael@0 1035 */
michael@0 1036 this.DownloadSource = function () {}
michael@0 1037
michael@0 1038 this.DownloadSource.prototype = {
michael@0 1039 /**
michael@0 1040 * String containing the URI for the download source.
michael@0 1041 */
michael@0 1042 url: null,
michael@0 1043
michael@0 1044 /**
michael@0 1045 * Indicates whether the download originated from a private window. This
michael@0 1046 * determines the context of the network request that is made to retrieve the
michael@0 1047 * resource.
michael@0 1048 */
michael@0 1049 isPrivate: false,
michael@0 1050
michael@0 1051 /**
michael@0 1052 * String containing the referrer URI of the download source, or null if no
michael@0 1053 * referrer should be sent or the download source is not HTTP.
michael@0 1054 */
michael@0 1055 referrer: null,
michael@0 1056
michael@0 1057 /**
michael@0 1058 * Returns a static representation of the current object state.
michael@0 1059 *
michael@0 1060 * @return A JavaScript object that can be serialized to JSON.
michael@0 1061 */
michael@0 1062 toSerializable: function ()
michael@0 1063 {
michael@0 1064 // Simplify the representation if we don't have other details.
michael@0 1065 if (!this.isPrivate && !this.referrer && !this._unknownProperties) {
michael@0 1066 return this.url;
michael@0 1067 }
michael@0 1068
michael@0 1069 let serializable = { url: this.url };
michael@0 1070 if (this.isPrivate) {
michael@0 1071 serializable.isPrivate = true;
michael@0 1072 }
michael@0 1073 if (this.referrer) {
michael@0 1074 serializable.referrer = this.referrer;
michael@0 1075 }
michael@0 1076
michael@0 1077 serializeUnknownProperties(this, serializable);
michael@0 1078 return serializable;
michael@0 1079 },
michael@0 1080 };
michael@0 1081
michael@0 1082 /**
michael@0 1083 * Creates a new DownloadSource object from its serializable representation.
michael@0 1084 *
michael@0 1085 * @param aSerializable
michael@0 1086 * Serializable representation of a DownloadSource object. This may be a
michael@0 1087 * string containing the URI for the download source, an nsIURI, or an
michael@0 1088 * object with the following properties:
michael@0 1089 * {
michael@0 1090 * url: String containing the URI for the download source.
michael@0 1091 * isPrivate: Indicates whether the download originated from a private
michael@0 1092 * window. If omitted, the download is public.
michael@0 1093 * referrer: String containing the referrer URI of the download source.
michael@0 1094 * Can be omitted or null if no referrer should be sent or
michael@0 1095 * the download source is not HTTP.
michael@0 1096 * }
michael@0 1097 *
michael@0 1098 * @return The newly created DownloadSource object.
michael@0 1099 */
michael@0 1100 this.DownloadSource.fromSerializable = function (aSerializable) {
michael@0 1101 let source = new DownloadSource();
michael@0 1102 if (isString(aSerializable)) {
michael@0 1103 // Convert String objects to primitive strings at this point.
michael@0 1104 source.url = aSerializable.toString();
michael@0 1105 } else if (aSerializable instanceof Ci.nsIURI) {
michael@0 1106 source.url = aSerializable.spec;
michael@0 1107 } else {
michael@0 1108 // Convert String objects to primitive strings at this point.
michael@0 1109 source.url = aSerializable.url.toString();
michael@0 1110 if ("isPrivate" in aSerializable) {
michael@0 1111 source.isPrivate = aSerializable.isPrivate;
michael@0 1112 }
michael@0 1113 if ("referrer" in aSerializable) {
michael@0 1114 source.referrer = aSerializable.referrer;
michael@0 1115 }
michael@0 1116
michael@0 1117 deserializeUnknownProperties(source, aSerializable, property =>
michael@0 1118 property != "url" && property != "isPrivate" && property != "referrer");
michael@0 1119 }
michael@0 1120
michael@0 1121 return source;
michael@0 1122 };
michael@0 1123
michael@0 1124 ////////////////////////////////////////////////////////////////////////////////
michael@0 1125 //// DownloadTarget
michael@0 1126
michael@0 1127 /**
michael@0 1128 * Represents the target of a download, for example a file in the global
michael@0 1129 * downloads directory, or a file in the system temporary directory.
michael@0 1130 */
michael@0 1131 this.DownloadTarget = function () {}
michael@0 1132
michael@0 1133 this.DownloadTarget.prototype = {
michael@0 1134 /**
michael@0 1135 * String containing the path of the target file.
michael@0 1136 */
michael@0 1137 path: null,
michael@0 1138
michael@0 1139 /**
michael@0 1140 * String containing the path of the ".part" file containing the data
michael@0 1141 * downloaded so far, or null to disable the use of a ".part" file to keep
michael@0 1142 * partially downloaded data.
michael@0 1143 */
michael@0 1144 partFilePath: null,
michael@0 1145
michael@0 1146 /**
michael@0 1147 * Returns a static representation of the current object state.
michael@0 1148 *
michael@0 1149 * @return A JavaScript object that can be serialized to JSON.
michael@0 1150 */
michael@0 1151 toSerializable: function ()
michael@0 1152 {
michael@0 1153 // Simplify the representation if we don't have other details.
michael@0 1154 if (!this.partFilePath && !this._unknownProperties) {
michael@0 1155 return this.path;
michael@0 1156 }
michael@0 1157
michael@0 1158 let serializable = { path: this.path,
michael@0 1159 partFilePath: this.partFilePath };
michael@0 1160 serializeUnknownProperties(this, serializable);
michael@0 1161 return serializable;
michael@0 1162 },
michael@0 1163 };
michael@0 1164
michael@0 1165 /**
michael@0 1166 * Creates a new DownloadTarget object from its serializable representation.
michael@0 1167 *
michael@0 1168 * @param aSerializable
michael@0 1169 * Serializable representation of a DownloadTarget object. This may be a
michael@0 1170 * string containing the path of the target file, an nsIFile, or an
michael@0 1171 * object with the following properties:
michael@0 1172 * {
michael@0 1173 * path: String containing the path of the target file.
michael@0 1174 * partFilePath: optional string containing the part file path.
michael@0 1175 * }
michael@0 1176 *
michael@0 1177 * @return The newly created DownloadTarget object.
michael@0 1178 */
michael@0 1179 this.DownloadTarget.fromSerializable = function (aSerializable) {
michael@0 1180 let target = new DownloadTarget();
michael@0 1181 if (isString(aSerializable)) {
michael@0 1182 // Convert String objects to primitive strings at this point.
michael@0 1183 target.path = aSerializable.toString();
michael@0 1184 } else if (aSerializable instanceof Ci.nsIFile) {
michael@0 1185 // Read the "path" property of nsIFile after checking the object type.
michael@0 1186 target.path = aSerializable.path;
michael@0 1187 } else {
michael@0 1188 // Read the "path" property of the serializable DownloadTarget
michael@0 1189 // representation, converting String objects to primitive strings.
michael@0 1190 target.path = aSerializable.path.toString();
michael@0 1191 if ("partFilePath" in aSerializable) {
michael@0 1192 target.partFilePath = aSerializable.partFilePath;
michael@0 1193 }
michael@0 1194
michael@0 1195 deserializeUnknownProperties(target, aSerializable, property =>
michael@0 1196 property != "path" && property != "partFilePath");
michael@0 1197 }
michael@0 1198 return target;
michael@0 1199 };
michael@0 1200
michael@0 1201 ////////////////////////////////////////////////////////////////////////////////
michael@0 1202 //// DownloadError
michael@0 1203
michael@0 1204 /**
michael@0 1205 * Provides detailed information about a download failure.
michael@0 1206 *
michael@0 1207 * @param aProperties
michael@0 1208 * Object which may contain any of the following properties:
michael@0 1209 * {
michael@0 1210 * result: Result error code, defaulting to Cr.NS_ERROR_FAILURE
michael@0 1211 * message: String error message to be displayed, or null to use the
michael@0 1212 * message associated with the result code.
michael@0 1213 * inferCause: If true, attempts to determine if the cause of the
michael@0 1214 * download is a network failure or a local file failure,
michael@0 1215 * based on a set of known values of the result code.
michael@0 1216 * This is useful when the error is received by a
michael@0 1217 * component that handles both aspects of the download.
michael@0 1218 * }
michael@0 1219 * The properties object may also contain any of the DownloadError's
michael@0 1220 * because properties, which will be set accordingly in the error object.
michael@0 1221 */
michael@0 1222 this.DownloadError = function (aProperties)
michael@0 1223 {
michael@0 1224 const NS_ERROR_MODULE_BASE_OFFSET = 0x45;
michael@0 1225 const NS_ERROR_MODULE_NETWORK = 6;
michael@0 1226 const NS_ERROR_MODULE_FILES = 13;
michael@0 1227
michael@0 1228 // Set the error name used by the Error object prototype first.
michael@0 1229 this.name = "DownloadError";
michael@0 1230 this.result = aProperties.result || Cr.NS_ERROR_FAILURE;
michael@0 1231 if (aProperties.message) {
michael@0 1232 this.message = aProperties.message;
michael@0 1233 } else if (aProperties.becauseBlocked ||
michael@0 1234 aProperties.becauseBlockedByParentalControls ||
michael@0 1235 aProperties.becauseBlockedByReputationCheck) {
michael@0 1236 this.message = "Download blocked.";
michael@0 1237 } else {
michael@0 1238 let exception = new Components.Exception("", this.result);
michael@0 1239 this.message = exception.toString();
michael@0 1240 }
michael@0 1241 if (aProperties.inferCause) {
michael@0 1242 let module = ((this.result & 0x7FFF0000) >> 16) -
michael@0 1243 NS_ERROR_MODULE_BASE_OFFSET;
michael@0 1244 this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK);
michael@0 1245 this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES);
michael@0 1246 }
michael@0 1247 else {
michael@0 1248 if (aProperties.becauseSourceFailed) {
michael@0 1249 this.becauseSourceFailed = true;
michael@0 1250 }
michael@0 1251 if (aProperties.becauseTargetFailed) {
michael@0 1252 this.becauseTargetFailed = true;
michael@0 1253 }
michael@0 1254 }
michael@0 1255
michael@0 1256 if (aProperties.becauseBlockedByParentalControls) {
michael@0 1257 this.becauseBlocked = true;
michael@0 1258 this.becauseBlockedByParentalControls = true;
michael@0 1259 } else if (aProperties.becauseBlockedByReputationCheck) {
michael@0 1260 this.becauseBlocked = true;
michael@0 1261 this.becauseBlockedByReputationCheck = true;
michael@0 1262 } else if (aProperties.becauseBlocked) {
michael@0 1263 this.becauseBlocked = true;
michael@0 1264 }
michael@0 1265
michael@0 1266 this.stack = new Error().stack;
michael@0 1267 }
michael@0 1268
michael@0 1269 this.DownloadError.prototype = {
michael@0 1270 __proto__: Error.prototype,
michael@0 1271
michael@0 1272 /**
michael@0 1273 * The result code associated with this error.
michael@0 1274 */
michael@0 1275 result: false,
michael@0 1276
michael@0 1277 /**
michael@0 1278 * Indicates an error occurred while reading from the remote location.
michael@0 1279 */
michael@0 1280 becauseSourceFailed: false,
michael@0 1281
michael@0 1282 /**
michael@0 1283 * Indicates an error occurred while writing to the local target.
michael@0 1284 */
michael@0 1285 becauseTargetFailed: false,
michael@0 1286
michael@0 1287 /**
michael@0 1288 * Indicates the download failed because it was blocked. If the reason for
michael@0 1289 * blocking is known, the corresponding property will be also set.
michael@0 1290 */
michael@0 1291 becauseBlocked: false,
michael@0 1292
michael@0 1293 /**
michael@0 1294 * Indicates the download was blocked because downloads are globally
michael@0 1295 * disallowed by the Parental Controls or Family Safety features on Windows.
michael@0 1296 */
michael@0 1297 becauseBlockedByParentalControls: false,
michael@0 1298
michael@0 1299 /**
michael@0 1300 * Indicates the download was blocked because it failed the reputation check
michael@0 1301 * and may be malware.
michael@0 1302 */
michael@0 1303 becauseBlockedByReputationCheck: false,
michael@0 1304 };
michael@0 1305
michael@0 1306 ////////////////////////////////////////////////////////////////////////////////
michael@0 1307 //// DownloadSaver
michael@0 1308
michael@0 1309 /**
michael@0 1310 * Template for an object that actually transfers the data for the download.
michael@0 1311 */
michael@0 1312 this.DownloadSaver = function () {}
michael@0 1313
michael@0 1314 this.DownloadSaver.prototype = {
michael@0 1315 /**
michael@0 1316 * Download object for raising notifications and reading properties.
michael@0 1317 *
michael@0 1318 * If the tryToKeepPartialData property of the download object is false, the
michael@0 1319 * saver should never try to keep partially downloaded data if the download
michael@0 1320 * fails.
michael@0 1321 */
michael@0 1322 download: null,
michael@0 1323
michael@0 1324 /**
michael@0 1325 * Executes the download.
michael@0 1326 *
michael@0 1327 * @param aSetProgressBytesFn
michael@0 1328 * This function may be called by the saver to report progress. It
michael@0 1329 * takes three arguments: the first is the number of bytes transferred
michael@0 1330 * until now, the second is the total number of bytes to be
michael@0 1331 * transferred (or -1 if unknown), the third indicates whether the
michael@0 1332 * partially downloaded data can be used when restarting the download
michael@0 1333 * if it fails or is canceled.
michael@0 1334 * @param aSetPropertiesFn
michael@0 1335 * This function may be called by the saver to report information
michael@0 1336 * about new download properties discovered by the saver during the
michael@0 1337 * download process. It takes an object where the keys represents
michael@0 1338 * the names of the properties to set, and the value represents the
michael@0 1339 * value to set.
michael@0 1340 *
michael@0 1341 * @return {Promise}
michael@0 1342 * @resolves When the download has finished successfully.
michael@0 1343 * @rejects JavaScript exception if the download failed.
michael@0 1344 */
michael@0 1345 execute: function DS_execute(aSetProgressBytesFn, aSetPropertiesFn)
michael@0 1346 {
michael@0 1347 throw new Error("Not implemented.");
michael@0 1348 },
michael@0 1349
michael@0 1350 /**
michael@0 1351 * Cancels the download.
michael@0 1352 */
michael@0 1353 cancel: function DS_cancel()
michael@0 1354 {
michael@0 1355 throw new Error("Not implemented.");
michael@0 1356 },
michael@0 1357
michael@0 1358 /**
michael@0 1359 * Removes any partial data kept as part of a canceled or failed download.
michael@0 1360 *
michael@0 1361 * This method is never called until the promise returned by "execute" is
michael@0 1362 * either resolved or rejected, and the "execute" method is not called again
michael@0 1363 * until the promise returned by this method is resolved or rejected.
michael@0 1364 *
michael@0 1365 * @return {Promise}
michael@0 1366 * @resolves When the operation has finished successfully.
michael@0 1367 * @rejects JavaScript exception.
michael@0 1368 */
michael@0 1369 removePartialData: function DS_removePartialData()
michael@0 1370 {
michael@0 1371 return Promise.resolve();
michael@0 1372 },
michael@0 1373
michael@0 1374 /**
michael@0 1375 * This can be called by the saver implementation when the download is already
michael@0 1376 * started, to add it to the browsing history. This method has no effect if
michael@0 1377 * the download is private.
michael@0 1378 */
michael@0 1379 addToHistory: function ()
michael@0 1380 {
michael@0 1381 if (this.download.source.isPrivate) {
michael@0 1382 return;
michael@0 1383 }
michael@0 1384
michael@0 1385 let sourceUri = NetUtil.newURI(this.download.source.url);
michael@0 1386 let referrer = this.download.source.referrer;
michael@0 1387 let referrerUri = referrer ? NetUtil.newURI(referrer) : null;
michael@0 1388 let targetUri = NetUtil.newURI(new FileUtils.File(
michael@0 1389 this.download.target.path));
michael@0 1390
michael@0 1391 // The start time is always available when we reach this point.
michael@0 1392 let startPRTime = this.download.startTime.getTime() * 1000;
michael@0 1393
michael@0 1394 gDownloadHistory.addDownload(sourceUri, referrerUri, startPRTime,
michael@0 1395 targetUri);
michael@0 1396 },
michael@0 1397
michael@0 1398 /**
michael@0 1399 * Returns a static representation of the current object state.
michael@0 1400 *
michael@0 1401 * @return A JavaScript object that can be serialized to JSON.
michael@0 1402 */
michael@0 1403 toSerializable: function ()
michael@0 1404 {
michael@0 1405 throw new Error("Not implemented.");
michael@0 1406 },
michael@0 1407
michael@0 1408 /**
michael@0 1409 * Returns the SHA-256 hash of the downloaded file, if it exists.
michael@0 1410 */
michael@0 1411 getSha256Hash: function ()
michael@0 1412 {
michael@0 1413 throw new Error("Not implemented.");
michael@0 1414 },
michael@0 1415
michael@0 1416 getSignatureInfo: function ()
michael@0 1417 {
michael@0 1418 throw new Error("Not implemented.");
michael@0 1419 },
michael@0 1420 }; // DownloadSaver
michael@0 1421
michael@0 1422 /**
michael@0 1423 * Creates a new DownloadSaver object from its serializable representation.
michael@0 1424 *
michael@0 1425 * @param aSerializable
michael@0 1426 * Serializable representation of a DownloadSaver object. If no initial
michael@0 1427 * state information for the saver object is needed, can be a string
michael@0 1428 * representing the class of the download operation, for example "copy".
michael@0 1429 *
michael@0 1430 * @return The newly created DownloadSaver object.
michael@0 1431 */
michael@0 1432 this.DownloadSaver.fromSerializable = function (aSerializable) {
michael@0 1433 let serializable = isString(aSerializable) ? { type: aSerializable }
michael@0 1434 : aSerializable;
michael@0 1435 let saver;
michael@0 1436 switch (serializable.type) {
michael@0 1437 case "copy":
michael@0 1438 saver = DownloadCopySaver.fromSerializable(serializable);
michael@0 1439 break;
michael@0 1440 case "legacy":
michael@0 1441 saver = DownloadLegacySaver.fromSerializable(serializable);
michael@0 1442 break;
michael@0 1443 default:
michael@0 1444 throw new Error("Unrecoginzed download saver type.");
michael@0 1445 }
michael@0 1446 return saver;
michael@0 1447 };
michael@0 1448
michael@0 1449 ////////////////////////////////////////////////////////////////////////////////
michael@0 1450 //// DownloadCopySaver
michael@0 1451
michael@0 1452 /**
michael@0 1453 * Saver object that simply copies the entire source file to the target.
michael@0 1454 */
michael@0 1455 this.DownloadCopySaver = function () {}
michael@0 1456
michael@0 1457 this.DownloadCopySaver.prototype = {
michael@0 1458 __proto__: DownloadSaver.prototype,
michael@0 1459
michael@0 1460 /**
michael@0 1461 * BackgroundFileSaver object currently handling the download.
michael@0 1462 */
michael@0 1463 _backgroundFileSaver: null,
michael@0 1464
michael@0 1465 /**
michael@0 1466 * Indicates whether the "cancel" method has been called. This is used to
michael@0 1467 * prevent the request from starting in case the operation is canceled before
michael@0 1468 * the BackgroundFileSaver instance has been created.
michael@0 1469 */
michael@0 1470 _canceled: false,
michael@0 1471
michael@0 1472 /**
michael@0 1473 * Save the SHA-256 hash in raw bytes of the downloaded file. This is null
michael@0 1474 * unless BackgroundFileSaver has successfully completed saving the file.
michael@0 1475 */
michael@0 1476 _sha256Hash: null,
michael@0 1477
michael@0 1478 /**
michael@0 1479 * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
michael@0 1480 * if the file is signed. This is empty if the file is unsigned, and null
michael@0 1481 * unless BackgroundFileSaver has successfully completed saving the file.
michael@0 1482 */
michael@0 1483 _signatureInfo: null,
michael@0 1484
michael@0 1485 /**
michael@0 1486 * True if the associated download has already been added to browsing history.
michael@0 1487 */
michael@0 1488 alreadyAddedToHistory: false,
michael@0 1489
michael@0 1490 /**
michael@0 1491 * String corresponding to the entityID property of the nsIResumableChannel
michael@0 1492 * used to execute the download, or null if the channel was not resumable or
michael@0 1493 * the saver was instructed not to keep partially downloaded data.
michael@0 1494 */
michael@0 1495 entityID: null,
michael@0 1496
michael@0 1497 /**
michael@0 1498 * Implements "DownloadSaver.execute".
michael@0 1499 */
michael@0 1500 execute: function DCS_execute(aSetProgressBytesFn, aSetPropertiesFn)
michael@0 1501 {
michael@0 1502 let copySaver = this;
michael@0 1503
michael@0 1504 this._canceled = false;
michael@0 1505
michael@0 1506 let download = this.download;
michael@0 1507 let targetPath = download.target.path;
michael@0 1508 let partFilePath = download.target.partFilePath;
michael@0 1509 let keepPartialData = download.tryToKeepPartialData;
michael@0 1510
michael@0 1511 return Task.spawn(function task_DCS_execute() {
michael@0 1512 // Add the download to history the first time it is started in this
michael@0 1513 // session. If the download is restarted in a different session, a new
michael@0 1514 // history visit will be added. We do this just to avoid the complexity
michael@0 1515 // of serializing this state between sessions, since adding a new visit
michael@0 1516 // does not have any noticeable side effect.
michael@0 1517 if (!this.alreadyAddedToHistory) {
michael@0 1518 this.addToHistory();
michael@0 1519 this.alreadyAddedToHistory = true;
michael@0 1520 }
michael@0 1521
michael@0 1522 // To reduce the chance that other downloads reuse the same final target
michael@0 1523 // file name, we should create a placeholder as soon as possible, before
michael@0 1524 // starting the network request. The placeholder is also required in case
michael@0 1525 // we are using a ".part" file instead of the final target while the
michael@0 1526 // download is in progress.
michael@0 1527 try {
michael@0 1528 // If the file already exists, don't delete its contents yet.
michael@0 1529 let file = yield OS.File.open(targetPath, { write: true });
michael@0 1530 yield file.close();
michael@0 1531 } catch (ex if ex instanceof OS.File.Error) {
michael@0 1532 // Throw a DownloadError indicating that the operation failed because of
michael@0 1533 // the target file. We cannot translate this into a specific result
michael@0 1534 // code, but we preserve the original message using the toString method.
michael@0 1535 let error = new DownloadError({ message: ex.toString() });
michael@0 1536 error.becauseTargetFailed = true;
michael@0 1537 throw error;
michael@0 1538 }
michael@0 1539
michael@0 1540 try {
michael@0 1541 let deferSaveComplete = Promise.defer();
michael@0 1542
michael@0 1543 if (this._canceled) {
michael@0 1544 // Don't create the BackgroundFileSaver object if we have been
michael@0 1545 // canceled meanwhile.
michael@0 1546 throw new DownloadError({ message: "Saver canceled." });
michael@0 1547 }
michael@0 1548
michael@0 1549 // Create the object that will save the file in a background thread.
michael@0 1550 let backgroundFileSaver = new BackgroundFileSaverStreamListener();
michael@0 1551 try {
michael@0 1552 // When the operation completes, reflect the status in the promise
michael@0 1553 // returned by this download execution function.
michael@0 1554 backgroundFileSaver.observer = {
michael@0 1555 onTargetChange: function () { },
michael@0 1556 onSaveComplete: (aSaver, aStatus) => {
michael@0 1557 // Send notifications now that we can restart if needed.
michael@0 1558 if (Components.isSuccessCode(aStatus)) {
michael@0 1559 // Save the hash before freeing backgroundFileSaver.
michael@0 1560 this._sha256Hash = aSaver.sha256Hash;
michael@0 1561 this._signatureInfo = aSaver.signatureInfo;
michael@0 1562 deferSaveComplete.resolve();
michael@0 1563 } else {
michael@0 1564 // Infer the origin of the error from the failure code, because
michael@0 1565 // BackgroundFileSaver does not provide more specific data.
michael@0 1566 let properties = { result: aStatus, inferCause: true };
michael@0 1567 deferSaveComplete.reject(new DownloadError(properties));
michael@0 1568 }
michael@0 1569 // Free the reference cycle, to release resources earlier.
michael@0 1570 backgroundFileSaver.observer = null;
michael@0 1571 this._backgroundFileSaver = null;
michael@0 1572 },
michael@0 1573 };
michael@0 1574
michael@0 1575 // Create a channel from the source, and listen to progress
michael@0 1576 // notifications.
michael@0 1577 let channel = NetUtil.newChannel(NetUtil.newURI(download.source.url));
michael@0 1578 if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
michael@0 1579 channel.setPrivate(download.source.isPrivate);
michael@0 1580 }
michael@0 1581 if (channel instanceof Ci.nsIHttpChannel &&
michael@0 1582 download.source.referrer) {
michael@0 1583 channel.referrer = NetUtil.newURI(download.source.referrer);
michael@0 1584 }
michael@0 1585
michael@0 1586 // If we have data that we can use to resume the download from where
michael@0 1587 // it stopped, try to use it.
michael@0 1588 let resumeAttempted = false;
michael@0 1589 let resumeFromBytes = 0;
michael@0 1590 if (channel instanceof Ci.nsIResumableChannel && this.entityID &&
michael@0 1591 partFilePath && keepPartialData) {
michael@0 1592 try {
michael@0 1593 let stat = yield OS.File.stat(partFilePath);
michael@0 1594 channel.resumeAt(stat.size, this.entityID);
michael@0 1595 resumeAttempted = true;
michael@0 1596 resumeFromBytes = stat.size;
michael@0 1597 } catch (ex if ex instanceof OS.File.Error &&
michael@0 1598 ex.becauseNoSuchFile) { }
michael@0 1599 }
michael@0 1600
michael@0 1601 channel.notificationCallbacks = {
michael@0 1602 QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
michael@0 1603 getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]),
michael@0 1604 onProgress: function DCSE_onProgress(aRequest, aContext, aProgress,
michael@0 1605 aProgressMax)
michael@0 1606 {
michael@0 1607 let currentBytes = resumeFromBytes + aProgress;
michael@0 1608 let totalBytes = aProgressMax == -1 ? -1 : (resumeFromBytes +
michael@0 1609 aProgressMax);
michael@0 1610 aSetProgressBytesFn(currentBytes, totalBytes, aProgress > 0 &&
michael@0 1611 partFilePath && keepPartialData);
michael@0 1612 },
michael@0 1613 onStatus: function () { },
michael@0 1614 };
michael@0 1615
michael@0 1616 // Open the channel, directing output to the background file saver.
michael@0 1617 backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
michael@0 1618 channel.asyncOpen({
michael@0 1619 onStartRequest: function (aRequest, aContext) {
michael@0 1620 backgroundFileSaver.onStartRequest(aRequest, aContext);
michael@0 1621
michael@0 1622 // Check if the request's response has been blocked by Windows
michael@0 1623 // Parental Controls with an HTTP 450 error code.
michael@0 1624 if (aRequest instanceof Ci.nsIHttpChannel &&
michael@0 1625 aRequest.responseStatus == 450) {
michael@0 1626 // Set a flag that can be retrieved later when handling the
michael@0 1627 // cancellation so that the proper error can be thrown.
michael@0 1628 this.download._blockedByParentalControls = true;
michael@0 1629 aRequest.cancel(Cr.NS_BINDING_ABORTED);
michael@0 1630 return;
michael@0 1631 }
michael@0 1632
michael@0 1633 aSetPropertiesFn({ contentType: channel.contentType });
michael@0 1634
michael@0 1635 // Ensure we report the value of "Content-Length", if available,
michael@0 1636 // even if the download doesn't generate any progress events
michael@0 1637 // later.
michael@0 1638 if (channel.contentLength >= 0) {
michael@0 1639 aSetProgressBytesFn(0, channel.contentLength);
michael@0 1640 }
michael@0 1641
michael@0 1642 // If the URL we are downloading from includes a file extension
michael@0 1643 // that matches the "Content-Encoding" header, for example ".gz"
michael@0 1644 // with a "gzip" encoding, we should save the file in its encoded
michael@0 1645 // form. In all other cases, we decode the body while saving.
michael@0 1646 if (channel instanceof Ci.nsIEncodedChannel &&
michael@0 1647 channel.contentEncodings) {
michael@0 1648 let uri = channel.URI;
michael@0 1649 if (uri instanceof Ci.nsIURL && uri.fileExtension) {
michael@0 1650 // Only the first, outermost encoding is considered.
michael@0 1651 let encoding = channel.contentEncodings.getNext();
michael@0 1652 if (encoding) {
michael@0 1653 channel.applyConversion =
michael@0 1654 gExternalHelperAppService.applyDecodingForExtension(
michael@0 1655 uri.fileExtension, encoding);
michael@0 1656 }
michael@0 1657 }
michael@0 1658 }
michael@0 1659
michael@0 1660 if (keepPartialData) {
michael@0 1661 // If the source is not resumable, don't keep partial data even
michael@0 1662 // if we were asked to try and do it.
michael@0 1663 if (aRequest instanceof Ci.nsIResumableChannel) {
michael@0 1664 try {
michael@0 1665 // If reading the ID succeeds, the source is resumable.
michael@0 1666 this.entityID = aRequest.entityID;
michael@0 1667 } catch (ex if ex instanceof Components.Exception &&
michael@0 1668 ex.result == Cr.NS_ERROR_NOT_RESUMABLE) {
michael@0 1669 keepPartialData = false;
michael@0 1670 }
michael@0 1671 } else {
michael@0 1672 keepPartialData = false;
michael@0 1673 }
michael@0 1674 }
michael@0 1675
michael@0 1676 // Enable hashing and signature verification before setting the
michael@0 1677 // target.
michael@0 1678 backgroundFileSaver.enableSha256();
michael@0 1679 backgroundFileSaver.enableSignatureInfo();
michael@0 1680 if (partFilePath) {
michael@0 1681 // If we actually resumed a request, append to the partial data.
michael@0 1682 if (resumeAttempted) {
michael@0 1683 // TODO: Handle Cr.NS_ERROR_ENTITY_CHANGED
michael@0 1684 backgroundFileSaver.enableAppend();
michael@0 1685 }
michael@0 1686
michael@0 1687 // Use a part file, determining if we should keep it on failure.
michael@0 1688 backgroundFileSaver.setTarget(new FileUtils.File(partFilePath),
michael@0 1689 keepPartialData);
michael@0 1690 } else {
michael@0 1691 // Set the final target file, and delete it on failure.
michael@0 1692 backgroundFileSaver.setTarget(new FileUtils.File(targetPath),
michael@0 1693 false);
michael@0 1694 }
michael@0 1695 }.bind(copySaver),
michael@0 1696
michael@0 1697 onStopRequest: function (aRequest, aContext, aStatusCode) {
michael@0 1698 try {
michael@0 1699 backgroundFileSaver.onStopRequest(aRequest, aContext,
michael@0 1700 aStatusCode);
michael@0 1701 } finally {
michael@0 1702 // If the data transfer completed successfully, indicate to the
michael@0 1703 // background file saver that the operation can finish. If the
michael@0 1704 // data transfer failed, the saver has been already stopped.
michael@0 1705 if (Components.isSuccessCode(aStatusCode)) {
michael@0 1706 if (partFilePath) {
michael@0 1707 // Move to the final target if we were using a part file.
michael@0 1708 backgroundFileSaver.setTarget(
michael@0 1709 new FileUtils.File(targetPath), false);
michael@0 1710 }
michael@0 1711 backgroundFileSaver.finish(Cr.NS_OK);
michael@0 1712 }
michael@0 1713 }
michael@0 1714 }.bind(copySaver),
michael@0 1715
michael@0 1716 onDataAvailable: function (aRequest, aContext, aInputStream,
michael@0 1717 aOffset, aCount) {
michael@0 1718 backgroundFileSaver.onDataAvailable(aRequest, aContext,
michael@0 1719 aInputStream, aOffset,
michael@0 1720 aCount);
michael@0 1721 }.bind(copySaver),
michael@0 1722 }, null);
michael@0 1723
michael@0 1724 // We should check if we have been canceled in the meantime, after
michael@0 1725 // all the previous asynchronous operations have been executed and
michael@0 1726 // just before we set the _backgroundFileSaver property.
michael@0 1727 if (this._canceled) {
michael@0 1728 throw new DownloadError({ message: "Saver canceled." });
michael@0 1729 }
michael@0 1730
michael@0 1731 // If the operation succeeded, store the object to allow cancellation.
michael@0 1732 this._backgroundFileSaver = backgroundFileSaver;
michael@0 1733 } catch (ex) {
michael@0 1734 // In case an error occurs while setting up the chain of objects for
michael@0 1735 // the download, ensure that we release the resources of the saver.
michael@0 1736 backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
michael@0 1737 throw ex;
michael@0 1738 }
michael@0 1739
michael@0 1740 // We will wait on this promise in case no error occurred while setting
michael@0 1741 // up the chain of objects for the download.
michael@0 1742 yield deferSaveComplete.promise;
michael@0 1743 } catch (ex) {
michael@0 1744 // Ensure we always remove the placeholder for the final target file on
michael@0 1745 // failure, independently of which code path failed. In some cases, the
michael@0 1746 // background file saver may have already removed the file.
michael@0 1747 try {
michael@0 1748 yield OS.File.remove(targetPath);
michael@0 1749 } catch (e2) {
michael@0 1750 // If we failed during the operation, we report the error but use the
michael@0 1751 // original one as the failure reason of the download. Note that on
michael@0 1752 // Windows we may get an access denied error instead of a no such file
michael@0 1753 // error if the file existed before, and was recently deleted.
michael@0 1754 if (!(e2 instanceof OS.File.Error &&
michael@0 1755 (e2.becauseNoSuchFile || e2.becauseAccessDenied))) {
michael@0 1756 Cu.reportError(e2);
michael@0 1757 }
michael@0 1758 }
michael@0 1759 throw ex;
michael@0 1760 }
michael@0 1761 }.bind(this));
michael@0 1762 },
michael@0 1763
michael@0 1764 /**
michael@0 1765 * Implements "DownloadSaver.cancel".
michael@0 1766 */
michael@0 1767 cancel: function DCS_cancel()
michael@0 1768 {
michael@0 1769 this._canceled = true;
michael@0 1770 if (this._backgroundFileSaver) {
michael@0 1771 this._backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
michael@0 1772 this._backgroundFileSaver = null;
michael@0 1773 }
michael@0 1774 },
michael@0 1775
michael@0 1776 /**
michael@0 1777 * Implements "DownloadSaver.removePartialData".
michael@0 1778 */
michael@0 1779 removePartialData: function ()
michael@0 1780 {
michael@0 1781 return Task.spawn(function task_DCS_removePartialData() {
michael@0 1782 if (this.download.target.partFilePath) {
michael@0 1783 try {
michael@0 1784 yield OS.File.remove(this.download.target.partFilePath);
michael@0 1785 } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) { }
michael@0 1786 }
michael@0 1787 }.bind(this));
michael@0 1788 },
michael@0 1789
michael@0 1790 /**
michael@0 1791 * Implements "DownloadSaver.toSerializable".
michael@0 1792 */
michael@0 1793 toSerializable: function ()
michael@0 1794 {
michael@0 1795 // Simplify the representation if we don't have other details.
michael@0 1796 if (!this.entityID && !this._unknownProperties) {
michael@0 1797 return "copy";
michael@0 1798 }
michael@0 1799
michael@0 1800 let serializable = { type: "copy",
michael@0 1801 entityID: this.entityID };
michael@0 1802 serializeUnknownProperties(this, serializable);
michael@0 1803 return serializable;
michael@0 1804 },
michael@0 1805
michael@0 1806 /**
michael@0 1807 * Implements "DownloadSaver.getSha256Hash"
michael@0 1808 */
michael@0 1809 getSha256Hash: function ()
michael@0 1810 {
michael@0 1811 return this._sha256Hash;
michael@0 1812 },
michael@0 1813
michael@0 1814 /*
michael@0 1815 * Implements DownloadSaver.getSignatureInfo.
michael@0 1816 */
michael@0 1817 getSignatureInfo: function ()
michael@0 1818 {
michael@0 1819 return this._signatureInfo;
michael@0 1820 }
michael@0 1821 };
michael@0 1822
michael@0 1823 /**
michael@0 1824 * Creates a new DownloadCopySaver object, with its initial state derived from
michael@0 1825 * its serializable representation.
michael@0 1826 *
michael@0 1827 * @param aSerializable
michael@0 1828 * Serializable representation of a DownloadCopySaver object.
michael@0 1829 *
michael@0 1830 * @return The newly created DownloadCopySaver object.
michael@0 1831 */
michael@0 1832 this.DownloadCopySaver.fromSerializable = function (aSerializable) {
michael@0 1833 let saver = new DownloadCopySaver();
michael@0 1834 if ("entityID" in aSerializable) {
michael@0 1835 saver.entityID = aSerializable.entityID;
michael@0 1836 }
michael@0 1837
michael@0 1838 deserializeUnknownProperties(saver, aSerializable, property =>
michael@0 1839 property != "entityID" && property != "type");
michael@0 1840
michael@0 1841 return saver;
michael@0 1842 };
michael@0 1843
michael@0 1844 ////////////////////////////////////////////////////////////////////////////////
michael@0 1845 //// DownloadLegacySaver
michael@0 1846
michael@0 1847 /**
michael@0 1848 * Saver object that integrates with the legacy nsITransfer interface.
michael@0 1849 *
michael@0 1850 * For more background on the process, see the DownloadLegacyTransfer object.
michael@0 1851 */
michael@0 1852 this.DownloadLegacySaver = function()
michael@0 1853 {
michael@0 1854 this.deferExecuted = Promise.defer();
michael@0 1855 this.deferCanceled = Promise.defer();
michael@0 1856 }
michael@0 1857
michael@0 1858 this.DownloadLegacySaver.prototype = {
michael@0 1859 __proto__: DownloadSaver.prototype,
michael@0 1860
michael@0 1861 /**
michael@0 1862 * Save the SHA-256 hash in raw bytes of the downloaded file. This may be
michael@0 1863 * null when nsExternalHelperAppService (and thus BackgroundFileSaver) is not
michael@0 1864 * invoked.
michael@0 1865 */
michael@0 1866 _sha256Hash: null,
michael@0 1867
michael@0 1868 /**
michael@0 1869 * Save the signature info as an nsIArray of nsIX509CertList of nsIX509Cert
michael@0 1870 * if the file is signed. This is empty if the file is unsigned, and null
michael@0 1871 * unless BackgroundFileSaver has successfully completed saving the file.
michael@0 1872 */
michael@0 1873 _signatureInfo: null,
michael@0 1874
michael@0 1875 /**
michael@0 1876 * nsIRequest object associated to the status and progress updates we
michael@0 1877 * received. This object is null before we receive the first status and
michael@0 1878 * progress update, and is also reset to null when the download is stopped.
michael@0 1879 */
michael@0 1880 request: null,
michael@0 1881
michael@0 1882 /**
michael@0 1883 * This deferred object contains a promise that is resolved as soon as this
michael@0 1884 * download finishes successfully, and is rejected in case the download is
michael@0 1885 * canceled or receives a failure notification through nsITransfer.
michael@0 1886 */
michael@0 1887 deferExecuted: null,
michael@0 1888
michael@0 1889 /**
michael@0 1890 * This deferred object contains a promise that is resolved if the download
michael@0 1891 * receives a cancellation request through the "cancel" method, and is never
michael@0 1892 * rejected. The nsITransfer implementation will register a handler that
michael@0 1893 * actually causes the download cancellation.
michael@0 1894 */
michael@0 1895 deferCanceled: null,
michael@0 1896
michael@0 1897 /**
michael@0 1898 * This is populated with the value of the aSetProgressBytesFn argument of the
michael@0 1899 * "execute" method, and is null before the method is called.
michael@0 1900 */
michael@0 1901 setProgressBytesFn: null,
michael@0 1902
michael@0 1903 /**
michael@0 1904 * Called by the nsITransfer implementation while the download progresses.
michael@0 1905 *
michael@0 1906 * @param aCurrentBytes
michael@0 1907 * Number of bytes transferred until now.
michael@0 1908 * @param aTotalBytes
michael@0 1909 * Total number of bytes to be transferred, or -1 if unknown.
michael@0 1910 */
michael@0 1911 onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes)
michael@0 1912 {
michael@0 1913 // Ignore progress notifications until we are ready to process them.
michael@0 1914 if (!this.setProgressBytesFn) {
michael@0 1915 return;
michael@0 1916 }
michael@0 1917
michael@0 1918 let hasPartFile = !!this.download.target.partFilePath;
michael@0 1919
michael@0 1920 this.progressWasNotified = true;
michael@0 1921 this.setProgressBytesFn(aCurrentBytes, aTotalBytes,
michael@0 1922 aCurrentBytes > 0 && hasPartFile);
michael@0 1923 },
michael@0 1924
michael@0 1925 /**
michael@0 1926 * Whether the onProgressBytes function has been called at least once.
michael@0 1927 */
michael@0 1928 progressWasNotified: false,
michael@0 1929
michael@0 1930 /**
michael@0 1931 * Called by the nsITransfer implementation when the request has started.
michael@0 1932 *
michael@0 1933 * @param aRequest
michael@0 1934 * nsIRequest associated to the status update.
michael@0 1935 * @param aAlreadyAddedToHistory
michael@0 1936 * Indicates that the nsIExternalHelperAppService component already
michael@0 1937 * added the download to the browsing history, unless it was started
michael@0 1938 * from a private browsing window. When this parameter is false, the
michael@0 1939 * download is added to the browsing history here. Private downloads
michael@0 1940 * are never added to history even if this parameter is false.
michael@0 1941 */
michael@0 1942 onTransferStarted: function (aRequest, aAlreadyAddedToHistory)
michael@0 1943 {
michael@0 1944 // Store the entity ID to use for resuming if required.
michael@0 1945 if (this.download.tryToKeepPartialData &&
michael@0 1946 aRequest instanceof Ci.nsIResumableChannel) {
michael@0 1947 try {
michael@0 1948 // If reading the ID succeeds, the source is resumable.
michael@0 1949 this.entityID = aRequest.entityID;
michael@0 1950 } catch (ex if ex instanceof Components.Exception &&
michael@0 1951 ex.result == Cr.NS_ERROR_NOT_RESUMABLE) { }
michael@0 1952 }
michael@0 1953
michael@0 1954 // For legacy downloads, we must update the referrer at this time.
michael@0 1955 if (aRequest instanceof Ci.nsIHttpChannel && aRequest.referrer) {
michael@0 1956 this.download.source.referrer = aRequest.referrer.spec;
michael@0 1957 }
michael@0 1958
michael@0 1959 if (!aAlreadyAddedToHistory) {
michael@0 1960 this.addToHistory();
michael@0 1961 }
michael@0 1962 },
michael@0 1963
michael@0 1964 /**
michael@0 1965 * Called by the nsITransfer implementation when the request has finished.
michael@0 1966 *
michael@0 1967 * @param aRequest
michael@0 1968 * nsIRequest associated to the status update.
michael@0 1969 * @param aStatus
michael@0 1970 * Status code received by the nsITransfer implementation.
michael@0 1971 */
michael@0 1972 onTransferFinished: function DLS_onTransferFinished(aRequest, aStatus)
michael@0 1973 {
michael@0 1974 // Store a reference to the request, used when handling completion.
michael@0 1975 this.request = aRequest;
michael@0 1976
michael@0 1977 if (Components.isSuccessCode(aStatus)) {
michael@0 1978 this.deferExecuted.resolve();
michael@0 1979 } else {
michael@0 1980 // Infer the origin of the error from the failure code, because more
michael@0 1981 // specific data is not available through the nsITransfer implementation.
michael@0 1982 let properties = { result: aStatus, inferCause: true };
michael@0 1983 this.deferExecuted.reject(new DownloadError(properties));
michael@0 1984 }
michael@0 1985 },
michael@0 1986
michael@0 1987 /**
michael@0 1988 * When the first execution of the download finished, it can be restarted by
michael@0 1989 * using a DownloadCopySaver object instead of the original legacy component
michael@0 1990 * that executed the download.
michael@0 1991 */
michael@0 1992 firstExecutionFinished: false,
michael@0 1993
michael@0 1994 /**
michael@0 1995 * In case the download is restarted after the first execution finished, this
michael@0 1996 * property contains a reference to the DownloadCopySaver that is executing
michael@0 1997 * the new download attempt.
michael@0 1998 */
michael@0 1999 copySaver: null,
michael@0 2000
michael@0 2001 /**
michael@0 2002 * String corresponding to the entityID property of the nsIResumableChannel
michael@0 2003 * used to execute the download, or null if the channel was not resumable or
michael@0 2004 * the saver was instructed not to keep partially downloaded data.
michael@0 2005 */
michael@0 2006 entityID: null,
michael@0 2007
michael@0 2008 /**
michael@0 2009 * Implements "DownloadSaver.execute".
michael@0 2010 */
michael@0 2011 execute: function DLS_execute(aSetProgressBytesFn)
michael@0 2012 {
michael@0 2013 // Check if this is not the first execution of the download. The Download
michael@0 2014 // object guarantees that this function is not re-entered during execution.
michael@0 2015 if (this.firstExecutionFinished) {
michael@0 2016 if (!this.copySaver) {
michael@0 2017 this.copySaver = new DownloadCopySaver();
michael@0 2018 this.copySaver.download = this.download;
michael@0 2019 this.copySaver.entityID = this.entityID;
michael@0 2020 this.copySaver.alreadyAddedToHistory = true;
michael@0 2021 }
michael@0 2022 return this.copySaver.execute.apply(this.copySaver, arguments);
michael@0 2023 }
michael@0 2024
michael@0 2025 this.setProgressBytesFn = aSetProgressBytesFn;
michael@0 2026
michael@0 2027 return Task.spawn(function task_DLS_execute() {
michael@0 2028 try {
michael@0 2029 // Wait for the component that executes the download to finish.
michael@0 2030 yield this.deferExecuted.promise;
michael@0 2031
michael@0 2032 // At this point, the "request" property has been populated. Ensure we
michael@0 2033 // report the value of "Content-Length", if available, even if the
michael@0 2034 // download didn't generate any progress events.
michael@0 2035 if (!this.progressWasNotified &&
michael@0 2036 this.request instanceof Ci.nsIChannel &&
michael@0 2037 this.request.contentLength >= 0) {
michael@0 2038 aSetProgressBytesFn(0, this.request.contentLength);
michael@0 2039 }
michael@0 2040
michael@0 2041 // If the component executing the download provides the path of a
michael@0 2042 // ".part" file, it means that it expects the listener to move the file
michael@0 2043 // to its final target path when the download succeeds. In this case,
michael@0 2044 // an empty ".part" file is created even if no data was received from
michael@0 2045 // the source.
michael@0 2046 if (this.download.target.partFilePath) {
michael@0 2047 yield OS.File.move(this.download.target.partFilePath,
michael@0 2048 this.download.target.path);
michael@0 2049 } else {
michael@0 2050 // The download implementation may not have created the target file if
michael@0 2051 // no data was received from the source. In this case, ensure that an
michael@0 2052 // empty file is created as expected.
michael@0 2053 try {
michael@0 2054 // This atomic operation is more efficient than an existence check.
michael@0 2055 let file = yield OS.File.open(this.download.target.path,
michael@0 2056 { create: true });
michael@0 2057 yield file.close();
michael@0 2058 } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { }
michael@0 2059 }
michael@0 2060 } catch (ex) {
michael@0 2061 // Ensure we always remove the final target file on failure,
michael@0 2062 // independently of which code path failed. In some cases, the
michael@0 2063 // component executing the download may have already removed the file.
michael@0 2064 try {
michael@0 2065 yield OS.File.remove(this.download.target.path);
michael@0 2066 } catch (e2) {
michael@0 2067 // If we failed during the operation, we report the error but use the
michael@0 2068 // original one as the failure reason of the download. Note that on
michael@0 2069 // Windows we may get an access denied error instead of a no such file
michael@0 2070 // error if the file existed before, and was recently deleted.
michael@0 2071 if (!(e2 instanceof OS.File.Error &&
michael@0 2072 (e2.becauseNoSuchFile || e2.becauseAccessDenied))) {
michael@0 2073 Cu.reportError(e2);
michael@0 2074 }
michael@0 2075 }
michael@0 2076 // In case the operation failed, ensure we stop downloading data. Since
michael@0 2077 // we never re-enter this function, deferCanceled is always available.
michael@0 2078 this.deferCanceled.resolve();
michael@0 2079 throw ex;
michael@0 2080 } finally {
michael@0 2081 // We don't need the reference to the request anymore. We must also set
michael@0 2082 // deferCanceled to null in order to free any indirect references it
michael@0 2083 // may hold to the request.
michael@0 2084 this.request = null;
michael@0 2085 this.deferCanceled = null;
michael@0 2086 // Allow the download to restart through a DownloadCopySaver.
michael@0 2087 this.firstExecutionFinished = true;
michael@0 2088 }
michael@0 2089 }.bind(this));
michael@0 2090 },
michael@0 2091
michael@0 2092 /**
michael@0 2093 * Implements "DownloadSaver.cancel".
michael@0 2094 */
michael@0 2095 cancel: function DLS_cancel()
michael@0 2096 {
michael@0 2097 // We may be using a DownloadCopySaver to handle resuming.
michael@0 2098 if (this.copySaver) {
michael@0 2099 return this.copySaver.cancel.apply(this.copySaver, arguments);
michael@0 2100 }
michael@0 2101
michael@0 2102 // If the download hasn't stopped already, resolve deferCanceled so that the
michael@0 2103 // operation is canceled as soon as a cancellation handler is registered.
michael@0 2104 // Note that the handler might not have been registered yet.
michael@0 2105 if (this.deferCanceled) {
michael@0 2106 this.deferCanceled.resolve();
michael@0 2107 }
michael@0 2108 },
michael@0 2109
michael@0 2110 /**
michael@0 2111 * Implements "DownloadSaver.removePartialData".
michael@0 2112 */
michael@0 2113 removePartialData: function ()
michael@0 2114 {
michael@0 2115 // DownloadCopySaver and DownloadLeagcySaver use the same logic for removing
michael@0 2116 // partially downloaded data, though this implementation isn't shared by
michael@0 2117 // other saver types, thus it isn't found on their shared prototype.
michael@0 2118 return DownloadCopySaver.prototype.removePartialData.call(this);
michael@0 2119 },
michael@0 2120
michael@0 2121 /**
michael@0 2122 * Implements "DownloadSaver.toSerializable".
michael@0 2123 */
michael@0 2124 toSerializable: function ()
michael@0 2125 {
michael@0 2126 // This object depends on legacy components that are created externally,
michael@0 2127 // thus it cannot be rebuilt during deserialization. To support resuming
michael@0 2128 // across different browser sessions, this object is transformed into a
michael@0 2129 // DownloadCopySaver for the purpose of serialization.
michael@0 2130 return DownloadCopySaver.prototype.toSerializable.call(this);
michael@0 2131 },
michael@0 2132
michael@0 2133 /**
michael@0 2134 * Implements "DownloadSaver.getSha256Hash".
michael@0 2135 */
michael@0 2136 getSha256Hash: function ()
michael@0 2137 {
michael@0 2138 if (this.copySaver) {
michael@0 2139 return this.copySaver.getSha256Hash();
michael@0 2140 }
michael@0 2141 return this._sha256Hash;
michael@0 2142 },
michael@0 2143
michael@0 2144 /**
michael@0 2145 * Called by the nsITransfer implementation when the hash is available.
michael@0 2146 */
michael@0 2147 setSha256Hash: function (hash)
michael@0 2148 {
michael@0 2149 this._sha256Hash = hash;
michael@0 2150 },
michael@0 2151
michael@0 2152 /**
michael@0 2153 * Implements "DownloadSaver.getSignatureInfo".
michael@0 2154 */
michael@0 2155 getSignatureInfo: function ()
michael@0 2156 {
michael@0 2157 if (this.copySaver) {
michael@0 2158 return this.copySaver.getSignatureInfo();
michael@0 2159 }
michael@0 2160 return this._signatureInfo;
michael@0 2161 },
michael@0 2162
michael@0 2163 /**
michael@0 2164 * Called by the nsITransfer implementation when the hash is available.
michael@0 2165 */
michael@0 2166 setSignatureInfo: function (signatureInfo)
michael@0 2167 {
michael@0 2168 this._signatureInfo = signatureInfo;
michael@0 2169 },
michael@0 2170 };
michael@0 2171
michael@0 2172 /**
michael@0 2173 * Returns a new DownloadLegacySaver object. This saver type has a
michael@0 2174 * deserializable form only when creating a new object in memory, because it
michael@0 2175 * cannot be serialized to disk.
michael@0 2176 */
michael@0 2177 this.DownloadLegacySaver.fromSerializable = function () {
michael@0 2178 return new DownloadLegacySaver();
michael@0 2179 };

mercurial