#include "chrome/browser/bookmarks/bookmark_html_writer.h"
#include <stddef.h>
#include <stdint.h>
#include <list>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include "base/base64.h"
#include "base/check.h"
#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/supports_user_data.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/bookmarks/browser/bookmark_codec.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/favicon/core/favicon_service.h"
#include "components/favicon_base/favicon_types.h"
#include "content/public/browser/browser_thread.h"
#include "ui/gfx/favicon_size.h"
using bookmarks::BookmarkCodec;
using bookmarks::BookmarkNode;
using content::BrowserThread;
namespace {
const char kBookmarkFaviconFetcherKey[] = "bookmark-favicon-fetcher";
const char kHeader[] =
"<!DOCTYPE NETSCAPE-Bookmark-file-1>\r\n"
"<!-- This is an automatically generated file.\r\n"
" It will be read and overwritten.\r\n"
" DO NOT EDIT! -->\r\n"
"<META HTTP-EQUIV=\"Content-Type\""
" CONTENT=\"text/html; charset=UTF-8\">\r\n"
"<TITLE>Bookmarks</TITLE>\r\n"
"<H1>Bookmarks</H1>\r\n"
"<DL><p>\r\n";
const char kNewline[] = "\r\n";
const char kBookmarkStart[] = "<DT><A HREF=\"";
const char kAddDate[] = "\" ADD_DATE=\"";
const char kIcon[] = "\" ICON=\"";
const char kBookmarkAttributeEnd[] = "\">";
const char kBookmarkEnd[] = "</A>";
const char kFolderStart[] = "<DT><H3 ADD_DATE=\"";
const char kLastModified[] = "\" LAST_MODIFIED=\"";
const char kBookmarkBar[] = "\" PERSONAL_TOOLBAR_FOLDER=\"true\">";
const char kFolderAttributeEnd[] = "\">";
const char kFolderEnd[] = "</H3>";
const char kFolderChildren[] = "<DL><p>";
const char kFolderChildrenEnd[] = "</DL><p>";
const size_t kIndentSize = 4;
class BookmarkFaviconFetcher : public base::SupportsUserData::Data {
public:
typedef std::map<std::string, scoped_refptr<base::RefCountedMemory>>
URLFaviconMap;
BookmarkFaviconFetcher(
Profile* profile,
const base::FilePath& path,
bookmark_html_writer::BookmarksExportCallback callback);
BookmarkFaviconFetcher(const BookmarkFaviconFetcher&) = delete;
BookmarkFaviconFetcher& operator=(const BookmarkFaviconFetcher&) = delete;
~BookmarkFaviconFetcher() override = default;
void ExportBookmarks();
private:
void ExtractUrls(const bookmarks::BookmarkNode* node);
void ExecuteWriter();
[[nodiscard]] bool FetchNextFavicon();
void OnFaviconDataAvailable(
const favicon_base::FaviconRawBitmapResult& bitmap_result);
raw_ptr<Profile> profile_;
std::list<std::string> bookmark_urls_;
base::CancelableTaskTracker cancelable_task_tracker_;
URLFaviconMap favicons_map_;
base::FilePath path_;
bookmark_html_writer::BookmarksExportCallback callback_;
};
class Writer : public base::RefCountedThreadSafe<Writer> {
public:
Writer(const bookmarks::BookmarkModel* model,
const base::FilePath& path,
BookmarkFaviconFetcher::URLFaviconMap favicons_map)
: path_(path), favicons_map_(std::move(favicons_map)) {
BookmarkCodec codec;
local_bookmarks_ =
codec.Encode(model->bookmark_bar_node(), model->other_node(),
model->mobile_node(), std::string());
if (model->account_bookmark_bar_node()) {
CHECK(model->account_other_node());
CHECK(model->account_mobile_node());
account_bookmarks_ = codec.Encode(
model->account_bookmark_bar_node(), model->account_other_node(),
model->account_mobile_node(), std::string());
} else {
CHECK(!model->account_other_node());
CHECK(!model->account_mobile_node());
}
}
Writer(const Writer&) = delete;
Writer& operator=(const Writer&) = delete;
bookmark_html_writer::Result DoWrite() {
if (!OpenFile()) {
return bookmark_html_writer::Result::kCouldNotCreateFile;
}
if (!Write(kHeader)) {
return bookmark_html_writer::Result::kCouldNotWriteHeader;
}
base::Value::Dict* local_permanent_folders =
local_bookmarks_.FindDict(BookmarkCodec::kRootsKey);
CHECK(local_permanent_folders);
base::Value::Dict* bookmark_bar_folder_value =
local_permanent_folders->FindDict(
BookmarkCodec::kBookmarkBarFolderNameKey);
CHECK(bookmark_bar_folder_value);
base::Value::Dict* other_folder_value = local_permanent_folders->FindDict(
BookmarkCodec::kOtherBookmarkFolderNameKey);
CHECK(other_folder_value);
base::Value::Dict* mobile_folder_value = local_permanent_folders->FindDict(
BookmarkCodec::kMobileBookmarkFolderNameKey);
CHECK(mobile_folder_value);
base::Value::Dict* account_permanent_folders =
account_bookmarks_.FindDict(BookmarkCodec::kRootsKey);
base::Value::Dict* account_bookmark_bar_folder_value = nullptr;
base::Value::Dict* account_other_folder_value = nullptr;
base::Value::Dict* account_mobile_folder_value = nullptr;
if (account_permanent_folders) {
account_bookmark_bar_folder_value = account_permanent_folders->FindDict(
BookmarkCodec::kBookmarkBarFolderNameKey);
account_other_folder_value = account_permanent_folders->FindDict(
BookmarkCodec::kOtherBookmarkFolderNameKey);
account_mobile_folder_value = account_permanent_folders->FindDict(
BookmarkCodec::kMobileBookmarkFolderNameKey);
CHECK(account_bookmark_bar_folder_value);
CHECK(account_other_folder_value);
CHECK(account_mobile_folder_value);
}
IncrementIndent();
if (!WriteFolderStart(*bookmark_bar_folder_value,
GetLatestTime({bookmark_bar_folder_value,
account_bookmark_bar_folder_value},
BookmarkCodec::kDateAddedKey),
GetLatestTime({bookmark_bar_folder_value,
account_bookmark_bar_folder_value},
BookmarkCodec::kDateModifiedKey),
BookmarkNode::BOOKMARK_BAR) ||
!WriteDescendants(*bookmark_bar_folder_value)) {
return bookmark_html_writer::Result::kCouldNotWriteNodes;
}
if (account_bookmark_bar_folder_value &&
!WriteDescendants(*account_bookmark_bar_folder_value)) {
return bookmark_html_writer::Result::kCouldNotWriteNodes;
}
if (!WriteFolderEnd()) {
return bookmark_html_writer::Result::kCouldNotWriteNodes;
}
if (!WriteDescendants(*other_folder_value)) {
return bookmark_html_writer::Result::kCouldNotWriteNodes;
}
if (account_other_folder_value &&
!WriteDescendants(*account_other_folder_value)) {
return bookmark_html_writer::Result::kCouldNotWriteNodes;
}
if (!WriteDescendants(*mobile_folder_value)) {
return bookmark_html_writer::Result::kCouldNotWriteNodes;
}
if (account_mobile_folder_value &&
!WriteDescendants(*account_mobile_folder_value)) {
return bookmark_html_writer::Result::kCouldNotWriteNodes;
}
DecrementIndent();
if (!Write(kFolderChildrenEnd) || !Write(kNewline)) {
return bookmark_html_writer::Result::kCouldNotWriteNodes;
}
file_.reset();
return bookmark_html_writer::Result::kSuccess;
}
private:
friend class base::RefCountedThreadSafe<Writer>;
enum TextType {
ATTRIBUTE_VALUE,
CONTENT
};
~Writer() = default;
std::string GetLatestTime(const std::vector<base::Value::Dict*>& folders,
std::string_view time_type_key) {
CHECK(std::ranges::any_of(
folders, [](const base::Value::Dict* folder) { return folder; }));
int64_t latest_time = 0;
for (base::Value::Dict* folder : folders) {
if (!folder) {
continue;
}
std::string* string_ptr = folder->FindString(time_type_key);
CHECK(string_ptr);
int64_t time;
CHECK(base::StringToInt64(*string_ptr, &time));
latest_time = std::max(latest_time, time);
}
return base::NumberToString(latest_time);
}
[[nodiscard]] bool OpenFile() {
int flags = base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE;
file_ = std::make_unique<base::File>(path_, flags);
if (!file_->IsValid()) {
PLOG(ERROR) << "Could not create " << path_;
return false;
}
return true;
}
void IncrementIndent() { indent_.resize(indent_.size() + kIndentSize, ' '); }
void DecrementIndent() {
CHECK(!indent_.empty());
indent_.resize(indent_.size() - kIndentSize, ' ');
}
[[nodiscard]] bool Write(const std::string& text) {
if (!text.length()) {
return true;
}
if (!file_->WriteAtCurrentPosAndCheck(base::as_byte_span(text))) {
PLOG(ERROR) << "Could not write text to " << path_;
return false;
}
return true;
}
[[nodiscard]] bool Write(const std::string& text, TextType type) {
DCHECK(base::IsStringUTF8(text));
std::string utf8_string;
switch (type) {
case ATTRIBUTE_VALUE:
utf8_string = text;
base::ReplaceSubstringsAfterOffset(&utf8_string, 0, "\"", """);
break;
case CONTENT:
utf8_string = base::EscapeForHTML(text);
break;
default:
NOTREACHED();
}
return Write(utf8_string);
}
[[nodiscard]] bool WriteIndent() { return Write(indent_); }
[[nodiscard]] bool WriteTime(const std::string& time_string) {
int64_t internal_value;
base::StringToInt64(time_string, &internal_value);
return Write(base::NumberToString(
base::Time::FromInternalValue(internal_value).ToTimeT()));
}
[[nodiscard]] bool WriteFolderStart(const base::Value::Dict& value,
const std::string& date_added,
const std::string& date_modified,
BookmarkNode::Type folder_type) {
const std::string* title = value.FindString(BookmarkCodec::kNameKey);
CHECK(title);
if (!WriteIndent() || !Write(kFolderStart) || !WriteTime(date_added) ||
!Write(kLastModified) || !WriteTime(date_modified)) {
return false;
}
switch (folder_type) {
case BookmarkNode::BOOKMARK_BAR:
if (!Write(kBookmarkBar)) {
return false;
}
break;
case BookmarkNode::FOLDER:
if (!Write(kFolderAttributeEnd)) {
return false;
}
break;
case BookmarkNode::URL:
case BookmarkNode::OTHER_NODE:
case BookmarkNode::MOBILE:
NOTREACHED();
}
if (!Write(*title, CONTENT) || !Write(kFolderEnd) || !Write(kNewline) ||
!WriteIndent() || !Write(kFolderChildren) || !Write(kNewline)) {
return false;
}
IncrementIndent();
return true;
}
[[nodiscard]] bool WriteDescendants(const base::Value::Dict& folder) {
const base::Value::List* child_values =
folder.FindList(BookmarkCodec::kChildrenKey);
CHECK(child_values);
for (const base::Value& child_value : *child_values) {
CHECK(child_value.is_dict());
if (!WriteNodeAndDescendants(child_value.GetDict())) {
return false;
}
}
return true;
}
[[nodiscard]] bool WriteFolderEnd() {
DecrementIndent();
return WriteIndent() && Write(kFolderChildrenEnd) && Write(kNewline);
}
[[nodiscard]] bool WriteNodeAndDescendants(const base::Value::Dict& value) {
const std::string* title_ptr = value.FindString(BookmarkCodec::kNameKey);
CHECK(title_ptr);
const std::string* date_added_string =
value.FindString(BookmarkCodec::kDateAddedKey);
CHECK(date_added_string);
const std::string* date_modified_string =
value.FindString(BookmarkCodec::kDateModifiedKey);
const std::string* type_string = value.FindString(BookmarkCodec::kTypeKey);
CHECK(type_string);
CHECK(*type_string == BookmarkCodec::kTypeURL ||
*type_string == BookmarkCodec::kTypeFolder);
std::string title = *title_ptr;
if (*type_string == BookmarkCodec::kTypeURL) {
const std::string* url_string = value.FindString(BookmarkCodec::kURLKey);
CHECK(url_string);
std::string favicon_string;
auto itr = favicons_map_.find(*url_string);
if (itr != favicons_map_.end()) {
scoped_refptr<base::RefCountedMemory> data = itr->second;
std::string favicon_base64_encoded = base::Base64Encode(*data);
GURL favicon_url("data:image/png;base64," + favicon_base64_encoded);
favicon_string = favicon_url.spec();
}
if (!WriteIndent() || !Write(kBookmarkStart) ||
!Write(*url_string, ATTRIBUTE_VALUE) || !Write(kAddDate) ||
!WriteTime(*date_added_string) ||
(!favicon_string.empty() &&
(!Write(kIcon) || !Write(favicon_string, ATTRIBUTE_VALUE))) ||
!Write(kBookmarkAttributeEnd) || !Write(title, CONTENT) ||
!Write(kBookmarkEnd) || !Write(kNewline)) {
return false;
}
return true;
}
CHECK(date_modified_string);
if (!WriteFolderStart(value, *date_added_string, *date_modified_string,
BookmarkNode::FOLDER)) {
return false;
}
if (!WriteDescendants(value)) {
return false;
}
if (!WriteFolderEnd()) {
return false;
}
return true;
}
base::Value::Dict local_bookmarks_;
base::Value::Dict account_bookmarks_;
base::FilePath path_;
BookmarkFaviconFetcher::URLFaviconMap favicons_map_;
std::unique_ptr<base::File> file_;
std::string indent_;
};
}
BookmarkFaviconFetcher::BookmarkFaviconFetcher(
Profile* profile,
const base::FilePath& path,
bookmark_html_writer::BookmarksExportCallback callback)
: profile_(profile), path_(path), callback_(std::move(callback)) {
DCHECK(!profile->IsOffTheRecord());
}
void BookmarkFaviconFetcher::ExportBookmarks() {
bookmarks::BookmarkModel* model =
BookmarkModelFactory::GetForBrowserContext(profile_);
ExtractUrls(model->bookmark_bar_node());
ExtractUrls(model->other_node());
ExtractUrls(model->mobile_node());
if (model->account_bookmark_bar_node()) {
CHECK(model->account_other_node());
CHECK(model->account_mobile_node());
ExtractUrls(model->account_bookmark_bar_node());
ExtractUrls(model->account_other_node());
ExtractUrls(model->account_mobile_node());
} else {
CHECK(!model->account_other_node());
CHECK(!model->account_mobile_node());
}
if (!bookmark_urls_.empty()) {
CHECK(favicons_map_.empty());
CHECK(FetchNextFavicon());
} else {
ExecuteWriter();
}
}
void BookmarkFaviconFetcher::ExtractUrls(const BookmarkNode* node) {
CHECK(node);
if (node->is_url()) {
std::string url = node->url().spec();
if (!url.empty()) {
bookmark_urls_.push_back(url);
}
} else {
for (const auto& child : node->children()) {
ExtractUrls(child.get());
}
}
}
void BookmarkFaviconFetcher::ExecuteWriter() {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&Writer::DoWrite,
base::MakeRefCounted<Writer>(
BookmarkModelFactory::GetForBrowserContext(profile_),
path_, std::move(favicons_map_))),
std::move(callback_));
profile_->RemoveUserData(kBookmarkFaviconFetcherKey);
}
bool BookmarkFaviconFetcher::FetchNextFavicon() {
if (bookmark_urls_.empty()) {
return false;
}
do {
std::string url = bookmark_urls_.front();
URLFaviconMap::const_iterator iter = favicons_map_.find(url);
if (favicons_map_.end() == iter) {
favicon::FaviconService* favicon_service =
FaviconServiceFactory::GetForProfile(
profile_, ServiceAccessType::EXPLICIT_ACCESS);
favicon_service->GetRawFaviconForPageURL(
GURL(url), {favicon_base::IconType::kFavicon}, gfx::kFaviconSize,
false,
base::BindOnce(&BookmarkFaviconFetcher::OnFaviconDataAvailable,
base::Unretained(this)),
&cancelable_task_tracker_);
return true;
} else {
bookmark_urls_.pop_front();
}
} while (!bookmark_urls_.empty());
return false;
}
void BookmarkFaviconFetcher::OnFaviconDataAvailable(
const favicon_base::FaviconRawBitmapResult& bitmap_result) {
GURL url;
if (!bookmark_urls_.empty()) {
url = GURL(bookmark_urls_.front());
bookmark_urls_.pop_front();
}
if (bitmap_result.is_valid() && !url.is_empty()) {
favicons_map_.insert(make_pair(url.spec(), bitmap_result.bitmap_data));
}
if (FetchNextFavicon()) {
return;
}
ExecuteWriter();
}
namespace bookmark_html_writer {
void WriteBookmarks(Profile* profile,
const base::FilePath& path,
BookmarksExportCallback callback) {
if (profile->GetUserData(kBookmarkFaviconFetcherKey)) {
return;
}
auto fetcher = std::make_unique<BookmarkFaviconFetcher>(profile, path,
std::move(callback));
auto* fetcher_ptr = fetcher.get();
profile->SetUserData(kBookmarkFaviconFetcherKey, std::move(fetcher));
fetcher_ptr->ExportBookmarks();
}
}