// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/renderer_host/data_transfer_util.h"

#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/uuid.h"
#include "content/browser/blob_storage/chrome_blob_storage_context.h"
#include "content/browser/file_system_access/file_system_access_manager_impl.h"
#include "content/public/browser/browser_thread.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/base/mime_util.h"
#include "services/network/public/mojom/referrer_policy.mojom-shared.h"
#include "storage/browser/blob/blob_storage_context.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_context.h"
#include "third_party/blink/public/common/page/source_effect_allowed_mojom_util.h"
#include "third_party/blink/public/mojom/blob/serialized_blob.mojom.h"
#include "third_party/blink/public/mojom/drag/drag.mojom.h"
#include "third_party/blink/public/mojom/file_system_access/file_system_access_data_transfer_token.mojom.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/file_info.h"
#include "url/gurl.h"

namespace content {

namespace {

// On Chrome OS paths that exist on an external mount point need to be treated
// differently to make sure the File System Access code accesses these paths via
// the correct file system backend. This method checks if this is the case, and
// updates `entry_path` to the path that should be used by the File System
// Access implementation.
content::PathType MaybeRemapPath(base::FilePath* entry_path) {
#if BUILDFLAG(IS_CHROMEOS)
  base::FilePath virtual_path;
  auto* external_mount_points =
      storage::ExternalMountPoints::GetSystemInstance();
  if (external_mount_points->GetVirtualPath(*entry_path, &virtual_path)) {
    *entry_path = std::move(virtual_path);
    return content::PathType::kExternal;
  }
#endif
  return content::PathType::kLocal;
}

// Parse the download metadata set in DataTransfer.setData. The metadata
// consists of a set of the following values separated by ":"
// * MIME type
// * File name
// * URL
// If the file name contains special characters, they need to be escaped
// appropriately.
// For example, we can have
//   text/plain:example.txt:http://example.com/example.txt
// TODO(crbug.com/497928951): Move this to the renderer.
std::optional<DownloadUrlMetadata> ParseDownloadMetadata(
    std::u16string_view metadata) {
  const char16_t separator = L':';

  size_t mime_type_end_pos = metadata.find(separator);
  if (mime_type_end_pos == std::u16string_view::npos) {
    return std::nullopt;
  }

  size_t file_name_end_pos = metadata.find(separator, mime_type_end_pos + 1);
  if (file_name_end_pos == std::u16string_view::npos) {
    return std::nullopt;
  }

  GURL parsed_url = GURL(metadata.substr(file_name_end_pos + 1));
  if (!parsed_url.is_valid()) {
    return std::nullopt;
  }

  DownloadUrlMetadata result;
  result.mime_type = base::UTF16ToUTF8(metadata.substr(0, mime_type_end_pos));
  result.suggested_file_name = base::UTF16ToUTF8(metadata.substr(
      mime_type_end_pos + 1, file_name_end_pos - mime_type_end_pos - 1));
  result.url = std::move(parsed_url);
  return result;
}

}  // namespace

std::vector<blink::mojom::DataTransferFilePtr> FileInfosToDataTransferFiles(
    const std::vector<ui::FileInfo>& filenames,
    FileSystemAccessManagerImpl* file_system_access_manager,
    int child_id) {
  std::vector<blink::mojom::DataTransferFilePtr> result;
  for (const ui::FileInfo& file_info : filenames) {
    blink::mojom::DataTransferFilePtr file =
        blink::mojom::DataTransferFile::New();
    file->path = file_info.path;
    file->display_name = file_info.display_name;
    mojo::PendingRemote<blink::mojom::FileSystemAccessDataTransferToken>
        pending_token;
    base::FilePath entry_path = file_info.path;
    content::PathType path_type = MaybeRemapPath(&entry_path);
    base::FilePath display_name = !file_info.display_name.empty()
                                      ? file_info.display_name
                                      : entry_path.BaseName();
    if (entry_path.empty() || display_name.empty()) {
      continue;
    }
    file_system_access_manager->CreateFileSystemAccessDataTransferToken(
        content::PathInfo(path_type, entry_path, display_name.AsUTF8Unsafe()),
        child_id, pending_token.InitWithNewPipeAndPassReceiver());
    file->file_system_access_token = std::move(pending_token);
    result.push_back(std::move(file));
  }
  return result;
}

std::vector<blink::mojom::DragItemFileSystemFilePtr>
FileSystemFileInfosToDragItemFileSystemFilePtr(
    std::vector<DropData::FileSystemFileInfo> file_system_file_infos,
    FileSystemAccessManagerImpl* file_system_access_manager,
    scoped_refptr<content::ChromeBlobStorageContext> context) {
  std::vector<blink::mojom::DragItemFileSystemFilePtr> result;
  for (const content::DropData::FileSystemFileInfo& file_system_file :
       file_system_file_infos) {
    blink::mojom::DragItemFileSystemFilePtr item =
        blink::mojom::DragItemFileSystemFile::New();
    item->url = file_system_file.url;
    item->size = file_system_file.size;
    item->file_system_id = file_system_file.filesystem_id;

    storage::FileSystemURL file_system_url =
        file_system_access_manager->context()->CrackURLInFirstPartyContext(
            file_system_file.url);
    CHECK(file_system_url.type() != storage::kFileSystemTypePersistent,
          base::NotFatalUntil::M152);
    CHECK(file_system_url.type() != storage::kFileSystemTypeTemporary,
          base::NotFatalUntil::M152);

    std::string uuid = base::Uuid::GenerateRandomV4().AsLowercaseString();

    std::string content_type;

    std::string mime_type;
    // TODO(crbug.com/40291155): Historically for blobs created from
    // file system URLs we've only considered well known content types to
    // avoid leaking the presence of locally installed applications when
    // creating blobs from files in the sandboxed file system. However, since
    // this code path should only deal with real/"trusted" paths, we could
    // consider taking platform defined mime type mappings into account here
    // as well. Note that the approach used here must not block or else it
    // can't be called from the UI thread (for example, calls to
    // GetMimeTypeFromExtension can block).
    if (net::GetWellKnownMimeTypeFromFile(file_system_url.path(), &mime_type)) {
      content_type = std::move(mime_type);
    }
    // TODO(crbug.com/41458368): Consider some kind of fallback type when
    // the above mime type detection fails.

    mojo::PendingRemote<blink::mojom::Blob> blob_remote;
    mojo::PendingReceiver<blink::mojom::Blob> blob_receiver =
        blob_remote.InitWithNewPipeAndPassReceiver();

    item->serialized_blob = blink::mojom::SerializedBlob::New(
        uuid, content_type, item->size, std::move(blob_remote));

    GetIOThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            &ChromeBlobStorageContext::CreateFileSystemBlob, context,
            base::WrapRefCounted(file_system_access_manager->context()),
            std::move(blob_receiver), std::move(file_system_url),
            std::move(uuid), std::move(content_type), item->size,
            base::Time()));

