/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This component implements the XPCOM interfaces required for integration with
 * the legacy download components.
 *
 * New code is expected to use the "Downloads.jsm" module directly, without
 * going through the interfaces implemented in this XPCOM component.  These
 * interfaces are only maintained for backwards compatibility with components
 * that still work synchronously on the main thread.
 */

"use strict";

// Globals

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                  "resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                  "resource://gre/modules/Promise.jsm");

// DownloadLegacyTransfer

/**
 * nsITransfer implementation that provides a bridge to a Download object.
 *
 * Legacy downloads work differently than the JavaScript implementation.  In the
 * latter, the caller only provides the properties for the Download object and
 * the entire process is handled by the "start" method.  In the legacy
 * implementation, the caller must create a separate object to execute the
 * download, and then make the download visible to the user by hooking it up to
 * an nsITransfer instance.
 *
 * Since nsITransfer instances may be created before the download system is
 * initialized, and initialization as well as other operations are asynchronous,
 * this implementation is able to delay all progress and status notifications it
 * receives until the associated Download object is finally created.
 *
 * Conversely, the DownloadLegacySaver object can also receive execution and
 * cancellation requests asynchronously, before or after it is connected to
 * this nsITransfer instance.  For that reason, those requests are communicated
 * in a potentially deferred way, using promise objects.
 *
 * The component that executes the download implements nsICancelable to receive
 * cancellation requests, but after cancellation it cannot be reused again.
 *
 * Since the components that execute the download may be different and they
 * don't always give consistent results, this bridge takes care of enforcing the
 * expectations, for example by ensuring the target file exists when the
 * download is successful, even if the source has a size of zero bytes.
 */
function DownloadLegacyTransfer()
{
  this._deferDownload = Promise.defer();
}

