// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/android/bookmarks/partner_bookmarks_shim.h"
#include <tuple>
#include <utility>
#include "base/i18n/case_conversion.h"
#include "base/i18n/string_search.h"
#include "base/lazy_instance.h"
#include "base/memory/ptr_util.h"
#include "base/strings/escape.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/android/bookmarks/partner_bookmarks_reader.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/url_formatter/url_formatter.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "ui/base/models/tree_node_iterator.h"
using bookmarks::BookmarkNode;
using content::BrowserThread;
namespace {
// PartnerModelKeeper is used as a singleton to store an immutable hierarchy
// of partner bookmarks. The hierarchy is retrieved from the partner bookmarks
// provider and doesn't depend on the user profile.
// The retrieved hierarchy persists
// PartnerBookmarksShim is responsible to applying and storing the user changes
// (deletions/renames) in the user profile, thus keeping the hierarchy intact.
struct PartnerModelKeeper {
std::unique_ptr<BookmarkNode> partner_bookmarks_root;
bool loaded;
PartnerModelKeeper()
: loaded(false) {}
};
base::LazyInstance<PartnerModelKeeper>::DestructorAtExit
g_partner_model_keeper = LAZY_INSTANCE_INITIALIZER;
const void* const kPartnerBookmarksShimUserDataKey =
&kPartnerBookmarksShimUserDataKey;
// Dictionary keys for entries in the kPartnerBookmarksMapping pref.
const char kMappingUrl[] = "url";
const char kMappingProviderTitle[] = "provider_title";
const char kMappingTitle[] = "mapped_title";
bool g_disable_partner_bookmarks_editing = false;
} // namespace
// static
PartnerBookmarksShim* PartnerBookmarksShim::BuildForBrowserContext(
content::BrowserContext* browser_context) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
PartnerBookmarksShim* data =
static_cast<PartnerBookmarksShim*>(
browser_context->GetUserData(kPartnerBookmarksShimUserDataKey));
if (data)
return data;
data = new PartnerBookmarksShim(
Profile::FromBrowserContext(browser_context)->GetPrefs());
browser_context->SetUserData(kPartnerBookmarksShimUserDataKey,
base::WrapUnique(data));
data->ReloadNodeMapping();
return data;
}
// static
void PartnerBookmarksShim::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterListPref(prefs::kPartnerBookmarkMappings);
}
// static
void PartnerBookmarksShim::DisablePartnerBookmarksEditing() {
g_disable_partner_bookmarks_editing = true;
}
bool PartnerBookmarksShim::IsLoaded() const {
return g_partner_model_keeper.Get().loaded;
}
bool PartnerBookmarksShim::HasPartnerBookmarks() const {
DCHECK(IsLoaded());
return g_partner_model_keeper.Get().partner_bookmarks_root.get() != NULL;
}
bool PartnerBookmarksShim::IsReachable(const BookmarkNode* node) const {
DCHECK(IsPartnerBookmark(node));
if (!HasPartnerBookmarks())
return false;
if (!g_disable_partner_bookmarks_editing) {
for (const BookmarkNode* i = node; i != NULL; i = i->parent()) {
const NodeRenamingMapKey key(i->url(), i->GetTitle());
NodeRenamingMap::const_iterator remap = node_rename_remove_map_.find(key);
if (remap != node_rename_remove_map_.end() && remap->second.empty())
return false;
}
}
return true;
}
bool PartnerBookmarksShim::IsEditable(const BookmarkNode* node) const {
DCHECK(IsPartnerBookmark(node));
if (!HasPartnerBookmarks())
return false;
if (g_disable_partner_bookmarks_editing)
return false;
return true;
}
void PartnerBookmarksShim::RemoveBookmark(const BookmarkNode* node) {
DCHECK(IsEditable(node));
RenameBookmark(node, std::u16string());
}
void PartnerBookmarksShim::RenameBookmark(const BookmarkNode* node,
const std::u16string& title) {
DCHECK(IsEditable(node));
const NodeRenamingMapKey key(node->url(), node->GetTitle());
node_rename_remove_map_[key] = title;
SaveNodeMapping();
for (PartnerBookmarksShim::Observer& observer : observers_)
observer.PartnerShimChanged(this);
}
void PartnerBookmarksShim::AddObserver(
PartnerBookmarksShim::Observer* observer) {
observers_.AddObserver(observer);
}
void PartnerBookmarksShim::RemoveObserver(
PartnerBookmarksShim::Observer* observer) {
observers_.RemoveObserver(observer);
}
const BookmarkNode* PartnerBookmarksShim::GetNodeByID(int64_t id) const {
DCHECK(IsLoaded());
if (!HasPartnerBookmarks())
return NULL;
return GetNodeByID(GetPartnerBookmarksRoot(), id);
}
std::u16string PartnerBookmarksShim::GetTitle(const BookmarkNode* node) const {
DCHECK(node);
DCHECK(IsPartnerBookmark(node));
if (!g_disable_partner_bookmarks_editing) {
const NodeRenamingMapKey key(node->url(), node->GetTitle());
NodeRenamingMap::const_iterator i = node_rename_remove_map_.find(key);
if (i != node_rename_remove_map_.end())
return i->second;
}
return node->GetTitle();
}
bool PartnerBookmarksShim::IsPartnerBookmark(const BookmarkNode* node) const {
DCHECK(IsLoaded());
if (!HasPartnerBookmarks())
return false;
const BookmarkNode* parent = node;
while (parent) {
if (parent == GetPartnerBookmarksRoot())
return true;
parent = parent->parent();
}
return false;
}
const BookmarkNode* PartnerBookmarksShim::GetPartnerBookmarksRoot() const {
return g_partner_model_keeper.Get().partner_bookmarks_root.get();
}
void PartnerBookmarksShim::SetPartnerBookmarksRoot(
std::unique_ptr<bookmarks::BookmarkNode> root_node) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
g_partner_model_keeper.Get().partner_bookmarks_root = std::move(root_node);
g_partner_model_keeper.Get().loaded = true;
for (PartnerBookmarksShim::Observer& observer : observers_)
observer.PartnerShimLoaded(this);
}
PartnerBookmarksShim::NodeRenamingMapKey::NodeRenamingMapKey(
const GURL& url,
const std::u16string& provider_title)
: url_(url), provider_title_(provider_title) {}
PartnerBookmarksShim::NodeRenamingMapKey::~NodeRenamingMapKey() {}
bool operator<(const PartnerBookmarksShim::NodeRenamingMapKey& a,
const PartnerBookmarksShim::NodeRenamingMapKey& b) {
return std::tie(a.url_, a.provider_title_) <
std::tie(b.url_, b.provider_title_);
}
// static
void PartnerBookmarksShim::ClearInBrowserContextForTesting(
content::BrowserContext* browser_context) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
browser_context->SetUserData(kPartnerBookmarksShimUserDataKey, 0);
}
// static
void PartnerBookmarksShim::ClearPartnerModelForTesting() {
g_partner_model_keeper.Get().loaded = false;
g_partner_model_keeper.Get().partner_bookmarks_root.reset(0);
}
// static
void PartnerBookmarksShim::EnablePartnerBookmarksEditing() {
g_disable_partner_bookmarks_editing = false;
}
PartnerBookmarksShim::PartnerBookmarksShim(PrefService* prefs)
: prefs_(prefs), observers_(base::ObserverListPolicy::EXISTING_ONLY) {}
PartnerBookmarksShim::~PartnerBookmarksShim() {
for (PartnerBookmarksShim::Observer& observer : observers_)
observer.ShimBeingDeleted(this);
}
const BookmarkNode* PartnerBookmarksShim::GetNodeByID(
const BookmarkNode* parent,
int64_t id) const {
if (parent->id() == id)
return parent;
for (const auto& node : parent->children()) {
const BookmarkNode* result = GetNodeByID(node.get(), id);
if (result)
return result;
}
return nullptr;
}
void PartnerBookmarksShim::ReloadNodeMapping() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
node_rename_remove_map_.clear();
if (!prefs_)
return;
const base::Value::List& list =
prefs_->GetList(prefs::kPartnerBookmarkMappings);
for (const auto& entry : list) {
if (!entry.is_dict()) {
NOTREACHED_IN_MIGRATION();
continue;
}
const base::Value::Dict& dict = entry.GetDict();
const std::string* url = dict.FindString(kMappingUrl);
const std::string* provider_title = dict.FindString(kMappingProviderTitle);
const std::string* mapped_title = dict.FindString(kMappingTitle);
if (!url || !provider_title || !mapped_title) {
NOTREACHED_IN_MIGRATION();
continue;
}
const NodeRenamingMapKey key(GURL(*url),
base::UTF8ToUTF16(*provider_title));
node_rename_remove_map_[key] = base::UTF8ToUTF16(*mapped_title);
}
}
void PartnerBookmarksShim::SaveNodeMapping() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!prefs_)
return;
base::Value::List list;
for (NodeRenamingMap::const_iterator i = node_rename_remove_map_.begin();
i != node_rename_remove_map_.end();
++i) {
base::Value::Dict dict;
dict.Set(kMappingUrl, i->first.url().spec());
dict.Set(kMappingProviderTitle, i->first.provider_title());
dict.Set(kMappingTitle, i->second);
list.Append(std::move(dict));
}
prefs_->SetList(prefs::kPartnerBookmarkMappings, std::move(list));
}
void PartnerBookmarksShim::GetPartnerBookmarksMatchingProperties(
const bookmarks::QueryFields& query,
size_t max_count,
std::vector<const BookmarkNode*>* nodes) {
DCHECK(nodes->size() <= max_count);
std::vector<std::u16string> query_words =
bookmarks::ParseBookmarkQuery(query);
if (query_words.empty())
return;
ui::TreeNodeIterator<const BookmarkNode> iterator(GetPartnerBookmarksRoot());
// The check that size < max_count is necessary because we will search for
// user (non-partner) bookmarks before calling this function
while (iterator.has_next() && nodes->size() < max_count) {
const BookmarkNode* node = iterator.Next();
// Make sure we don't include the "Partner Bookmarks" folder
if (node == GetPartnerBookmarksRoot())
continue;
if (!query_words.empty() && !bookmarks::DoesBookmarkContainWords(
GetTitle(node), node->url(), query_words))
continue;
if (query.title && GetTitle(node) != *query.title)
continue;
nodes->push_back(node);
}
}