    result.push_back(std::move(item));
  }
  return result;
}

blink::mojom::DragDataPtr DropDataToDragData(
    const DropData& drop_data,
    FileSystemAccessManagerImpl* file_system_access_manager,
    int child_id,
    scoped_refptr<ChromeBlobStorageContext> chrome_blob_storage_context) {
  // These fields are currently unused when dragging into Blink.
  // TODO(crbug.com/540942115): CHECK-exclusion: Convert to a CHECK once we are
  // confident it won't be triggered.
  DCHECK(!drop_data.download_metadata.has_value());

  std::vector<blink::mojom::DragItemPtr> items;
  if (drop_data.text) {
    blink::mojom::DragItemStringPtr item = blink::mojom::DragItemString::New();
    item->string_type = ui::kMimeTypePlainText;
    item->string_data = *drop_data.text;
    items.push_back(blink::mojom::DragItem::NewString(std::move(item)));
  }
  if (!drop_data.url_infos.empty()) {
    // Merge all URLs into a single item with CRLF as separator.
    // This is the format used by the text/uri-list MIME type.
    std::string merged_urls;
    for (const auto& info : drop_data.url_infos) {
      if (!merged_urls.empty()) {
        merged_urls += "\r\n";
      }
      merged_urls += info.url.spec();
    }

    blink::mojom::DragItemStringPtr item = blink::mojom::DragItemString::New();
    item->string_type = ui::kMimeTypeUriList;
    item->string_data = base::UTF8ToUTF16(merged_urls);
    item->title = drop_data.url_infos.front().title;
    items.push_back(blink::mojom::DragItem::NewString(std::move(item)));
  }
  if (drop_data.html) {
    blink::mojom::DragItemStringPtr item = blink::mojom::DragItemString::New();
    item->string_type = ui::kMimeTypeHtml;
    item->string_data = *drop_data.html;
    item->base_url = drop_data.html_base_url;
    items.push_back(blink::mojom::DragItem::NewString(std::move(item)));
  }
  std::vector<blink::mojom::DataTransferFilePtr> files =
      FileInfosToDataTransferFiles(drop_data.filenames,
                                   file_system_access_manager, child_id);
  for (auto& file : files) {
    items.push_back(blink::mojom::DragItem::NewFile(std::move(file)));
  }

  std::vector<blink::mojom::DragItemFileSystemFilePtr> file_system_files =
      FileSystemFileInfosToDragItemFileSystemFilePtr(
          drop_data.file_system_files, file_system_access_manager,
          std::move(chrome_blob_storage_context));
  for (auto& file_system_file : file_system_files) {
    items.push_back(
        blink::mojom::DragItem::NewFileSystemFile(std::move(file_system_file)));
  }
  // A genuine file drag (a JS-constructed File round-trip or a Chromium
  // file-promise) carries a source URL or a Content-Disposition. A plain <img>
  // drag populates `file_contents` on macOS (kept for browser-side consumers
  // such as Glic) but carries neither signal, so it must not surface as a File
  // in the renderer's DataTransfer.files.
  if (!drop_data.file_contents.empty() &&
      (drop_data.file_contents_source_url.is_valid() ||
       !drop_data.file_contents_content_disposition.empty())) {
    blink::mojom::DragItemBinaryPtr item = blink::mojom::DragItemBinary::New();
    item->data = mojo_base::BigBuffer(drop_data.file_contents);
    item->is_image_accessible = drop_data.file_contents_image_accessible;
    // source_url is synthesized from Content-Disposition for JS-constructed
    // File objects. If absent (e.g. dropped from a non-Chromium app), leave
    // it empty; the renderer will produce a File with an empty name.
    if (drop_data.file_contents_source_url.is_valid()) {
      item->source_url = drop_data.file_contents_source_url;
    }
    item->filename_extension =
        base::FilePath(drop_data.file_contents_filename_extension);
    if (!drop_data.file_contents_content_disposition.empty()) {
      item->content_disposition = drop_data.file_contents_content_disposition;
    }
    items.push_back(blink::mojom::DragItem::NewBinary(std::move(item)));
  }
  for (const std::pair<const std::u16string, std::u16string>& data :
       drop_data.custom_data) {
    blink::mojom::DragItemStringPtr item = blink::mojom::DragItemString::New();
    item->string_type = base::UTF16ToUTF8(data.first);
    item->string_data = data.second;
    items.push_back(blink::mojom::DragItem::NewString(std::move(item)));
  }

  std::optional<blink::mojom::SourceEffectAllowed> source_effect_allowed;
  if (drop_data.source_effect_allowed &&
      !drop_data.source_effect_allowed->empty()) {
    source_effect_allowed = blink::SourceEffectAllowedFromString(
        base::UTF16ToUTF8(*drop_data.source_effect_allowed));
  }

  return blink::mojom::DragData::New(
      std::move(items),
      // While this shouldn't be a problem in production code, as the
      // real file_system_id should never be empty if used in browser to
      // renderer messages, some tests use this function to test renderer to
      // browser messages, in which case the field is unused and this will hit
      // a DCHECK.
      drop_data.filesystem_id.empty()
          ? std::nullopt
          : std::optional<std::string>(
                base::UTF16ToUTF8(drop_data.filesystem_id)),
      /*force_default_action=*/!drop_data.document_is_handling_drag,
      source_effect_allowed, drop_data.referrer_policy);
}

