chromium/chrome/browser/extensions/api/printing/print_job_submitter.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/extensions/api/printing/print_job_submitter.h"

#include <cstring>
#include <utility>

#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "chrome/browser/extensions/api/printing/printing_api_utils.h"
#include "chrome/browser/printing/pdf_blob_data_flattener.h"
#include "chrome/browser/printing/print_job_controller.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/extensions/extensions_dialogs.h"
#include "chrome/common/pref_names.h"
#include "chromeos/crosapi/mojom/local_printer.mojom.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/blob_reader.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/image_loader.h"
#include "extensions/common/extension.h"
#include "printing/metafile_skia.h"
#include "printing/print_settings.h"
#include "printing/printing_utils.h"
#include "third_party/skia/include/codec/SkCodec.h"
#include "third_party/skia/include/codec/SkPngDecoder.h"
#include "third_party/skia/include/core/SkCanvas.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/docs/SkPDFDocument.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/native_window_tracker.h"

namespace extensions {

namespace {

constexpr char kPdfMimeType[] = "application/pdf";
constexpr char kPngMimeType[] = "image/png";

constexpr char kUnsupportedContentType[] = "Unsupported content type";
constexpr char kInvalidTicket[] = "Invalid ticket";
constexpr char kInvalidPrinterId[] = "Invalid printer ID";
constexpr char kPrinterUnavailable[] = "Printer is unavailable at the moment";
constexpr char kUnsupportedTicket[] =
    "Ticket is unsupported on the given printer";
constexpr char kInvalidData[] = "Invalid document";
constexpr char kPrintingFailed[] = "Printing failed";

constexpr int kIconSize = 64;

// There is no easy way to interact with UI dialogs, so we want to have an
// ability to skip this stage for browser tests.
bool g_skip_confirmation_dialog_for_testing = false;

// Returns true if print job request dialog should be shown.
bool IsUserConfirmationRequired(content::BrowserContext* browser_context,
                                const std::string& extension_id) {
  if (g_skip_confirmation_dialog_for_testing)
    return false;
  const base::Value::List& list =
      Profile::FromBrowserContext(browser_context)
          ->GetPrefs()
          ->GetList(prefs::kPrintingAPIExtensionsAllowlist);
  return !base::Contains(list, base::Value(extension_id));
}

}  // namespace

PrintJobSubmitter::PrintJobSubmitter(
    gfx::NativeWindow native_window,
    content::BrowserContext* browser_context,
    printing::PrintJobController* print_job_controller,
    printing::PdfBlobDataFlattener* pdf_blob_data_flattener,
    scoped_refptr<const extensions::Extension> extension,
    api::printing::SubmitJobRequest request,
    crosapi::mojom::LocalPrinter* local_printer,
    SubmitJobCallback callback)
    : native_window_(native_window),
      browser_context_(browser_context),
      print_job_controller_(print_job_controller),
      pdf_blob_data_flattener_(*pdf_blob_data_flattener),
      extension_(extension),
      request_(std::move(request)),
      local_printer_(local_printer),
      callback_(std::move(callback)) {
  DCHECK(extension);
  if (native_window)
    native_window_tracker_ = views::NativeWindowTracker::Create(native_window);
}

PrintJobSubmitter::~PrintJobSubmitter() = default;

// static
void PrintJobSubmitter::Run(std::unique_ptr<PrintJobSubmitter> submitter) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(submitter->callback_);
  PrintJobSubmitter* ptr = submitter.get();
  ptr->callback_ = std::move(ptr->callback_)
                       .Then(base::OnceClosure(
                           base::DoNothingWithBoundArgs(std::move(submitter))));
  ptr->Start();
}

void PrintJobSubmitter::Start() {
  if (!CheckContentType()) {
    FireErrorCallback(kUnsupportedContentType);
    return;
  }
  if (!CheckPrintTicket()) {
    FireErrorCallback(kInvalidTicket);
    return;
  }
  CheckPrinter();
}

bool PrintJobSubmitter::CheckContentType() const {
  return request_.job.content_type == kPdfMimeType ||
         request_.job.content_type == kPngMimeType;
}

bool PrintJobSubmitter::CheckPrintTicket() {
  settings_ = ParsePrintTicket(request_.job.ticket.ToValue());
  if (!settings_)
    return false;
  settings_->set_title(base::UTF8ToUTF16(request_.job.title));
  settings_->set_device_name(base::UTF8ToUTF16(request_.job.printer_id));
  return true;
}

