blob: 0795929407a226c923ea31716843c9deb3f7fc06 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/bookmarks/browser/bookmark_model.h"
#include <algorithm>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/i18n/string_compare.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/strings/string_util.h"
#include "base/task/thread_pool.h"
#include "base/uuid.h"
#include "components/bookmarks/browser/bookmark_load_details.h"
#include "components/bookmarks/browser/bookmark_model_observer.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_storage.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/bookmarks/browser/bookmark_uuids.h"
#include "components/bookmarks/browser/model_loader.h"
#include "components/bookmarks/browser/scoped_group_bookmark_actions.h"
#include "components/bookmarks/browser/titled_url_index.h"
#include "components/bookmarks/browser/titled_url_match.h"
#include "components/bookmarks/browser/typed_count_sorter.h"
#include "components/bookmarks/browser/url_and_title.h"
#include "components/bookmarks/browser/url_index.h"
#include "components/bookmarks/common/bookmark_constants.h"
#include "components/bookmarks/common/bookmark_features.h"
#include "components/bookmarks/common/bookmark_metrics.h"
#include "components/favicon_base/favicon_types.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/favicon_size.h"
using base::Time;
namespace bookmarks {
namespace {
bool AreFoldersForAccountStorageAllowed() {
return base::FeatureList::IsEnabled(
switches::kSyncEnableBookmarksInTransportMode);
}
// Helper to get a mutable bookmark node.
BookmarkNode* AsMutable(const BookmarkNode* node) {
return const_cast<BookmarkNode*>(node);
}
// Traverses ancestors to find a permanent node or null in the rare case where
// the node has no ancestor permanent node. This can happen if `node` is the
// root node or because `node` is in the process of being deleted (i.e. removed
// from the indices), typically as a result of feature code reacting to
// BookmarkModelObserver::BookmarkNodeRemoved().
const BookmarkNode* GetSelfOrAncestorPermanentNode(const BookmarkNode* node) {
CHECK(node);
while (node && !node->is_permanent_node()) {
node = node->parent();
}
return node;
}
// Gets the number of user-generated folders from `node` (inclusive) along the
// ancestor path till it hits a permanent node (which is not a user-generated
// folder).
int GetUserFolderDepth(const BookmarkNode* node) {
int result = 0;
if (!node) {
return result;
}
// Exit the loop if we have reached the root node, or a peremanent node.
// The root node is a non-permanent folder, but shouldn't be considered as
// user-generated.
while (node->parent() && !node->is_permanent_node()) {
if (node->is_folder()) {
result++;
}
node = node->parent();
}
return result;
}
// Comparator used when sorting permanent nodes. Nodes that are initially
// visible are sorted before nodes that are initially hidden.
class VisibilityComparator {
public:
explicit VisibilityComparator(BookmarkClient* client) : client_(client) {}
// Returns true if `n1` precedes `n2`.
bool operator()(const std::unique_ptr<BookmarkNode>& n1,
const std::unique_ptr<BookmarkNode>& n2) {
DCHECK(n1->is_permanent_node());
DCHECK(n2->is_permanent_node());
bool n1_visible = BookmarkPermanentNode::IsTypeVisibleWhenEmpty(n1->type());
bool n2_visible = BookmarkPermanentNode::IsTypeVisibleWhenEmpty(n2->type());
return n1_visible != n2_visible && n1_visible;
}
private:
raw_ptr<BookmarkClient> client_;
};
// Comparator used when sorting bookmarks. Folders are sorted first, then
// bookmarks.
class SortComparator {
public:
explicit SortComparator(icu::Collator* collator) : collator_(collator) {}
// Returns true if `n1` precedes `n2`.
bool operator()(const std::unique_ptr<BookmarkNode>& n1,
const std::unique_ptr<BookmarkNode>& n2) {
if (n1->type() == n2->type()) {
// Types are the same, compare the names.
if (!collator_) {
return n1->GetTitle() < n2->GetTitle();
}
return base::i18n::CompareString16WithCollator(
*collator_, n1->GetTitle(), n2->GetTitle()) == UCOL_LESS;
}
// Types differ, sort such that folders come first.
return n1->is_folder();
}
private:
raw_ptr<icu::Collator> collator_;
};
} // namespace
// BookmarkModel --------------------------------------------------------------
BookmarkModel::BookmarkModel(std::unique_ptr<BookmarkClient> client)
: owned_root_(std::make_unique<BookmarkNode>(
/*id=*/0,
base::Uuid::ParseLowercase(kRootNodeUuid),
GURL())),
root_(owned_root_.get()),
observers_(base::ObserverListPolicy::EXISTING_ONLY),
client_(std::move(client)) {
DCHECK(client_);
uuid_index_.emplace(NodeTypeForUuidLookup::kLocalOrSyncableNodes,
UuidIndex());
uuid_index_.emplace(NodeTypeForUuidLookup::kAccountNodes, UuidIndex());
client_->Init(this);
}
BookmarkModel::~BookmarkModel() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkModelBeingDeleted();
}
// The stores maintain a reference back to us. Destroy them early so that they
// don't try and invoke a method back on `this` again.
local_or_syncable_store_.reset();
account_store_.reset();
// `TitledUrlIndex` owns a `TypedCountSorter` that keeps a raw_ptr to the
// client. So titled_url_index_ must be reset first.
titled_url_index_.reset();
// ChromeBookmarkClient indirectly observes the model. The client should thus
// be reset before the observer list.
client_.reset();
// Set raw_ptr values to null to avoid danling pointer detection when UrlIndex
// is destroyed.
account_bookmark_bar_node_ = nullptr;
account_other_node_ = nullptr;
account_mobile_node_ = nullptr;
}
void BookmarkModel::Load(const base::FilePath& profile_path) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If the stores are non-null, it means Load was already invoked. Load should
// only be invoked once.
CHECK(!local_or_syncable_store_);
CHECK(!account_store_);
const base::FilePath local_or_syncable_file_path =
profile_path.Append(kLocalOrSyncableBookmarksFileName);
const base::FilePath account_file_path =
AreFoldersForAccountStorageAllowed()
? profile_path.Append(kAccountBookmarksFileName)
: base::FilePath();
local_or_syncable_store_ = std::make_unique<BookmarkStorage>(
this, BookmarkStorage::kSelectLocalOrSyncableNodes,
local_or_syncable_file_path);
if (!account_file_path.empty()) {
account_store_ = std::make_unique<BookmarkStorage>(
this, BookmarkStorage::kSelectAccountNodes, account_file_path);
}
// Creating ModelLoader schedules the load on a backend task runner.
model_loader_ = ModelLoader::Create(
local_or_syncable_file_path, account_file_path,
client_->GetLoadManagedNodeCallback(),
base::BindOnce(&BookmarkModel::DoneLoading, AsWeakPtr()));
}
scoped_refptr<ModelLoader> BookmarkModel::model_loader() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return model_loader_;
}
const BookmarkNode* BookmarkModel::account_bookmark_bar_node() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Must be null if the feature flag isn't enabled.
CHECK(!account_bookmark_bar_node_ || AreFoldersForAccountStorageAllowed());
return account_bookmark_bar_node_;
}
const BookmarkNode* BookmarkModel::account_other_node() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Must be null if the feature flag isn't enabled.
CHECK(!account_other_node_ || AreFoldersForAccountStorageAllowed());
return account_other_node_;
}
const BookmarkNode* BookmarkModel::account_mobile_node() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Must be null if the feature flag isn't enabled.
CHECK(!account_mobile_node_ || AreFoldersForAccountStorageAllowed());
return account_mobile_node_;
}
bool BookmarkModel::IsNodeVisible(const BookmarkNode& node) const {
if (!node.is_permanent_node()) {
return true;
}
if (!node.children().empty()) {
return true;
}
if (!BookmarkPermanentNode::IsTypeVisibleWhenEmpty(node.type())) {
return false;
}
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
if (IsLocalOnlyNode(node) && account_bookmark_bar_node() &&
!HasLocalOrSyncableBookmarks(this)) {
// Prune this local empty permanent node, since the user has account
// permanent folders.
return false;
}
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
return true;
}
bool BookmarkModel::IsLocalOnlyNode(const BookmarkNode& node) const {
if (is_root_node(&node)) {
// The semantics aren't clear for the root, but returning true seems most
// sensible as the root is a synthetic node that doesn't get uploaded to
// servers.
return true;
}
const BookmarkNode* ancestor_permanent_node =
GetSelfOrAncestorPermanentNode(&node);
if (!ancestor_permanent_node) {
// In rare cases, `node` may already be 'dettached' from the bookmark tree.
// This can happen for example if this function is exercised as a reaction
// to BookmarkModelObserver::BookmarkNodeRemoved(). In this case, the
// semantics of this function aren't clear, but following the same rationale
// as for the root node, discussed above, returning true seems most
// sensible.
return true;
}
if (client_->IsNodeManaged(ancestor_permanent_node)) {
// Managed nodes don't sync.
return true;
}
if (client_->IsSyncFeatureEnabledIncludingBookmarks()) {
// If sync-the-feature is on, including bookmarks, then there is no
// separation between local and account bookmarks, and all bookmarks are
// getting sync-ed to the server.
return false;
}
// If sync is off, the only remaining possibility to return false is if `node`
// is actually a descendant of an account permanent folder (if they exist).
return ancestor_permanent_node != account_bookmark_bar_node_ &&
ancestor_permanent_node != account_other_node_ &&
ancestor_permanent_node != account_mobile_node_;
}
void BookmarkModel::AddObserver(BookmarkModelObserver* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observers_.AddObserver(observer);
}
void BookmarkModel::RemoveObserver(BookmarkModelObserver* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
observers_.RemoveObserver(observer);
}
void BookmarkModel::BeginExtensiveChanges() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (++extensive_changes_ == 1) {
for (BookmarkModelObserver& observer : observers_) {
observer.ExtensiveBookmarkChangesBeginning();
}
}
}
void BookmarkModel::EndExtensiveChanges() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
--extensive_changes_;
DCHECK_GE(extensive_changes_, 0);
if (extensive_changes_ == 0) {
for (BookmarkModelObserver& observer : observers_) {
observer.ExtensiveBookmarkChangesEnded();
}
}
}
void BookmarkModel::BeginGroupedChanges() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (BookmarkModelObserver& observer : observers_) {
observer.GroupedBookmarkChangesBeginning();
}
}
void BookmarkModel::EndGroupedChanges() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (BookmarkModelObserver& observer : observers_) {
observer.GroupedBookmarkChangesEnded();
}
}
void BookmarkModel::Remove(const BookmarkNode* node,
metrics::BookmarkEditSource source,
const base::Location& location) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(loaded_);
DCHECK(node);
DCHECK(!is_root_node(node));
const BookmarkNode* parent = node->parent();
DCHECK(parent);
std::optional<size_t> index = parent->GetIndexOf(node);
DCHECK(index.has_value());
// Removing a permanent node is problematic and can cause crashes elsewhere
// that are difficult to trace back.
CHECK(!is_permanent_node(node)) << "for type " << node->type();
RemoveChildAt(parent, index.value(), location, source, /*is_undoable=*/true,
/*notify_observers=*/true);
}
void BookmarkModel::RemoveLastChild(const BookmarkNode* parent,
metrics::BookmarkEditSource source,
const base::Location& location) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(loaded_);
CHECK(parent);
CHECK(!is_root_node(parent));
CHECK(parent->is_folder());
CHECK(!parent->children().empty());
RemoveChildAt(parent, /*index=*/parent->children().size() - 1, location,
source, /*is_undoable=*/true, /*notify_observers=*/true);
}
void BookmarkModel::RemoveAllUserBookmarks(const base::Location& location) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(loaded_);
std::set<GURL> removed_urls;
struct RemoveNodeData {
raw_ptr<const BookmarkNode> parent;
int index;
std::unique_ptr<BookmarkNode> node;
};
std::vector<RemoveNodeData> removed_node_data_list;
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillRemoveAllUserBookmarks(location);
}
BeginExtensiveChanges();
// Skip deleting permanent nodes. Permanent bookmark nodes are the root and
// its immediate children. For removing all non permanent nodes just remove
// all children of non-root permanent nodes.
{
for (const auto& permanent_node : root_->children()) {
if (client_->IsNodeManaged(permanent_node.get())) {
continue;
}
const NodeTypeForUuidLookup type_for_uuid_lookup =
DetermineTypeForUuidLookupForExistingNode(permanent_node.get());
for (int j = static_cast<int>(permanent_node->children().size() - 1);
j >= 0; --j) {
std::unique_ptr<BookmarkNode> node =
url_index_->RemoveChildAt(permanent_node.get(), j, &removed_urls);
RemoveNodeFromIndicesRecursive(node.get(), type_for_uuid_lookup);
removed_node_data_list.push_back(
{permanent_node.get(), j, std::move(node)});
}
// Note that scheduling redundant saves is a no-op so it's done here
// inside the loop for simplicity.
ScheduleSaveForNode(permanent_node.get());
}
}
EndExtensiveChanges();
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkAllUserNodesRemoved(removed_urls, location);
}
BeginGroupedChanges();
for (auto& removed_node_data : removed_node_data_list) {
client_->OnBookmarkNodeRemovedUndoable(removed_node_data.parent,
removed_node_data.index,
std::move(removed_node_data.node));
}
EndGroupedChanges();
}
void BookmarkModel::Move(const BookmarkNode* node,
const BookmarkNode* new_parent,
size_t index) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(loaded_);
DCHECK(node);
DCHECK(node->HasAncestor(root_node()));
CHECK(new_parent->HasAncestor(root_node()));
DCHECK(IsValidIndex(new_parent, index, true));
DCHECK(!is_root_node(new_parent));
DCHECK(!is_permanent_node(node));
DCHECK(!new_parent->HasAncestor(node));
SCOPED_CRASH_KEY_NUMBER("BookmarkModelMove", "newParentType",
new_parent->type());
DUMP_WILL_BE_CHECK(new_parent->is_folder());
const BookmarkNode* old_parent = node->parent();
size_t old_index = old_parent->GetIndexOf(node).value();
if (old_parent == new_parent &&
(index == old_index || index == old_index + 1)) {
// Node is already in this position, nothing to do.
return;
}
if (old_parent == new_parent && index > old_index) {
index--;
}
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillMoveBookmarkNode(old_parent, old_index, new_parent, index);
}
SetDateFolderModified(new_parent, Time::Now());
const NodeTypeForUuidLookup old_type_for_uuid_lookup =
DetermineTypeForUuidLookupForExistingNode(old_parent);
const NodeTypeForUuidLookup new_type_for_uuid_lookup =
DetermineTypeForUuidLookupForExistingNode(new_parent);
if (old_type_for_uuid_lookup != new_type_for_uuid_lookup) {
UpdateUuidIndexUponNodeMoveRecursive(node, old_type_for_uuid_lookup,
new_type_for_uuid_lookup);
}
BookmarkNode* mutable_old_parent = AsMutable(old_parent);
std::unique_ptr<BookmarkNode> owned_node =
mutable_old_parent->Remove(old_index);
BookmarkNode* mutable_new_parent = AsMutable(new_parent);
mutable_new_parent->Add(std::move(owned_node), index);
// These two calls don't guarantee that they get scheduled at the same time,
// which increases the risk that, if two JSON files are involved in this move,
// only one of them may succeed to write to disk (leading to data loss or data
// duplication). These scenarios should be very rare and the scheduling aspect
// of it is only a smart part of it, so we don't bother being too smart about
// it. Other risks are inherent to the use of two files.
ScheduleSaveForNode(old_parent);
ScheduleSaveForNode(new_parent);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeMoved(old_parent, old_index, new_parent, index);
}
if (old_parent != new_parent) {
// TODO(crbug.com/40074470): Remove if check once the root cause of this
// crash is identified and addressed, and new_parent->is_folder() is
// checked at the top of this method.
if (new_parent->is_folder()) {
metrics::RecordBookmarkMovedTo(GetFolderType(new_parent));
}
}
}
void BookmarkModel::UpdateLastUsedTime(const BookmarkNode* node,
const base::Time time,
bool just_opened) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(loaded_);
DCHECK(node);
base::Time last_used_time = node->date_last_used();
UpdateLastUsedTimeImpl(node, time);
if (just_opened) {
metrics::RecordBookmarkOpened(time, last_used_time, node->date_added(),
GetStorageStateForUma(node), node->is_url(),
GetUserFolderDepth(node->parent()));
}
}
void BookmarkModel::UpdateLastUsedTimeImpl(const BookmarkNode* node,
const base::Time time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(loaded_);
DCHECK(node);
BookmarkNode* mutable_node = AsMutable(node);
mutable_node->set_date_last_used(time);
ScheduleSaveForNode(node);
}
void BookmarkModel::ClearLastUsedTimeInRange(const base::Time delete_begin,
const base::Time delete_end) {
ClearLastUsedTimeInRangeRecursive(root_, delete_begin, delete_end);
}
void BookmarkModel::ClearLastUsedTimeInRangeRecursive(
BookmarkNode* node,
const base::Time delete_begin,
const base::Time delete_end) {
bool within_range = node->date_last_used() >= delete_begin &&
node->date_last_used() < delete_end;
bool for_all_time =
delete_begin.is_null() && (delete_end.is_null() || delete_end.is_max());
if (node->is_url() && (within_range || for_all_time)) {
UpdateLastUsedTimeImpl(node, Time());
}
for (size_t i = 0; i < node->children().size(); ++i) {
ClearLastUsedTimeInRangeRecursive(node->children()[i].get(), delete_begin,
delete_end);
}
}
const gfx::Image& BookmarkModel::GetFavicon(const BookmarkNode* node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(node);
if (node->favicon_state() == BookmarkNode::INVALID_FAVICON) {
BookmarkNode* mutable_node = AsMutable(node);
LoadFavicon(mutable_node);
}
return node->favicon();
}
void BookmarkModel::SetTitle(const BookmarkNode* node,
const std::u16string& title,
metrics::BookmarkEditSource source) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(node);
if (node->GetTitle() == title) {
return;
}
if (is_permanent_node(node) && !client_->CanSetPermanentNodeTitle(node)) {
NOTREACHED();
}
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillChangeBookmarkNode(node);
}
// The title index doesn't support changing the title, instead we remove then
// add it back. Only do this for URL nodes. A directory node can have its
// title changed but should be excluded from the index.
if (node->is_url()) {
titled_url_index_->Remove(node);
} else {
titled_url_index_->RemovePath(node);
}
url_index_->SetTitle(AsMutable(node), title);
if (node->is_url()) {
titled_url_index_->Add(node);
} else {
titled_url_index_->AddPath(node);
}
ScheduleSaveForNode(node);
metrics::RecordTitleEdit(source);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeChanged(node);
}
}
void BookmarkModel::SetURL(const BookmarkNode* node,
const GURL& url,
metrics::BookmarkEditSource source) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(node);
DCHECK(!node->is_folder());
if (node->url() == url) {
return;
}
BookmarkNode* mutable_node = AsMutable(node);
mutable_node->InvalidateFavicon();
CancelPendingFaviconLoadRequests(mutable_node);
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillChangeBookmarkNode(node);
}
// The title index doesn't support changing the URL, instead we remove then
// add it back.
titled_url_index_->Remove(mutable_node);
url_index_->SetUrl(mutable_node, url);
titled_url_index_->Add(mutable_node);
ScheduleSaveForNode(node);
metrics::RecordURLEdit(source);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeChanged(node);
}
}
void BookmarkModel::SetNodeMetaInfo(const BookmarkNode* node,
const std::string& key,
const std::string& value) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(node);
CHECK(!is_root_node(node));
std::string old_value;
if (node->GetMetaInfo(key, &old_value) && old_value == value) {
return;
}
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillChangeBookmarkMetaInfo(node);
}
if (AsMutable(node)->SetMetaInfo(key, value)) {
ScheduleSaveForNode(node);
}
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkMetaInfoChanged(node);
}
}
void BookmarkModel::SetNodeMetaInfoMap(
const BookmarkNode* node,
const BookmarkNode::MetaInfoMap& meta_info_map) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(node);
CHECK(!is_root_node(node));
const BookmarkNode::MetaInfoMap* old_meta_info_map = node->GetMetaInfoMap();
if ((!old_meta_info_map && meta_info_map.empty()) ||
(old_meta_info_map && meta_info_map == *old_meta_info_map)) {
return;
}
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillChangeBookmarkMetaInfo(node);
}
AsMutable(node)->SetMetaInfoMap(meta_info_map);
ScheduleSaveForNode(node);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkMetaInfoChanged(node);
}
}
void BookmarkModel::DeleteNodeMetaInfo(const BookmarkNode* node,
const std::string& key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const BookmarkNode::MetaInfoMap* meta_info_map = node->GetMetaInfoMap();
if (!meta_info_map || meta_info_map->find(key) == meta_info_map->end()) {
return;
}
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillChangeBookmarkMetaInfo(node);
}
if (AsMutable(node)->DeleteMetaInfo(key)) {
ScheduleSaveForNode(node);
}
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkMetaInfoChanged(node);
}
}
void BookmarkModel::OnFaviconsChanged(const std::set<GURL>& page_urls,
const GURL& icon_url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!loaded_) {
return;
}
std::set<const BookmarkNode*> to_update;
for (const GURL& page_url : page_urls) {
std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes =
GetNodesByURL(page_url);
to_update.insert(nodes.begin(), nodes.end());
}
if (!icon_url.is_empty()) {
// TODO(pkotwicz): Do something more efficient if `icon_url` is non-empty
// many times a day for each user.
url_index_->GetNodesWithIconUrl(icon_url, &to_update);
}
for (const BookmarkNode* node : to_update) {
// Rerequest the favicon.
BookmarkNode* mutable_node = AsMutable(node);
mutable_node->InvalidateFavicon();
CancelPendingFaviconLoadRequests(mutable_node);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeFaviconChanged(node);
}
}
}
void BookmarkModel::SetDateAdded(const BookmarkNode* node, Time date_added) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(node);
DCHECK(!is_permanent_node(node));
if (node->date_added() == date_added) {
return;
}
AsMutable(node)->set_date_added(date_added);
// Syncing might result in dates newer than the folder's last modified date.
if (date_added > node->parent()->date_folder_modified()) {
// Will trigger BookmarkStorage::ScheduleSaveForNode().
SetDateFolderModified(node->parent(), date_added);
} else {
ScheduleSaveForNode(node);
}
}
std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>
BookmarkModel::GetNodesByURL(const GURL& url) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes;
if (url_index_) {
url_index_->GetNodesByUrl(url, &nodes);
}
return nodes;
}
const BookmarkNode* BookmarkModel::GetNodeByUuid(
const base::Uuid& uuid,
NodeTypeForUuidLookup type) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Because of having to create a dummy node, the invalid-UUID case needs
// special handling.
if (!uuid.is_valid()) {
return nullptr;
}
const UuidIndex& uuid_index = uuid_index_.at(type);
auto it = uuid_index.find(uuid);
return it == uuid_index.end() ? nullptr : *it;
}
const BookmarkNode* BookmarkModel::GetMostRecentlyAddedUserNodeForURL(
const GURL& url) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes =
GetNodesByURL(url);
std::sort(nodes.begin(), nodes.end(), &MoreRecentlyAdded);
// Look for the first node that the user can edit.
for (size_t i = 0; i < nodes.size(); ++i) {
if (!client_->IsNodeManaged(nodes[i])) {
return nodes[i];
}
}
return nullptr;
}
bool BookmarkModel::HasBookmarks() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return url_index_ && url_index_->HasBookmarks();
}
bool BookmarkModel::HasNoUserCreatedBookmarksOrFolders() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return bookmark_bar_node_->children().empty() &&
other_node_->children().empty() && mobile_node_->children().empty();
}
bool BookmarkModel::IsBookmarked(const GURL& url) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return url_index_ && url_index_->IsBookmarked(url);
}
std::vector<UrlAndTitle> BookmarkModel::GetUniqueUrls() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!url_index_) {
return {};
}
return url_index_->GetUniqueUrls();
}
metrics::BookmarkFolderTypeForUMA BookmarkModel::GetFolderType(
const BookmarkNode* folder) const {
CHECK(folder->is_folder());
if (folder == bookmark_bar_node()) {
return metrics::BookmarkFolderTypeForUMA::kBookmarksBar;
} else if (folder == other_node()) {
return metrics::BookmarkFolderTypeForUMA::kOtherBookmarks;
} else if (folder == mobile_node()) {
return metrics::BookmarkFolderTypeForUMA::kMobileBookmarks;
}
return metrics::BookmarkFolderTypeForUMA::kUserGeneratedFolder;
}
const BookmarkNode* BookmarkModel::AddFolder(
const BookmarkNode* parent,
size_t index,
const std::u16string& title,
const BookmarkNode::MetaInfoMap* meta_info,
std::optional<base::Time> creation_time,
std::optional<base::Uuid> uuid) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(loaded_);
DCHECK(parent);
DCHECK(parent->is_folder());
DCHECK(!is_root_node(parent));
DCHECK(parent->HasAncestor(root_node()));
DCHECK(IsValidIndex(parent, index, true));
DCHECK(!uuid || uuid->is_valid());
const base::Time provided_creation_time_or_now =
creation_time.value_or(Time::Now());
auto new_node = std::make_unique<BookmarkNode>(
generate_next_node_id(), uuid.value_or(base::Uuid::GenerateRandomV4()),
GURL());
new_node->set_date_added(provided_creation_time_or_now);
new_node->set_date_folder_modified(provided_creation_time_or_now);
// Folders shouldn't have line breaks in their titles.
new_node->SetTitle(title);
if (meta_info) {
new_node->SetMetaInfoMap(*meta_info);
}
metrics::RecordBookmarkFolderAdded(GetFolderType(parent),
GetStorageStateForUma(parent));
// TODO(mastiz): `added_by_user` should be true below or the parameter
// renamed.
return AddNode(AsMutable(parent), index, std::move(new_node),
/*added_by_user=*/false,
DetermineTypeForUuidLookupForExistingNode(parent));
}
const BookmarkNode* BookmarkModel::AddNewURL(
const BookmarkNode* parent,
size_t index,
const std::u16string& title,
const GURL& url,
const BookmarkNode::MetaInfoMap* meta_info) {
metrics::RecordUrlBookmarkAdded(GetFolderType(parent),
GetStorageStateForUma(parent),
GetUserFolderDepth(parent));
return AddURL(parent, index, title, url, meta_info, std::nullopt,
std::nullopt, true);
}
const BookmarkNode* BookmarkModel::AddURL(
const BookmarkNode* parent,
size_t index,
const std::u16string& title,
const GURL& url,
const BookmarkNode::MetaInfoMap* meta_info,
std::optional<base::Time> creation_time,
std::optional<base::Uuid> uuid,
bool added_by_user) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(loaded_);
DCHECK(url.is_valid());
DCHECK(parent);
DCHECK(parent->is_folder());
DCHECK(!is_root_node(parent));
DCHECK(parent->HasAncestor(root_node()));
DCHECK(IsValidIndex(parent, index, true));
DCHECK(!uuid || uuid->is_valid());
const base::Time provided_creation_time_or_now =
creation_time.value_or(Time::Now());
// Syncing may result in dates newer than the last modified date.
if (provided_creation_time_or_now > parent->date_folder_modified()) {
SetDateFolderModified(parent, provided_creation_time_or_now);
}
auto new_node = std::make_unique<BookmarkNode>(
generate_next_node_id(), uuid.value_or(base::Uuid::GenerateRandomV4()),
url);
new_node->SetTitle(title);
new_node->set_date_added(provided_creation_time_or_now);
if (meta_info) {
new_node->SetMetaInfoMap(*meta_info);
}
return AddNode(AsMutable(parent), index, std::move(new_node), added_by_user,
DetermineTypeForUuidLookupForExistingNode(parent));
}
void BookmarkModel::SortChildren(const BookmarkNode* parent) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!client_->IsNodeManaged(parent));
if (!parent || !parent->is_folder() || is_root_node(parent) ||
parent->children().size() <= 1) {
return;
}
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillReorderBookmarkNode(parent);
}
UErrorCode error = U_ZERO_ERROR;
std::unique_ptr<icu::Collator> collator(icu::Collator::createInstance(error));
if (U_FAILURE(error)) {
collator.reset(nullptr);
}
AsMutable(parent)->SortChildren(SortComparator(collator.get()));
ScheduleSaveForNode(parent);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeChildrenReordered(parent);
}
}
void BookmarkModel::ReorderChildren(
const BookmarkNode* parent,
const std::vector<const BookmarkNode*>& ordered_nodes) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!client_->IsNodeManaged(parent));
// Workaround for callers that provide an unexpected vector size in
// `ordered_nodes`, as there is evidence that Java callers may run into this
// scenario. While the underlying issue is investigated, avoid CHECK failures
// or other undesired side effects by simply ignoring the call.
// TODO(crbug.com/390764681): Investigate and fix the actual issue in Java
// instead of ignoring the call here.
if (parent->children().size() != ordered_nodes.size()) {
return;
}
// Ensure that all children in `parent` are in `ordered_nodes`.
CHECK_EQ(parent->children().size(), ordered_nodes.size());
for (const BookmarkNode* node : ordered_nodes) {
CHECK_EQ(parent, node->parent());
}
bool reordering_needed = false;
for (size_t i = 0; i < ordered_nodes.size(); ++i) {
if (ordered_nodes[i] != parent->children()[i].get()) {
reordering_needed = true;
break;
}
}
if (!reordering_needed) {
// Nothing to do.
return;
}
CHECK_GE(ordered_nodes.size(), 2u);
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillReorderBookmarkNode(parent);
}
std::map<const BookmarkNode*, int> order;
for (size_t i = 0; i < ordered_nodes.size(); ++i) {
order[ordered_nodes[i]] = i;
}
std::vector<size_t> new_order(ordered_nodes.size());
for (size_t old_index = 0; old_index < parent->children().size();
++old_index) {
const BookmarkNode* node = parent->children()[old_index].get();
size_t new_index = order[node];
new_order[old_index] = new_index;
}
AsMutable(parent)->ReorderChildren(new_order);
ScheduleSaveForNode(parent);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeChildrenReordered(parent);
}
}
void BookmarkModel::SetDateFolderModified(const BookmarkNode* parent,
const Time time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(parent);
AsMutable(parent)->set_date_folder_modified(time);
ScheduleSaveForNode(parent);
}
void BookmarkModel::ResetDateFolderModified(const BookmarkNode* node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
SetDateFolderModified(node, Time());
}
std::vector<TitledUrlMatch> BookmarkModel::GetBookmarksMatching(
const std::u16string& query,
size_t max_count_hint,
query_parser::MatchingAlgorithm matching_algorithm) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!loaded_) {
return {};
}
return titled_url_index_->GetResultsMatching(query, max_count_hint,
matching_algorithm);
}
void BookmarkModel::DisableWritesToDiskForTest() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
local_or_syncable_store_.reset();
account_store_.reset();
}
void BookmarkModel::LoadEmptyForTest() {
auto details = std::make_unique<BookmarkLoadDetails>();
model_loader_ = ModelLoader::CreateForTest(
client_->GetLoadManagedNodeCallback(), details.get());
DoneLoading(std::move(details));
CHECK(loaded_);
}
void BookmarkModel::CommitPendingWriteForTest() {
if (local_or_syncable_store_) {
local_or_syncable_store_->SaveNowIfScheduledForTesting(); // IN-TEST
}
if (account_store_) {
account_store_->SaveNowIfScheduledForTesting(); // IN-TEST
}
}
bool BookmarkModel::LocalOrSyncableStorageHasPendingWriteForTest() const {
return local_or_syncable_store_->HasScheduledSaveForTesting(); // IN-TEST
}
bool BookmarkModel::AccountStorageHasPendingWriteForTest() const {
CHECK(account_store_);
return account_store_->HasScheduledSaveForTesting(); // IN-TEST
}
void BookmarkModel::RestoreRemovedNode(const BookmarkNode* parent,
size_t index,
std::unique_ptr<BookmarkNode> node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
BookmarkNode* node_ptr = node.get();
AddNode(AsMutable(parent), index, std::move(node), /*added_by_user=*/false,
DetermineTypeForUuidLookupForExistingNode(parent));
// We might be restoring a folder node that have already contained a set of
// child nodes. We need to notify all of them.
NotifyNodeAddedForAllDescendants(node_ptr, /*added_by_user=*/false);
}
BookmarkModel::NodeTypeForUuidLookup
BookmarkModel::DetermineTypeForUuidLookupForExistingNode(
const BookmarkNode* node) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!is_root_node(node));
for (const auto& type_and_uuid_index : uuid_index_) {
if (GetNodeByUuid(node->uuid(), type_and_uuid_index.first) == node) {
return type_and_uuid_index.first;
}
}
NOTREACHED();
}
void BookmarkModel::NotifyNodeAddedForAllDescendants(const BookmarkNode* node,
bool added_by_user) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (size_t i = 0; i < node->children().size(); ++i) {
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeAdded(node, i, added_by_user);
}
NotifyNodeAddedForAllDescendants(node->children()[i].get(), added_by_user);
}
}
void BookmarkModel::DoneLoading(std::unique_ptr<BookmarkLoadDetails> details) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(details);
DCHECK(!loaded_);
DCHECK(details->required_recovery() || !details->ids_reassigned());
next_node_id_ = details->max_id();
titled_url_index_ = details->owned_titled_url_index();
uuid_index_[NodeTypeForUuidLookup::kLocalOrSyncableNodes] =
details->owned_local_or_syncable_uuid_index();
uuid_index_[NodeTypeForUuidLookup::kAccountNodes] =
details->owned_account_uuid_index();
url_index_ = details->url_index();
root_ = url_index_->root();
// See declaration for details on why `owned_root_` is reset.
owned_root_.reset();
bookmark_bar_node_ = details->bb_node();
other_node_ = details->other_folder_node();
mobile_node_ = details->mobile_folder_node();
account_bookmark_bar_node_ = details->account_bb_node();
account_other_node_ = details->account_other_folder_node();
account_mobile_node_ = details->account_mobile_folder_node();
if (!AreFoldersForAccountStorageAllowed()) {
CHECK(!account_bookmark_bar_node_);
CHECK(!account_other_node_);
CHECK(!account_mobile_node_);
CHECK(uuid_index_[NodeTypeForUuidLookup::kAccountNodes].empty());
}
titled_url_index_->SetNodeSorter(
std::make_unique<TypedCountSorter>(client_.get()));
// Sorting the permanent nodes has to happen on the main thread, so we do it
// here, after loading completes.
root_->SortChildren(VisibilityComparator(client_.get()));
// Decoding of sync metadata may invoke `RemoveAccountPermanentFolders()`,
// which can lead to dangling raw_ptr members.
details->ResetPermanentNodePointers();
if (details->required_recovery()) {
// If the from-disk loading went through a recovery (e.g. IDs were
// reassigned due to collisions), it is best to save the result back to
// disk so it won't keep happening upon every restart.
if (local_or_syncable_store_) {
local_or_syncable_store_->ScheduleSave();
}
if (account_store_) {
CHECK(AreFoldersForAccountStorageAllowed());
account_store_->ScheduleSave();
}
client_->RequiredRecoveryToLoad(
details->local_or_syncable_reassigned_ids_per_old_id());
}
client_->DecodeLocalOrSyncableBookmarkSyncMetadata(
details->local_or_syncable_sync_metadata_str(),
local_or_syncable_store_
? base::BindRepeating(
&BookmarkStorage::ScheduleSave,
base::Unretained(local_or_syncable_store_.get()))
: base::DoNothing());
if (AreFoldersForAccountStorageAllowed()) {
switch (client_->DecodeAccountBookmarkSyncMetadata(
details->account_sync_metadata_str(),
account_store_
? base::BindRepeating(&BookmarkStorage::ScheduleSave,
base::Unretained(account_store_.get()))
: base::DoNothing())) {
case BookmarkClient::DecodeAccountBookmarkSyncMetadataResult::kSuccess:
// Nothing to do.
break;
case BookmarkClient::DecodeAccountBookmarkSyncMetadataResult::
kMustRemoveAccountPermanentFolders:
RemoveAccountPermanentFoldersImpl(/*notify_observers=*/false);
break;
}
}
const base::TimeDelta load_duration =
base::TimeTicks::Now() - details->load_start();
metrics::RecordTimeToLoadAtStartup(load_duration);
loaded_ = true;
// Notify our direct observers.
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkModelLoaded(details->ids_reassigned());
}
}
BookmarkNode* BookmarkModel::AddNode(
BookmarkNode* parent,
size_t index,
std::unique_ptr<BookmarkNode> node,
bool added_by_user,
NodeTypeForUuidLookup type_for_uuid_lookup) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
BookmarkNode* node_ptr = node.get();
url_index_->Add(parent, index, std::move(node));
ScheduleSaveForNode(node_ptr);
AddNodeToIndicesRecursive(node_ptr, type_for_uuid_lookup);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeAdded(parent, index, added_by_user);
}
return node_ptr;
}
void BookmarkModel::AddNodeToIndicesRecursive(
const BookmarkNode* node,
NodeTypeForUuidLookup type_for_uuid_lookup) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
bool uuid_is_unique = uuid_index_[type_for_uuid_lookup].insert(node).second;
DUMP_WILL_BE_CHECK(uuid_is_unique);
if (node->is_url()) {
titled_url_index_->Add(node);
} else {
titled_url_index_->AddPath(node);
}
for (const auto& child : node->children()) {
AddNodeToIndicesRecursive(child.get(), type_for_uuid_lookup);
}
}
void BookmarkModel::RemoveChildAt(
const BookmarkNode* parent,
size_t index,
const base::Location& location,
std::optional<metrics::BookmarkEditSource> source,
bool is_undoable,
bool notify_observers) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(loaded_ || !notify_observers);
DCHECK(parent);
DCHECK(IsValidIndex(parent, index, false));
const BookmarkNode* node = parent->children()[index].get();
const NodeTypeForUuidLookup type_for_uuid_lookup =
DetermineTypeForUuidLookupForExistingNode(node);
if (notify_observers) {
for (BookmarkModelObserver& observer : observers_) {
observer.OnWillRemoveBookmarks(parent, index, node, location);
}
}
// Schedule the save before actually removing the node for
// `ScheduleSaveNode()` to determine which underlying storage is relevant.
// `parent` could be used instead except for the case where permanent account
// folders are removed, which also exercises this codepath.
ScheduleSaveForNode(node);
std::set<GURL> removed_urls;
std::unique_ptr<BookmarkNode> owned_node =
url_index_->RemoveChildAt(AsMutable(parent), index, &removed_urls);
RemoveNodeFromIndicesRecursive(owned_node.get(), type_for_uuid_lookup);
if (notify_observers) {
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeRemoved(parent, index, node, removed_urls, location);
}
}
if (is_undoable) {
client_->OnBookmarkNodeRemovedUndoable(parent, index,
std::move(owned_node));
}
if (source.has_value()) {
metrics::RecordBookmarkRemoved(*source);
}
}
void BookmarkModel::RemoveAccountPermanentFoldersImpl(bool notify_observers) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(AreFoldersForAccountStorageAllowed());
// No-op if account permanent folders don't exist.
if (!account_bookmark_bar_node_) {
CHECK(!account_other_node_);
CHECK(!account_mobile_node_);
return;
}
base::ScopedUmaHistogramTimer scoped_timer(
"Bookmarks.RemoveAccountPermanentFoldersDuration",
base::ScopedUmaHistogramTimer::ScopedHistogramTiming::kMediumTimes);
CHECK(account_other_node_);
CHECK(account_mobile_node_);
// Make a copy of the pointers before deleting the nodes, to avoid raw_ptr
// reporting dangling pointers.
std::vector<BookmarkNode*> account_permanent_folders{
account_mobile_node_, account_other_node_, account_bookmark_bar_node_};
account_bookmark_bar_node_ = nullptr;
account_other_node_ = nullptr;
account_mobile_node_ = nullptr;
for (const BookmarkNode* node : account_permanent_folders) {
RemoveChildAt(node->parent(), node->parent()->GetIndexOf(node).value(),
FROM_HERE, /*source=*/std::nullopt, /*is_undoable=*/false,
notify_observers);
}
}
void BookmarkModel::RemoveNodeFromIndicesRecursive(
BookmarkNode* node,
NodeTypeForUuidLookup type_for_uuid_lookup) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!is_permanent_node(node));
if (node->is_url()) {
titled_url_index_->Remove(node);
} else {
titled_url_index_->RemovePath(node);
}
uuid_index_[type_for_uuid_lookup].erase(node);
// Reset favicon state for the case when the `node` is restored.
CancelPendingFaviconLoadRequests(node);
node->InvalidateFavicon();
// Recurse through children.
for (size_t i = node->children().size(); i > 0; --i) {
RemoveNodeFromIndicesRecursive(node->children()[i - 1].get(),
type_for_uuid_lookup);
}
}
void BookmarkModel::UpdateUuidIndexUponNodeMoveRecursive(
const BookmarkNode* node,
NodeTypeForUuidLookup old_type_for_uuid_lookup,
NodeTypeForUuidLookup new_type_for_uuid_lookup) {
CHECK(node);
CHECK_NE(old_type_for_uuid_lookup, new_type_for_uuid_lookup);
uuid_index_[old_type_for_uuid_lookup].erase(node);
bool success = uuid_index_[new_type_for_uuid_lookup].insert(node).second;
if (!success) {
// It is possible that the UUID exists in the new index. In this case, to
// avoid the collision, it is necessary to assign a new UUID.
AsMutable(node)->SetNewRandomUuid();
CHECK(uuid_index_[new_type_for_uuid_lookup].insert(node).second);
}
// Recursively do the same for all descendants.
for (const auto& child : node->children()) {
UpdateUuidIndexUponNodeMoveRecursive(child.get(), old_type_for_uuid_lookup,
new_type_for_uuid_lookup);
}
}
bool BookmarkModel::IsValidIndex(const BookmarkNode* parent,
size_t index,
bool allow_end) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return parent && parent->is_folder() &&
(index < parent->children().size() ||
(allow_end && index == parent->children().size()));
}
void BookmarkModel::OnFaviconDataAvailable(
BookmarkNode* node,
const favicon_base::FaviconImageResult& image_result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(node);
node->set_favicon_load_task_id(base::CancelableTaskTracker::kBadTaskId);
node->set_favicon_state(BookmarkNode::LOADED_FAVICON);
if (!image_result.image.IsEmpty()) {
node->set_favicon(image_result.image);
node->set_icon_url(image_result.icon_url);
FaviconLoaded(node);
} else {
// No favicon available, but we still notify observers.
FaviconLoaded(node);
}
}
void BookmarkModel::LoadFavicon(BookmarkNode* node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (node->is_folder()) {
return;
}
DCHECK(node->url().is_valid());
node->set_favicon_state(BookmarkNode::LOADING_FAVICON);
base::CancelableTaskTracker::TaskId taskId =
client_->GetFaviconImageForPageURL(
node->url(),
base::BindOnce(&BookmarkModel::OnFaviconDataAvailable,
base::Unretained(this), node),
&cancelable_task_tracker_);
if (taskId != base::CancelableTaskTracker::kBadTaskId) {
node->set_favicon_load_task_id(taskId);
}
}
void BookmarkModel::FaviconLoaded(const BookmarkNode* node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (BookmarkModelObserver& observer : observers_) {
observer.BookmarkNodeFaviconChanged(node);
}
}
void BookmarkModel::CancelPendingFaviconLoadRequests(BookmarkNode* node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (node->favicon_load_task_id() != base::CancelableTaskTracker::kBadTaskId) {
cancelable_task_tracker_.TryCancel(node->favicon_load_task_id());
node->set_favicon_load_task_id(base::CancelableTaskTracker::kBadTaskId);
}
}
int64_t BookmarkModel::generate_next_node_id() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(loaded_);
return next_node_id_++;
}
void BookmarkModel::CreateAccountPermanentFolders() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(AreFoldersForAccountStorageAllowed());
CHECK(loaded_);
{
std::unique_ptr<BookmarkPermanentNode> account_bookmark_bar_node =
BookmarkPermanentNode::CreateBookmarkBar(next_node_id_++);
account_bookmark_bar_node_ = account_bookmark_bar_node.get();
AddNode(root_, root_->children().size(),
std::move(account_bookmark_bar_node),
/*added_by_user=*/false, NodeTypeForUuidLookup::kAccountNodes);
}
{
std::unique_ptr<BookmarkPermanentNode> account_other_node =
BookmarkPermanentNode::CreateOtherBookmarks(next_node_id_++);
account_other_node_ = account_other_node.get();
AddNode(root_, root_->children().size(), std::move(account_other_node),
/*added_by_user=*/false, NodeTypeForUuidLookup::kAccountNodes);
}
{
std::unique_ptr<BookmarkPermanentNode> account_mobile_node =
BookmarkPermanentNode::CreateMobileBookmarks(next_node_id_++);
account_mobile_node_ = account_mobile_node.get();
AddNode(root_, root_->children().size(), std::move(account_mobile_node),
/*added_by_user=*/false, NodeTypeForUuidLookup::kAccountNodes);
}
}
void BookmarkModel::RemoveAccountPermanentFolders() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(AreFoldersForAccountStorageAllowed());
CHECK(loaded_);
RemoveAccountPermanentFoldersImpl(/*notify_observers=*/true);
}
size_t BookmarkModel::GetTotalNumberOfUrlsAndFoldersIncludingManagedNodes()
const {
size_t number_of_nodes = 0;
for (auto const& [lookup, uuid_index] : uuid_index_) {
number_of_nodes += uuid_index.size();
}
return number_of_nodes;
}
void BookmarkModel::ScheduleSaveForNode(const BookmarkNode* node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
BookmarkStorage* storage = GetStorageForNode(node);
if (storage) {
storage->ScheduleSave();
}
}
BookmarkStorage* BookmarkModel::GetStorageForNode(const BookmarkNode* node) {
CHECK(node);
CHECK(!is_root_node(node));
const BookmarkNode* permanent_node = GetSelfOrAncestorPermanentNode(node);
CHECK(permanent_node);
if (permanent_node == account_bookmark_bar_node_ ||
permanent_node == account_other_node_ ||
permanent_node == account_mobile_node_) {
CHECK(AreFoldersForAccountStorageAllowed());
return account_store_.get();
}
return local_or_syncable_store_.get();
}
metrics::StorageStateForUma BookmarkModel::GetStorageStateForUma(
const BookmarkNode* node) const {
CHECK(node);
CHECK(!is_root_node(node));
const BookmarkNode* permanent_node = GetSelfOrAncestorPermanentNode(node);
CHECK(permanent_node);
if (permanent_node == account_bookmark_bar_node_ ||
permanent_node == account_other_node_ ||
permanent_node == account_mobile_node_) {
CHECK(AreFoldersForAccountStorageAllowed());
return metrics::StorageStateForUma::kAccount;
}
// The ancestor is a local-or-syncable permanent folder.
return client_->IsSyncFeatureEnabledIncludingBookmarks()
? metrics::StorageStateForUma::kSyncEnabled
: metrics::StorageStateForUma::kLocalOnly;
}
} // namespace bookmarks