chromium/chrome/browser/ash/extensions/file_manager/image_loader_private_api.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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ash/extensions/file_manager/image_loader_private_api.h"

#include <utility>

#include "base/base64.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "base/strings/strcat.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/arc/fileapi/arc_documents_provider_root_map.h"
#include "chrome/browser/ash/arc/fileapi/arc_file_system_operation_runner.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/extensions/file_manager/private_api_util.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/pdf/pdf_pref_names.h"
#include "chrome/browser/pdf/pdf_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/image_loader_private.h"
#include "chrome/services/pdf/public/mojom/pdf_service.mojom.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "net/base/mime_sniffer.h"
#include "net/base/mime_util.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/common/file_system/file_system_types.h"
#include "storage/common/file_system/file_system_util.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkData.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkStream.h"
#include "third_party/skia/include/encode/SkPngEncoder.h"
#include "ui/gfx/geometry/size.h"

namespace extensions {
namespace {

constexpr char kMimeTypeImagePng[] = "image/png";

// Encodes binary data as a data URL.
std::string MakeThumbnailDataUrlOnThreadPool(const std::string& mimeType,
                                             base::span<const uint8_t> data) {
  base::AssertLongCPUWorkAllowed();
  return base::StrCat(
      {"data:", mimeType, ";base64,", base::Base64Encode(data)});
}

// Converts bitmap to a PNG image and encodes it as a data URL.
std::string ConvertAndEncode(const SkBitmap& bitmap) {
  if (bitmap.isNull()) {
    DLOG(WARNING) << "Got an invalid bitmap";
    return std::string();
  }
  SkDynamicMemoryWStream stream;
  if (!SkPngEncoder::Encode(&stream, bitmap.pixmap(), {}) ||
      !stream.bytesWritten()) {
    DLOG(WARNING) << "Thumbnail encoding error";
    return std::string();
  }
  sk_sp<SkData> png_data = stream.detachAsData();
  return MakeThumbnailDataUrlOnThreadPool(
      kMimeTypeImagePng, base::make_span(png_data->bytes(), png_data->size()));
}

// The maximum size of the input PDF file for which thumbnails are generated.
constexpr uint32_t kMaxPdfSizeInBytes = 1024u * 1024u;

// A function that performs IO operations to read and render PDF thumbnail
// Must be run by a blocking task runner.
std::string ReadLocalPdf(const base::FilePath& pdf_file_path) {
  int64_t file_size;
  if (!base::GetFileSize(pdf_file_path, &file_size)) {
    DLOG(ERROR) << "Failed to get file size of " << pdf_file_path;
    return std::string();
  }
  if (file_size > kMaxPdfSizeInBytes) {
    DLOG(ERROR) << "File " << pdf_file_path << " is too large " << file_size;
    return std::string();
  }
  std::string contents;
  if (!base::ReadFileToString(pdf_file_path, &contents)) {
    DLOG(ERROR) << "Failed to load " << pdf_file_path;
    return std::string();
  }
  return contents;
}

// Max size of a file returned by DocumentsProvider openDocumentThumbnail()
// Arbitrary, but sets some sensible upper limit.
constexpr int kDocumentsProviderMaxThumbnailSizeInBytes = 1024 * 1024;

std::string ReadMojoHandleToDataUrl(mojo::PlatformHandle&& handle) {
  base::ScopedFILE file(FileToFILE(base::File(handle.ReleaseFD()), "rb"));
  std::string contents;
  if (!base::ReadStreamToStringWithMaxSize(
          file.get(), kDocumentsProviderMaxThumbnailSizeInBytes, &contents)) {
    return std::string();
  }
  std::string mime_type;
  if (!net::SniffMimeTypeFromLocalData(contents, &mime_type)) {
    return std::string();
  }
  if (!net::MatchesMimeType("image/*", mime_type)) {
    return std::string();
  }
  return MakeThumbnailDataUrlOnThreadPool(mime_type,
                                          base::as_byte_span(contents));
}

}  // namespace

ImageLoaderPrivateGetThumbnailFunction::
    ImageLoaderPrivateGetThumbnailFunction() = default;

void ImageLoaderPrivateGetThumbnailFunction::SendEncodedThumbnail(
    std::string thumbnail_data_url) {
  Respond(WithArguments(std::move(thumbnail_data_url)));
}

ImageLoaderPrivateGetDriveThumbnailFunction::
    ImageLoaderPrivateGetDriveThumbnailFunction() {
  SetWarningThresholds(base::Seconds(5), base::Minutes(1));
}

ImageLoaderPrivateGetDriveThumbnailFunction::
    ~ImageLoaderPrivateGetDriveThumbnailFunction() = default;

ExtensionFunction::ResponseAction
ImageLoaderPrivateGetDriveThumbnailFunction::Run() {
  using extensions::api::image_loader_private::GetDriveThumbnail::Params;
  const std::optional<Params> params = Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  Profile* const profile = Profile::FromBrowserContext(browser_context());
  scoped_refptr<storage::FileSystemContext> file_system_context =
      file_manager::util::GetFileManagerFileSystemContext(profile);
  const GURL url = GURL(params->url);
  const storage::FileSystemURL file_system_url =
      file_system_context->CrackURLInFirstPartyContext(url);

  if (file_system_url.type() != storage::kFileSystemTypeDriveFs) {
    return RespondNow(Error("Expected a Drivefs URL"));
  }

  auto* drive_integration_service =
      drive::DriveIntegrationServiceFactory::FindForProfile(profile);
  if (!drive_integration_service) {
    return RespondNow(Error("Drive service not available"));
  }

  base::FilePath path;
  if (!drive_integration_service->GetRelativeDrivePath(file_system_url.path(),
                                                       &path)) {
    return RespondNow(Error("File not found"));
  }

  auto* drivefs_interface = drive_integration_service->GetDriveFsInterface();
  if (!drivefs_interface) {
    return RespondNow(Error("Drivefs not available"));
  }

  drivefs_interface->GetThumbnail(
      path, params->crop_to_square,
      mojo::WrapCallbackWithDefaultInvokeIfNotRun(
          base::BindOnce(
              &ImageLoaderPrivateGetDriveThumbnailFunction::GotThumbnail, this),
          std::nullopt));
  return RespondLater();
}

void ImageLoaderPrivateGetDriveThumbnailFunction::GotThumbnail(
    const std::optional<std::vector<uint8_t>>& data) {
  if (!data) {
    Respond(WithArguments(""));
    return;
  }
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&MakeThumbnailDataUrlOnThreadPool, kMimeTypeImagePng,
                     *data),
      base::BindOnce(
          &ImageLoaderPrivateGetDriveThumbnailFunction::SendEncodedThumbnail,
          this));
}