blink::mojom::DragDataPtr DropMetaDataToDragData(
    const std::vector<DropData::Metadata>& drop_meta_data) {
  std::vector<blink::mojom::DragItemPtr> items;

  for (const auto& meta_data_item : drop_meta_data) {
    if (meta_data_item.kind == DropData::Kind::STRING) {
      blink::mojom::DragItemStringPtr item =
          blink::mojom::DragItemString::New();
      item->string_type = base::UTF16ToUTF8(meta_data_item.mime_type);
      // Have to pass a dummy URL here instead of an empty URL because the
      // DropData received by browser_plugins goes through a round trip:
      // DropData::MetaData --> WebDragData-->DropData. In the end, DropData
      // will contain an empty URL (which means no URL is dragged) if the URL in
      // WebDragData is empty.
      if (base::EqualsASCII(meta_data_item.mime_type, ui::kMimeTypeUriList)) {
        item->string_data = u"about:dragdrop-placeholder";
      }
      items.push_back(blink::mojom::DragItem::NewString(std::move(item)));
      continue;
    }

    // TODO(hush): crbug.com/584789. Blink needs to support creating a file with
    // just the mimetype. This is needed to drag files to WebView on Android
    // platform.
    if ((meta_data_item.kind == DropData::Kind::FILENAME) &&
        !meta_data_item.filename.empty()) {
      blink::mojom::DataTransferFilePtr item =
          blink::mojom::DataTransferFile::New();
      item->path = meta_data_item.filename;
      item->display_name = meta_data_item.display_name;
      items.push_back(blink::mojom::DragItem::NewFile(std::move(item)));
      continue;
    }

    if (meta_data_item.kind == DropData::Kind::FILESYSTEMFILE) {
      blink::mojom::DragItemFileSystemFilePtr item =
          blink::mojom::DragItemFileSystemFile::New();
      item->url = meta_data_item.file_system_url;
      items.push_back(
          blink::mojom::DragItem::NewFileSystemFile(std::move(item)));
      continue;
    }

    if (meta_data_item.kind == DropData::Kind::BINARY) {
      blink::mojom::DragItemBinaryPtr item =
          blink::mojom::DragItemBinary::New();
      item->source_url = meta_data_item.file_contents_url;
      items.push_back(blink::mojom::DragItem::NewBinary(std::move(item)));
      continue;
    }
  }
  return blink::mojom::DragData::New(std::move(items), std::nullopt,
                                     /*force_default_action=*/false,
                                     /*source_effect_allowed=*/std::nullopt,
                                     network::mojom::ReferrerPolicy::kDefault);
}

