forked from mirrors/gecko-dev
		
	This is a medium sized patch to legacy download construction. It takes advantage of the new property added in Bug 1762033 to prevent the downloads panel from being automatically shown when a download is added after an interaction with the unknown content type dialog or the file picker dialog. I chose to not do the same for failed transfers since I thought it might serve some use, but that might be wrong. I don't know if there's a way to test the dialog that appears when you download an executable without going through the same path I adjusted with the patch. It seems like it's covered but I could be wrong. Also add a test to cover these changes from the bottom up. Thanks and apologies for my sloppy C++, though I'm sure I'll learn a lot more from the review 😅 Differential Revision: https://phabricator.services.mozilla.com/D145312
		
			
				
	
	
		
			513 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			513 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
/**
 | 
						|
 * This 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";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  lazy,
 | 
						|
  "Downloads",
 | 
						|
  "resource://gre/modules/Downloads.jsm"
 | 
						|
);
 | 
						|
ChromeUtils.defineModuleGetter(
 | 
						|
  lazy,
 | 
						|
  "DownloadError",
 | 
						|
  "resource://gre/modules/DownloadCore.jsm"
 | 
						|
);
 | 
						|
 | 
						|
/**
 | 
						|
 * 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._promiseDownload = new Promise(r => (this._resolveDownload = r));
 | 
						|
}
 | 
						|
 | 
						|
DownloadLegacyTransfer.prototype = {
 | 
						|
  classID: Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}"),
 | 
						|
 | 
						|
  QueryInterface: ChromeUtils.generateQI([
 | 
						|
    "nsIWebProgressListener",
 | 
						|
    "nsIWebProgressListener2",
 | 
						|
    "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._promiseDownload
 | 
						|
        .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);
 | 
						|
 | 
						|
          // 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);
 | 
						|
            }
 | 
						|
          });
 | 
						|
        })
 | 
						|
        .catch(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._promiseDownload
 | 
						|
        .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(aStatus);
 | 
						|
        })
 | 
						|
        .catch(Cu.reportError);
 | 
						|
 | 
						|
      // Release the reference to the component executing the download.
 | 
						|
      this._cancelable = null;
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  // nsIWebProgressListener
 | 
						|
  onProgressChange: function DLT_onProgressChange(
 | 
						|
    aWebProgress,
 | 
						|
    aRequest,
 | 
						|
    aCurSelfProgress,
 | 
						|
    aMaxSelfProgress,
 | 
						|
    aCurTotalProgress,
 | 
						|
    aMaxTotalProgress
 | 
						|
  ) {
 | 
						|
    this.onProgressChange64(
 | 
						|
      aWebProgress,
 | 
						|
      aRequest,
 | 
						|
      aCurSelfProgress,
 | 
						|
      aMaxSelfProgress,
 | 
						|
      aCurTotalProgress,
 | 
						|
      aMaxTotalProgress
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  onLocationChange() {},
 | 
						|
 | 
						|
  // nsIWebProgressListener
 | 
						|
  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._promiseDownload
 | 
						|
        .then(download => {
 | 
						|
          download.saver.onTransferFinished(aStatus);
 | 
						|
        })
 | 
						|
        .catch(Cu.reportError);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  onSecurityChange() {},
 | 
						|
 | 
						|
  onContentBlockingEvent() {},
 | 
						|
 | 
						|
  // nsIWebProgressListener2
 | 
						|
  onProgressChange64: function DLT_onProgressChange64(
 | 
						|
    aWebProgress,
 | 
						|
    aRequest,
 | 
						|
    aCurSelfProgress,
 | 
						|
    aMaxSelfProgress,
 | 
						|
    aCurTotalProgress,
 | 
						|
    aMaxTotalProgress
 | 
						|
  ) {
 | 
						|
    // Since this progress function is invoked frequently, we use a slightly
 | 
						|
    // more complex solution that optimizes the case where we already have an
 | 
						|
    // associated Download object, avoiding the Promise overhead.
 | 
						|
    if (this._download) {
 | 
						|
      this._hasDelayedProgress = false;
 | 
						|
      this._download.saver.onProgressBytes(
 | 
						|
        aCurTotalProgress,
 | 
						|
        aMaxTotalProgress
 | 
						|
      );
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // If we don't have a Download object yet, store the most recent progress
 | 
						|
    // notification to send later. We must do this because there is no guarantee
 | 
						|
    // that a future notification will be sent if the download stalls.
 | 
						|
    this._delayedCurTotalProgress = aCurTotalProgress;
 | 
						|
    this._delayedMaxTotalProgress = aMaxTotalProgress;
 | 
						|
 | 
						|
    // Do not enqueue multiple callbacks for the progress report.
 | 
						|
    if (this._hasDelayedProgress) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this._hasDelayedProgress = true;
 | 
						|
 | 
						|
    this._promiseDownload
 | 
						|
      .then(download => {
 | 
						|
        // Check whether an immediate progress report has been already processed
 | 
						|
        // before we could send the delayed progress report.
 | 
						|
        if (!this._hasDelayedProgress) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        download.saver.onProgressBytes(
 | 
						|
          this._delayedCurTotalProgress,
 | 
						|
          this._delayedMaxTotalProgress
 | 
						|
        );
 | 
						|
      })
 | 
						|
      .catch(Cu.reportError);
 | 
						|
  },
 | 
						|
  _hasDelayedProgress: false,
 | 
						|
  _delayedCurTotalProgress: 0,
 | 
						|
  _delayedMaxTotalProgress: 0,
 | 
						|
 | 
						|
  // nsIWebProgressListener2
 | 
						|
  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,
 | 
						|
    aSourceOriginalURI,
 | 
						|
    aTarget,
 | 
						|
    aDisplayName,
 | 
						|
    aMIMEInfo,
 | 
						|
    aStartTime,
 | 
						|
    aTempFile,
 | 
						|
    aCancelable,
 | 
						|
    aIsPrivate,
 | 
						|
    aDownloadClassification,
 | 
						|
    aReferrerInfo,
 | 
						|
    aOpenDownloadsListOnStart
 | 
						|
  ) {
 | 
						|
    return this._nsITransferInitInternal(
 | 
						|
      aSource,
 | 
						|
      aSourceOriginalURI,
 | 
						|
      aTarget,
 | 
						|
      aDisplayName,
 | 
						|
      aMIMEInfo,
 | 
						|
      aStartTime,
 | 
						|
      aTempFile,
 | 
						|
      aCancelable,
 | 
						|
      aIsPrivate,
 | 
						|
      aDownloadClassification,
 | 
						|
      aReferrerInfo,
 | 
						|
      aOpenDownloadsListOnStart
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  // nsITransfer
 | 
						|
  initWithBrowsingContext(
 | 
						|
    aSource,
 | 
						|
    aTarget,
 | 
						|
    aDisplayName,
 | 
						|
    aMIMEInfo,
 | 
						|
    aStartTime,
 | 
						|
    aTempFile,
 | 
						|
    aCancelable,
 | 
						|
    aIsPrivate,
 | 
						|
    aDownloadClassification,
 | 
						|
    aReferrerInfo,
 | 
						|
    aOpenDownloadsListOnStart,
 | 
						|
    aBrowsingContext,
 | 
						|
    aHandleInternally,
 | 
						|
    aHttpChannel
 | 
						|
  ) {
 | 
						|
    let browsingContextId;
 | 
						|
    let userContextId;
 | 
						|
    if (aBrowsingContext && aBrowsingContext.currentWindowGlobal) {
 | 
						|
      browsingContextId = aBrowsingContext.id;
 | 
						|
      let windowGlobal = aBrowsingContext.currentWindowGlobal;
 | 
						|
      let originAttributes = windowGlobal.documentPrincipal.originAttributes;
 | 
						|
      userContextId = originAttributes.userContextId;
 | 
						|
    }
 | 
						|
    return this._nsITransferInitInternal(
 | 
						|
      aSource,
 | 
						|
      null,
 | 
						|
      aTarget,
 | 
						|
      aDisplayName,
 | 
						|
      aMIMEInfo,
 | 
						|
      aStartTime,
 | 
						|
      aTempFile,
 | 
						|
      aCancelable,
 | 
						|
      aIsPrivate,
 | 
						|
      aDownloadClassification,
 | 
						|
      aReferrerInfo,
 | 
						|
      aOpenDownloadsListOnStart,
 | 
						|
      userContextId,
 | 
						|
      browsingContextId,
 | 
						|
      aHandleInternally,
 | 
						|
      aHttpChannel
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  _nsITransferInitInternal(
 | 
						|
    aSource,
 | 
						|
    aSourceOriginalURI,
 | 
						|
    aTarget,
 | 
						|
    aDisplayName,
 | 
						|
    aMIMEInfo,
 | 
						|
    aStartTime,
 | 
						|
    aTempFile,
 | 
						|
    aCancelable,
 | 
						|
    isPrivate,
 | 
						|
    aDownloadClassification,
 | 
						|
    referrerInfo,
 | 
						|
    openDownloadsListOnStart = true,
 | 
						|
    userContextId = 0,
 | 
						|
    browsingContextId = 0,
 | 
						|
    handleInternally = false,
 | 
						|
    aHttpChannel = null
 | 
						|
  ) {
 | 
						|
    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.
 | 
						|
    let authHeader = null;
 | 
						|
    if (aHttpChannel) {
 | 
						|
      try {
 | 
						|
        authHeader = aHttpChannel.getRequestHeader("Authorization");
 | 
						|
      } catch (e) {}
 | 
						|
    }
 | 
						|
    let serialisedDownload = {
 | 
						|
      source: {
 | 
						|
        url: aSource.spec,
 | 
						|
        originalUrl: aSourceOriginalURI && aSourceOriginalURI.spec,
 | 
						|
        isPrivate,
 | 
						|
        userContextId,
 | 
						|
        browsingContextId,
 | 
						|
        referrerInfo,
 | 
						|
        authHeader,
 | 
						|
      },
 | 
						|
      target: {
 | 
						|
        path: aTarget.QueryInterface(Ci.nsIFileURL).file.path,
 | 
						|
        partFilePath: aTempFile && aTempFile.path,
 | 
						|
      },
 | 
						|
      saver: "legacy",
 | 
						|
      launchWhenSucceeded,
 | 
						|
      contentType,
 | 
						|
      launcherPath,
 | 
						|
      handleInternally,
 | 
						|
      openDownloadsListOnStart,
 | 
						|
    };
 | 
						|
 | 
						|
    // In case the Download was classified as insecure/dangerous
 | 
						|
    // it is already canceled, so we need to generate and attach the
 | 
						|
    // corresponding error to the download.
 | 
						|
    if (aDownloadClassification == Ci.nsITransfer.DOWNLOAD_POTENTIALLY_UNSAFE) {
 | 
						|
      Services.telemetry
 | 
						|
        .getKeyedHistogramById("DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD")
 | 
						|
        .add(lazy.DownloadError.BLOCK_VERDICT_INSECURE, 0);
 | 
						|
 | 
						|
      serialisedDownload.errorObj = {
 | 
						|
        becauseBlockedByReputationCheck: true,
 | 
						|
        reputationCheckVerdict: lazy.DownloadError.BLOCK_VERDICT_INSECURE,
 | 
						|
      };
 | 
						|
      // hasBlockedData needs to be true
 | 
						|
      // because the unblock UI is hidden if there is
 | 
						|
      // no data to be unblocked.
 | 
						|
      serialisedDownload.hasBlockedData = true;
 | 
						|
      // We cannot use the legacy saver here, as the original channel
 | 
						|
      // is already closed. A copy saver would create a new channel once
 | 
						|
      // start() is called.
 | 
						|
      serialisedDownload.saver = "copy";
 | 
						|
 | 
						|
      // Since the download is canceled already, we do not need to keep refrences
 | 
						|
      this._download = null;
 | 
						|
      this._cancelable = null;
 | 
						|
    }
 | 
						|
 | 
						|
    lazy.Downloads.createDownload(serialisedDownload)
 | 
						|
      .then(async 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._download = aDownload;
 | 
						|
        this._resolveDownload(aDownload);
 | 
						|
 | 
						|
        // Add the download to the list, allowing it to be seen and canceled.
 | 
						|
        await (await lazy.Downloads.getList(lazy.Downloads.ALL)).add(aDownload);
 | 
						|
        if (serialisedDownload.errorObj) {
 | 
						|
          // In case we added an already canceled dummy download
 | 
						|
          // we need to manually trigger a change event
 | 
						|
          // as all the animations for finishing downloads are
 | 
						|
          // listening on onChange.
 | 
						|
          aDownload._notifyChange();
 | 
						|
        }
 | 
						|
      })
 | 
						|
      .catch(Cu.reportError);
 | 
						|
  },
 | 
						|
 | 
						|
  setSha256Hash(hash) {
 | 
						|
    this._sha256Hash = hash;
 | 
						|
  },
 | 
						|
 | 
						|
  setSignatureInfo(signatureInfo) {
 | 
						|
    this._signatureInfo = signatureInfo;
 | 
						|
  },
 | 
						|
 | 
						|
  setRedirects(redirects) {
 | 
						|
    this._redirects = redirects;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Download object associated with this nsITransfer instance. This is not
 | 
						|
   * available immediately when the nsITransfer instance is created.
 | 
						|
   */
 | 
						|
  _download: null,
 | 
						|
 | 
						|
  /**
 | 
						|
   * Promise that resolves to the Download object associated with this
 | 
						|
   * nsITransfer instance after the _resolveDownload method is invoked.
 | 
						|
   *
 | 
						|
   * Waiting on this promise using "then" ensures that the callbacks are invoked
 | 
						|
   * in the correct order even if enqueued before the object is available.
 | 
						|
   */
 | 
						|
  _promiseDownload: null,
 | 
						|
  _resolveDownload: 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,
 | 
						|
};
 | 
						|
 | 
						|
var EXPORTED_SYMBOLS = ["DownloadLegacyTransfer"];
 |