ImageLoaderPrivateGetPdfThumbnailFunction::
    ImageLoaderPrivateGetPdfThumbnailFunction() = default;

ImageLoaderPrivateGetPdfThumbnailFunction::
    ~ImageLoaderPrivateGetPdfThumbnailFunction() = default;

ExtensionFunction::ResponseAction
ImageLoaderPrivateGetPdfThumbnailFunction::Run() {
  using extensions::api::image_loader_private::GetPdfThumbnail::Params;
  const std::optional<Params> params = Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  scoped_refptr<storage::FileSystemContext> file_system_context =
      file_manager::util::GetFileManagerFileSystemContext(
          Profile::FromBrowserContext(browser_context()));
  const GURL url = GURL(params->url);
  const storage::FileSystemURL file_system_url =
      file_system_context->CrackURLInFirstPartyContext(url);

  if (file_system_url.type() != storage::kFileSystemTypeLocal) {
    return RespondNow(Error("Expected a native local URL"));
  }

  base::FilePath path =
      file_manager::util::GetLocalPathFromURL(file_system_context, url);
  if (path.empty() ||
      base::FilePath::CompareIgnoreCase(path.Extension(), ".pdf") != 0) {
    return RespondNow(Error("Can only handle PDF files"));
  }

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
      base::BindOnce(&ReadLocalPdf, std::move(path)),
      base::BindOnce(&ImageLoaderPrivateGetPdfThumbnailFunction::FetchThumbnail,
                     this, gfx::Size(params->width, params->height)));
  return RespondLater();
}

void ImageLoaderPrivateGetPdfThumbnailFunction::FetchThumbnail(
    const gfx::Size& size,
    const std::string& content) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (content.empty()) {
    Respond(Error("Failed to read PDF file"));
    return;
  }
  auto pdf_region = base::ReadOnlySharedMemoryRegion::Create(content.size());
  if (!pdf_region.IsValid()) {
    Respond(Error("Failed allocate memory for PDF file"));
    return;
  }
  base::as_writable_chars(base::span(pdf_region.mapping)).copy_from(content);
  DCHECK(!pdf_thumbnailer_.is_bound());
  GetPdfService()->BindPdfThumbnailer(
      pdf_thumbnailer_.BindNewPipeAndPassReceiver());
  pdf_thumbnailer_.set_disconnect_handler(base::BindOnce(
      &ImageLoaderPrivateGetPdfThumbnailFunction::ThumbnailDisconnected,
      base::Unretained(this)));
  const PrefService* prefs =
      Profile::FromBrowserContext(browser_context())->GetPrefs();
  if (prefs && prefs->IsManagedPreference(prefs::kPdfUseSkiaRendererEnabled)) {
    pdf_thumbnailer_->SetUseSkiaRendererPolicy(
        prefs->GetBoolean(prefs::kPdfUseSkiaRendererEnabled));
  }
  auto params = pdf::mojom::ThumbParams::New(
      /*size_px=*/size, /*dpi=*/gfx::Size(kDpi, kDpi),
      /*stretch_to_bounds=*/false, /*keep_aspect_ratio=*/true);
  pdf_thumbnailer_->GetThumbnail(
      std::move(params), std::move(pdf_region.region),
      base::BindOnce(&ImageLoaderPrivateGetPdfThumbnailFunction::GotThumbnail,
                     this));
}

