blob: c54ff751fb3dc38c0e5e74a99f3963ff0c6a2577 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/sync/engine/commit_contribution_impl.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/guid.h"
#include "base/logging.h"
#include "base/values.h"
#include "components/sync/base/data_type_histogram.h"
#include "components/sync/base/features.h"
#include "components/sync/base/passphrase_enums.h"
#include "components/sync/base/time.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/engine/commit_and_get_updates_types.h"
#include "components/sync/engine/cycle/entity_change_metric_recording.h"
#include "components/sync/engine/model_type_worker.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/protocol/proto_value_conversions.h"
#include "components/sync/protocol/sync.pb.h"
#include "components/sync/protocol/sync_entity.pb.h"
namespace syncer {
namespace {
CommitResponseData BuildCommitResponseData(
const CommitRequestData& commit_request,
const sync_pb::CommitResponse_EntryResponse& entry_response) {
CommitResponseData response_data;
response_data.id = entry_response.id_string();
response_data.response_version = entry_response.version();
response_data.client_tag_hash = commit_request.entity->client_tag_hash;
response_data.sequence_number = commit_request.sequence_number;
response_data.specifics_hash = commit_request.specifics_hash;
response_data.unsynced_time = commit_request.unsynced_time;
return response_data;
}
FailedCommitResponseData BuildFailedCommitResponseData(
const CommitRequestData& commit_request,
const sync_pb::CommitResponse_EntryResponse& entry_response) {
FailedCommitResponseData response_data;
response_data.client_tag_hash = commit_request.entity->client_tag_hash;
response_data.response_type = entry_response.response_type();
response_data.datatype_specific_error =
entry_response.datatype_specific_error();
return response_data;
}
} // namespace
CommitContributionImpl::CommitContributionImpl(
ModelType type,
const sync_pb::DataTypeContext& context,
CommitRequestDataList commit_requests,
base::OnceCallback<void(const CommitResponseDataList&,
const FailedCommitResponseDataList&)>
on_commit_response_callback,
base::OnceCallback<void(SyncCommitError)> on_full_commit_failure_callback,
Cryptographer* cryptographer,
PassphraseType passphrase_type,
bool only_commit_specifics)
: type_(type),
on_commit_response_callback_(std::move(on_commit_response_callback)),
on_full_commit_failure_callback_(
std::move(on_full_commit_failure_callback)),
cryptographer_(cryptographer),
passphrase_type_(passphrase_type),
context_(context),
commit_requests_(std::move(commit_requests)),
only_commit_specifics_(only_commit_specifics) {}
CommitContributionImpl::~CommitContributionImpl() = default;
void CommitContributionImpl::AddToCommitMessage(
sync_pb::ClientToServerMessage* msg) {
sync_pb::CommitMessage* commit_message = msg->mutable_commit();
entries_start_index_ = commit_message->entries_size();
commit_message->mutable_entries()->Reserve(commit_message->entries_size() +
commit_requests_.size());
for (const std::unique_ptr<CommitRequestData>& commit_request :
commit_requests_) {
sync_pb::SyncEntity* sync_entity = commit_message->add_entries();
if (only_commit_specifics_) {
DCHECK(!commit_request->entity->is_deleted());
DCHECK(!cryptographer_);
// Only send specifics to server for commit-only types.
sync_entity->mutable_specifics()->CopyFrom(
commit_request->entity->specifics);
} else {
PopulateCommitProto(type_, *commit_request, sync_entity);
AdjustCommitProto(sync_entity);
}
// Purposefully crash if we have client only data, as this could result in
// sending password in plain text.
CHECK(
!sync_entity->specifics().password().has_client_only_encrypted_data());
// Purposefully crash since no metadata should be uploaded if a custom
// passphrase is set.
CHECK(!IsExplicitPassphrase(passphrase_type_) ||
!sync_entity->specifics().password().has_unencrypted_metadata());
// Record the size of the sync entity being committed.
syncer::SyncRecordModelTypeEntitySizeHistogram(
type_, sync_entity->specifics().ByteSizeLong());
if (commit_request->entity->is_deleted()) {
RecordEntityChangeMetrics(type_, ModelTypeEntityChange::kLocalDeletion);
} else if (commit_request->base_version <= 0) {
RecordEntityChangeMetrics(type_, ModelTypeEntityChange::kLocalCreation);
} else {
RecordEntityChangeMetrics(type_, ModelTypeEntityChange::kLocalUpdate);
}
}
if (!context_.context().empty())
commit_message->add_client_contexts()->CopyFrom(context_);
}
SyncerError CommitContributionImpl::ProcessCommitResponse(
const sync_pb::ClientToServerResponse& response,
StatusController* status) {
CommitResponseDataList success_response_list;
FailedCommitResponseDataList error_response_list;
bool has_unknown_error = false;
bool has_conflicting_commits = false;
bool has_transient_error_commits = false;
for (size_t i = 0; i < commit_requests_.size(); ++i) {
// Fill |success_response_list| or |error_response_list|.
const sync_pb::CommitResponse_EntryResponse& entry_response =
response.commit().entryresponse(entries_start_index_ + i);
if (entry_response.response_type() == sync_pb::CommitResponse::SUCCESS) {
success_response_list.push_back(
BuildCommitResponseData(*commit_requests_[i], entry_response));
} else {
error_response_list.push_back(
BuildFailedCommitResponseData(*commit_requests_[i], entry_response));
}
// Update |status| and mark the presence of specific errors (e.g.
// conflicting commits).
switch (entry_response.response_type()) {
case sync_pb::CommitResponse::SUCCESS:
status->increment_num_successful_commits();
if (type_ == BOOKMARKS) {
status->increment_num_successful_bookmark_commits();
}
break;
case sync_pb::CommitResponse::INVALID_MESSAGE:
DLOG(ERROR) << "Server reports commit message is invalid.";
has_unknown_error = true;
break;
case sync_pb::CommitResponse::CONFLICT:
DVLOG(1) << "Server reports conflict for commit message.";
status->increment_num_server_conflicts();
has_conflicting_commits = true;
break;
case sync_pb::CommitResponse::OVER_QUOTA:
case sync_pb::CommitResponse::RETRY:
case sync_pb::CommitResponse::TRANSIENT_ERROR:
DLOG(WARNING) << "Entity commit blocked by transient error.";
has_transient_error_commits = true;
break;
}
}
// Send whatever successful and failed responses we did get back to our
// parent. It's the schedulers job to handle the failures, but parent may
// react to them as well.
std::move(on_commit_response_callback_)
.Run(success_response_list, error_response_list);
// Commit was successfully processed. We do not want to call both
// |on_commit_response_callback_| and |on_full_commit_failure_callback_|.
on_full_commit_failure_callback_.Reset();
// Let the scheduler know about the failures.
if (has_unknown_error) {
return SyncerError(SyncerError::SERVER_RETURN_UNKNOWN_ERROR);
}
if (has_transient_error_commits) {
return SyncerError(SyncerError::SERVER_RETURN_TRANSIENT_ERROR);
}
if (has_conflicting_commits) {
return SyncerError(SyncerError::SERVER_RETURN_CONFLICT);
}
return SyncerError(SyncerError::SYNCER_OK);
}
void CommitContributionImpl::ProcessCommitFailure(
SyncCommitError commit_error) {
std::move(on_full_commit_failure_callback_).Run(commit_error);
on_commit_response_callback_.Reset();
}
size_t CommitContributionImpl::GetNumEntries() const {
return commit_requests_.size();
}
// static
void CommitContributionImpl::PopulateCommitProto(
ModelType type,
const CommitRequestData& commit_entity,
sync_pb::SyncEntity* commit_proto) {
const EntityData& entity_data = *commit_entity.entity;
DCHECK(!entity_data.specifics.has_encrypted());
commit_proto->set_id_string(entity_data.id);
if (type == NIGORI) {
// Client tags are irrelevant for NIGORI since it uses the root node. For
// historical reasons (although it's unclear if this continues to be
// needed), the root node is considered a folder.
commit_proto->set_folder(true);
} else if (type != BOOKMARKS ||
!entity_data.client_tag_hash.value().empty()) {
// The client tag is mandatory for all datatypes except bookmarks, and
// for bookmarks it depends on the version of the browser that was used
// to originally create the bookmark.
commit_proto->set_client_tag_hash(entity_data.client_tag_hash.value());
}
commit_proto->set_version(commit_entity.base_version);
commit_proto->set_deleted(entity_data.is_deleted());
commit_proto->set_name(entity_data.name);
if (!entity_data.is_deleted()) {
// Handle bookmarks separately.
if (type == BOOKMARKS) {
// Populate SyncEntity.folder for backward-compatibility.
switch (entity_data.specifics.bookmark().type()) {
case sync_pb::BookmarkSpecifics::UNSPECIFIED:
NOTREACHED();
break;
case sync_pb::BookmarkSpecifics::URL:
commit_proto->set_folder(false);
break;
case sync_pb::BookmarkSpecifics::FOLDER:
commit_proto->set_folder(true);
break;
}
const UniquePosition unique_position = UniquePosition::FromProto(
entity_data.specifics.bookmark().unique_position());
DCHECK(unique_position.IsValid());
*commit_proto->mutable_unique_position() = unique_position.ToProto();
// parent_id field is set only for legacy clients only, before M99.
if (!entity_data.legacy_parent_id.empty()) {
commit_proto->set_parent_id_string(entity_data.legacy_parent_id);
}
}
commit_proto->set_ctime(TimeToProtoTime(entity_data.creation_time));
commit_proto->set_mtime(TimeToProtoTime(entity_data.modification_time));
commit_proto->mutable_specifics()->CopyFrom(entity_data.specifics);
}
}
void CommitContributionImpl::AdjustCommitProto(
sync_pb::SyncEntity* commit_proto) {
if (commit_proto->version() == kUncommittedVersion) {
commit_proto->set_version(0);
// Initial commits need our help to generate a client ID if they don't have
// any. Bookmarks create their own IDs on the frontend side to be able to
// match them after commits. For other data types we generate one here. And
// since bookmarks don't have client tags, their server id should be stable
// across restarts in case of recommitting an item, it doesn't result in
// creating a duplicate.
if (commit_proto->id_string().empty()) {
commit_proto->set_id_string(base::GenerateGUID());
}
}
// Encrypt the specifics and hide the title if necessary.
if (commit_proto->specifics().has_password()) {
DCHECK(cryptographer_);
const sync_pb::PasswordSpecifics& password_specifics =
commit_proto->specifics().password();
const sync_pb::PasswordSpecificsData& password_data =
password_specifics.client_only_encrypted_data();
sync_pb::EntitySpecifics encrypted_password;
// Keep the unencrypted metadata for non-custom passphrase users.
if (!IsExplicitPassphrase(passphrase_type_)) {
*encrypted_password.mutable_password()->mutable_unencrypted_metadata() =
commit_proto->specifics().password().unencrypted_metadata();
}
bool result = cryptographer_->Encrypt(
password_data,
encrypted_password.mutable_password()->mutable_encrypted());
DCHECK(result);
if (base::FeatureList::IsEnabled(syncer::kPasswordNotesWithBackup)) {
// `encrypted_notes_backup` field needs to be populated regardless of
// whether or not there are any notes.
result = cryptographer_->Encrypt(password_data.notes(),
encrypted_password.mutable_password()
->mutable_encrypted_notes_backup());
DCHECK(result);
// When encrypting both blobs succeeds, both encrypted blobs must use the
// key name.
DCHECK_EQ(
encrypted_password.password().encrypted().key_name(),
encrypted_password.password().encrypted_notes_backup().key_name());
}
*commit_proto->mutable_specifics() = std::move(encrypted_password);
commit_proto->set_name("encrypted");
} else if (cryptographer_) {
if (commit_proto->has_specifics()) {
sync_pb::EntitySpecifics encrypted_specifics;
bool result = cryptographer_->Encrypt(
commit_proto->specifics(), encrypted_specifics.mutable_encrypted());
DCHECK(result);
commit_proto->mutable_specifics()->CopyFrom(encrypted_specifics);
}
commit_proto->set_name("encrypted");
}
// See crbug.com/915133: Certain versions of Chrome (e.g. M71) handle corrupt
// SESSIONS data poorly. Let's guard against future versions from committing
// problematic data that could cause crashes on other syncing devices.
if (commit_proto->specifics().session().has_tab()) {
CHECK_GE(commit_proto->specifics().session().tab_node_id(), 0);
}
// Always include enough specifics to identify the type. Do this even in
// deletion requests, where the specifics are otherwise invalid.
AddDefaultFieldValue(type_, commit_proto->mutable_specifics());
}
} // namespace syncer