chromium/chrome/browser/ui/webui/print_preview/local_printer_handler_chromeos.cc

// Copyright 2016 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/webui/print_preview/local_printer_handler_chromeos.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/types/optional_util.h"
#include "base/values.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/ui/webui/print_preview/print_preview_utils.h"
#include "chrome/common/printing/printer_capabilities.h"
#include "chromeos/crosapi/mojom/local_printer.mojom.h"
#include "components/device_event_log/device_event_log.h"
#include "content/public/browser/browser_thread.h"
#include "printing/backend/print_backend.h"
#include "printing/backend/print_backend_consts.h"
#include "printing/backend/printing_restrictions.h"
#include "printing/mojom/print.mojom.h"
#include "printing/print_job_constants.h"
#include "printing/print_settings_conversion_chromeos.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/local_printer_ash.h"
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/lacros/lacros_service.h"
#endif

namespace printing {

namespace {

void OnGetPrintersComplete(
    LocalPrinterHandlerChromeos::AddedPrintersCallback callback,
    std::vector<crosapi::mojom::LocalDestinationInfoPtr> printers) {
  if (!printers.empty()) {
    base::Value::List list;
    for (const crosapi::mojom::LocalDestinationInfoPtr& p : printers) {
      list.Append(LocalPrinterHandlerChromeos::PrinterToValue(*p));
    }
    std::move(callback).Run(std::move(list));
  }
}

base::Value::Dict AddProfileUsernameToJobSettings(
    base::Value::Dict settings,
    const std::optional<std::string>& username) {
  if (username.has_value() && !username->empty()) {
    settings.Set(kSettingUsername, *username);
    settings.Set(kSettingSendUserInfo, true);
  }
  return settings;
}

base::Value::Dict AddOAuthTokenToJobSettings(
    base::Value::Dict settings,
    crosapi::mojom::GetOAuthAccessTokenResultPtr oauth_result) {
  if (oauth_result->is_token()) {
    settings.Set(kSettingChromeOSAccessOAuthToken,
                 oauth_result->get_token()->token);
  } else if (oauth_result->is_error()) {
    LOG(ERROR) << "Error when obtaining an oauth token for a local printer";
  }
  return settings;
}

base::Value::Dict AddIppClientInfoToJobSettings(
    base::Value::Dict settings,
    std::vector<mojom::IppClientInfoPtr> client_infos) {
  std::vector<printing::mojom::IppClientInfo> client_info_list;
  client_info_list.reserve(client_infos.size());
  for (const printing::mojom::IppClientInfoPtr& client_info : client_infos) {
    client_info_list.emplace_back(std::move(*client_info));
  }
  if (!client_info_list.empty()) {
    settings.Set(kSettingIppClientInfo,
                 ConvertClientInfoToJobSetting(client_info_list));
  }
  return settings;
}

// Combines the 16 bit DPI values into a single 32 bit int. Places the DPI width
// value into bits 31-16 and the DPI height value into bits 15-0.
int HashDpiValue(int width, int height) {
  CHECK(width <= std::numeric_limits<uint16_t>::max() &&
        height <= std::numeric_limits<uint16_t>::max());
  return (width << 16) + height;
}

void RecordDpi(const PrinterSemanticCapsAndDefaults& capabilities) {
  const int default_width = capabilities.default_dpi.width();
  const int default_height = capabilities.default_dpi.height();
  if (default_width <= std::numeric_limits<uint16_t>::max() &&
      default_height <= std::numeric_limits<uint16_t>::max()) {
    base::UmaHistogramSparse("Printing.CUPS.DPI.Default",
                             HashDpiValue(default_width, default_height));
  }

  const std::vector<gfx::Size> dpis = capabilities.dpis;
  const int dpis_count = dpis.size();
  base::UmaHistogramCounts100("Printing.CUPS.DPI.Count", dpis_count);
  if (dpis_count == 0) {
    return;
  }

  std::optional<std::pair<int, int>> max_dpi;
  std::optional<std::pair<int, int>> min_dpi;
  for (const auto& dpi : dpis) {
    const int width = dpi.width();
    const int height = dpi.height();
    if (width <= std::numeric_limits<uint16_t>::max() &&
        height <= std::numeric_limits<uint16_t>::max()) {
      base::UmaHistogramSparse("Printing.CUPS.DPI.AllValues",
                               HashDpiValue(width, height));

      const int dpi_total = width * height;
      if (!min_dpi || dpi_total < (min_dpi->first * min_dpi->second)) {
        min_dpi = std::pair<int, int>(width, height);
      }
      if (!max_dpi || dpi_total > (max_dpi->first * max_dpi->second)) {
        max_dpi = std::pair<int, int>(width, height);
      }
    }
  }

  if (min_dpi) {
    base::UmaHistogramSparse("Printing.CUPS.DPI.Min",
                             HashDpiValue(min_dpi->first, min_dpi->second));
  }
  if (max_dpi) {
    base::UmaHistogramSparse("Printing.CUPS.DPI.Max",
                             HashDpiValue(max_dpi->first, max_dpi->second));
  }
}

}  // namespace

// static
std::unique_ptr<LocalPrinterHandlerChromeos>
LocalPrinterHandlerChromeos::Create(
    content::WebContents* preview_web_contents) {
  auto handler =
      std::make_unique<LocalPrinterHandlerChromeos>(preview_web_contents);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  DCHECK(crosapi::CrosapiManager::IsInitialized());
  handler->local_printer_ =
      crosapi::CrosapiManager::Get()->crosapi_ash()->local_printer_ash();
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
  chromeos::LacrosService* service = chromeos::LacrosService::Get();
  if (!service->IsAvailable<crosapi::mojom::LocalPrinter>()) {
    PRINTER_LOG(ERROR) << "Local printer not available (Create)";
    return handler;
  }
  handler->local_printer_ =
      service->GetRemote<crosapi::mojom::LocalPrinter>().get();
  handler->local_printer_version_ =
      service->GetInterfaceVersion<crosapi::mojom::LocalPrinter>();
#endif
  return handler;
}

std::unique_ptr<LocalPrinterHandlerChromeos>
LocalPrinterHandlerChromeos::CreateForTesting(
    crosapi::mojom::LocalPrinter* local_printer) {
  auto handler = std::make_unique<LocalPrinterHandlerChromeos>(nullptr);
  handler->local_printer_ = local_printer;
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  handler->local_printer_version_ = INT_MAX;
#endif
  return handler;
}

LocalPrinterHandlerChromeos::LocalPrinterHandlerChromeos(
    content::WebContents* preview_web_contents)
    : preview_web_contents_(preview_web_contents) {}

LocalPrinterHandlerChromeos::~LocalPrinterHandlerChromeos() = default;

// static
base::Value::Dict LocalPrinterHandlerChromeos::PrinterToValue(
    const crosapi::mojom::LocalDestinationInfo& printer) {
  base::Value::Dict value;
  value.Set(kSettingDeviceName, printer.id);
  value.Set(kSettingPrinterName, printer.name);
  value.Set(kSettingPrinterDescription, printer.description);
  value.Set(kCUPSEnterprisePrinter, printer.configured_via_policy);
  value.Set(kPrinterStatus, printer.printer_status
                                ? StatusToValue(*printer.printer_status)
                                : base::Value::Dict());
  return value;
}

// static
base::Value::Dict LocalPrinterHandlerChromeos::CapabilityToValue(
    crosapi::mojom::CapabilitiesResponsePtr caps) {
  if (!caps) {
    return base::Value::Dict();
  }

  if (caps->capabilities) {
    RecordDpi(caps->capabilities.value());
  }

  return AssemblePrinterSettings(
      caps->basic_info->id,
      PrinterBasicInfo(
          caps->basic_info->id, caps->basic_info->name,
          caps->basic_info->description, 0, false,
          PrinterBasicInfoOptions{
              {kCUPSEnterprisePrinter, caps->basic_info->configured_via_policy
                                           ? kValueTrue
                                           : kValueFalse}}),
      caps->has_secure_protocol, base::OptionalToPtr(caps->capabilities));
}

// static
base::Value::Dict LocalPrinterHandlerChromeos::StatusToValue(
    const crosapi::mojom::PrinterStatus& status) {
  base::Value::Dict dict;
  dict.Set("printerId", status.printer_id);
  dict.Set("timestamp",
           status.timestamp.InMillisecondsFSinceUnixEpochIgnoringNull());
  base::Value::List status_reasons;
  for (const crosapi::mojom::StatusReasonPtr& reason_ptr :
       status.status_reasons) {
    base::Value::Dict status_reason;
    status_reason.Set("reason", static_cast<int>(reason_ptr->reason));
    status_reason.Set("severity", static_cast<int>(reason_ptr->severity));
    status_reasons.Append(std::move(status_reason));
  }
  dict.Set("statusReasons", std::move(status_reasons));
  return dict;
}

void LocalPrinterHandlerChromeos::Reset() {}

void LocalPrinterHandlerChromeos::GetDefaultPrinter(
    DefaultPrinterCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // TODO(b/172229329): Add default printers to ChromeOS.
  std::move(callback).Run(std::string());
}

void LocalPrinterHandlerChromeos::StartGetPrinters(
    AddedPrintersCallback callback,
    GetPrintersDoneCallback done_callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!local_printer_) {
    PRINTER_LOG(ERROR) << "Local printer not available (StartGetPrinters)";
    std::move(done_callback).Run();
    return;
  }
  local_printer_->GetPrinters(
      base::BindOnce(OnGetPrintersComplete, std::move(callback))
          .Then(std::move(done_callback)));
}