void PrintJobSubmitter::CheckPrinter() {
  CHECK(local_printer_);
  local_printer_->GetCapability(
      request_.job.printer_id,
      base::BindOnce(&PrintJobSubmitter::CheckCapabilitiesCompatibility,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PrintJobSubmitter::CheckCapabilitiesCompatibility(
    crosapi::mojom::CapabilitiesResponsePtr caps) {
  if (!caps) {
    FireErrorCallback(kInvalidPrinterId);
    return;
  }
  printer_name_ = base::UTF8ToUTF16(caps->basic_info->name);
  if (!caps->capabilities) {
    FireErrorCallback(kPrinterUnavailable);
    return;
  }
  if (!CheckSettingsAndCapabilitiesCompatibility(*settings_,
                                                 *caps->capabilities)) {
    FireErrorCallback(kUnsupportedTicket);
    return;
  }
  ReadDocumentData();
}

void PrintJobSubmitter::ReadDocumentData() {
  CHECK(request_.document_blob_uuid);
  if (request_.job.content_type == kPdfMimeType) {
    pdf_blob_data_flattener_->ReadAndFlattenPdf(
        browser_context_->GetBlobRemote(*request_.document_blob_uuid),
        base::BindOnce(&PrintJobSubmitter::OnPdfReadAndFlattened,
                       weak_ptr_factory_.GetWeakPtr()));
  } else {
    BlobReader::Read(
        browser_context_->GetBlobRemote(*request_.document_blob_uuid),
        base::BindOnce(&PrintJobSubmitter::OnImageDataRead,
                       weak_ptr_factory_.GetWeakPtr()));
  }
}

void PrintJobSubmitter::OnPdfReadAndFlattened(
    std::unique_ptr<printing::FlattenPdfResult> result) {
  if (!result) {
    FireErrorCallback(kInvalidData);
    return;
  }

  flatten_pdf_result_ = std::move(result);

  // Directly submit the job if the extension is allowed.
  if (!IsUserConfirmationRequired(browser_context_, extension_->id())) {
    StartPrintJob();
    return;
  }
  extensions::ImageLoader::Get(browser_context_)
      ->LoadImageAtEveryScaleFactorAsync(
          extension_.get(), gfx::Size(kIconSize, kIconSize),
          base::BindOnce(&PrintJobSubmitter::ShowPrintJobConfirmationDialog,
                         weak_ptr_factory_.GetWeakPtr()));
}

// Handle PNG input by converting it to PDF, and then sending the resulting
// PDF to the printer like we would if it had been submitted directly.
void PrintJobSubmitter::OnImageDataRead(std::string data,
                                        int64_t /*blob_total_size*/) {
  sk_sp<SkData> image_data = SkData::MakeWithCopy(data.data(), data.size());
  std::unique_ptr<SkCodec> codec = SkPngDecoder::Decode(image_data, nullptr);
  if (!codec) {
    LOG(WARNING) << "Failed to decode PNG";
    FireErrorCallback(kInvalidData);
    return;
  }

  auto img_tuple = codec->getImage();
  CHECK(std::get<1>(img_tuple) == SkCodec::Result::kSuccess);
  sk_sp<SkImage> image = std::get<0>(img_tuple);

  SkDynamicMemoryWStream buffer;
  SkPDF::Metadata metadata;
  auto pdf_document = SkPDF::MakeDocument(&buffer, metadata);
  CHECK(pdf_document);
  SkCanvas* canvas = pdf_document->beginPage(image->width(), image->height());
  canvas->drawImage(image, 0, 0);
  pdf_document->endPage();
  pdf_document->close();

  // The generated PDF consists of a single image and does not contain forms,
  // JavaScript, etc. So it is already flattened, and can be treated as such.
  sk_sp<SkData> pdf_data = buffer.detachAsData();
  auto metafile = std::make_unique<printing::MetafileSkia>();
  CHECK(metafile->InitFromData({pdf_data->bytes(), pdf_data->size()}));
  OnPdfReadAndFlattened(
      std::make_unique<printing::FlattenPdfResult>(std::move(metafile), 1));
}

void PrintJobSubmitter::ShowPrintJobConfirmationDialog(
    const gfx::Image& extension_icon) {
  // If the browser window was closed during API request handling, change
  // |native_window_| appropriately.
  if (native_window_tracker_ &&
      native_window_tracker_->WasNativeWindowDestroyed())
    native_window_ = gfx::NativeWindow();

  extensions::ShowPrintJobConfirmationDialog(
      native_window_, extension_->id(), base::UTF8ToUTF16(extension_->name()),
      extension_icon.AsImageSkia(), settings_->title(), printer_name_,
      base::BindOnce(&PrintJobSubmitter::OnPrintJobConfirmationDialogClosed,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PrintJobSubmitter::OnPrintJobConfirmationDialogClosed(bool accepted) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(callback_);
  // If the user hasn't accepted a print job or the extension is
  // unloaded/disabled by the time the dialog is closed, reject the request.
  if (!accepted || !ExtensionRegistry::Get(browser_context_)
                        ->enabled_extensions()
                        .Contains(extension_->id())) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback_), base::unexpected(std::nullopt)));
    return;
  }
  StartPrintJob();
}

void PrintJobSubmitter::StartPrintJob() {
  CHECK(extension_);
  CHECK(settings_);
  CHECK(flatten_pdf_result_);

  auto flatten_pdf_result = std::move(flatten_pdf_result_);
  uint32_t page_count = flatten_pdf_result->page_count;
  print_job_controller_->CreatePrintJob(
      std::move(flatten_pdf_result->flattened_pdf), std::move(settings_),
      page_count, crosapi::mojom::PrintJob::Source::kExtension,
      extension_->id(),
      base::BindOnce(&PrintJobSubmitter::OnPrintJobCreated,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PrintJobSubmitter::OnPrintJobCreated(
    std::optional<printing::PrintJobCreatedInfo> info) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!info) {
    FireErrorCallback(kPrintingFailed);
    return;
  }
  DCHECK(callback_);
  std::move(callback_).Run(std::move(*info));
}

void PrintJobSubmitter::FireErrorCallback(const std::string& error) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(callback_);
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback_), base::unexpected(error)));
}

// static
base::AutoReset<bool> PrintJobSubmitter::DisablePdfFlatteningForTesting() {
  return printing::PdfBlobDataFlattener::DisablePdfFlatteningForTesting();
}

// static
void PrintJobSubmitter::SkipConfirmationDialogForTesting() {
  g_skip_confirmation_dialog_for_testing = true;
}

}  // namespace extensions