blob: 44b669120b26c559edd01e79136a8e37dd598137 [file]
// Copyright 2018 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/model_loader.h"
#include <optional>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/hash/hash.h"
#include "base/json/json_reader.h"
#include "base/json/json_string_value_serializer.h"
#include "base/numerics/clamped_math.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/bind_post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/bookmarks/browser/bookmark_codec.h"
#include "components/bookmarks/browser/bookmark_load_details.h"
#include "components/bookmarks/browser/titled_url_index.h"
#include "components/bookmarks/browser/url_index.h"
#include "components/bookmarks/common/bookmark_features.h"
#include "components/bookmarks/common/bookmark_metrics.h"
#include "components/bookmarks/common/storage_file_encryption_type.h"
#include "components/bookmarks/common/url_load_stats.h"
#include "components/bookmarks/common/user_folder_load_stats.h"
#include "components/os_crypt/async/common/encryptor.h"
namespace bookmarks {
namespace {
const base::FilePath::CharType kBackupExtension[] = FILE_PATH_LITERAL("bak");
// Loads a JSON file determined by `file_path` and returns its content as a
// string or an error code if something fails.
// No kSuccess result is returned in case of success, it is implied by having a
// string returned.
// This function does not parse or decode the bookmarks data, so
// kJSONParsingFailed and kBookmarkCodecDecodingFailed aren't possible return
// values.
base::expected<std::string, metrics::BookmarksFileLoadResult> ReadFile(
scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
encryptor,
StorageFileEncryptionType encryption_type,
const base::FilePath& file_path,
metrics::StorageFileForUma storage_file_for_uma) {
const base::TimeTicks start_time = base::TimeTicks::Now();
if (!base::PathExists(file_path)) {
return base::unexpected(metrics::BookmarksFileLoadResult::kFileMissing);
}
std::string json_string;
if (!base::ReadFileToString(file_path, &json_string)) {
return base::unexpected(
metrics::BookmarksFileLoadResult::kContentLoadingFailed);
}
if (encryption_type == StorageFileEncryptionType::kClearText) {
metrics::RecordTimeToReadFile(storage_file_for_uma, encryption_type,
base::TimeTicks::Now() - start_time);
return json_string;
}
CHECK(encryptor);
std::string decrypted_json_string;
if (!encryptor->data.DecryptString(json_string, &decrypted_json_string)) {
return base::unexpected(
metrics::BookmarksFileLoadResult::kDecryptionFailed);
}
metrics::RecordTimeToReadFile(storage_file_for_uma, encryption_type,
base::TimeTicks::Now() - start_time);
return decrypted_json_string;
}
// Deserializes the given JSON string. Returns it in the form of a dictionary,
// or the kJSONParsingFailed error code if something fails.
base::expected<base::DictValue, metrics::BookmarksFileLoadResult>
DeserializeStringToDict(std::string_view json_string) {
// Titles may end up containing invalid utf and we shouldn't throw away
// all bookmarks if some titles have invalid utf.
JSONStringValueDeserializer deserializer(
json_string, base::JSON_REPLACE_INVALID_CHARACTERS);
std::unique_ptr<base::Value> root = deserializer.Deserialize(
/*error_code=*/nullptr, /*error_message=*/nullptr);
if (!root || !root->is_dict()) {
return base::unexpected(
metrics::BookmarksFileLoadResult::kJSONParsingFailed);
}
return std::move(*root).TakeDict();
}
void ReadBookmarksInSecondaryFileAndVerifyHashOnBackgroundSequence(
size_t json_string_hash,
scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
encryptor,
StorageFileEncryptionType encryption_type,
const base::FilePath secondary_file_path,
metrics::StorageFileForUma storage_file_for_uma,
ModelLoader::SaveSingleFileCallback save_single_file_callback) {
base::expected<std::string, metrics::BookmarksFileLoadResult>
secondary_json_string =
ReadFile(encryptor, encryption_type, secondary_file_path,
storage_file_for_uma);
metrics::RecordBookmarksFileLoadResult(
storage_file_for_uma, encryption_type,
secondary_json_string.error_or(
metrics::BookmarksFileLoadResult::kSuccess));
bool file_matches = false;
if (secondary_json_string.has_value()) {
const size_t secondary_json_string_hash =
base::FastHash(secondary_json_string.value());
file_matches = json_string_hash == secondary_json_string_hash;
metrics::RecordEncryptedBookmarksFileMatchesResult(storage_file_for_uma,
file_matches);
}
if (!file_matches) {
std::move(save_single_file_callback).Run(encryption_type);
}
}
void MaybeScheduleReadBookmarksInSecondaryFileAndVerifyHash(
std::string_view json_string,
const scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
encryptor,
StorageFileEncryptionType encryption_type,
const base::FilePath& secondary_file_path,
metrics::StorageFileForUma storage_file_for_uma,
ModelLoader::SaveSingleFileCallback save_single_file_callback) {
if (!ShouldVerifyBookmarksDataInSecondaryFileOnLoad()) {
return;
}
CHECK(encryptor);
CHECK(!secondary_file_path.empty());
const size_t json_string_hash = base::FastHash(json_string);
// Validate the encrypted data on a different task in order not to impact the
// bookmarks load time.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
&ReadBookmarksInSecondaryFileAndVerifyHashOnBackgroundSequence,
json_string_hash, encryptor, encryption_type, secondary_file_path,
storage_file_for_uma, std::move(save_single_file_callback)));
}
struct StorageFileReadConfig {
// The type of file content that should be used as the primary source.
StorageFileEncryptionType primary_source_encryption_type;
// Whether the encrypted file was supposed to be used but was missing so need
// to fall back to the clear text file.
bool is_clear_text_fallback;
};
StorageFileReadConfig DetermineStorageFileReadConfig(
const base::FilePath& clear_text_file_path,
const base::FilePath& encrypted_file_path) {
if (!ShouldUseEncryptedBookmarksAsPrimarySource()) {
return {StorageFileEncryptionType::kClearText,
/*is_clear_text_fallback=*/false};
}
// Fallback to clear text if the encrypted file is missing and clear text
// file exists.
if (!base::PathExists(encrypted_file_path) &&
base::PathExists(clear_text_file_path)) {
return {StorageFileEncryptionType::kClearText,
/*is_clear_text_fallback=*/true};
}
return {StorageFileEncryptionType::kEncrypted,
/*is_clear_text_fallback=*/false};
}
void OnFileLoaded(
metrics::StorageFileForUma storage_file_for_uma,
StorageFileEncryptionType encryption_type,
StorageFileReadConfig storage_file_read_config,
ModelLoader::SaveSingleFileCallback& save_single_file_callback,
metrics::BookmarksFileLoadResult result) {
if (storage_file_read_config.is_clear_text_fallback) {
metrics::RecordFallbackToClearTextFileOnLoadResult(storage_file_for_uma,
result);
// We were supposed to use the encrypted file but it is missing.
// Recreate it on a different task after model loading is complete.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(save_single_file_callback),
StorageFileEncryptionType::kEncrypted));
} else {
metrics::RecordBookmarksFileLoadResult(storage_file_for_uma,
encryption_type, result);
}
}
void MaybeCleanUpFiles(StorageFileEncryptionType primary_source_encryption_type,
metrics::StorageFileForUma storage_file_for_uma,
const base::FilePath& clear_text_file_path) {
if (!ShouldDeleteClearTextBookmarksFile() ||
primary_source_encryption_type == StorageFileEncryptionType::kClearText) {
return;
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
[](metrics::StorageFileForUma storage_file_for_uma,
const base::FilePath& clear_text_file_path) {
bool deleted =
base::DeleteFile(clear_text_file_path) &&
base::DeleteFile(
clear_text_file_path.ReplaceExtension(kBackupExtension));
metrics::RecordClearTextFileDeletionResult(storage_file_for_uma,
deleted);
},
storage_file_for_uma, clear_text_file_path));
}
std::unique_ptr<BookmarkLoadDetails> LoadBookmarks(
const scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
encryptor,
const base::FilePath& local_or_syncable_file_path,
const base::FilePath& encrypted_local_or_syncable_file_path,
const base::FilePath& account_file_path,
const base::FilePath& encrypted_account_file_path,
ModelLoader::SaveSingleFileCallback
save_local_or_syncable_single_file_callback,
ModelLoader::SaveSingleFileCallback save_account_single_file_callback) {
auto details = std::make_unique<BookmarkLoadDetails>();
std::set<int64_t> ids_assigned_to_account_nodes;
// Decode account bookmarks (if any). Doing this before decoding
// local-or-syncable ones is interesting because, in case there are ID
// collisions, it will lead to ID reassignments on the local-or-syncable part,
// which is usually harmless. Doing the opposite would imply that account
// bookmarks need to be redownloaded from the server (because ID reassignment
// leads to invalidating sync metadata). This is particularly interesting on
// iOS, in case the files were written by two independent BookmarkModel
// instances (and hence the two files are prone to ID collisions) and later
// loaded into a single BookmarkModel instance.
if (!account_file_path.empty()) {
std::string sync_metadata_str;
int64_t max_node_id = 0;
const StorageFileReadConfig account_storage_file_read_config =
DetermineStorageFileReadConfig(account_file_path,
encrypted_account_file_path);
const StorageFileEncryptionType account_encryption_type =
account_storage_file_read_config.primary_source_encryption_type;
std::unique_ptr<BookmarkPermanentNode> account_bb_node =
BookmarkPermanentNode::CreateBookmarkBar(0, /*is_account_node=*/true);
std::unique_ptr<BookmarkPermanentNode> account_other_folder_node =
BookmarkPermanentNode::CreateOtherBookmarks(0,
/*is_account_node=*/true);
std::unique_ptr<BookmarkPermanentNode> account_mobile_folder_node =
BookmarkPermanentNode::CreateMobileBookmarks(0,
/*is_account_node=*/true);
const base::expected<std::string, metrics::BookmarksFileLoadResult>
json_string = ReadFile(
encryptor, account_encryption_type,
account_encryption_type == StorageFileEncryptionType::kEncrypted
? encrypted_account_file_path
: account_file_path,
metrics::StorageFileForUma::kAccount);
base::expected<base::DictValue, metrics::BookmarksFileLoadResult>
root_dict = json_string.and_then(DeserializeStringToDict);
BookmarkCodec codec;
if (!root_dict.has_value()) {
OnFileLoaded(metrics::StorageFileForUma::kAccount,
account_encryption_type, account_storage_file_read_config,
save_account_single_file_callback, root_dict.error());
} else if (codec.Decode(*root_dict, /*already_assigned_ids=*/{},
account_bb_node.get(),
account_other_folder_node.get(),
account_mobile_folder_node.get(), &max_node_id,
&sync_metadata_str)) {
ids_assigned_to_account_nodes = codec.release_assigned_ids();
// A successful decoding must have set proper IDs.
CHECK_NE(0, account_bb_node->id());
CHECK_NE(0, account_other_folder_node->id());
CHECK_NE(0, account_mobile_folder_node->id());
details->AddAccountPermanentNodes(std::move(account_bb_node),
std::move(account_other_folder_node),
std::move(account_mobile_folder_node));
details->set_account_sync_metadata_str(std::move(sync_metadata_str));
details->set_max_id(std::max(max_node_id, details->max_id()));
details->set_ids_reassigned(details->ids_reassigned() ||
codec.ids_reassigned());
details->set_required_recovery(details->required_recovery() ||
codec.required_recovery());
// Record metrics that indicate whether or not IDs were reassigned for
// account bookmarks.
metrics::RecordIdsReassignedOnProfileLoad(
metrics::StorageFileForUma::kAccount, codec.ids_reassigned());
OnFileLoaded(metrics::StorageFileForUma::kAccount,
account_encryption_type, account_storage_file_read_config,
save_account_single_file_callback,
metrics::BookmarksFileLoadResult::kSuccess);
if (!account_storage_file_read_config.is_clear_text_fallback) {
CHECK(save_account_single_file_callback);
const StorageFileEncryptionType secondary_account_encryption_type =
account_encryption_type == StorageFileEncryptionType::kEncrypted
? StorageFileEncryptionType::kClearText
: StorageFileEncryptionType::kEncrypted;
MaybeScheduleReadBookmarksInSecondaryFileAndVerifyHash(
json_string.value(), encryptor, secondary_account_encryption_type,
secondary_account_encryption_type ==
StorageFileEncryptionType::kEncrypted
? encrypted_account_file_path
: account_file_path,
metrics::StorageFileForUma::kAccount,
std::move(save_account_single_file_callback));
MaybeCleanUpFiles(account_encryption_type,
metrics::StorageFileForUma::kAccount,
account_file_path);
}
} else {
// In the failure case, it is still possible that sync metadata was
// decoded, which includes legit scenarios like sync metadata indicating
// that there were too many bookmarks in sync, server-side.
details->set_account_sync_metadata_str(std::move(sync_metadata_str));
OnFileLoaded(
metrics::StorageFileForUma::kAccount, account_encryption_type,
account_storage_file_read_config, save_account_single_file_callback,
metrics::BookmarksFileLoadResult::kBookmarkCodecDecodingFailed);
}
}
// Decode local-or-syncable bookmarks.
{
const StorageFileReadConfig local_or_syncable_storage_file_read_config =
DetermineStorageFileReadConfig(local_or_syncable_file_path,
encrypted_local_or_syncable_file_path);
const StorageFileEncryptionType local_or_syncable_encryption_type =
local_or_syncable_storage_file_read_config
.primary_source_encryption_type;
std::string sync_metadata_str;
int64_t max_node_id = 0;
const base::expected<std::string, metrics::BookmarksFileLoadResult>
json_string = ReadFile(encryptor, local_or_syncable_encryption_type,
local_or_syncable_encryption_type ==
StorageFileEncryptionType::kEncrypted
? encrypted_local_or_syncable_file_path
: local_or_syncable_file_path,
metrics::StorageFileForUma::kLocalOrSyncable);
base::expected<base::DictValue, metrics::BookmarksFileLoadResult>
root_dict = json_string.and_then(DeserializeStringToDict);
BookmarkCodec codec;
if (!root_dict.has_value()) {
OnFileLoaded(metrics::StorageFileForUma::kLocalOrSyncable,
local_or_syncable_encryption_type,
local_or_syncable_storage_file_read_config,
save_local_or_syncable_single_file_callback,
root_dict.error());
} else if (codec.Decode(*root_dict,
std::move(ids_assigned_to_account_nodes),
details->bb_node(), details->other_folder_node(),
details->mobile_folder_node(), &max_node_id,
&sync_metadata_str)) {
details->set_local_or_syncable_sync_metadata_str(
std::move(sync_metadata_str));
details->set_max_id(std::max(max_node_id, details->max_id()));
details->set_ids_reassigned(details->ids_reassigned() ||
codec.ids_reassigned());
details->set_required_recovery(details->required_recovery() ||
codec.required_recovery());
details->set_local_or_syncable_reassigned_ids_per_old_id(
codec.release_reassigned_ids_per_old_id());
// Record metrics that indicate whether or not IDs were reassigned for
// local-or-syncable bookmarks.
metrics::RecordIdsReassignedOnProfileLoad(
metrics::StorageFileForUma::kLocalOrSyncable, codec.ids_reassigned());
OnFileLoaded(metrics::StorageFileForUma::kLocalOrSyncable,
local_or_syncable_encryption_type,
local_or_syncable_storage_file_read_config,
save_local_or_syncable_single_file_callback,
metrics::BookmarksFileLoadResult::kSuccess);
if (!local_or_syncable_storage_file_read_config.is_clear_text_fallback) {
CHECK(save_local_or_syncable_single_file_callback);
const StorageFileEncryptionType
secondary_local_or_syncable_encryption_type =
local_or_syncable_encryption_type ==
StorageFileEncryptionType::kEncrypted
? StorageFileEncryptionType::kClearText
: StorageFileEncryptionType::kEncrypted;
MaybeScheduleReadBookmarksInSecondaryFileAndVerifyHash(
json_string.value(), encryptor,
secondary_local_or_syncable_encryption_type,
secondary_local_or_syncable_encryption_type ==
StorageFileEncryptionType::kEncrypted
? encrypted_local_or_syncable_file_path
: local_or_syncable_file_path,
metrics::StorageFileForUma::kLocalOrSyncable,
std::move(save_local_or_syncable_single_file_callback));
MaybeCleanUpFiles(local_or_syncable_encryption_type,
metrics::StorageFileForUma::kLocalOrSyncable,
local_or_syncable_file_path);
}
} else {
OnFileLoaded(
metrics::StorageFileForUma::kLocalOrSyncable,
local_or_syncable_encryption_type,
local_or_syncable_storage_file_read_config,
save_local_or_syncable_single_file_callback,
metrics::BookmarksFileLoadResult::kBookmarkCodecDecodingFailed);
}
}
return details;
}
void LoadManagedNode(LoadManagedNodeCallback load_managed_node_callback,
BookmarkLoadDetails& details) {
if (!load_managed_node_callback) {
return;
}
int64_t max_node_id = details.max_id();
std::unique_ptr<BookmarkPermanentNode> managed_node =
std::move(load_managed_node_callback).Run(&max_node_id);
if (managed_node) {
details.AddManagedNode(std::move(managed_node));
details.set_max_id(std::max(max_node_id, details.max_id()));
}
}
uint64_t GetFileSizeOrZero(const base::FilePath& file_path) {
return base::GetFileSize(file_path).value_or(0);
}
void RecordLoadMetrics(
BookmarkLoadDetails* details,
const base::FilePath& local_or_syncable_file_path,
const base::FilePath& encrypted_local_or_syncable_file_path,
const base::FilePath& account_file_path,
const base::FilePath& encrypted_account_file_path) {
UrlLoadStats url_stats = details->url_index()->ComputeStats();
metrics::RecordUrlLoadStatsOnProfileLoad(url_stats);
UserFolderLoadStats user_folder_stats = details->ComputeUserFolderStats();
metrics::RecordUserFolderLoadStatsOnProfileLoad(user_folder_stats);
// Only record clear text file size metrics when clear text file is used.
if (!ShouldUseEncryptedBookmarksAsPrimarySource() ||
ShouldVerifyBookmarksDataInSecondaryFileOnLoad()) {
const uint64_t local_or_syncable_file_size =
GetFileSizeOrZero(local_or_syncable_file_path);
const uint64_t account_file_size =
account_file_path.empty() ? 0U : GetFileSizeOrZero(account_file_path);
if (local_or_syncable_file_size != 0) {
metrics::RecordFileSizeAtStartup(StorageFileEncryptionType::kClearText,
local_or_syncable_file_size);
}
if (account_file_size != 0) {
metrics::RecordFileSizeAtStartup(StorageFileEncryptionType::kClearText,
account_file_size);
}
metrics::RecordAverageNodeSizeAtStartupIfNonZero(
StorageFileEncryptionType::kClearText,
url_stats.total_url_bookmark_count,
local_or_syncable_file_size + account_file_size);
}
// Only record encrypted file size metrics when encrypted file is used.
if (ShouldUseEncryptedBookmarksAsPrimarySource() ||
ShouldVerifyBookmarksDataInSecondaryFileOnLoad()) {
const uint64_t encrypted_local_or_syncable_file_size =
GetFileSizeOrZero(encrypted_local_or_syncable_file_path);
if (encrypted_local_or_syncable_file_size != 0) {
metrics::RecordFileSizeAtStartup(StorageFileEncryptionType::kEncrypted,
encrypted_local_or_syncable_file_size);
}
const uint64_t encrypted_account_file_size =
GetFileSizeOrZero(encrypted_account_file_path);
if (encrypted_account_file_size != 0) {
metrics::RecordFileSizeAtStartup(StorageFileEncryptionType::kEncrypted,
encrypted_account_file_size);
}
metrics::RecordAverageNodeSizeAtStartupIfNonZero(
StorageFileEncryptionType::kEncrypted,
url_stats.total_url_bookmark_count,
encrypted_local_or_syncable_file_size + encrypted_account_file_size);
}
}
} // namespace
// static
scoped_refptr<ModelLoader> ModelLoader::Create(
scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
encryptor,
const base::FilePath& local_or_syncable_file_path,
const base::FilePath& encrypted_local_or_syncable_file_path,
const base::FilePath& account_file_path,
const base::FilePath& encrypted_account_file_path,
LoadManagedNodeCallback load_managed_node_callback,
SaveSingleFileCallback save_local_or_syncable_single_file_callback,
SaveSingleFileCallback save_account_single_file_callback,
LoadCallback callback) {
CHECK(!local_or_syncable_file_path.empty());
// Note: base::MakeRefCounted is not available here, as ModelLoader's
// constructor is private.
auto model_loader = base::WrapRefCounted(new ModelLoader());
model_loader->backend_task_runner_ =
base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN});
auto save_local_or_syncable_single_file_callback_on_main_sequence =
base::BindPostTaskToCurrentDefault(
std::move(save_local_or_syncable_single_file_callback));
auto save_account_single_file_callback_on_main_sequence =
base::BindPostTaskToCurrentDefault(
std::move(save_account_single_file_callback));
model_loader->backend_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(
&ModelLoader::DoLoadOnBackgroundThread, model_loader, encryptor,
local_or_syncable_file_path, encrypted_local_or_syncable_file_path,
account_file_path, encrypted_account_file_path,
std::move(
save_local_or_syncable_single_file_callback_on_main_sequence),
std::move(save_account_single_file_callback_on_main_sequence),
std::move(load_managed_node_callback)),
std::move(callback));
return model_loader;
}
void ModelLoader::BlockTillLoaded() {
loaded_signal_.Wait();
}
ModelLoader::ModelLoader()
: loaded_signal_(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED) {}
ModelLoader::~ModelLoader() = default;
std::unique_ptr<BookmarkLoadDetails> ModelLoader::DoLoadOnBackgroundThread(
const scoped_refptr<base::RefCountedData<const os_crypt_async::Encryptor>>
encryptor,
const base::FilePath& local_or_syncable_file_path,
const base::FilePath& encrypted_local_or_syncable_file_path,
const base::FilePath& account_file_path,
const base::FilePath& encrypted_account_file_path,
SaveSingleFileCallback save_local_or_syncable_single_file_callback,
SaveSingleFileCallback save_account_single_file_callback,
LoadManagedNodeCallback load_managed_node_callback) {
std::unique_ptr<BookmarkLoadDetails> details =
LoadBookmarks(encryptor, local_or_syncable_file_path,
encrypted_local_or_syncable_file_path, account_file_path,
encrypted_account_file_path,
std::move(save_local_or_syncable_single_file_callback),
std::move(save_account_single_file_callback));
CHECK(details);
details->PopulateNodeIdsForLocalOrSyncablePermanentNodes();
LoadManagedNode(std::move(load_managed_node_callback), *details);
// Building the indices can take a while so it's done on the background
// thread.
details->CreateIndices();
RecordLoadMetrics(details.get(), local_or_syncable_file_path,
encrypted_local_or_syncable_file_path, account_file_path,
encrypted_account_file_path);
history_bookmark_model_ = details->url_index();
loaded_signal_.Signal();
return details;
}
// static
scoped_refptr<ModelLoader> ModelLoader::CreateForTest(
LoadManagedNodeCallback load_managed_node_callback,
BookmarkLoadDetails* details) {
CHECK(details);
details->PopulateNodeIdsForLocalOrSyncablePermanentNodes();
LoadManagedNode(std::move(load_managed_node_callback), *details);
details->CreateIndices();
// Note: base::MakeRefCounted is not available here, as ModelLoader's
// constructor is private.
auto model_loader = base::WrapRefCounted(new ModelLoader());
model_loader->history_bookmark_model_ = details->url_index();
model_loader->loaded_signal_.Signal();
return model_loader;
}
} // namespace bookmarks