bookmark_specifics_conversions.cc 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. // Copyright 2018 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/sync_bookmarks/bookmark_specifics_conversions.h"
  5. #include <string>
  6. #include <unordered_set>
  7. #include <utility>
  8. #include <vector>
  9. #include "base/containers/contains.h"
  10. #include "base/containers/span.h"
  11. #include "base/feature_list.h"
  12. #include "base/guid.h"
  13. #include "base/hash/sha1.h"
  14. #include "base/logging.h"
  15. #include "base/metrics/histogram_functions.h"
  16. #include "base/strings/strcat.h"
  17. #include "base/strings/string_util.h"
  18. #include "base/strings/stringprintf.h"
  19. #include "base/strings/utf_string_conversions.h"
  20. #include "components/bookmarks/browser/bookmark_model.h"
  21. #include "components/bookmarks/browser/bookmark_node.h"
  22. #include "components/favicon/core/favicon_service.h"
  23. #include "components/sync/base/unique_position.h"
  24. #include "components/sync/protocol/bookmark_specifics.pb.h"
  25. #include "components/sync/protocol/entity_data.h"
  26. #include "components/sync/protocol/entity_specifics.pb.h"
  27. #include "components/sync_bookmarks/switches.h"
  28. #include "ui/gfx/favicon_size.h"
  29. #include "url/gurl.h"
  30. namespace sync_bookmarks {
  31. namespace {
  32. // Maximum number of bytes to allow in a legacy canonicalized title (must match
  33. // sync's internal limits; see write_node.cc).
  34. const int kLegacyCanonicalizedTitleLimitBytes = 255;
  35. // The list of bookmark titles which are reserved for use by the server.
  36. const char* const kForbiddenTitles[] = {"", ".", ".."};
  37. // Maximum size for the favicon URL. This limit should be very generous in most
  38. // cases, the notable exception being data: URLs that encode the content of
  39. // the favicon itself in the URL, and may be arbitrarily large.
  40. const int kMaxFaviconUrlSize = 4096;
  41. // Used in metrics: "Sync.InvalidBookmarkSpecifics". These values are
  42. // persisted to logs. Entries should not be renumbered and numeric values
  43. // should never be reused.
  44. enum class InvalidBookmarkSpecificsError {
  45. kEmptySpecifics = 0,
  46. kInvalidURL = 1,
  47. kIconURLWithoutFavicon = 2,
  48. kInvalidIconURL = 3,
  49. kNonUniqueMetaInfoKeys = 4,
  50. kInvalidGUID = 5,
  51. kInvalidParentGUID = 6,
  52. kInvalidUniquePosition = 7,
  53. kBannedGUID = 8,
  54. kMaxValue = kBannedGUID,
  55. };
  56. void LogInvalidSpecifics(InvalidBookmarkSpecificsError error) {
  57. base::UmaHistogramEnumeration("Sync.InvalidBookmarkSpecifics", error);
  58. }
  59. void LogFaviconContainedInSpecifics(bool contains_favicon) {
  60. base::UmaHistogramBoolean(
  61. "Sync.BookmarkSpecificsExcludingFoldersContainFavicon", contains_favicon);
  62. }
  63. void UpdateBookmarkSpecificsMetaInfo(
  64. const bookmarks::BookmarkNode::MetaInfoMap* metainfo_map,
  65. sync_pb::BookmarkSpecifics* bm_specifics) {
  66. for (const auto& [key, value] : *metainfo_map) {
  67. sync_pb::MetaInfo* meta_info = bm_specifics->add_meta_info();
  68. meta_info->set_key(key);
  69. meta_info->set_value(value);
  70. }
  71. }
  72. // Metainfo entries in |specifics| must have unique keys.
  73. bookmarks::BookmarkNode::MetaInfoMap GetBookmarkMetaInfo(
  74. const sync_pb::BookmarkSpecifics& specifics) {
  75. bookmarks::BookmarkNode::MetaInfoMap meta_info_map;
  76. for (const sync_pb::MetaInfo& meta_info : specifics.meta_info()) {
  77. meta_info_map[meta_info.key()] = meta_info.value();
  78. }
  79. DCHECK_EQ(static_cast<size_t>(specifics.meta_info_size()),
  80. meta_info_map.size());
  81. return meta_info_map;
  82. }
  83. // Sets the favicon of the given bookmark node from the given specifics.
  84. void SetBookmarkFaviconFromSpecifics(
  85. const sync_pb::BookmarkSpecifics& specifics,
  86. const bookmarks::BookmarkNode* bookmark_node,
  87. favicon::FaviconService* favicon_service) {
  88. DCHECK(bookmark_node);
  89. DCHECK(!bookmark_node->is_folder());
  90. DCHECK(favicon_service);
  91. favicon_service->AddPageNoVisitForBookmark(bookmark_node->url(),
  92. bookmark_node->GetTitle());
  93. const std::string& icon_bytes_str = specifics.favicon();
  94. scoped_refptr<base::RefCountedString> icon_bytes(
  95. new base::RefCountedString());
  96. icon_bytes->data().assign(icon_bytes_str);
  97. GURL icon_url(specifics.icon_url());
  98. if (icon_bytes->size() == 0 && icon_url.is_empty()) {
  99. // Empty icon URL and no bitmap data means no icon mapping.
  100. LogFaviconContainedInSpecifics(false);
  101. favicon_service->DeleteFaviconMappings({bookmark_node->url()},
  102. favicon_base::IconType::kFavicon);
  103. return;
  104. }
  105. LogFaviconContainedInSpecifics(true);
  106. if (icon_url.is_empty()) {
  107. // See documentation in BookmarkSpecifics to understand the (rare) scenarios
  108. // where |icon_url| may be missing despite a favicon image itself (proto
  109. // field |favicon|) being set. In this case, use the page URL as a fake icon
  110. // URL as it is guaranteed to be unique.
  111. icon_url = GURL(bookmark_node->url());
  112. }
  113. // The client may have cached the favicon at 2x. Use MergeFavicon() as not to
  114. // overwrite the cached 2x favicon bitmap. Sync favicons are always
  115. // gfx::kFaviconSize in width and height. Store the favicon into history
  116. // as such.
  117. gfx::Size pixel_size(gfx::kFaviconSize, gfx::kFaviconSize);
  118. favicon_service->MergeFavicon(bookmark_node->url(), icon_url,
  119. favicon_base::IconType::kFavicon, icon_bytes,
  120. pixel_size);
  121. }
  122. // This is an exact copy of the same code in bookmark_update_preprocessing.cc.
  123. std::string ComputeGuidFromBytes(base::span<const uint8_t> bytes) {
  124. DCHECK_GE(bytes.size(), 16U);
  125. // This implementation is based on the equivalent logic in base/guid.cc.
  126. // Set the GUID to version 4 as described in RFC 4122, section 4.4.
  127. // The format of GUID version 4 must be xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx,
  128. // where y is one of [8, 9, A, B].
  129. // Clear the version bits and set the version to 4:
  130. const uint8_t byte6 = (bytes[6] & 0x0fU) | 0xf0U;
  131. // Set the two most significant bits (bits 6 and 7) of the
  132. // clock_seq_hi_and_reserved to zero and one, respectively:
  133. const uint8_t byte8 = (bytes[8] & 0x3fU) | 0x80U;
  134. return base::StringPrintf(
  135. "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
  136. bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], byte6,
  137. bytes[7], byte8, bytes[9], bytes[10], bytes[11], bytes[12], bytes[13],
  138. bytes[14], bytes[15]);
  139. }
  140. // This is an exact copy of the same code in bookmark_update_preprocessing.cc,
  141. // which could be removed if eventually client tags are adapted/inferred in
  142. // ModelTypeWorker. The reason why this is non-trivial today is that some users
  143. // are known to contain corrupt data in the sense that several different
  144. // entities (identified by their server-provided ID) use the same client tag
  145. // (and GUID). Currently BookmarkModelMerger has logic to prefer folders over
  146. // regular URLs and reassign GUIDs.
  147. std::string InferGuidForLegacyBookmark(
  148. const std::string& originator_cache_guid,
  149. const std::string& originator_client_item_id) {
  150. DCHECK(
  151. !base::GUID::ParseCaseInsensitive(originator_client_item_id).is_valid());
  152. const std::string unique_tag =
  153. base::StrCat({originator_cache_guid, originator_client_item_id});
  154. const base::SHA1Digest hash =
  155. base::SHA1HashSpan(base::as_bytes(base::make_span(unique_tag)));
  156. static_assert(base::kSHA1Length >= 16, "16 bytes needed to infer GUID");
  157. const std::string guid = ComputeGuidFromBytes(base::make_span(hash));
  158. DCHECK(base::GUID::ParseLowercase(guid).is_valid());
  159. return guid;
  160. }
  161. bool IsForbiddenTitleWithMaybeTrailingSpaces(const std::string& title) {
  162. return base::Contains(
  163. kForbiddenTitles,
  164. base::TrimWhitespaceASCII(title, base::TrimPositions::TRIM_TRAILING));
  165. }
  166. std::u16string NodeTitleFromSpecifics(
  167. const sync_pb::BookmarkSpecifics& specifics) {
  168. if (specifics.has_full_title()) {
  169. return base::UTF8ToUTF16(specifics.full_title());
  170. }
  171. std::string node_title = specifics.legacy_canonicalized_title();
  172. if (base::EndsWith(node_title, " ") &&
  173. IsForbiddenTitleWithMaybeTrailingSpaces(node_title)) {
  174. // Legacy clients added an extra space to the real title, so remove it here.
  175. // See also FullTitleToLegacyCanonicalizedTitle().
  176. node_title.pop_back();
  177. }
  178. return base::UTF8ToUTF16(node_title);
  179. }
  180. void MoveAllChildren(bookmarks::BookmarkModel* model,
  181. const bookmarks::BookmarkNode* old_parent,
  182. const bookmarks::BookmarkNode* new_parent) {
  183. DCHECK(old_parent && old_parent->is_folder());
  184. DCHECK(new_parent && new_parent->is_folder());
  185. DCHECK(old_parent != new_parent);
  186. DCHECK(new_parent->children().empty());
  187. if (old_parent->children().empty()) {
  188. return;
  189. }
  190. // This code relies on the underlying type to store children in the
  191. // BookmarkModel which is vector. It moves the last child from |old_parent| to
  192. // the end of |new_parent| step by step (which reverses the order of
  193. // children). After that all children must be reordered to keep the original
  194. // order in |new_parent|.
  195. // This algorithm is used because of performance reasons.
  196. std::vector<const bookmarks::BookmarkNode*> children_order(
  197. old_parent->children().size(), nullptr);
  198. for (size_t i = old_parent->children().size(); i > 0; --i) {
  199. const size_t old_index = i - 1;
  200. const bookmarks::BookmarkNode* child_to_move =
  201. old_parent->children()[old_index].get();
  202. children_order[old_index] = child_to_move;
  203. model->Move(child_to_move, new_parent, new_parent->children().size());
  204. }
  205. model->ReorderChildren(new_parent, children_order);
  206. }
  207. } // namespace
  208. std::string FullTitleToLegacyCanonicalizedTitle(const std::string& node_title) {
  209. // Add an extra space for backward compatibility with legacy clients.
  210. std::string specifics_title =
  211. IsForbiddenTitleWithMaybeTrailingSpaces(node_title) ? node_title + " "
  212. : node_title;
  213. base::TruncateUTF8ToByteSize(
  214. specifics_title, kLegacyCanonicalizedTitleLimitBytes, &specifics_title);
  215. return specifics_title;
  216. }
  217. bool IsBookmarkEntityReuploadNeeded(
  218. const syncer::EntityData& remote_entity_data) {
  219. DCHECK(remote_entity_data.server_defined_unique_tag.empty());
  220. // Do not initiate a reupload for a remote deletion.
  221. if (remote_entity_data.is_deleted()) {
  222. return false;
  223. }
  224. DCHECK(remote_entity_data.specifics.has_bookmark());
  225. if (!remote_entity_data
  226. .is_bookmark_unique_position_in_specifics_preprocessed) {
  227. return false;
  228. }
  229. return base::FeatureList::IsEnabled(switches::kSyncReuploadBookmarks);
  230. }
  231. sync_pb::EntitySpecifics CreateSpecificsFromBookmarkNode(
  232. const bookmarks::BookmarkNode* node,
  233. bookmarks::BookmarkModel* model,
  234. const sync_pb::UniquePosition& unique_position,
  235. bool force_favicon_load) {
  236. sync_pb::EntitySpecifics specifics;
  237. sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
  238. bm_specifics->set_type(GetProtoTypeFromBookmarkNode(node));
  239. if (!node->is_folder()) {
  240. bm_specifics->set_url(node->url().spec());
  241. }
  242. DCHECK(node->guid().is_valid()) << "Actual: " << node->guid();
  243. bm_specifics->set_guid(node->guid().AsLowercaseString());
  244. DCHECK(node->parent()->guid().is_valid())
  245. << "Actual: " << node->parent()->guid();
  246. bm_specifics->set_parent_guid(node->parent()->guid().AsLowercaseString());
  247. const std::string node_title = base::UTF16ToUTF8(node->GetTitle());
  248. bm_specifics->set_legacy_canonicalized_title(
  249. FullTitleToLegacyCanonicalizedTitle(node_title));
  250. bm_specifics->set_full_title(node_title);
  251. bm_specifics->set_creation_time_us(
  252. node->date_added().ToDeltaSinceWindowsEpoch().InMicroseconds());
  253. *bm_specifics->mutable_unique_position() = unique_position;
  254. if (!node->is_folder() && node->date_last_used() != base::Time()) {
  255. bm_specifics->set_last_used_time_us(
  256. node->date_last_used().ToDeltaSinceWindowsEpoch().InMicroseconds());
  257. }
  258. if (node->GetMetaInfoMap()) {
  259. UpdateBookmarkSpecificsMetaInfo(node->GetMetaInfoMap(), bm_specifics);
  260. }
  261. if (!force_favicon_load && !node->is_favicon_loaded()) {
  262. return specifics;
  263. }
  264. // Encodes a bookmark's favicon into raw PNG data.
  265. scoped_refptr<base::RefCountedMemory> favicon_bytes(nullptr);
  266. const gfx::Image& favicon = model->GetFavicon(node);
  267. // Check for empty images. This can happen if the favicon is still being
  268. // loaded.
  269. if (!favicon.IsEmpty()) {
  270. // Re-encode the BookmarkNode's favicon as a PNG.
  271. favicon_bytes = favicon.As1xPNGBytes();
  272. }
  273. if (favicon_bytes.get() && favicon_bytes->size() != 0) {
  274. bm_specifics->set_favicon(favicon_bytes->front(), favicon_bytes->size());
  275. // Avoid sync-ing favicon URLs that are unreasonably large, as determined by
  276. // |kMaxFaviconUrlSize|. Most notably, URLs prefixed with the data: scheme
  277. // to embed the content of the image itself in the URL may be arbitrarily
  278. // large and run into the server-side enforced limit per sync entity.
  279. if (node->icon_url() &&
  280. (node->icon_url()->spec().size() <= kMaxFaviconUrlSize ||
  281. !base::FeatureList::IsEnabled(
  282. switches::kSyncOmitLargeBookmarkFaviconUrl))) {
  283. bm_specifics->set_icon_url(node->icon_url()->spec());
  284. } else {
  285. bm_specifics->set_icon_url(std::string());
  286. }
  287. }
  288. return specifics;
  289. }
  290. const bookmarks::BookmarkNode* CreateBookmarkNodeFromSpecifics(
  291. const sync_pb::BookmarkSpecifics& specifics,
  292. const bookmarks::BookmarkNode* parent,
  293. size_t index,
  294. bookmarks::BookmarkModel* model,
  295. favicon::FaviconService* favicon_service) {
  296. DCHECK(parent);
  297. DCHECK(model);
  298. DCHECK(favicon_service);
  299. DCHECK(IsValidBookmarkSpecifics(specifics));
  300. const base::GUID guid = base::GUID::ParseLowercase(specifics.guid());
  301. DCHECK(guid.is_valid());
  302. const base::GUID parent_guid =
  303. base::GUID::ParseLowercase(specifics.parent_guid());
  304. DCHECK(parent_guid.is_valid());
  305. DCHECK_EQ(parent_guid, parent->guid());
  306. bookmarks::BookmarkNode::MetaInfoMap metainfo =
  307. GetBookmarkMetaInfo(specifics);
  308. const int64_t creation_time_us = specifics.creation_time_us();
  309. const base::Time creation_time = base::Time::FromDeltaSinceWindowsEpoch(
  310. // Use FromDeltaSinceWindowsEpoch because creation_time_us has
  311. // always used the Windows epoch.
  312. base::Microseconds(creation_time_us));
  313. switch (specifics.type()) {
  314. case sync_pb::BookmarkSpecifics::UNSPECIFIED:
  315. NOTREACHED();
  316. break;
  317. case sync_pb::BookmarkSpecifics::URL: {
  318. const bookmarks::BookmarkNode* node =
  319. model->AddURL(parent, index, NodeTitleFromSpecifics(specifics),
  320. GURL(specifics.url()), &metainfo, creation_time, guid);
  321. if (specifics.has_last_used_time_us()) {
  322. const int64_t last_used_time_us = specifics.last_used_time_us();
  323. const base::Time last_used_time =
  324. base::Time::FromDeltaSinceWindowsEpoch(
  325. // Use FromDeltaSinceWindowsEpoch because last_used_time_us has
  326. // always used the Windows epoch.
  327. base::Microseconds(last_used_time_us));
  328. model->UpdateLastUsedTime(node, last_used_time);
  329. }
  330. SetBookmarkFaviconFromSpecifics(specifics, node, favicon_service);
  331. return node;
  332. }
  333. case sync_pb::BookmarkSpecifics::FOLDER:
  334. return model->AddFolder(parent, index, NodeTitleFromSpecifics(specifics),
  335. &metainfo, creation_time, guid);
  336. }
  337. NOTREACHED();
  338. return nullptr;
  339. }
  340. void UpdateBookmarkNodeFromSpecifics(
  341. const sync_pb::BookmarkSpecifics& specifics,
  342. const bookmarks::BookmarkNode* node,
  343. bookmarks::BookmarkModel* model,
  344. favicon::FaviconService* favicon_service) {
  345. DCHECK(node);
  346. DCHECK(model);
  347. DCHECK(favicon_service);
  348. // We shouldn't try to update the properties of the BookmarkNode before
  349. // resolving any conflict in GUID. Either GUIDs are the same, or the GUID in
  350. // specifics is invalid, and hence we can ignore it.
  351. base::GUID guid = base::GUID::ParseLowercase(specifics.guid());
  352. DCHECK(!guid.is_valid() || guid == node->guid());
  353. model->SetTitle(node, NodeTitleFromSpecifics(specifics));
  354. model->SetNodeMetaInfoMap(node, GetBookmarkMetaInfo(specifics));
  355. if (!node->is_folder()) {
  356. model->SetURL(node, GURL(specifics.url()));
  357. SetBookmarkFaviconFromSpecifics(specifics, node, favicon_service);
  358. if (specifics.has_last_used_time_us()) {
  359. const int64_t last_used_time_us = specifics.last_used_time_us();
  360. const base::Time last_used_time = base::Time::FromDeltaSinceWindowsEpoch(
  361. // Use FromDeltaSinceWindowsEpoch because last_used_time_us has
  362. // always used the Windows epoch.
  363. base::Microseconds(last_used_time_us));
  364. model->UpdateLastUsedTime(node, last_used_time);
  365. }
  366. }
  367. }
  368. sync_pb::BookmarkSpecifics::Type GetProtoTypeFromBookmarkNode(
  369. const bookmarks::BookmarkNode* node) {
  370. DCHECK(node);
  371. switch (node->type()) {
  372. case bookmarks::BookmarkNode::URL:
  373. DCHECK(!node->is_folder());
  374. return sync_pb::BookmarkSpecifics::URL;
  375. case bookmarks::BookmarkNode::FOLDER:
  376. case bookmarks::BookmarkNode::BOOKMARK_BAR:
  377. case bookmarks::BookmarkNode::OTHER_NODE:
  378. case bookmarks::BookmarkNode::MOBILE:
  379. DCHECK(node->is_folder());
  380. return sync_pb::BookmarkSpecifics::FOLDER;
  381. }
  382. }
  383. const bookmarks::BookmarkNode* ReplaceBookmarkNodeGUID(
  384. const bookmarks::BookmarkNode* node,
  385. const base::GUID& guid,
  386. bookmarks::BookmarkModel* model) {
  387. DCHECK(guid.is_valid());
  388. if (node->guid() == guid) {
  389. // Nothing to do.
  390. return node;
  391. }
  392. const bookmarks::BookmarkNode* new_node = nullptr;
  393. if (node->is_folder()) {
  394. new_node = model->AddFolder(
  395. node->parent(), node->parent()->GetIndexOf(node).value(),
  396. node->GetTitle(), node->GetMetaInfoMap(), node->date_added(), guid);
  397. MoveAllChildren(model, node, new_node);
  398. } else {
  399. new_node =
  400. model->AddURL(node->parent(), node->parent()->GetIndexOf(node).value(),
  401. node->GetTitle(), node->url(), node->GetMetaInfoMap(),
  402. node->date_added(), guid);
  403. }
  404. model->Remove(node);
  405. return new_node;
  406. }
  407. bool IsValidBookmarkSpecifics(const sync_pb::BookmarkSpecifics& specifics) {
  408. bool is_valid = true;
  409. if (specifics.ByteSize() == 0) {
  410. DLOG(ERROR) << "Invalid bookmark: empty specifics.";
  411. LogInvalidSpecifics(InvalidBookmarkSpecificsError::kEmptySpecifics);
  412. is_valid = false;
  413. }
  414. const base::GUID guid = base::GUID::ParseLowercase(specifics.guid());
  415. if (!guid.is_valid()) {
  416. DLOG(ERROR) << "Invalid bookmark: invalid GUID in specifics.";
  417. LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidGUID);
  418. is_valid = false;
  419. } else if (guid.AsLowercaseString() ==
  420. bookmarks::BookmarkNode::kBannedGuidDueToPastSyncBug) {
  421. DLOG(ERROR) << "Invalid bookmark: banned GUID in specifics.";
  422. LogInvalidSpecifics(InvalidBookmarkSpecificsError::kBannedGUID);
  423. is_valid = false;
  424. }
  425. const base::GUID parent_guid =
  426. base::GUID::ParseLowercase(specifics.parent_guid());
  427. if (!parent_guid.is_valid()) {
  428. DLOG(ERROR) << "Invalid bookmark: invalid parent GUID in specifics.";
  429. LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidParentGUID);
  430. is_valid = false;
  431. }
  432. switch (specifics.type()) {
  433. case sync_pb::BookmarkSpecifics::UNSPECIFIED:
  434. // Note that old data doesn't run into this because ModelTypeWorker takes
  435. // care of backfilling the field.
  436. DLOG(ERROR) << "Invalid bookmark: invalid type in specifics.";
  437. is_valid = false;
  438. break;
  439. case sync_pb::BookmarkSpecifics::URL:
  440. if (!GURL(specifics.url()).is_valid()) {
  441. DLOG(ERROR) << "Invalid bookmark: invalid url in the specifics.";
  442. LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidURL);
  443. is_valid = false;
  444. }
  445. if (specifics.favicon().empty() && !specifics.icon_url().empty()) {
  446. DLOG(ERROR) << "Invalid bookmark: specifics cannot have an icon_url "
  447. "without having a favicon.";
  448. LogInvalidSpecifics(
  449. InvalidBookmarkSpecificsError::kIconURLWithoutFavicon);
  450. is_valid = false;
  451. }
  452. if (!specifics.icon_url().empty() &&
  453. !GURL(specifics.icon_url()).is_valid()) {
  454. DLOG(ERROR) << "Invalid bookmark: invalid icon_url in specifics.";
  455. LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidIconURL);
  456. is_valid = false;
  457. }
  458. break;
  459. case sync_pb::BookmarkSpecifics::FOLDER:
  460. break;
  461. }
  462. if (!syncer::UniquePosition::FromProto(specifics.unique_position())
  463. .IsValid()) {
  464. // Ignore updates with invalid positions.
  465. DLOG(ERROR) << "Invalid bookmark: invalid unique position.";
  466. LogInvalidSpecifics(InvalidBookmarkSpecificsError::kInvalidUniquePosition);
  467. is_valid = false;
  468. }
  469. // Verify all keys in meta_info are unique.
  470. std::unordered_set<base::StringPiece, base::StringPieceHash> keys;
  471. for (const sync_pb::MetaInfo& meta_info : specifics.meta_info()) {
  472. if (!keys.insert(meta_info.key()).second) {
  473. DLOG(ERROR) << "Invalid bookmark: keys in meta_info aren't unique.";
  474. LogInvalidSpecifics(
  475. InvalidBookmarkSpecificsError::kNonUniqueMetaInfoKeys);
  476. is_valid = false;
  477. }
  478. }
  479. return is_valid;
  480. }
  481. base::GUID InferGuidFromLegacyOriginatorId(
  482. const std::string& originator_cache_guid,
  483. const std::string& originator_client_item_id) {
  484. // Bookmarks created around 2016, between [M44..M52) use an uppercase GUID
  485. // as originator client item ID, so it requires case-insensitive parsing.
  486. base::GUID guid = base::GUID::ParseCaseInsensitive(originator_client_item_id);
  487. if (guid.is_valid()) {
  488. return guid;
  489. }
  490. return base::GUID::ParseLowercase(InferGuidForLegacyBookmark(
  491. originator_cache_guid, originator_client_item_id));
  492. }
  493. bool HasExpectedBookmarkGuid(const sync_pb::BookmarkSpecifics& specifics,
  494. const syncer::ClientTagHash& client_tag_hash,
  495. const std::string& originator_cache_guid,
  496. const std::string& originator_client_item_id) {
  497. DCHECK(base::GUID::ParseLowercase(specifics.guid()).is_valid());
  498. if (!client_tag_hash.value().empty()) {
  499. return syncer::ClientTagHash::FromUnhashed(
  500. syncer::BOOKMARKS, specifics.guid()) == client_tag_hash;
  501. }
  502. // Guard against returning true for cases where the GUID cannot be inferred.
  503. if (originator_cache_guid.empty() && originator_client_item_id.empty()) {
  504. return false;
  505. }
  506. return base::GUID::ParseLowercase(specifics.guid()) ==
  507. InferGuidFromLegacyOriginatorId(originator_cache_guid,
  508. originator_client_item_id);
  509. }
  510. } // namespace sync_bookmarks