| // 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 |