chromium/chrome/browser/ui/ash/thumbnail_loader/thumbnail_loader.cc

// 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 "chrome/browser/ui/ash/thumbnail_loader/thumbnail_loader.h"

#include <algorithm>
#include <optional>
#include <utility>

#include "ash/public/cpp/image_downloader.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/json/values_util.h"
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/extensions/api/messaging/native_message_port.h"
#include "chrome/browser/image_decoder/image_decoder.h"
#include "chrome/browser/profiles/profile.h"
#include "extensions/browser/api/messaging/channel_endpoint.h"
#include "extensions/browser/api/messaging/message_service.h"
#include "extensions/browser/api/messaging/native_message_host.h"
#include "extensions/common/api/messaging/messaging_endpoint.h"
#include "extensions/common/api/messaging/port_id.h"
#include "extensions/common/extension.h"
#include "extensions/common/mojom/message_port.mojom-shared.h"
#include "net/base/data_url.h"
#include "net/base/mime_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "storage/browser/file_system/file_system_context.h"
#include "third_party/re2/src/re2/re2.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/image/image_skia.h"
#include "url/gurl.h"

namespace ash {

namespace {

// The native host name that will identify the thumbnail loader to the image
// loader extension.
constexpr char kNativeMessageHostName[] = "com.google.ash_thumbnail_loader";

// Returns whether the given `file_path` is supported by the `ThumbnailLoader`.
bool IsSupported(const base::FilePath& file_path) {
  constexpr std::array<std::pair<const char*, const char*>, 25>
      kFileMatchPatterns = {{
          // Document types ----------------------------------------------------
          {
              /*extension=*/"(?i)\\.pdf$",
              /*mime_type=*/"(?i)application\\/pdf",
          },
          // Image types -------------------------------------------------------
          {
              /*extension=*/"(?i)\\.jpe?g$",
              /*mime_type=*/"(?i)image\\/jpeg",
          },
          {
              /*extension=*/"(?i)\\.bmp$",
              /*mime_type=*/"(?i)image\\/bmp",
          },
          {
              /*extension=*/"(?i)\\.gif$",
              /*mime_type=*/"(?i)image\\/gif",
          },
          {
              /*extension=*/"(?i)\\.ico$",
              /*mime_type=*/"(?i)image\\/x\\-icon",
          },
          {
              /*extension=*/"(?i)\\.png$",
              /*mime_type=*/"(?i)image\\/png",
          },
          {
              /*extension=*/"(?i)\\.webp$",
              /*mime_type=*/"(?i)image\\/webp",
          },
          {
              /*extension=*/"(?i)\\.tiff?$",
              /*mime_type=*/"(?i)image\\/tiff",
          },
          {
              /*extension=*/"(?i)\\.svg$",
              /*mime_type=*/"(?i)image\\/svg\\+xml",
          },
          {
              /*extension=*/"(?i)\\.avif$",
              /*mime_type=*/"(?i)image\\/avif",
          },
          // Raw types ---------------------------------------------------------
          {
              /*extension=*/"(?i)\\.arw$",
              /*mime_type=*/nullptr,
          },
          {
              /*extension=*/"(?i)\\.cr2$",
              /*mime_type=*/nullptr,
          },
          {
              /*extension=*/"(?i)\\.dng$",
              /*mime_type=*/nullptr,
          },
          {
              /*extension=*/"(?i)\\.nef$",
              /*mime_type=*/nullptr,
          },
          {
              /*extension=*/"(?i)\\.nrw$",
              /*mime_type=*/nullptr,
          },
          {
              /*extension=*/"(?i)\\.orf$",
              /*mime_type=*/nullptr,
          },
          {
              /*extension=*/"(?i)\\.raf$",
              /*mime_type=*/nullptr,
          },
          {
              /*extension=*/"(?i)\\.rw2$",
              /*mime_type=*/nullptr,
          },
          // Video types -------------------------------------------------------
          {
              /*extension=*/"(?i)\\.3gpp?$",
              /*mime_type=*/"(?i)video\\/3gpp",
          },
          {
              /*extension=*/"(?i)\\.avi$",
              /*mime_type=*/"(?i)video\\/x\\-msvideo",
          },
          {
              /*extension=*/"(?i)\\.mov$",
              /*mime_type=*/"(?i)video\\/quicktime",
          },
          {
              /*extension=*/"\\.mkv$",
              /*mime_type=*/"video\\/x\\-matroska",
          },
          {
              /*extension=*/"(?i)\\.m(p4|4v|pg|peg|pg4|peg4)$",
              /*mime_type=*/"(?i)video\\/mp(4|eg)",
          },
          {
              /*extension=*/"(?i)\\.og(m|v|x)$",
              /*mime_type=*/"(?i)(application|video)\\/ogg",
          },
          {
              /*extension=*/"(?i)\\.webm$",
              /*mime_type=*/"(?i)video\\/webm",
          },
      }};

  // First attempt to match based on `mime_type`.
  std::string ext = file_path.Extension();
  std::string mime_type;
  if (!ext.empty() &&
      net::GetWellKnownMimeTypeFromExtension(ext.substr(1), &mime_type)) {
    for (const auto& file_match_pattern : kFileMatchPatterns) {
      if (file_match_pattern.second &&
          re2::RE2::FullMatch(mime_type, file_match_pattern.second)) {
        return true;
      }
    }
  }

  // Then attempt to match based on `file_path` extension.
  for (const auto& file_match_pattern : kFileMatchPatterns) {
    if (re2::RE2::FullMatch(file_path.Extension(), file_match_pattern.first))
      return true;
  }

  return false;
}

using ThumbnailDataCallback = base::OnceCallback<void(const std::string& data)>;

// Handles a parsed message sent from image loader extension in response to a
// thumbnail request.
void HandleParsedThumbnailResponse(
    const std::string& request_id,
    ThumbnailDataCallback callback,
    data_decoder::DataDecoder::ValueOrError result) {
  if (!result.has_value()) {
    VLOG(2) << "Failed to parse request response " << result.error();
    std::move(callback).Run("");
    return;
  }

  if (!result->is_dict()) {
    VLOG(2) << "Invalid response format";
    std::move(callback).Run("");
    return;
  }

  const std::string* received_request_id =
      result->GetDict().FindString("taskId");
  const std::string* data = result->GetDict().FindString("data");

  if (!data || !received_request_id || *received_request_id != request_id) {
    std::move(callback).Run("");
    return;
  }

  std::move(callback).Run(*data);
}

// Native message host for communication to the image loader extension.
// It handles a single image request - when the connection to the extension is
// established, it send a message containing an image request to the image
// loader. It closes the connection once it receives a response from the image
// loader.
class ThumbnailLoaderNativeMessageHost : public extensions::NativeMessageHost {
 public:
  ThumbnailLoaderNativeMessageHost(const std::string& request_id,
                                   const std::string& message,
                                   ThumbnailDataCallback callback)
      : request_id_(request_id),
        message_(message),
        callback_(std::move(callback)) {}

