|  | // 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 <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, | 
|  | 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)); | 
|  | } | 
|  |  | 
|  | 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 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 old_version = local_entry->GetBaseVersion(); | 
|  | int64 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 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 |