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