  ~ThumbnailLoaderNativeMessageHost() override {
    if (callback_)
      std::move(callback_).Run("");
  }

  void OnMessage(const std::string& message) override {
    if (response_received_)
      return;
    response_received_ = true;

    // Detach the callback from the message host in case the extension closes
    // connection by the time the response is parsed.
    data_decoder::DataDecoder::ParseJsonIsolated(
        message, base::BindOnce(&HandleParsedThumbnailResponse, request_id_,
                                std::move(callback_)));

    client_->CloseChannel("");
    client_ = nullptr;
  }

  void Start(Client* client) override {
    client_ = client;
    client_->PostMessageFromNativeHost(message_);
  }

  scoped_refptr<base::SingleThreadTaskRunner> task_runner() const override {
    return task_runner_;
  }

 private:
  const std::string request_id_;
  const std::string message_;
  ThumbnailDataCallback callback_;

  raw_ptr<Client> client_ = nullptr;

  bool response_received_ = false;

  const scoped_refptr<base::SingleThreadTaskRunner> task_runner_ =
      base::SingleThreadTaskRunner::GetCurrentDefault();
};

}  // namespace

// Converts a data URL to bitmap.
class ThumbnailLoader::ThumbnailDecoder : public ImageDecoder::ImageRequest {
 public:
  ThumbnailDecoder() = default;

  ThumbnailDecoder(const ThumbnailDecoder&) = delete;
  ThumbnailDecoder& operator=(const ThumbnailDecoder&) = delete;
  ~ThumbnailDecoder() override = default;