void LocalPrinterHandlerChromeos::StartGetCapability(
    const std::string& device_name,
    GetCapabilityCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!local_printer_) {
    PRINTER_LOG(ERROR) << "Local printer not available (StartGetCapability)";
    std::move(callback).Run(base::Value::Dict());
    return;
  }
  local_printer_->GetCapability(
      device_name, base::BindOnce(CapabilityToValue).Then(std::move(callback)));
}

void LocalPrinterHandlerChromeos::StartPrint(
    const std::u16string& job_title,
    base::Value::Dict settings,
    scoped_refptr<base::RefCountedMemory> print_data,
    PrintCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  size_t size_in_kb = print_data->size() / 1024;
  base::UmaHistogramMemoryKB("Printing.CUPS.PrintDocumentSize", size_in_kb);

  std::string printer_id = *settings.FindString(kSettingDeviceName);
  auto call_start_local_print_callback =
      base::BindOnce(&LocalPrinterHandlerChromeos::CallStartLocalPrint,
                     weak_ptr_factory_.GetWeakPtr(), std::move(print_data),
                     std::move(callback));
  GetAshJobSettings(std::move(printer_id),
                    std::move(call_start_local_print_callback),
                    std::move(settings));
}

void LocalPrinterHandlerChromeos::GetAshJobSettingsForTesting(
    std::string printer_id,
    AshJobSettingsCallback callback,
    base::Value::Dict settings) {
  GetAshJobSettings(std::move(printer_id), std::move(callback),
                    std::move(settings));
}