DropData DragDataToDropData(const blink::mojom::DragData& drag_data) {
  // This field should be empty when dragging from the renderer.
  CHECK(!drag_data.file_system_id, base::NotFatalUntil::M152);

  DropData result;
  if (drag_data.source_effect_allowed.has_value()) {
    result.source_effect_allowed = base::UTF8ToUTF16(
        blink::SourceEffectAllowedToString(*drag_data.source_effect_allowed));
  }
  for (const blink::mojom::DragItemPtr& item : drag_data.items) {
    switch (item->which()) {
      case blink::mojom::DragItemDataView::Tag::kString: {
        const blink::mojom::DragItemStringPtr& string_item = item->get_string();
        std::string str_type = string_item->string_type;
        if (str_type == ui::kMimeTypePlainText) {
          result.text = string_item->string_data;
        } else if (str_type == ui::kMimeTypeUriList) {
          // Parse the string data into a vector of URLs with RFC 2483 support
          std::vector<std::u16string_view> lines = base::SplitStringPiece(
              string_item->string_data, u"\r\n", base::KEEP_WHITESPACE,
              base::SPLIT_WANT_NONEMPTY);

          for (const auto& line : lines) {
            // Skip comment lines as per RFC 2483
            if (line.starts_with('#')) {
              continue;
            }

            GURL gurl(line);
            if (gurl.is_valid()) {
              result.url_infos.emplace_back(gurl, u"");
            }
          }
          // Only the first URL can have a title (e.g., when dragging a link).
          if (!result.url_infos.empty() && string_item->title) {
            result.url_infos.front().title = *string_item->title;
          }
        } else if (str_type == ui::kMimeTypeDownloadUrl) {
          result.download_metadata =
              ParseDownloadMetadata(string_item->string_data);
          result.referrer_policy = drag_data.referrer_policy;
        } else if (str_type == ui::kMimeTypeHtml) {
          result.html = string_item->string_data;
          if (string_item->base_url)
            result.html_base_url = *string_item->base_url;
        } else {
          result.custom_data.emplace(
              base::UTF8ToUTF16(string_item->string_type),
              string_item->string_data);
        }
        break;
      }
      case blink::mojom::DragItemDataView::Tag::kBinary: {
        // DropData only supports a single file_contents entry.
        // Skip additional binary items until multi-file support is added.
        if (!result.file_contents.empty()) {
          break;
        }

        const blink::mojom::DragItemBinaryPtr& binary_item = item->get_binary();
        base::span<const uint8_t> contents(binary_item->data);
        result.file_contents.assign(contents.begin(), contents.end());
        result.file_contents_image_accessible =
            binary_item->is_image_accessible;
        result.file_contents_source_url = binary_item->source_url;
        result.file_contents_filename_extension =
            binary_item->filename_extension.BaseName().value();
        if (binary_item->content_disposition) {
          result.file_contents_content_disposition =
              *binary_item->content_disposition;
        }
        break;
      }
      case blink::mojom::DragItemDataView::Tag::kFile: {
        const blink::mojom::DataTransferFilePtr& file_item = item->get_file();
        // TODO(varunjain): This only works on chromeos. Support win/mac/gtk.
        result.filenames.emplace_back(file_item->path, file_item->display_name);
        break;
      }
      case blink::mojom::DragItemDataView::Tag::kFileSystemFile: {
        const blink::mojom::DragItemFileSystemFilePtr& file_system_file_item =
            item->get_file_system_file();
        // This field should be empty when dragging from the renderer.
        CHECK(!file_system_file_item->file_system_id,
              base::NotFatalUntil::M152);

        DropData::FileSystemFileInfo info;
        info.url = file_system_file_item->url;
        info.size = file_system_file_item->size;
        info.filesystem_id = std::string();
        result.file_system_files.push_back(std::move(info));
        break;
      }
    }
  }
  return result;
}

}  // namespace content
