blob: abac9e464d7b8cf1e623a08650b14f06db957a81 [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_utils.h"
#include <stdint.h>
#include <algorithm>
#include <array>
#include <memory>
#include <unordered_set>
#include <utility>
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/i18n/case_conversion.h"
#include "base/i18n/string_search.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "components/bookmarks/browser/bookmark_client.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/scoped_group_bookmark_actions.h"
#include "components/bookmarks/common/bookmark_pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/query_parser/query_parser.h"
#include "components/url_formatter/url_formatter.h"
#include "ui/base/models/tree_node_iterator.h"
#include "url/gurl.h"
using base::Time;
namespace bookmarks {
namespace {
void CloneBookmarkNodeImpl(BookmarkModel* model,
const BookmarkNodeData::Element& element,
const BookmarkNode* parent,
size_t index_to_add_at,
bool reset_node_times) {
// Make sure to not copy non clonable keys.
BookmarkNode::MetaInfoMap meta_info_map = element.meta_info_map;
if (element.is_url) {
Time date_added = reset_node_times ? Time::Now() : element.date_added;
DCHECK(!date_added.is_null());
const BookmarkNode* node = model->AddURL(
parent, index_to_add_at, element.title, element.url, &meta_info_map);
model->SetDateAdded(node, date_added);
} else {
const BookmarkNode* cloned_node = model->AddFolder(
parent, index_to_add_at, element.title, &meta_info_map);
if (!reset_node_times) {
DCHECK(!element.date_folder_modified.is_null());
model->SetDateFolderModified(cloned_node, element.date_folder_modified);
}
for (int i = 0; i < static_cast<int>(element.children.size()); ++i) {
CloneBookmarkNodeImpl(model, element.children[i], cloned_node, i,
reset_node_times);
}
}
}
// Returns true if `text` contains each string in `words`. This is used when
// searching for bookmarks.
bool DoesBookmarkTextContainWords(const std::u16string& text,
const std::vector<std::u16string>& words) {
for (size_t i = 0; i < words.size(); ++i) {
if (!base::i18n::StringSearchIgnoringCaseAndAccents(words[i], text, nullptr,
nullptr)) {
return false;
}
}
return true;
}
// Recursively searches for a node satisfying the functor `pred` . Returns
// nullptr if not found.
template <typename Predicate>
const BookmarkNode* FindNode(const BookmarkNode* node, Predicate pred) {
if (pred(node)) {
return node;
}
for (const auto& child : node->children()) {
const BookmarkNode* result = FindNode(child.get(), pred);
if (result) {
return result;
}
}
return nullptr;
}
template <class type>
std::vector<const BookmarkNode*> GetBookmarksMatchingPropertiesImpl(
type& iterator,
BookmarkModel* model,
const QueryFields& query,
const std::vector<std::u16string>& query_words,
size_t max_count) {
std::vector<const BookmarkNode*> nodes;
while (iterator.has_next()) {
const BookmarkNode* node = iterator.Next();
if ((!query_words.empty() &&
!DoesBookmarkContainWords(node->GetTitle(), node->url(),
query_words)) ||
model->is_permanent_node(node)) {
continue;
}
if (query.title && node->GetTitle() != *query.title) {
continue;
}
nodes.push_back(node);
if (nodes.size() == max_count) {
break;
}
}
return nodes;
}
template <class Comparator>
void GetMostRecentEntries(
BookmarkModel* model,
size_t limit,
std::multiset<const BookmarkNode*, Comparator>* nodes_set) {
ui::TreeNodeIterator<const BookmarkNode> iterator(model->root_node());
while (iterator.has_next()) {
const BookmarkNode* node = iterator.Next();
if (node->is_url()) {
nodes_set->insert(node);
if (nodes_set->size() > limit) {
nodes_set->erase(std::next(nodes_set->begin(), limit),
nodes_set->end());
}
}
}
}
} // namespace
QueryFields::QueryFields() = default;
QueryFields::~QueryFields() = default;
VectorIterator::VectorIterator(
std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>* nodes)
: nodes_(nodes), current_(nodes->begin()) {}
VectorIterator::~VectorIterator() = default;
bool VectorIterator::has_next() {
return (current_ != nodes_->end());
}
const BookmarkNode* VectorIterator::Next() {
const BookmarkNode* result = *current_;
++current_;
return result;
}
void CloneBookmarkNode(BookmarkModel* model,
const std::vector<BookmarkNodeData::Element>& elements,
const BookmarkNode* parent,
size_t index_to_add_at,
bool reset_node_times) {
if (!parent->is_folder() || !model) {
NOTREACHED();
}
for (size_t i = 0; i < elements.size(); ++i) {
CloneBookmarkNodeImpl(model, elements[i], parent, index_to_add_at + i,
reset_node_times);
}
metrics::RecordCloneBookmarkNode(elements.size());
}
std::vector<const BookmarkNode*> GetMostRecentlyModifiedUserFolders(
BookmarkModel* model) {
std::vector<const BookmarkNode*> nodes;
ui::TreeNodeIterator<const BookmarkNode> iterator(
model->root_node(), base::BindRepeating(&PruneFoldersForDisplay, model));
while (iterator.has_next()) {
nodes.push_back(iterator.Next());
}
const std::array<const BookmarkNode*, 3>
account_permanent_nodes_possibly_null = {
model->account_mobile_node(), model->account_bookmark_bar_node(),
model->account_other_node()};
const BookmarkNode* default_node =
#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
model->account_mobile_node() ? model->account_mobile_node()
: model->mobile_node();
#else // BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS).
model->account_other_node() ? model->account_other_node()
: model->other_node();
#endif // BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
auto more_recently_modified = [account_permanent_nodes_possibly_null,
default_node](const BookmarkNode* n1,
const BookmarkNode* n2) {
base::Time t1 = base::Contains(account_permanent_nodes_possibly_null, n1)
? std::max(n1->date_folder_modified(), n1->date_added())
: n1->date_folder_modified();
base::Time t2 = base::Contains(account_permanent_nodes_possibly_null, n2)
? std::max(n2->date_folder_modified(), n2->date_added())
: n2->date_folder_modified();
// If no node has been modified more recently, choose a default folder.
return t1 == t2 ? (n1 == default_node || n2 != default_node) : (t1 > t2);
};
std::ranges::stable_sort(nodes, more_recently_modified);
return nodes;
}
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
BookmarkNodesSplitByAccountAndLocal::BookmarkNodesSplitByAccountAndLocal() =
default;
BookmarkNodesSplitByAccountAndLocal::BookmarkNodesSplitByAccountAndLocal(
const BookmarkNodesSplitByAccountAndLocal&) = default;
BookmarkNodesSplitByAccountAndLocal&
BookmarkNodesSplitByAccountAndLocal::operator=(
const BookmarkNodesSplitByAccountAndLocal&) = default;
BookmarkNodesSplitByAccountAndLocal::~BookmarkNodesSplitByAccountAndLocal() =
default;
BookmarkNodesSplitByAccountAndLocal GetMostRecentlyUsedFoldersForDisplay(
BookmarkModel* model,
const BookmarkNode* displayed_node) {
// `displayed_node` is meant to be a bookmark. Code below is not tested for
// folders.
CHECK(!displayed_node->is_folder());
// Max number of most recently used non-permanent-node folders.
static constexpr size_t kMaxMRUFolders = 5;
std::vector<const BookmarkNode*> mru_nodes =
bookmarks::GetMostRecentlyModifiedUserFolders(model);
const BookmarkNode* const most_recent_node =
mru_nodes.empty() ? nullptr : mru_nodes[0];
// Special case the parent item, it'll either remain first or filtered out as
// a permanent node and added back later.
std::erase(mru_nodes, displayed_node->parent()); // No-op if not present.
mru_nodes.insert(mru_nodes.begin(), displayed_node->parent());
// Remove permanent nodes, they'll be re-added at the end if used later.
std::erase_if(mru_nodes, [](const BookmarkNode* mru_node) {
return mru_node->is_permanent_node();
});
// Figure out which permanent nodes to add.
const bool account_nodes_exist =
model->account_bookmark_bar_node() != nullptr;
const std::vector<const BookmarkNode*> account_permanent_nodes(
{model->account_bookmark_bar_node(), model->account_other_node(),
model->account_mobile_node()});
const std::vector<const BookmarkNode*> local_permanent_nodes(
{model->bookmark_bar_node(), model->other_node(), model->mobile_node()});
std::vector<const BookmarkNode*> permanent_nodes_included;
for (const BookmarkNode* permanent_node :
account_nodes_exist ? account_permanent_nodes : local_permanent_nodes) {
if (!permanent_node->IsVisible()) {
continue;
}
permanent_nodes_included.push_back(permanent_node);
}
if (account_nodes_exist) {
// Add back the most recent node and the parent node if either of them are
// local permanent nodes. Permanent account nodes are preferred.
auto append_if_permanent_local_node = [model, &permanent_nodes_included](
const BookmarkNode* mru_node) {
if (mru_node->is_permanent_node() && model->IsLocalOnlyNode(*mru_node)) {
permanent_nodes_included.push_back(mru_node);
}
};
if (most_recent_node) {
append_if_permanent_local_node(most_recent_node);
}
if (displayed_node->parent() != most_recent_node) {
append_if_permanent_local_node(displayed_node->parent());
}
}
// Cap total number of non-permanent nodes to kMaxMRUFolders.
mru_nodes.resize(std::min(mru_nodes.size(), kMaxMRUFolders));
// Add permanent nodes at the end (note that these lists are both sorted and
// will remain sorted (permanent last) when split them up below.
mru_nodes.insert(mru_nodes.end(), permanent_nodes_included.begin(),
permanent_nodes_included.end());
// Split between account and local nodes if there are account nodes.
BookmarkNodesSplitByAccountAndLocal result;
if (account_nodes_exist) {
std::vector<const BookmarkNode*> account_nodes;
std::vector<const BookmarkNode*> local_nodes;
for (const BookmarkNode* mru_node : mru_nodes) {
(model->IsLocalOnlyNode(*mru_node) ? result.local_nodes
: result.account_nodes)
.push_back(mru_node);
}
} else {
result.local_nodes = std::move(mru_nodes);
}
return result;
}
BookmarkNodesSplitByAccountAndLocal GetPermanentNodesForDisplay(
const BookmarkModel* model) {
BookmarkNodesSplitByAccountAndLocal permanent_nodes;
for (const auto& permanent_node : model->root_node()->children()) {
BookmarkNode* node = permanent_node.get();
// Do not include permanent nodes if they should not be visible.
if (PruneFoldersForDisplay(model, node)) {
continue;
}
if (model->IsLocalOnlyNode(*node) ||
model->client()->IsSyncFeatureEnabledIncludingBookmarks()) {
permanent_nodes.local_nodes.push_back(node);
} else {
permanent_nodes.account_nodes.push_back(node);
}
}
return permanent_nodes;
}
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
bool HasLocalOrSyncableBookmarks(const BookmarkModel* model) {
return std::ranges::any_of(
std::array{model->bookmark_bar_node(), model->other_node(),
model->mobile_node()},
[](const BookmarkNode* node) { return !node->children().empty(); });
}
void GetMostRecentlyAddedEntries(BookmarkModel* model,
size_t count,
std::vector<const BookmarkNode*>* nodes) {
// std::set is used here since insert element into std::vector is slower than
// std::set, so we use std::set to find the most recent bookmarks, and then
// return to users as std::vector.
std::multiset<const BookmarkNode*, decltype(&MoreRecentlyAdded)> nodes_set(
&MoreRecentlyAdded);
GetMostRecentEntries(model, count, &nodes_set);
nodes->reserve(nodes_set.size());
std::move(nodes_set.begin(), nodes_set.end(), std::back_inserter(*nodes));
}
bool MoreRecentlyAdded(const BookmarkNode* n1, const BookmarkNode* n2) {
return n1->date_added() > n2->date_added();
}
void GetMostRecentlyUsedEntries(BookmarkModel* model,
size_t count,
std::vector<const BookmarkNode*>* nodes) {
// std::set is used here since insert element into std::vector is slower than
// std::set, so we use std::set to find the most recent bookmarks, and then
// return to users as std::vector.
auto lastUsedComp = [](const BookmarkNode* n1, const BookmarkNode* n2) {
if (n1->date_last_used() == n2->date_last_used()) {
// Both bookmarks have same used date, we compare added date instead,
// normally this happens when both bookmarks are never used.
return n1->date_added() > n2->date_added();
}
return n1->date_last_used() > n2->date_last_used();
};
std::multiset<const BookmarkNode*, decltype(lastUsedComp)> nodes_set(
lastUsedComp);
GetMostRecentEntries(model, count, &nodes_set);
nodes->reserve(nodes_set.size());
std::move(nodes_set.begin(), nodes_set.end(), std::back_inserter(*nodes));
}
std::vector<const BookmarkNode*> GetBookmarksMatchingProperties(
BookmarkModel* model,
const QueryFields& query,
size_t max_count) {
std::vector<std::u16string> query_words = ParseBookmarkQuery(query);
if (query.word_phrase_query && query_words.empty()) {
return {};
}
if (query.url) {
// Shortcut into the BookmarkModel if searching for URL.
GURL url(*query.url);
std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>
url_matched_nodes;
if (url.is_valid()) {
url_matched_nodes = model->GetNodesByURL(url);
}
VectorIterator iterator(&url_matched_nodes);
return GetBookmarksMatchingPropertiesImpl<VectorIterator>(
iterator, model, query, query_words, max_count);
}
ui::TreeNodeIterator<const BookmarkNode> iterator(model->root_node());
return GetBookmarksMatchingPropertiesImpl<
ui::TreeNodeIterator<const BookmarkNode>>(iterator, model, query,
query_words, max_count);
}
// Parses the provided query and returns a vector of query words.
std::vector<std::u16string> ParseBookmarkQuery(
const bookmarks::QueryFields& query) {
std::vector<std::u16string> query_words;
if (query.word_phrase_query) {
query_parser::QueryParser::ParseQueryWords(
base::i18n::ToLower(*query.word_phrase_query),
query_parser::MatchingAlgorithm::DEFAULT, &query_words);
}
return query_words;
}
// Returns true if `node`s title or url contains the strings in `words`.
bool DoesBookmarkContainWords(const std::u16string& title,
const GURL& url,
const std::vector<std::u16string>& words) {
return DoesBookmarkTextContainWords(title, words) ||
DoesBookmarkTextContainWords(base::UTF8ToUTF16(url.spec()), words) ||
DoesBookmarkTextContainWords(
url_formatter::FormatUrl(url, url_formatter::kFormatUrlOmitNothing,
base::UnescapeRule::NORMAL, nullptr,
nullptr, nullptr),
words);
}
void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterBooleanPref(
prefs::kShowBookmarkBar, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(prefs::kEditBookmarksEnabled, true);
registry->RegisterBooleanPref(
prefs::kShowAppsShortcutInBookmarkBar, false,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(
prefs::kShowTabGroupsInBookmarkBar, true,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterBooleanPref(
prefs::kShowManagedBookmarksInBookmarkBar, true,
user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
registry->RegisterTimePref(prefs::kBookmarkStorageComputationLastUpdatePref,
base::Time());
RegisterManagedBookmarksPrefs(registry);
}
void RegisterManagedBookmarksPrefs(PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kManagedBookmarks);
registry->RegisterStringPref(prefs::kManagedBookmarksFolderName,
std::string());
}
void DeleteBookmarkFolders(BookmarkModel* model,
const std::vector<int64_t>& ids,
const base::Location& location) {
// Remove the folders that were removed. This has to be done after all the
// other changes have been committed.
for (auto iter = ids.begin(); iter != ids.end(); ++iter) {
const BookmarkNode* node = GetBookmarkNodeByID(model, *iter);
if (!node) {
continue;
}
model->Remove(node, metrics::BookmarkEditSource::kUser, location);
}
}
const BookmarkNode* AddIfNotBookmarked(BookmarkModel* model,
const GURL& url,
const std::u16string& title) {
// Nothing to do, a user bookmark with that url already exists.
if (IsBookmarkedByUser(model, url)) {
return nullptr;
}
base::RecordAction(base::UserMetricsAction("BookmarkAdded"));
const BookmarkNode* parent_to_use = GetParentForNewNodes(model, url);
return model->AddNewURL(parent_to_use, parent_to_use->children().size(),
title, url);
}
void RemoveAllBookmarks(BookmarkModel* model,
const GURL& url,
const base::Location& location) {
// Remove all the user bookmarks.
for (const BookmarkNode* node : model->GetNodesByURL(url)) {
if (!model->client()->IsNodeManaged(node)) {
model->Remove(node, metrics::BookmarkEditSource::kUser, location);
}
}
}
bool IsBookmarkedByUser(BookmarkModel* model, const GURL& url) {
for (const BookmarkNode* node : model->GetNodesByURL(url)) {
if (!model->client()->IsNodeManaged(node)) {
return true;
}
}
return false;
}
const BookmarkNode* GetBookmarkNodeByID(const BookmarkModel* model,
int64_t id) {
return FindNode(model->root_node(),
[id](const BookmarkNode* node) { return node->id() == id; });
}
bool IsDescendantOf(const BookmarkNode* node, const BookmarkNode* root) {
return node && node->HasAncestor(root);
}
bool HasDescendantsOf(
const std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>& list,
const BookmarkNode* root) {
for (const BookmarkNode* node : list) {
if (IsDescendantOf(node, root)) {
return true;
}
}
return false;
}
const BookmarkNode* GetParentForNewNodes(BookmarkModel* model,
const GURL& url) {
const BookmarkNode* parent = model->client()->GetSuggestedSaveLocation(url);
if (parent) {
return parent;
}
// Return the last modified folder if there is no save location suggestion.
std::vector<const BookmarkNode*> nodes =
GetMostRecentlyModifiedUserFolders(model);
CHECK(!nodes.empty());
return nodes[0];
}
bool PruneFoldersForDisplay(const BookmarkModel* model,
const BookmarkNode* node) {
return !node->IsVisible() || !node->is_folder() ||
model->client()->IsNodeManaged(node);
}
} // namespace bookmarks