blob: c4442eabef9b963e5d50caf28c9c7780f344f02f [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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/pdf_printer_handler.h"
#include <utility>
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/i18n/file_util_icu.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/printing/print_preview_dialog_controller.h"
#include "chrome/browser/printing/print_preview_sticky_settings.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/chrome_select_file_policy.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/grit/generated_resources.h"
#include "components/account_id/account_id.h"
#include "components/cloud_devices/common/printer_description.h"
#include "components/url_formatter/url_formatter.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "net/base/filename_util.h"
#include "printing/backend/print_backend.h"
#include "printing/mojom/print.mojom.h"
#include "printing/print_job_constants.h"
#include "printing/printing_context.h"
#include "printing/units.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/size.h"
#include "url/gurl.h"
#if defined(OS_MAC)
#include "chrome/common/printing/printer_capabilities_mac.h"
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service.h"
#include "chrome/browser/ui/ash/holding_space/holding_space_keyed_service_factory.h"
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chromeos/crosapi/mojom/drive_integration_service.mojom.h"
#include "chromeos/crosapi/mojom/holding_space_service.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#endif
namespace printing {
namespace {
constexpr base::FilePath::CharType kPdfExtension[] = FILE_PATH_LITERAL("pdf");
class PrintingContextDelegate : public PrintingContext::Delegate {
public:
// PrintingContext::Delegate methods.
gfx::NativeView GetParentView() override { return NULL; }
std::string GetAppLocale() override {
return g_browser_process->GetApplicationLocale();
}
};
const AccountId& GetAccountId(Profile* profile) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
const auto* user = ash::ProfileHelper::Get()->GetUserByProfile(profile);
return user ? user->GetAccountId() : EmptyAccountId();
#else
return EmptyAccountId();
#endif
}
gfx::Size GetDefaultPdfMediaSizeMicrons() {
PrintingContextDelegate delegate;
// The `PrintingContext` for "Save as PDF" does need to make system printing
// calls.
auto printing_context(
PrintingContext::Create(&delegate, /*skip_system_calls=*/false));
if (mojom::ResultCode::kSuccess != printing_context->UsePdfSettings() ||
printing_context->settings().device_units_per_inch() <= 0) {
return gfx::Size();
}
gfx::Size pdf_media_size = printing_context->GetPdfPaperSizeDeviceUnits();
float device_microns_per_device_unit =
static_cast<float>(kMicronsPerInch) /
printing_context->settings().device_units_per_inch();
return gfx::Size(pdf_media_size.width() * device_microns_per_device_unit,
pdf_media_size.height() * device_microns_per_device_unit);
}
base::Value GetPdfCapabilities(
const std::string& locale,
PrinterSemanticCapsAndDefaults::Papers custom_papers) {
using cloud_devices::printer::MediaType;
cloud_devices::CloudDeviceDescription description;
cloud_devices::printer::OrientationCapability orientation;
orientation.AddOption(cloud_devices::printer::OrientationType::PORTRAIT);
orientation.AddOption(cloud_devices::printer::OrientationType::LANDSCAPE);
orientation.AddDefaultOption(
cloud_devices::printer::OrientationType::AUTO_ORIENTATION, true);
orientation.SaveTo(&description);
cloud_devices::printer::ColorCapability color;
{
cloud_devices::printer::Color standard_color(
cloud_devices::printer::ColorType::STANDARD_COLOR);
standard_color.vendor_id =
base::NumberToString(static_cast<int>(mojom::ColorModel::kColor));
color.AddDefaultOption(standard_color, true);
}
color.SaveTo(&description);
static const MediaType kPdfMedia[] = {
MediaType::ISO_A0, MediaType::ISO_A1, MediaType::ISO_A2,
MediaType::ISO_A3, MediaType::ISO_A4, MediaType::ISO_A5,
MediaType::NA_LEGAL, MediaType::NA_LETTER, MediaType::NA_LEDGER};
const gfx::Size default_media_size = GetDefaultPdfMediaSizeMicrons();
cloud_devices::printer::Media default_media(std::string(), std::string(),
default_media_size.width(),
default_media_size.height());
if (!default_media.MatchBySize() ||
!base::Contains(kPdfMedia, default_media.type)) {
default_media = cloud_devices::printer::Media(
locale == "en-US" ? MediaType::NA_LETTER : MediaType::ISO_A4);
}
cloud_devices::printer::MediaCapability media;
for (const auto& pdf_media : kPdfMedia) {
cloud_devices::printer::Media media_option(pdf_media);
media.AddDefaultOption(media_option,
default_media.type == media_option.type);
}
for (const PrinterSemanticCapsAndDefaults::Paper& paper : custom_papers) {
cloud_devices::printer::Media media_option(
paper.display_name, paper.vendor_id, paper.size_um.width(),
paper.size_um.height());
media.AddOption(media_option);
}
media.SaveTo(&description);
return std::move(description).ToValue();
}
// Callback that stores a PDF file on disk.
void PrintToPdfCallback(scoped_refptr<base::RefCountedMemory> data,
const base::FilePath& path) {
base::WriteFile(path, *data);
}
// Callback that runs after `PrintToPdfCallback()` returns.
void OnPdfPrintedCallback(const AccountId& account_id,
bool from_incognito_profile,
const base::FilePath& path,
base::OnceClosure pdf_file_saved_closure) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
Profile* profile =
ash::ProfileHelper::Get()->GetProfileByAccountId(account_id);
if (profile) {
ash::HoldingSpaceKeyedService* holding_space_keyed_service =
ash::HoldingSpaceKeyedServiceFactory::GetInstance()->GetService(
profile);
if (holding_space_keyed_service)
holding_space_keyed_service->AddPrintedPdf(path, from_incognito_profile);
}
#elif BUILDFLAG(IS_CHROMEOS_LACROS)
auto* service = chromeos::LacrosService::Get();
if (service && service->IsAvailable<crosapi::mojom::HoldingSpaceService>()) {
service->GetRemote<crosapi::mojom::HoldingSpaceService>()->AddPrintedPdf(
path, from_incognito_profile);
}
#endif
if (!pdf_file_saved_closure.is_null())
std::move(pdf_file_saved_closure).Run();
}
base::FilePath SelectSaveDirectory(const base::FilePath& path,
const base::FilePath& default_path) {
if (base::DirectoryExists(path))
return path;
if (!base::DirectoryExists(default_path))
base::CreateDirectory(default_path);
return default_path;
}
void ConstructCapabilitiesAndCompleteCallback(
const std::string& destination_id,
PdfPrinterHandler::GetCapabilityCallback callback,
PrinterSemanticCapsAndDefaults::Papers custom_papers) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
base::Value printer_info(base::Value::Type::DICTIONARY);
printer_info.SetStringKey(kSettingDeviceName, destination_id);
printer_info.SetKey(
kSettingCapabilities,
GetPdfCapabilities(g_browser_process->GetApplicationLocale(),
custom_papers));
std::move(callback).Run(std::move(printer_info));
}
} // namespace
PdfPrinterHandler::PdfPrinterHandler(
Profile* profile,
content::WebContents* preview_web_contents,
PrintPreviewStickySettings* sticky_settings)
: preview_web_contents_(preview_web_contents),
profile_(profile),
sticky_settings_(sticky_settings) {}
PdfPrinterHandler::~PdfPrinterHandler() {
if (select_file_dialog_.get())
select_file_dialog_->ListenerDestroyed();
}
void PdfPrinterHandler::Reset() {
weak_ptr_factory_.InvalidateWeakPtrs();
}
void PdfPrinterHandler::StartGetPrinters(
AddedPrintersCallback added_printers_callback,
GetPrintersDoneCallback done_callback) {
NOTREACHED();
}
void PdfPrinterHandler::StartGetCapability(const std::string& destination_id,
GetCapabilityCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
#if defined(OS_MAC)
// Read the Mac custom paper sizes on a separate thread.
// USER_VISIBLE because the result is displayed in the print preview dialog.
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(&GetMacCustomPaperSizes),
base::BindOnce(&ConstructCapabilitiesAndCompleteCallback, destination_id,
std::move(callback)));
#else
ConstructCapabilitiesAndCompleteCallback(
destination_id, std::move(callback),
PrinterSemanticCapsAndDefaults::Papers());
#endif
}
void PdfPrinterHandler::StartPrint(
const std::u16string& job_title,
base::Value settings,
scoped_refptr<base::RefCountedMemory> print_data,
PrintCallback callback) {
print_data_ = print_data;
if (!print_to_pdf_path_.empty()) {
// User has already selected a path, no need to show the dialog again.
PostPrintToPdfTask();
return;
}
if (select_file_dialog_ &&
select_file_dialog_->IsRunning(
platform_util::GetTopLevel(preview_web_contents_->GetNativeView()))) {
// Dialog is already showing.
return;
}
DCHECK(!print_callback_);
print_callback_ = std::move(callback);
PrintPreviewDialogController* dialog_controller =
PrintPreviewDialogController::GetInstance();
content::WebContents* initiator =
dialog_controller ? dialog_controller->GetInitiator(preview_web_contents_)
: nullptr;
GURL initiator_url;
bool is_savable = false;
if (initiator) {
initiator_url = initiator->GetLastCommittedURL();
is_savable = initiator->IsSavable();
}
base::FilePath path = GetFileName(initiator_url, job_title, is_savable);
base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess();
bool prompt_user = !cmdline->HasSwitch(switches::kKioskModePrinting);
#if BUILDFLAG(IS_CHROMEOS_ASH)
use_drive_mount_ =
settings.FindBoolKey(kSettingPrintToGoogleDrive).value_or(false);
#endif
SelectFile(path, initiator, prompt_user);
}
void PdfPrinterHandler::FileSelected(const base::FilePath& path,
int /* index */,
void* /* params */) {
// Update downloads location and save sticky settings.
DownloadPrefs* download_prefs = DownloadPrefs::FromBrowserContext(profile_);
download_prefs->SetSaveFilePath(path.DirName());
sticky_settings_->SaveInPrefs(profile_->GetPrefs());
print_to_pdf_path_ = path;
PostPrintToPdfTask();
}
void PdfPrinterHandler::FileSelectionCanceled(void* params) {
std::move(print_callback_).Run(base::Value("PDFPrintCanceled"));
}
void PdfPrinterHandler::SetPdfSavedClosureForTesting(
base::OnceClosure closure) {
pdf_file_saved_closure_ = std::move(closure);
}
void PdfPrinterHandler::SetPrintToPdfPathForTesting(
const base::FilePath& path) {
print_to_pdf_path_ = path;
}
// static
base::FilePath PdfPrinterHandler::GetFileNameForPrintJobTitle(
const std::u16string& job_title) {
DCHECK(!job_title.empty());
#if defined(OS_WIN)
base::FilePath::StringType print_job_title(base::AsWString(job_title));
#elif defined(OS_POSIX)
base::FilePath::StringType print_job_title = base::UTF16ToUTF8(job_title);
#endif
base::i18n::ReplaceIllegalCharactersInPath(&print_job_title, '_');
base::FilePath default_filename(print_job_title);
base::FilePath::StringType ext = default_filename.Extension();
if (!ext.empty()) {
ext = ext.substr(1);
if (ext == kPdfExtension)
return default_filename;
}
return default_filename.AddExtension(kPdfExtension);
}
// static
base::FilePath PdfPrinterHandler::GetFileNameForURL(const GURL& url) {
DCHECK(url.is_valid());
// TODO(thestig): This code is based on similar code in SavePackage in
// content/ that is not exposed via the public content API. Consider looking
// for a sane way to share the code.
if (url.SchemeIs(url::kDataScheme)) {
return base::FilePath::FromUTF8Unsafe("dataurl").ReplaceExtension(
kPdfExtension);
}
base::FilePath name =
net::GenerateFileName(url, std::string(), std::string(), std::string(),
std::string(), std::string());
// If host is used as file name, try to decode punycode.
if (name.AsUTF8Unsafe() == url.host()) {
name = base::FilePath::FromUTF16Unsafe(
url_formatter::IDNToUnicode(url.host()));
}
if (name.AsUTF8Unsafe() == url.host())
return name.AddExtension(kPdfExtension);
return name.ReplaceExtension(kPdfExtension);
}
// static
base::FilePath PdfPrinterHandler::GetFileName(const GURL& url,
const std::u16string& job_title,
bool is_savable) {
if (is_savable) {
bool title_is_url =
job_title.empty() || url_formatter::FormatUrl(url) == job_title;
return title_is_url ? GetFileNameForURL(url)
: GetFileNameForPrintJobTitle(job_title);
}
base::FilePath name = net::GenerateFileName(
url, std::string(), std::string(), std::string(), std::string(),
l10n_util::GetStringUTF8(IDS_DEFAULT_DOWNLOAD_FILENAME));
return name.ReplaceExtension(kPdfExtension);
}
void PdfPrinterHandler::SelectFile(const base::FilePath& default_filename,
content::WebContents* initiator,
bool prompt_user) {
// Handle case where user expects to be prompted but policy disallows file
// selection. Call CanOpenSelectFileDialog() to notify user and early return.
if (prompt_user) {
ChromeSelectFilePolicy policy(initiator);
if (!policy.CanOpenSelectFileDialog()) {
policy.SelectFileDenied();
std::move(print_callback_).Run(base::Value("PDFPrintCannotSelect"));
return;
}
}
sticky_settings_->SaveInPrefs(profile_->GetPrefs());
#if BUILDFLAG(IS_CHROMEOS_LACROS)
auto* service = chromeos::LacrosService::Get();
if (service &&
service->IsAvailable<crosapi::mojom::DriveIntegrationService>()) {
service->GetRemote<crosapi::mojom::DriveIntegrationService>()
->GetMountPointPath(
base::BindOnce(&PdfPrinterHandler::OnSaveLocationReady,
weak_ptr_factory_.GetWeakPtr(),
std::move(default_filename), prompt_user));
return;
}
#endif
OnSaveLocationReady(default_filename, prompt_user, GetSaveLocation());
}
void PdfPrinterHandler::OnSaveLocationReady(
const base::FilePath& default_filename,
bool prompt_user,
const base::FilePath& path) {
// Handle the no prompting case. Like the dialog prompt, this function
// returns and eventually FileSelected() gets called.
if (!prompt_user) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&base::GetUniquePath, path.Append(default_filename)),
base::BindOnce(&PdfPrinterHandler::OnGotUniqueFileName,
weak_ptr_factory_.GetWeakPtr()));
return;
}
// If the directory is empty there is no reason to create it or use the
// default location.
if (path.empty()) {
OnDirectorySelected(default_filename, path);
return;
}
// Get default download directory. This will be used as a fallback if the
// save directory does not exist.
DownloadPrefs* download_prefs = DownloadPrefs::FromBrowserContext(profile_);
base::FilePath default_path = download_prefs->DownloadPath();
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&SelectSaveDirectory, path, default_path),
base::BindOnce(&PdfPrinterHandler::OnDirectorySelected,
weak_ptr_factory_.GetWeakPtr(), default_filename));
}
void PdfPrinterHandler::PostPrintToPdfTask() {
base::ThreadPool::PostTaskAndReply(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&PrintToPdfCallback, print_data_, print_to_pdf_path_),
base::BindOnce(&OnPdfPrintedCallback, GetAccountId(profile_),
profile_->IsIncognitoProfile(), print_to_pdf_path_,
std::move(pdf_file_saved_closure_)));
print_to_pdf_path_.clear();
if (print_callback_)
std::move(print_callback_).Run(base::Value());
}
void PdfPrinterHandler::OnGotUniqueFileName(const base::FilePath& path) {
FileSelected(path, 0, nullptr);
}
void PdfPrinterHandler::OnDirectorySelected(const base::FilePath& filename,
const base::FilePath& directory) {
base::FilePath path = directory.Append(filename);
// Prompts the user to select the file.
ui::SelectFileDialog::FileTypeInfo file_type_info;
file_type_info.extensions.resize(1);
file_type_info.extensions[0].push_back(FILE_PATH_LITERAL("pdf"));
file_type_info.include_all_files = true;
// Print Preview requires native paths to write PDF files.
// Note that Chrome OS save-as dialog has Google Drive as a saving location
// even when a client requires native paths. In this case, Chrome OS save-as
// dialog returns native paths to write files and uploads the saved files to
// Google Drive later.
file_type_info.allowed_paths =
ui::SelectFileDialog::FileTypeInfo::NATIVE_PATH;
select_file_dialog_ =
ui::SelectFileDialog::Create(this, nullptr /*policy already checked*/);
select_file_dialog_->SelectFile(
ui::SelectFileDialog::SELECT_SAVEAS_FILE, std::u16string(), path,
&file_type_info, 0, base::FilePath::StringType(),
platform_util::GetTopLevel(preview_web_contents_->GetNativeView()), NULL);
}
base::FilePath PdfPrinterHandler::GetSaveLocation() const {
#if BUILDFLAG(IS_CHROMEOS_ASH)
drive::DriveIntegrationService* drive_service =
drive::DriveIntegrationServiceFactory::GetForProfile(profile_);
if (use_drive_mount_ && drive_service && drive_service->IsMounted()) {
return drive_service->GetMountPointPath().Append(
drive::util::kDriveMyDriveRootDirName);
}
#endif
DownloadPrefs* download_prefs = DownloadPrefs::FromBrowserContext(profile_);
return download_prefs->SaveFilePath();
}
} // namespace printing