blob: 935b76d05ddf6c1e56b979a03ec1dbf8b76b2685 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/sync_sessions/session_store.h"
#include <stdint.h>
#include <algorithm>
#include <set>
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/pickle.h"
#include "base/strings/stringprintf.h"
#include "base/trace_event/trace_event.h"
#include "components/sync/base/time.h"
#include "components/sync/device_info/device_info.h"
#include "components/sync/device_info/device_info_util.h"
#include "components/sync/model/entity_change.h"
#include "components/sync/model/metadata_batch.h"
#include "components/sync/model/mutable_data_batch.h"
#include "components/sync/protocol/model_type_state.pb.h"
#include "components/sync/protocol/sync.pb.h"
#include "components/sync_sessions/session_sync_prefs.h"
#include "components/sync_sessions/sync_sessions_client.h"
namespace sync_sessions {
namespace {
using sync_pb::SessionSpecifics;
using syncer::MetadataChangeList;
using syncer::ModelTypeStore;
std::string TabNodeIdToClientTag(const std::string& session_tag,
int tab_node_id) {
CHECK_GT(tab_node_id, TabNodePool::kInvalidTabNodeID);
return base::StringPrintf("%s %d", session_tag.c_str(), tab_node_id);
}
std::string EncodeStorageKey(const std::string& session_tag, int tab_node_id) {
base::Pickle pickle;
pickle.WriteString(session_tag);
pickle.WriteInt(tab_node_id);
return std::string(static_cast<const char*>(pickle.data()), pickle.size());
}
bool DecodeStorageKey(const std::string& storage_key,
std::string* session_tag,
int* tab_node_id) {
base::Pickle pickle(storage_key.c_str(), storage_key.size());
base::PickleIterator iter(pickle);
if (!iter.ReadString(session_tag)) {
return false;
}
if (!iter.ReadInt(tab_node_id)) {
return false;
}
return true;
}
std::unique_ptr<syncer::EntityData> MoveToEntityData(
const std::string& client_name,
SessionSpecifics* specifics) {
auto entity_data = std::make_unique<syncer::EntityData>();
entity_data->non_unique_name = client_name;
if (specifics->has_header()) {
entity_data->non_unique_name += " (header)";
} else if (specifics->has_tab()) {
entity_data->non_unique_name +=
base::StringPrintf(" (tab node %d)", specifics->tab_node_id());
}
entity_data->specifics.mutable_session()->Swap(specifics);
return entity_data;
}
std::string GetSessionTagWithPrefs(const std::string& cache_guid,
SessionSyncPrefs* sync_prefs) {
DCHECK(sync_prefs);
const std::string persisted_guid = sync_prefs->GetSyncSessionsGUID();
if (!persisted_guid.empty()) {
DVLOG(1) << "Restoring persisted session sync guid: " << persisted_guid;
return persisted_guid;
}
const std::string new_guid =
base::StringPrintf("session_sync%s", cache_guid.c_str());
DVLOG(1) << "Creating session sync guid: " << new_guid;
sync_prefs->SetSyncSessionsGUID(new_guid);
return new_guid;
}
void ForwardError(syncer::OnceModelErrorHandler error_handler,
const base::Optional<syncer::ModelError>& error) {
if (error) {
std::move(error_handler).Run(*error);
}
}
} // namespace
// static
void SessionStore::Open(
const syncer::DeviceInfo& device_info,
const RestoredForeignTabCallback& restored_foreign_tab_callback,
SyncSessionsClient* sessions_client,
OpenCallback callback) {
DCHECK(sessions_client);
DCHECK(!device_info.guid().empty());
SessionStore::SessionInfo session_info;
session_info.client_name = device_info.client_name();
session_info.device_type = device_info.device_type();
session_info.session_tag = GetSessionTagWithPrefs(
device_info.guid(), sessions_client->GetSessionSyncPrefs());
DVLOG(1) << "Opening session store";
// WrapUnique() used because constructor is private.
auto session_store = base::WrapUnique(new SessionStore(
session_info, restored_foreign_tab_callback, sessions_client));
sessions_client->GetStoreFactory().Run(
syncer::SESSIONS,
base::BindOnce(&OnStoreCreated, std::move(session_store),
std::move(callback)));
}
SessionStore::WriteBatch::WriteBatch(
std::unique_ptr<ModelTypeStore::WriteBatch> batch,
CommitCallback commit_cb,
syncer::OnceModelErrorHandler error_handler,
SyncedSessionTracker* session_tracker)
: batch_(std::move(batch)),
commit_cb_(std::move(commit_cb)),
error_handler_(std::move(error_handler)),
session_tracker_(session_tracker) {
DCHECK(batch_);
DCHECK(commit_cb_);
DCHECK(error_handler_);
DCHECK(session_tracker_);
}
SessionStore::WriteBatch::~WriteBatch() {
DCHECK(!batch_) << "Destructed without prior commit";
}
std::string SessionStore::WriteBatch::PutAndUpdateTracker(
const sync_pb::SessionSpecifics& specifics,
base::Time modification_time) {
UpdateTrackerWithSpecifics(specifics, modification_time, session_tracker_);
return PutWithoutUpdatingTracker(specifics);
}
std::vector<std::string>
SessionStore::WriteBatch::DeleteForeignEntityAndUpdateTracker(
const std::string& storage_key) {
std::string session_tag;
int tab_node_id;
bool success = DecodeStorageKey(storage_key, &session_tag, &tab_node_id);
DCHECK(success);
DCHECK_NE(session_tag, session_tracker_->GetLocalSessionTag());
std::vector<std::string> deleted_storage_keys;
deleted_storage_keys.push_back(storage_key);
if (tab_node_id == TabNodePool::kInvalidTabNodeID) {
// Removal of a foreign header entity cascades the deletion of all tabs in
// the same session too.
for (int cascading_tab_node_id :
session_tracker_->LookupTabNodeIds(session_tag)) {
std::string tab_storage_key =
GetTabStorageKey(session_tag, cascading_tab_node_id);
// Note that DeleteForeignSession() below takes care of removing all tabs
// from the tracker, so no DeleteForeignTab() needed.
batch_->DeleteData(tab_storage_key);
deleted_storage_keys.push_back(std::move(tab_storage_key));
}
// Delete session itself.
session_tracker_->DeleteForeignSession(session_tag);
} else {
// Removal of a foreign tab entity.
session_tracker_->DeleteForeignTab(session_tag, tab_node_id);
}
batch_->DeleteData(storage_key);
return deleted_storage_keys;
}
std::string SessionStore::WriteBatch::PutWithoutUpdatingTracker(
const sync_pb::SessionSpecifics& specifics) {
DCHECK(AreValidSpecifics(specifics));
const std::string storage_key = GetStorageKey(specifics);
batch_->WriteData(storage_key, specifics.SerializeAsString());
return storage_key;
}
std::string SessionStore::WriteBatch::DeleteLocalTabWithoutUpdatingTracker(
int tab_node_id) {
std::string storage_key =
EncodeStorageKey(session_tracker_->GetLocalSessionTag(), tab_node_id);
batch_->DeleteData(storage_key);
return storage_key;
}
MetadataChangeList* SessionStore::WriteBatch::GetMetadataChangeList() {
return batch_->GetMetadataChangeList();
}
// static
void SessionStore::WriteBatch::Commit(std::unique_ptr<WriteBatch> batch) {
DCHECK(batch);
std::move(batch->commit_cb_)
.Run(std::move(batch->batch_),
base::BindOnce(&ForwardError, std::move(batch->error_handler_)));
}
// static
bool SessionStore::AreValidSpecifics(const SessionSpecifics& specifics) {
if (specifics.session_tag().empty()) {
return false;
}
if (specifics.has_tab()) {
return specifics.tab_node_id() >= 0 && specifics.tab().tab_id() > 0;
}
if (specifics.has_header()) {
// Verify that tab IDs appear only once within a header. Intended to prevent
// http://crbug.com/360822.
std::set<int> session_tab_ids;
for (const sync_pb::SessionWindow& window : specifics.header().window()) {
for (int tab_id : window.tab()) {
bool success = session_tab_ids.insert(tab_id).second;
if (!success) {
return false;
}
}
}
return !specifics.has_tab() &&
specifics.tab_node_id() == TabNodePool::kInvalidTabNodeID;
}
return false;
}
// static
std::string SessionStore::GetClientTag(const SessionSpecifics& specifics) {
DCHECK(AreValidSpecifics(specifics));
if (specifics.has_header()) {
return specifics.session_tag();
}
DCHECK(specifics.has_tab());
return TabNodeIdToClientTag(specifics.session_tag(), specifics.tab_node_id());
}
// static
std::string SessionStore::GetStorageKey(const SessionSpecifics& specifics) {
DCHECK(AreValidSpecifics(specifics));
return EncodeStorageKey(specifics.session_tag(), specifics.tab_node_id());
}
// static
std::string SessionStore::GetHeaderStorageKey(const std::string& session_tag) {
return EncodeStorageKey(session_tag, TabNodePool::kInvalidTabNodeID);
}
// static
std::string SessionStore::GetTabStorageKey(const std::string& session_tag,
int tab_node_id) {
DCHECK_GE(tab_node_id, 0);
return EncodeStorageKey(session_tag, tab_node_id);
}
bool SessionStore::StorageKeyMatchesLocalSession(
const std::string& storage_key) const {
std::string session_tag;
int tab_node_id;
bool success = DecodeStorageKey(storage_key, &session_tag, &tab_node_id);
DCHECK(success);
return session_tag == local_session_info_.session_tag;
}
// static
std::string SessionStore::GetTabClientTagForTest(const std::string& session_tag,
int tab_node_id) {
return TabNodeIdToClientTag(session_tag, tab_node_id);
}
// static
void SessionStore::OnStoreCreated(
std::unique_ptr<SessionStore> session_store,
OpenCallback callback,
const base::Optional<syncer::ModelError>& error,
std::unique_ptr<ModelTypeStore> underlying_store) {
if (error) {
std::move(callback).Run(error, /*store=*/nullptr,
/*metadata_batch=*/nullptr);
return;
}
DCHECK(underlying_store);
ModelTypeStore* underlying_store_copy = underlying_store.get();
underlying_store_copy->ReadAllMetadata(
base::BindOnce(&OnReadAllMetadata, std::move(session_store),
std::move(callback), std::move(underlying_store)));
}
// static
void SessionStore::OnReadAllMetadata(
std::unique_ptr<SessionStore> session_store,
OpenCallback callback,
std::unique_ptr<ModelTypeStore> underlying_store,
const base::Optional<syncer::ModelError>& error,
std::unique_ptr<syncer::MetadataBatch> metadata_batch) {
if (error) {
std::move(callback).Run(error, /*store=*/nullptr,
/*metadata_batch=*/nullptr);
return;
}
ModelTypeStore* underlying_store_copy = underlying_store.get();
underlying_store_copy->ReadAllData(base::BindOnce(
&OnReadAllData, std::move(session_store), std::move(callback),
std::move(underlying_store), std::move(metadata_batch)));
}
// static
void SessionStore::OnReadAllData(
std::unique_ptr<SessionStore> session_store,
OpenCallback callback,
std::unique_ptr<ModelTypeStore> underlying_store,
std::unique_ptr<syncer::MetadataBatch> metadata_batch,
const base::Optional<syncer::ModelError>& error,
std::unique_ptr<ModelTypeStore::RecordList> record_list) {
// Remove after fixing https://crbug.com/902203.
TRACE_EVENT0("browser", "OnReadAllMetadata");
if (error) {
std::move(callback).Run(error, /*store=*/nullptr,
/*metadata_batch=*/nullptr);
return;
}
std::map<std::string, sync_pb::SessionSpecifics> initial_data;
for (ModelTypeStore::Record& record : *record_list) {
const std::string& storage_key = record.id;
SessionSpecifics specifics;
if (storage_key.empty() ||
!specifics.ParseFromString(std::move(record.value))) {
DVLOG(1) << "Ignoring corrupt database entry with key: " << storage_key;
continue;
}
initial_data[storage_key].Swap(&specifics);
}
// We avoid initialization of the store if the callback was cancelled, in
// case dependencies (SessionSyncClient) are already destroyed, even though
// the current implementation doesn't seem to crash otherwise.
if (callback.IsCancelled()) {
return;
}
session_store->Init(std::move(underlying_store), std::move(initial_data),
metadata_batch->GetAllMetadata());
std::move(callback).Run(/*error=*/base::nullopt, std::move(session_store),
std::move(metadata_batch));
}
SessionStore::SessionStore(
const SessionInfo& local_session_info,
const RestoredForeignTabCallback& restored_foreign_tab_callback,
SyncSessionsClient* sessions_client)
: local_session_info_(local_session_info),
restored_foreign_tab_callback_(restored_foreign_tab_callback),
session_tracker_(sessions_client),
weak_ptr_factory_(this) {
session_tracker_.InitLocalSession(local_session_info_.session_tag,
local_session_info_.client_name,
local_session_info_.device_type);
}
void SessionStore::Init(
std::unique_ptr<ModelTypeStore> store,
std::map<std::string, sync_pb::SessionSpecifics> initial_data,
const syncer::EntityMetadataMap& initial_metadata) {
DCHECK(store);
store_ = std::move(store);
DVLOG(1) << "Initializing session store with " << initial_data.size()
<< " restored entities and " << initial_metadata.size()
<< " metadata entries.";
bool found_local_header = false;
for (auto& storage_key_and_specifics : initial_data) {
const std::string& storage_key = storage_key_and_specifics.first;
SessionSpecifics& specifics = storage_key_and_specifics.second;
// The store should not contain invalid data, but as a precaution we filter
// out anyway in case the persisted data is corrupted.
if (!AreValidSpecifics(specifics)) {
continue;
}
// Metadata should be available if data is available. If not, it means
// the local store is corrupt, because we delete all data and metadata
// at the same time (e.g. sync is disabled).
auto metadata_it = initial_metadata.find(storage_key);
if (metadata_it == initial_metadata.end()) {
continue;
}
const base::Time mtime =
syncer::ProtoTimeToTime(metadata_it->second.modification_time());
if (specifics.session_tag() != local_session_info_.session_tag) {
UpdateTrackerWithSpecifics(specifics, mtime, &session_tracker_);
// Notify listeners. In practice, this has the goal to load the URLs and
// visit times into the in-memory favicon cache.
if (specifics.has_tab()) {
restored_foreign_tab_callback_.Run(specifics.tab(), mtime);
}
} else if (specifics.has_header()) {
// This is previously stored local header information. Restoring the local
// is actually needed on Android only where we might not have a complete
// view of local window/tabs.
// Two local headers cannot coexist because they would use the very same
// storage key in ModelTypeStore/LevelDB.
DCHECK(!found_local_header);
found_local_header = true;
UpdateTrackerWithSpecifics(specifics, mtime, &session_tracker_);
DVLOG(1) << "Loaded local header.";
} else {
DCHECK(specifics.has_tab());
// This is a valid old tab node, add it to the tracker and associate
// it (using the new tab id).
DVLOG(1) << "Associating local tab " << specifics.tab().tab_id()
<< " with node " << specifics.tab_node_id();
// TODO(mastiz): Move call to ReassociateLocalTab() into
// UpdateTrackerWithSpecifics(), possibly merge with OnTabNodeSeen(). Also
// consider merging this branch with processing of foreign tabs above.
session_tracker_.ReassociateLocalTab(
specifics.tab_node_id(),
SessionID::FromSerializedValue(specifics.tab().tab_id()));
UpdateTrackerWithSpecifics(specifics, mtime, &session_tracker_);
}
}
// Cleanup all foreign sessions, since orphaned tabs may have been added after
// the header.
for (const SyncedSession* session :
session_tracker_.LookupAllForeignSessions(SyncedSessionTracker::RAW)) {
session_tracker_.CleanupSession(session->session_tag);
}
}
SessionStore::~SessionStore() = default;
std::unique_ptr<syncer::DataBatch> SessionStore::GetSessionDataForKeys(
const std::vector<std::string>& storage_keys) const {
// Decode |storage_keys| into a map that can be fed to
// SerializePartialTrackerToSpecifics().
std::map<std::string, std::set<int>> session_tag_to_node_ids;
for (const std::string& storage_key : storage_keys) {
std::string session_tag;
int tab_node_id;
bool success = DecodeStorageKey(storage_key, &session_tag, &tab_node_id);
DCHECK(success);
session_tag_to_node_ids[session_tag].insert(tab_node_id);
}
// Run the actual serialization into a data batch.
auto batch = std::make_unique<syncer::MutableDataBatch>();
SerializePartialTrackerToSpecifics(
session_tracker_, session_tag_to_node_ids,
base::BindRepeating(
[](syncer::MutableDataBatch* batch, const std::string& session_name,
sync_pb::SessionSpecifics* specifics) {
DCHECK(AreValidSpecifics(*specifics));
// Local variable used to avoid assuming argument evaluation order.
const std::string storage_key = GetStorageKey(*specifics);
batch->Put(storage_key, MoveToEntityData(session_name, specifics));
},
batch.get()));
return batch;
}
std::unique_ptr<syncer::DataBatch> SessionStore::GetAllSessionData() const {
auto batch = std::make_unique<syncer::MutableDataBatch>();
SerializeTrackerToSpecifics(
session_tracker_,
base::BindRepeating(
[](syncer::MutableDataBatch* batch, const std::string& session_name,
sync_pb::SessionSpecifics* specifics) {
DCHECK(AreValidSpecifics(*specifics));
// Local variable used to avoid assuming argument evaluation order.
const std::string storage_key = GetStorageKey(*specifics);
batch->Put(storage_key, MoveToEntityData(session_name, specifics));
},
batch.get()));
return batch;
}
std::unique_ptr<SessionStore::WriteBatch> SessionStore::CreateWriteBatch(
syncer::OnceModelErrorHandler error_handler) {
// The store is guaranteed to outlive WriteBatch instances (as per API
// requirement).
return std::make_unique<WriteBatch>(
store_->CreateWriteBatch(),
base::BindOnce(&ModelTypeStore::CommitWriteBatch,
base::Unretained(store_.get())),
std::move(error_handler), &session_tracker_);
}
void SessionStore::DeleteAllDataAndMetadata() {
session_tracker_.Clear();
store_->DeleteAllDataAndMetadata(base::DoNothing());
// At all times, the local session must be tracked.
session_tracker_.InitLocalSession(local_session_info_.session_tag,
local_session_info_.client_name,
local_session_info_.device_type);
}
} // namespace sync_sessions