  // ImageDecoder::ImageRequest:
  void OnImageDecoded(const SkBitmap& bitmap) override {
    std::move(callback_).Run(&bitmap, base::File::FILE_OK);
  }

  // ImageDecoder::ImageRequest:
  void OnDecodeImageFailed() override {
    std::move(callback_).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_FAILED);
  }

  void Start(const std::string& data, ThumbnailLoader::ImageCallback callback) {
    DCHECK(!callback_);

    // The data sent from the image loader extension should be in form of a data
    // URL.
    GURL data_url(data);
    if (!data_url.is_valid() || !data_url.SchemeIs(url::kDataScheme)) {
      std::move(callback).Run(/*bitmap=*/nullptr,
                              base::File::FILE_ERROR_FAILED);
      return;
    }

    std::string mime_type, charset, image_data;
    if (!net::DataURL::Parse(data_url, &mime_type, &charset, &image_data)) {
      std::move(callback).Run(/*bitmap=*/nullptr,
                              base::File::FILE_ERROR_FAILED);
      return;
    }

    callback_ = std::move(callback);
    ImageDecoder::Start(this, std::move(image_data));
  }

 private:
  ThumbnailLoader::ImageCallback callback_;
};

ThumbnailLoader::ThumbnailLoader(Profile* profile) : profile_(profile) {}

ThumbnailLoader::~ThumbnailLoader() {
  // Run any pending callbacks to clean them up.
  for (auto it = requests_.begin(); it != requests_.end();) {
    std::move(it->second).Run(nullptr, base::File::Error::FILE_ERROR_ABORT);
    it = requests_.erase(it);
  }
}

ThumbnailLoader::ThumbnailRequest::ThumbnailRequest(
    const base::FilePath& file_path,
    const gfx::Size& size)
    : file_path(file_path), size(size) {}

ThumbnailLoader::ThumbnailRequest::~ThumbnailRequest() = default;

base::WeakPtr<ThumbnailLoader> ThumbnailLoader::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

void ThumbnailLoader::Load(const ThumbnailRequest& request,
                           ImageCallback callback) {
  // Get the file's last modified time - this will be used for cache lookup in
  // the image loader extension.
  GURL source_url = extensions::Extension::GetBaseURLFromExtensionId(
      file_manager::kImageLoaderExtensionId);
  file_manager::util::GetMetadataForPath(
      file_manager::util::GetFileSystemContextForSourceURL(profile_,
                                                           source_url),
      request.file_path,
      {storage::FileSystemOperation::GetMetadataField::kIsDirectory,
       storage::FileSystemOperation::GetMetadataField::kLastModified},
      base::BindOnce(&ThumbnailLoader::LoadForFileWithMetadata,
                     weak_factory_.GetWeakPtr(), request, std::move(callback)));
}

