910e62b5创建于 1月15日历史提交
// Copyright 2012 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/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";

// File header.
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";

// Newline separator.
const char kNewline[] = "\r\n";

// The following are used for bookmarks.

// Start of a bookmark.
const char kBookmarkStart[] = "<DT><A HREF=\"";
// After kBookmarkStart.
const char kAddDate[] = "\" ADD_DATE=\"";
// After kAddDate.
const char kIcon[] = "\" ICON=\"";
// After kIcon.
const char kBookmarkAttributeEnd[] = "\">";
// End of a bookmark.
const char kBookmarkEnd[] = "</A>";

// The following are used when writing folders.

// Start of a folder.
const char kFolderStart[] = "<DT><H3 ADD_DATE=\"";
// After kFolderStart.
const char kLastModified[] = "\" LAST_MODIFIED=\"";
// After kLastModified when writing the bookmark bar.
const char kBookmarkBar[] = "\" PERSONAL_TOOLBAR_FOLDER=\"true\">";
// After kLastModified when writing a user created folder.
const char kFolderAttributeEnd[] = "\">";
// End of the folder.
const char kFolderEnd[] = "</H3>";
// Start of the children of a folder.
const char kFolderChildren[] = "<DL><p>";
// End of the children for a folder.
const char kFolderChildrenEnd[] = "</DL><p>";

// Number of characters to indent by.
const size_t kIndentSize = 4;

// Fetches favicons for list of bookmarks and then starts Writer which outputs
// bookmarks and favicons to html file.
class BookmarkFaviconFetcher : public base::SupportsUserData::Data {
 public:
  // Map of URL and corresponding favicons.
  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;

  // Executes bookmark export process.
  void ExportBookmarks();

 private:
  // Recursively extracts URLs from bookmarks.
  void ExtractUrls(const bookmarks::BookmarkNode* node);

  // Executes Writer task that writes bookmarks data to html file.
  void ExecuteWriter();

  // Starts async fetch for the next bookmark favicon.
  // Takes single url from bookmark_urls_ and removes it from the list.
  // Returns true if there are more favicons to extract.
  [[nodiscard]] bool FetchNextFavicon();

  // Favicon fetch callback. After all favicons are fetched executes
  // html output with |background_io_task_runner_|.
  void OnFaviconDataAvailable(
      const favicon_base::FaviconRawBitmapResult& bitmap_result);

  // The Profile object used for accessing FaviconService, bookmarks model.
  raw_ptr<Profile> profile_;

  // All URLs that are extracted from bookmarks. Used to fetch favicons
  // for each of them. After favicon is fetched top url is removed from list.
  std::list<std::string> bookmark_urls_;

  // Tracks favicon tasks.
  base::CancelableTaskTracker cancelable_task_tracker_;

  // Map that stores favicon per URL.
  URLFaviconMap favicons_map_;

  // Path where html output is stored.
  base::FilePath path_;

  bookmark_html_writer::BookmarksExportCallback callback_;
};

