// Copyright 2019 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/printing_api_handler.h"
#include <utility>
#include "base/check.h"
#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/location.h"
#include "base/no_destructor.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_runner.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/chromeos/printing/cups_wrapper.h"
#include "chrome/browser/chromeos/printing/printer_error_codes.h"
#include "chrome/browser/extensions/api/printing/print_job_submitter.h"
#include "chrome/browser/extensions/api/printing/printing_api_utils.h"
#include "chrome/browser/printing/local_printer_utils_chromeos.h"
#include "chrome/browser/printing/pdf_blob_data_flattener.h"
#include "chrome/browser/printing/print_job.h"
#include "chrome/browser/printing/print_job_controller.h"
#include "chrome/browser/printing/print_preview_sticky_settings.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "chromeos/crosapi/mojom/local_printer.mojom.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/printing/common/cloud_print_cdd_conversion.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_registry.h"
#include "printing/print_settings.h"
#include "printing/printed_document.h"
namespace extensions {
namespace {
constexpr char kInvalidPrinterIdError[] = "Invalid printer ID";
constexpr char kNoActivePrintJobWithIdError[] =
"No active print job with given ID";
} // namespace
// static
std::unique_ptr<PrintingAPIHandler> PrintingAPIHandler::CreateForTesting(
content::BrowserContext* browser_context,
EventRouter* event_router,
ExtensionRegistry* extension_registry,
std::unique_ptr<printing::PrintJobController> print_job_controller,
std::unique_ptr<chromeos::CupsWrapper> cups_wrapper,
crosapi::mojom::LocalPrinter* local_printer) {
return std::make_unique<PrintingAPIHandler>(
browser_context, event_router, extension_registry,
std::move(print_job_controller), std::move(cups_wrapper), local_printer);
}
PrintingAPIHandler::PrintingAPIHandler(content::BrowserContext* browser_context)
: PrintingAPIHandler(browser_context,
EventRouter::Get(browser_context),
ExtensionRegistry::Get(browser_context),
std::make_unique<printing::PrintJobController>(),
chromeos::CupsWrapper::Create(),
printing::GetLocalPrinterInterface()) {
CHECK(local_printer_);
local_printer_->AddPrintJobObserver(
receiver_.BindNewPipeAndPassRemoteWithVersion(),
crosapi::mojom::PrintJobSource::kExtension, base::DoNothing());
}
PrintingAPIHandler::PrintingAPIHandler(
content::BrowserContext* browser_context,
EventRouter* event_router,
ExtensionRegistry* extension_registry,
std::unique_ptr<printing::PrintJobController> print_job_controller,
std::unique_ptr<chromeos::CupsWrapper> cups_wrapper,
crosapi::mojom::LocalPrinter* local_printer)
: browser_context_(browser_context),
event_router_(event_router),
extension_registry_(extension_registry),
print_job_controller_(std::move(print_job_controller)),
cups_wrapper_(std::move(cups_wrapper)),
pdf_blob_data_flattener_(std::make_unique<printing::PdfBlobDataFlattener>(
Profile::FromBrowserContext(browser_context))),
local_printer_(local_printer) {
CHECK(local_printer_);
}
PrintingAPIHandler::~PrintingAPIHandler() = default;
// static
std::string PrintingAPIHandler::CreateUniqueId(const std::string& printer_id,
int job_id) {
return base::StringPrintf("%s%d", printer_id.c_str(), job_id);
}
// static
BrowserContextKeyedAPIFactory<PrintingAPIHandler>*
PrintingAPIHandler::GetFactoryInstance() {
static base::NoDestructor<BrowserContextKeyedAPIFactory<PrintingAPIHandler>>
instance;
return instance.get();
}
// static
PrintingAPIHandler* PrintingAPIHandler::Get(
content::BrowserContext* browser_context) {
return BrowserContextKeyedAPIFactory<PrintingAPIHandler>::Get(
browser_context);
}
// static
void PrintingAPIHandler::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kPrintingAPIExtensionsAllowlist);
}
void PrintingAPIHandler::SubmitJob(
gfx::NativeWindow native_window,
scoped_refptr<const extensions::Extension> extension,
std::optional<api::printing::SubmitJob::Params> params,
SubmitJobCallback callback) {
DCHECK(params);
// PrintingAPIHandler must outlive PrintJobSubmitter. Even if the WeakPtr
// expires, PrintJobSubmitter will continue to access PrintingAPIHandler
// member variables.
std::string extension_id = extension->id();
PrintJobSubmitter::Run(std::make_unique<PrintJobSubmitter>(
native_window, browser_context_, print_job_controller_.get(),
pdf_blob_data_flattener_.get(), std::move(extension),
std::move(params->request), local_printer_,
base::BindOnce(&PrintingAPIHandler::OnPrintJobSubmitted,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
std::move(extension_id))));
}
void PrintingAPIHandler::OnPrintJobSubmitted(
SubmitJobCallback callback,
const std::string& extension_id,
PrintJobSubmitter::PrintJobCreationResult result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!result.has_value()) {
std::optional<std::string> error = std::move(result).error();
std::optional<api::printing::SubmitJobStatus> status;
if (!error)
status = api::printing::SubmitJobStatus::kUserRejected;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), status, std::nullopt,
std::move(error)));
return;
}
printing::PrintJobCreatedInfo info = std::move(result).value();
std::string printer_id =
base::UTF16ToUTF8(info.document->settings().device_name());
DCHECK(!printer_id.empty());
std::string cups_id = CreateUniqueId(printer_id, info.job_id);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), api::printing::SubmitJobStatus::kOk,
cups_id, std::nullopt));
DCHECK(!base::Contains(print_jobs_, cups_id));
print_jobs_[cups_id] = PrintJobInfo{printer_id, info.job_id, extension_id};
#if BUILDFLAG(IS_CHROMEOS_LACROS)
NotifyAshJobCreated(info.job_id, *info.document,
crosapi::mojom::PrintJob::Source::kExtension,
extension_id, local_printer_);
#endif
if (!extension_registry_->enabled_extensions().Contains(extension_id)) {
return;
}
auto event =
std::make_unique<Event>(events::PRINTING_ON_JOB_STATUS_CHANGED,
api::printing::OnJobStatusChanged::kEventName,
api::printing::OnJobStatusChanged::Create(
cups_id, api::printing::JobStatus::kPending));
event_router_->DispatchEventToExtension(extension_id, std::move(event));
}
std::optional<std::string> PrintingAPIHandler::CancelJob(
const std::string& extension_id,
const std::string& job_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto it = print_jobs_.find(job_id);
// If there was no print job with specified id sent by the extension return
// an error.
if (it == print_jobs_.end() || it->second.extension_id != extension_id) {
return kNoActivePrintJobWithIdError;
}
local_printer_->CancelPrintJob(it->second.printer_id, it->second.job_id,
base::DoNothing());
return std::nullopt;
}
void PrintingAPIHandler::GetPrinters(GetPrintersCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
local_printer_->GetPrinters(
base::BindOnce(&PrintingAPIHandler::OnPrintersRetrieved,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void PrintingAPIHandler::OnPrintersRetrieved(
GetPrintersCallback callback,
std::vector<crosapi::mojom::LocalDestinationInfoPtr> data) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
PrefService* prefs =
Profile::FromBrowserContext(browser_context_)->GetPrefs();
std::optional<DefaultPrinterRules> default_printer_rules =
GetDefaultPrinterRules(prefs->GetString(
prefs::kPrintPreviewDefaultDestinationSelectionRules));
auto* sticky_settings = printing::PrintPreviewStickySettings::GetInstance();
sticky_settings->RestoreFromPrefs(prefs);
base::flat_map<std::string, int> recently_used_ranks =
sticky_settings->GetPrinterRecentlyUsedRanks();
std::vector<api::printing::Printer> printers;
printers.reserve(data.size());
for (const crosapi::mojom::LocalDestinationInfoPtr& ptr : data) {
printers.push_back(
PrinterToIdl(*ptr, default_printer_rules, recently_used_ranks));
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::move(printers)));
}
void PrintingAPIHandler::GetPrinterInfo(const std::string& printer_id,
GetPrinterInfoCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
local_printer_->GetCapability(
printer_id,
base::BindOnce(&PrintingAPIHandler::OnPrinterCapabilitiesRetrieved,
weak_ptr_factory_.GetWeakPtr(), printer_id,
std::move(callback)));
}
void PrintingAPIHandler::OnPrinterCapabilitiesRetrieved(
const std::string& printer_id,
GetPrinterInfoCallback callback,
crosapi::mojom::CapabilitiesResponsePtr caps) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!caps) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), /*capabilities=*/std::nullopt,
/*status=*/std::nullopt, kInvalidPrinterIdError));
return;
}
if (!caps->capabilities) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback), /*capabilities=*/std::nullopt,
/*status=*/api::printing::PrinterStatus::kUnreachable,
/*error=*/std::nullopt));
return;
}
cups_wrapper_->QueryCupsPrinterStatus(
printer_id,
base::BindOnce(&PrintingAPIHandler::OnPrinterStatusRetrieved,
weak_ptr_factory_.GetWeakPtr(), std::move(callback),
cloud_print::PrinterSemanticCapsAndDefaultsToCdd(
*caps->capabilities)));
}
void PrintingAPIHandler::OnPrinterStatusRetrieved(
GetPrinterInfoCallback callback,
base::Value capabilities,
std::unique_ptr<::printing::PrinterStatus> printer_status) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!printer_status) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::move(capabilities),
api::printing::PrinterStatus::kUnreachable,
/*error=*/std::nullopt));
return;
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
std::move(callback), std::move(capabilities),
PrinterStatusToIdl(chromeos::PrinterErrorCodeFromPrinterStatusReasons(
*printer_status)),
/*error=*/std::nullopt));
}
void PrintingAPIHandler::OnPrintJobUpdateDeprecated(
const std::string& printer_id,
unsigned int job_id,
crosapi::mojom::PrintJobStatus status) {
NOTREACHED_IN_MIGRATION();
}
void PrintingAPIHandler::OnPrintJobUpdate(
const std::string& printer_id,
unsigned int job_id,
crosapi::mojom::PrintJobUpdatePtr update) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
bool done = true;
api::printing::JobStatus job_status;
switch (update->status) {
case crosapi::mojom::PrintJobStatus::kStarted:
job_status = api::printing::JobStatus::kInProgress;
done = false;
break;
case crosapi::mojom::PrintJobStatus::kDone:
job_status = api::printing::JobStatus::kPrinted;
break;
case crosapi::mojom::PrintJobStatus::kError:
job_status = api::printing::JobStatus::kFailed;
break;
case crosapi::mojom::PrintJobStatus::kCancelled:
job_status = api::printing::JobStatus::kCanceled;
break;
default: // crosapi::mojom::PrintJobStatus::kCreated
return;
}
std::string cups_id = CreateUniqueId(printer_id, job_id);
auto it = print_jobs_.find(cups_id);
if (it == print_jobs_.end())
return;
const std::string& extension_id = it->second.extension_id;
if (extension_registry_->enabled_extensions().Contains(extension_id)) {
auto event = std::make_unique<Event>(
events::PRINTING_ON_JOB_STATUS_CHANGED,
api::printing::OnJobStatusChanged::kEventName,
api::printing::OnJobStatusChanged::Create(cups_id, job_status));
event_router_->DispatchEventToExtension(extension_id, std::move(event));
}
if (done)
print_jobs_.erase(it);
}
template <>
KeyedService*
BrowserContextKeyedAPIFactory<PrintingAPIHandler>::BuildServiceInstanceFor(
content::BrowserContext* context) const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
Profile* profile = Profile::FromBrowserContext(context);
// We do not want an instance of PrintingAPIHandler on the lock screen.
// This will lead to multiple printing notifications.
if (!profile->IsRegularProfile()) {
return nullptr;
}
return new PrintingAPIHandler(context);
}
} // namespace extensions