void ThumbnailLoader::LoadForFileWithMetadata(
    const ThumbnailRequest& request,
    ImageCallback callback,
    base::File::Error result,
    const base::File::Info& file_info) {
  if (result != base::File::FILE_OK) {
    std::move(callback).Run(/*bitmap=*/nullptr, result);
    return;
  }

  // Short-circuit icons for folders.
  if (file_info.is_directory) {
    // `FILE_ERROR_NOT_A_FILE` is a special value used to signify that the
    // file for which the thumbnail was requested is actually a folder.
    std::move(callback).Run(/*bitmap=*/nullptr,
                            base::File::FILE_ERROR_NOT_A_FILE);
    return;
  }

  // Short-circuit if unsupported.
  if (!IsSupported(request.file_path)) {
    std::move(callback).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_ABORT);
    return;
  }

  GURL thumbnail_url;
  if (!file_manager::util::ConvertAbsoluteFilePathToFileSystemUrl(
          profile_, request.file_path,
          extensions::Extension::GetBaseURLFromExtensionId(
              file_manager::kImageLoaderExtensionId),
          &thumbnail_url)) {
    std::move(callback).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_FAILED);
    return;
  }

  extensions::MessageService* const message_service =
      extensions::MessageService::Get(profile_);
  if (!message_service) {  // May be `nullptr` in tests.
    std::move(callback).Run(/*bitmap=*/nullptr, base::File::FILE_ERROR_FAILED);
    return;
  }

  base::UnguessableToken request_id = base::UnguessableToken::Create();
  requests_[request_id] = std::move(callback);

  // Unfortunately the image loader only supports cropping to square dimensions
  // but a request for a non-cropped, non-square image would result in image
  // distortion. To work around this we always request square images and then
  // crop to requested dimensions on our end if necessary after bitmap decoding.
  const int size = std::max(request.size.width(), request.size.height());

  // Generate an image loader request. The request type is defined in
  // ui/file_manager/image_loader/load_image_request.js.
  base::Value::Dict request_dict;
  request_dict.Set("taskId", base::Value(request_id.ToString()));
  request_dict.Set("url", base::Value(thumbnail_url.spec()));
  request_dict.Set("timestamp", base::TimeToValue(file_info.last_modified));
  // TODO(crbug.com/2650014) : Add an arg to set this to false for sharesheet.
  request_dict.Set("cache", true);
  request_dict.Set("crop", true);
  request_dict.Set("priority", base::Value(1));
  request_dict.Set("width", base::Value(size));
  request_dict.Set("height", base::Value(size));

  std::string request_message;
  base::JSONWriter::Write(request_dict, &request_message);

  // Open a channel to the image loader extension using a message host that send
  // the image loader request.
  auto native_message_host = std::make_unique<ThumbnailLoaderNativeMessageHost>(
      request_id.ToString(), request_message,
      base::BindOnce(&ThumbnailLoader::OnThumbnailLoaded,
                     weak_factory_.GetWeakPtr(), request_id, request.size));
  const extensions::PortId port_id(
      base::UnguessableToken::Create(), 1 /* port_number */,
      true /* is_opener */, extensions::mojom::SerializationFormat::kJson);
  auto native_message_port = std::make_unique<extensions::NativeMessagePort>(
      message_service->GetChannelDelegate(), port_id,
      std::move(native_message_host));
  message_service->OpenChannelToExtension(
      extensions::ChannelEndpoint(profile_), port_id,
      extensions::MessagingEndpoint::ForNativeApp(kNativeMessageHostName),
      std::move(native_message_port), file_manager::kImageLoaderExtensionId,
      GURL(), extensions::mojom::ChannelType::kNative,
      std::string() /* channel_name */);
}

void ThumbnailLoader::OnThumbnailLoaded(
    const base::UnguessableToken& request_id,
    const gfx::Size& requested_size,
    const std::string& data) {
  if (!requests_.count(request_id))
    return;

  if (data.empty()) {
    RespondToRequest(request_id, requested_size, /*bitmap=*/nullptr,
                     base::File::FILE_ERROR_FAILED);
    return;
  }

  auto thumbnail_decoder = std::make_unique<ThumbnailDecoder>();
  ThumbnailDecoder* thumbnail_decoder_ptr = thumbnail_decoder.get();
  thumbnail_decoders_.emplace(request_id, std::move(thumbnail_decoder));
  thumbnail_decoder_ptr->Start(
      data,
      base::BindOnce(&ThumbnailLoader::RespondToRequest,
                     weak_factory_.GetWeakPtr(), request_id, requested_size));
}

void ThumbnailLoader::RespondToRequest(const base::UnguessableToken& request_id,
                                       const gfx::Size& requested_size,
                                       const SkBitmap* bitmap,
                                       base::File::Error error) {
  thumbnail_decoders_.erase(request_id);
  auto request_it = requests_.find(request_id);
  if (request_it == requests_.end())
    return;

  // To work around cropping limitations of the image loader, we requested a
  // square image. If requested dimensions were non-square, we need to perform
  // additional cropping on our end.
  SkBitmap cropped_bitmap;
  if (bitmap) {
    gfx::Rect cropped_rect(0, 0, bitmap->width(), bitmap->height());
    if (cropped_rect.size() != requested_size) {
      cropped_bitmap = *bitmap;
      cropped_rect.ClampToCenteredSize(requested_size);
      bitmap->extractSubset(&cropped_bitmap, gfx::RectToSkIRect(cropped_rect));
    }
  }

  ImageCallback callback = std::move(request_it->second);
  requests_.erase(request_it);
  std::move(callback).Run(cropped_bitmap.isNull() ? bitmap : &cropped_bitmap,
                          error);
}

}  // namespace ash