void LocalPrinterHandlerChromeos::GetAshJobSettings(
    std::string printer_id,
    AshJobSettingsCallback callback,
    base::Value::Dict settings) {
  if (!local_printer_) {
    LOG(ERROR) << "Local printer not available";
    std::move(callback).Run(std::move(settings));
    return;
  }

  // Start a chain of async calls, `GetUsernamePerPolicy()` -> `GetOAuthToken()`
  // -> `GetIppClientInfo()` -> `callback()`.
  auto get_client_info_callback = base::BindOnce(
      &LocalPrinterHandlerChromeos::GetIppClientInfo,
      weak_ptr_factory_.GetWeakPtr(), printer_id, std::move(callback));
  auto get_oauth_token_callback =
      base::BindOnce(&LocalPrinterHandlerChromeos::GetOAuthToken,
                     weak_ptr_factory_.GetWeakPtr(), printer_id,
                     std::move(get_client_info_callback));
  GetUsernamePerPolicy(std::move(get_oauth_token_callback),
                       std::move(settings));
}

void LocalPrinterHandlerChromeos::GetUsernamePerPolicy(
    AshJobSettingsCallback callback,
    base::Value::Dict settings) const {
  auto add_profile_username_callback =
      base::BindOnce(AddProfileUsernameToJobSettings, std::move(settings))
          .Then(std::move(callback));

#if BUILDFLAG(IS_CHROMEOS_LACROS)
  if (local_printer_version_ <
      int{crosapi::mojom::LocalPrinter::MethodMinVersions::
              kGetUsernamePerPolicyMinVersion}) {
    LOG(WARNING) << "Ash LocalPrinter version " << local_printer_version_
                 << " does not support GetUsernamePerPolicy().";
    std::move(add_profile_username_callback).Run(std::nullopt);
    return;
  }
#endif

  local_printer_->GetUsernamePerPolicy(
      std::move(add_profile_username_callback));
}