DownloadLegacyTransfer.prototype = {
  classID: Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"),

  // nsISupports

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                         Ci.nsIWebProgressListener2,
                                         Ci.nsITransfer]),

  // nsIWebProgressListener

  onStateChange: function DLT_onStateChange(aWebProgress, aRequest, aStateFlags,
                                            aStatus)
  {
    if (!Components.isSuccessCode(aStatus)) {
      this._componentFailed = true;
    }

    if ((aStateFlags & Ci.nsIWebProgressListener.STATE_START) &&
        (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {

      let blockedByParentalControls = false;
      // If it is a failed download, aRequest.responseStatus doesn't exist.
      // (missing file on the server, network failure to download)
      try {
        // If the request's response has been blocked by Windows Parental Controls
        // with an HTTP 450 error code, we must cancel the request synchronously.
        blockedByParentalControls = aRequest instanceof Ci.nsIHttpChannel &&
                                      aRequest.responseStatus == 450;
      } catch (e) {
        if (e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
          aRequest.cancel(Cr.NS_BINDING_ABORTED);
        }
      }

      if (blockedByParentalControls) {
        aRequest.cancel(Cr.NS_BINDING_ABORTED);
      }

      // The main request has just started.  Wait for the associated Download
      // object to be available before notifying.
      this._deferDownload.promise.then(download => {
        // If the request was blocked, now that we have the download object we
        // should set a flag that can be retrieved later when handling the
        // cancellation so that the proper error can be thrown.
        if (blockedByParentalControls) {
          download._blockedByParentalControls = true;
        }

        download.saver.onTransferStarted(
                         aRequest,
                         this._cancelable instanceof Ci.nsIHelperAppLauncher);

        // To handle asynchronous cancellation properly, we should hook up the
        // handler only after we have been notified that the main request
        // started.  We will wait until the main request stopped before
        // notifying that the download has been canceled.  Since the request has
        // not completed yet, deferCanceled is guaranteed to be set.
        return download.saver.deferCanceled.promise.then(() => {
          // Only cancel if the object executing the download is still running.
          if (this._cancelable && !this._componentFailed) {
            this._cancelable.cancel(Cr.NS_ERROR_ABORT);
            if (this._cancelable instanceof Ci.nsIWebBrowserPersist) {
              // This component will not send the STATE_STOP notification.
              download.saver.onTransferFinished(aRequest, Cr.NS_ERROR_ABORT);
              this._cancelable = null;
            }
          }
        });
      }).then(null, Cu.reportError);
    } else if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) &&
        (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK)) {
      // The last file has been received, or the download failed.  Wait for the
      // associated Download object to be available before notifying.
      this._deferDownload.promise.then(download => {
        // At this point, the hash has been set and we need to copy it to the
        // DownloadSaver.
        if (Components.isSuccessCode(aStatus)) {
          download.saver.setSha256Hash(this._sha256Hash);
          download.saver.setSignatureInfo(this._signatureInfo);
          download.saver.setRedirects(this._redirects);
        }
        download.saver.onTransferFinished(aRequest, aStatus);
      }).then(null, Cu.reportError);

      // Release the reference to the component executing the download.
      this._cancelable = null;
    }
  },

  onProgressChange: function DLT_onProgressChange(aWebProgress, aRequest,
                                                  aCurSelfProgress,
                                                  aMaxSelfProgress,
                                                  aCurTotalProgress,
                                                  aMaxTotalProgress)
  {
    this.onProgressChange64(aWebProgress, aRequest, aCurSelfProgress,
                            aMaxSelfProgress, aCurTotalProgress,
                            aMaxTotalProgress);
  },

  onLocationChange: function () { },

  onStatusChange: function DLT_onStatusChange(aWebProgress, aRequest, aStatus,
                                              aMessage)
  {
    // The status change may optionally be received in addition to the state
    // change, but if no network request actually started, it is possible that
    // we only receive a status change with an error status code.
    if (!Components.isSuccessCode(aStatus)) {
      this._componentFailed = true;

      // Wait for the associated Download object to be available.
      this._deferDownload.promise.then(function DLT_OSC_onDownload(aDownload) {
        aDownload.saver.onTransferFinished(aRequest, aStatus);
      }).then(null, Cu.reportError);
    }
  },

  onSecurityChange: function () { },

  // nsIWebProgressListener2

  onProgressChange64: function DLT_onProgressChange64(aWebProgress, aRequest,
                                                      aCurSelfProgress,
                                                      aMaxSelfProgress,
                                                      aCurTotalProgress,
                                                      aMaxTotalProgress)
  {
    // Wait for the associated Download object to be available.
    this._deferDownload.promise.then(function DLT_OPC64_onDownload(aDownload) {
      aDownload.saver.onProgressBytes(aCurTotalProgress, aMaxTotalProgress);
    }).then(null, Cu.reportError);
  },

  onRefreshAttempted: function DLT_onRefreshAttempted(aWebProgress, aRefreshURI,
                                                      aMillis, aSameURI)
  {
    // Indicate that refreshes and redirects are allowed by default.  However,
    // note that download components don't usually call this method at all.
    return true;
  },

  // nsITransfer

  init: function DLT_init(aSource, aTarget, aDisplayName, aMIMEInfo, aStartTime,
                          aTempFile, aCancelable, aIsPrivate)
  {
    this._cancelable = aCancelable;

    let launchWhenSucceeded = false, contentType = null, launcherPath = null;

    if (aMIMEInfo instanceof Ci.nsIMIMEInfo) {
      launchWhenSucceeded =
                aMIMEInfo.preferredAction != Ci.nsIMIMEInfo.saveToDisk;
      contentType = aMIMEInfo.type;

      let appHandler = aMIMEInfo.preferredApplicationHandler;
      if (aMIMEInfo.preferredAction == Ci.nsIMIMEInfo.useHelperApp &&
          appHandler instanceof Ci.nsILocalHandlerApp) {
        launcherPath = appHandler.executable.path;
      }
    }

    // Create a new Download object associated to a DownloadLegacySaver, and
    // wait for it to be available.  This operation may cause the entire
    // download system to initialize before the object is created.
    Downloads.createDownload({
      source: { url: aSource.spec, isPrivate: aIsPrivate },
      target: { path: aTarget.QueryInterface(Ci.nsIFileURL).file.path,
                partFilePath: aTempFile && aTempFile.path },
      saver: "legacy",
      launchWhenSucceeded: launchWhenSucceeded,
      contentType: contentType,
      launcherPath: launcherPath
    }).then(function DLT_I_onDownload(aDownload) {
      // Legacy components keep partial data when they use a ".part" file.
      if (aTempFile) {
        aDownload.tryToKeepPartialData = true;
      }

      // Start the download before allowing it to be controlled.  Ignore errors.
      aDownload.start().catch(() => {});

      // Start processing all the other events received through nsITransfer.
      this._deferDownload.resolve(aDownload);

      // Add the download to the list, allowing it to be seen and canceled.
      return Downloads.getList(Downloads.ALL).then(list => list.add(aDownload));
    }.bind(this)).then(null, Cu.reportError);
  },

  setSha256Hash: function (hash)
  {
    this._sha256Hash = hash;
  },

  setSignatureInfo: function (signatureInfo)
  {
    this._signatureInfo = signatureInfo;
  },

  setRedirects: function (redirects)
  {
    this._redirects = redirects;
  },

  // Private methods and properties

  /**
   * This deferred object contains a promise that is resolved with the Download
   * object associated with this nsITransfer instance, when it is available.
   */
  _deferDownload: null,

  /**
   * Reference to the component that is executing the download.  This component
   * allows cancellation through its nsICancelable interface.
   */
  _cancelable: null,

  /**
   * Indicates that the component that executes the download has notified a
   * failure condition.  In this case, we should never use the component methods
   * that cancel the download.
   */
  _componentFailed: false,

  /**
   * Save the SHA-256 hash in raw bytes of the downloaded file.
   */
  _sha256Hash: null,

  /**
   * Save the signature info in a serialized protobuf of the downloaded file.
   */
  _signatureInfo: null,
};

// Module

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadLegacyTransfer]);