// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/download/base_file.h"

#include <windows.h>
#include <cguid.h>
#include <objbase.h>
#include <shellapi.h>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/guid.h"
#include "base/metrics/histogram.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "content/browser/download/download_interrupt_reasons_impl.h"
#include "content/browser/download/download_stats.h"
#include "content/browser/safe_util_win.h"
#include "content/public/browser/browser_thread.h"

namespace content {
namespace {

const int kAllSpecialShFileOperationCodes[] = {
  // Should be kept in sync with the case statement below.
  ERROR_ACCESS_DENIED,
  ERROR_SHARING_VIOLATION,
  ERROR_INVALID_PARAMETER,
  0x71,
  0x72,
  0x73,
  0x74,
  0x75,
  0x76,
  0x78,
  0x79,
  0x7A,
  0x7C,
  0x7D,
  0x7E,
  0x80,
  0x81,
  0x82,
  0x83,
  0x84,
  0x85,
  0x86,
  0x87,
  0x88,
  0xB7,
  0x402,
  0x10000,
  0x10074,
};

// Maps the result of a call to |SHFileOperation()| onto a
// |DownloadInterruptReason|.
//
// These return codes are *old* (as in, DOS era), and specific to
// |SHFileOperation()|.
// They do not appear in any windows header.
//
// See http://msdn.microsoft.com/en-us/library/bb762164(VS.85).aspx.
DownloadInterruptReason MapShFileOperationCodes(int code) {
  DownloadInterruptReason result = DOWNLOAD_INTERRUPT_REASON_NONE;

  // Check these pre-Win32 error codes first, then check for matches
  // in Winerror.h.
  // This switch statement should be kept in sync with the list of codes
  // above.
  switch (code) {
    // Not a pre-Win32 error code; here so that this particular case shows up in
    // our histograms. Unfortunately, it is used not just to signal actual
    // ACCESS_DENIED errors, but many other errors as well. So we treat it as a
    // transient error.
    case ERROR_ACCESS_DENIED:  // Access is denied.
      result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
      break;

    // This isn't documented but returned from SHFileOperation. Sharing
    // violations indicate that another process had the file open while we were
    // trying to rename. Anti-virus is believed to be the cause of this error in
    // the wild. Treated as a transient error on the assumption that the file
    // will be made available for renaming at a later time.
    case ERROR_SHARING_VIOLATION:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
      break;

    // This is also not a documented return value of SHFileOperation, but has
    // been observed in the wild. We are treating it as a transient error based
    // on the cases we have seen so far.  See http://crbug.com/368455.
    case ERROR_INVALID_PARAMETER:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
      break;

    // The source and destination files are the same file.
    // DE_SAMEFILE == 0x71
    case 0x71:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // The operation was canceled by the user, or silently canceled if the
    // appropriate flags were supplied to SHFileOperation.
    // DE_OPCANCELLED == 0x75
    case 0x75:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // Security settings denied access to the source.
    // DE_ACCESSDENIEDSRC == 0x78
    case 0x78:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
      break;

    // The source or destination path exceeded or would exceed MAX_PATH.
    // DE_PATHTOODEEP == 0x79
    case 0x79:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG;
      break;

    // The path in the source or destination or both was invalid.
    // DE_INVALIDFILES == 0x7C
    case 0x7C:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // The destination path is an existing file.
    // DE_FLDDESTISFILE == 0x7E
    case 0x7E:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // The destination path is an existing folder.
    // DE_FILEDESTISFLD == 0x80
    case 0x80:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // The name of the file exceeds MAX_PATH.
    // DE_FILENAMETOOLONG == 0x81
    case 0x81:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG;
      break;

    // The destination is a read-only CD-ROM, possibly unformatted.
    // DE_DEST_IS_CDROM == 0x82
    case 0x82:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
      break;

    // The destination is a read-only DVD, possibly unformatted.
    // DE_DEST_IS_DVD == 0x83
    case 0x83:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
      break;

    // The destination is a writable CD-ROM, possibly unformatted.
    // DE_DEST_IS_CDRECORD == 0x84
    case 0x84:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
      break;

    // The file involved in the operation is too large for the destination
    // media or file system.
    // DE_FILE_TOO_LARGE == 0x85
    case 0x85:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE;
      break;

    // The source is a read-only CD-ROM, possibly unformatted.
    // DE_SRC_IS_CDROM == 0x86
    case 0x86:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
      break;

    // The source is a read-only DVD, possibly unformatted.
    // DE_SRC_IS_DVD == 0x87
    case 0x87:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
      break;

    // The source is a writable CD-ROM, possibly unformatted.
    // DE_SRC_IS_CDRECORD == 0x88
    case 0x88:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
      break;

    // MAX_PATH was exceeded during the operation.
    // DE_ERROR_MAX == 0xB7
    case 0xB7:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG;
      break;

    // An unspecified error occurred on the destination.
    // XE_ERRORONDEST == 0x10000
    case 0x10000:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // Multiple file paths were specified in the source buffer, but only one
    // destination file path.
    // DE_MANYSRC1DEST == 0x72
    case 0x72:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // Rename operation was specified but the destination path is
    // a different directory. Use the move operation instead.
    // DE_DIFFDIR == 0x73
    case 0x73:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // The source is a root directory, which cannot be moved or renamed.
    // DE_ROOTDIR == 0x74
    case 0x74:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // The destination is a subtree of the source.
    // DE_DESTSUBTREE == 0x76
    case 0x76:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // The operation involved multiple destination paths,
    // which can fail in the case of a move operation.
    // DE_MANYDEST == 0x7A
    case 0x7A:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // The source and destination have the same parent folder.
    // DE_DESTSAMETREE == 0x7D
    case 0x7D:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // An unknown error occurred.  This is typically due to an invalid path in
    // the source or destination.  This error does not occur on Windows Vista
    // and later.
    // DE_UNKNOWN_ERROR == 0x402
    case 0x402:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;

    // Destination is a root directory and cannot be renamed.
    // DE_ROOTDIR | ERRORONDEST == 0x10074
    case 0x10074:
      result = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;
  }

  // Narrow down on the reason we're getting some catch-all interrupt reasons.
  if (result == DOWNLOAD_INTERRUPT_REASON_FILE_FAILED) {
    UMA_HISTOGRAM_CUSTOM_ENUMERATION(
        "Download.MapWinShErrorFileFailed", code,
        base::CustomHistogram::ArrayToCustomRanges(
            kAllSpecialShFileOperationCodes,
            arraysize(kAllSpecialShFileOperationCodes)));
  }

  if (result == DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED) {
    UMA_HISTOGRAM_CUSTOM_ENUMERATION(
        "Download.MapWinShErrorAccessDenied", code,
        base::CustomHistogram::ArrayToCustomRanges(
            kAllSpecialShFileOperationCodes,
            arraysize(kAllSpecialShFileOperationCodes)));
  }

  if (result == DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR) {
    UMA_HISTOGRAM_CUSTOM_ENUMERATION(
        "Download.MapWinShErrorTransientError", code,
        base::CustomHistogram::ArrayToCustomRanges(
            kAllSpecialShFileOperationCodes,
            arraysize(kAllSpecialShFileOperationCodes)));
  }

  if (result != DOWNLOAD_INTERRUPT_REASON_NONE)
    return result;

  // If not one of the above codes, it should be a standard Windows error code.
  return ConvertFileErrorToInterruptReason(
      base::File::OSErrorToFileError(code));
}

// Maps a return code from ScanAndSaveDownloadedFile() to a
// DownloadInterruptReason. The return code in |result| is usually from the
// final IAttachmentExecute::Save() call.
DownloadInterruptReason MapScanAndSaveErrorCodeToInterruptReason(
    HRESULT result) {
  if (SUCCEEDED(result))
    return DOWNLOAD_INTERRUPT_REASON_NONE;

  switch (result) {
    case INET_E_SECURITY_PROBLEM:       // 0x800c000e
      // This is returned if the download was blocked due to security
      // restrictions. E.g. if the source URL was in the Restricted Sites zone
      // and downloads are blocked on that zone, then the download would be
      // deleted and this error code is returned.
      return DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED;

    case E_FAIL:                        // 0x80004005
      // Returned if an anti-virus product reports an infection in the
      // downloaded file during IAE::Save().
      return DOWNLOAD_INTERRUPT_REASON_FILE_VIRUS_INFECTED;

    default:
      // Any other error that occurs during IAttachmentExecute::Save() likely
      // indicates a problem with the security check, but not necessarily the
      // download. See http://crbug.com/153212.
      return DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED;
  }
}

} // namespace

// Renames a file using the SHFileOperation API to ensure that the target file
// gets the correct default security descriptor in the new path.
// Returns a network error, or net::OK for success.
DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions(
    const base::FilePath& new_path) {
  base::ThreadRestrictions::AssertIOAllowed();

  // The parameters to SHFileOperation must be terminated with 2 NULL chars.
  base::FilePath::StringType source = full_path_.value();
  base::FilePath::StringType target = new_path.value();

  source.append(1, L'\0');
  target.append(1, L'\0');

  SHFILEOPSTRUCT move_info = {0};
  move_info.wFunc = FO_MOVE;
  move_info.pFrom = source.c_str();
  move_info.pTo = target.c_str();
  move_info.fFlags = FOF_SILENT | FOF_NOCONFIRMATION | FOF_NOERRORUI |
      FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS;

  int result = SHFileOperation(&move_info);
  DownloadInterruptReason interrupt_reason = DOWNLOAD_INTERRUPT_REASON_NONE;

  if (result == 0 && move_info.fAnyOperationsAborted)
    interrupt_reason = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
  else if (result != 0)
    interrupt_reason = MapShFileOperationCodes(result);

  if (interrupt_reason != DOWNLOAD_INTERRUPT_REASON_NONE)
    return LogInterruptReason("SHFileOperation", result, interrupt_reason);
  return interrupt_reason;
}

DownloadInterruptReason BaseFile::AnnotateWithSourceInformation() {
  DCHECK_CURRENTLY_ON(BrowserThread::FILE);
  DCHECK(!detached_);

  bound_net_log_.BeginEvent(net::NetLog::TYPE_DOWNLOAD_FILE_ANNOTATED);
  DownloadInterruptReason result = DOWNLOAD_INTERRUPT_REASON_NONE;
  std::string braces_guid = "{" + client_guid_ + "}";
  GUID guid = GUID_NULL;
  if (base::IsValidGUID(client_guid_)) {
    HRESULT hr = CLSIDFromString(
        base::UTF8ToUTF16(braces_guid).c_str(), &guid);
    if (FAILED(hr))
      guid = GUID_NULL;
  }

  HRESULT hr = AVScanFile(full_path_, source_url_.spec(), guid);

  // If the download file is missing after the call, then treat this as an
  // interrupted download.
  //
  // If the ScanAndSaveDownloadedFile() call failed, but the downloaded file is
  // still around, then don't interrupt the download. Attachment Execution
  // Services deletes the submitted file if the downloaded file is blocked by
  // policy or if it was found to be infected.
  //
  // If the file is still there, then the error could be due to AES not being
  // available or some other error during the AES invocation. In either case,
  // we don't surface the error to the user.
  if (!base::PathExists(full_path_)) {
    DCHECK(FAILED(hr));
    result = MapScanAndSaveErrorCodeToInterruptReason(hr);
    if (result == DOWNLOAD_INTERRUPT_REASON_NONE) {
      RecordDownloadCount(FILE_MISSING_AFTER_SUCCESSFUL_SCAN_COUNT);
      result = DOWNLOAD_INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED;
    }
    LogInterruptReason("ScanAndSaveDownloadedFile", hr, result);
  }
  bound_net_log_.EndEvent(net::NetLog::TYPE_DOWNLOAD_FILE_ANNOTATED);
  return result;
}

}  // namespace content