void LocalPrinterHandlerChromeos::GetOAuthToken(
    const std::string& printer_id,
    AshJobSettingsCallback callback,
    base::Value::Dict settings) const {
  auto add_oauth_token_callback =
      base::BindOnce(AddOAuthTokenToJobSettings, std::move(settings))
          .Then(std::move(callback));

#if BUILDFLAG(IS_CHROMEOS_LACROS)
  if (local_printer_version_ <
      int{crosapi::mojom::LocalPrinter::MethodMinVersions::
              kGetOAuthAccessTokenMinVersion}) {
    LOG(WARNING) << "Ash LocalPrinter version " << local_printer_version_
                 << " does not support GetOAuthToken().";
    std::move(add_oauth_token_callback)
        .Run(crosapi::mojom::GetOAuthAccessTokenResult::NewNone(
            crosapi::mojom::OAuthNotNeeded::New()));
    return;
  }
#endif

  local_printer_->GetOAuthAccessToken(printer_id,
                                      std::move(add_oauth_token_callback));
}

void LocalPrinterHandlerChromeos::GetIppClientInfo(
    const std::string& printer_id,
    AshJobSettingsCallback callback,
    base::Value::Dict settings) const {
  auto add_ipp_client_info_callback =
      base::BindOnce(AddIppClientInfoToJobSettings, std::move(settings))
          .Then(std::move(callback));

  if (printer_id.empty()) {
    LOG(ERROR) << "Cannot call GetIppClientInfo: empty printer_id";
    std::move(add_ipp_client_info_callback).Run({});
    return;
  }

#if BUILDFLAG(IS_CHROMEOS_LACROS)
  if (local_printer_version_ <
      int{crosapi::mojom::LocalPrinter::MethodMinVersions::
              kGetIppClientInfoMinVersion}) {
    LOG(WARNING) << "Ash LocalPrinter version " << local_printer_version_
                 << " does not support GetIppClientInfo().";
    std::move(add_ipp_client_info_callback).Run({});
    return;
  }
#endif

  local_printer_->GetIppClientInfo(printer_id,
                                   std::move(add_ipp_client_info_callback));
}

void LocalPrinterHandlerChromeos::CallStartLocalPrint(
    scoped_refptr<base::RefCountedMemory> print_data,
    PrinterHandler::PrintCallback callback,
    base::Value::Dict settings) {
  StartLocalPrint(std::move(settings), std::move(print_data),
                  preview_web_contents_, std::move(callback));
}

void LocalPrinterHandlerChromeos::StartGetEulaUrl(
    const std::string& destination_id,
    GetEulaUrlCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!local_printer_) {
    PRINTER_LOG(ERROR) << "Local printer not available (StartGetEulaUrl)";
    std::move(callback).Run("");
    return;
  }
  local_printer_->GetEulaUrl(destination_id,
                             base::BindOnce([](const GURL& url) {
                               return url.spec();
                             }).Then(std::move(callback)));
}

void LocalPrinterHandlerChromeos::StartPrinterStatusRequest(
    const std::string& printer_id,
    PrinterStatusRequestCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!local_printer_) {
    PRINTER_LOG(ERROR)
        << "Local printer not available (StartPrinterStatusRequest)";
    std::move(callback).Run(std::nullopt);
    return;
  }
  local_printer_->GetStatus(
      printer_id, base::BindOnce([](crosapi::mojom::PrinterStatusPtr ptr) {
                    return StatusToValue(*ptr);
                  }).Then(std::move(callback)));
}

}  // namespace printing