// Class responsible for the actual writing.
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)) {
    // BookmarkModel isn't thread safe (nor would we want to lock it down
    // for the duration of the write), as such we make a copy of the
    // BookmarkModel using BookmarkCodec then write from that.
    BookmarkCodec codec;
    local_bookmarks_ =
        codec.Encode(model->bookmark_bar_node(), model->other_node(),
                     model->mobile_node(), /*sync_metadata_str=*/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(), /*sync_metadata_str=*/std::string());
    } else {
      CHECK(!model->account_other_node());
      CHECK(!model->account_mobile_node());
    }
  }

  Writer(const Writer&) = delete;
  Writer& operator=(const Writer&) = delete;

  // Writing bookmarks and favicons data to file.
  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();

    // Bookmarks are written with the following hierarchy - note the descendents
    // of the other and mobile folders are shifted up one level (compared to the
    // descendents of the bookmark bar). This is for compatibility with the
    // pre-existing file format user by other browsers.
    //
    // - Bookmarks bar (with PERSONAL_TOOLBAR_FOLDER="true" attribute)
    //   - All descendants of the local bookmark bar
    //   - All descendants of the account bookmark bar
    // - All descendants of the local other bookmarks folder
    // - All descendants of the account other bookmarks folder
    // - All descendants of the local mobile bookmarks folder
    // - All descendants of the account mobile bookmarks folder

    // Add the bookmark bar folder, and local descendants.
    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;
    }

    // Add account bookmark bar descendants if they exist.
    if (account_bookmark_bar_folder_value &&
        !WriteDescendants(*account_bookmark_bar_folder_value)) {
      return bookmark_html_writer::Result::kCouldNotWriteNodes;
    }

    // Close the bookmark bar folder.
    if (!WriteFolderEnd()) {
      return bookmark_html_writer::Result::kCouldNotWriteNodes;
    }

    // Add the other bookmarks descendants: local, then account if they exist.
    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;
    }

    // Add the mobile bookmarks descendants: local, then account if they exist.
    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 close is forced so that unit test could read it.
    file_.reset();

    return bookmark_html_writer::Result::kSuccess;
  }

 private:
  friend class base::RefCountedThreadSafe<Writer>;

  // Types of text being written out. The type dictates how the text is
  // escaped.
  enum TextType {
    // The text is the value of an html attribute, eg foo in
    // <a href="foo">.
    ATTRIBUTE_VALUE,

    // Actual content, eg foo in <h1>foo</h2>.
    CONTENT
  };

  ~Writer() = default;

  // Get the latest time of a given type, across a list of folders.
  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);
  }

  // Opens the file, returning true on success.
  [[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;
  }

  // Increments the indent.
  void IncrementIndent() { indent_.resize(indent_.size() + kIndentSize, ' '); }

  // Decrements the indent.
  void DecrementIndent() {
    CHECK(!indent_.empty());
    indent_.resize(indent_.size() - kIndentSize, ' ');
  }

  // Writes raw text out returning true on success. This does not escape
  // the text in anyway.
  [[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;
  }

  // Writes out the text string (as UTF8). The text is escaped based on
  // type.
  [[nodiscard]] bool Write(const std::string& text, TextType type) {
    DCHECK(base::IsStringUTF8(text));
    std::string utf8_string;

    switch (type) {
      case ATTRIBUTE_VALUE:
        // Convert " to &quot;
        utf8_string = text;
        base::ReplaceSubstringsAfterOffset(&utf8_string, 0, "\"", "&quot;");
        break;

      case CONTENT:
        utf8_string = base::EscapeForHTML(text);
        break;

      default:
        NOTREACHED();
    }

    return Write(utf8_string);
  }

  // Indents the current line.
  [[nodiscard]] bool WriteIndent() { return Write(indent_); }

  // Converts a time string written to the JSON codec into a time_t string
  // (used by bookmarks.html) and writes it.
  [[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()));
  }

  // Writes the start of a folder section, ready for subsequent calls to write
  // out children of the folder. `value` is the folder to be written, which must
  // be of `folder_type` either `BOOKMARK_BAR` or `FOLDER`.
  [[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;
  }

  // Writes the child nodes of folder `folder` (this does not include writing
  // the folder itself).
  [[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;
  }

  // Writes the end of a folder section that was previously created with
  // `WriteFolderStart()`.
  [[nodiscard]] bool WriteFolderEnd() {
    DecrementIndent();
    return WriteIndent() && Write(kFolderChildrenEnd) && Write(kNewline);
  }

  // Writes the node and all its children, returning true on success.
  [[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;
    }

    // Folder.
    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;
  }

  // The BookmarkModel as a base::Value, split into local and account bookmarks.
  // These values were generated from the BookmarkCodec.
  base::Value::Dict local_bookmarks_;
  base::Value::Dict account_bookmarks_;

  // Path we're writing to.
  base::FilePath path_;

  // Map that stores favicon per URL.
  BookmarkFaviconFetcher::URLFaviconMap favicons_map_;

  // File we're writing to.
  std::unique_ptr<base::File> file_;

  // How much we indent when writing a bookmark/folder. This is modified
  // via IncrementIndent and DecrementIndent.
  std::string indent_;
};

}  // namespace

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()) {
    // There are bookmarks for which to fetch favicons, and the favicon map is
    // empty (since it was just created). There is therefore async work to do.
    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);
  // |this| is deleted!
}

bool BookmarkFaviconFetcher::FetchNextFavicon() {
  if (bookmark_urls_.empty()) {
    return false;
  }
  do {
    std::string url = bookmark_urls_.front();
    // Filter out urls that we've already got favicon for.
    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,
          /*fallback_to_host=*/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) {
  // We allow only one concurrent bookmark export operation per profile.
  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();
}

}  // namespace bookmark_html_writer