| // Copyright 2013 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 "sync/engine/commit_util.h" |
| |
| #include <stdint.h> |
| |
| #include <limits> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/strings/string_util.h" |
| #include "sync/engine/syncer_proto_util.h" |
| #include "sync/internal_api/public/base/attachment_id_proto.h" |
| #include "sync/internal_api/public/base/unique_position.h" |
| #include "sync/protocol/bookmark_specifics.pb.h" |
| #include "sync/protocol/sync.pb.h" |
| #include "sync/sessions/sync_session.h" |
| #include "sync/syncable/directory.h" |
| #include "sync/syncable/entry.h" |
| #include "sync/syncable/model_neutral_mutable_entry.h" |
| #include "sync/syncable/syncable_base_transaction.h" |
| #include "sync/syncable/syncable_base_write_transaction.h" |
| #include "sync/syncable/syncable_changes_version.h" |
| #include "sync/syncable/syncable_proto_util.h" |
| #include "sync/syncable/syncable_util.h" |
| #include "sync/util/time.h" |
| |
| using std::set; |
| using std::string; |
| using std::vector; |
| |
| namespace syncer { |
| |
| using syncable::Entry; |
| using syncable::Id; |
| |
| namespace commit_util { |
| |
| void AddExtensionsActivityToMessage( |
| ExtensionsActivity* activity, |
| ExtensionsActivity::Records* extensions_activity_buffer, |
| sync_pb::CommitMessage* message) { |
| // This isn't perfect, since the set of extensions activity may not correlate |
| // exactly with the items being committed. That's OK as long as we're looking |
| // for a rough estimate of extensions activity, not an precise mapping of |
| // which commits were triggered by which extension. |
| // |
| // We will push this list of extensions activity back into the |
| // ExtensionsActivityMonitor if this commit fails. That's why we must keep a |
| // copy of these records in the session. |
| activity->GetAndClearRecords(extensions_activity_buffer); |
| |
| const ExtensionsActivity::Records& records = *extensions_activity_buffer; |
| for (ExtensionsActivity::Records::const_iterator it = |
| records.begin(); |
| it != records.end(); ++it) { |
| sync_pb::ChromiumExtensionsActivity* activity_message = |
| message->add_extensions_activity(); |
| activity_message->set_extension_id(it->second.extension_id); |
| activity_message->set_bookmark_writes_since_last_commit( |
| it->second.bookmark_write_count); |
| } |
| } |
| |
| void AddClientConfigParamsToMessage( |
| ModelTypeSet enabled_types, |
| bool cookie_jar_mismatch, |
| sync_pb::CommitMessage* message) { |
| sync_pb::ClientConfigParams* config_params = message->mutable_config_params(); |
| for (ModelTypeSet::Iterator it = enabled_types.First(); it.Good(); it.Inc()) { |
| if (ProxyTypes().Has(it.Get())) |
| continue; |
| int field_number = GetSpecificsFieldNumberFromModelType(it.Get()); |
| config_params->mutable_enabled_type_ids()->Add(field_number); |
| } |
| config_params->set_tabs_datatype_enabled( |
| enabled_types.Has(syncer::PROXY_TABS)); |
| config_params->set_cookie_jar_mismatch(cookie_jar_mismatch); |
| } |
| |
| namespace { |
| |
| void SetEntrySpecifics(const Entry& meta_entry, |
| sync_pb::SyncEntity* sync_entry) { |
| // Add the new style extension and the folder bit. |
| sync_entry->mutable_specifics()->CopyFrom(meta_entry.GetSpecifics()); |
| sync_entry->set_folder(meta_entry.GetIsDir()); |
| |
| CHECK(!sync_entry->specifics().password().has_client_only_encrypted_data()); |
| DCHECK_EQ(meta_entry.GetModelType(), GetModelType(*sync_entry)); |
| } |
| |
| void SetAttachmentIds(const Entry& meta_entry, |
| sync_pb::SyncEntity* sync_entry) { |
| const sync_pb::AttachmentMetadata& attachment_metadata = |
| meta_entry.GetAttachmentMetadata(); |
| for (int i = 0; i < attachment_metadata.record_size(); ++i) { |
| *sync_entry->add_attachment_id() = attachment_metadata.record(i).id(); |
| } |
| } |
| |
| } // namespace |
| |
| void BuildCommitItem( |
| const syncable::Entry& meta_entry, |
| sync_pb::SyncEntity* sync_entry) { |
| syncable::Id id = meta_entry.GetId(); |
| sync_entry->set_id_string(SyncableIdToProto(id)); |
| |
| string name = meta_entry.GetNonUniqueName(); |
| CHECK(!name.empty()); // Make sure this isn't an update. |
| // Note: Truncation is also performed in WriteNode::SetTitle(..). But this |
| // call is still necessary to handle any title changes that might originate |
| // elsewhere, or already be persisted in the directory. |
| base::TruncateUTF8ToByteSize(name, 255, &name); |
| sync_entry->set_name(name); |
| |
| // Set the non_unique_name. If we do, the server ignores |
| // the |name| value (using |non_unique_name| instead), and will return |
| // in the CommitResponse a unique name if one is generated. |
| // We send both because it may aid in logging. |
| sync_entry->set_non_unique_name(name); |
| |
| if (!meta_entry.GetUniqueClientTag().empty()) { |
| sync_entry->set_client_defined_unique_tag( |
| meta_entry.GetUniqueClientTag()); |
| } |
| |
| // Deleted items with server-unknown parent ids can be a problem so we set |
| // the parent to 0. (TODO(sync): Still true in protocol?). |
| Id new_parent_id; |
| if (meta_entry.GetIsDel() && |
| !meta_entry.GetParentId().ServerKnows()) { |
| new_parent_id = syncable::BaseTransaction::root_id(); |
| } else { |
| new_parent_id = meta_entry.GetParentId(); |
| } |
| |
| if (meta_entry.ShouldMaintainHierarchy()) { |
| sync_entry->set_parent_id_string(SyncableIdToProto(new_parent_id)); |
| } |
| |
| // If our parent has changed, send up the old one so the server |
| // can correctly deal with multiple parents. |
| // TODO(nick): With the server keeping track of the primary sync parent, |
| // it should not be necessary to provide the old_parent_id: the version |
| // number should suffice. |
| Id server_parent_id = meta_entry.GetServerParentId(); |
| if (new_parent_id != server_parent_id && !server_parent_id.IsNull() && |
| 0 != meta_entry.GetBaseVersion() && |
| syncable::CHANGES_VERSION != meta_entry.GetBaseVersion()) { |
| sync_entry->set_old_parent_id(SyncableIdToProto(server_parent_id)); |
| } |
| |
| int64_t version = meta_entry.GetBaseVersion(); |
| if (syncable::CHANGES_VERSION == version || 0 == version) { |
| // Undeletions are only supported for items that have a client tag. |
| DCHECK(!id.ServerKnows() || |
| !meta_entry.GetUniqueClientTag().empty()) |
| << meta_entry; |
| |
| // Version 0 means to create or undelete an object. |
| sync_entry->set_version(0); |
| } else { |
| DCHECK(id.ServerKnows()) << meta_entry; |
| sync_entry->set_version(meta_entry.GetBaseVersion()); |
| } |
| sync_entry->set_ctime(TimeToProtoTime(meta_entry.GetCtime())); |
| sync_entry->set_mtime(TimeToProtoTime(meta_entry.GetMtime())); |
| |
| SetAttachmentIds(meta_entry, sync_entry); |
| |
| // Handle bookmarks separately. |
| if (meta_entry.GetSpecifics().has_bookmark()) { |
| if (meta_entry.GetIsDel()) { |
| sync_entry->set_deleted(true); |
| } else { |
| // Both insert_after_item_id and position_in_parent fields are set only |
| // for legacy reasons. See comments in sync.proto for more information. |
| const Id& prev_id = meta_entry.GetPredecessorId(); |
| string prev_id_string = |
| prev_id.IsNull() ? string() : prev_id.GetServerId(); |
| sync_entry->set_insert_after_item_id(prev_id_string); |
| sync_entry->set_position_in_parent( |
| meta_entry.GetUniquePosition().ToInt64()); |
| meta_entry.GetUniquePosition().ToProto( |
| sync_entry->mutable_unique_position()); |
| if (!meta_entry.GetUniquePosition().IsValid()) { |
| // Should never upload invalid unique position for bookmark to server. |
| base::debug::DumpWithoutCrashing(); |
| } |
| } |
| // Always send specifics for bookmarks. |
| SetEntrySpecifics(meta_entry, sync_entry); |
| return; |
| } |
| |
| // Deletion is final on the server, let's move things and then delete them. |
| if (meta_entry.GetIsDel()) { |
| sync_entry->set_deleted(true); |
| |
| sync_pb::EntitySpecifics type_only_specifics; |
| AddDefaultFieldValue(meta_entry.GetModelType(), |
| sync_entry->mutable_specifics()); |
| } else { |
| SetEntrySpecifics(meta_entry, sync_entry); |
| } |
| } |
| |
| // Helpers for ProcessSingleCommitResponse. |
| namespace { |
| |
| void LogServerError(const sync_pb::CommitResponse_EntryResponse& res) { |
| if (res.has_error_message()) |
| LOG(WARNING) << " " << res.error_message(); |
| else |
| LOG(WARNING) << " No detailed error message returned from server"; |
| } |
| |
| const string& GetResultingPostCommitName( |
| const sync_pb::SyncEntity& committed_entry, |
| const sync_pb::CommitResponse_EntryResponse& entry_response) { |
| const string& response_name = |
| SyncerProtoUtil::NameFromCommitEntryResponse(entry_response); |
| if (!response_name.empty()) |
| return response_name; |
| return SyncerProtoUtil::NameFromSyncEntity(committed_entry); |
| } |
| |
| bool UpdateVersionAfterCommit( |
| const sync_pb::SyncEntity& committed_entry, |
| const sync_pb::CommitResponse_EntryResponse& entry_response, |
| const syncable::Id& pre_commit_id, |
| syncable::ModelNeutralMutableEntry* local_entry) { |
| int64_t old_version = local_entry->GetBaseVersion(); |
| int64_t new_version = entry_response.version(); |
| bool bad_commit_version = false; |
| if (committed_entry.deleted() && |
| !local_entry->GetUniqueClientTag().empty()) { |
| // If the item was deleted, and it's undeletable (uses the client tag), |
| // change the version back to zero. We must set the version to zero so |
| // that the server knows to re-create the item if it gets committed |
| // later for undeletion. |
| new_version = 0; |
| } else if (!pre_commit_id.ServerKnows()) { |
| bad_commit_version = 0 == new_version; |
| } else { |
| bad_commit_version = old_version > new_version; |
| } |
| if (bad_commit_version) { |
| LOG(ERROR) << "Bad version in commit return for " << *local_entry |
| << " new_id:" << SyncableIdFromProto(entry_response.id_string()) |
| << " new_version:" << entry_response.version(); |
| return false; |
| } |
| |
| // Update the base version and server version. The base version must change |
| // here, even if syncing_was_set is false; that's because local changes were |
| // on top of the successfully committed version. |
| local_entry->PutBaseVersion(new_version); |
| DVLOG(1) << "Commit is changing base version of " << local_entry->GetId() |
| << " to: " << new_version; |
| local_entry->PutServerVersion(new_version); |
| return true; |
| } |
| |
| bool ChangeIdAfterCommit( |
| const sync_pb::CommitResponse_EntryResponse& entry_response, |
| const syncable::Id& pre_commit_id, |
| syncable::ModelNeutralMutableEntry* local_entry) { |
| syncable::BaseWriteTransaction* trans = local_entry->base_write_transaction(); |
| const syncable::Id& entry_response_id = |
| SyncableIdFromProto(entry_response.id_string()); |
| if (entry_response_id != pre_commit_id) { |
| if (pre_commit_id.ServerKnows()) { |
| // The server can sometimes generate a new ID on commit; for example, |
| // when committing an undeletion. |
| DVLOG(1) << " ID changed while committing an old entry. " |
| << pre_commit_id << " became " << entry_response_id << "."; |
| } |
| syncable::ModelNeutralMutableEntry same_id( |
| trans, |
| syncable::GET_BY_ID, |
| entry_response_id); |
| // We should trap this before this function. |
| if (same_id.good()) { |
| LOG(ERROR) << "ID clash with id " << entry_response_id |
| << " during commit " << same_id; |
| return false; |
| } |
| ChangeEntryIDAndUpdateChildren(trans, local_entry, entry_response_id); |
| DVLOG(1) << "Changing ID to " << entry_response_id; |
| } |
| return true; |
| } |
| |
| void UpdateServerFieldsAfterCommit( |
| const sync_pb::SyncEntity& committed_entry, |
| const sync_pb::CommitResponse_EntryResponse& entry_response, |
| syncable::ModelNeutralMutableEntry* local_entry) { |
| |
| // We just committed an entry successfully, and now we want to make our view |
| // of the server state consistent with the server state. We must be careful; |
| // |entry_response| and |committed_entry| have some identically named |
| // fields. We only want to consider fields from |committed_entry| when there |
| // is not an overriding field in the |entry_response|. We do not want to |
| // update the server data from the local data in the entry -- it's possible |
| // that the local data changed during the commit, and even if not, the server |
| // has the last word on the values of several properties. |
| |
| local_entry->PutServerIsDel(committed_entry.deleted()); |
| if (committed_entry.deleted()) { |
| // Don't clobber any other fields of deleted objects. |
| return; |
| } |
| |
| local_entry->PutServerIsDir( |
| (committed_entry.folder() || |
| committed_entry.bookmarkdata().bookmark_folder())); |
| local_entry->PutServerSpecifics(committed_entry.specifics()); |
| local_entry->PutServerAttachmentMetadata( |
| CreateAttachmentMetadata(committed_entry.attachment_id())); |
| local_entry->PutServerMtime(ProtoTimeToTime(committed_entry.mtime())); |
| local_entry->PutServerCtime(ProtoTimeToTime(committed_entry.ctime())); |
| if (committed_entry.has_unique_position()) { |
| local_entry->PutServerUniquePosition( |
| UniquePosition::FromProto( |
| committed_entry.unique_position())); |
| } |
| |
| // TODO(nick): The server doesn't set entry_response.server_parent_id in |
| // practice; to update SERVER_PARENT_ID appropriately here we'd need to |
| // get the post-commit ID of the parent indicated by |
| // committed_entry.parent_id_string(). That should be inferrable from the |
| // information we have, but it's a bit convoluted to pull it out directly. |
| // Getting this right is important: SERVER_PARENT_ID gets fed back into |
| // old_parent_id during the next commit. |
| local_entry->PutServerParentId(local_entry->GetParentId()); |
| local_entry->PutServerNonUniqueName( |
| GetResultingPostCommitName(committed_entry, entry_response)); |
| |
| if (local_entry->GetIsUnappliedUpdate()) { |
| // This shouldn't happen; an unapplied update shouldn't be committed, and |
| // if it were, the commit should have failed. But if it does happen: we've |
| // just overwritten the update info, so clear the flag. |
| local_entry->PutIsUnappliedUpdate(false); |
| } |
| } |
| |
| void ProcessSuccessfulCommitResponse( |
| const sync_pb::SyncEntity& committed_entry, |
| const sync_pb::CommitResponse_EntryResponse& entry_response, |
| const syncable::Id& pre_commit_id, |
| syncable::ModelNeutralMutableEntry* local_entry, |
| bool dirty_sync_was_set, set<syncable::Id>* deleted_folders) { |
| DCHECK(local_entry->GetIsUnsynced()); |
| |
| // Update SERVER_VERSION and BASE_VERSION. |
| if (!UpdateVersionAfterCommit(committed_entry, entry_response, pre_commit_id, |
| local_entry)) { |
| LOG(ERROR) << "Bad version in commit return for " << *local_entry |
| << " new_id:" << SyncableIdFromProto(entry_response.id_string()) |
| << " new_version:" << entry_response.version(); |
| return; |
| } |
| |
| // If the server gave us a new ID, apply it. |
| if (!ChangeIdAfterCommit(entry_response, pre_commit_id, local_entry)) { |
| return; |
| } |
| |
| // Update our stored copy of the server state. |
| UpdateServerFieldsAfterCommit(committed_entry, entry_response, local_entry); |
| |
| // If the item doesn't need to be committed again (an item might need to be |
| // committed again if it changed locally during the commit), we can remove |
| // it from the unsynced list. |
| if (!dirty_sync_was_set) { |
| local_entry->PutIsUnsynced(false); |
| } |
| |
| // Make a note of any deleted folders, whose children would have |
| // been recursively deleted. |
| // TODO(nick): Here, commit_message.deleted() would be more correct than |
| // local_entry->GetIsDel(). For example, an item could be renamed, and then |
| // deleted during the commit of the rename. Unit test & fix. |
| if (local_entry->GetIsDir() && local_entry->GetIsDel()) { |
| deleted_folders->insert(local_entry->GetId()); |
| } |
| } |
| |
| } // namespace |
| |
| sync_pb::CommitResponse::ResponseType ProcessSingleCommitResponse( |
| syncable::BaseWriteTransaction* trans, |
| const sync_pb::CommitResponse_EntryResponse& server_entry, |
| const sync_pb::SyncEntity& commit_request_entry, |
| int64_t metahandle, |
| set<syncable::Id>* deleted_folders) { |
| syncable::ModelNeutralMutableEntry local_entry( |
| trans, |
| syncable::GET_BY_HANDLE, |
| metahandle); |
| CHECK(local_entry.good()); |
| bool dirty_sync_was_set = local_entry.GetDirtySync(); |
| local_entry.PutDirtySync(false); |
| local_entry.PutSyncing(false); |
| |
| sync_pb::CommitResponse::ResponseType response = server_entry.response_type(); |
| if (!sync_pb::CommitResponse::ResponseType_IsValid(response)) { |
| LOG(ERROR) << "Commit response has unknown response type! Possibly out " |
| "of date client?"; |
| return sync_pb::CommitResponse::INVALID_MESSAGE; |
| } |
| if (sync_pb::CommitResponse::TRANSIENT_ERROR == response) { |
| DVLOG(1) << "Transient Error Committing: " << local_entry; |
| LogServerError(server_entry); |
| return sync_pb::CommitResponse::TRANSIENT_ERROR; |
| } |
| if (sync_pb::CommitResponse::INVALID_MESSAGE == response) { |
| LOG(ERROR) << "Error Commiting: " << local_entry; |
| LogServerError(server_entry); |
| return response; |
| } |
| if (sync_pb::CommitResponse::CONFLICT == response) { |
| DVLOG(1) << "Conflict Committing: " << local_entry; |
| return response; |
| } |
| if (sync_pb::CommitResponse::RETRY == response) { |
| DVLOG(1) << "Retry Committing: " << local_entry; |
| return response; |
| } |
| if (sync_pb::CommitResponse::OVER_QUOTA == response) { |
| LOG(WARNING) << "Hit deprecated OVER_QUOTA Committing: " << local_entry; |
| return response; |
| } |
| if (!server_entry.has_id_string()) { |
| LOG(ERROR) << "Commit response has no id"; |
| return sync_pb::CommitResponse::INVALID_MESSAGE; |
| } |
| |
| // Implied by the IsValid call above, but here for clarity. |
| DCHECK_EQ(sync_pb::CommitResponse::SUCCESS, response) << response; |
| // Check to see if we've been given the ID of an existing entry. If so treat |
| // it as an error response and retry later. |
| const syncable::Id& server_entry_id = |
| SyncableIdFromProto(server_entry.id_string()); |
| if (local_entry.GetId() != server_entry_id) { |
| Entry e(trans, syncable::GET_BY_ID, server_entry_id); |
| if (e.good()) { |
| LOG(ERROR) |
| << "Got duplicate id when commiting id: " |
| << local_entry.GetId() |
| << ". Treating as an error return"; |
| return sync_pb::CommitResponse::INVALID_MESSAGE; |
| } |
| } |
| |
| if (server_entry.version() == 0) { |
| LOG(WARNING) << "Server returned a zero version on a commit response."; |
| } |
| |
| ProcessSuccessfulCommitResponse(commit_request_entry, server_entry, |
| local_entry.GetId(), &local_entry, dirty_sync_was_set, deleted_folders); |
| return response; |
| } |
| |
| } // namespace commit_util |
| |
| } // namespace syncer |