blob: d595441c9396e6de402828e7dbff33d6efdcbe55 [file] [log] [blame]
// Copyright 2018 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 "components/sync_bookmarks/bookmark_remote_updates_handler.h"
#include <algorithm>
#include <memory>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include "base/guid.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/engine/model_type_processor_metrics.h"
#include "components/sync/model/conflict_resolution.h"
#include "components/sync/protocol/entity_metadata.pb.h"
#include "components/sync/protocol/unique_position.pb.h"
#include "components/sync_bookmarks/bookmark_specifics_conversions.h"
#include "components/sync_bookmarks/switches.h"
#include "components/sync_bookmarks/synced_bookmark_tracker_entity.h"
namespace sync_bookmarks {
namespace {
// Used in metrics: "Sync.ProblematicServerSideBookmarks". These values are
// persisted to logs. Entries should not be renumbered and numeric values
// should never be reused.
enum class RemoteBookmarkUpdateError {
// Remote and local bookmarks types don't match (URL vs. Folder).
kConflictingTypes = 0,
// Invalid specifics.
kInvalidSpecifics = 1,
// Invalid unique position.
// kDeprecatedInvalidUniquePosition = 2,
// Permanent node creation in an incremental update.
// kDeprecatedPermanentNodeCreationAfterMerge = 3,
// Parent entity not found in server.
kMissingParentEntity = 4,
// Parent node not found locally.
kMissingParentNode = 5,
// Parent entity not found in server when processing a conflict.
kMissingParentEntityInConflict = 6,
// Parent Parent node not found locally when processing a conflict.
kMissingParentNodeInConflict = 7,
// Failed to create a bookmark.
kCreationFailure = 8,
// The bookmark's GUID did not match the originator client item ID.
kUnexpectedGuid = 9,
// Parent is not a folder.
kParentNotFolder = 10,
// The GUID changed for an already-tracked server ID.
kGuidChangedForTrackedServerId = 11,
// An update to a permanent node received without a server-defined unique tag.
kTrackedServerIdWithoutServerTagMatchesPermanentNode = 12,
kMaxValue = kTrackedServerIdWithoutServerTagMatchesPermanentNode,
};
void LogProblematicBookmark(RemoteBookmarkUpdateError problem) {
base::UmaHistogramEnumeration("Sync.ProblematicServerSideBookmarks", problem);
}
// Recursive method to traverse a forest created by ReorderValidUpdates() to
// emit updates in top-down order. |ordered_updates| must not be null because
// traversed updates are appended to |*ordered_updates|.
void TraverseAndAppendChildren(
const base::GUID& node_guid,
const std::unordered_multimap<base::GUID,
const syncer::UpdateResponseData*,
base::GUIDHash>& guid_to_updates,
const std::unordered_map<base::GUID,
std::vector<base::GUID>,
base::GUIDHash>& node_to_children,
std::vector<const syncer::UpdateResponseData*>* ordered_updates) {
// If no children to traverse, we are done.
if (node_to_children.count(node_guid) == 0) {
return;
}
// Recurse over all children.
for (const base::GUID& child : node_to_children.at(node_guid)) {
auto [begin, end] = guid_to_updates.equal_range(child);
DCHECK(begin != end);
for (auto it = begin; it != end; ++it) {
ordered_updates->push_back(it->second);
}
TraverseAndAppendChildren(child, guid_to_updates, node_to_children,
ordered_updates);
}
}
syncer::UniquePosition ComputeUniquePositionForTrackedBookmarkNode(
const SyncedBookmarkTracker* bookmark_tracker,
const bookmarks::BookmarkNode* bookmark_node) {
DCHECK(bookmark_tracker);
const SyncedBookmarkTrackerEntity* child_entity =
bookmark_tracker->GetEntityForBookmarkNode(bookmark_node);
DCHECK(child_entity);
// TODO(crbug.com/1113139): precompute UniquePosition to prevent its
// calculation on each remote update.
return syncer::UniquePosition::FromProto(
child_entity->metadata()->unique_position());
}
size_t ComputeChildNodeIndex(const bookmarks::BookmarkNode* parent,
const sync_pb::UniquePosition& unique_position,
const SyncedBookmarkTracker* bookmark_tracker) {
DCHECK(parent);
DCHECK(bookmark_tracker);
const syncer::UniquePosition position =
syncer::UniquePosition::FromProto(unique_position);
auto iter = std::partition_point(
parent->children().begin(), parent->children().end(),
[bookmark_tracker,
&position](const std::unique_ptr<bookmarks::BookmarkNode>& child) {
// Return true for all |parent|'s children whose position is less than
// |position|.
return !position.LessThan(ComputeUniquePositionForTrackedBookmarkNode(
bookmark_tracker, child.get()));
});
return iter - parent->children().begin();
}
bool IsPermanentNodeUpdate(const syncer::EntityData& update_entity) {
return !update_entity.server_defined_unique_tag.empty();
}
// Checks that the |update_entity| is valid and returns false otherwise. It is
// used to verify non-deletion updates. |update| must not be a deletion and a
// permanent node (they are processed in a different way).
bool IsValidUpdate(const syncer::EntityData& update_entity) {
DCHECK(!update_entity.is_deleted());
DCHECK(!IsPermanentNodeUpdate(update_entity));
if (!IsValidBookmarkSpecifics(update_entity.specifics.bookmark())) {
// Ignore updates with invalid specifics.
DLOG(ERROR)
<< "Couldn't process an update bookmark with an invalid specifics.";
LogProblematicBookmark(RemoteBookmarkUpdateError::kInvalidSpecifics);
return false;
}
if (!HasExpectedBookmarkGuid(update_entity.specifics.bookmark(),
update_entity.client_tag_hash,
update_entity.originator_cache_guid,
update_entity.originator_client_item_id)) {
// Ignore updates with an unexpected GUID.
DLOG(ERROR) << "Couldn't process an update bookmark with unexpected GUID: "
<< update_entity.specifics.bookmark().guid();
LogProblematicBookmark(RemoteBookmarkUpdateError::kUnexpectedGuid);
return false;
}
return true;
}
// Determines the parent's GUID included in |update_entity|. |update_entity|
// must be a valid update as defined in IsValidUpdate().
base::GUID GetParentGUIDInUpdate(const syncer::EntityData& update_entity) {
DCHECK(IsValidUpdate(update_entity));
base::GUID parent_guid = base::GUID::ParseLowercase(
update_entity.specifics.bookmark().parent_guid());
DCHECK(parent_guid.is_valid());
return parent_guid;
}
void ApplyRemoteUpdate(
const syncer::UpdateResponseData& update,
const SyncedBookmarkTrackerEntity* tracked_entity,
const SyncedBookmarkTrackerEntity* new_parent_tracked_entity,
bookmarks::BookmarkModel* model,
SyncedBookmarkTracker* tracker,
favicon::FaviconService* favicon_service) {
const syncer::EntityData& update_entity = update.entity;
DCHECK(!update_entity.is_deleted());
DCHECK(tracked_entity);
DCHECK(tracked_entity->bookmark_node());
DCHECK(new_parent_tracked_entity);
DCHECK(model);
DCHECK(tracker);
DCHECK(favicon_service);
DCHECK_EQ(
tracked_entity->bookmark_node()->guid(),
base::GUID::ParseLowercase(update_entity.specifics.bookmark().guid()));
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
const bookmarks::BookmarkNode* old_parent = node->parent();
const bookmarks::BookmarkNode* new_parent =
new_parent_tracked_entity->bookmark_node();
DCHECK(old_parent);
DCHECK(new_parent);
DCHECK(old_parent->is_folder());
DCHECK(new_parent->is_folder());
if (update_entity.specifics.bookmark().type() !=
GetProtoTypeFromBookmarkNode(node)) {
DLOG(ERROR) << "Could not update bookmark node due to conflicting types";
LogProblematicBookmark(RemoteBookmarkUpdateError::kConflictingTypes);
return;
}
UpdateBookmarkNodeFromSpecifics(update_entity.specifics.bookmark(), node,
model, favicon_service);
// Compute index information before updating the |tracker|.
const size_t old_index = static_cast<size_t>(old_parent->GetIndexOf(node));
const size_t new_index = ComputeChildNodeIndex(
new_parent, update_entity.specifics.bookmark().unique_position(),
tracker);
tracker->Update(tracked_entity, update.response_version,
update_entity.modification_time, update_entity.specifics);
if (new_parent == old_parent &&
(new_index == old_index || new_index == old_index + 1)) {
// Node hasn't moved. No more work to do.
return;
}
// Node has moved to another position under the same parent. Update the model.
// BookmarkModel takes care of placing the node in the correct position if the
// node is move to the left. (i.e. no need to subtract one from |new_index|).
model->Move(node, new_parent, new_index);
}
} // namespace
BookmarkRemoteUpdatesHandler::BookmarkRemoteUpdatesHandler(
bookmarks::BookmarkModel* bookmark_model,
favicon::FaviconService* favicon_service,
SyncedBookmarkTracker* bookmark_tracker)
: bookmark_model_(bookmark_model),
favicon_service_(favicon_service),
bookmark_tracker_(bookmark_tracker) {
DCHECK(bookmark_model);
DCHECK(bookmark_tracker);
DCHECK(favicon_service);
}
void BookmarkRemoteUpdatesHandler::Process(
const syncer::UpdateResponseDataList& updates,
bool got_new_encryption_requirements) {
TRACE_EVENT0("sync", "BookmarkRemoteUpdatesHandler::Process");
bookmark_tracker_->CheckAllNodesTracked(bookmark_model_);
// If new encryption requirements come from the server, the entities that are
// in |updates| will be recorded here so they can be ignored during the
// re-encryption phase at the end.
std::unordered_set<std::string> entities_with_up_to_date_encryption;
for (const syncer::UpdateResponseData* update :
ReorderValidUpdates(&updates)) {
const syncer::EntityData& update_entity = update->entity;
DCHECK(!IsPermanentNodeUpdate(update_entity));
DCHECK(update_entity.is_deleted() || IsValidUpdate(update_entity));
bool should_ignore_update = false;
const SyncedBookmarkTrackerEntity* tracked_entity =
DetermineLocalTrackedEntityToUpdate(update_entity,
&should_ignore_update);
if (should_ignore_update) {
continue;
}
// Filter out permanent nodes once again (in case the server tag wasn't
// populated and yet the entity ID points to a permanent node). This case
// shoudn't be possible with a well-behaving server.
if (tracked_entity && tracked_entity->bookmark_node() &&
tracked_entity->bookmark_node()->is_permanent_node()) {
DLOG(ERROR) << "Ignoring update to permanent node without server defined "
"unique tag for ID "
<< update_entity.id;
LogProblematicBookmark(
RemoteBookmarkUpdateError::
kTrackedServerIdWithoutServerTagMatchesPermanentNode);
continue;
}
// Ignore updates that have already been seen according to the version.
if (tracked_entity && tracked_entity->metadata()->server_version() >=
update->response_version) {
if (update_entity.id == tracked_entity->metadata()->server_id()) {
// Seen this update before. This update may be a reflection and may have
// missing the GUID in specifics. Next reupload will populate GUID in
// specifics and this codepath will not repeat indefinitely. This logic
// is needed for the case when there is only one device and hence the
// GUID will not be set by other devices.
ReuploadEntityIfNeeded(update_entity, tracked_entity);
}
continue;
}
// Record freshness of the update to UMA. To mimic the behavior in
// ClientTagBasedModelTypeProcessor, one scenario is special-cased: an
// incoming tombstone for an entity that is not tracked.
if (tracked_entity || !update_entity.is_deleted()) {
syncer::LogNonReflectionUpdateFreshnessToUma(
syncer::BOOKMARKS,
/*remote_modification_time=*/
update_entity.modification_time);
}
// The server ID has changed for a tracked entity (matched via client tag).
// This can happen if a commit succeeds, but the response does not come back
// fast enough(e.g. before shutdown or crash), then the |bookmark_tracker_|
// might assume that it was never committed. The server will track the
// client that sent up the original commit and return this in a get updates
// response. This also may happen due to duplicate GUIDs. In this case it's
// better to update to the latest server ID.
if (tracked_entity) {
bookmark_tracker_->UpdateSyncIdIfNeeded(tracked_entity,
/*sync_id=*/update_entity.id);
}
if (tracked_entity && tracked_entity->IsUnsynced()) {
tracked_entity = ProcessConflict(*update, tracked_entity);
if (!tracked_entity) {
// During conflict resolution, the entity could be dropped in case of
// a conflict between local and remote deletions. We shouldn't worry
// about changes to the encryption in that case.
continue;
}
} else if (update_entity.is_deleted()) {
ProcessDelete(update_entity, tracked_entity);
// If the local entity has been deleted, no need to check for out of date
// encryption. Therefore, we can go ahead and process the next update.
continue;
} else if (!tracked_entity) {
tracked_entity = ProcessCreate(*update);
if (!tracked_entity) {
// If no new node has been tracked, we shouldn't worry about changes to
// the encryption.
continue;
}
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
} else {
ProcessUpdate(*update, tracked_entity);
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
}
// If the received entity has out of date encryption, we schedule another
// commit to fix it.
if (bookmark_tracker_->model_type_state().encryption_key_name() !=
update->encryption_key_name) {
DVLOG(2) << "Bookmarks: Requesting re-encrypt commit "
<< update->encryption_key_name << " -> "
<< bookmark_tracker_->model_type_state().encryption_key_name();
bookmark_tracker_->IncrementSequenceNumber(tracked_entity);
}
if (got_new_encryption_requirements) {
entities_with_up_to_date_encryption.insert(update_entity.id);
}
}
// Recommit entities with out of date encryption.
if (got_new_encryption_requirements) {
std::vector<const SyncedBookmarkTrackerEntity*> all_entities =
bookmark_tracker_->GetAllEntities();
for (const SyncedBookmarkTrackerEntity* entity : all_entities) {
// No need to recommit tombstones and permanent nodes.
if (entity->metadata()->is_deleted()) {
continue;
}
DCHECK(entity->bookmark_node());
if (entity->bookmark_node()->is_permanent_node()) {
continue;
}
if (entities_with_up_to_date_encryption.count(
entity->metadata()->server_id()) != 0) {
continue;
}
bookmark_tracker_->IncrementSequenceNumber(entity);
}
}
bookmark_tracker_->CheckAllNodesTracked(bookmark_model_);
}
// static
std::vector<const syncer::UpdateResponseData*>
BookmarkRemoteUpdatesHandler::ReorderValidUpdatesForTest(
const syncer::UpdateResponseDataList* updates) {
return ReorderValidUpdates(updates);
}
// static
size_t BookmarkRemoteUpdatesHandler::ComputeChildNodeIndexForTest(
const bookmarks::BookmarkNode* parent,
const sync_pb::UniquePosition& unique_position,
const SyncedBookmarkTracker* bookmark_tracker) {
return ComputeChildNodeIndex(parent, unique_position, bookmark_tracker);
}
// static
std::vector<const syncer::UpdateResponseData*>
BookmarkRemoteUpdatesHandler::ReorderValidUpdates(
const syncer::UpdateResponseDataList* updates) {
// This method sorts the remote updates according to the following rules:
// 1. Creations and updates come before deletions.
// 2. Parent creation/update should come before child creation/update.
// 3. No need to further order deletions. Parent deletions can happen before
// child deletions. This is safe because all updates (e.g. moves) should
// have been processed already.
// The algorithm works by constructing a forest of all non-deletion updates
// and then traverses each tree in the forest recursively: Forest
// Construction:
// 1. Iterate over all updates and construct the |parent_to_children| map and
// collect all parents in |roots|.
// 2. Iterate over all updates again and drop any parent that has a
// coressponding update. What's left in |roots| are the roots of the
// forest.
// 3. Start at each root in |roots|, emit the update and recurse over its
// children.
// Normally there shouldn't be multiple updates for the same GUID, but let's
// avoiding dedupping here just in case (e.g. the could in theory be a
// combination of client-tagged and non-client-tagged updated that
// ModelTypeWorker failed to deduplicate.
std::unordered_multimap<base::GUID, const syncer::UpdateResponseData*,
base::GUIDHash>
guid_to_updates;
// Add only valid, non-deletions to |guid_to_updates|.
int invalid_updates_count = 0;
int root_node_updates_count = 0;
for (const syncer::UpdateResponseData& update : *updates) {
const syncer::EntityData& update_entity = update.entity;
// Ignore updates to root nodes.
if (IsPermanentNodeUpdate(update_entity)) {
++root_node_updates_count;
continue;
}
if (update_entity.is_deleted()) {
continue;
}
if (!IsValidUpdate(update_entity)) {
++invalid_updates_count;
continue;
}
base::GUID guid =
base::GUID::ParseLowercase(update_entity.specifics.bookmark().guid());
DCHECK(guid.is_valid());
guid_to_updates.emplace(std::move(guid), &update);
}
// Iterate over |guid_to_updates| and construct |roots| and
// |parent_to_children|.
std::set<base::GUID> roots;
std::unordered_map<base::GUID, std::vector<base::GUID>, base::GUIDHash>
parent_to_children;
for (const auto& [guid, update] : guid_to_updates) {
base::GUID parent_guid = GetParentGUIDInUpdate(update->entity);
base::GUID child_guid =
base::GUID::ParseLowercase(update->entity.specifics.bookmark().guid());
DCHECK(child_guid.is_valid());
parent_to_children[parent_guid].emplace_back(std::move(child_guid));
// If this entity's parent has no pending update, add it to |roots|.
if (guid_to_updates.count(parent_guid) == 0) {
roots.insert(std::move(parent_guid));
}
}
// |roots| contains only root of all trees in the forest all of which are
// ready to be processed because none has a pending update.
std::vector<const syncer::UpdateResponseData*> ordered_updates;
for (const base::GUID& root : roots) {
TraverseAndAppendChildren(root, guid_to_updates, parent_to_children,
&ordered_updates);
}
// Add deletions.
for (const syncer::UpdateResponseData& update : *updates) {
const syncer::EntityData& update_entity = update.entity;
if (!IsPermanentNodeUpdate(update_entity) && update_entity.is_deleted()) {
ordered_updates.push_back(&update);
}
}
// All non root updates should have been included in |ordered_updates|.
DCHECK_EQ(updates->size(), ordered_updates.size() + root_node_updates_count +
invalid_updates_count);
return ordered_updates;
}
const SyncedBookmarkTrackerEntity*
BookmarkRemoteUpdatesHandler::DetermineLocalTrackedEntityToUpdate(
const syncer::EntityData& update_entity,
bool* should_ignore_update) {
*should_ignore_update = false;
// If there's nothing other than a server ID to issue a lookup, just do that
// and return immediately. This is the case for permanent nodes and possibly
// tombstones (at least the LoopbackServer only sets the server ID).
if (update_entity.originator_client_item_id.empty() &&
update_entity.client_tag_hash.value().empty()) {
return bookmark_tracker_->GetEntityForSyncId(update_entity.id);
}
// Parse the client tag hash in the update or infer it from the originator
// information (all of which are immutable properties of a sync entity).
const syncer::ClientTagHash client_tag_hash_in_update =
!update_entity.client_tag_hash.value().empty()
? update_entity.client_tag_hash
: SyncedBookmarkTracker::GetClientTagHashFromGUID(
InferGuidFromLegacyOriginatorId(
update_entity.originator_cache_guid,
update_entity.originator_client_item_id));
const SyncedBookmarkTrackerEntity* const tracked_entity_by_client_tag =
bookmark_tracker_->GetEntityForClientTagHash(client_tag_hash_in_update);
const SyncedBookmarkTrackerEntity* const tracked_entity_by_sync_id =
bookmark_tracker_->GetEntityForSyncId(update_entity.id);
// The most common scenario is that both lookups, client-tag-based and
// server-ID-based, refer to the same tracked entity or both lookups fail. In
// that case there's nothing to reconcile and the function can return
// trivially.
if (tracked_entity_by_client_tag == tracked_entity_by_sync_id) {
return tracked_entity_by_client_tag;
}
// Client-tags (GUIDs) are known at all times and immutable (as opposed to
// server IDs which get a temp value for local creations), so they cannot have
// changed.
if (tracked_entity_by_sync_id &&
tracked_entity_by_sync_id->GetClientTagHash() !=
client_tag_hash_in_update) {
// The client tag has changed for an already-tracked entity, which is a
// protocol violation. This should be practically unreachable, but guard
// against misbehaving servers.
DLOG(ERROR) << "Ignoring remote bookmark update with protocol violation: "
"GUID must be immutable";
LogProblematicBookmark(
RemoteBookmarkUpdateError::kGuidChangedForTrackedServerId);
*should_ignore_update = true;
return nullptr;
}
// At this point |tracked_entity_by_client_tag| must be non-null because
// otherwise one of the two codepaths above would have returned early.
DCHECK(tracked_entity_by_client_tag);
DCHECK(!tracked_entity_by_sync_id);
return tracked_entity_by_client_tag;
}
const SyncedBookmarkTrackerEntity* BookmarkRemoteUpdatesHandler::ProcessCreate(
const syncer::UpdateResponseData& update) {
const syncer::EntityData& update_entity = update.entity;
DCHECK(!update_entity.is_deleted());
DCHECK(!IsPermanentNodeUpdate(update_entity));
DCHECK(IsValidBookmarkSpecifics(update_entity.specifics.bookmark()));
const bookmarks::BookmarkNode* parent_node = GetParentNode(update_entity);
if (!parent_node) {
// If we cannot find the parent, we can do nothing.
LogProblematicBookmark(RemoteBookmarkUpdateError::kMissingParentNode);
bookmark_tracker_->RecordIgnoredServerUpdateDueToMissingParent(
update.response_version);
return nullptr;
}
if (!parent_node->is_folder()) {
LogProblematicBookmark(RemoteBookmarkUpdateError::kParentNotFolder);
return nullptr;
}
const bookmarks::BookmarkNode* bookmark_node =
CreateBookmarkNodeFromSpecifics(
update_entity.specifics.bookmark(), parent_node,
ComputeChildNodeIndex(
parent_node, update_entity.specifics.bookmark().unique_position(),
bookmark_tracker_),
bookmark_model_, favicon_service_);
DCHECK(bookmark_node);
const SyncedBookmarkTrackerEntity* entity = bookmark_tracker_->Add(
bookmark_node, update_entity.id, update.response_version,
update_entity.creation_time, update_entity.specifics);
ReuploadEntityIfNeeded(update_entity, entity);
return entity;
}
void BookmarkRemoteUpdatesHandler::ProcessUpdate(
const syncer::UpdateResponseData& update,
const SyncedBookmarkTrackerEntity* tracked_entity) {
const syncer::EntityData& update_entity = update.entity;
// Can only update existing nodes.
DCHECK(tracked_entity);
DCHECK(tracked_entity->bookmark_node());
DCHECK(!tracked_entity->bookmark_node()->is_permanent_node());
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
// Must not be a deletion.
DCHECK(!update_entity.is_deleted());
DCHECK(!IsPermanentNodeUpdate(update_entity));
DCHECK(IsValidBookmarkSpecifics(update_entity.specifics.bookmark()));
DCHECK(!tracked_entity->IsUnsynced());
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
const bookmarks::BookmarkNode* old_parent = node->parent();
DCHECK(old_parent);
DCHECK(old_parent->is_folder());
const SyncedBookmarkTrackerEntity* new_parent_entity =
bookmark_tracker_->GetEntityForGUID(GetParentGUIDInUpdate(update_entity));
if (!new_parent_entity) {
LogProblematicBookmark(RemoteBookmarkUpdateError::kMissingParentEntity);
return;
}
const bookmarks::BookmarkNode* new_parent =
new_parent_entity->bookmark_node();
if (!new_parent) {
LogProblematicBookmark(RemoteBookmarkUpdateError::kMissingParentNode);
return;
}
if (!new_parent->is_folder()) {
LogProblematicBookmark(RemoteBookmarkUpdateError::kParentNotFolder);
return;
}
// Node update could be either in the node data (e.g. title or
// unique_position), or it could be that the node has moved under another
// parent without any data change.
if (tracked_entity->MatchesData(update_entity)) {
DCHECK_EQ(new_parent, old_parent);
bookmark_tracker_->Update(tracked_entity, update.response_version,
update_entity.modification_time,
update_entity.specifics);
ReuploadEntityIfNeeded(update_entity, tracked_entity);
return;
}
ApplyRemoteUpdate(update, tracked_entity, new_parent_entity, bookmark_model_,
bookmark_tracker_, favicon_service_);
ReuploadEntityIfNeeded(update_entity, tracked_entity);
}
void BookmarkRemoteUpdatesHandler::ProcessDelete(
const syncer::EntityData& update_entity,
const SyncedBookmarkTrackerEntity* tracked_entity) {
DCHECK(update_entity.is_deleted());
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
// Handle corner cases first.
if (tracked_entity == nullptr) {
// Process deletion only if the entity is still tracked. It could have
// been recursively deleted already with an earlier deletion of its
// parent.
DVLOG(1) << "Received remote delete for a non-existing item.";
return;
}
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
DCHECK(node);
// Changes to permanent nodes have been filtered out earlier.
DCHECK(!node->is_permanent_node());
// Remove the entities of |node| and its children.
RemoveEntityAndChildrenFromTracker(node);
// Remove the node and its children from the model.
bookmark_model_->Remove(node);
}
const SyncedBookmarkTrackerEntity*
BookmarkRemoteUpdatesHandler::ProcessConflict(
const syncer::UpdateResponseData& update,
const SyncedBookmarkTrackerEntity* tracked_entity) {
const syncer::EntityData& update_entity = update.entity;
// TODO(crbug.com/516866): Handle the case of conflict as a result of
// re-encryption request.
// Can only conflict with existing nodes.
DCHECK(tracked_entity);
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
DCHECK(!tracked_entity->bookmark_node() ||
!tracked_entity->bookmark_node()->is_permanent_node());
DCHECK(!IsPermanentNodeUpdate(update_entity));
if (tracked_entity->metadata()->is_deleted() && update_entity.is_deleted()) {
// Both have been deleted, delete the corresponding entity from the tracker.
bookmark_tracker_->Remove(tracked_entity);
DLOG(WARNING) << "Conflict: CHANGES_MATCH";
return nullptr;
}
if (update_entity.is_deleted()) {
// Only remote has been deleted. Local wins. Record that we received the
// update from the server but leave the pending commit intact.
bookmark_tracker_->UpdateServerVersion(tracked_entity,
update.response_version);
DLOG(WARNING) << "Conflict: USE_LOCAL";
return tracked_entity;
}
DCHECK(IsValidBookmarkSpecifics(update_entity.specifics.bookmark()));
if (tracked_entity->metadata()->is_deleted()) {
// Only local node has been deleted. It should be restored from the server
// data as a remote creation.
bookmark_tracker_->Remove(tracked_entity);
DLOG(WARNING) << "Conflict: USE_REMOTE";
return ProcessCreate(update);
}
// No deletions, there are potentially conflicting updates.
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
const bookmarks::BookmarkNode* old_parent = node->parent();
DCHECK(old_parent);
DCHECK(old_parent->is_folder());
const SyncedBookmarkTrackerEntity* new_parent_entity =
bookmark_tracker_->GetEntityForGUID(GetParentGUIDInUpdate(update_entity));
// The |new_parent_entity| could be null in some racy conditions. For
// example, when a client A moves a node and deletes the old parent and
// commits, and then updates the node again, and at the same time client B
// updates before receiving the move updates. The client B update will arrive
// at client A after the parent entity has been deleted already.
if (!new_parent_entity) {
LogProblematicBookmark(
RemoteBookmarkUpdateError::kMissingParentEntityInConflict);
return tracked_entity;
}
const bookmarks::BookmarkNode* new_parent =
new_parent_entity->bookmark_node();
// |new_parent| would be null if the parent has been deleted locally and not
// committed yet. Deletions are executed recursively, so a parent deletions
// entails child deletion, and if this child has been updated on another
// client, this would cause conflict.
if (!new_parent) {
LogProblematicBookmark(
RemoteBookmarkUpdateError::kMissingParentNodeInConflict);
return tracked_entity;
}
// Either local and remote data match or server wins, and in both cases we
// should squash any pending commits.
bookmark_tracker_->AckSequenceNumber(tracked_entity);
// Node update could be either in the node data (e.g. title or
// unique_position), or it could be that the node has moved under another
// parent without any data change.
if (tracked_entity->MatchesData(update_entity)) {
DCHECK_EQ(new_parent, old_parent);
bookmark_tracker_->Update(tracked_entity, update.response_version,
update_entity.modification_time,
update_entity.specifics);
// The changes are identical so there isn't a real conflict.
DLOG(WARNING) << "Conflict: CHANGES_MATCH";
} else {
// Conflict where data don't match and no remote deletion, and hence server
// wins. Update the model from server data.
DLOG(WARNING) << "Conflict: USE_REMOTE";
ApplyRemoteUpdate(update, tracked_entity, new_parent_entity,
bookmark_model_, bookmark_tracker_, favicon_service_);
}
ReuploadEntityIfNeeded(update_entity, tracked_entity);
return tracked_entity;
}
void BookmarkRemoteUpdatesHandler::RemoveEntityAndChildrenFromTracker(
const bookmarks::BookmarkNode* node) {
DCHECK(node);
DCHECK(!node->is_permanent_node());
const SyncedBookmarkTrackerEntity* entity =
bookmark_tracker_->GetEntityForBookmarkNode(node);
DCHECK(entity);
bookmark_tracker_->Remove(entity);
for (const auto& child : node->children())
RemoveEntityAndChildrenFromTracker(child.get());
}
const bookmarks::BookmarkNode* BookmarkRemoteUpdatesHandler::GetParentNode(
const syncer::EntityData& update_entity) const {
DCHECK(IsValidBookmarkSpecifics(update_entity.specifics.bookmark()));
const SyncedBookmarkTrackerEntity* parent_entity =
bookmark_tracker_->GetEntityForGUID(GetParentGUIDInUpdate(update_entity));
if (!parent_entity) {
return nullptr;
}
return parent_entity->bookmark_node();
}
void BookmarkRemoteUpdatesHandler::ReuploadEntityIfNeeded(
const syncer::EntityData& entity_data,
const SyncedBookmarkTrackerEntity* tracked_entity) {
DCHECK(tracked_entity);
DCHECK_EQ(tracked_entity->metadata()->server_id(), entity_data.id);
DCHECK(!tracked_entity->bookmark_node() ||
!tracked_entity->bookmark_node()->is_permanent_node());
// Do not initiate reupload if the local entity is a tombstone.
const bool is_reupload_needed = tracked_entity->bookmark_node() &&
IsBookmarkEntityReuploadNeeded(entity_data);
base::UmaHistogramBoolean(
"Sync.BookmarkEntityReuploadNeeded.OnIncrementalUpdate",
is_reupload_needed);
if (is_reupload_needed) {
bookmark_tracker_->IncrementSequenceNumber(tracked_entity);
}
}
} // namespace sync_bookmarks