void ImageLoaderPrivateGetPdfThumbnailFunction::ThumbnailDisconnected() {
  DLOG(WARNING) << "PDF thumbnail disconnected";
  Respond(Error("PDF service disconnected"));
}

void ImageLoaderPrivateGetPdfThumbnailFunction::GotThumbnail(
    const SkBitmap& bitmap) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  pdf_thumbnailer_.reset();
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&ConvertAndEncode, bitmap),
      base::BindOnce(
          &ImageLoaderPrivateGetPdfThumbnailFunction::SendEncodedThumbnail,
          this));
}

ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::
    ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction() = default;

ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::
    ~ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction() = default;

ExtensionFunction::ResponseAction
ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::Run() {
  using extensions::api::image_loader_private::
      GetArcDocumentsProviderThumbnail::Params;
  const std::optional<Params> params = Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  scoped_refptr<storage::FileSystemContext> file_system_context =
      file_manager::util::GetFileManagerFileSystemContext(
          Profile::FromBrowserContext(browser_context()));
  const GURL url = GURL(params->url);
  const storage::FileSystemURL file_system_url =
      file_system_context->CrackURLInFirstPartyContext(url);

  auto* root_map =
      arc::ArcDocumentsProviderRootMap::GetForBrowserContext(browser_context());
  if (!root_map) {
    return RespondNow(Error("File not found"));
  }
  base::FilePath path;
  auto* root = root_map->ParseAndLookup(file_system_url, &path);
  if (!root) {
    return RespondNow(Error("File not found"));
  }

  const gfx::Size size_hint(params->width_hint, params->height_hint);
  root->GetExtraFileMetadata(
      path, base::BindOnce(
                &ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::
                    OnGetExtraFileMetadata,
                this, size_hint, file_system_url));
  return RespondLater();
}

void ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::
    OnGetExtraFileMetadata(
        const gfx::Size& size_hint,
        const storage::FileSystemURL& file_system_url,
        base::File::Error result,
        const arc::ArcDocumentsProviderRoot::ExtraFileMetadata& metadata) {
  if (result != base::File::FILE_OK) {
    Respond(Error(base::File::ErrorToString(result)));
    return;
  }

  if (!metadata.supports_thumbnail) {
    Respond(WithArguments(""));
    return;
  }

  file_manager::util::ConvertToContentUrls(
      Profile::FromBrowserContext(browser_context()),
      std::vector<storage::FileSystemURL>{file_system_url},
      base::BindOnce(
          &ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::
              GotContentUrls,
          this, size_hint));
}

void ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::GotContentUrls(
    const gfx::Size& size_hint,
    const std::vector<GURL>& urls,
    const std::vector<base::FilePath>& paths_to_share) {
  if (urls.size() != 1 || urls[0] == GURL()) {
    Respond(Error("Failed to resolve to countent URL"));
    return;
  }
  if (!paths_to_share.empty()) {
    Respond(
        Error("paths_to_share should be empty when getting "
              "ArcDocumentsProviderThumbnail URL"));
    return;
  }

  auto* runner = arc::ArcFileSystemOperationRunner::GetForBrowserContext(
      browser_context());
  if (!runner) {
    Respond(Error("ArcFileSystemOperationRunner is unavailable"));
    return;
  }
  const auto& url = urls[0];
  runner->OpenThumbnail(
      url, size_hint,
      mojo::WrapCallbackWithDefaultInvokeIfNotRun(
          base::BindOnce(
              &ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::
                  GotArcThumbnailFileHandle,
              this),
          mojo::ScopedHandle()));
}

void ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::
    GotArcThumbnailFileHandle(mojo::ScopedHandle handle) {
  mojo::PlatformHandle platform_handle =
      mojo::UnwrapPlatformHandle(std::move(handle));

  if (!platform_handle.is_valid()) {
    Respond(Error("File not found"));
    return;
  }

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&ReadMojoHandleToDataUrl, std::move(platform_handle)),
      base::BindOnce(
          &ImageLoaderPrivateGetArcDocumentsProviderThumbnailFunction::
              SendEncodedThumbnail,
          this));
}

}  // namespace extensions