blob: 2e071b28d4105f81212d6ff36ed4d0772eb7c185 [file] [log] [blame]
// Copyright (c) 2012 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/service/cloud_print/cloud_print_connector.h"
#include <stddef.h>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/location.h"
#include "base/md5.h"
#include "base/rand_util.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "chrome/common/cloud_print/cloud_print_constants.h"
#include "chrome/common/cloud_print/cloud_print_helpers.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/service/cloud_print/cloud_print_service_helpers.h"
#include "net/base/mime_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "ui/base/l10n/l10n_util.h"
namespace cloud_print {
CloudPrintConnector::CloudPrintConnector(
Client* client,
const ConnectorSettings& settings,
const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation)
: client_(client),
next_response_handler_(NULL),
partial_traffic_annotation_(partial_traffic_annotation),
stats_ptr_factory_(this) {
settings_.CopyFrom(settings);
}
bool CloudPrintConnector::InitPrintSystem() {
if (print_system_.get())
return true;
print_system_ = PrintSystem::CreateInstance(
settings_.print_system_settings());
if (!print_system_.get()) {
NOTREACHED();
return false; // No memory.
}
PrintSystem::PrintSystemResult result = print_system_->Init();
if (!result.succeeded()) {
print_system_ = NULL;
// We could not initialize the print system. We need to notify the server.
ReportUserMessage(kPrintSystemFailedMessageId, result.message());
return false;
}
return true;
}
void CloudPrintConnector::ScheduleStatsReport() {
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CloudPrintConnector::ReportStats,
stats_ptr_factory_.GetWeakPtr()),
base::TimeDelta::FromHours(1));
}
void CloudPrintConnector::ReportStats() {
PrinterJobHandler::ReportsStats();
ScheduleStatsReport();
}
bool CloudPrintConnector::Start() {
VLOG(1) << "CP_CONNECTOR: Starting connector"
<< ", proxy id: " << settings_.proxy_id();
pending_tasks_.clear();
if (!InitPrintSystem())
return false;
ScheduleStatsReport();
// Start watching for updates from the print system.
print_server_watcher_ = print_system_->CreatePrintServerWatcher();
print_server_watcher_->StartWatching(this);
// Get list of registered printers.
AddPendingAvailableTask();
return true;
}
void CloudPrintConnector::Stop() {
VLOG(1) << "CP_CONNECTOR: Stopping connector"
<< ", proxy id: " << settings_.proxy_id();
DCHECK(IsRunning());
// Do uninitialization here.
stats_ptr_factory_.InvalidateWeakPtrs();
pending_tasks_.clear();
print_server_watcher_ = NULL;
request_ = NULL;
}
bool CloudPrintConnector::IsRunning() {
return print_server_watcher_.get() != NULL;
}
std::list<std::string> CloudPrintConnector::GetPrinterIds() const {
std::list<std::string> printer_ids;
for (const auto& it : job_handler_map_)
printer_ids.push_back(it.first);
return printer_ids;
}
void CloudPrintConnector::RegisterPrinters(
const printing::PrinterList& printers) {
if (!IsRunning())
return;
for (const auto& it : printers) {
if (settings_.ShouldConnect(it.printer_name))
AddPendingRegisterTask(it);
}
}
// Check for jobs for specific printer
void CloudPrintConnector::CheckForJobs(const std::string& reason,
const std::string& printer_id) {
if (!IsRunning())
return;
if (printer_id.empty()) {
for (const auto& it : job_handler_map_)
it.second->CheckForJobs(reason);
return;
}
auto printer_it = job_handler_map_.find(printer_id);
if (printer_it == job_handler_map_.end()) {
std::string status_message =
l10n_util::GetStringUTF8(IDS_CLOUD_PRINT_ZOMBIE_PRINTER);
LOG(ERROR) << "CP_CONNECTOR: " << status_message
<< " Printer_id: " << printer_id;
ReportUserMessage(kZombiePrinterMessageId, status_message);
return;
}
printer_it->second->CheckForJobs(reason);
}
void CloudPrintConnector::UpdatePrinterSettings(const std::string& printer_id) {
// Since connector is managing many printers we need to go through all of them
// to select the correct settings.
GURL printer_list_url = GetUrlForPrinterList(
settings_.server_url(), settings_.proxy_id());
StartGetRequest(
printer_list_url,
kCloudPrintRegisterMaxRetryCount,
&CloudPrintConnector::HandlePrinterListResponseSettingsUpdate);
}
void CloudPrintConnector::OnPrinterAdded() {
AddPendingAvailableTask();
}
void CloudPrintConnector::OnPrinterDeleted(const std::string& printer_id) {
AddPendingDeleteTask(printer_id);
}
void CloudPrintConnector::OnAuthError() {
client_->OnAuthFailed();
}
// CloudPrintURLFetcher::Delegate implementation.
CloudPrintURLFetcher::ResponseAction CloudPrintConnector::HandleRawData(
const net::URLFetcher* source,
const GURL& url,
const std::string& data) {
// If this notification came as a result of user message call, stop it.
// Otherwise proceed continue processing.
if (user_message_request_ && user_message_request_->IsSameRequest(source))
return CloudPrintURLFetcher::STOP_PROCESSING;
return CloudPrintURLFetcher::CONTINUE_PROCESSING;
}
CloudPrintURLFetcher::ResponseAction CloudPrintConnector::HandleJSONData(
const net::URLFetcher* source,
const GURL& url,
const base::DictionaryValue* json_data,
bool succeeded) {
if (!IsRunning()) // Orphan response. Connector has been stopped already.
return CloudPrintURLFetcher::STOP_PROCESSING;
DCHECK(next_response_handler_);
return (this->*next_response_handler_)(source, url, json_data, succeeded);
}
CloudPrintURLFetcher::ResponseAction CloudPrintConnector::OnRequestAuthError() {
OnAuthError();
return CloudPrintURLFetcher::STOP_PROCESSING;
}
std::string CloudPrintConnector::GetAuthHeader() {
return GetCloudPrintAuthHeaderFromStore();
}
CloudPrintConnector::~CloudPrintConnector() {}
CloudPrintURLFetcher::ResponseAction
CloudPrintConnector::HandlePrinterListResponse(
const net::URLFetcher* source,
const GURL& url,
const base::DictionaryValue* json_data,
bool succeeded) {
DCHECK(succeeded);
if (!succeeded)
return CloudPrintURLFetcher::RETRY_REQUEST;
UpdateSettingsFromPrintersList(json_data);
// Now we need to get the list of printers from the print system
// and split printers into 3 categories:
// - existing and registered printers
// - new printers
// - deleted printers
// Get list of the printers from the print system.
printing::PrinterList local_printers;
PrintSystem::PrintSystemResult result =
print_system_->EnumeratePrinters(&local_printers);
bool full_list = result.succeeded();
if (!full_list) {
std::string message = result.message();
if (message.empty())
message = l10n_util::GetStringFUTF8(IDS_CLOUD_PRINT_ENUM_FAILED,
l10n_util::GetStringUTF16(IDS_GOOGLE_CLOUD_PRINT));
// There was a failure enumerating printers. Send a message to the server.
ReportUserMessage(kEnumPrintersFailedMessageId, message);
}
// Go through the list of the cloud printers and init print job handlers.
const base::ListValue* printer_list = nullptr;
// There may be no "printers" value in the JSON
if (json_data->GetList(kPrinterListValue, &printer_list) && printer_list) {
for (size_t i = 0; i < printer_list->GetSize(); ++i) {
const base::DictionaryValue* printer_data = nullptr;
if (!printer_list->GetDictionary(i, &printer_data))
continue;
std::string printer_name;
printer_data->GetString(kNameValue, &printer_name);
std::string printer_id;
printer_data->GetString(kIdValue, &printer_id);
if (!settings_.ShouldConnect(printer_name)) {
VLOG(1) << "CP_CONNECTOR: Deleting " << printer_name
<< " id: " << printer_id << " as blacklisted";
AddPendingDeleteTask(printer_id);
} else if (RemovePrinterFromList(printer_name, &local_printers)) {
InitJobHandlerForPrinter(printer_data);
} else {
// Cloud printer is not found on the local system.
if (full_list || settings_.delete_on_enum_fail()) {
// Delete if we get the full list of printers or
// |delete_on_enum_fail_| is set.
VLOG(1) << "CP_CONNECTOR: Deleting " << printer_name
<< " id: " << printer_id << " full_list: " << full_list
<< " delete_on_enum_fail: "
<< settings_.delete_on_enum_fail();
AddPendingDeleteTask(printer_id);
} else {
LOG(ERROR) << "CP_CONNECTOR: Printer: " << printer_name
<< " id: " << printer_id
<< " not found in print system and full printer list was"
<< " not received. Printer will not be able to process"
<< " jobs.";
}
}
}
}
request_ = nullptr;
RegisterPrinters(local_printers);
ContinuePendingTaskProcessing(); // Continue processing background tasks.
return CloudPrintURLFetcher::STOP_PROCESSING;
}
CloudPrintURLFetcher::ResponseAction
CloudPrintConnector::HandlePrinterListResponseSettingsUpdate(
const net::URLFetcher* source,
const GURL& url,
const base::DictionaryValue* json_data,
bool succeeded) {
DCHECK(succeeded);
if (!succeeded)
return CloudPrintURLFetcher::RETRY_REQUEST;
UpdateSettingsFromPrintersList(json_data);
return CloudPrintURLFetcher::STOP_PROCESSING;
}
CloudPrintURLFetcher::ResponseAction
CloudPrintConnector::HandlePrinterDeleteResponse(
const net::URLFetcher* source,
const GURL& url,
const base::DictionaryValue* json_data,
bool succeeded) {
VLOG(1) << "CP_CONNECTOR: Handler printer delete response"
<< ", succeeded: " << succeeded
<< ", url: " << url;
ContinuePendingTaskProcessing(); // Continue processing background tasks.
return CloudPrintURLFetcher::STOP_PROCESSING;
}
CloudPrintURLFetcher::ResponseAction
CloudPrintConnector::HandleRegisterPrinterResponse(
const net::URLFetcher* source,
const GURL& url,
const base::DictionaryValue* json_data,
bool succeeded) {
VLOG(1) << "CP_CONNECTOR: Handler printer register response"
<< ", succeeded: " << succeeded
<< ", url: " << url;
if (succeeded) {
const base::ListValue* printer_list = nullptr;
// There should be a "printers" value in the JSON
if (json_data->GetList(kPrinterListValue, &printer_list)) {
const base::DictionaryValue* printer_data = nullptr;
if (printer_list->GetDictionary(0, &printer_data))
InitJobHandlerForPrinter(printer_data);
}
}
ContinuePendingTaskProcessing(); // Continue processing background tasks.
return CloudPrintURLFetcher::STOP_PROCESSING;
}
void CloudPrintConnector::StartGetRequest(const GURL& url,
int max_retries,
ResponseHandler handler) {
next_response_handler_ = handler;
request_ = CloudPrintURLFetcher::Create(partial_traffic_annotation_);
request_->StartGetRequest(CloudPrintURLFetcher::REQUEST_UPDATE_JOB,
url, this, max_retries, std::string());
}
void CloudPrintConnector::StartPostRequest(
CloudPrintURLFetcher::RequestType type,
const GURL& url,
int max_retries,
const std::string& mime_type,
const std::string& post_data,
ResponseHandler handler) {
next_response_handler_ = handler;
request_ = CloudPrintURLFetcher::Create(partial_traffic_annotation_);
request_->StartPostRequest(
type, url, this, max_retries, mime_type, post_data, std::string());
}
void CloudPrintConnector::ReportUserMessage(const std::string& message_id,
const std::string& failure_msg) {
// This is a fire and forget type of function.
// Result of this request will be ignored.
std::string mime_boundary = net::GenerateMimeMultipartBoundary();
GURL url = GetUrlForUserMessage(settings_.server_url(), message_id);
std::string post_data;
net::AddMultipartValueForUpload(kMessageTextValue, failure_msg, mime_boundary,
std::string(), &post_data);
net::AddMultipartFinalDelimiterForUpload(mime_boundary, &post_data);
std::string mime_type("multipart/form-data; boundary=");
mime_type += mime_boundary;
user_message_request_ =
CloudPrintURLFetcher::Create(partial_traffic_annotation_);
user_message_request_->StartPostRequest(
CloudPrintURLFetcher::REQUEST_USER_MESSAGE, url, this, 1, mime_type,
post_data, std::string());
}
bool CloudPrintConnector::RemovePrinterFromList(
const std::string& printer_name,
printing::PrinterList* printer_list) {
for (auto it = printer_list->begin(); it != printer_list->end(); ++it) {
if (IsSamePrinter(it->printer_name, printer_name)) {
printer_list->erase(it);
return true;
}
}
return false;
}
void CloudPrintConnector::InitJobHandlerForPrinter(
const base::DictionaryValue* printer_data) {
DCHECK(printer_data);
PrinterJobHandler::PrinterInfoFromCloud printer_info_cloud;
printer_data->GetString(kIdValue, &printer_info_cloud.printer_id);
DCHECK(!printer_info_cloud.printer_id.empty());
VLOG(1) << "CP_CONNECTOR: Init job handler"
<< ", printer id: " << printer_info_cloud.printer_id;
if (ContainsKey(job_handler_map_, printer_info_cloud.printer_id))
return; // Nothing to do if we already have a job handler for this printer.
printing::PrinterBasicInfo printer_info;
printer_data->GetString(kNameValue, &printer_info.printer_name);
DCHECK(!printer_info.printer_name.empty());
printer_data->GetString(kPrinterDescValue,
&printer_info.printer_description);
// Printer status is a string value which actually contains an integer.
std::string printer_status;
if (printer_data->GetString(kPrinterStatusValue, &printer_status)) {
base::StringToInt(printer_status, &printer_info.printer_status);
}
printer_data->GetString(kPrinterCapsHashValue,
&printer_info_cloud.caps_hash);
const base::ListValue* tags_list = nullptr;
if (printer_data->GetList(kTagsValue, &tags_list) && tags_list) {
for (size_t i = 0; i < tags_list->GetSize(); ++i) {
std::string tag;
if (tags_list->GetString(i, &tag) &&
base::StartsWith(tag, kCloudPrintServiceTagsHashTagName,
base::CompareCase::INSENSITIVE_ASCII)) {
std::vector<std::string> tag_parts = base::SplitString(
tag, "=", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (tag_parts.size() == 2)
printer_info_cloud.tags_hash = tag_parts[1];
}
}
}
int xmpp_timeout = 0;
printer_data->GetInteger(kLocalSettingsPendingXmppValue, &xmpp_timeout);
printer_info_cloud.current_xmpp_timeout = settings_.xmpp_ping_timeout_sec();
printer_info_cloud.pending_xmpp_timeout = xmpp_timeout;
scoped_refptr<PrinterJobHandler> job_handler;
job_handler = new PrinterJobHandler(printer_info,
printer_info_cloud,
settings_.server_url(),
print_system_.get(),
this);
job_handler_map_[printer_info_cloud.printer_id] = job_handler;
job_handler->Initialize();
}
void CloudPrintConnector::UpdateSettingsFromPrintersList(
const base::DictionaryValue* json_data) {
const base::ListValue* printer_list = nullptr;
int min_xmpp_timeout = std::numeric_limits<int>::max();
// There may be no "printers" value in the JSON
if (json_data->GetList(kPrinterListValue, &printer_list) && printer_list) {
for (size_t i = 0; i < printer_list->GetSize(); ++i) {
const base::DictionaryValue* printer_data = nullptr;
if (printer_list->GetDictionary(i, &printer_data)) {
int xmpp_timeout = 0;
if (printer_data->GetInteger(kLocalSettingsPendingXmppValue,
&xmpp_timeout)) {
min_xmpp_timeout = std::min(xmpp_timeout, min_xmpp_timeout);
}
}
}
}
if (min_xmpp_timeout != std::numeric_limits<int>::max()) {
DCHECK(min_xmpp_timeout >= kMinXmppPingTimeoutSecs);
settings_.SetXmppPingTimeoutSec(min_xmpp_timeout);
client_->OnXmppPingUpdated(min_xmpp_timeout);
}
}
void CloudPrintConnector::AddPendingAvailableTask() {
PendingTask task;
task.type = PENDING_PRINTERS_AVAILABLE;
AddPendingTask(task);
}
void CloudPrintConnector::AddPendingDeleteTask(const std::string& id) {
PendingTask task;
task.type = PENDING_PRINTER_DELETE;
task.printer_id = id;
AddPendingTask(task);
}
void CloudPrintConnector::AddPendingRegisterTask(
const printing::PrinterBasicInfo& info) {
PendingTask task;
task.type = PENDING_PRINTER_REGISTER;
task.printer_info = info;
AddPendingTask(task);
}
void CloudPrintConnector::AddPendingTask(const PendingTask& task) {
pending_tasks_.push_back(task);
// If this is the only pending task, we need to start the process.
if (pending_tasks_.size() == 1) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&CloudPrintConnector::ProcessPendingTask, this));
}
}
void CloudPrintConnector::ProcessPendingTask() {
if (!IsRunning())
return; // Orphan call.
if (pending_tasks_.empty())
return; // No peding tasks.
PendingTask task = pending_tasks_.front();
switch (task.type) {
case PENDING_PRINTERS_AVAILABLE :
OnPrintersAvailable();
break;
case PENDING_PRINTER_REGISTER :
OnPrinterRegister(task.printer_info);
break;
case PENDING_PRINTER_DELETE :
OnPrinterDelete(task.printer_id);
break;
default:
NOTREACHED();
}
}
void CloudPrintConnector::ContinuePendingTaskProcessing() {
if (pending_tasks_.empty())
return; // No pending tasks.
// Delete current task and repost if we have more task available.
pending_tasks_.pop_front();
if (pending_tasks_.empty())
return;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&CloudPrintConnector::ProcessPendingTask, this));
}
void CloudPrintConnector::OnPrintersAvailable() {
GURL printer_list_url = GetUrlForPrinterList(
settings_.server_url(), settings_.proxy_id());
StartGetRequest(printer_list_url,
kCloudPrintRegisterMaxRetryCount,
&CloudPrintConnector::HandlePrinterListResponse);
}
void CloudPrintConnector::OnPrinterRegister(
const printing::PrinterBasicInfo& info) {
for (const auto& it : job_handler_map_) {
if (IsSamePrinter(it.second->GetPrinterName(), info.printer_name)) {
// Printer already registered, continue to the next task.
ContinuePendingTaskProcessing();
return;
}
}
// Asynchronously fetch the printer caps and defaults. The story will
// continue in OnReceivePrinterCaps.
print_system_->GetPrinterCapsAndDefaults(
info.printer_name.c_str(),
base::Bind(&CloudPrintConnector::OnReceivePrinterCaps,
base::Unretained(this)));
}
void CloudPrintConnector::OnPrinterDelete(const std::string& printer_id) {
// Remove corresponding printer job handler.
auto it = job_handler_map_.find(printer_id);
if (it != job_handler_map_.end()) {
it->second->Shutdown();
job_handler_map_.erase(it);
}
// TODO(gene): We probably should not try indefinitely here. Just once or
// twice should be enough.
// Bug: http://code.google.com/p/chromium/issues/detail?id=101850
GURL url = GetUrlForPrinterDelete(
settings_.server_url(), printer_id, "printer_deleted");
StartGetRequest(url,
kCloudPrintAPIMaxRetryCount,
&CloudPrintConnector::HandlePrinterDeleteResponse);
}
void CloudPrintConnector::OnReceivePrinterCaps(
bool succeeded,
const std::string& printer_name,
const printing::PrinterCapsAndDefaults& caps_and_defaults) {
if (!IsRunning())
return; // Orphan call.
DCHECK(!pending_tasks_.empty());
DCHECK_EQ(PENDING_PRINTER_REGISTER, pending_tasks_.front().type);
if (!succeeded) {
LOG(ERROR) << "CP_CONNECTOR: Failed to get printer info"
<< ", printer name: " << printer_name;
// This printer failed to register, notify the server of this failure.
base::string16 printer_name_utf16 = base::UTF8ToUTF16(printer_name);
std::string status_message = l10n_util::GetStringFUTF8(
IDS_CLOUD_PRINT_REGISTER_PRINTER_FAILED,
printer_name_utf16,
l10n_util::GetStringUTF16(IDS_GOOGLE_CLOUD_PRINT));
ReportUserMessage(kGetPrinterCapsFailedMessageId, status_message);
ContinuePendingTaskProcessing(); // Skip this printer registration.
return;
}
const printing::PrinterBasicInfo& info = pending_tasks_.front().printer_info;
DCHECK(IsSamePrinter(info.printer_name, printer_name));
std::string mime_boundary = net::GenerateMimeMultipartBoundary();
std::string post_data;
net::AddMultipartValueForUpload(kProxyIdValue,
settings_.proxy_id(), mime_boundary, std::string(), &post_data);
net::AddMultipartValueForUpload(kPrinterNameValue,
info.printer_name, mime_boundary, std::string(), &post_data);
net::AddMultipartValueForUpload(kPrinterDescValue,
info.printer_description, mime_boundary, std::string(), &post_data);
net::AddMultipartValueForUpload(kPrinterStatusValue,
base::IntToString(info.printer_status),
mime_boundary, std::string(), &post_data);
// Add local_settings with a current XMPP ping interval.
net::AddMultipartValueForUpload(kPrinterLocalSettingsValue,
base::StringPrintf(kCreateLocalSettingsXmppPingFormat,
settings_.xmpp_ping_timeout_sec()),
mime_boundary, std::string(), &post_data);
post_data += GetPostDataForPrinterInfo(info, mime_boundary);
if (caps_and_defaults.caps_mime_type == kContentTypeJSON) {
net::AddMultipartValueForUpload(kUseCDD, "true", mime_boundary,
std::string(), &post_data);
}
net::AddMultipartValueForUpload(kPrinterCapsValue,
caps_and_defaults.printer_capabilities, mime_boundary,
caps_and_defaults.caps_mime_type, &post_data);
net::AddMultipartValueForUpload(kPrinterDefaultsValue,
caps_and_defaults.printer_defaults, mime_boundary,
caps_and_defaults.defaults_mime_type, &post_data);
// Send a hash of the printer capabilities to the server. We will use this
// later to check if the capabilities have changed
net::AddMultipartValueForUpload(kPrinterCapsHashValue,
base::MD5String(caps_and_defaults.printer_capabilities),
mime_boundary, std::string(), &post_data);
net::AddMultipartFinalDelimiterForUpload(mime_boundary, &post_data);
std::string mime_type("multipart/form-data; boundary=");
mime_type += mime_boundary;
GURL post_url = GetUrlForPrinterRegistration(settings_.server_url());
StartPostRequest(CloudPrintURLFetcher::REQUEST_REGISTER, post_url,
kCloudPrintAPIMaxRetryCount, mime_type, post_data,
&CloudPrintConnector::HandleRegisterPrinterResponse);
}
bool CloudPrintConnector::IsSamePrinter(const std::string& name1,
const std::string& name2) const {
return base::EqualsCaseInsensitiveASCII(name1, name2);
}
} // namespace cloud_print