blob: 6a815bb5e14a09ab05d9514833b88ae852c02e3c [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_specifics_conversions.h"
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "base/guid.h"
#include "base/hash/sha1.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/favicon/core/favicon_service.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/protocol/bookmark_specifics.pb.h"
#include "components/sync/protocol/entity_data.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync_bookmarks/switches.h"
#include "ui/gfx/favicon_size.h"
#include "url/gurl.h"
namespace sync_bookmarks {
namespace {
// Maximum number of bytes to allow in a legacy canonicalized title (must match
// sync's internal limits; see write_node.cc).
const int kLegacyCanonicalizedTitleLimitBytes = 255;
// The list of bookmark titles which are reserved for use by the server.
const char* const kForbiddenTitles[] = {"", ".", ".."};
// Maximum size for the favicon URL. This limit should be very generous in most
// cases, the notable exception being data: URLs that encode the content of
// the favicon itself in the URL, and may be arbitrarily large.
const int kMaxFaviconUrlSize = 4096;
// Used in metrics: "Sync.InvalidBookmarkSpecifics". These values are
// persisted to logs. Entries should not be renumbered and numeric values
// should never be reused.
enum class InvalidBookmarkSpecificsError {
kEmptySpecifics = 0,
kInvalidURL = 1,
kIconURLWithoutFavicon = 2,
kInvalidIconURL = 3,
kNonUniqueMetaInfoKeys = 4,
kInvalidGUID = 5,
kInvalidParentGUID = 6,
kInvalidUniquePosition = 7,
kBannedGUID = 8,
kMaxValue = kBannedGUID,
};
void LogInvalidSpecifics(InvalidBookmarkSpecificsError error) {
base::UmaHistogramEnumeration("Sync.InvalidBookmarkSpecifics", error);
}
void LogFaviconContainedInSpecifics(bool contains_favicon) {
base::UmaHistogramBoolean(
"Sync.BookmarkSpecificsExcludingFoldersContainFavicon", contains_favicon);
}
void UpdateBookmarkSpecificsMetaInfo(
const bookmarks::BookmarkNode::MetaInfoMap* metainfo_map,
sync_pb::BookmarkSpecifics* bm_specifics) {
for (const auto& [key, value] : *metainfo_map) {
sync_pb::MetaInfo* meta_info = bm_specifics->add_meta_info();
meta_info->set_key(key);
meta_info->set_value(value);
}
}
// Metainfo entries in |specifics| must have unique keys.
bookmarks::BookmarkNode::MetaInfoMap GetBookmarkMetaInfo(
const sync_pb::BookmarkSpecifics& specifics) {
bookmarks::BookmarkNode::MetaInfoMap meta_info_map;
for (const sync_pb::MetaInfo& meta_info : specifics.meta_info()) {
meta_info_map[meta_info.key()] = meta_info.value();
}
DCHECK_EQ(static_cast<size_t>(specifics.meta_info_size()),
meta_info_map.size());
return meta_info_map;
}
// Sets the favicon of the given bookmark node from the given specifics.
void SetBookmarkFaviconFromSpecifics(
const sync_pb::BookmarkSpecifics& specifics,
const bookmarks::BookmarkNode* bookmark_node,
favicon::FaviconService* favicon_service) {
DCHECK(bookmark_node);
DCHECK(!bookmark_node->is_folder());
DCHECK(favicon_service);
favicon_service->AddPageNoVisitForBookmark(bookmark_node->url(),
bookmark_node->GetTitle());
const std::string& icon_bytes_str = specifics.favicon();
scoped_refptr<base::RefCountedString> icon_bytes(
new base::RefCountedString());
icon_bytes->data().assign(icon_bytes_str);
GURL icon_url(specifics.icon_url());
if (icon_bytes->size() == 0 && icon_url.is_empty()) {
// Empty icon URL and no bitmap data means no icon mapping.
LogFaviconContainedInSpecifics(false);
favicon_service->DeleteFaviconMappings({bookmark_node->url()},
favicon_base::IconType::kFavicon);
return;
}
LogFaviconContainedInSpecifics(true);
if (icon_url.is_empty()) {
// See documentation in BookmarkSpecifics to understand the (rare) scenarios
// where |icon_url| may be missing despite a favicon image itself (proto
// field |favicon|) being set. In this case, use the page URL as a fake icon
// URL as it is guaranteed to be unique.
icon_url = GURL(bookmark_node->url());
}
// The client may have cached the favicon at 2x. Use MergeFavicon() as not to
// overwrite the cached 2x favicon bitmap. Sync favicons are always
// gfx::kFaviconSize in width and height. Store the favicon into history
// as such.
gfx::Size pixel_size(gfx::kFaviconSize, gfx::kFaviconSize);
favicon_service->MergeFavicon(bookmark_node->url(), icon_url,
favicon_base::IconType::kFavicon, icon_bytes,
pixel_size);
}
// This is an exact copy of the same code in bookmark_update_preprocessing.cc.
std::string ComputeGuidFromBytes(base::span<const uint8_t> bytes) {
DCHECK_GE(bytes.size(), 16U);
// This implementation is based on the equivalent logic in base/guid.cc.
// Set the GUID to version 4 as described in RFC 4122, section 4.4.
// The format of GUID version 4 must be xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx,
// where y is one of [8, 9, A, B].
// Clear the version bits and set the version to 4:
const uint8_t byte6 = (bytes[6] & 0x0fU) | 0xf0U;
// Set the two most significant bits (bits 6 and 7) of the
// clock_seq_hi_and_reserved to zero and one, respectively:
const uint8_t byte8 = (bytes[8] & 0x3fU) | 0x80U;
return base::StringPrintf(
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], byte6,
bytes[7], byte8, bytes[9], bytes[10], bytes[11], bytes[12], bytes[13],
bytes[14], bytes[15]);
}
// This is an exact copy of the same code in bookmark_update_preprocessing.cc,
// which could be removed if eventually client tags are adapted/inferred in
// ModelTypeWorker. The reason why this is non-trivial today is that some users
// are known to contain corrupt data in the sense that several different
// entities (identified by their server-provided ID) use the same client tag
// (and GUID). Currently BookmarkModelMerger has logic to prefer folders over
// regular URLs and reassign GUIDs.
std::string InferGuidForLegacyBookmark(
const std::string& originator_cache_guid,
const std::string& originator_client_item_id) {
DCHECK(
!base::GUID::ParseCaseInsensitive(originator_client_item_id).is_valid());
const std::string unique_tag =
base::StrCat({originator_cache_guid, originator_client_item_id});
const base::SHA1Digest hash =
base::SHA1HashSpan(base::as_bytes(base::make_span(unique_tag)));
static_assert(base::kSHA1Length >= 16, "16 bytes needed to infer GUID");
const std::string guid = ComputeGuidFromBytes(base::make_span(hash));
DCHECK(base::GUID::ParseLowercase(guid).is_valid());
return guid;
}
bool IsForbiddenTitleWithMaybeTrailingSpaces(const std::string& title) {
return base::Contains(
kForbiddenTitles,
base::TrimWhitespaceASCII(title, base::TrimPositions::TRIM_TRAILING));
}
std::u16string NodeTitleFromSpecifics(
const sync_pb::BookmarkSpecifics& specifics) {
if (specifics.has_full_title()) {
return base::UTF8ToUTF16(specifics.full_title());
}
std::string node_title = specifics.legacy_canonicalized_title();
if (base::EndsWith(node_title, " ") &&
IsForbiddenTitleWithMaybeTrailingSpaces(node_title)) {
// Legacy clients added an extra space to the real title, so remove it here.
// See also FullTitleToLegacyCanonicalizedTitle().
node_title.pop_back();
}
return base::UTF8ToUTF16(node_title);
}
void MoveAllChildren(bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* old_parent,
const bookmarks::BookmarkNode* new_parent) {
DCHECK(old_parent && old_parent->is_folder());
DCHECK(new_parent && new_parent->is_folder());
DCHECK(old_parent != new_parent);
DCHECK(new_parent->children().empty());
if (old_parent->children().empty()) {
return;
}
// This code relies on the underlying type to store children in the
// BookmarkModel which is vector. It moves the last child from |old_parent| to
// the end of |new_parent| step by step (which reverses the order of
// children). After that all children must be reordered to keep the original
// order in |new_parent|.
// This algorithm is used because of performance reasons.
std::vector<const bookmarks::BookmarkNode*> children_order(
old_parent->children().size(), nullptr);
for (size_t i = old_parent->children().size(); i > 0; --i) {
const size_t old_index = i - 1;
const bookmarks::BookmarkNode* child_to_move =
old_parent->children()[old_index].get();
children_order[old_index] = child_to_move;
model->Move(child_to_move, new_parent, new_parent->children().size());
}
model->ReorderChildren(new_parent, children_order);
}
} // namespace
std::string FullTitleToLegacyCanonicalizedTitle(const std::string& node_title) {
// Add an extra space for backward compatibility with legacy clients.
std::string specifics_title =
IsForbiddenTitleWithMaybeTrailingSpaces(node_title) ? node_title + " "
: node_title;
base::TruncateUTF8ToByteSize(
specifics_title, kLegacyCanonicalizedTitleLimitBytes, &specifics_title);
return specifics_title;
}
bool IsBookmarkEntityReuploadNeeded(
const syncer::EntityData& remote_entity_data) {
DCHECK(remote_entity_data.server_defined_unique_tag.empty());
// Do not initiate a reupload for a remote deletion.
if (remote_entity_data.is_deleted()) {
return false;
}
DCHECK(remote_entity_data.specifics.has_bookmark());
if (!remote_entity_data
.is_bookmark_unique_position_in_specifics_preprocessed) {
return false;
}
return base::FeatureList::IsEnabled(switches::kSyncReuploadBookmarks);
}
sync_pb::EntitySpecifics CreateSpecificsFromBookmarkNode(
const bookmarks::BookmarkNode* node,
bookmarks::BookmarkModel* model,
const sync_pb::UniquePosition& unique_position,
bool force_favicon_load) {
sync_pb::EntitySpecifics specifics;
sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
bm_specifics->set_type(GetProtoTypeFromBookmarkNode(node));
if (!node->is_folder()) {
bm_specifics->set_url(node->url().spec());
}
DCHECK(node->guid().is_valid()) << "Actual: " << node->guid();
bm_specifics->set_guid(node->guid().AsLowercaseString());
DCHECK(node->parent()->guid().is_valid())
<< "Actual: " << node->parent()->guid();
bm_specifics->set_parent_guid(node->parent()->guid().AsLowercaseString());
const std::string node_title = base::UTF16ToUTF8(node->GetTitle());
bm_specifics->set_legacy_canonicalized_title(
FullTitleToLegacyCanonicalizedTitle(node_title));
bm_specifics->set_full_title(node_title);
bm_specifics->set_creation_time_us(
node->date_added().ToDeltaSinceWindowsEpoch().InMicroseconds());
*bm_specifics->mutable_unique_position() = unique_position;
if (node->GetMetaInfoMap()) {
UpdateBookmarkSpecificsMetaInfo(node->GetMetaInfoMap(), bm_specifics);
}
if (!force_favicon_load && !node->is_favicon_loaded()) {
return specifics;
}
// Encodes a bookmark's favicon into raw PNG data.
scoped_refptr<base::RefCountedMemory> favicon_bytes(nullptr);
const gfx::Image& favicon = model->GetFavicon(node);
// Check for empty images. This can happen if the favicon is still being
// loaded.
if (!favicon.IsEmpty()) {
// Re-encode the BookmarkNode's favicon as a PNG.
favicon_bytes = favicon.As1xPNGBytes();
}
if (favicon_bytes.get() && favicon_bytes->size() != 0) {
bm_specifics->set_favicon(favicon_bytes->front(), favicon_bytes->size());
// Avoid sync-ing favicon URLs that are unreasonably large, as determined by
// |kMaxFaviconUrlSize|. Most notably, URLs prefixed with the data: scheme
// to embed the content of the image itself in the URL may be arbitrarily
// large and run into the server-side enforced limit per sync entity.
if (node->icon_url() &&
(node->icon_url()->spec().size() <= kMaxFaviconUrlSize ||
!base::FeatureList::IsEnabled(
switches::kSyncOmitLargeBookmarkFaviconUrl))) {
bm_specifics->set_icon_url(node->icon_url()->spec());
} else {
bm_specifics->set_icon_url(std::string());
}
}
return specifics;
}
const bookmarks::BookmarkNode* CreateBookmarkNodeFromSpecifics(
const sync_pb::BookmarkSpecifics& specifics,
const bookmarks::BookmarkNode* parent,
size_t index,
bookmarks::BookmarkModel* model,
favicon::FaviconService* favicon_service) {
DCHECK(parent);
DCHECK(model);
DCHECK(favicon_service);
DCHECK(IsValidBookmarkSpecifics(specifics));
const base::GUID guid = base::GUID::ParseLowercase(specifics.guid());
DCHECK(guid.is_valid());
const base::GUID parent_guid =
base::GUID::ParseLowercase(specifics.parent_guid());
DCHECK(parent_guid.is_valid());
DCHECK_EQ(parent_guid, parent->guid());
bookmarks::BookmarkNode::MetaInfoMap metainfo =
GetBookmarkMetaInfo(specifics);
const int64_t creation_time_us = specifics.creation_time_us();
const base::Time creation_time = base::Time::FromDeltaSinceWindowsEpoch(
// Use FromDeltaSinceWindowsEpoch because creation_time_us has
// always used the Windows epoch.
base::Microseconds(creation_time_us));
switch (specifics.type()) {
case sync_pb::BookmarkSpecifics::UNSPECIFIED:
NOTREACHED();
break;
case sync_pb::BookmarkSpecifics::URL: {
const bookmarks::BookmarkNode* node =
model->AddURL(parent, index, NodeTitleFromSpecifics(specifics),
GURL(specifics.url()), &metainfo, creation_time, guid);
SetBookmarkFaviconFromSpecifics(specifics, node, favicon_service);
return node;
}
case sync_pb::BookmarkSpecifics::FOLDER:
return model->AddFolder(parent, index, NodeTitleFromSpecifics(specifics),
&metainfo, creation_time, guid);
}
NOTREACHED();
return nullptr;
}
void UpdateBookmarkNodeFromSpecifics(
const sync_pb::BookmarkSpecifics& specifics,
const bookmarks::BookmarkNode* node,
bookmarks::BookmarkModel* model,
favicon::FaviconService* favicon_service) {
DCHECK(node);
DCHECK(model);
DCHECK(favicon_service);
// We shouldn't try to update the properties of the BookmarkNode before
// resolving any conflict in GUID. Either GUIDs are the same, or the GUID in
// specifics is invalid, and hence we can ignore it.
base::GUID guid = base::GUID::ParseLowercase(specifics.guid());
DCHECK(!guid.is_valid() || guid == node->guid());
model->SetTitle(node, NodeTitleFromSpecifics(specifics));
model->SetNodeMetaInfoMap(node, GetBookmarkMetaInfo(specifics));
if (!node->is_folder()) {
model->SetURL(node, GURL(specifics.url()));
SetBookmarkFaviconFromSpecifics(specifics, node, favicon_service);
}
}
sync_pb::BookmarkSpecifics::Type GetProtoTypeFromBookmarkNode(
const bookmarks::BookmarkNode* node) {
DCHECK(node);
switch (node->type()) {
case bookmarks::BookmarkNode::URL:
DCHECK(!node->is_folder());
return sync_pb::BookmarkSpecifics::URL;
case bookmarks::BookmarkNode::FOLDER:
case bookmarks::BookmarkNode::BOOKMARK_BAR:
case bookmarks::BookmarkNode::OTHER_NODE:
case bookmarks::BookmarkNode::MOBILE:
DCHECK(node->is_folder());
return sync_pb::BookmarkSpecifics::FOLDER;
}
}
const bookmarks::BookmarkNode* ReplaceBookmarkNodeGUID(
const bookmarks::BookmarkNode* node,
const base::GUID& guid,
bookmarks::BookmarkModel* model) {
DCHECK(guid.is_valid());
if (node->guid() == guid) {
// Nothing to do.
return node;
}
const bookmarks::BookmarkNode* new_node = nullptr;
if (node->is_folder()) {
new_node = model->AddFolder(
node->parent(), node->parent()->GetIndexOf(node), node->GetTitle(),
node->GetMetaInfoMap(), node->date_added(), guid);
MoveAllChildren(model, node, new_node);
} else {
new_node = model->AddURL(node->parent(), node->parent()->GetIndexOf(node),
node->GetTitle(), node->url(),
node->GetMetaInfoMap(), node->date_added(), guid);
}
model->Remove(node);
return new_node;
}
bool IsValidBookmarkSpecifics(const sync_pb::BookmarkSpecifics& specifics) {
bool is_valid = true;
if (specifics.ByteSize() == 0) {
DLOG(ERROR) << "Invalid bookmark: empty specifics.";
LogInvalidSpecifics(InvalidBookmarkSpecificsError::kEmptySpecifics);
is_valid = false;
}
const base::GUID guid = base::GUID::ParseLowercase(specifics.guid());
if (!guid.is_valid()) {
DLOG(ERROR) << "Invalid bookmark: invalid GUID in specifics.";
LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidGUID);
is_valid = false;
} else if (guid.AsLowercaseString() ==
bookmarks::BookmarkNode::kBannedGuidDueToPastSyncBug) {
DLOG(ERROR) << "Invalid bookmark: banned GUID in specifics.";
LogInvalidSpecifics(InvalidBookmarkSpecificsError::kBannedGUID);
is_valid = false;
}
const base::GUID parent_guid =
base::GUID::ParseLowercase(specifics.parent_guid());
if (!parent_guid.is_valid()) {
DLOG(ERROR) << "Invalid bookmark: invalid parent GUID in specifics.";
LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidParentGUID);
is_valid = false;
}
switch (specifics.type()) {
case sync_pb::BookmarkSpecifics::UNSPECIFIED:
// Note that old data doesn't run into this because ModelTypeWorker takes
// care of backfilling the field.
DLOG(ERROR) << "Invalid bookmark: invalid type in specifics.";
is_valid = false;
break;
case sync_pb::BookmarkSpecifics::URL:
if (!GURL(specifics.url()).is_valid()) {
DLOG(ERROR) << "Invalid bookmark: invalid url in the specifics.";
LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidURL);
is_valid = false;
}
if (specifics.favicon().empty() && !specifics.icon_url().empty()) {
DLOG(ERROR) << "Invalid bookmark: specifics cannot have an icon_url "
"without having a favicon.";
LogInvalidSpecifics(
InvalidBookmarkSpecificsError::kIconURLWithoutFavicon);
is_valid = false;
}
if (!specifics.icon_url().empty() &&
!GURL(specifics.icon_url()).is_valid()) {
DLOG(ERROR) << "Invalid bookmark: invalid icon_url in specifics.";
LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidIconURL);
is_valid = false;
}
break;
case sync_pb::BookmarkSpecifics::FOLDER:
break;
}
if (!syncer::UniquePosition::FromProto(specifics.unique_position())
.IsValid()) {
// Ignore updates with invalid positions.
DLOG(ERROR) << "Invalid bookmark: invalid unique position.";
LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidUniquePosition);
is_valid = false;
}
// Verify all keys in meta_info are unique.
std::unordered_set<base::StringPiece, base::StringPieceHash> keys;
for (const sync_pb::MetaInfo& meta_info : specifics.meta_info()) {
if (!keys.insert(meta_info.key()).second) {
DLOG(ERROR) << "Invalid bookmark: keys in meta_info aren't unique.";
LogInvalidSpecifics(
InvalidBookmarkSpecificsError::kNonUniqueMetaInfoKeys);
is_valid = false;
}
}
return is_valid;
}
base::GUID InferGuidFromLegacyOriginatorId(
const std::string& originator_cache_guid,
const std::string& originator_client_item_id) {
// Bookmarks created around 2016, between [M44..M52) use an uppercase GUID
// as originator client item ID, so it requires case-insensitive parsing.
base::GUID guid = base::GUID::ParseCaseInsensitive(originator_client_item_id);
if (guid.is_valid()) {
return guid;
}
return base::GUID::ParseLowercase(InferGuidForLegacyBookmark(
originator_cache_guid, originator_client_item_id));
}
bool HasExpectedBookmarkGuid(const sync_pb::BookmarkSpecifics& specifics,
const syncer::ClientTagHash& client_tag_hash,
const std::string& originator_cache_guid,
const std::string& originator_client_item_id) {
DCHECK(base::GUID::ParseLowercase(specifics.guid()).is_valid());
if (!client_tag_hash.value().empty()) {
return syncer::ClientTagHash::FromUnhashed(
syncer::BOOKMARKS, specifics.guid()) == client_tag_hash;
}
// Guard against returning true for cases where the GUID cannot be inferred.
if (originator_cache_guid.empty() && originator_client_item_id.empty()) {
return false;
}
return base::GUID::ParseLowercase(specifics.guid()) ==
InferGuidFromLegacyOriginatorId(originator_cache_guid,
originator_client_item_id);
}
} // namespace sync_bookmarks