diff options
author | akalin@chromium.org <akalin@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-04-21 09:59:42 +0000 |
---|---|---|
committer | akalin@chromium.org <akalin@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-04-21 09:59:42 +0000 |
commit | 91835ca82c6ff7f1aeb68e314a3842da1aca6841 (patch) | |
tree | c7e8119bd0fb2a8e1d6910c969e1d3f62dbff1f8 /sync/internal_api | |
parent | 7983f3bae6ab9c86aea81816e6bb1bd15ab114fe (diff) | |
download | chromium_src-91835ca82c6ff7f1aeb68e314a3842da1aca6841.zip chromium_src-91835ca82c6ff7f1aeb68e314a3842da1aca6841.tar.gz chromium_src-91835ca82c6ff7f1aeb68e314a3842da1aca6841.tar.bz2 |
[Sync] Move 'syncapi_core' and 'sync_unit_tests' targets to sync/
Also move related test files.
Lock down deps for sync/internal_api.
Clean up some deps on chrome/browser/sync.
BUG=117585
TEST=
Review URL: https://chromiumcodereview.appspot.com/10147003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@133349 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'sync/internal_api')
45 files changed, 10169 insertions, 0 deletions
diff --git a/sync/internal_api/DEPS b/sync/internal_api/DEPS new file mode 100644 index 0000000..955b31e --- /dev/null +++ b/sync/internal_api/DEPS @@ -0,0 +1,14 @@ +include_rules = [ + "+googleurl", + "+net/base/net_errors.h", + "+net/base/network_change_notifier.h", + "+net/http/http_status_code.h", + "+sync/engine", + "+sync/js", + "+sync/notifier", + "+sync/protocol", + "+sync/sessions", + "+sync/syncable", + "+sync/test", + "+sync/util", +] diff --git a/sync/internal_api/README b/sync/internal_api/README new file mode 100644 index 0000000..32987bb --- /dev/null +++ b/sync/internal_api/README @@ -0,0 +1,32 @@ +This file defines the "sync API", an interface to the syncer +backend that exposes (1) the core functionality of maintaining a consistent +local snapshot of a hierarchical object set; (2) a means to transactionally +access and modify those objects; (3) a means to control client/server +synchronization tasks, namely: pushing local object modifications to a +server, pulling nonlocal object modifications from a server to this client, +and resolving conflicts that may arise between the two; and (4) an +abstraction of some external functionality that is to be provided by the +host environment. + +This interface is used as the entry point into the syncer backend +when the backend is compiled as a library and embedded in another +application. A goal for this interface layer is to depend on very few +external types, so that an application can use the sync backend +without introducing a dependency on specific types. A non-goal is to +have binary compatibility across versions or compilers; this allows the +interface to use C++ classes. An application wishing to use the sync API +should ideally compile the syncer backend and this API as part of the +application's own build, to avoid e.g. mismatches in calling convention, +structure padding, or name mangling that could arise if there were a +compiler mismatch. + +The schema of the objects in the sync domain is based on the model, which +is essentially a hierarchy of items and folders similar to a filesystem, +but with a few important differences. The sync API contains fields +such as URL to easily allow the embedding application to store web +browser bookmarks. Also, the sync API allows duplicate titles in a parent. +Consequently, it does not support looking up an object by title +and parent, since such a lookup is not uniquely determined. Lastly, +unlike a filesystem model, objects in the Sync API model have a strict +ordering within a parent; the position is manipulable by callers, and +children of a node can be enumerated in the order of their position. diff --git a/sync/internal_api/all_status.cc b/sync/internal_api/all_status.cc new file mode 100644 index 0000000..89fe414 --- /dev/null +++ b/sync/internal_api/all_status.cc @@ -0,0 +1,167 @@ +// Copyright (c) 2012 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 "sync/internal_api/all_status.h" + +#include <algorithm> + +#include "base/logging.h" +#include "base/port.h" +#include "sync/engine/net/server_connection_manager.h" +#include "sync/protocol/service_constants.h" +#include "sync/sessions/session_state.h" +#include "sync/syncable/model_type.h" + +namespace browser_sync { + +AllStatus::AllStatus() { + status_.initial_sync_ended = true; + status_.notifications_enabled = false; + status_.cryptographer_ready = false; + status_.crypto_has_pending_keys = false; +} + +AllStatus::~AllStatus() { +} + +sync_api::SyncManager::Status AllStatus::CreateBlankStatus() const { + // Status is initialized with the previous status value. Variables + // whose values accumulate (e.g. lifetime counters like updates_received) + // are not to be cleared here. + sync_api::SyncManager::Status status = status_; + status.unsynced_count = 0; + status.encryption_conflicts = 0; + status.hierarchy_conflicts = 0; + status.simple_conflicts = 0; + status.server_conflicts = 0; + status.committed_count = 0; + status.initial_sync_ended = false; + status.updates_available = 0; + return status; +} + +sync_api::SyncManager::Status AllStatus::CalcSyncing( + const SyncEngineEvent &event) const { + sync_api::SyncManager::Status status = CreateBlankStatus(); + const sessions::SyncSessionSnapshot* snapshot = event.snapshot; + status.unsynced_count = static_cast<int>(snapshot->unsynced_count); + status.encryption_conflicts = snapshot->num_encryption_conflicts; + status.hierarchy_conflicts = snapshot->num_hierarchy_conflicts; + status.simple_conflicts = snapshot->num_simple_conflicts; + status.server_conflicts = snapshot->num_server_conflicts; + status.committed_count = snapshot->syncer_status.num_successful_commits; + + if (event.what_happened == SyncEngineEvent::SYNC_CYCLE_BEGIN) { + status.syncing = true; + } else if (event.what_happened == SyncEngineEvent::SYNC_CYCLE_ENDED) { + status.syncing = false; + } + + status.initial_sync_ended |= snapshot->is_share_usable; + + status.updates_available += snapshot->num_server_changes_remaining; + status.sync_protocol_error = snapshot->errors.sync_protocol_error; + + // Accumulate update count only once per session to avoid double-counting. + // TODO(ncarter): Make this realtime by having the syncer_status + // counter preserve its value across sessions. http://crbug.com/26339 + if (event.what_happened == SyncEngineEvent::SYNC_CYCLE_ENDED) { + status.updates_received += + snapshot->syncer_status.num_updates_downloaded_total; + status.tombstone_updates_received += + snapshot->syncer_status.num_tombstone_updates_downloaded_total; + status.reflected_updates_received += + snapshot->syncer_status.num_reflected_updates_downloaded_total; + status.num_local_overwrites_total += + snapshot->syncer_status.num_local_overwrites; + status.num_server_overwrites_total += + snapshot->syncer_status.num_server_overwrites; + if (snapshot->syncer_status.num_updates_downloaded_total == 0) { + ++status.empty_get_updates; + } else { + ++status.nonempty_get_updates; + } + if (snapshot->syncer_status.num_successful_commits == 0) { + ++status.sync_cycles_without_commits; + } else { + ++status.sync_cycles_with_commits; + } + if (snapshot->syncer_status.num_successful_commits == 0 && + snapshot->syncer_status.num_updates_downloaded_total == 0) { + ++status.useless_sync_cycles; + } else { + ++status.useful_sync_cycles; + } + } + return status; +} + +void AllStatus::OnSyncEngineEvent(const SyncEngineEvent& event) { + ScopedStatusLock lock(this); + switch (event.what_happened) { + case SyncEngineEvent::SYNC_CYCLE_BEGIN: + case SyncEngineEvent::STATUS_CHANGED: + case SyncEngineEvent::SYNC_CYCLE_ENDED: + status_ = CalcSyncing(event); + break; + case SyncEngineEvent::STOP_SYNCING_PERMANENTLY: + case SyncEngineEvent::UPDATED_TOKEN: + case SyncEngineEvent::CLEAR_SERVER_DATA_FAILED: + case SyncEngineEvent::CLEAR_SERVER_DATA_SUCCEEDED: + break; + case SyncEngineEvent::ACTIONABLE_ERROR: + status_ = CreateBlankStatus(); + status_.sync_protocol_error = event.snapshot->errors.sync_protocol_error; + break; + default: + LOG(ERROR) << "Unrecognized Syncer Event: " << event.what_happened; + break; + } +} + +sync_api::SyncManager::Status AllStatus::status() const { + base::AutoLock lock(mutex_); + return status_; +} + +void AllStatus::SetNotificationsEnabled(bool notifications_enabled) { + ScopedStatusLock lock(this); + status_.notifications_enabled = notifications_enabled; +} + +void AllStatus::IncrementNotificationsReceived() { + ScopedStatusLock lock(this); + ++status_.notifications_received; +} + +void AllStatus::SetEncryptedTypes(syncable::ModelTypeSet types) { + ScopedStatusLock lock(this); + status_.encrypted_types = types; +} + +void AllStatus::SetCryptographerReady(bool ready) { + ScopedStatusLock lock(this); + status_.cryptographer_ready = ready; +} + +void AllStatus::SetCryptoHasPendingKeys(bool has_pending_keys) { + ScopedStatusLock lock(this); + status_.crypto_has_pending_keys = has_pending_keys; +} + +void AllStatus::SetUniqueId(const std::string& guid) { + ScopedStatusLock lock(this); + status_.unique_id = guid; +} + +ScopedStatusLock::ScopedStatusLock(AllStatus* allstatus) + : allstatus_(allstatus) { + allstatus->mutex_.Acquire(); +} + +ScopedStatusLock::~ScopedStatusLock() { + allstatus_->mutex_.Release(); +} + +} // namespace browser_sync diff --git a/sync/internal_api/all_status.h b/sync/internal_api/all_status.h new file mode 100644 index 0000000..d108727 --- /dev/null +++ b/sync/internal_api/all_status.h @@ -0,0 +1,81 @@ +// Copyright (c) 2012 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. +// +// The AllStatus object watches various sync engine components and aggregates +// the status of all of them into one place. + +#ifndef SYNC_INTERNAL_API_ALL_STATUS_H_ +#define SYNC_INTERNAL_API_ALL_STATUS_H_ +#pragma once + +#include <map> + +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/synchronization/lock.h" +#include "sync/engine/syncer_types.h" +#include "sync/internal_api/sync_manager.h" +#include "sync/syncable/model_type.h" + +namespace browser_sync { + +class ScopedStatusLock; +struct ServerConnectionEvent; + +// TODO(rlarocque): +// Most of this data ends up on the about:sync page. But the page is only +// 'pinged' to update itself at the end of a sync cycle. A user could refresh +// manually, but unless their timing is excellent it's unlikely that a user will +// see any state in mid-sync cycle. We have no plans to change this. +// +// What we do intend to do is improve the UI so that changes following a sync +// cycle are more visible. Without such a change, the status summary for a +// healthy syncer will constantly display as "READY" and never provide any +// indication of a sync cycle being performed. See crbug.com/108100. + +class AllStatus : public SyncEngineEventListener { + friend class ScopedStatusLock; + public: + AllStatus(); + virtual ~AllStatus(); + + virtual void OnSyncEngineEvent(const SyncEngineEvent& event) OVERRIDE; + + sync_api::SyncManager::Status status() const; + + void SetNotificationsEnabled(bool notifications_enabled); + + void IncrementNotifiableCommits(); + + void IncrementNotificationsReceived(); + + void SetEncryptedTypes(syncable::ModelTypeSet types); + void SetCryptographerReady(bool ready); + void SetCryptoHasPendingKeys(bool has_pending_keys); + + void SetUniqueId(const std::string& guid); + + protected: + // Examines syncer to calculate syncing and the unsynced count, + // and returns a Status with new values. + sync_api::SyncManager::Status CalcSyncing(const SyncEngineEvent& event) const; + sync_api::SyncManager::Status CreateBlankStatus() const; + + sync_api::SyncManager::Status status_; + + mutable base::Lock mutex_; // Protects all data members. + DISALLOW_COPY_AND_ASSIGN(AllStatus); +}; + +class ScopedStatusLock { + public: + explicit ScopedStatusLock(AllStatus* allstatus); + ~ScopedStatusLock(); + protected: + AllStatus* allstatus_; +}; + +} // namespace browser_sync + +#endif // SYNC_INTERNAL_API_ALL_STATUS_H_ diff --git a/sync/internal_api/base_node.cc b/sync/internal_api/base_node.cc new file mode 100644 index 0000000..552ee71 --- /dev/null +++ b/sync/internal_api/base_node.cc @@ -0,0 +1,346 @@ +// Copyright (c) 2012 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 "sync/internal_api/base_node.h" + +#include "base/base64.h" +#include "base/sha1.h" +#include "base/string_number_conversions.h" +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "sync/internal_api/base_transaction.h" +#include "sync/internal_api/syncapi_internal.h" +#include "sync/protocol/app_specifics.pb.h" +#include "sync/protocol/autofill_specifics.pb.h" +#include "sync/protocol/bookmark_specifics.pb.h" +#include "sync/protocol/extension_specifics.pb.h" +#include "sync/protocol/nigori_specifics.pb.h" +#include "sync/protocol/password_specifics.pb.h" +#include "sync/protocol/session_specifics.pb.h" +#include "sync/protocol/theme_specifics.pb.h" +#include "sync/protocol/typed_url_specifics.pb.h" +#include "sync/syncable/syncable.h" +#include "sync/syncable/syncable_id.h" +#include "sync/util/time.h" + +using syncable::SPECIFICS; +using sync_pb::AutofillProfileSpecifics; + +namespace sync_api { + +// Helper function to look up the int64 metahandle of an object given the ID +// string. +static int64 IdToMetahandle(syncable::BaseTransaction* trans, + const syncable::Id& id) { + syncable::Entry entry(trans, syncable::GET_BY_ID, id); + if (!entry.good()) + return kInvalidId; + return entry.Get(syncable::META_HANDLE); +} + +static bool EndsWithSpace(const std::string& string) { + return !string.empty() && *string.rbegin() == ' '; +} + +// In the reverse direction, if a server name matches the pattern of a +// server-illegal name followed by one or more spaces, remove the trailing +// space. +static void ServerNameToSyncAPIName(const std::string& server_name, + std::string* out) { + CHECK(out); + int length_to_copy = server_name.length(); + if (IsNameServerIllegalAfterTrimming(server_name) && + EndsWithSpace(server_name)) { + --length_to_copy; + } + *out = std::string(server_name.c_str(), length_to_copy); +} + +BaseNode::BaseNode() : password_data_(new sync_pb::PasswordSpecificsData) {} + +BaseNode::~BaseNode() {} + +std::string BaseNode::GenerateSyncableHash( + syncable::ModelType model_type, const std::string& client_tag) { + // Blank PB with just the field in it has termination symbol, + // handy for delimiter. + sync_pb::EntitySpecifics serialized_type; + syncable::AddDefaultFieldValue(model_type, &serialized_type); + std::string hash_input; + serialized_type.AppendToString(&hash_input); + hash_input.append(client_tag); + + std::string encode_output; + CHECK(base::Base64Encode(base::SHA1HashString(hash_input), &encode_output)); + return encode_output; +} + +bool BaseNode::DecryptIfNecessary() { + if (!GetEntry()->Get(syncable::UNIQUE_SERVER_TAG).empty()) + return true; // Ignore unique folders. + const sync_pb::EntitySpecifics& specifics = + GetEntry()->Get(syncable::SPECIFICS); + if (specifics.has_password()) { + // Passwords have their own legacy encryption structure. + scoped_ptr<sync_pb::PasswordSpecificsData> data(DecryptPasswordSpecifics( + specifics, GetTransaction()->GetCryptographer())); + if (!data.get()) { + LOG(ERROR) << "Failed to decrypt password specifics."; + return false; + } + password_data_.swap(data); + return true; + } + + // We assume any node with the encrypted field set has encrypted data and if + // not we have no work to do, with the exception of bookmarks. For bookmarks + // we must make sure the bookmarks data has the title field supplied. If not, + // we fill the unencrypted_data_ with a copy of the bookmark specifics that + // follows the new bookmarks format. + if (!specifics.has_encrypted()) { + if (GetModelType() == syncable::BOOKMARKS && + !specifics.bookmark().has_title() && + !GetTitle().empty()) { // Last check ensures this isn't a new node. + // We need to fill in the title. + std::string title = GetTitle(); + std::string server_legal_title; + SyncAPINameToServerName(title, &server_legal_title); + DVLOG(1) << "Reading from legacy bookmark, manually returning title " + << title; + unencrypted_data_.CopyFrom(specifics); + unencrypted_data_.mutable_bookmark()->set_title( + server_legal_title); + } + return true; + } + + const sync_pb::EncryptedData& encrypted = specifics.encrypted(); + std::string plaintext_data = GetTransaction()->GetCryptographer()-> + DecryptToString(encrypted); + if (plaintext_data.length() == 0 || + !unencrypted_data_.ParseFromString(plaintext_data)) { + LOG(ERROR) << "Failed to decrypt encrypted node of type " << + syncable::ModelTypeToString(GetModelType()) << "."; + return false; + } + DVLOG(2) << "Decrypted specifics of type " + << syncable::ModelTypeToString(GetModelType()) + << " with content: " << plaintext_data; + return true; +} + +const sync_pb::EntitySpecifics& BaseNode::GetUnencryptedSpecifics( + const syncable::Entry* entry) const { + const sync_pb::EntitySpecifics& specifics = entry->Get(SPECIFICS); + if (specifics.has_encrypted()) { + DCHECK_NE(syncable::GetModelTypeFromSpecifics(unencrypted_data_), + syncable::UNSPECIFIED); + return unencrypted_data_; + } else { + // Due to the change in bookmarks format, we need to check to see if this is + // a legacy bookmarks (and has no title field in the proto). If it is, we + // return the unencrypted_data_, which was filled in with the title by + // DecryptIfNecessary(). + if (GetModelType() == syncable::BOOKMARKS) { + const sync_pb::BookmarkSpecifics& bookmark_specifics = + specifics.bookmark(); + if (bookmark_specifics.has_title() || + GetTitle().empty() || // For the empty node case + !GetEntry()->Get(syncable::UNIQUE_SERVER_TAG).empty()) { + // It's possible we previously had to convert and set + // |unencrypted_data_| but then wrote our own data, so we allow + // |unencrypted_data_| to be non-empty. + return specifics; + } else { + DCHECK_EQ(syncable::GetModelTypeFromSpecifics(unencrypted_data_), + syncable::BOOKMARKS); + return unencrypted_data_; + } + } else { + DCHECK_EQ(syncable::GetModelTypeFromSpecifics(unencrypted_data_), + syncable::UNSPECIFIED); + return specifics; + } + } +} + +int64 BaseNode::GetParentId() const { + return IdToMetahandle(GetTransaction()->GetWrappedTrans(), + GetEntry()->Get(syncable::PARENT_ID)); +} + +int64 BaseNode::GetId() const { + return GetEntry()->Get(syncable::META_HANDLE); +} + +const base::Time& BaseNode::GetModificationTime() const { + return GetEntry()->Get(syncable::MTIME); +} + +bool BaseNode::GetIsFolder() const { + return GetEntry()->Get(syncable::IS_DIR); +} + +std::string BaseNode::GetTitle() const { + std::string result; + // TODO(zea): refactor bookmarks to not need this functionality. + if (syncable::BOOKMARKS == GetModelType() && + GetEntry()->Get(syncable::SPECIFICS).has_encrypted()) { + // Special case for legacy bookmarks dealing with encryption. + ServerNameToSyncAPIName(GetBookmarkSpecifics().title(), &result); + } else { + ServerNameToSyncAPIName(GetEntry()->Get(syncable::NON_UNIQUE_NAME), + &result); + } + return result; +} + +GURL BaseNode::GetURL() const { + return GURL(GetBookmarkSpecifics().url()); +} + +bool BaseNode::HasChildren() const { + syncable::Directory* dir = GetTransaction()->GetDirectory(); + syncable::BaseTransaction* trans = GetTransaction()->GetWrappedTrans(); + return dir->HasChildren(trans, GetEntry()->Get(syncable::ID)); +} + +int64 BaseNode::GetPredecessorId() const { + syncable::Id id_string = GetEntry()->Get(syncable::PREV_ID); + if (id_string.IsRoot()) + return kInvalidId; + return IdToMetahandle(GetTransaction()->GetWrappedTrans(), id_string); +} + +int64 BaseNode::GetSuccessorId() const { + syncable::Id id_string = GetEntry()->Get(syncable::NEXT_ID); + if (id_string.IsRoot()) + return kInvalidId; + return IdToMetahandle(GetTransaction()->GetWrappedTrans(), id_string); +} + +int64 BaseNode::GetFirstChildId() const { + syncable::Directory* dir = GetTransaction()->GetDirectory(); + syncable::BaseTransaction* trans = GetTransaction()->GetWrappedTrans(); + syncable::Id id_string; + // TODO(akalin): Propagate up the error further (see + // http://crbug.com/100907). + CHECK(dir->GetFirstChildId(trans, + GetEntry()->Get(syncable::ID), &id_string)); + if (id_string.IsRoot()) + return kInvalidId; + return IdToMetahandle(GetTransaction()->GetWrappedTrans(), id_string); +} + +DictionaryValue* BaseNode::GetSummaryAsValue() const { + DictionaryValue* node_info = new DictionaryValue(); + node_info->SetString("id", base::Int64ToString(GetId())); + node_info->SetBoolean("isFolder", GetIsFolder()); + node_info->SetString("title", GetTitle()); + node_info->Set("type", ModelTypeToValue(GetModelType())); + return node_info; +} + +DictionaryValue* BaseNode::GetDetailsAsValue() const { + DictionaryValue* node_info = GetSummaryAsValue(); + node_info->SetString( + "modificationTime", + browser_sync::GetTimeDebugString(GetModificationTime())); + node_info->SetString("parentId", base::Int64ToString(GetParentId())); + // Specifics are already in the Entry value, so no need to duplicate + // it here. + node_info->SetString("externalId", + base::Int64ToString(GetExternalId())); + node_info->SetString("predecessorId", + base::Int64ToString(GetPredecessorId())); + node_info->SetString("successorId", + base::Int64ToString(GetSuccessorId())); + node_info->SetString("firstChildId", + base::Int64ToString(GetFirstChildId())); + node_info->Set("entry", GetEntry()->ToValue()); + return node_info; +} + +void BaseNode::GetFaviconBytes(std::vector<unsigned char>* output) const { + if (!output) + return; + const std::string& favicon = GetBookmarkSpecifics().favicon(); + output->assign(reinterpret_cast<const unsigned char*>(favicon.data()), + reinterpret_cast<const unsigned char*>(favicon.data() + + favicon.length())); +} + +int64 BaseNode::GetExternalId() const { + return GetEntry()->Get(syncable::LOCAL_EXTERNAL_ID); +} + +const sync_pb::AppSpecifics& BaseNode::GetAppSpecifics() const { + DCHECK_EQ(syncable::APPS, GetModelType()); + return GetEntitySpecifics().app(); +} + +const sync_pb::AutofillSpecifics& BaseNode::GetAutofillSpecifics() const { + DCHECK_EQ(syncable::AUTOFILL, GetModelType()); + return GetEntitySpecifics().autofill(); +} + +const AutofillProfileSpecifics& BaseNode::GetAutofillProfileSpecifics() const { + DCHECK_EQ(GetModelType(), syncable::AUTOFILL_PROFILE); + return GetEntitySpecifics().autofill_profile(); +} + +const sync_pb::BookmarkSpecifics& BaseNode::GetBookmarkSpecifics() const { + DCHECK_EQ(syncable::BOOKMARKS, GetModelType()); + return GetEntitySpecifics().bookmark(); +} + +const sync_pb::NigoriSpecifics& BaseNode::GetNigoriSpecifics() const { + DCHECK_EQ(syncable::NIGORI, GetModelType()); + return GetEntitySpecifics().nigori(); +} + +const sync_pb::PasswordSpecificsData& BaseNode::GetPasswordSpecifics() const { + DCHECK_EQ(syncable::PASSWORDS, GetModelType()); + return *password_data_; +} + +const sync_pb::ThemeSpecifics& BaseNode::GetThemeSpecifics() const { + DCHECK_EQ(syncable::THEMES, GetModelType()); + return GetEntitySpecifics().theme(); +} + +const sync_pb::TypedUrlSpecifics& BaseNode::GetTypedUrlSpecifics() const { + DCHECK_EQ(syncable::TYPED_URLS, GetModelType()); + return GetEntitySpecifics().typed_url(); +} + +const sync_pb::ExtensionSpecifics& BaseNode::GetExtensionSpecifics() const { + DCHECK_EQ(syncable::EXTENSIONS, GetModelType()); + return GetEntitySpecifics().extension(); +} + +const sync_pb::SessionSpecifics& BaseNode::GetSessionSpecifics() const { + DCHECK_EQ(syncable::SESSIONS, GetModelType()); + return GetEntitySpecifics().session(); +} + +const sync_pb::EntitySpecifics& BaseNode::GetEntitySpecifics() const { + return GetUnencryptedSpecifics(GetEntry()); +} + +syncable::ModelType BaseNode::GetModelType() const { + return GetEntry()->GetModelType(); +} + +void BaseNode::SetUnencryptedSpecifics( + const sync_pb::EntitySpecifics& specifics) { + syncable::ModelType type = syncable::GetModelTypeFromSpecifics(specifics); + DCHECK_NE(syncable::UNSPECIFIED, type); + if (GetModelType() != syncable::UNSPECIFIED) { + DCHECK_EQ(GetModelType(), type); + } + unencrypted_data_.CopyFrom(specifics); +} + +} // namespace sync_api diff --git a/sync/internal_api/base_node.h b/sync/internal_api/base_node.h new file mode 100644 index 0000000..42c3b80 --- /dev/null +++ b/sync/internal_api/base_node.h @@ -0,0 +1,255 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_BASE_NODE_H_ +#define SYNC_INTERNAL_API_BASE_NODE_H_ +#pragma once + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "googleurl/src/gurl.h" +#include "sync/protocol/sync.pb.h" +#include "sync/syncable/model_type.h" + +// Forward declarations of internal class types so that sync API objects +// may have opaque pointers to these types. +namespace base { +class DictionaryValue; +} + +namespace syncable { +class BaseTransaction; +class Entry; +} + +namespace sync_pb { +class AppSpecifics; +class AutofillSpecifics; +class AutofillProfileSpecifics; +class BookmarkSpecifics; +class EntitySpecifics; +class ExtensionSpecifics; +class SessionSpecifics; +class NigoriSpecifics; +class PreferenceSpecifics; +class PasswordSpecificsData; +class ThemeSpecifics; +class TypedUrlSpecifics; +} + +namespace sync_api { + +class BaseTransaction; + +// A valid BaseNode will never have an ID of zero. +static const int64 kInvalidId = 0; + +// BaseNode wraps syncable::Entry, and corresponds to a single object's state. +// This, like syncable::Entry, is intended for use on the stack. A valid +// transaction is necessary to create a BaseNode or any of its children. +// Unlike syncable::Entry, a sync API BaseNode is identified primarily by its +// int64 metahandle, which we call an ID here. +class BaseNode { + public: + // Enumerates the possible outcomes of trying to initialize a sync node. + enum InitByLookupResult { + INIT_OK, + // Could not find an entry matching the lookup criteria. + INIT_FAILED_ENTRY_NOT_GOOD, + // Found an entry, but it is already deleted. + INIT_FAILED_ENTRY_IS_DEL, + // Found an entry, but was unable to decrypt. + INIT_FAILED_DECRYPT_IF_NECESSARY, + // A precondition was not met for calling init, such as legal input + // arguments. + INIT_FAILED_PRECONDITION, + }; + + // All subclasses of BaseNode must provide a way to initialize themselves by + // doing an ID lookup. Returns false on failure. An invalid or deleted + // ID will result in failure. + virtual InitByLookupResult InitByIdLookup(int64 id) = 0; + + // All subclasses of BaseNode must also provide a way to initialize themselves + // by doing a client tag lookup. Returns false on failure. A deleted node + // will return FALSE. + virtual InitByLookupResult InitByClientTagLookup( + syncable::ModelType model_type, + const std::string& tag) = 0; + + // Each object is identified by a 64-bit id (internally, the syncable + // metahandle). These ids are strictly local handles. They will persist + // on this client, but the same object on a different client may have a + // different ID value. + virtual int64 GetId() const; + + // Returns the modification time of the object. + const base::Time& GetModificationTime() const; + + // Nodes are hierarchically arranged into a single-rooted tree. + // InitByRootLookup on ReadNode allows access to the root. GetParentId is + // how you find a node's parent. + int64 GetParentId() const; + + // Nodes are either folders or not. This corresponds to the IS_DIR property + // of syncable::Entry. + bool GetIsFolder() const; + + // Returns the title of the object. + // Uniqueness of the title is not enforced on siblings -- it is not an error + // for two children to share a title. + std::string GetTitle() const; + + // Returns the model type of this object. The model type is set at node + // creation time and is expected never to change. + syncable::ModelType GetModelType() const; + + // Getter specific to the BOOKMARK datatype. Returns protobuf + // data. Can only be called if GetModelType() == BOOKMARK. + const sync_pb::BookmarkSpecifics& GetBookmarkSpecifics() const; + + // Legacy, bookmark-specific getter that wraps GetBookmarkSpecifics() above. + // Returns the URL of a bookmark object. + // TODO(ncarter): Remove this datatype-specific accessor. + GURL GetURL() const; + + // Legacy, bookmark-specific getter that wraps GetBookmarkSpecifics() above. + // Fill in a vector with the byte data of this node's favicon. Assumes + // that the node is a bookmark. + // Favicons are expected to be PNG images, and though no verification is + // done on the syncapi client of this, the server may reject favicon updates + // that are invalid for whatever reason. + // TODO(ncarter): Remove this datatype-specific accessor. + void GetFaviconBytes(std::vector<unsigned char>* output) const; + + // Getter specific to the APPS datatype. Returns protobuf + // data. Can only be called if GetModelType() == APPS. + const sync_pb::AppSpecifics& GetAppSpecifics() const; + + // Getter specific to the AUTOFILL datatype. Returns protobuf + // data. Can only be called if GetModelType() == AUTOFILL. + const sync_pb::AutofillSpecifics& GetAutofillSpecifics() const; + + virtual const sync_pb::AutofillProfileSpecifics& + GetAutofillProfileSpecifics() const; + + // Getter specific to the NIGORI datatype. Returns protobuf + // data. Can only be called if GetModelType() == NIGORI. + const sync_pb::NigoriSpecifics& GetNigoriSpecifics() const; + + // Getter specific to the PASSWORD datatype. Returns protobuf + // data. Can only be called if GetModelType() == PASSWORD. + const sync_pb::PasswordSpecificsData& GetPasswordSpecifics() const; + + // Getter specific to the PREFERENCE datatype. Returns protobuf + // data. Can only be called if GetModelType() == PREFERENCE. + const sync_pb::PreferenceSpecifics& GetPreferenceSpecifics() const; + + // Getter specific to the THEME datatype. Returns protobuf + // data. Can only be called if GetModelType() == THEME. + const sync_pb::ThemeSpecifics& GetThemeSpecifics() const; + + // Getter specific to the TYPED_URLS datatype. Returns protobuf + // data. Can only be called if GetModelType() == TYPED_URLS. + const sync_pb::TypedUrlSpecifics& GetTypedUrlSpecifics() const; + + // Getter specific to the EXTENSIONS datatype. Returns protobuf + // data. Can only be called if GetModelType() == EXTENSIONS. + const sync_pb::ExtensionSpecifics& GetExtensionSpecifics() const; + + // Getter specific to the SESSIONS datatype. Returns protobuf + // data. Can only be called if GetModelType() == SESSIONS. + const sync_pb::SessionSpecifics& GetSessionSpecifics() const; + + const sync_pb::EntitySpecifics& GetEntitySpecifics() const; + + // Returns the local external ID associated with the node. + int64 GetExternalId() const; + + // Returns true iff this node has children. + bool HasChildren() const; + + // Return the ID of the node immediately before this in the sibling order. + // For the first node in the ordering, return 0. + int64 GetPredecessorId() const; + + // Return the ID of the node immediately after this in the sibling order. + // For the last node in the ordering, return 0. + int64 GetSuccessorId() const; + + // Return the ID of the first child of this node. If this node has no + // children, return 0. + int64 GetFirstChildId() const; + + // These virtual accessors provide access to data members of derived classes. + virtual const syncable::Entry* GetEntry() const = 0; + virtual const BaseTransaction* GetTransaction() const = 0; + + // Dumps a summary of node info into a DictionaryValue and returns it. + // Transfers ownership of the DictionaryValue to the caller. + base::DictionaryValue* GetSummaryAsValue() const; + + // Dumps all node details into a DictionaryValue and returns it. + // Transfers ownership of the DictionaryValue to the caller. + base::DictionaryValue* GetDetailsAsValue() const; + + protected: + BaseNode(); + virtual ~BaseNode(); + // The server has a size limit on client tags, so we generate a fixed length + // hash locally. This also ensures that ModelTypes have unique namespaces. + static std::string GenerateSyncableHash(syncable::ModelType model_type, + const std::string& client_tag); + + // Determines whether part of the entry is encrypted, and if so attempts to + // decrypt it. Unless decryption is necessary and fails, this will always + // return |true|. If the contents are encrypted, the decrypted data will be + // stored in |unencrypted_data_|. + // This method is invoked once when the BaseNode is initialized. + bool DecryptIfNecessary(); + + // Returns the unencrypted specifics associated with |entry|. If |entry| was + // not encrypted, it directly returns |entry|'s EntitySpecifics. Otherwise, + // returns |unencrypted_data_|. + const sync_pb::EntitySpecifics& GetUnencryptedSpecifics( + const syncable::Entry* entry) const; + + // Copy |specifics| into |unencrypted_data_|. + void SetUnencryptedSpecifics(const sync_pb::EntitySpecifics& specifics); + + private: + // Have to friend the test class as well to allow member functions to access + // protected/private BaseNode methods. + friend class SyncManagerTest; + FRIEND_TEST_ALL_PREFIXES(SyncApiTest, GenerateSyncableHash); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, UpdateEntryWithEncryption); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, + UpdatePasswordSetEntitySpecificsNoChange); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, UpdatePasswordSetPasswordSpecifics); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, UpdatePasswordNewPassphrase); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, UpdatePasswordReencryptEverything); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, SetBookmarkTitle); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, SetBookmarkTitleWithEncryption); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, SetNonBookmarkTitle); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, SetNonBookmarkTitleWithEncryption); + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, SetPreviouslyEncryptedSpecifics); + + void* operator new(size_t size); // Node is meant for stack use only. + + // A holder for the unencrypted data stored in an encrypted node. + sync_pb::EntitySpecifics unencrypted_data_; + + // Same as |unencrypted_data_|, but for legacy password encryption. + scoped_ptr<sync_pb::PasswordSpecificsData> password_data_; + + DISALLOW_COPY_AND_ASSIGN(BaseNode); +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_BASE_NODE_H_ diff --git a/sync/internal_api/base_transaction.cc b/sync/internal_api/base_transaction.cc new file mode 100644 index 0000000..879e4e3 --- /dev/null +++ b/sync/internal_api/base_transaction.cc @@ -0,0 +1,32 @@ +// Copyright (c) 2012 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 "sync/internal_api/base_transaction.h" + +#include "sync/syncable/syncable.h" +#include "sync/util/cryptographer.h" + +using browser_sync::Cryptographer; + +namespace sync_api { + +////////////////////////////////////////////////////////////////////////// +// BaseTransaction member definitions +BaseTransaction::BaseTransaction(UserShare* share) { + DCHECK(share && share->directory.get()); + directory_ = share->directory.get(); +} +BaseTransaction::~BaseTransaction() { +} + +browser_sync::Cryptographer* BaseTransaction::GetCryptographer() const { + return directory_->GetCryptographer(this->GetWrappedTrans()); +} + +syncable::ModelTypeSet GetEncryptedTypes( + const sync_api::BaseTransaction* trans) { + return trans->GetCryptographer()->GetEncryptedTypes(); +} + +} // namespace sync_api diff --git a/sync/internal_api/base_transaction.h b/sync/internal_api/base_transaction.h new file mode 100644 index 0000000..f39bca6 --- /dev/null +++ b/sync/internal_api/base_transaction.h @@ -0,0 +1,54 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_BASE_TRANSACTION_H_ +#define SYNC_INTERNAL_API_BASE_TRANSACTION_H_ +#pragma once + +#include "sync/internal_api/user_share.h" + +#include "sync/util/cryptographer.h" + +namespace syncable { +class BaseTransaction; +class Directory; +} + +namespace sync_api { + +// Sync API's BaseTransaction, ReadTransaction, and WriteTransaction allow for +// batching of several read and/or write operations. The read and write +// operations are performed by creating ReadNode and WriteNode instances using +// the transaction. These transaction classes wrap identically named classes in +// syncable, and are used in a similar way. Unlike syncable::BaseTransaction, +// whose construction requires an explicit syncable::Directory, a sync +// API BaseTransaction is created from a UserShare object. +class BaseTransaction { + public: + // Provide access to the underlying syncable.h objects from BaseNode. + virtual syncable::BaseTransaction* GetWrappedTrans() const = 0; + browser_sync::Cryptographer* GetCryptographer() const; + + syncable::Directory* GetDirectory() const { + return directory_; + } + + protected: + explicit BaseTransaction(UserShare* share); + virtual ~BaseTransaction(); + + BaseTransaction() : directory_(NULL) { } + + private: + syncable::Directory* directory_; + + DISALLOW_COPY_AND_ASSIGN(BaseTransaction); +}; + +syncable::ModelTypeSet GetEncryptedTypes( + const sync_api::BaseTransaction* trans); + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_BASE_TRANSACTION_H_ diff --git a/sync/internal_api/change_record.cc b/sync/internal_api/change_record.cc new file mode 100644 index 0000000..3eefb74 --- /dev/null +++ b/sync/internal_api/change_record.cc @@ -0,0 +1,69 @@ +// Copyright (c) 2012 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 "sync/internal_api/change_record.h" + +#include "base/string_number_conversions.h" +#include "base/values.h" +#include "sync/internal_api/base_node.h" +#include "sync/internal_api/read_node.h" +#include "sync/protocol/proto_value_conversions.h" + +namespace sync_api { + +ChangeRecord::ChangeRecord() + : id(kInvalidId), action(ACTION_ADD) {} + +ChangeRecord::~ChangeRecord() {} + +DictionaryValue* ChangeRecord::ToValue() const { + DictionaryValue* value = new DictionaryValue(); + std::string action_str; + switch (action) { + case ACTION_ADD: + action_str = "Add"; + break; + case ACTION_DELETE: + action_str = "Delete"; + break; + case ACTION_UPDATE: + action_str = "Update"; + break; + default: + NOTREACHED(); + action_str = "Unknown"; + break; + } + value->SetString("action", action_str); + value->SetString("id", base::Int64ToString(id)); + if (action == ACTION_DELETE) { + if (extra.get()) { + value->Set("extra", extra->ToValue()); + } + value->Set("specifics", + browser_sync::EntitySpecificsToValue(specifics)); + } + return value; +} + +ExtraPasswordChangeRecordData::ExtraPasswordChangeRecordData() {} + +ExtraPasswordChangeRecordData::ExtraPasswordChangeRecordData( + const sync_pb::PasswordSpecificsData& data) + : unencrypted_(data) { +} + +ExtraPasswordChangeRecordData::~ExtraPasswordChangeRecordData() {} + +DictionaryValue* ExtraPasswordChangeRecordData::ToValue() const { + return browser_sync::PasswordSpecificsDataToValue(unencrypted_); +} + +const sync_pb::PasswordSpecificsData& + ExtraPasswordChangeRecordData::unencrypted() const { + return unencrypted_; +} + +} // namespace sync_api + diff --git a/sync/internal_api/change_record.h b/sync/internal_api/change_record.h new file mode 100644 index 0000000..3d2f4ee --- /dev/null +++ b/sync/internal_api/change_record.h @@ -0,0 +1,67 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_CHANGE_RECORD_H_ +#define SYNC_INTERNAL_API_CHANGE_RECORD_H_ + +#include <vector> + +#include "base/basictypes.h" +#include "base/memory/linked_ptr.h" +#include "sync/protocol/password_specifics.pb.h" +#include "sync/protocol/sync.pb.h" +#include "sync/util/immutable.h" + +namespace base { +class DictionaryValue; +} // namespace base + +namespace sync_api { + +// TODO(zea): One day get passwords playing nicely with the rest of encryption +// and get rid of this. +class ExtraPasswordChangeRecordData { + public: + ExtraPasswordChangeRecordData(); + explicit ExtraPasswordChangeRecordData( + const sync_pb::PasswordSpecificsData& data); + virtual ~ExtraPasswordChangeRecordData(); + + // Transfers ownership of the DictionaryValue to the caller. + virtual base::DictionaryValue* ToValue() const; + + const sync_pb::PasswordSpecificsData& unencrypted() const; + private: + sync_pb::PasswordSpecificsData unencrypted_; +}; + +// ChangeRecord indicates a single item that changed as a result of a sync +// operation. This gives the sync id of the node that changed, and the type +// of change. To get the actual property values after an ADD or UPDATE, the +// client should get the node with InitByIdLookup(), using the provided id. +struct ChangeRecord { + enum Action { + ACTION_ADD, + ACTION_DELETE, + ACTION_UPDATE, + }; + ChangeRecord(); + ~ChangeRecord(); + + // Transfers ownership of the DictionaryValue to the caller. + base::DictionaryValue* ToValue() const; + + int64 id; + Action action; + sync_pb::EntitySpecifics specifics; + linked_ptr<ExtraPasswordChangeRecordData> extra; +}; + +typedef std::vector<ChangeRecord> ChangeRecordList; + +typedef browser_sync::Immutable<ChangeRecordList> ImmutableChangeRecordList; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_CHANGE_RECORD_H_ diff --git a/sync/internal_api/change_record_unittest.cc b/sync/internal_api/change_record_unittest.cc new file mode 100644 index 0000000..9a1d1d3 --- /dev/null +++ b/sync/internal_api/change_record_unittest.cc @@ -0,0 +1,137 @@ +// Copyright (c) 2012 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 "sync/internal_api/change_record.h" + +#include "base/memory/scoped_ptr.h" +#include "base/string_number_conversions.h" +#include "base/test/values_test_util.h" +#include "base/values.h" +#include "sync/protocol/extension_specifics.pb.h" +#include "sync/protocol/proto_value_conversions.h" +#include "sync/protocol/sync.pb.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace sync_api { +namespace { + +using base::ExpectDictDictionaryValue; +using base::ExpectDictStringValue; +using testing::Invoke; +using testing::StrictMock; + +class ChangeRecordTest : public testing::Test {}; + +void ExpectChangeRecordActionValue(ChangeRecord::Action expected_value, + const base::DictionaryValue& value, + const std::string& key) { + std::string str_value; + EXPECT_TRUE(value.GetString(key, &str_value)); + switch (expected_value) { + case ChangeRecord::ACTION_ADD: + EXPECT_EQ("Add", str_value); + break; + case ChangeRecord::ACTION_UPDATE: + EXPECT_EQ("Update", str_value); + break; + case ChangeRecord::ACTION_DELETE: + EXPECT_EQ("Delete", str_value); + break; + default: + NOTREACHED(); + break; + } +} + +void CheckChangeRecordValue( + const ChangeRecord& record, + const base::DictionaryValue& value) { + ExpectChangeRecordActionValue(record.action, value, "action"); + ExpectDictStringValue(base::Int64ToString(record.id), value, "id"); + if (record.action == ChangeRecord::ACTION_DELETE) { + scoped_ptr<base::DictionaryValue> expected_extra_value; + if (record.extra.get()) { + expected_extra_value.reset(record.extra->ToValue()); + } + base::Value* extra_value = NULL; + EXPECT_EQ(record.extra.get() != NULL, + value.Get("extra", &extra_value)); + EXPECT_TRUE(Value::Equals(extra_value, expected_extra_value.get())); + + scoped_ptr<DictionaryValue> expected_specifics_value( + browser_sync::EntitySpecificsToValue(record.specifics)); + ExpectDictDictionaryValue(*expected_specifics_value, + value, "specifics"); + } +} + +class MockExtraChangeRecordData + : public ExtraPasswordChangeRecordData { + public: + MOCK_CONST_METHOD0(ToValue, DictionaryValue*()); +}; + +TEST_F(ChangeRecordTest, ChangeRecordToValue) { + sync_pb::EntitySpecifics old_specifics; + old_specifics.mutable_extension()->set_id("old"); + sync_pb::EntitySpecifics new_specifics; + old_specifics.mutable_extension()->set_id("new"); + + const int64 kTestId = 5; + + // Add + { + ChangeRecord record; + record.action = ChangeRecord::ACTION_ADD; + record.id = kTestId; + record.specifics = old_specifics; + record.extra.reset(new StrictMock<MockExtraChangeRecordData>()); + scoped_ptr<DictionaryValue> value(record.ToValue()); + CheckChangeRecordValue(record, *value); + } + + // Update + { + ChangeRecord record; + record.action = ChangeRecord::ACTION_UPDATE; + record.id = kTestId; + record.specifics = old_specifics; + record.extra.reset(new StrictMock<MockExtraChangeRecordData>()); + scoped_ptr<DictionaryValue> value(record.ToValue()); + CheckChangeRecordValue(record, *value); + } + + // Delete (no extra) + { + ChangeRecord record; + record.action = ChangeRecord::ACTION_DELETE; + record.id = kTestId; + record.specifics = old_specifics; + scoped_ptr<DictionaryValue> value(record.ToValue()); + CheckChangeRecordValue(record, *value); + } + + // Delete (with extra) + { + ChangeRecord record; + record.action = ChangeRecord::ACTION_DELETE; + record.id = kTestId; + record.specifics = old_specifics; + + DictionaryValue extra_value; + extra_value.SetString("foo", "bar"); + scoped_ptr<StrictMock<MockExtraChangeRecordData> > extra( + new StrictMock<MockExtraChangeRecordData>()); + EXPECT_CALL(*extra, ToValue()).Times(2).WillRepeatedly( + Invoke(&extra_value, &DictionaryValue::DeepCopy)); + + record.extra.reset(extra.release()); + scoped_ptr<DictionaryValue> value(record.ToValue()); + CheckChangeRecordValue(record, *value); + } +} + +} // namespace +} // namespace sync_api diff --git a/sync/internal_api/change_reorder_buffer.cc b/sync/internal_api/change_reorder_buffer.cc new file mode 100644 index 0000000..7fbada4 --- /dev/null +++ b/sync/internal_api/change_reorder_buffer.cc @@ -0,0 +1,229 @@ +// Copyright (c) 2012 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 "sync/internal_api/change_reorder_buffer.h" + +#include <limits> +#include <queue> +#include <set> +#include <utility> // for pair<> +#include <vector> + +#include "sync/internal_api/read_node.h" +#include "sync/syncable/model_type.h" +#include "sync/syncable/syncable.h" + +using std::numeric_limits; +using std::pair; +using std::queue; +using std::set; +using std::vector; + +namespace sync_api { + +// Traversal provides a way to collect a set of nodes from the syncable +// directory structure and then traverse them, along with any intermediate +// nodes, in a top-down fashion, starting from a single common ancestor. A +// Traversal starts out empty and is grown by means of the ExpandToInclude +// method. Once constructed, the top(), begin_children(), and end_children() +// methods can be used to explore the nodes in root-to-leaf order. +class ChangeReorderBuffer::Traversal { + public: + typedef pair<int64, int64> ParentChildLink; + typedef set<ParentChildLink> LinkSet; + + Traversal() : top_(kInvalidId) { } + + // Expand the traversal so that it includes the node indicated by + // |child_handle|. + void ExpandToInclude(syncable::BaseTransaction* trans, + int64 child_handle) { + // If |top_| is invalid, this is the first insertion -- easy. + if (top_ == kInvalidId) { + top_ = child_handle; + return; + } + + int64 node_to_include = child_handle; + while (node_to_include != kInvalidId && node_to_include != top_) { + int64 node_parent = 0; + + syncable::Entry node(trans, syncable::GET_BY_HANDLE, node_to_include); + CHECK(node.good()); + if (node.Get(syncable::ID).IsRoot()) { + // If we've hit the root, and the root isn't already in the tree + // (it would have to be |top_| if it were), start a new expansion + // upwards from |top_| to unite the original traversal with the + // path we just added that goes from |child_handle| to the root. + node_to_include = top_; + top_ = node.Get(syncable::META_HANDLE); + } else { + // Otherwise, get the parent ID so that we can add a ParentChildLink. + syncable::Entry parent(trans, syncable::GET_BY_ID, + node.Get(syncable::PARENT_ID)); + CHECK(parent.good()); + node_parent = parent.Get(syncable::META_HANDLE); + + ParentChildLink link(node_parent, node_to_include); + + // If the link exists in the LinkSet |links_|, we don't need to search + // any higher; we are done. + if (links_.find(link) != links_.end()) + return; + + // Otherwise, extend |links_|, and repeat on the parent. + links_.insert(link); + node_to_include = node_parent; + } + } + } + + // Return the top node of the traversal. Use this as a starting point + // for walking the tree. + int64 top() const { return top_; } + + // Return an iterator corresponding to the first child (in the traversal) + // of the node specified by |parent_id|. Iterate this return value until + // it is equal to the value returned by end_children(parent_id). The + // enumeration thus provided is unordered. + LinkSet::const_iterator begin_children(int64 parent_id) const { + return links_.upper_bound( + ParentChildLink(parent_id, numeric_limits<int64>::min())); + } + + // Return an iterator corresponding to the last child in the traversal + // of the node specified by |parent_id|. + LinkSet::const_iterator end_children(int64 parent_id) const { + return begin_children(parent_id + 1); + } + + private: + // The topmost point in the directory hierarchy that is in the traversal, + // and thus the first node to be traversed. If the traversal is empty, + // this is kInvalidId. If the traversal contains exactly one member, |top_| + // will be the solitary member, and |links_| will be empty. + int64 top_; + // A set of single-level links that compose the traversal below |top_|. The + // (parent, child) ordering of values enables efficient lookup of children + // given the parent handle, which is used for top-down traversal. |links_| + // is expected to be connected -- every node that appears as a parent in a + // link must either appear as a child of another link, or else be the + // topmost node, |top_|. + LinkSet links_; + + DISALLOW_COPY_AND_ASSIGN(Traversal); +}; + +ChangeReorderBuffer::ChangeReorderBuffer() { +} + +ChangeReorderBuffer::~ChangeReorderBuffer() { +} + +bool ChangeReorderBuffer::GetAllChangesInTreeOrder( + const BaseTransaction* sync_trans, + ImmutableChangeRecordList* changes) { + syncable::BaseTransaction* trans = sync_trans->GetWrappedTrans(); + + // Step 1: Iterate through the operations, doing three things: + // (a) Push deleted items straight into the |changelist|. + // (b) Construct a traversal spanning all non-deleted items. + // (c) Construct a set of all parent nodes of any position changes. + set<int64> parents_of_position_changes; + Traversal traversal; + + ChangeRecordList changelist; + + OperationMap::const_iterator i; + for (i = operations_.begin(); i != operations_.end(); ++i) { + if (i->second == OP_DELETE) { + ChangeRecord record; + record.id = i->first; + record.action = ChangeRecord::ACTION_DELETE; + if (specifics_.find(record.id) != specifics_.end()) + record.specifics = specifics_[record.id]; + if (extra_data_.find(record.id) != extra_data_.end()) + record.extra = extra_data_[record.id]; + changelist.push_back(record); + } else { + traversal.ExpandToInclude(trans, i->first); + if (i->second == OP_ADD || + i->second == OP_UPDATE_POSITION_AND_PROPERTIES) { + ReadNode node(sync_trans); + CHECK_EQ(BaseNode::INIT_OK, node.InitByIdLookup(i->first)); + + // We only care about parents of entry's with position-sensitive models. + if (syncable::ShouldMaintainPosition( + node.GetEntry()->GetModelType())) { + parents_of_position_changes.insert(node.GetParentId()); + } + } + } + } + + // Step 2: Breadth-first expansion of the traversal, enumerating children in + // the syncable sibling order if there were any position updates. + queue<int64> to_visit; + to_visit.push(traversal.top()); + while (!to_visit.empty()) { + int64 next = to_visit.front(); + to_visit.pop(); + + // If the node has an associated action, output a change record. + i = operations_.find(next); + if (i != operations_.end()) { + ChangeRecord record; + record.id = next; + if (i->second == OP_ADD) + record.action = ChangeRecord::ACTION_ADD; + else + record.action = ChangeRecord::ACTION_UPDATE; + if (specifics_.find(record.id) != specifics_.end()) + record.specifics = specifics_[record.id]; + if (extra_data_.find(record.id) != extra_data_.end()) + record.extra = extra_data_[record.id]; + changelist.push_back(record); + } + + // Now add the children of |next| to |to_visit|. + if (parents_of_position_changes.find(next) == + parents_of_position_changes.end()) { + // No order changes on this parent -- traverse only the nodes listed + // in the traversal (and not in sibling order). + Traversal::LinkSet::const_iterator j = traversal.begin_children(next); + Traversal::LinkSet::const_iterator end = traversal.end_children(next); + for (; j != end; ++j) { + CHECK(j->first == next); + to_visit.push(j->second); + } + } else { + // There were ordering changes on the children of this parent, so + // enumerate all the children in the sibling order. + syncable::Entry parent(trans, syncable::GET_BY_HANDLE, next); + syncable::Id id; + if (!trans->directory()->GetFirstChildId( + trans, parent.Get(syncable::ID), &id)) { + *changes = ImmutableChangeRecordList(); + return false; + } + while (!id.IsRoot()) { + syncable::Entry child(trans, syncable::GET_BY_ID, id); + CHECK(child.good()); + int64 handle = child.Get(syncable::META_HANDLE); + to_visit.push(handle); + // If there is no operation on this child node, record it as as an + // update, so that the listener gets notified of all nodes in the new + // ordering. + if (operations_.find(handle) == operations_.end()) + operations_[handle] = OP_UPDATE_POSITION_AND_PROPERTIES; + id = child.Get(syncable::NEXT_ID); + } + } + } + + *changes = ImmutableChangeRecordList(&changelist); + return true; +} + +} // namespace sync_api diff --git a/sync/internal_api/change_reorder_buffer.h b/sync/internal_api/change_reorder_buffer.h new file mode 100644 index 0000000..edb59f3 --- /dev/null +++ b/sync/internal_api/change_reorder_buffer.h @@ -0,0 +1,124 @@ +// Copyright (c) 2012 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. +// +// Defines ChangeReorderBuffer, which can be used to sort a list of item +// actions to achieve the ordering constraint required by the SyncObserver +// interface of the SyncAPI. + +#ifndef SYNC_INTERNAL_API_CHANGE_REORDER_BUFFER_H_ +#define SYNC_INTERNAL_API_CHANGE_REORDER_BUFFER_H_ +#pragma once + +#include <map> +#include <vector> + +#include "base/compiler_specific.h" +#include "base/memory/linked_ptr.h" +#include "sync/internal_api/base_transaction.h" +#include "sync/internal_api/change_record.h" +#include "sync/protocol/sync.pb.h" + +namespace sync_api { + +// ChangeReorderBuffer is a utility type which accepts an unordered set +// of changes (via its Push methods), and yields an ImmutableChangeRecordList +// (via the GetAllChangesInTreeOrder method) that are in the order that +// the SyncObserver expects them to be. A buffer is initially empty. +// +// The ordering produced by ChangeReorderBuffer is as follows: +// (a) All Deleted items appear first. +// (b) For Updated and/or Added items, parents appear before their children. +// (c) When there are changes to the sibling order (this means Added items, +// or Updated items with the |position_changed| parameter set to true), +// all siblings under a parent will appear in the output, even if they +// are not explicitly pushed. The sibling order will be preserved in +// the output list -- items will appear before their sibling-order +// successors. +// (d) When there are no changes to the sibling order under a parent node, +// the sibling order is not necessarily preserved in the output for +// its children. +class ChangeReorderBuffer { + public: + ChangeReorderBuffer(); + ~ChangeReorderBuffer(); + + // Insert an item, identified by the metahandle |id|, into the reorder + // buffer. This item will appear in the output list as an ACTION_ADD + // ChangeRecord. + void PushAddedItem(int64 id) { + operations_[id] = OP_ADD; + } + + // Insert an item, identified by the metahandle |id|, into the reorder + // buffer. This item will appear in the output list as an ACTION_DELETE + // ChangeRecord. + void PushDeletedItem(int64 id) { + operations_[id] = OP_DELETE; + } + + // Insert an item, identified by the metahandle |id|, into the reorder + // buffer. This item will appear in the output list as an ACTION_UPDATE + // ChangeRecord. Also, if |position_changed| is true, all siblings of this + // item will appear in the output list as well; if it wasn't explicitly + // pushed, the siblings will have an ACTION_UPDATE ChangeRecord. + void PushUpdatedItem(int64 id, bool position_changed) { + operations_[id] = position_changed ? OP_UPDATE_POSITION_AND_PROPERTIES : + OP_UPDATE_PROPERTIES_ONLY; + } + + void SetExtraDataForId(int64 id, ExtraPasswordChangeRecordData* extra) { + extra_data_[id] = make_linked_ptr<ExtraPasswordChangeRecordData>(extra); + } + + void SetSpecificsForId(int64 id, const sync_pb::EntitySpecifics& specifics) { + specifics_[id] = specifics; + } + + // Reset the buffer, forgetting any pushed items, so that it can be used + // again to reorder a new set of changes. + void Clear() { + operations_.clear(); + } + + bool IsEmpty() const { + return operations_.empty(); + } + + // Output a reordered list of changes to |changes| using the items + // that were pushed into the reorder buffer. |sync_trans| is used to + // determine the ordering. Returns true if successful, or false if + // an error was encountered. + bool GetAllChangesInTreeOrder( + const BaseTransaction* sync_trans, + ImmutableChangeRecordList* changes) WARN_UNUSED_RESULT; + + private: + class Traversal; + enum Operation { + OP_ADD, // AddedItem. + OP_DELETE, // DeletedItem. + OP_UPDATE_PROPERTIES_ONLY, // UpdatedItem with position_changed=0. + OP_UPDATE_POSITION_AND_PROPERTIES, // UpdatedItem with position_changed=1. + }; + typedef std::map<int64, Operation> OperationMap; + typedef std::map<int64, sync_pb::EntitySpecifics> SpecificsMap; + typedef std::map<int64, linked_ptr<ExtraPasswordChangeRecordData> > + ExtraDataMap; + + // Stores the items that have been pushed into the buffer, and the type of + // operation that was associated with them. + OperationMap operations_; + + // Stores entity-specific ChangeRecord data per-ID. + SpecificsMap specifics_; + + // Stores type-specific extra data per-ID. + ExtraDataMap extra_data_; + + DISALLOW_COPY_AND_ASSIGN(ChangeReorderBuffer); +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_CHANGE_REORDER_BUFFER_H_ diff --git a/sync/internal_api/configure_reason.h b/sync/internal_api/configure_reason.h new file mode 100644 index 0000000..6008f5f --- /dev/null +++ b/sync/internal_api/configure_reason.h @@ -0,0 +1,36 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_CONFIGURE_REASON_H_ +#define SYNC_INTERNAL_API_CONFIGURE_REASON_H_ +#pragma once + +namespace sync_api { + +// Note: This should confirm with the enums in sync.proto for +// GetUpdatesCallerInfo. They will have 1:1 mapping but this will only map +// to a subset of the GetUpdatesCallerInfo enum values. +enum ConfigureReason { + // We should never be here during actual configure. This is for setting + // default values. + CONFIGURE_REASON_UNKNOWN, + + // The client is configuring because the user opted to sync a different set + // of datatypes. + CONFIGURE_REASON_RECONFIGURATION, + + // The client is configuring because the client is being asked to migrate. + CONFIGURE_REASON_MIGRATION, + + // Setting up sync performs an initial config to download NIGORI data, and + // also a config to download initial data once the user selects types. + CONFIGURE_REASON_NEW_CLIENT, + + // A new datatype is enabled for syncing due to a client upgrade. + CONFIGURE_REASON_NEWLY_ENABLED_DATA_TYPE, +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_CONFIGURE_REASON_H_ diff --git a/sync/internal_api/debug_info_event_listener.cc b/sync/internal_api/debug_info_event_listener.cc new file mode 100644 index 0000000..53fc9b1 --- /dev/null +++ b/sync/internal_api/debug_info_event_listener.cc @@ -0,0 +1,174 @@ +// Copyright (c) 2012 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 "sync/internal_api/debug_info_event_listener.h" + +using browser_sync::sessions::SyncSessionSnapshot; +namespace sync_api { + +DebugInfoEventListener::DebugInfoEventListener() + : events_dropped_(false), + cryptographer_has_pending_keys_(false), + cryptographer_ready_(false) { +} + +DebugInfoEventListener::~DebugInfoEventListener() { +} + +void DebugInfoEventListener::OnSyncCycleCompleted( + const SyncSessionSnapshot* snapshot) { + if (!snapshot) + return; + + sync_pb::DebugEventInfo event_info; + sync_pb::SyncCycleCompletedEventInfo* sync_completed_event_info = + event_info.mutable_sync_cycle_completed_event_info(); + + sync_completed_event_info->set_num_encryption_conflicts( + snapshot->num_encryption_conflicts); + sync_completed_event_info->set_num_hierarchy_conflicts( + snapshot->num_hierarchy_conflicts); + sync_completed_event_info->set_num_simple_conflicts( + snapshot->num_simple_conflicts); + sync_completed_event_info->set_num_server_conflicts( + snapshot->num_server_conflicts); + + sync_completed_event_info->set_num_updates_downloaded( + snapshot->syncer_status.num_updates_downloaded_total); + sync_completed_event_info->set_num_reflected_updates_downloaded( + snapshot->syncer_status.num_reflected_updates_downloaded_total); + sync_completed_event_info->mutable_caller_info()->set_source( + snapshot->source.updates_source); + sync_completed_event_info->mutable_caller_info()->set_notifications_enabled( + snapshot->notifications_enabled); + + AddEventToQueue(event_info); +} + +void DebugInfoEventListener::OnInitializationComplete( + const browser_sync::WeakHandle<browser_sync::JsBackend>& js_backend, + bool success) { + CreateAndAddEvent(sync_pb::DebugEventInfo::INITIALIZATION_COMPLETE); +} + +void DebugInfoEventListener::OnConnectionStatusChange( + sync_api::ConnectionStatus status) { + CreateAndAddEvent(sync_pb::DebugEventInfo::CONNECTION_STATUS_CHANGE); +} + +void DebugInfoEventListener::OnPassphraseRequired( + sync_api::PassphraseRequiredReason reason, + const sync_pb::EncryptedData& pending_keys) { + CreateAndAddEvent(sync_pb::DebugEventInfo::PASSPHRASE_REQUIRED); +} + +void DebugInfoEventListener::OnPassphraseAccepted() { + CreateAndAddEvent(sync_pb::DebugEventInfo::PASSPHRASE_ACCEPTED); +} + +void DebugInfoEventListener::OnBootstrapTokenUpdated( + const std::string& bootstrap_token) { + CreateAndAddEvent(sync_pb::DebugEventInfo::BOOTSTRAP_TOKEN_UPDATED); +} + +void DebugInfoEventListener::OnStopSyncingPermanently() { + CreateAndAddEvent(sync_pb::DebugEventInfo::STOP_SYNCING_PERMANENTLY); +} + +void DebugInfoEventListener::OnUpdatedToken(const std::string& token) { + CreateAndAddEvent(sync_pb::DebugEventInfo::UPDATED_TOKEN); +} + +void DebugInfoEventListener::OnClearServerDataFailed() { + // This command is not implemented on the client side. + NOTREACHED(); +} + +void DebugInfoEventListener::OnClearServerDataSucceeded() { + // This command is not implemented on the client side. + NOTREACHED(); +} + +void DebugInfoEventListener::OnEncryptedTypesChanged( + syncable::ModelTypeSet encrypted_types, + bool encrypt_everything) { + CreateAndAddEvent(sync_pb::DebugEventInfo::ENCRYPTED_TYPES_CHANGED); +} + +void DebugInfoEventListener::OnEncryptionComplete() { + CreateAndAddEvent(sync_pb::DebugEventInfo::ENCRYPTION_COMPLETE); +} + +void DebugInfoEventListener::OnActionableError( + const browser_sync::SyncProtocolError& sync_error) { + CreateAndAddEvent(sync_pb::DebugEventInfo::ACTIONABLE_ERROR); +} + +void DebugInfoEventListener::SetCrytographerHasPendingKeys(bool pending_keys) { + cryptographer_has_pending_keys_ = pending_keys; +} + +void DebugInfoEventListener::SetCryptographerReady(bool ready) { + cryptographer_ready_ = ready; +} + +void DebugInfoEventListener::OnNudgeFromDatatype( + syncable::ModelType datatype) { + sync_pb::DebugEventInfo event_info; + event_info.set_nudging_datatype( + syncable::GetSpecificsFieldNumberFromModelType(datatype)); + AddEventToQueue(event_info); +} + +void DebugInfoEventListener::OnIncomingNotification( + const syncable::ModelTypePayloadMap& type_payloads) { + sync_pb::DebugEventInfo event_info; + syncable::ModelTypeSet types = ModelTypePayloadMapToEnumSet(type_payloads); + + for (syncable::ModelTypeSet::Iterator it = types.First(); + it.Good(); it.Inc()) { + event_info.add_datatypes_notified_from_server( + syncable::GetSpecificsFieldNumberFromModelType(it.Get())); + } + + AddEventToQueue(event_info); +} + +void DebugInfoEventListener::GetAndClearDebugInfo( + sync_pb::DebugInfo* debug_info) { + DCHECK(events_.size() <= sync_api::kMaxEntries); + while (!events_.empty()) { + sync_pb::DebugEventInfo* event_info = debug_info->add_events(); + const sync_pb::DebugEventInfo& debug_event_info = events_.front(); + event_info->CopyFrom(debug_event_info); + events_.pop(); + } + + debug_info->set_events_dropped(events_dropped_); + debug_info->set_cryptographer_ready(cryptographer_ready_); + debug_info->set_cryptographer_has_pending_keys( + cryptographer_has_pending_keys_); + + events_dropped_ = false; +} + +void DebugInfoEventListener::CreateAndAddEvent( + sync_pb::DebugEventInfo::EventType type) { + sync_pb::DebugEventInfo event_info; + event_info.set_type(type); + AddEventToQueue(event_info); +} + +void DebugInfoEventListener::AddEventToQueue( + const sync_pb::DebugEventInfo& event_info) { + if (events_.size() >= sync_api::kMaxEntries) { + DVLOG(1) << "DebugInfoEventListener::AddEventToQueue Dropping an old event " + << "because of full queue"; + + events_.pop(); + events_dropped_ = true; + } + events_.push(event_info); +} +} // namespace sync_api diff --git a/sync/internal_api/debug_info_event_listener.h b/sync/internal_api/debug_info_event_listener.h new file mode 100644 index 0000000..0169043 --- /dev/null +++ b/sync/internal_api/debug_info_event_listener.h @@ -0,0 +1,91 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_DEBUG_INFO_EVENT_LISTENER_H_ +#define SYNC_INTERNAL_API_DEBUG_INFO_EVENT_LISTENER_H_ + +#include <queue> +#include <string> + +#include "base/compiler_specific.h" +#include "sync/internal_api/sync_manager.h" +#include "sync/js/js_backend.h" +#include "sync/protocol/sync.pb.h" +#include "sync/sessions/debug_info_getter.h" +#include "sync/sessions/session_state.h" +#include "sync/util/weak_handle.h" + +namespace sync_api { + +const unsigned int kMaxEntries = 6; + +// Listens to events and records them in a queue. And passes the events to +// syncer when requested. +class DebugInfoEventListener : public sync_api::SyncManager::Observer, + public browser_sync::sessions::DebugInfoGetter { + public: + DebugInfoEventListener(); + virtual ~DebugInfoEventListener(); + + // SyncManager::Observer implementation. + virtual void OnSyncCycleCompleted( + const browser_sync::sessions::SyncSessionSnapshot* snapshot) OVERRIDE; + virtual void OnInitializationComplete( + const browser_sync::WeakHandle<browser_sync::JsBackend>& js_backend, + bool success) OVERRIDE; + virtual void OnConnectionStatusChange( + sync_api::ConnectionStatus connection_status) OVERRIDE; + virtual void OnPassphraseRequired( + sync_api::PassphraseRequiredReason reason, + const sync_pb::EncryptedData& pending_keys) OVERRIDE; + virtual void OnPassphraseAccepted() OVERRIDE; + virtual void OnBootstrapTokenUpdated( + const std::string& bootstrap_token) OVERRIDE; + virtual void OnStopSyncingPermanently() OVERRIDE; + virtual void OnUpdatedToken(const std::string& token) OVERRIDE; + virtual void OnClearServerDataFailed() OVERRIDE; + virtual void OnClearServerDataSucceeded() OVERRIDE; + virtual void OnEncryptedTypesChanged( + syncable::ModelTypeSet encrypted_types, + bool encrypt_everything) OVERRIDE; + virtual void OnEncryptionComplete() OVERRIDE; + virtual void OnActionableError( + const browser_sync::SyncProtocolError& sync_error) OVERRIDE; + + // Sync manager events. + void OnNudgeFromDatatype(syncable::ModelType datatype); + void OnIncomingNotification( + const syncable::ModelTypePayloadMap& type_payloads); + + // DebugInfoGetter Implementation. + virtual void GetAndClearDebugInfo(sync_pb::DebugInfo* debug_info) OVERRIDE; + + // Functions to set cryptographer state. + void SetCrytographerHasPendingKeys(bool pending_keys); + void SetCryptographerReady(bool ready); + + private: + FRIEND_TEST_ALL_PREFIXES(DebugInfoEventListenerTest, VerifyEventsAdded); + FRIEND_TEST_ALL_PREFIXES(DebugInfoEventListenerTest, VerifyQueueSize); + FRIEND_TEST_ALL_PREFIXES(DebugInfoEventListenerTest, VerifyGetAndClearEvents); + + void AddEventToQueue(const sync_pb::DebugEventInfo& event_info); + void CreateAndAddEvent(sync_pb::DebugEventInfo::EventType type); + std::queue<sync_pb::DebugEventInfo> events_; + + // True indicates we had to drop one or more events to keep our limit of + // |kMaxEntries|. + bool events_dropped_; + + // Cryptographer has keys that are not yet decrypted. + bool cryptographer_has_pending_keys_; + + // Cryptographer is initialized and does not have pending keys. + bool cryptographer_ready_; + + DISALLOW_COPY_AND_ASSIGN(DebugInfoEventListener); +}; + +} // namespace sync_api +#endif // SYNC_INTERNAL_API_DEBUG_INFO_EVENT_LISTENER_H_ diff --git a/sync/internal_api/debug_info_event_listener_unittest.cc b/sync/internal_api/debug_info_event_listener_unittest.cc new file mode 100644 index 0000000..801dafd --- /dev/null +++ b/sync/internal_api/debug_info_event_listener_unittest.cc @@ -0,0 +1,47 @@ +// Copyright (c) 2012 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 "sync/internal_api/debug_info_event_listener.h" +#include "testing/gtest/include/gtest/gtest.h" + +typedef testing::Test DebugInfoEventListenerTest; + +namespace sync_api { +TEST_F(DebugInfoEventListenerTest, VerifyEventsAdded) { + sync_api::DebugInfoEventListener debug_info_event_listener; + debug_info_event_listener.CreateAndAddEvent( + sync_pb::DebugEventInfo::ENCRYPTION_COMPLETE); + ASSERT_EQ(debug_info_event_listener.events_.size(), 1U); + const sync_pb::DebugEventInfo& debug_info = + debug_info_event_listener.events_.back(); + ASSERT_TRUE(debug_info.has_type()); + ASSERT_EQ(debug_info.type(), sync_pb::DebugEventInfo::ENCRYPTION_COMPLETE); +} + +TEST_F(DebugInfoEventListenerTest, VerifyQueueSize) { + sync_api::DebugInfoEventListener debug_info_event_listener; + for (int i = 0; i < 10; ++i) { + debug_info_event_listener.CreateAndAddEvent( + sync_pb::DebugEventInfo::ENCRYPTION_COMPLETE); + } + ASSERT_EQ(debug_info_event_listener.events_.size(), + sync_api::kMaxEntries); +} + +TEST_F(DebugInfoEventListenerTest, VerifyGetAndClearEvents) { + sync_api::DebugInfoEventListener debug_info_event_listener; + debug_info_event_listener.CreateAndAddEvent( + sync_pb::DebugEventInfo::ENCRYPTION_COMPLETE); + ASSERT_EQ(debug_info_event_listener.events_.size(), 1U); + sync_pb::DebugInfo debug_info; + debug_info_event_listener.GetAndClearDebugInfo(&debug_info); + ASSERT_EQ(debug_info_event_listener.events_.size(), 0U); + ASSERT_EQ(debug_info.events_size(), 1); + ASSERT_TRUE(debug_info.events(0).has_type()); + ASSERT_EQ(debug_info.events(0).type(), + sync_pb::DebugEventInfo::ENCRYPTION_COMPLETE); +} + +} // namespace sync_api diff --git a/sync/internal_api/http_post_provider_factory.h b/sync/internal_api/http_post_provider_factory.h new file mode 100644 index 0000000..445fd6a --- /dev/null +++ b/sync/internal_api/http_post_provider_factory.h @@ -0,0 +1,34 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_HTTP_POST_PROVIDER_FACTORY_H_ +#define SYNC_INTERNAL_API_HTTP_POST_PROVIDER_FACTORY_H_ +#pragma once + +namespace sync_api { + +class HttpPostProviderInterface; + +// A factory to create HttpPostProviders to hide details about the +// implementations and dependencies. +// A factory instance itself should be owned by whomever uses it to create +// HttpPostProviders. +class HttpPostProviderFactory { + public: + virtual ~HttpPostProviderFactory() {} + + // Obtain a new HttpPostProviderInterface instance, owned by caller. + virtual HttpPostProviderInterface* Create() = 0; + + // When the interface is no longer needed (ready to be cleaned up), clients + // must call Destroy(). + // This allows actual HttpPostProvider subclass implementations to be + // reference counted, which is useful if a particular implementation uses + // multiple threads to serve network requests. + virtual void Destroy(HttpPostProviderInterface* http) = 0; +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_HTTP_POST_PROVIDER_FACTORY_H_ diff --git a/sync/internal_api/http_post_provider_interface.h b/sync/internal_api/http_post_provider_interface.h new file mode 100644 index 0000000..10b02b5 --- /dev/null +++ b/sync/internal_api/http_post_provider_interface.h @@ -0,0 +1,66 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_HTTP_POST_PROVIDER_INTERFACE_H_ +#define SYNC_INTERNAL_API_HTTP_POST_PROVIDER_INTERFACE_H_ +#pragma once + +#include <string> + +namespace sync_api { + +// An interface the embedding application (e.g. Chromium) implements to provide +// required HTTP POST functionality to the syncer backend. This interface is +// designed for one-time use. You create one, use it, and create another if you +// want to make a subsequent POST. +class HttpPostProviderInterface { + public: + virtual ~HttpPostProviderInterface() {} + + // Use specified user agent string when POSTing. If not called a default UA + // may be used. + virtual void SetUserAgent(const char* user_agent) = 0; + + // Add additional headers to the request. + virtual void SetExtraRequestHeaders(const char* headers) = 0; + + // Set the URL to POST to. + virtual void SetURL(const char* url, int port) = 0; + + // Set the type, length and content of the POST payload. + // |content_type| is a null-terminated MIME type specifier. + // |content| is a data buffer; Do not interpret as a null-terminated string. + // |content_length| is the total number of chars in |content|. It is used to + // assign/copy |content| data. + virtual void SetPostPayload(const char* content_type, + int content_length, + const char* content) = 0; + + // Returns true if the URL request succeeded. If the request failed, + // error() may be non-zero and hence contain more information. + virtual bool MakeSynchronousPost(int* error_code, int* response_code) = 0; + + // Get the length of the content returned in the HTTP response. + // This does not count the trailing null-terminating character returned + // by GetResponseContent, so it is analogous to calling string.length. + virtual int GetResponseContentLength() const = 0; + + // Get the content returned in the HTTP response. + // This is a null terminated string of characters. + // Value should be copied. + virtual const char* GetResponseContent() const = 0; + + // Get the value of a header returned in the HTTP response. + // If the header is not present, returns the empty string. + virtual const std::string GetResponseHeaderValue( + const std::string& name) const = 0; + + // Abandon any pending POST and unblock caller in MakeSynchronousPost. + // This must be safe to call from any thread. + virtual void Abort() = 0; +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_HTTP_POST_PROVIDER_INTERFACE_H_ diff --git a/sync/internal_api/js_mutation_event_observer.cc b/sync/internal_api/js_mutation_event_observer.cc new file mode 100644 index 0000000..9833b09 --- /dev/null +++ b/sync/internal_api/js_mutation_event_observer.cc @@ -0,0 +1,112 @@ +// Copyright (c) 2012 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 "sync/internal_api/js_mutation_event_observer.h" + +#include <string> + +#include "base/location.h" +#include "base/logging.h" +#include "base/string_number_conversions.h" +#include "base/values.h" +#include "sync/js/js_event_details.h" +#include "sync/js/js_event_handler.h" + +namespace browser_sync { + +JsMutationEventObserver::JsMutationEventObserver() + : weak_ptr_factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)) {} + +JsMutationEventObserver::~JsMutationEventObserver() { + DCHECK(non_thread_safe_.CalledOnValidThread()); +} + +base::WeakPtr<JsMutationEventObserver> JsMutationEventObserver::AsWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +void JsMutationEventObserver::InvalidateWeakPtrs() { + weak_ptr_factory_.InvalidateWeakPtrs(); +} + +void JsMutationEventObserver::SetJsEventHandler( + const WeakHandle<JsEventHandler>& event_handler) { + event_handler_ = event_handler; +} + +namespace { + +// Max number of changes we attempt to convert to values (to avoid +// running out of memory). +const size_t kChangeLimit = 100; + +} // namespace + +void JsMutationEventObserver::OnChangesApplied( + syncable::ModelType model_type, + int64 write_transaction_id, + const sync_api::ImmutableChangeRecordList& changes) { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.SetString("modelType", syncable::ModelTypeToString(model_type)); + details.SetString("writeTransactionId", + base::Int64ToString(write_transaction_id)); + base::Value* changes_value = NULL; + const size_t changes_size = changes.Get().size(); + if (changes_size <= kChangeLimit) { + ListValue* changes_list = new ListValue(); + for (sync_api::ChangeRecordList::const_iterator it = + changes.Get().begin(); it != changes.Get().end(); ++it) { + changes_list->Append(it->ToValue()); + } + changes_value = changes_list; + } else { + changes_value = + Value::CreateStringValue( + base::Uint64ToString(static_cast<uint64>(changes_size)) + + " changes"); + } + details.Set("changes", changes_value); + HandleJsEvent(FROM_HERE, "onChangesApplied", JsEventDetails(&details)); +} + +void JsMutationEventObserver::OnChangesComplete( + syncable::ModelType model_type) { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.SetString("modelType", syncable::ModelTypeToString(model_type)); + HandleJsEvent(FROM_HERE, "onChangesComplete", JsEventDetails(&details)); +} + +void JsMutationEventObserver::OnTransactionWrite( + const syncable::ImmutableWriteTransactionInfo& write_transaction_info, + syncable::ModelTypeSet models_with_changes) { + DCHECK(non_thread_safe_.CalledOnValidThread()); + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.Set("writeTransactionInfo", + write_transaction_info.Get().ToValue(kChangeLimit)); + details.Set("modelsWithChanges", + syncable::ModelTypeSetToValue(models_with_changes)); + HandleJsEvent(FROM_HERE, "onTransactionWrite", JsEventDetails(&details)); +} + +void JsMutationEventObserver::HandleJsEvent( + const tracked_objects::Location& from_here, + const std::string& name, const JsEventDetails& details) { + if (!event_handler_.IsInitialized()) { + NOTREACHED(); + return; + } + event_handler_.Call(from_here, + &JsEventHandler::HandleJsEvent, name, details); +} + +} // namespace browser_sync diff --git a/sync/internal_api/js_mutation_event_observer.h b/sync/internal_api/js_mutation_event_observer.h new file mode 100644 index 0000000..60a33a2 --- /dev/null +++ b/sync/internal_api/js_mutation_event_observer.h @@ -0,0 +1,70 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_JS_MUTATION_EVENT_OBSERVER_H_ +#define SYNC_INTERNAL_API_JS_MUTATION_EVENT_OBSERVER_H_ +#pragma once + +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/non_thread_safe.h" +#include "sync/internal_api/sync_manager.h" +#include "sync/syncable/transaction_observer.h" +#include "sync/util/weak_handle.h" + +namespace tracked_objects { +class Location; +} // namespace tracked_objects + +namespace browser_sync { + +class JsEventDetails; +class JsEventHandler; + +// Observes all change- and transaction-related events and routes a +// summarized version to a JsEventHandler. +class JsMutationEventObserver + : public sync_api::SyncManager::ChangeObserver, + public syncable::TransactionObserver { + public: + JsMutationEventObserver(); + + virtual ~JsMutationEventObserver(); + + base::WeakPtr<JsMutationEventObserver> AsWeakPtr(); + + void InvalidateWeakPtrs(); + + void SetJsEventHandler(const WeakHandle<JsEventHandler>& event_handler); + + // sync_api::SyncManager::ChangeObserver implementation. + virtual void OnChangesApplied( + syncable::ModelType model_type, + int64 write_transaction_id, + const sync_api::ImmutableChangeRecordList& changes) OVERRIDE; + virtual void OnChangesComplete(syncable::ModelType model_type) OVERRIDE; + + // syncable::TransactionObserver implementation. + virtual void OnTransactionWrite( + const syncable::ImmutableWriteTransactionInfo& write_transaction_info, + syncable::ModelTypeSet models_with_changes) OVERRIDE; + + private: + base::NonThreadSafe non_thread_safe_; + base::WeakPtrFactory<JsMutationEventObserver> weak_ptr_factory_; + WeakHandle<JsEventHandler> event_handler_; + + void HandleJsEvent( + const tracked_objects::Location& from_here, + const std::string& name, const JsEventDetails& details); + + DISALLOW_COPY_AND_ASSIGN(JsMutationEventObserver); +}; + +} // namespace browser_sync + +#endif // SYNC_INTERNAL_API_JS_MUTATION_EVENT_OBSERVER_H_ diff --git a/sync/internal_api/js_mutation_event_observer_unittest.cc b/sync/internal_api/js_mutation_event_observer_unittest.cc new file mode 100644 index 0000000..4092b82 --- /dev/null +++ b/sync/internal_api/js_mutation_event_observer_unittest.cc @@ -0,0 +1,127 @@ +// Copyright (c) 2012 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 "sync/internal_api/js_mutation_event_observer.h" + +#include "base/basictypes.h" +#include "base/message_loop.h" +#include "base/values.h" +#include "sync/js/js_event_details.h" +#include "sync/js/js_test_util.h" +#include "sync/syncable/model_type.h" +#include "sync/util/weak_handle.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace browser_sync { +namespace { + +using ::testing::InSequence; +using ::testing::StrictMock; + +class JsMutationEventObserverTest : public testing::Test { + protected: + JsMutationEventObserverTest() { + js_mutation_event_observer_.SetJsEventHandler( + mock_js_event_handler_.AsWeakHandle()); + } + + private: + // This must be destroyed after the member variables below in order + // for WeakHandles to be destroyed properly. + MessageLoop message_loop_; + + protected: + StrictMock<MockJsEventHandler> mock_js_event_handler_; + JsMutationEventObserver js_mutation_event_observer_; + + void PumpLoop() { + message_loop_.RunAllPending(); + } +}; + +TEST_F(JsMutationEventObserverTest, OnChangesApplied) { + InSequence dummy; + + // We don't test with passwords as that requires additional setup. + + // Build a list of example ChangeRecords. + sync_api::ChangeRecord changes[syncable::MODEL_TYPE_COUNT]; + for (int i = syncable::AUTOFILL_PROFILE; + i < syncable::MODEL_TYPE_COUNT; ++i) { + changes[i].id = i; + switch (i % 3) { + case 0: + changes[i].action = + sync_api::ChangeRecord::ACTION_ADD; + break; + case 1: + changes[i].action = + sync_api::ChangeRecord::ACTION_UPDATE; + break; + default: + changes[i].action = + sync_api::ChangeRecord::ACTION_DELETE; + break; + } + } + + // For each i, we call OnChangesApplied() with the first arg equal + // to i cast to ModelType and the second argument with the changes + // starting from changes[i]. + + // Set expectations for each data type. + for (int i = syncable::AUTOFILL_PROFILE; + i < syncable::MODEL_TYPE_COUNT; ++i) { + const std::string& model_type_str = + syncable::ModelTypeToString(syncable::ModelTypeFromInt(i)); + DictionaryValue expected_details; + expected_details.SetString("modelType", model_type_str); + expected_details.SetString("writeTransactionId", "0"); + ListValue* expected_changes = new ListValue(); + expected_details.Set("changes", expected_changes); + for (int j = i; j < syncable::MODEL_TYPE_COUNT; ++j) { + expected_changes->Append(changes[j].ToValue()); + } + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onChangesApplied", + HasDetailsAsDictionary(expected_details))); + } + + // Fire OnChangesApplied() for each data type. + for (int i = syncable::AUTOFILL_PROFILE; + i < syncable::MODEL_TYPE_COUNT; ++i) { + sync_api::ChangeRecordList + local_changes(changes + i, changes + arraysize(changes)); + js_mutation_event_observer_.OnChangesApplied( + syncable::ModelTypeFromInt(i), + 0, sync_api::ImmutableChangeRecordList(&local_changes)); + } + + PumpLoop(); +} + +TEST_F(JsMutationEventObserverTest, OnChangesComplete) { + InSequence dummy; + + for (int i = syncable::FIRST_REAL_MODEL_TYPE; + i < syncable::MODEL_TYPE_COUNT; ++i) { + DictionaryValue expected_details; + expected_details.SetString( + "modelType", + syncable::ModelTypeToString(syncable::ModelTypeFromInt(i))); + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onChangesComplete", + HasDetailsAsDictionary(expected_details))); + } + + for (int i = syncable::FIRST_REAL_MODEL_TYPE; + i < syncable::MODEL_TYPE_COUNT; ++i) { + js_mutation_event_observer_.OnChangesComplete( + syncable::ModelTypeFromInt(i)); + } + PumpLoop(); +} + +} // namespace +} // namespace browser_sync diff --git a/sync/internal_api/js_sync_manager_observer.cc b/sync/internal_api/js_sync_manager_observer.cc new file mode 100644 index 0000000..b9cf9f3 --- /dev/null +++ b/sync/internal_api/js_sync_manager_observer.cc @@ -0,0 +1,169 @@ +// Copyright (c) 2012 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 "sync/internal_api/js_sync_manager_observer.h" + +#include <cstddef> + +#include "base/location.h" +#include "base/logging.h" +#include "base/string_number_conversions.h" +#include "base/values.h" +#include "sync/internal_api/change_record.h" +#include "sync/js/js_arg_list.h" +#include "sync/js/js_event_details.h" +#include "sync/js/js_event_handler.h" +#include "sync/sessions/session_state.h" +#include "sync/syncable/model_type.h" + +namespace browser_sync { + +using browser_sync::SyncProtocolError; + +JsSyncManagerObserver::JsSyncManagerObserver() {} + +JsSyncManagerObserver::~JsSyncManagerObserver() {} + +void JsSyncManagerObserver::SetJsEventHandler( + const WeakHandle<JsEventHandler>& event_handler) { + event_handler_ = event_handler; +} + +void JsSyncManagerObserver::OnSyncCycleCompleted( + const sessions::SyncSessionSnapshot* snapshot) { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.Set("snapshot", snapshot->ToValue()); + HandleJsEvent(FROM_HERE, "onSyncCycleCompleted", JsEventDetails(&details)); +} + +void JsSyncManagerObserver::OnConnectionStatusChange( + sync_api::ConnectionStatus status) { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.SetString("status", sync_api::ConnectionStatusToString(status)); + HandleJsEvent(FROM_HERE, + "onConnectionStatusChange", JsEventDetails(&details)); +} + +void JsSyncManagerObserver::OnUpdatedToken(const std::string& token) { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.SetString("token", "<redacted>"); + HandleJsEvent(FROM_HERE, "onUpdatedToken", JsEventDetails(&details)); +} + +void JsSyncManagerObserver::OnPassphraseRequired( + sync_api::PassphraseRequiredReason reason, + const sync_pb::EncryptedData& pending_keys) { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.SetString("reason", + sync_api::PassphraseRequiredReasonToString(reason)); + HandleJsEvent(FROM_HERE, "onPassphraseRequired", JsEventDetails(&details)); +} + +void JsSyncManagerObserver::OnPassphraseAccepted() { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + HandleJsEvent(FROM_HERE, "onPassphraseAccepted", JsEventDetails(&details)); +} + +void JsSyncManagerObserver::OnBootstrapTokenUpdated( + const std::string& boostrap_token) { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.SetString("bootstrapToken", "<redacted>"); + HandleJsEvent(FROM_HERE, "OnBootstrapTokenUpdated", JsEventDetails(&details)); +} + +void JsSyncManagerObserver::OnEncryptedTypesChanged( + syncable::ModelTypeSet encrypted_types, + bool encrypt_everything) { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.Set("encryptedTypes", + syncable::ModelTypeSetToValue(encrypted_types)); + details.SetBoolean("encryptEverything", encrypt_everything); + HandleJsEvent(FROM_HERE, + "onEncryptedTypesChanged", JsEventDetails(&details)); +} + +void JsSyncManagerObserver::OnEncryptionComplete() { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + HandleJsEvent(FROM_HERE, "onEncryptionComplete", JsEventDetails()); +} + +void JsSyncManagerObserver::OnActionableError( + const SyncProtocolError& sync_error) { + if (!event_handler_.IsInitialized()) { + return; + } + DictionaryValue details; + details.Set("syncError", sync_error.ToValue()); + HandleJsEvent(FROM_HERE, "onActionableError", + JsEventDetails(&details)); +} + +void JsSyncManagerObserver::OnInitializationComplete( + const WeakHandle<JsBackend>& js_backend, + bool success) { + if (!event_handler_.IsInitialized()) { + return; + } + // Ignore the |js_backend| argument; it's not really convertible to + // JSON anyway. + HandleJsEvent(FROM_HERE, "onInitializationComplete", JsEventDetails()); +} + +void JsSyncManagerObserver::OnStopSyncingPermanently() { + if (!event_handler_.IsInitialized()) { + return; + } + HandleJsEvent(FROM_HERE, "onStopSyncingPermanently", JsEventDetails()); +} + +void JsSyncManagerObserver::OnClearServerDataSucceeded() { + if (!event_handler_.IsInitialized()) { + return; + } + HandleJsEvent(FROM_HERE, "onClearServerDataSucceeded", JsEventDetails()); +} + +void JsSyncManagerObserver::OnClearServerDataFailed() { + if (!event_handler_.IsInitialized()) { + return; + } + HandleJsEvent(FROM_HERE, "onClearServerDataFailed", JsEventDetails()); +} + +void JsSyncManagerObserver::HandleJsEvent( + const tracked_objects::Location& from_here, + const std::string& name, const JsEventDetails& details) { + if (!event_handler_.IsInitialized()) { + NOTREACHED(); + return; + } + event_handler_.Call(from_here, + &JsEventHandler::HandleJsEvent, name, details); +} + +} // namespace browser_sync diff --git a/sync/internal_api/js_sync_manager_observer.h b/sync/internal_api/js_sync_manager_observer.h new file mode 100644 index 0000000..025cfa1 --- /dev/null +++ b/sync/internal_api/js_sync_manager_observer.h @@ -0,0 +1,69 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_JS_SYNC_MANAGER_OBSERVER_H_ +#define SYNC_INTERNAL_API_JS_SYNC_MANAGER_OBSERVER_H_ +#pragma once + +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "sync/internal_api/sync_manager.h" +#include "sync/protocol/sync_protocol_error.h" +#include "sync/util/weak_handle.h" + +namespace tracked_objects { +class Location; +} // namespace tracked_objects + +namespace browser_sync { + +class JsEventDetails; +class JsEventHandler; + +// Routes SyncManager events to a JsEventHandler. +class JsSyncManagerObserver : public sync_api::SyncManager::Observer { + public: + JsSyncManagerObserver(); + virtual ~JsSyncManagerObserver(); + + void SetJsEventHandler(const WeakHandle<JsEventHandler>& event_handler); + + // sync_api::SyncManager::Observer implementation. + virtual void OnSyncCycleCompleted( + const sessions::SyncSessionSnapshot* snapshot) OVERRIDE; + virtual void OnConnectionStatusChange( + sync_api::ConnectionStatus status) OVERRIDE; + virtual void OnUpdatedToken(const std::string& token) OVERRIDE; + virtual void OnPassphraseRequired( + sync_api::PassphraseRequiredReason reason, + const sync_pb::EncryptedData& pending_keys) OVERRIDE; + virtual void OnPassphraseAccepted() OVERRIDE; + virtual void OnBootstrapTokenUpdated( + const std::string& bootstrap_token) OVERRIDE; + virtual void OnEncryptedTypesChanged( + syncable::ModelTypeSet encrypted_types, + bool encrypt_everything) OVERRIDE; + virtual void OnEncryptionComplete() OVERRIDE; + virtual void OnInitializationComplete( + const WeakHandle<JsBackend>& js_backend, bool success) OVERRIDE; + virtual void OnStopSyncingPermanently() OVERRIDE; + virtual void OnClearServerDataSucceeded() OVERRIDE; + virtual void OnClearServerDataFailed() OVERRIDE; + virtual void OnActionableError( + const browser_sync::SyncProtocolError& sync_protocol_error) OVERRIDE; + + private: + void HandleJsEvent(const tracked_objects::Location& from_here, + const std::string& name, const JsEventDetails& details); + + WeakHandle<JsEventHandler> event_handler_; + + DISALLOW_COPY_AND_ASSIGN(JsSyncManagerObserver); +}; + +} // namespace browser_sync + +#endif // SYNC_INTERNAL_API_JS_SYNC_MANAGER_OBSERVER_H_ diff --git a/sync/internal_api/js_sync_manager_observer_unittest.cc b/sync/internal_api/js_sync_manager_observer_unittest.cc new file mode 100644 index 0000000..e2f63ab --- /dev/null +++ b/sync/internal_api/js_sync_manager_observer_unittest.cc @@ -0,0 +1,221 @@ +// Copyright (c) 2012 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 "sync/internal_api/js_sync_manager_observer.h" + +#include "base/basictypes.h" +#include "base/location.h" +#include "base/message_loop.h" +#include "base/values.h" +#include "sync/js/js_event_details.h" +#include "sync/js/js_test_util.h" +#include "sync/protocol/sync_protocol_error.h" +#include "sync/sessions/session_state.h" +#include "sync/syncable/model_type.h" +#include "sync/util/weak_handle.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace browser_sync { +namespace { + +using ::testing::InSequence; +using ::testing::StrictMock; + +class JsSyncManagerObserverTest : public testing::Test { + protected: + JsSyncManagerObserverTest() { + js_sync_manager_observer_.SetJsEventHandler( + mock_js_event_handler_.AsWeakHandle()); + } + + private: + // This must be destroyed after the member variables below in order + // for WeakHandles to be destroyed properly. + MessageLoop message_loop_; + + protected: + StrictMock<MockJsEventHandler> mock_js_event_handler_; + JsSyncManagerObserver js_sync_manager_observer_; + + void PumpLoop() { + message_loop_.RunAllPending(); + } +}; + +TEST_F(JsSyncManagerObserverTest, NoArgNotifiations) { + InSequence dummy; + + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onInitializationComplete", + HasDetails(JsEventDetails()))); + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onStopSyncingPermanently", + HasDetails(JsEventDetails()))); + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onClearServerDataSucceeded", + HasDetails(JsEventDetails()))); + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onClearServerDataFailed", + HasDetails(JsEventDetails()))); + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onEncryptionComplete", + HasDetails(JsEventDetails()))); + + js_sync_manager_observer_.OnInitializationComplete(WeakHandle<JsBackend>(), + true); + js_sync_manager_observer_.OnStopSyncingPermanently(); + js_sync_manager_observer_.OnClearServerDataSucceeded(); + js_sync_manager_observer_.OnClearServerDataFailed(); + js_sync_manager_observer_.OnEncryptionComplete(); + PumpLoop(); +} + +TEST_F(JsSyncManagerObserverTest, OnSyncCycleCompleted) { + std::string download_progress_markers[syncable::MODEL_TYPE_COUNT]; + sessions::SyncSessionSnapshot snapshot(sessions::SyncerStatus(), + sessions::ErrorCounters(), + 100, + false, + syncable::ModelTypeSet(), + download_progress_markers, + false, + true, + 100, + 8, + 5, + 2, + 7, + false, + sessions::SyncSourceInfo(), + false, + 0, + base::Time::Now(), + false); + DictionaryValue expected_details; + expected_details.Set("snapshot", snapshot.ToValue()); + + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onSyncCycleCompleted", + HasDetailsAsDictionary(expected_details))); + + js_sync_manager_observer_.OnSyncCycleCompleted(&snapshot); + PumpLoop(); +} + +TEST_F(JsSyncManagerObserverTest, OnActionableError) { + browser_sync::SyncProtocolError sync_error; + sync_error.action = browser_sync::CLEAR_USER_DATA_AND_RESYNC; + sync_error.error_type = browser_sync::TRANSIENT_ERROR; + DictionaryValue expected_details; + expected_details.Set("syncError", sync_error.ToValue()); + + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onActionableError", + HasDetailsAsDictionary(expected_details))); + + js_sync_manager_observer_.OnActionableError(sync_error); + PumpLoop(); +} + + +TEST_F(JsSyncManagerObserverTest, OnConnectionStatusChange) { + const sync_api::ConnectionStatus kStatus = + sync_api::CONNECTION_AUTH_ERROR; + DictionaryValue expected_details; + expected_details.SetString("status", + sync_api::ConnectionStatusToString(kStatus)); + + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onConnectionStatusChange", + HasDetailsAsDictionary(expected_details))); + + js_sync_manager_observer_.OnConnectionStatusChange(kStatus); + PumpLoop(); +} + +TEST_F(JsSyncManagerObserverTest, OnPassphraseRequired) { + InSequence dummy; + + DictionaryValue reason_passphrase_not_required_details; + DictionaryValue reason_encryption_details; + DictionaryValue reason_decryption_details; + + reason_passphrase_not_required_details.SetString( + "reason", + sync_api::PassphraseRequiredReasonToString( + sync_api::REASON_PASSPHRASE_NOT_REQUIRED)); + reason_encryption_details.SetString( + "reason", + sync_api::PassphraseRequiredReasonToString(sync_api::REASON_ENCRYPTION)); + reason_decryption_details.SetString( + "reason", + sync_api::PassphraseRequiredReasonToString(sync_api::REASON_DECRYPTION)); + + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onPassphraseRequired", + HasDetailsAsDictionary( + reason_passphrase_not_required_details))); + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onPassphraseRequired", + HasDetailsAsDictionary(reason_encryption_details))); + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onPassphraseRequired", + HasDetailsAsDictionary(reason_decryption_details))); + + js_sync_manager_observer_.OnPassphraseRequired( + sync_api::REASON_PASSPHRASE_NOT_REQUIRED, + sync_pb::EncryptedData()); + js_sync_manager_observer_.OnPassphraseRequired(sync_api::REASON_ENCRYPTION, + sync_pb::EncryptedData()); + js_sync_manager_observer_.OnPassphraseRequired(sync_api::REASON_DECRYPTION, + sync_pb::EncryptedData()); + PumpLoop(); +} + +TEST_F(JsSyncManagerObserverTest, SensitiveNotifiations) { + DictionaryValue redacted_token_details; + redacted_token_details.SetString("token", "<redacted>"); + DictionaryValue redacted_bootstrap_token_details; + redacted_bootstrap_token_details.SetString("bootstrapToken", "<redacted>"); + + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onUpdatedToken", + HasDetailsAsDictionary(redacted_token_details))); + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent( + "OnBootstrapTokenUpdated", + HasDetailsAsDictionary(redacted_bootstrap_token_details))); + + js_sync_manager_observer_.OnUpdatedToken("sensitive_token"); + js_sync_manager_observer_.OnBootstrapTokenUpdated("sensitive_token"); + PumpLoop(); +} + +TEST_F(JsSyncManagerObserverTest, OnEncryptedTypesChanged) { + DictionaryValue expected_details; + ListValue* encrypted_type_values = new ListValue(); + const bool encrypt_everything = false; + expected_details.Set("encryptedTypes", encrypted_type_values); + expected_details.SetBoolean("encryptEverything", encrypt_everything); + syncable::ModelTypeSet encrypted_types; + + for (int i = syncable::FIRST_REAL_MODEL_TYPE; + i < syncable::MODEL_TYPE_COUNT; ++i) { + syncable::ModelType type = syncable::ModelTypeFromInt(i); + encrypted_types.Put(type); + encrypted_type_values->Append(Value::CreateStringValue( + syncable::ModelTypeToString(type))); + } + + EXPECT_CALL(mock_js_event_handler_, + HandleJsEvent("onEncryptedTypesChanged", + HasDetailsAsDictionary(expected_details))); + + js_sync_manager_observer_.OnEncryptedTypesChanged( + encrypted_types, encrypt_everything); + PumpLoop(); +} + +} // namespace +} // namespace browser_sync diff --git a/sync/internal_api/read_node.cc b/sync/internal_api/read_node.cc new file mode 100644 index 0000000..00ee6bc --- /dev/null +++ b/sync/internal_api/read_node.cc @@ -0,0 +1,97 @@ +// Copyright (c) 2012 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 "sync/internal_api/read_node.h" + +#include "base/logging.h" +#include "sync/internal_api/base_transaction.h" +#include "sync/syncable/syncable.h" + +namespace sync_api { + +////////////////////////////////////////////////////////////////////////// +// ReadNode member definitions +ReadNode::ReadNode(const BaseTransaction* transaction) + : entry_(NULL), transaction_(transaction) { + DCHECK(transaction); +} + +ReadNode::ReadNode() { + entry_ = NULL; + transaction_ = NULL; +} + +ReadNode::~ReadNode() { + delete entry_; +} + +void ReadNode::InitByRootLookup() { + DCHECK(!entry_) << "Init called twice"; + syncable::BaseTransaction* trans = transaction_->GetWrappedTrans(); + entry_ = new syncable::Entry(trans, syncable::GET_BY_ID, trans->root_id()); + if (!entry_->good()) + DCHECK(false) << "Could not lookup root node for reading."; +} + +BaseNode::InitByLookupResult ReadNode::InitByIdLookup(int64 id) { + DCHECK(!entry_) << "Init called twice"; + DCHECK_NE(id, kInvalidId); + syncable::BaseTransaction* trans = transaction_->GetWrappedTrans(); + entry_ = new syncable::Entry(trans, syncable::GET_BY_HANDLE, id); + if (!entry_->good()) + return INIT_FAILED_ENTRY_NOT_GOOD; + if (entry_->Get(syncable::IS_DEL)) + return INIT_FAILED_ENTRY_IS_DEL; + syncable::ModelType model_type = GetModelType(); + LOG_IF(WARNING, model_type == syncable::UNSPECIFIED || + model_type == syncable::TOP_LEVEL_FOLDER) + << "SyncAPI InitByIdLookup referencing unusual object."; + return DecryptIfNecessary() ? INIT_OK : INIT_FAILED_DECRYPT_IF_NECESSARY; +} + +BaseNode::InitByLookupResult ReadNode::InitByClientTagLookup( + syncable::ModelType model_type, + const std::string& tag) { + DCHECK(!entry_) << "Init called twice"; + if (tag.empty()) + return INIT_FAILED_PRECONDITION; + + const std::string hash = GenerateSyncableHash(model_type, tag); + + entry_ = new syncable::Entry(transaction_->GetWrappedTrans(), + syncable::GET_BY_CLIENT_TAG, hash); + if (!entry_->good()) + return INIT_FAILED_ENTRY_NOT_GOOD; + if (entry_->Get(syncable::IS_DEL)) + return INIT_FAILED_ENTRY_IS_DEL; + return DecryptIfNecessary() ? INIT_OK : INIT_FAILED_DECRYPT_IF_NECESSARY; +} + +const syncable::Entry* ReadNode::GetEntry() const { + return entry_; +} + +const BaseTransaction* ReadNode::GetTransaction() const { + return transaction_; +} + +BaseNode::InitByLookupResult ReadNode::InitByTagLookup( + const std::string& tag) { + DCHECK(!entry_) << "Init called twice"; + if (tag.empty()) + return INIT_FAILED_PRECONDITION; + syncable::BaseTransaction* trans = transaction_->GetWrappedTrans(); + entry_ = new syncable::Entry(trans, syncable::GET_BY_SERVER_TAG, tag); + if (!entry_->good()) + return INIT_FAILED_ENTRY_NOT_GOOD; + if (entry_->Get(syncable::IS_DEL)) + return INIT_FAILED_ENTRY_IS_DEL; + syncable::ModelType model_type = GetModelType(); + LOG_IF(WARNING, model_type == syncable::UNSPECIFIED || + model_type == syncable::TOP_LEVEL_FOLDER) + << "SyncAPI InitByTagLookup referencing unusually typed object."; + return DecryptIfNecessary() ? INIT_OK : INIT_FAILED_DECRYPT_IF_NECESSARY; +} + +} // namespace sync_api diff --git a/sync/internal_api/read_node.h b/sync/internal_api/read_node.h new file mode 100644 index 0000000..ea48660 --- /dev/null +++ b/sync/internal_api/read_node.h @@ -0,0 +1,66 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_READ_NODE_H_ +#define SYNC_INTERNAL_API_READ_NODE_H_ +#pragma once + +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "sync/internal_api/base_node.h" +#include "sync/syncable/model_type.h" + +namespace sync_api { + +// ReadNode wraps a syncable::Entry to provide the functionality of a +// read-only BaseNode. +class ReadNode : public BaseNode { + public: + // Create an unpopulated ReadNode on the given transaction. Call some flavor + // of Init to populate the ReadNode with a database entry. + explicit ReadNode(const BaseTransaction* transaction); + virtual ~ReadNode(); + + // A client must use one (and only one) of the following Init variants to + // populate the node. + + // BaseNode implementation. + virtual InitByLookupResult InitByIdLookup(int64 id) OVERRIDE; + virtual InitByLookupResult InitByClientTagLookup( + syncable::ModelType model_type, + const std::string& tag) OVERRIDE; + + // There is always a root node, so this can't fail. The root node is + // never mutable, so root lookup is only possible on a ReadNode. + void InitByRootLookup(); + + // Each server-created permanent node is tagged with a unique string. + // Look up the node with the particular tag. If it does not exist, + // return false. + InitByLookupResult InitByTagLookup(const std::string& tag); + + // Implementation of BaseNode's abstract virtual accessors. + virtual const syncable::Entry* GetEntry() const OVERRIDE; + virtual const BaseTransaction* GetTransaction() const OVERRIDE; + + protected: + ReadNode(); + + private: + void* operator new(size_t size); // Node is meant for stack use only. + + // The underlying syncable object which this class wraps. + syncable::Entry* entry_; + + // The sync API transaction that is the parent of this node. + const BaseTransaction* transaction_; + + DISALLOW_COPY_AND_ASSIGN(ReadNode); +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_READ_NODE_H_ diff --git a/sync/internal_api/read_transaction.cc b/sync/internal_api/read_transaction.cc new file mode 100644 index 0000000..5c38f6c --- /dev/null +++ b/sync/internal_api/read_transaction.cc @@ -0,0 +1,38 @@ +// Copyright (c) 2012 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 "sync/internal_api/read_transaction.h" + +#include "sync/syncable/syncable.h" + +namespace sync_api { + +////////////////////////////////////////////////////////////////////////// +// ReadTransaction member definitions +ReadTransaction::ReadTransaction(const tracked_objects::Location& from_here, + UserShare* share) + : BaseTransaction(share), + transaction_(NULL), + close_transaction_(true) { + transaction_ = new syncable::ReadTransaction(from_here, + share->directory.get()); +} + +ReadTransaction::ReadTransaction(UserShare* share, + syncable::BaseTransaction* trans) + : BaseTransaction(share), + transaction_(trans), + close_transaction_(false) {} + +ReadTransaction::~ReadTransaction() { + if (close_transaction_) { + delete transaction_; + } +} + +syncable::BaseTransaction* ReadTransaction::GetWrappedTrans() const { + return transaction_; +} + +} // namespace sync_api diff --git a/sync/internal_api/read_transaction.h b/sync/internal_api/read_transaction.h new file mode 100644 index 0000000..e45acc6 --- /dev/null +++ b/sync/internal_api/read_transaction.h @@ -0,0 +1,46 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_READ_TRANSACTION_H_ +#define SYNC_INTERNAL_API_READ_TRANSACTION_H_ + +#include "base/compiler_specific.h" +#include "sync/internal_api/base_transaction.h" + +namespace tracked_objects { +class Location; +} // namespace tracked_objects + +namespace sync_api { + +struct UserShare; + +// Sync API's ReadTransaction is a read-only BaseTransaction. It wraps +// a syncable::ReadTransaction. +class ReadTransaction : public BaseTransaction { + public: + // Start a new read-only transaction on the specified repository. + ReadTransaction(const tracked_objects::Location& from_here, + UserShare* share); + + // Resume the middle of a transaction. Will not close transaction. + ReadTransaction(UserShare* share, syncable::BaseTransaction* trans); + + virtual ~ReadTransaction(); + + // BaseTransaction override. + virtual syncable::BaseTransaction* GetWrappedTrans() const OVERRIDE; + private: + void* operator new(size_t size); // Transaction is meant for stack use only. + + // The underlying syncable object which this class wraps. + syncable::BaseTransaction* transaction_; + bool close_transaction_; + + DISALLOW_COPY_AND_ASSIGN(ReadTransaction); +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_READ_TRANSACTION_H_ diff --git a/sync/internal_api/sync_manager.cc b/sync/internal_api/sync_manager.cc new file mode 100644 index 0000000..ebe870e --- /dev/null +++ b/sync/internal_api/sync_manager.cc @@ -0,0 +1,2575 @@ +// Copyright (c) 2012 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 "sync/internal_api/sync_manager.h" + +#include <string> + +#include "base/base64.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/command_line.h" +#include "base/compiler_specific.h" +#include "base/json/json_writer.h" +#include "base/memory/ref_counted.h" +#include "base/metrics/histogram.h" +#include "base/observer_list.h" +#include "base/string_number_conversions.h" +#include "base/values.h" +#include "net/base/network_change_notifier.h" +#include "sync/engine/net/server_connection_manager.h" +#include "sync/engine/nigori_util.h" +#include "sync/engine/polling_constants.h" +#include "sync/engine/sync_scheduler.h" +#include "sync/engine/syncer_types.h" +#include "sync/internal_api/all_status.h" +#include "sync/internal_api/base_node.h" +#include "sync/internal_api/change_reorder_buffer.h" +#include "sync/internal_api/configure_reason.h" +#include "sync/internal_api/debug_info_event_listener.h" +#include "sync/internal_api/js_mutation_event_observer.h" +#include "sync/internal_api/js_sync_manager_observer.h" +#include "sync/internal_api/read_node.h" +#include "sync/internal_api/read_transaction.h" +#include "sync/internal_api/syncapi_internal.h" +#include "sync/internal_api/syncapi_server_connection_manager.h" +#include "sync/internal_api/user_share.h" +#include "sync/internal_api/write_node.h" +#include "sync/internal_api/write_transaction.h" +#include "sync/js/js_arg_list.h" +#include "sync/js/js_backend.h" +#include "sync/js/js_event_details.h" +#include "sync/js/js_event_handler.h" +#include "sync/js/js_reply_handler.h" +#include "sync/notifier/sync_notifier.h" +#include "sync/notifier/sync_notifier_observer.h" +#include "sync/protocol/encryption.pb.h" +#include "sync/protocol/proto_value_conversions.h" +#include "sync/protocol/sync.pb.h" +#include "sync/syncable/directory_change_delegate.h" +#include "sync/syncable/model_type.h" +#include "sync/syncable/model_type_payload_map.h" +#include "sync/syncable/syncable.h" +#include "sync/util/cryptographer.h" +#include "sync/util/get_session_name.h" +#include "sync/util/time.h" + +using base::TimeDelta; +using browser_sync::AllStatus; +using browser_sync::Cryptographer; +using browser_sync::Encryptor; +using browser_sync::JsArgList; +using browser_sync::JsBackend; +using browser_sync::JsEventDetails; +using browser_sync::JsEventHandler; +using browser_sync::JsEventHandler; +using browser_sync::JsReplyHandler; +using browser_sync::JsMutationEventObserver; +using browser_sync::JsSyncManagerObserver; +using browser_sync::ModelSafeWorkerRegistrar; +using browser_sync::kNigoriTag; +using browser_sync::KeyParams; +using browser_sync::ModelSafeRoutingInfo; +using browser_sync::ReportUnrecoverableErrorFunction; +using browser_sync::ServerConnectionEvent; +using browser_sync::ServerConnectionEventListener; +using browser_sync::SyncEngineEvent; +using browser_sync::SyncEngineEventListener; +using browser_sync::SyncScheduler; +using browser_sync::Syncer; +using browser_sync::UnrecoverableErrorHandler; +using browser_sync::WeakHandle; +using browser_sync::sessions::SyncSessionContext; +using syncable::ImmutableWriteTransactionInfo; +using syncable::ModelType; +using syncable::ModelTypeSet; +using syncable::SPECIFICS; +using sync_pb::GetUpdatesCallerInfo; + +namespace { + +// Delays for syncer nudges. +static const int kSyncRefreshDelayMsec = 500; +static const int kSyncSchedulerDelayMsec = 250; + +GetUpdatesCallerInfo::GetUpdatesSource GetSourceFromReason( + sync_api::ConfigureReason reason) { + switch (reason) { + case sync_api::CONFIGURE_REASON_RECONFIGURATION: + return GetUpdatesCallerInfo::RECONFIGURATION; + case sync_api::CONFIGURE_REASON_MIGRATION: + return GetUpdatesCallerInfo::MIGRATION; + case sync_api::CONFIGURE_REASON_NEW_CLIENT: + return GetUpdatesCallerInfo::NEW_CLIENT; + case sync_api::CONFIGURE_REASON_NEWLY_ENABLED_DATA_TYPE: + return GetUpdatesCallerInfo::NEWLY_SUPPORTED_DATATYPE; + default: + NOTREACHED(); + } + + return GetUpdatesCallerInfo::UNKNOWN; +} + +// The maximum number of times we will automatically overwrite the nigori node +// because the encryption keys don't match (per chrome instantiation). +static const int kNigoriOverwriteLimit = 10; + +} // namespace + +namespace sync_api { + +const int SyncManager::kDefaultNudgeDelayMilliseconds = 200; +const int SyncManager::kPreferencesNudgeDelayMilliseconds = 2000; + +// Maximum count and size for traffic recorder. +const unsigned int kMaxMessagesToRecord = 10; +const unsigned int kMaxMessageSizeToRecord = 5 * 1024; + +////////////////////////////////////////////////////////////////////////// +// SyncManager's implementation: SyncManager::SyncInternal +class SyncManager::SyncInternal + : public net::NetworkChangeNotifier::IPAddressObserver, + public browser_sync::Cryptographer::Observer, + public sync_notifier::SyncNotifierObserver, + public JsBackend, + public SyncEngineEventListener, + public ServerConnectionEventListener, + public syncable::DirectoryChangeDelegate { + public: + explicit SyncInternal(const std::string& name) + : name_(name), + weak_ptr_factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)), + enable_sync_tabs_for_other_clients_(false), + registrar_(NULL), + change_delegate_(NULL), + initialized_(false), + testing_mode_(NON_TEST), + observing_ip_address_changes_(false), + traffic_recorder_(kMaxMessagesToRecord, kMaxMessageSizeToRecord), + encryptor_(NULL), + unrecoverable_error_handler_(NULL), + report_unrecoverable_error_function_(NULL), + created_on_loop_(MessageLoop::current()), + nigori_overwrite_count_(0) { + // Pre-fill |notification_info_map_|. + for (int i = syncable::FIRST_REAL_MODEL_TYPE; + i < syncable::MODEL_TYPE_COUNT; ++i) { + notification_info_map_.insert( + std::make_pair(syncable::ModelTypeFromInt(i), NotificationInfo())); + } + + // Bind message handlers. + BindJsMessageHandler( + "getNotificationState", + &SyncManager::SyncInternal::GetNotificationState); + BindJsMessageHandler( + "getNotificationInfo", + &SyncManager::SyncInternal::GetNotificationInfo); + BindJsMessageHandler( + "getRootNodeDetails", + &SyncManager::SyncInternal::GetRootNodeDetails); + BindJsMessageHandler( + "getNodeSummariesById", + &SyncManager::SyncInternal::GetNodeSummariesById); + BindJsMessageHandler( + "getNodeDetailsById", + &SyncManager::SyncInternal::GetNodeDetailsById); + BindJsMessageHandler( + "getAllNodes", + &SyncManager::SyncInternal::GetAllNodes); + BindJsMessageHandler( + "getChildNodeIds", + &SyncManager::SyncInternal::GetChildNodeIds); + BindJsMessageHandler( + "getClientServerTraffic", + &SyncManager::SyncInternal::GetClientServerTraffic); + } + + virtual ~SyncInternal() { + CHECK(!initialized_); + } + + bool Init(const FilePath& database_location, + const WeakHandle<JsEventHandler>& event_handler, + const std::string& sync_server_and_path, + int port, + bool use_ssl, + const scoped_refptr<base::TaskRunner>& blocking_task_runner, + HttpPostProviderFactory* post_factory, + ModelSafeWorkerRegistrar* model_safe_worker_registrar, + browser_sync::ExtensionsActivityMonitor* + extensions_activity_monitor, + ChangeDelegate* change_delegate, + const std::string& user_agent, + const SyncCredentials& credentials, + bool enable_sync_tabs_for_other_clients, + sync_notifier::SyncNotifier* sync_notifier, + const std::string& restored_key_for_bootstrapping, + TestingMode testing_mode, + Encryptor* encryptor, + UnrecoverableErrorHandler* unrecoverable_error_handler, + ReportUnrecoverableErrorFunction + report_unrecoverable_error_function); + + // Sign into sync with given credentials. + // We do not verify the tokens given. After this call, the tokens are set + // and the sync DB is open. True if successful, false if something + // went wrong. + bool SignIn(const SyncCredentials& credentials); + + // Update tokens that we're using in Sync. Email must stay the same. + void UpdateCredentials(const SyncCredentials& credentials); + + // Called when the user disables or enables a sync type. + void UpdateEnabledTypes(); + + // Conditionally sets the flag in the Nigori node which instructs other + // clients to start syncing tabs. + void MaybeSetSyncTabsInNigoriNode(ModelTypeSet enabled_types); + + // Tell the sync engine to start the syncing process. + void StartSyncingNormally(); + + // Whether or not the Nigori node is encrypted using an explicit passphrase. + bool IsUsingExplicitPassphrase(); + + // Update the Cryptographer from the current nigori node and write back any + // necessary changes to the nigori node. We also detect missing encryption + // keys and write them into the nigori node. + // Also updates or adds the device information into the nigori node. + // Note: opens a transaction and can trigger an ON_PASSPHRASE_REQUIRED, so + // should only be called after syncapi is fully initialized. + // Calls the callback argument with true if cryptographer is ready, false + // otherwise. + void UpdateCryptographerAndNigori( + const std::string& chrome_version, + const base::Closure& done_callback); + + // Stores the current set of encryption keys (if the cryptographer is ready) + // and encrypted types into the nigori node. + void UpdateNigoriEncryptionState(Cryptographer* cryptographer, + WriteNode* nigori_node); + + // Updates the nigori node with any new encrypted types and then + // encrypts the nodes for those new data types as well as other + // nodes that should be encrypted but aren't. Triggers + // OnPassphraseRequired if the cryptographer isn't ready. + void RefreshEncryption(); + + // Re-encrypts the encrypted data types using the passed passphrase, and sets + // a flag in the nigori node specifying whether the current passphrase is + // explicit (custom passphrase) or non-explicit (GAIA). If the existing + // encryption passphrase is "explicit", the data cannot be re-encrypted and + // SetEncryptionPassphrase will do nothing. + // If !is_explicit and there are pending keys, we will attempt to decrypt them + // using this passphrase. If this fails, we will save this encryption key to + // be applied later after the pending keys are resolved. + // Calls FinishSetPassphrase at the end, which notifies observers of the + // result of the set passphrase operation, updates the nigori node, and does + // re-encryption. + void SetEncryptionPassphrase(const std::string& passphrase, bool is_explicit); + + // Provides a passphrase for decrypting the user's existing sync data. Calls + // FinishSetPassphrase at the end, which notifies observers of the result of + // the set passphrase operation, updates the nigori node, and does + // re-encryption. + void SetDecryptionPassphrase(const std::string& passphrase); + + // The final step of SetEncryptionPassphrase and SetDecryptionPassphrase that + // notifies observers of the result of the set passphrase operation, updates + // the nigori node, and does re-encryption. + // |success|: true if the operation was successful and false otherwise. If + // success == false, we send an OnPassphraseRequired notification. + // |bootstrap_token|: used to inform observers if the cryptographer's + // bootstrap token was updated. + // |is_explicit|: used to differentiate between a custom passphrase (true) and + // a GAIA passphrase that is implicitly used for encryption + // (false). + // |trans| and |nigori_node|: used to access data in the cryptographer. + void FinishSetPassphrase( + bool success, + const std::string& bootstrap_token, + bool is_explicit, + WriteTransaction* trans, + WriteNode* nigori_node); + + // Call periodically from a database-safe thread to persist recent changes + // to the syncapi model. + void SaveChanges(); + + // DirectoryChangeDelegate implementation. + // This listener is called upon completion of a syncable transaction, and + // builds the list of sync-engine initiated changes that will be forwarded to + // the SyncManager's Observers. + virtual void HandleTransactionCompleteChangeEvent( + ModelTypeSet models_with_changes) OVERRIDE; + virtual ModelTypeSet HandleTransactionEndingChangeEvent( + const ImmutableWriteTransactionInfo& write_transaction_info, + syncable::BaseTransaction* trans) OVERRIDE; + virtual void HandleCalculateChangesChangeEventFromSyncApi( + const ImmutableWriteTransactionInfo& write_transaction_info, + syncable::BaseTransaction* trans) OVERRIDE; + virtual void HandleCalculateChangesChangeEventFromSyncer( + const ImmutableWriteTransactionInfo& write_transaction_info, + syncable::BaseTransaction* trans) OVERRIDE; + + // Open the directory named with username_for_share + bool OpenDirectory(); + + // Cryptographer::Observer implementation. + virtual void OnEncryptedTypesChanged( + syncable::ModelTypeSet encrypted_types, + bool encrypt_everything) OVERRIDE; + + // SyncNotifierObserver implementation. + virtual void OnNotificationStateChange( + bool notifications_enabled) OVERRIDE; + + virtual void OnIncomingNotification( + const syncable::ModelTypePayloadMap& type_payloads, + sync_notifier::IncomingNotificationSource source) OVERRIDE; + + virtual void StoreState(const std::string& cookie) OVERRIDE; + + void AddObserver(SyncManager::Observer* observer); + void RemoveObserver(SyncManager::Observer* observer); + + // Accessors for the private members. + syncable::Directory* directory() { return share_.directory.get(); } + SyncAPIServerConnectionManager* connection_manager() { + return connection_manager_.get(); + } + SyncScheduler* scheduler() const { return scheduler_.get(); } + UserShare* GetUserShare() { + DCHECK(initialized_); + return &share_; + } + + // Return the currently active (validated) username for use with syncable + // types. + const std::string& username_for_share() const { + return share_.name; + } + + Status GetStatus(); + + void RequestNudge(const tracked_objects::Location& nudge_location); + + void RequestNudgeForDataTypes( + const tracked_objects::Location& nudge_location, + ModelTypeSet type); + + TimeDelta GetNudgeDelayTimeDelta(const ModelType& model_type); + + void NotifyCryptographerState(Cryptographer* cryptographer); + + // See SyncManager::Shutdown* for information. + void StopSyncingForShutdown(const base::Closure& callback); + void ShutdownOnSyncThread(); + + // If this is a deletion for a password, sets the legacy + // ExtraPasswordChangeRecordData field of |buffer|. Otherwise sets + // |buffer|'s specifics field to contain the unencrypted data. + void SetExtraChangeRecordData(int64 id, + syncable::ModelType type, + ChangeReorderBuffer* buffer, + Cryptographer* cryptographer, + const syncable::EntryKernel& original, + bool existed_before, + bool exists_now); + + // Called only by our NetworkChangeNotifier. + virtual void OnIPAddressChanged() OVERRIDE; + + bool InitialSyncEndedForAllEnabledTypes() { + syncable::ModelTypeSet types; + ModelSafeRoutingInfo enabled_types; + registrar_->GetModelSafeRoutingInfo(&enabled_types); + for (ModelSafeRoutingInfo::const_iterator i = enabled_types.begin(); + i != enabled_types.end(); ++i) { + types.Put(i->first); + } + + return InitialSyncEndedForTypes(types, &share_); + } + + // SyncEngineEventListener implementation. + virtual void OnSyncEngineEvent(const SyncEngineEvent& event) OVERRIDE; + + // ServerConnectionEventListener implementation. + virtual void OnServerConnectionEvent( + const ServerConnectionEvent& event) OVERRIDE; + + // JsBackend implementation. + virtual void SetJsEventHandler( + const WeakHandle<JsEventHandler>& event_handler) OVERRIDE; + virtual void ProcessJsMessage( + const std::string& name, const JsArgList& args, + const WeakHandle<JsReplyHandler>& reply_handler) OVERRIDE; + + private: + struct NotificationInfo { + int total_count; + std::string payload; + + NotificationInfo() : total_count(0) {} + + ~NotificationInfo() {} + + // Returned pointer owned by the caller. + DictionaryValue* ToValue() const { + DictionaryValue* value = new DictionaryValue(); + value->SetInteger("totalCount", total_count); + value->SetString("payload", payload); + return value; + } + }; + + typedef std::map<syncable::ModelType, NotificationInfo> NotificationInfoMap; + typedef JsArgList + (SyncManager::SyncInternal::*UnboundJsMessageHandler)(const JsArgList&); + typedef base::Callback<JsArgList(const JsArgList&)> JsMessageHandler; + typedef std::map<std::string, JsMessageHandler> JsMessageHandlerMap; + + // Internal callback of UpdateCryptographerAndNigoriCallback. + void UpdateCryptographerAndNigoriCallback( + const std::string& chrome_version, + const base::Closure& done_callback, + const std::string& session_name); + + // Determine if the parents or predecessors differ between the old and new + // versions of an entry stored in |a| and |b|. Note that a node's index may + // change without its NEXT_ID changing if the node at NEXT_ID also moved (but + // the relative order is unchanged). To handle such cases, we rely on the + // caller to treat a position update on any sibling as updating the positions + // of all siblings. + static bool VisiblePositionsDiffer( + const syncable::EntryKernelMutation& mutation) { + const syncable::EntryKernel& a = mutation.original; + const syncable::EntryKernel& b = mutation.mutated; + // If the datatype isn't one where the browser model cares about position, + // don't bother notifying that data model of position-only changes. + if (!ShouldMaintainPosition( + syncable::GetModelTypeFromSpecifics(b.ref(SPECIFICS)))) + return false; + if (a.ref(syncable::NEXT_ID) != b.ref(syncable::NEXT_ID)) + return true; + if (a.ref(syncable::PARENT_ID) != b.ref(syncable::PARENT_ID)) + return true; + return false; + } + + // Determine if any of the fields made visible to clients of the Sync API + // differ between the versions of an entry stored in |a| and |b|. A return + // value of false means that it should be OK to ignore this change. + static bool VisiblePropertiesDiffer( + const syncable::EntryKernelMutation& mutation, + Cryptographer* cryptographer) { + const syncable::EntryKernel& a = mutation.original; + const syncable::EntryKernel& b = mutation.mutated; + const sync_pb::EntitySpecifics& a_specifics = a.ref(SPECIFICS); + const sync_pb::EntitySpecifics& b_specifics = b.ref(SPECIFICS); + DCHECK_EQ(syncable::GetModelTypeFromSpecifics(a_specifics), + syncable::GetModelTypeFromSpecifics(b_specifics)); + syncable::ModelType model_type = + syncable::GetModelTypeFromSpecifics(b_specifics); + // Suppress updates to items that aren't tracked by any browser model. + if (model_type < syncable::FIRST_REAL_MODEL_TYPE || + !a.ref(syncable::UNIQUE_SERVER_TAG).empty()) { + return false; + } + if (a.ref(syncable::IS_DIR) != b.ref(syncable::IS_DIR)) + return true; + if (!AreSpecificsEqual(cryptographer, + a.ref(syncable::SPECIFICS), + b.ref(syncable::SPECIFICS))) { + return true; + } + // We only care if the name has changed if neither specifics is encrypted + // (encrypted nodes blow away the NON_UNIQUE_NAME). + if (!a_specifics.has_encrypted() && !b_specifics.has_encrypted() && + a.ref(syncable::NON_UNIQUE_NAME) != b.ref(syncable::NON_UNIQUE_NAME)) + return true; + if (VisiblePositionsDiffer(mutation)) + return true; + return false; + } + + bool ChangeBuffersAreEmpty() { + for (int i = 0; i < syncable::MODEL_TYPE_COUNT; ++i) { + if (!change_buffers_[i].IsEmpty()) + return false; + } + return true; + } + + void ReEncryptEverything(WriteTransaction* trans); + + // Called for every notification. This updates the notification statistics + // to be displayed in about:sync. + void UpdateNotificationInfo( + const syncable::ModelTypePayloadMap& type_payloads); + + // Checks for server reachabilty and requests a nudge. + void OnIPAddressChangedImpl(); + + // Helper function used only by the constructor. + void BindJsMessageHandler( + const std::string& name, UnboundJsMessageHandler unbound_message_handler); + + // Returned pointer is owned by the caller. + static DictionaryValue* NotificationInfoToValue( + const NotificationInfoMap& notification_info); + + // JS message handlers. + JsArgList GetNotificationState(const JsArgList& args); + JsArgList GetNotificationInfo(const JsArgList& args); + JsArgList GetRootNodeDetails(const JsArgList& args); + JsArgList GetAllNodes(const JsArgList& args); + JsArgList GetNodeSummariesById(const JsArgList& args); + JsArgList GetNodeDetailsById(const JsArgList& args); + JsArgList GetChildNodeIds(const JsArgList& args); + JsArgList GetClientServerTraffic(const JsArgList& args); + + FilePath database_path_; + + const std::string name_; + + base::ThreadChecker thread_checker_; + + base::WeakPtrFactory<SyncInternal> weak_ptr_factory_; + + // Thread-safe handle used by + // HandleCalculateChangesChangeEventFromSyncApi(), which can be + // called from any thread. Valid only between between calls to + // Init() and Shutdown(). + // + // TODO(akalin): Ideally, we wouldn't need to store this; instead, + // we'd have another worker class which implements + // HandleCalculateChangesChangeEventFromSyncApi() and we'd pass it a + // WeakHandle when we construct it. + WeakHandle<SyncInternal> weak_handle_this_; + + // |blocking_task_runner| is a TaskRunner to be used for tasks that + // may block on disk I/O. + scoped_refptr<base::TaskRunner> blocking_task_runner_; + + // We give a handle to share_ to clients of the API for use when constructing + // any transaction type. + UserShare share_; + + // This can be called from any thread, but only between calls to + // OpenDirectory() and ShutdownOnSyncThread(). + browser_sync::WeakHandle<SyncManager::ChangeObserver> change_observer_; + + ObserverList<SyncManager::Observer> observers_; + + // The ServerConnectionManager used to abstract communication between the + // client (the Syncer) and the sync server. + scoped_ptr<SyncAPIServerConnectionManager> connection_manager_; + + // The scheduler that runs the Syncer. Needs to be explicitly + // Start()ed. + scoped_ptr<SyncScheduler> scheduler_; + + bool enable_sync_tabs_for_other_clients_; + + // The SyncNotifier which notifies us when updates need to be downloaded. + scoped_ptr<sync_notifier::SyncNotifier> sync_notifier_; + + // A multi-purpose status watch object that aggregates stats from various + // sync components. + AllStatus allstatus_; + + // Each element of this array is a store of change records produced by + // HandleChangeEvent during the CALCULATE_CHANGES step. The changes are + // segregated by model type, and are stored here to be processed and + // forwarded to the observer slightly later, at the TRANSACTION_ENDING + // step by HandleTransactionEndingChangeEvent. The list is cleared in the + // TRANSACTION_COMPLETE step by HandleTransactionCompleteChangeEvent. + ChangeReorderBuffer change_buffers_[syncable::MODEL_TYPE_COUNT]; + + // The entity that provides us with information about which types to sync. + // The instance is shared between the SyncManager and the Syncer. + ModelSafeWorkerRegistrar* registrar_; + + SyncManager::ChangeDelegate* change_delegate_; + + // Set to true once Init has been called. + bool initialized_; + + // Controls the disabling of certain SyncManager features. + // Can be used to disable communication with the server and the use of an + // on-disk file for maintaining syncer state. + // TODO(117836): Clean up implementation of SyncManager unit tests. + TestingMode testing_mode_; + + bool observing_ip_address_changes_; + + // Map used to store the notification info to be displayed in + // about:sync page. + NotificationInfoMap notification_info_map_; + + // These are for interacting with chrome://sync-internals. + JsMessageHandlerMap js_message_handlers_; + WeakHandle<JsEventHandler> js_event_handler_; + JsSyncManagerObserver js_sync_manager_observer_; + JsMutationEventObserver js_mutation_event_observer_; + + // This is for keeping track of client events to send to the server. + DebugInfoEventListener debug_info_event_listener_; + + browser_sync::TrafficRecorder traffic_recorder_; + + Encryptor* encryptor_; + UnrecoverableErrorHandler* unrecoverable_error_handler_; + ReportUnrecoverableErrorFunction report_unrecoverable_error_function_; + + MessageLoop* const created_on_loop_; + + // The number of times we've automatically (i.e. not via SetPassphrase or + // conflict resolver) updated the nigori's encryption keys in this chrome + // instantiation. + int nigori_overwrite_count_; +}; + +// A class to calculate nudge delays for types. +class NudgeStrategy { + public: + static TimeDelta GetNudgeDelayTimeDelta(const ModelType& model_type, + SyncManager::SyncInternal* core) { + NudgeDelayStrategy delay_type = GetNudgeDelayStrategy(model_type); + return GetNudgeDelayTimeDeltaFromType(delay_type, + model_type, + core); + } + + private: + // Possible types of nudge delay for datatypes. + // Note: These are just hints. If a sync happens then all dirty entries + // would be committed as part of the sync. + enum NudgeDelayStrategy { + // Sync right away. + IMMEDIATE, + + // Sync this change while syncing another change. + ACCOMPANY_ONLY, + + // The datatype does not use one of the predefined wait times but defines + // its own wait time logic for nudge. + CUSTOM, + }; + + static NudgeDelayStrategy GetNudgeDelayStrategy(const ModelType& type) { + switch (type) { + case syncable::AUTOFILL: + return ACCOMPANY_ONLY; + case syncable::PREFERENCES: + case syncable::SESSIONS: + return CUSTOM; + default: + return IMMEDIATE; + } + } + + static TimeDelta GetNudgeDelayTimeDeltaFromType( + const NudgeDelayStrategy& delay_type, const ModelType& model_type, + const SyncManager::SyncInternal* core) { + CHECK(core); + TimeDelta delay = TimeDelta::FromMilliseconds( + SyncManager::kDefaultNudgeDelayMilliseconds); + switch (delay_type) { + case IMMEDIATE: + delay = TimeDelta::FromMilliseconds( + SyncManager::kDefaultNudgeDelayMilliseconds); + break; + case ACCOMPANY_ONLY: + delay = TimeDelta::FromSeconds( + browser_sync::kDefaultShortPollIntervalSeconds); + break; + case CUSTOM: + switch (model_type) { + case syncable::PREFERENCES: + delay = TimeDelta::FromMilliseconds( + SyncManager::kPreferencesNudgeDelayMilliseconds); + break; + case syncable::SESSIONS: + delay = core->scheduler()->sessions_commit_delay(); + break; + default: + NOTREACHED(); + } + break; + default: + NOTREACHED(); + } + return delay; + } +}; + +SyncManager::ChangeDelegate::~ChangeDelegate() {} + +SyncManager::ChangeObserver::~ChangeObserver() {} + +SyncManager::Observer::~Observer() {} + +SyncManager::SyncManager(const std::string& name) + : data_(new SyncInternal(name)) {} + +SyncManager::Status::Status() + : notifications_enabled(false), + notifications_received(0), + unsynced_count(0), + encryption_conflicts(0), + hierarchy_conflicts(0), + simple_conflicts(0), + server_conflicts(0), + committed_count(0), + syncing(false), + initial_sync_ended(false), + updates_available(0), + updates_received(0), + reflected_updates_received(0), + tombstone_updates_received(0), + num_local_overwrites_total(0), + num_server_overwrites_total(0), + nonempty_get_updates(0), + empty_get_updates(0), + sync_cycles_with_commits(0), + sync_cycles_without_commits(0), + useless_sync_cycles(0), + useful_sync_cycles(0), + cryptographer_ready(false), + crypto_has_pending_keys(false) { +} + +SyncManager::Status::~Status() { +} + +bool SyncManager::Init( + const FilePath& database_location, + const WeakHandle<JsEventHandler>& event_handler, + const std::string& sync_server_and_path, + int sync_server_port, + bool use_ssl, + const scoped_refptr<base::TaskRunner>& blocking_task_runner, + HttpPostProviderFactory* post_factory, + ModelSafeWorkerRegistrar* registrar, + browser_sync::ExtensionsActivityMonitor* extensions_activity_monitor, + ChangeDelegate* change_delegate, + const std::string& user_agent, + const SyncCredentials& credentials, + bool enable_sync_tabs_for_other_clients, + sync_notifier::SyncNotifier* sync_notifier, + const std::string& restored_key_for_bootstrapping, + TestingMode testing_mode, + Encryptor* encryptor, + UnrecoverableErrorHandler* unrecoverable_error_handler, + ReportUnrecoverableErrorFunction report_unrecoverable_error_function) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(post_factory); + DVLOG(1) << "SyncManager starting Init..."; + std::string server_string(sync_server_and_path); + return data_->Init(database_location, + event_handler, + server_string, + sync_server_port, + use_ssl, + blocking_task_runner, + post_factory, + registrar, + extensions_activity_monitor, + change_delegate, + user_agent, + credentials, + enable_sync_tabs_for_other_clients, + sync_notifier, + restored_key_for_bootstrapping, + testing_mode, + encryptor, + unrecoverable_error_handler, + report_unrecoverable_error_function); +} + +void SyncManager::UpdateCredentials(const SyncCredentials& credentials) { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->UpdateCredentials(credentials); +} + +void SyncManager::UpdateEnabledTypes() { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->UpdateEnabledTypes(); +} + +void SyncManager::MaybeSetSyncTabsInNigoriNode( + ModelTypeSet enabled_types) { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->MaybeSetSyncTabsInNigoriNode(enabled_types); +} + +void SyncManager::ThrowUnrecoverableError() { + DCHECK(thread_checker_.CalledOnValidThread()); + ReadTransaction trans(FROM_HERE, GetUserShare()); + trans.GetWrappedTrans()->OnUnrecoverableError( + FROM_HERE, "Simulating unrecoverable error for testing purposes."); +} + +bool SyncManager::InitialSyncEndedForAllEnabledTypes() { + return data_->InitialSyncEndedForAllEnabledTypes(); +} + +void SyncManager::StartSyncingNormally() { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->StartSyncingNormally(); +} + +void SyncManager::SetEncryptionPassphrase(const std::string& passphrase, + bool is_explicit) { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->SetEncryptionPassphrase(passphrase, is_explicit); +} + +void SyncManager::SetDecryptionPassphrase(const std::string& passphrase) { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->SetDecryptionPassphrase(passphrase); +} + +void SyncManager::EnableEncryptEverything() { + DCHECK(thread_checker_.CalledOnValidThread()); + { + // Update the cryptographer to know we're now encrypting everything. + WriteTransaction trans(FROM_HERE, GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + // Only set encrypt everything if we know we can encrypt. This allows the + // user to cancel encryption if they have forgotten their passphrase. + if (cryptographer->is_ready()) + cryptographer->set_encrypt_everything(); + } + + // Reads from cryptographer so will automatically encrypt all + // datatypes and update the nigori node as necessary. Will trigger + // OnPassphraseRequired if necessary. + data_->RefreshEncryption(); +} + +bool SyncManager::EncryptEverythingEnabledForTest() const { + ReadTransaction trans(FROM_HERE, GetUserShare()); + return trans.GetCryptographer()->encrypt_everything(); +} + +bool SyncManager::IsUsingExplicitPassphrase() { + return data_ && data_->IsUsingExplicitPassphrase(); +} + +void SyncManager::RequestCleanupDisabledTypes() { + DCHECK(thread_checker_.CalledOnValidThread()); + if (data_->scheduler()) + data_->scheduler()->ScheduleCleanupDisabledTypes(); +} + +void SyncManager::RequestClearServerData() { + DCHECK(thread_checker_.CalledOnValidThread()); + if (data_->scheduler()) + data_->scheduler()->ScheduleClearUserData(); +} + +void SyncManager::RequestConfig( + ModelTypeSet types, ConfigureReason reason) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (!data_->scheduler()) { + LOG(INFO) + << "SyncManager::RequestConfig: bailing out because scheduler is " + << "null"; + return; + } + StartConfigurationMode(base::Closure()); + data_->scheduler()->ScheduleConfig(types, GetSourceFromReason(reason)); +} + +void SyncManager::StartConfigurationMode(const base::Closure& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (!data_->scheduler()) { + LOG(INFO) + << "SyncManager::StartConfigurationMode: could not start " + << "configuration mode because because scheduler is null"; + return; + } + data_->scheduler()->Start( + browser_sync::SyncScheduler::CONFIGURATION_MODE, callback); +} + +bool SyncManager::SyncInternal::Init( + const FilePath& database_location, + const WeakHandle<JsEventHandler>& event_handler, + const std::string& sync_server_and_path, + int port, + bool use_ssl, + const scoped_refptr<base::TaskRunner>& blocking_task_runner, + HttpPostProviderFactory* post_factory, + ModelSafeWorkerRegistrar* model_safe_worker_registrar, + browser_sync::ExtensionsActivityMonitor* extensions_activity_monitor, + ChangeDelegate* change_delegate, + const std::string& user_agent, + const SyncCredentials& credentials, + bool enable_sync_tabs_for_other_clients, + sync_notifier::SyncNotifier* sync_notifier, + const std::string& restored_key_for_bootstrapping, + TestingMode testing_mode, + Encryptor* encryptor, + UnrecoverableErrorHandler* unrecoverable_error_handler, + ReportUnrecoverableErrorFunction report_unrecoverable_error_function) { + CHECK(!initialized_); + + DCHECK(thread_checker_.CalledOnValidThread()); + + DVLOG(1) << "Starting SyncInternal initialization."; + + weak_handle_this_ = MakeWeakHandle(weak_ptr_factory_.GetWeakPtr()); + + blocking_task_runner_ = blocking_task_runner; + + registrar_ = model_safe_worker_registrar; + change_delegate_ = change_delegate; + testing_mode_ = testing_mode; + + enable_sync_tabs_for_other_clients_ = enable_sync_tabs_for_other_clients; + + sync_notifier_.reset(sync_notifier); + + AddObserver(&js_sync_manager_observer_); + SetJsEventHandler(event_handler); + + AddObserver(&debug_info_event_listener_); + + database_path_ = database_location.Append( + syncable::Directory::kSyncDatabaseFilename); + encryptor_ = encryptor; + unrecoverable_error_handler_ = unrecoverable_error_handler; + report_unrecoverable_error_function_ = report_unrecoverable_error_function; + share_.directory.reset( + new syncable::Directory(encryptor_, + unrecoverable_error_handler_, + report_unrecoverable_error_function_)); + + connection_manager_.reset(new SyncAPIServerConnectionManager( + sync_server_and_path, port, use_ssl, user_agent, post_factory)); + + net::NetworkChangeNotifier::AddIPAddressObserver(this); + observing_ip_address_changes_ = true; + + connection_manager()->AddListener(this); + + + // Test mode does not use a syncer context or syncer thread. + if (testing_mode_ == NON_TEST) { + // Build a SyncSessionContext and store the worker in it. + DVLOG(1) << "Sync is bringing up SyncSessionContext."; + std::vector<SyncEngineEventListener*> listeners; + listeners.push_back(&allstatus_); + listeners.push_back(this); + SyncSessionContext* context = new SyncSessionContext( + connection_manager_.get(), + directory(), + model_safe_worker_registrar, + extensions_activity_monitor, + listeners, + &debug_info_event_listener_, + &traffic_recorder_); + context->set_account_name(credentials.email); + // The SyncScheduler takes ownership of |context|. + scheduler_.reset(new SyncScheduler(name_, context, new Syncer())); + } + + bool signed_in = SignIn(credentials); + + if (signed_in) { + if (scheduler()) { + scheduler()->Start( + browser_sync::SyncScheduler::CONFIGURATION_MODE, base::Closure()); + } + + initialized_ = true; + + // Cryptographer should only be accessed while holding a + // transaction. Grabbing the user share for the transaction + // checks the initialization state, so this must come after + // |initialized_| is set to true. + ReadTransaction trans(FROM_HERE, GetUserShare()); + trans.GetCryptographer()->Bootstrap(restored_key_for_bootstrapping); + trans.GetCryptographer()->AddObserver(this); + } + + // Notify that initialization is complete. Note: This should be the last to + // execute if |signed_in| is false. Reason being in that case we would + // post a task to shutdown sync. But if this function posts any other tasks + // on the UI thread and if shutdown wins then that tasks would execute on + // a freed pointer. This is because UI thread is not shut down. + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnInitializationComplete( + MakeWeakHandle(weak_ptr_factory_.GetWeakPtr()), + signed_in)); + + if (!signed_in && testing_mode_ == NON_TEST) + return false; + + sync_notifier_->AddObserver(this); + + return signed_in; +} + +void SyncManager::SyncInternal::UpdateCryptographerAndNigori( + const std::string& chrome_version, + const base::Closure& done_callback) { + DCHECK(initialized_); + browser_sync::GetSessionName( + blocking_task_runner_, + base::Bind( + &SyncManager::SyncInternal::UpdateCryptographerAndNigoriCallback, + weak_ptr_factory_.GetWeakPtr(), + chrome_version, + done_callback)); +} + +void SyncManager::SyncInternal::UpdateNigoriEncryptionState( + Cryptographer* cryptographer, + WriteNode* nigori_node) { + DCHECK(nigori_node); + sync_pb::NigoriSpecifics nigori = nigori_node->GetNigoriSpecifics(); + + if (cryptographer->is_ready() && + nigori_overwrite_count_ < kNigoriOverwriteLimit) { + // Does not modify the encrypted blob if the unencrypted data already + // matches what is about to be written. + sync_pb::EncryptedData original_keys = nigori.encrypted(); + if (!cryptographer->GetKeys(nigori.mutable_encrypted())) + NOTREACHED(); + + if (nigori.encrypted().SerializeAsString() != + original_keys.SerializeAsString()) { + // We've updated the nigori node's encryption keys. In order to prevent + // a possible looping of two clients constantly overwriting each other, + // we limit the absolute number of overwrites per client instantiation. + nigori_overwrite_count_++; + UMA_HISTOGRAM_COUNTS("Sync.AutoNigoriOverwrites", + nigori_overwrite_count_); + } + + // Note: we don't try to set using_explicit_passphrase here since if that + // is lost the user can always set it again. The main point is to preserve + // the encryption keys so all data remains decryptable. + } + cryptographer->UpdateNigoriFromEncryptedTypes(&nigori); + + // If nothing has changed, this is a no-op. + nigori_node->SetNigoriSpecifics(nigori); +} + +void SyncManager::SyncInternal::UpdateCryptographerAndNigoriCallback( + const std::string& chrome_version, + const base::Closure& done_callback, + const std::string& session_name) { + if (!directory()->initial_sync_ended_for_type(syncable::NIGORI)) { + done_callback.Run(); // Should only happen during first time sync. + return; + } + + bool success = false; + { + WriteTransaction trans(FROM_HERE, GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + WriteNode node(&trans); + + if (node.InitByTagLookup(kNigoriTag) == sync_api::BaseNode::INIT_OK) { + sync_pb::NigoriSpecifics nigori(node.GetNigoriSpecifics()); + Cryptographer::UpdateResult result = cryptographer->Update(nigori); + if (result == Cryptographer::NEEDS_PASSPHRASE) { + sync_pb::EncryptedData pending_keys; + if (cryptographer->has_pending_keys()) + pending_keys = cryptographer->GetPendingKeys(); + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnPassphraseRequired(sync_api::REASON_DECRYPTION, + pending_keys)); + } + + + // Add or update device information. + bool contains_this_device = false; + for (int i = 0; i < nigori.device_information_size(); ++i) { + const sync_pb::DeviceInformation& device_information = + nigori.device_information(i); + if (device_information.cache_guid() == directory()->cache_guid()) { + // Update the version number in case it changed due to an update. + if (device_information.chrome_version() != chrome_version) { + sync_pb::DeviceInformation* mutable_device_information = + nigori.mutable_device_information(i); + mutable_device_information->set_chrome_version( + chrome_version); + } + contains_this_device = true; + } + } + + if (!contains_this_device) { + sync_pb::DeviceInformation* device_information = + nigori.add_device_information(); + device_information->set_cache_guid(directory()->cache_guid()); +#if defined(OS_CHROMEOS) + device_information->set_platform("ChromeOS"); +#elif defined(OS_LINUX) + device_information->set_platform("Linux"); +#elif defined(OS_MACOSX) + device_information->set_platform("Mac"); +#elif defined(OS_WIN) + device_information->set_platform("Windows"); +#endif + device_information->set_name(session_name); + device_information->set_chrome_version(chrome_version); + } + // Disabled to avoid nigori races. TODO(zea): re-enable. crbug.com/122837 + // node.SetNigoriSpecifics(nigori); + + // Make sure the nigori node has the up to date encryption info. + UpdateNigoriEncryptionState(cryptographer, &node); + + NotifyCryptographerState(cryptographer); + allstatus_.SetEncryptedTypes(cryptographer->GetEncryptedTypes()); + + success = cryptographer->is_ready(); + } else { + NOTREACHED(); + } + } + + if (success) + RefreshEncryption(); + done_callback.Run(); +} + +void SyncManager::SyncInternal::NotifyCryptographerState( + Cryptographer * cryptographer) { + // TODO(lipalani): Explore the possibility of hooking this up to + // SyncManager::Observer and making |AllStatus| a listener for that. + allstatus_.SetCryptographerReady(cryptographer->is_ready()); + allstatus_.SetCryptoHasPendingKeys(cryptographer->has_pending_keys()); + debug_info_event_listener_.SetCryptographerReady(cryptographer->is_ready()); + debug_info_event_listener_.SetCrytographerHasPendingKeys( + cryptographer->has_pending_keys()); +} + +void SyncManager::SyncInternal::StartSyncingNormally() { + // Start the sync scheduler. + if (scheduler()) // NULL during certain unittests. + scheduler()->Start(SyncScheduler::NORMAL_MODE, base::Closure()); +} + +bool SyncManager::SyncInternal::OpenDirectory() { + DCHECK(!initialized_) << "Should only happen once"; + + // Set before Open(). + change_observer_ = + browser_sync::MakeWeakHandle(js_mutation_event_observer_.AsWeakPtr()); + WeakHandle<syncable::TransactionObserver> transaction_observer( + browser_sync::MakeWeakHandle(js_mutation_event_observer_.AsWeakPtr())); + + syncable::DirOpenResult open_result = syncable::NOT_INITIALIZED; + if (testing_mode_ == TEST_IN_MEMORY) { + open_result = directory()->OpenInMemoryForTest( + username_for_share(), this, transaction_observer); + } else { + open_result = directory()->Open( + database_path_, username_for_share(), this, transaction_observer); + } + if (open_result != syncable::OPENED) { + LOG(ERROR) << "Could not open share for:" << username_for_share(); + return false; + } + + connection_manager()->set_client_id(directory()->cache_guid()); + return true; +} + +bool SyncManager::SyncInternal::SignIn(const SyncCredentials& credentials) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK(share_.name.empty()); + share_.name = credentials.email; + + DVLOG(1) << "Signing in user: " << username_for_share(); + if (!OpenDirectory()) + return false; + + // Retrieve and set the sync notifier state. This should be done + // only after OpenDirectory is called. + std::string unique_id = directory()->cache_guid(); + std::string state = directory()->GetNotificationState(); + DVLOG(1) << "Read notification unique ID: " << unique_id; + if (VLOG_IS_ON(1)) { + std::string encoded_state; + base::Base64Encode(state, &encoded_state); + DVLOG(1) << "Read notification state: " << encoded_state; + } + allstatus_.SetUniqueId(unique_id); + sync_notifier_->SetUniqueId(unique_id); + sync_notifier_->SetState(state); + + UpdateCredentials(credentials); + UpdateEnabledTypes(); + return true; +} + +void SyncManager::SyncInternal::UpdateCredentials( + const SyncCredentials& credentials) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_EQ(credentials.email, share_.name); + DCHECK(!credentials.email.empty()); + DCHECK(!credentials.sync_token.empty()); + + observing_ip_address_changes_ = true; + if (connection_manager()->set_auth_token(credentials.sync_token)) { + sync_notifier_->UpdateCredentials( + credentials.email, credentials.sync_token); + if (testing_mode_ == NON_TEST && initialized_) { + if (scheduler()) + scheduler()->OnCredentialsUpdated(); + } + } +} + +void SyncManager::SyncInternal::UpdateEnabledTypes() { + DCHECK(thread_checker_.CalledOnValidThread()); + ModelSafeRoutingInfo routes; + registrar_->GetModelSafeRoutingInfo(&routes); + const ModelTypeSet enabled_types = GetRoutingInfoTypes(routes); + sync_notifier_->UpdateEnabledTypes(enabled_types); + if (enable_sync_tabs_for_other_clients_) + MaybeSetSyncTabsInNigoriNode(enabled_types); +} + +void SyncManager::SyncInternal::MaybeSetSyncTabsInNigoriNode( + const ModelTypeSet enabled_types) { + // The initialized_ check is to ensure that we don't CHECK in GetUserShare + // when this is called on start-up. It's ok to ignore that case, since + // presumably this would've run when the user originally enabled sessions. + if (initialized_ && enabled_types.Has(syncable::SESSIONS)) { + WriteTransaction trans(FROM_HERE, GetUserShare()); + WriteNode node(&trans); + if (node.InitByTagLookup(kNigoriTag) != sync_api::BaseNode::INIT_OK) { + LOG(WARNING) << "Unable to set 'sync_tabs' bit because Nigori node not " + << "found."; + return; + } + + sync_pb::NigoriSpecifics specifics(node.GetNigoriSpecifics()); + specifics.set_sync_tabs(true); + node.SetNigoriSpecifics(specifics); + } +} + +void SyncManager::SyncInternal::SetEncryptionPassphrase( + const std::string& passphrase, + bool is_explicit) { + // We do not accept empty passphrases. + if (passphrase.empty()) { + NOTREACHED() << "Cannot encrypt with an empty passphrase."; + return; + } + + // All accesses to the cryptographer are protected by a transaction. + WriteTransaction trans(FROM_HERE, GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + KeyParams key_params = {"localhost", "dummy", passphrase}; + WriteNode node(&trans); + if (node.InitByTagLookup(kNigoriTag) != sync_api::BaseNode::INIT_OK) { + // TODO(albertb): Plumb an UnrecoverableError all the way back to the PSS. + NOTREACHED(); + return; + } + + bool nigori_has_explicit_passphrase = + node.GetNigoriSpecifics().using_explicit_passphrase(); + std::string bootstrap_token; + sync_pb::EncryptedData pending_keys; + if (cryptographer->has_pending_keys()) + pending_keys = cryptographer->GetPendingKeys(); + bool success = false; + + + // There are six cases to handle here: + // 1. The user has no pending keys and is setting their current GAIA password + // as the encryption passphrase. This happens either during first time sync + // with a clean profile, or after re-authenticating on a profile that was + // already signed in with the cryptographer ready. + // 2. The user has no pending keys, and is overwriting an (already provided) + // implicit passphrase with an explicit (custom) passphrase. + // 3. The user has pending keys for an explicit passphrase that is somehow set + // to their current GAIA passphrase. + // 4. The user has pending keys encrypted with their current GAIA passphrase + // and the caller passes in the current GAIA passphrase. + // 5. The user has pending keys encrypted with an older GAIA passphrase + // and the caller passes in the current GAIA passphrase. + // 6. The user has previously done encryption with an explicit passphrase. + // Furthermore, we enforce the fact that the bootstrap encryption token will + // always be derived from the newest GAIA password if the account is using + // an implicit passphrase (even if the data is encrypted with an old GAIA + // password). If the account is using an explicit (custom) passphrase, the + // bootstrap token will be derived from the most recently provided explicit + // passphrase (that was able to decrypt the data). + if (!nigori_has_explicit_passphrase) { + if (!cryptographer->has_pending_keys()) { + if (cryptographer->AddKey(key_params)) { + // Case 1 and 2. We set a new GAIA passphrase when there are no pending + // keys (1), or overwriting an implicit passphrase with a new explicit + // one (2) when there are no pending keys. + DVLOG(1) << "Setting " << (is_explicit ? "explicit" : "implicit" ) + << " passphrase for encryption."; + cryptographer->GetBootstrapToken(&bootstrap_token); + success = true; + } else { + NOTREACHED() << "Failed to add key to cryptographer."; + success = false; + } + } else { // cryptographer->has_pending_keys() == true + if (is_explicit) { + // This can only happen if the nigori node is updated with a new + // implicit passphrase while a client is attempting to set a new custom + // passphrase (race condition). + DVLOG(1) << "Failing because an implicit passphrase is already set."; + success = false; + } else { // is_explicit == false + if (cryptographer->DecryptPendingKeys(key_params)) { + // Case 4. We successfully decrypted with the implicit GAIA passphrase + // passed in. + DVLOG(1) << "Implicit internal passphrase accepted for decryption."; + cryptographer->GetBootstrapToken(&bootstrap_token); + success = true; + } else { + // Case 5. Encryption was done with an old GAIA password, but we were + // provided with the current GAIA password. We need to generate a new + // bootstrap token to preserve it. We build a temporary cryptographer + // to allow us to extract these params without polluting our current + // cryptographer. + DVLOG(1) << "Implicit internal passphrase failed to decrypt, adding " + << "anyways as default passphrase and persisting via " + << "bootstrap token."; + Cryptographer temp_cryptographer(encryptor_); + temp_cryptographer.AddKey(key_params); + temp_cryptographer.GetBootstrapToken(&bootstrap_token); + // We then set the new passphrase as the default passphrase of the + // real cryptographer, even though we have pending keys. This is safe, + // as although Cryptographer::is_initialized() will now be true, + // is_ready() will remain false due to having pending keys. + cryptographer->AddKey(key_params); + success = false; + } + } // is_explicit + } // cryptographer->has_pending_keys() + } else { // nigori_has_explicit_passphrase == true + // Case 6. We do not want to override a previously set explicit passphrase, + // so we return a failure. + DVLOG(1) << "Failing because an explicit passphrase is already set."; + success = false; + } + + DVLOG_IF(1, !success) + << "Failure in SetEncryptionPassphrase; notifying and returning."; + DVLOG_IF(1, success) + << "Successfully set encryption passphrase; updating nigori and " + "reencrypting."; + + FinishSetPassphrase( + success, bootstrap_token, is_explicit, &trans, &node); +} + +void SyncManager::SyncInternal::SetDecryptionPassphrase( + const std::string& passphrase) { + // We do not accept empty passphrases. + if (passphrase.empty()) { + NOTREACHED() << "Cannot decrypt with an empty passphrase."; + return; + } + + // All accesses to the cryptographer are protected by a transaction. + WriteTransaction trans(FROM_HERE, GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + KeyParams key_params = {"localhost", "dummy", passphrase}; + WriteNode node(&trans); + if (node.InitByTagLookup(kNigoriTag) != sync_api::BaseNode::INIT_OK) { + // TODO(albertb): Plumb an UnrecoverableError all the way back to the PSS. + NOTREACHED(); + return; + } + + if (!cryptographer->has_pending_keys()) { + // Note that this *can* happen in a rare situation where data is + // re-encrypted on another client while a SetDecryptionPassphrase() call is + // in-flight on this client. It is rare enough that we choose to do nothing. + NOTREACHED() << "Attempt to set decryption passphrase failed because there " + << "were no pending keys."; + return; + } + + bool nigori_has_explicit_passphrase = + node.GetNigoriSpecifics().using_explicit_passphrase(); + std::string bootstrap_token; + sync_pb::EncryptedData pending_keys; + pending_keys = cryptographer->GetPendingKeys(); + bool success = false; + + // There are three cases to handle here: + // 7. We're using the current GAIA password to decrypt the pending keys. This + // happens when signing in to an account with a previously set implicit + // passphrase, where the data is already encrypted with the newest GAIA + // password. + // 8. The user is providing an old GAIA password to decrypt the pending keys. + // In this case, the user is using an implicit passphrase, but has changed + // their password since they last encrypted their data, and therefore + // their current GAIA password was unable to decrypt the data. This will + // happen when the user is setting up a new profile with a previously + // encrypted account (after changing passwords). + // 9. The user is providing a previously set explicit passphrase to decrypt + // the pending keys. + if (!nigori_has_explicit_passphrase) { + if (cryptographer->is_initialized()) { + // We only want to change the default encryption key to the pending + // one if the pending keybag already contains the current default. + // This covers the case where a different client re-encrypted + // everything with a newer gaia passphrase (and hence the keybag + // contains keys from all previously used gaia passphrases). + // Otherwise, we're in a situation where the pending keys are + // encrypted with an old gaia passphrase, while the default is the + // current gaia passphrase. In that case, we preserve the default. + Cryptographer temp_cryptographer(encryptor_); + temp_cryptographer.SetPendingKeys(cryptographer->GetPendingKeys()); + if (temp_cryptographer.DecryptPendingKeys(key_params)) { + // Check to see if the pending bag of keys contains the current + // default key. + sync_pb::EncryptedData encrypted; + cryptographer->GetKeys(&encrypted); + if (temp_cryptographer.CanDecrypt(encrypted)) { + DVLOG(1) << "Implicit user provided passphrase accepted for " + << "decryption, overwriting default."; + // Case 7. The pending keybag contains the current default. Go ahead + // and update the cryptographer, letting the default change. + cryptographer->DecryptPendingKeys(key_params); + cryptographer->GetBootstrapToken(&bootstrap_token); + success = true; + } else { + // Case 8. The pending keybag does not contain the current default + // encryption key. We decrypt the pending keys here, and in + // FinishSetPassphrase, re-encrypt everything with the current GAIA + // passphrase instead of the passphrase just provided by the user. + DVLOG(1) << "Implicit user provided passphrase accepted for " + << "decryption, restoring implicit internal passphrase " + << "as default."; + std::string bootstrap_token_from_current_key; + cryptographer->GetBootstrapToken( + &bootstrap_token_from_current_key); + cryptographer->DecryptPendingKeys(key_params); + // Overwrite the default from the pending keys. + cryptographer->AddKeyFromBootstrapToken( + bootstrap_token_from_current_key); + success = true; + } + } else { // !temp_cryptographer.DecryptPendingKeys(..) + DVLOG(1) << "Implicit user provided passphrase failed to decrypt."; + success = false; + } // temp_cryptographer.DecryptPendingKeys(...) + } else { // cryptographer->is_initialized() == false + if (cryptographer->DecryptPendingKeys(key_params)) { + // This can happpen in two cases: + // - First time sync on android, where we'll never have a + // !user_provided passphrase. + // - This is a restart for a client that lost their bootstrap token. + // In both cases, we should go ahead and initialize the cryptographer + // and persist the new bootstrap token. + // + // Note: at this point, we cannot distinguish between cases 7 and 8 + // above. This user provided passphrase could be the current or the + // old. But, as long as we persist the token, there's nothing more + // we can do. + cryptographer->GetBootstrapToken(&bootstrap_token); + DVLOG(1) << "Implicit user provided passphrase accepted, initializing" + << " cryptographer."; + success = true; + } else { + DVLOG(1) << "Implicit user provided passphrase failed to decrypt."; + success = false; + } + } // cryptographer->is_initialized() + } else { // nigori_has_explicit_passphrase == true + // Case 9. Encryption was done with an explicit passphrase, and we decrypt + // with the passphrase provided by the user. + if (cryptographer->DecryptPendingKeys(key_params)) { + DVLOG(1) << "Explicit passphrase accepted for decryption."; + cryptographer->GetBootstrapToken(&bootstrap_token); + success = true; + } else { + DVLOG(1) << "Explicit passphrase failed to decrypt."; + success = false; + } + } // nigori_has_explicit_passphrase + + DVLOG_IF(1, !success) + << "Failure in SetDecryptionPassphrase; notifying and returning."; + DVLOG_IF(1, success) + << "Successfully set decryption passphrase; updating nigori and " + "reencrypting."; + + FinishSetPassphrase(success, + bootstrap_token, + nigori_has_explicit_passphrase, + &trans, + &node); +} + +void SyncManager::SyncInternal::FinishSetPassphrase( + bool success, + const std::string& bootstrap_token, + bool is_explicit, + WriteTransaction* trans, + WriteNode* nigori_node) { + Cryptographer* cryptographer = trans->GetCryptographer(); + NotifyCryptographerState(cryptographer); + + // It's possible we need to change the bootstrap token even if we failed to + // set the passphrase (for example if we need to preserve the new GAIA + // passphrase). + if (!bootstrap_token.empty()) { + DVLOG(1) << "Bootstrap token updated."; + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnBootstrapTokenUpdated(bootstrap_token)); + } + + if (!success) { + if (cryptographer->is_ready()) { + LOG(ERROR) << "Attempt to change passphrase failed while cryptographer " + << "was ready."; + } else if (cryptographer->has_pending_keys()) { + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnPassphraseRequired(sync_api::REASON_DECRYPTION, + cryptographer->GetPendingKeys())); + } else { + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnPassphraseRequired(sync_api::REASON_ENCRYPTION, + sync_pb::EncryptedData())); + } + return; + } + + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnPassphraseAccepted()); + DCHECK(cryptographer->is_ready()); + + // TODO(tim): Bug 58231. It would be nice if setting a passphrase didn't + // require messing with the Nigori node, because we can't set a passphrase + // until download conditions are met vs Cryptographer init. It seems like + // it's safe to defer this work. + sync_pb::NigoriSpecifics specifics(nigori_node->GetNigoriSpecifics()); + // Does not modify specifics.encrypted() if the original decrypted data was + // the same. + if (!cryptographer->GetKeys(specifics.mutable_encrypted())) { + NOTREACHED(); + return; + } + specifics.set_using_explicit_passphrase(is_explicit); + nigori_node->SetNigoriSpecifics(specifics); + + // Does nothing if everything is already encrypted or the cryptographer has + // pending keys. + ReEncryptEverything(trans); +} + +bool SyncManager::SyncInternal::IsUsingExplicitPassphrase() { + ReadTransaction trans(FROM_HERE, &share_); + ReadNode node(&trans); + if (node.InitByTagLookup(kNigoriTag) != sync_api::BaseNode::INIT_OK) { + // TODO(albertb): Plumb an UnrecoverableError all the way back to the PSS. + NOTREACHED(); + return false; + } + + return node.GetNigoriSpecifics().using_explicit_passphrase(); +} + +void SyncManager::SyncInternal::RefreshEncryption() { + DCHECK(initialized_); + + WriteTransaction trans(FROM_HERE, GetUserShare()); + WriteNode node(&trans); + if (node.InitByTagLookup(kNigoriTag) != sync_api::BaseNode::INIT_OK) { + NOTREACHED() << "Unable to set encrypted datatypes because Nigori node not " + << "found."; + return; + } + + Cryptographer* cryptographer = trans.GetCryptographer(); + + if (!cryptographer->is_ready()) { + DVLOG(1) << "Attempting to encrypt datatypes when cryptographer not " + << "initialized, prompting for passphrase."; + // TODO(zea): this isn't really decryption, but that's the only way we have + // to prompt the user for a passsphrase. See http://crbug.com/91379. + sync_pb::EncryptedData pending_keys; + if (cryptographer->has_pending_keys()) + pending_keys = cryptographer->GetPendingKeys(); + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnPassphraseRequired(sync_api::REASON_DECRYPTION, + pending_keys)); + return; + } + + UpdateNigoriEncryptionState(cryptographer, &node); + + allstatus_.SetEncryptedTypes(cryptographer->GetEncryptedTypes()); + + // We reencrypt everything regardless of whether the set of encrypted + // types changed to ensure that any stray unencrypted entries are overwritten. + ReEncryptEverything(&trans); +} + +void SyncManager::SyncInternal::ReEncryptEverything(WriteTransaction* trans) { + Cryptographer* cryptographer = trans->GetCryptographer(); + if (!cryptographer || !cryptographer->is_ready()) + return; + syncable::ModelTypeSet encrypted_types = GetEncryptedTypes(trans); + ModelSafeRoutingInfo routes; + registrar_->GetModelSafeRoutingInfo(&routes); + std::string tag; + for (syncable::ModelTypeSet::Iterator iter = encrypted_types.First(); + iter.Good(); iter.Inc()) { + if (iter.Get() == syncable::PASSWORDS || + iter.Get() == syncable::NIGORI || + routes.count(iter.Get()) == 0) + continue; + ReadNode type_root(trans); + tag = syncable::ModelTypeToRootTag(iter.Get()); + if (type_root.InitByTagLookup(tag) != sync_api::BaseNode::INIT_OK) { + // This can happen when we enable a datatype for the first time on restart + // (for example when we upgrade) and therefore haven't done the initial + // download for that type at the time we RefreshEncryption. There's + // nothing we can do for now, so just move on to the next type. + continue; + } + + // Iterate through all children of this datatype. + std::queue<int64> to_visit; + int64 child_id = type_root.GetFirstChildId(); + to_visit.push(child_id); + while (!to_visit.empty()) { + child_id = to_visit.front(); + to_visit.pop(); + if (child_id == kInvalidId) + continue; + + WriteNode child(trans); + if (child.InitByIdLookup(child_id) != sync_api::BaseNode::INIT_OK) { + NOTREACHED(); + continue; + } + if (child.GetIsFolder()) { + to_visit.push(child.GetFirstChildId()); + } + if (child.GetEntry()->Get(syncable::UNIQUE_SERVER_TAG).empty()) { + // Rewrite the specifics of the node with encrypted data if necessary + // (only rewrite the non-unique folders). + child.ResetFromSpecifics(); + } + to_visit.push(child.GetSuccessorId()); + } + } + + if (routes.count(syncable::PASSWORDS) > 0) { + // Passwords are encrypted with their own legacy scheme. + ReadNode passwords_root(trans); + std::string passwords_tag = + syncable::ModelTypeToRootTag(syncable::PASSWORDS); + // It's possible we'll have the password routing info and not the password + // root if we attempted to set a passphrase before passwords was enabled. + if (passwords_root.InitByTagLookup(passwords_tag) == + sync_api::BaseNode::INIT_OK) { + int64 child_id = passwords_root.GetFirstChildId(); + while (child_id != kInvalidId) { + WriteNode child(trans); + if (child.InitByIdLookup(child_id) != sync_api::BaseNode::INIT_OK) { + NOTREACHED(); + return; + } + child.SetPasswordSpecifics(child.GetPasswordSpecifics()); + child_id = child.GetSuccessorId(); + } + } + } + + // NOTE: We notify from within a transaction. + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, OnEncryptionComplete()); +} + +SyncManager::~SyncManager() { + DCHECK(thread_checker_.CalledOnValidThread()); + delete data_; +} + +void SyncManager::AddObserver(Observer* observer) { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->AddObserver(observer); +} + +void SyncManager::RemoveObserver(Observer* observer) { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->RemoveObserver(observer); +} + +void SyncManager::StopSyncingForShutdown(const base::Closure& callback) { + data_->StopSyncingForShutdown(callback); +} + +void SyncManager::SyncInternal::StopSyncingForShutdown( + const base::Closure& callback) { + DVLOG(2) << "StopSyncingForShutdown"; + if (scheduler()) // May be null in tests. + scheduler()->RequestStop(callback); + else + created_on_loop_->PostTask(FROM_HERE, callback); + + if (connection_manager_.get()) + connection_manager_->TerminateAllIO(); +} + +void SyncManager::ShutdownOnSyncThread() { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->ShutdownOnSyncThread(); +} + +void SyncManager::SyncInternal::ShutdownOnSyncThread() { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Prevent any in-flight method calls from running. Also + // invalidates |weak_handle_this_| and |change_observer_|. + weak_ptr_factory_.InvalidateWeakPtrs(); + js_mutation_event_observer_.InvalidateWeakPtrs(); + + scheduler_.reset(); + + SetJsEventHandler(WeakHandle<JsEventHandler>()); + RemoveObserver(&js_sync_manager_observer_); + + RemoveObserver(&debug_info_event_listener_); + + if (sync_notifier_.get()) { + sync_notifier_->RemoveObserver(this); + } + sync_notifier_.reset(); + + if (connection_manager_.get()) { + connection_manager_->RemoveListener(this); + } + connection_manager_.reset(); + + net::NetworkChangeNotifier::RemoveIPAddressObserver(this); + observing_ip_address_changes_ = false; + + if (initialized_ && directory()) { + { + // Cryptographer should only be accessed while holding a + // transaction. + ReadTransaction trans(FROM_HERE, GetUserShare()); + trans.GetCryptographer()->RemoveObserver(this); + } + directory()->SaveChanges(); + } + + share_.directory.reset(); + + change_delegate_ = NULL; + registrar_ = NULL; + + initialized_ = false; + + // We reset these here, since only now we know they will not be + // accessed from other threads (since we shut down everything). + change_observer_.Reset(); + weak_handle_this_.Reset(); +} + +void SyncManager::SyncInternal::OnIPAddressChanged() { + DVLOG(1) << "IP address change detected"; + if (!observing_ip_address_changes_) { + DVLOG(1) << "IP address change dropped."; + return; + } + + OnIPAddressChangedImpl(); +} + +void SyncManager::SyncInternal::OnIPAddressChangedImpl() { + DCHECK(thread_checker_.CalledOnValidThread()); + if (scheduler()) + scheduler()->OnConnectionStatusChange(); +} + +void SyncManager::SyncInternal::OnServerConnectionEvent( + const ServerConnectionEvent& event) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (event.connection_code == + browser_sync::HttpResponse::SERVER_CONNECTION_OK) { + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnConnectionStatusChange(CONNECTION_OK)); + } + + if (event.connection_code == browser_sync::HttpResponse::SYNC_AUTH_ERROR) { + observing_ip_address_changes_ = false; + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnConnectionStatusChange(CONNECTION_AUTH_ERROR)); + } + + if (event.connection_code == + browser_sync::HttpResponse::SYNC_SERVER_ERROR) { + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnConnectionStatusChange(CONNECTION_SERVER_ERROR)); + } +} + +void SyncManager::SyncInternal::HandleTransactionCompleteChangeEvent( + ModelTypeSet models_with_changes) { + // This notification happens immediately after the transaction mutex is + // released. This allows work to be performed without blocking other threads + // from acquiring a transaction. + if (!change_delegate_) + return; + + // Call commit. + for (ModelTypeSet::Iterator it = models_with_changes.First(); + it.Good(); it.Inc()) { + change_delegate_->OnChangesComplete(it.Get()); + change_observer_.Call( + FROM_HERE, &SyncManager::ChangeObserver::OnChangesComplete, it.Get()); + } +} + +ModelTypeSet + SyncManager::SyncInternal::HandleTransactionEndingChangeEvent( + const ImmutableWriteTransactionInfo& write_transaction_info, + syncable::BaseTransaction* trans) { + // This notification happens immediately before a syncable WriteTransaction + // falls out of scope. It happens while the channel mutex is still held, + // and while the transaction mutex is held, so it cannot be re-entrant. + if (!change_delegate_ || ChangeBuffersAreEmpty()) + return ModelTypeSet(); + + // This will continue the WriteTransaction using a read only wrapper. + // This is the last chance for read to occur in the WriteTransaction + // that's closing. This special ReadTransaction will not close the + // underlying transaction. + ReadTransaction read_trans(GetUserShare(), trans); + + ModelTypeSet models_with_changes; + for (int i = syncable::FIRST_REAL_MODEL_TYPE; + i < syncable::MODEL_TYPE_COUNT; ++i) { + const syncable::ModelType type = syncable::ModelTypeFromInt(i); + if (change_buffers_[type].IsEmpty()) + continue; + + ImmutableChangeRecordList ordered_changes; + // TODO(akalin): Propagate up the error further (see + // http://crbug.com/100907). + CHECK(change_buffers_[type].GetAllChangesInTreeOrder(&read_trans, + &ordered_changes)); + if (!ordered_changes.Get().empty()) { + change_delegate_-> + OnChangesApplied(type, &read_trans, ordered_changes); + change_observer_.Call(FROM_HERE, + &SyncManager::ChangeObserver::OnChangesApplied, + type, write_transaction_info.Get().id, ordered_changes); + models_with_changes.Put(type); + } + change_buffers_[i].Clear(); + } + return models_with_changes; +} + +void SyncManager::SyncInternal::HandleCalculateChangesChangeEventFromSyncApi( + const ImmutableWriteTransactionInfo& write_transaction_info, + syncable::BaseTransaction* trans) { + if (!scheduler()) { + return; + } + + // We have been notified about a user action changing a sync model. + LOG_IF(WARNING, !ChangeBuffersAreEmpty()) << + "CALCULATE_CHANGES called with unapplied old changes."; + + // The mutated model type, or UNSPECIFIED if nothing was mutated. + syncable::ModelTypeSet mutated_model_types; + + const syncable::ImmutableEntryKernelMutationMap& mutations = + write_transaction_info.Get().mutations; + for (syncable::EntryKernelMutationMap::const_iterator it = + mutations.Get().begin(); it != mutations.Get().end(); ++it) { + if (!it->second.mutated.ref(syncable::IS_UNSYNCED)) { + continue; + } + + syncable::ModelType model_type = + syncable::GetModelTypeFromSpecifics( + it->second.mutated.ref(SPECIFICS)); + if (model_type < syncable::FIRST_REAL_MODEL_TYPE) { + NOTREACHED() << "Permanent or underspecified item changed via syncapi."; + continue; + } + + // Found real mutation. + if (model_type != syncable::UNSPECIFIED) { + mutated_model_types.Put(model_type); + } + } + + // Nudge if necessary. + if (!mutated_model_types.Empty()) { + if (weak_handle_this_.IsInitialized()) { + weak_handle_this_.Call(FROM_HERE, + &SyncInternal::RequestNudgeForDataTypes, + FROM_HERE, + mutated_model_types); + } else { + NOTREACHED(); + } + } +} + +void SyncManager::SyncInternal::SetExtraChangeRecordData(int64 id, + syncable::ModelType type, ChangeReorderBuffer* buffer, + Cryptographer* cryptographer, const syncable::EntryKernel& original, + bool existed_before, bool exists_now) { + // If this is a deletion and the datatype was encrypted, we need to decrypt it + // and attach it to the buffer. + if (!exists_now && existed_before) { + sync_pb::EntitySpecifics original_specifics(original.ref(SPECIFICS)); + if (type == syncable::PASSWORDS) { + // Passwords must use their own legacy ExtraPasswordChangeRecordData. + scoped_ptr<sync_pb::PasswordSpecificsData> data( + DecryptPasswordSpecifics(original_specifics, cryptographer)); + if (!data.get()) { + NOTREACHED(); + return; + } + buffer->SetExtraDataForId(id, new ExtraPasswordChangeRecordData(*data)); + } else if (original_specifics.has_encrypted()) { + // All other datatypes can just create a new unencrypted specifics and + // attach it. + const sync_pb::EncryptedData& encrypted = original_specifics.encrypted(); + if (!cryptographer->Decrypt(encrypted, &original_specifics)) { + NOTREACHED(); + return; + } + } + buffer->SetSpecificsForId(id, original_specifics); + } +} + +void SyncManager::SyncInternal::HandleCalculateChangesChangeEventFromSyncer( + const ImmutableWriteTransactionInfo& write_transaction_info, + syncable::BaseTransaction* trans) { + // We only expect one notification per sync step, so change_buffers_ should + // contain no pending entries. + LOG_IF(WARNING, !ChangeBuffersAreEmpty()) << + "CALCULATE_CHANGES called with unapplied old changes."; + + Cryptographer* crypto = directory()->GetCryptographer(trans); + const syncable::ImmutableEntryKernelMutationMap& mutations = + write_transaction_info.Get().mutations; + for (syncable::EntryKernelMutationMap::const_iterator it = + mutations.Get().begin(); it != mutations.Get().end(); ++it) { + bool existed_before = !it->second.original.ref(syncable::IS_DEL); + bool exists_now = !it->second.mutated.ref(syncable::IS_DEL); + + // Omit items that aren't associated with a model. + syncable::ModelType type = + syncable::GetModelTypeFromSpecifics( + it->second.mutated.ref(SPECIFICS)); + if (type < syncable::FIRST_REAL_MODEL_TYPE) + continue; + + int64 handle = it->first; + if (exists_now && !existed_before) + change_buffers_[type].PushAddedItem(handle); + else if (!exists_now && existed_before) + change_buffers_[type].PushDeletedItem(handle); + else if (exists_now && existed_before && + VisiblePropertiesDiffer(it->second, crypto)) { + change_buffers_[type].PushUpdatedItem( + handle, VisiblePositionsDiffer(it->second)); + } + + SetExtraChangeRecordData(handle, type, &change_buffers_[type], crypto, + it->second.original, existed_before, exists_now); + } +} + +SyncManager::Status SyncManager::SyncInternal::GetStatus() { + return allstatus_.status(); +} + +void SyncManager::SyncInternal::RequestNudge( + const tracked_objects::Location& location) { + if (scheduler()) { + scheduler()->ScheduleNudge( + TimeDelta::FromMilliseconds(0), browser_sync::NUDGE_SOURCE_LOCAL, + ModelTypeSet(), location); + } +} + +TimeDelta SyncManager::SyncInternal::GetNudgeDelayTimeDelta( + const ModelType& model_type) { + return NudgeStrategy::GetNudgeDelayTimeDelta(model_type, this); +} + +void SyncManager::SyncInternal::RequestNudgeForDataTypes( + const tracked_objects::Location& nudge_location, + ModelTypeSet types) { + if (!scheduler()) { + NOTREACHED(); + return; + } + + debug_info_event_listener_.OnNudgeFromDatatype(types.First().Get()); + + // TODO(lipalani) : Calculate the nudge delay based on all types. + base::TimeDelta nudge_delay = NudgeStrategy::GetNudgeDelayTimeDelta( + types.First().Get(), + this); + scheduler()->ScheduleNudge(nudge_delay, + browser_sync::NUDGE_SOURCE_LOCAL, + types, + nudge_location); +} + +void SyncManager::SyncInternal::OnSyncEngineEvent( + const SyncEngineEvent& event) { + DCHECK(thread_checker_.CalledOnValidThread()); + // Only send an event if this is due to a cycle ending and this cycle + // concludes a canonical "sync" process; that is, based on what is known + // locally we are "all happy" and up-to-date. There may be new changes on + // the server, but we'll get them on a subsequent sync. + // + // Notifications are sent at the end of every sync cycle, regardless of + // whether we should sync again. + if (event.what_happened == SyncEngineEvent::SYNC_CYCLE_ENDED) { + ModelSafeRoutingInfo enabled_types; + registrar_->GetModelSafeRoutingInfo(&enabled_types); + { + // Check to see if we need to notify the frontend that we have newly + // encrypted types or that we require a passphrase. + ReadTransaction trans(FROM_HERE, GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + // If we've completed a sync cycle and the cryptographer isn't ready + // yet, prompt the user for a passphrase. + if (cryptographer->has_pending_keys()) { + DVLOG(1) << "OnPassPhraseRequired Sent"; + sync_pb::EncryptedData pending_keys = cryptographer->GetPendingKeys(); + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnPassphraseRequired(sync_api::REASON_DECRYPTION, + pending_keys)); + } else if (!cryptographer->is_ready() && + event.snapshot->initial_sync_ended.Has(syncable::NIGORI)) { + DVLOG(1) << "OnPassphraseRequired sent because cryptographer is not " + << "ready"; + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnPassphraseRequired(sync_api::REASON_ENCRYPTION, + sync_pb::EncryptedData())); + } + + NotifyCryptographerState(cryptographer); + allstatus_.SetEncryptedTypes(cryptographer->GetEncryptedTypes()); + } + + if (!initialized_) { + LOG(INFO) << "OnSyncCycleCompleted not sent because sync api is not " + << "initialized"; + return; + } + + if (!event.snapshot->has_more_to_sync) { + // To account for a nigori node arriving with stale/bad data, we ensure + // that the nigori node is up to date at the end of each cycle. + WriteTransaction trans(FROM_HERE, GetUserShare()); + WriteNode nigori_node(&trans); + if (nigori_node.InitByTagLookup(kNigoriTag) == + sync_api::BaseNode::INIT_OK) { + Cryptographer* cryptographer = trans.GetCryptographer(); + UpdateNigoriEncryptionState(cryptographer, &nigori_node); + } + + DVLOG(1) << "Sending OnSyncCycleCompleted"; + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnSyncCycleCompleted(event.snapshot)); + } + + // This is here for tests, which are still using p2p notifications. + // + // TODO(chron): Consider changing this back to track has_more_to_sync + // only notify peers if a successful commit has occurred. + bool is_notifiable_commit = + (event.snapshot->syncer_status.num_successful_commits > 0); + if (is_notifiable_commit) { + if (sync_notifier_.get()) { + const ModelTypeSet changed_types = + syncable::ModelTypePayloadMapToEnumSet( + event.snapshot->source.types); + sync_notifier_->SendNotification(changed_types); + } else { + DVLOG(1) << "Not sending notification: sync_notifier_ is NULL"; + } + } + } + + if (event.what_happened == SyncEngineEvent::STOP_SYNCING_PERMANENTLY) { + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnStopSyncingPermanently()); + return; + } + + if (event.what_happened == SyncEngineEvent::CLEAR_SERVER_DATA_SUCCEEDED) { + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnClearServerDataSucceeded()); + return; + } + + if (event.what_happened == SyncEngineEvent::CLEAR_SERVER_DATA_FAILED) { + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnClearServerDataFailed()); + return; + } + + if (event.what_happened == SyncEngineEvent::UPDATED_TOKEN) { + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnUpdatedToken(event.updated_token)); + return; + } + + if (event.what_happened == SyncEngineEvent::ACTIONABLE_ERROR) { + FOR_EACH_OBSERVER(SyncManager::Observer, observers_, + OnActionableError( + event.snapshot->errors.sync_protocol_error)); + return; + } + +} + +void SyncManager::SyncInternal::SetJsEventHandler( + const WeakHandle<JsEventHandler>& event_handler) { + js_event_handler_ = event_handler; + js_sync_manager_observer_.SetJsEventHandler(js_event_handler_); + js_mutation_event_observer_.SetJsEventHandler(js_event_handler_); +} + +void SyncManager::SyncInternal::ProcessJsMessage( + const std::string& name, const JsArgList& args, + const WeakHandle<JsReplyHandler>& reply_handler) { + if (!initialized_) { + NOTREACHED(); + return; + } + + if (!reply_handler.IsInitialized()) { + DVLOG(1) << "Uninitialized reply handler; dropping unknown message " + << name << " with args " << args.ToString(); + return; + } + + JsMessageHandler js_message_handler = js_message_handlers_[name]; + if (js_message_handler.is_null()) { + DVLOG(1) << "Dropping unknown message " << name + << " with args " << args.ToString(); + return; + } + + reply_handler.Call(FROM_HERE, + &JsReplyHandler::HandleJsReply, + name, js_message_handler.Run(args)); +} + +void SyncManager::SyncInternal::BindJsMessageHandler( + const std::string& name, + UnboundJsMessageHandler unbound_message_handler) { + js_message_handlers_[name] = + base::Bind(unbound_message_handler, base::Unretained(this)); +} + +DictionaryValue* SyncManager::SyncInternal::NotificationInfoToValue( + const NotificationInfoMap& notification_info) { + DictionaryValue* value = new DictionaryValue(); + + for (NotificationInfoMap::const_iterator it = notification_info.begin(); + it != notification_info.end(); ++it) { + const std::string& model_type_str = + syncable::ModelTypeToString(it->first); + value->Set(model_type_str, it->second.ToValue()); + } + + return value; +} + +JsArgList SyncManager::SyncInternal::GetNotificationState( + const JsArgList& args) { + bool notifications_enabled = allstatus_.status().notifications_enabled; + ListValue return_args; + return_args.Append(Value::CreateBooleanValue(notifications_enabled)); + return JsArgList(&return_args); +} + +JsArgList SyncManager::SyncInternal::GetNotificationInfo( + const JsArgList& args) { + ListValue return_args; + return_args.Append(NotificationInfoToValue(notification_info_map_)); + return JsArgList(&return_args); +} + +JsArgList SyncManager::SyncInternal::GetRootNodeDetails( + const JsArgList& args) { + ReadTransaction trans(FROM_HERE, GetUserShare()); + ReadNode root(&trans); + root.InitByRootLookup(); + ListValue return_args; + return_args.Append(root.GetDetailsAsValue()); + return JsArgList(&return_args); +} + +JsArgList SyncManager::SyncInternal::GetClientServerTraffic( + const JsArgList& args) { + ListValue return_args; + ListValue* value = traffic_recorder_.ToValue(); + if (value != NULL) + return_args.Append(value); + return JsArgList(&return_args); +} + +namespace { + +int64 GetId(const ListValue& ids, int i) { + std::string id_str; + if (!ids.GetString(i, &id_str)) { + return kInvalidId; + } + int64 id = kInvalidId; + if (!base::StringToInt64(id_str, &id)) { + return kInvalidId; + } + return id; +} + +JsArgList GetNodeInfoById(const JsArgList& args, + UserShare* user_share, + DictionaryValue* (BaseNode::*info_getter)() const) { + CHECK(info_getter); + ListValue return_args; + ListValue* node_summaries = new ListValue(); + return_args.Append(node_summaries); + ListValue* id_list = NULL; + ReadTransaction trans(FROM_HERE, user_share); + if (args.Get().GetList(0, &id_list)) { + CHECK(id_list); + for (size_t i = 0; i < id_list->GetSize(); ++i) { + int64 id = GetId(*id_list, i); + if (id == kInvalidId) { + continue; + } + ReadNode node(&trans); + if (node.InitByIdLookup(id) != sync_api::BaseNode::INIT_OK) { + continue; + } + node_summaries->Append((node.*info_getter)()); + } + } + return JsArgList(&return_args); +} + +} // namespace + +JsArgList SyncManager::SyncInternal::GetNodeSummariesById( + const JsArgList& args) { + return GetNodeInfoById(args, GetUserShare(), &BaseNode::GetSummaryAsValue); +} + +JsArgList SyncManager::SyncInternal::GetNodeDetailsById( + const JsArgList& args) { + return GetNodeInfoById(args, GetUserShare(), &BaseNode::GetDetailsAsValue); +} + +JsArgList SyncManager::SyncInternal::GetAllNodes( + const JsArgList& args) { + ListValue return_args; + ListValue* result = new ListValue(); + return_args.Append(result); + + ReadTransaction trans(FROM_HERE, GetUserShare()); + std::vector<const syncable::EntryKernel*> entry_kernels; + trans.GetDirectory()->GetAllEntryKernels(trans.GetWrappedTrans(), + &entry_kernels); + + for (std::vector<const syncable::EntryKernel*>::const_iterator it = + entry_kernels.begin(); it != entry_kernels.end(); ++it) { + result->Append((*it)->ToValue()); + } + + return JsArgList(&return_args); +} + +JsArgList SyncManager::SyncInternal::GetChildNodeIds( + const JsArgList& args) { + ListValue return_args; + ListValue* child_ids = new ListValue(); + return_args.Append(child_ids); + int64 id = GetId(args.Get(), 0); + if (id != kInvalidId) { + ReadTransaction trans(FROM_HERE, GetUserShare()); + syncable::Directory::ChildHandles child_handles; + trans.GetDirectory()->GetChildHandlesByHandle(trans.GetWrappedTrans(), + id, &child_handles); + for (syncable::Directory::ChildHandles::const_iterator it = + child_handles.begin(); it != child_handles.end(); ++it) { + child_ids->Append(Value::CreateStringValue( + base::Int64ToString(*it))); + } + } + return JsArgList(&return_args); +} + +void SyncManager::SyncInternal::OnEncryptedTypesChanged( + syncable::ModelTypeSet encrypted_types, + bool encrypt_everything) { + // NOTE: We're in a transaction. + FOR_EACH_OBSERVER( + SyncManager::Observer, observers_, + OnEncryptedTypesChanged(encrypted_types, encrypt_everything)); +} + +void SyncManager::SyncInternal::OnNotificationStateChange( + bool notifications_enabled) { + DVLOG(1) << "P2P: Notifications enabled = " + << (notifications_enabled ? "true" : "false"); + allstatus_.SetNotificationsEnabled(notifications_enabled); + if (scheduler()) { + scheduler()->set_notifications_enabled(notifications_enabled); + } + if (js_event_handler_.IsInitialized()) { + DictionaryValue details; + details.Set("enabled", Value::CreateBooleanValue(notifications_enabled)); + js_event_handler_.Call(FROM_HERE, + &JsEventHandler::HandleJsEvent, + "onNotificationStateChange", + JsEventDetails(&details)); + } +} + +void SyncManager::SyncInternal::UpdateNotificationInfo( + const syncable::ModelTypePayloadMap& type_payloads) { + for (syncable::ModelTypePayloadMap::const_iterator it = type_payloads.begin(); + it != type_payloads.end(); ++it) { + NotificationInfo* info = ¬ification_info_map_[it->first]; + info->total_count++; + info->payload = it->second; + } +} + +void SyncManager::SyncInternal::OnIncomingNotification( + const syncable::ModelTypePayloadMap& type_payloads, + sync_notifier::IncomingNotificationSource source) { + DCHECK(thread_checker_.CalledOnValidThread()); + if (source == sync_notifier::LOCAL_NOTIFICATION) { + if (scheduler()) { + scheduler()->ScheduleNudgeWithPayloads( + TimeDelta::FromMilliseconds(kSyncRefreshDelayMsec), + browser_sync::NUDGE_SOURCE_LOCAL_REFRESH, + type_payloads, FROM_HERE); + } + } else if (!type_payloads.empty()) { + if (scheduler()) { + scheduler()->ScheduleNudgeWithPayloads( + TimeDelta::FromMilliseconds(kSyncSchedulerDelayMsec), + browser_sync::NUDGE_SOURCE_NOTIFICATION, + type_payloads, FROM_HERE); + } + allstatus_.IncrementNotificationsReceived(); + UpdateNotificationInfo(type_payloads); + debug_info_event_listener_.OnIncomingNotification(type_payloads); + } else { + LOG(WARNING) << "Sync received notification without any type information."; + } + + if (js_event_handler_.IsInitialized()) { + DictionaryValue details; + ListValue* changed_types = new ListValue(); + details.Set("changedTypes", changed_types); + for (syncable::ModelTypePayloadMap::const_iterator + it = type_payloads.begin(); + it != type_payloads.end(); ++it) { + const std::string& model_type_str = + syncable::ModelTypeToString(it->first); + changed_types->Append(Value::CreateStringValue(model_type_str)); + } + details.SetString("source", (source == sync_notifier::LOCAL_NOTIFICATION) ? + "LOCAL_NOTIFICATION" : "REMOTE_NOTIFICATION"); + js_event_handler_.Call(FROM_HERE, + &JsEventHandler::HandleJsEvent, + "onIncomingNotification", + JsEventDetails(&details)); + } +} + +void SyncManager::SyncInternal::StoreState( + const std::string& state) { + if (!directory()) { + LOG(ERROR) << "Could not write notification state"; + // TODO(akalin): Propagate result callback all the way to this + // function and call it with "false" to signal failure. + return; + } + if (VLOG_IS_ON(1)) { + std::string encoded_state; + base::Base64Encode(state, &encoded_state); + DVLOG(1) << "Writing notification state: " << encoded_state; + } + directory()->SetNotificationState(state); + directory()->SaveChanges(); +} + +void SyncManager::SyncInternal::AddObserver( + SyncManager::Observer* observer) { + observers_.AddObserver(observer); +} + +void SyncManager::SyncInternal::RemoveObserver( + SyncManager::Observer* observer) { + observers_.RemoveObserver(observer); +} + +SyncManager::Status SyncManager::GetDetailedStatus() const { + return data_->GetStatus(); +} + +void SyncManager::SaveChanges() { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->SaveChanges(); +} + +void SyncManager::SyncInternal::SaveChanges() { + directory()->SaveChanges(); +} + +UserShare* SyncManager::GetUserShare() const { + return data_->GetUserShare(); +} + +void SyncManager::RefreshNigori(const std::string& chrome_version, + const base::Closure& done_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->UpdateCryptographerAndNigori( + chrome_version, + done_callback); +} + +TimeDelta SyncManager::GetNudgeDelayTimeDelta( + const ModelType& model_type) { + return data_->GetNudgeDelayTimeDelta(model_type); +} + +syncable::ModelTypeSet SyncManager::GetEncryptedDataTypesForTest() const { + ReadTransaction trans(FROM_HERE, GetUserShare()); + return GetEncryptedTypes(&trans); +} + +bool SyncManager::ReceivedExperimentalTypes(syncable::ModelTypeSet* to_add) + const { + ReadTransaction trans(FROM_HERE, GetUserShare()); + ReadNode node(&trans); + if (node.InitByTagLookup(kNigoriTag) != sync_api::BaseNode::INIT_OK) { + DVLOG(1) << "Couldn't find Nigori node."; + return false; + } + if (node.GetNigoriSpecifics().sync_tabs()) { + to_add->Put(syncable::SESSIONS); + return true; + } + return false; +} + +bool SyncManager::HasUnsyncedItems() const { + sync_api::ReadTransaction trans(FROM_HERE, GetUserShare()); + return (trans.GetWrappedTrans()->directory()->unsynced_entity_count() != 0); +} + +void SyncManager::TriggerOnNotificationStateChangeForTest( + bool notifications_enabled) { + DCHECK(thread_checker_.CalledOnValidThread()); + data_->OnNotificationStateChange(notifications_enabled); +} + +void SyncManager::TriggerOnIncomingNotificationForTest( + ModelTypeSet model_types) { + DCHECK(thread_checker_.CalledOnValidThread()); + syncable::ModelTypePayloadMap model_types_with_payloads = + syncable::ModelTypePayloadMapFromEnumSet(model_types, + std::string()); + + data_->OnIncomingNotification(model_types_with_payloads, + sync_notifier::REMOTE_NOTIFICATION); +} + +const char* ConnectionStatusToString(ConnectionStatus status) { + switch (status) { + case CONNECTION_OK: + return "CONNECTION_OK"; + case CONNECTION_AUTH_ERROR: + return "CONNECTION_AUTH_ERROR"; + case CONNECTION_SERVER_ERROR: + return "CONNECTION_SERVER_ERROR"; + default: + NOTREACHED(); + return "INVALID_CONNECTION_STATUS"; + } +} + +// Helper function that converts a PassphraseRequiredReason value to a string. +const char* PassphraseRequiredReasonToString( + PassphraseRequiredReason reason) { + switch (reason) { + case REASON_PASSPHRASE_NOT_REQUIRED: + return "REASON_PASSPHRASE_NOT_REQUIRED"; + case REASON_ENCRYPTION: + return "REASON_ENCRYPTION"; + case REASON_DECRYPTION: + return "REASON_DECRYPTION"; + default: + NOTREACHED(); + return "INVALID_REASON"; + } +} + +// Helper function to determine if initial sync had ended for types. +bool InitialSyncEndedForTypes(syncable::ModelTypeSet types, + sync_api::UserShare* share) { + for (syncable::ModelTypeSet::Iterator i = types.First(); + i.Good(); i.Inc()) { + if (!share->directory->initial_sync_ended_for_type(i.Get())) + return false; + } + return true; +} + +syncable::ModelTypeSet GetTypesWithEmptyProgressMarkerToken( + syncable::ModelTypeSet types, + sync_api::UserShare* share) { + syncable::ModelTypeSet result; + for (syncable::ModelTypeSet::Iterator i = types.First(); + i.Good(); i.Inc()) { + sync_pb::DataTypeProgressMarker marker; + share->directory->GetDownloadProgress(i.Get(), &marker); + + if (marker.token().empty()) + result.Put(i.Get()); + + } + return result; +} + +} // namespace sync_api diff --git a/sync/internal_api/sync_manager.h b/sync/internal_api/sync_manager.h new file mode 100644 index 0000000..d5f08a7 --- /dev/null +++ b/sync/internal_api/sync_manager.h @@ -0,0 +1,644 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_SYNC_MANAGER_H_ +#define SYNC_INTERNAL_API_SYNC_MANAGER_H_ + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/file_path.h" +#include "base/memory/ref_counted.h" +#include "base/task_runner.h" +#include "base/threading/thread_checker.h" +#include "base/time.h" +#include "sync/internal_api/change_record.h" +#include "sync/internal_api/configure_reason.h" +#include "sync/protocol/sync_protocol_error.h" +#include "sync/syncable/model_type.h" +#include "sync/util/report_unrecoverable_error_function.h" +#include "sync/util/unrecoverable_error_handler.h" +#include "sync/util/weak_handle.h" + +namespace browser_sync { +class Encryptor; +class ExtensionsActivityMonitor; +class JsBackend; +class JsEventHandler; +class ModelSafeWorkerRegistrar; + +namespace sessions { +struct SyncSessionSnapshot; +} // namespace sessions +} // namespace browser_sync + +namespace sync_notifier { +class SyncNotifier; +} // namespace sync_notifier + +namespace sync_pb { +class EncryptedData; +} // namespace sync_pb + +namespace sync_api { + +class BaseTransaction; +class HttpPostProviderFactory; +struct UserShare; + +// Used by SyncManager::OnConnectionStatusChange(). +enum ConnectionStatus { + CONNECTION_OK, + CONNECTION_AUTH_ERROR, + CONNECTION_SERVER_ERROR +}; + +// Reasons due to which browser_sync::Cryptographer might require a passphrase. +enum PassphraseRequiredReason { + REASON_PASSPHRASE_NOT_REQUIRED = 0, // Initial value. + REASON_ENCRYPTION = 1, // The cryptographer requires a + // passphrase for its first attempt at + // encryption. Happens only during + // migration or upgrade. + REASON_DECRYPTION = 2, // The cryptographer requires a + // passphrase for its first attempt at + // decryption. +}; + + +// Contains everything needed to talk to and identify a user account. +struct SyncCredentials { + std::string email; + std::string sync_token; +}; + +// SyncManager encapsulates syncable::Directory and serves as the parent of all +// other objects in the sync API. If multiple threads interact with the same +// local sync repository (i.e. the same sqlite database), they should share a +// single SyncManager instance. The caller should typically create one +// SyncManager for the lifetime of a user session. +// +// Unless stated otherwise, all methods of SyncManager should be called on the +// same thread. +class SyncManager { + public: + // SyncInternal contains the implementation of SyncManager, while abstracting + // internal types from clients of the interface. + class SyncInternal; + + // Status encapsulates detailed state about the internals of the SyncManager. + struct Status { + Status(); + ~Status(); + + bool notifications_enabled; // True only if subscribed for notifications. + + // Notifications counters updated by the actions in synapi. + int notifications_received; + + browser_sync::SyncProtocolError sync_protocol_error; + + // Number of unsynced items counted at the start of most recent sync cycle. + int unsynced_count; + + // Number of encryption conflicts counted during most recent sync cycle. + int encryption_conflicts; + + // Number of hierarchy conflicts counted during most recent sync cycle. + int hierarchy_conflicts; + + // Number of simple conflicts counted during most recent sync cycle. + int simple_conflicts; + + // Number of items the server refused to commit due to conflict during most + // recent sync cycle. + int server_conflicts; + + // Number of items successfully committed during most recent sync cycle. + int committed_count; + + bool syncing; + // True after a client has done a first sync. + bool initial_sync_ended; + + // Total updates available. If zero, nothing left to download. + int64 updates_available; + // Total updates received by the syncer since browser start. + int updates_received; + // Total updates received that are echoes of our own changes. + int reflected_updates_received; + + // Of updates_received, how many were tombstones. + int tombstone_updates_received; + + // Total number of overwrites due to conflict resolver since browser start. + int num_local_overwrites_total; + int num_server_overwrites_total; + + // Count of empty and non empty getupdates; + int nonempty_get_updates; + int empty_get_updates; + + // Count of sync cycles that successfully committed items; + int sync_cycles_with_commits; + int sync_cycles_without_commits; + + // Count of useless and useful syncs we perform. + int useless_sync_cycles; + int useful_sync_cycles; + + // Encryption related. + syncable::ModelTypeSet encrypted_types; + bool cryptographer_ready; + bool crypto_has_pending_keys; + + // The unique identifer for this client. + std::string unique_id; + }; + + // An interface the embedding application implements to be notified + // on change events. Note that these methods may be called on *any* + // thread. + class ChangeDelegate { + public: + // Notify the delegate that changes have been applied to the sync model. + // + // This will be invoked on the same thread as on which ApplyChanges was + // called. |changes| is an array of size |change_count|, and contains the + // ID of each individual item that was changed. |changes| exists only for + // the duration of the call. If items of multiple data types change at + // the same time, this method is invoked once per data type and |changes| + // is restricted to items of the ModelType indicated by |model_type|. + // Because the observer is passed a |trans|, the observer can assume a + // read lock on the sync model that will be released after the function + // returns. + // + // The SyncManager constructs |changes| in the following guaranteed order: + // + // 1. Deletions, from leaves up to parents. + // 2. Updates to existing items with synced parents & predecessors. + // 3. New items with synced parents & predecessors. + // 4. Items with parents & predecessors in |changes|. + // 5. Repeat #4 until all items are in |changes|. + // + // Thus, an implementation of OnChangesApplied should be able to + // process the change records in the order without having to worry about + // forward dependencies. But since deletions come before reparent + // operations, a delete may temporarily orphan a node that is + // updated later in the list. + virtual void OnChangesApplied( + syncable::ModelType model_type, + const BaseTransaction* trans, + const ImmutableChangeRecordList& changes) = 0; + + // OnChangesComplete gets called when the TransactionComplete event is + // posted (after OnChangesApplied finishes), after the transaction lock + // and the change channel mutex are released. + // + // The purpose of this function is to support processors that require + // split-transactions changes. For example, if a model processor wants to + // perform blocking I/O due to a change, it should calculate the changes + // while holding the transaction lock (from within OnChangesApplied), buffer + // those changes, let the transaction fall out of scope, and then commit + // those changes from within OnChangesComplete (postponing the blocking + // I/O to when it no longer holds any lock). + virtual void OnChangesComplete(syncable::ModelType model_type) = 0; + + protected: + virtual ~ChangeDelegate(); + }; + + // Like ChangeDelegate, except called only on the sync thread and + // not while a transaction is held. For objects that want to know + // when changes happen, but don't need to process them. + class ChangeObserver { + public: + // Ids referred to in |changes| may or may not be in the write + // transaction specified by |write_transaction_id|. If they're + // not, that means that the node didn't actually change, but we + // marked them as changed for some other reason (e.g., siblings of + // re-ordered nodes). + // + // TODO(sync, long-term): Ideally, ChangeDelegate/Observer would + // be passed a transformed version of EntryKernelMutation instead + // of a transaction that would have to be used to look up the + // changed nodes. That is, ChangeDelegate::OnChangesApplied() + // would still be called under the transaction, but all the needed + // data will be passed down. + // + // Even more ideally, we would have sync semantics such that we'd + // be able to apply changes without being under a transaction. + // But that's a ways off... + virtual void OnChangesApplied( + syncable::ModelType model_type, + int64 write_transaction_id, + const ImmutableChangeRecordList& changes) = 0; + + virtual void OnChangesComplete(syncable::ModelType model_type) = 0; + + protected: + virtual ~ChangeObserver(); + }; + + // An interface the embedding application implements to receive + // notifications from the SyncManager. Register an observer via + // SyncManager::AddObserver. All methods are called only on the + // sync thread. + class Observer { + public: + // A round-trip sync-cycle took place and the syncer has resolved any + // conflicts that may have arisen. + virtual void OnSyncCycleCompleted( + const browser_sync::sessions::SyncSessionSnapshot* snapshot) = 0; + + // Called when the status of the connection to the sync server has + // changed. + virtual void OnConnectionStatusChange(ConnectionStatus status) = 0; + + // Called when a new auth token is provided by the sync server. + virtual void OnUpdatedToken(const std::string& token) = 0; + + // Called when user interaction is required to obtain a valid passphrase. + // - If the passphrase is required for encryption, |reason| will be + // REASON_ENCRYPTION. + // - If the passphrase is required for the decryption of data that has + // already been encrypted, |reason| will be REASON_DECRYPTION. + // - If the passphrase is required because decryption failed, and a new + // passphrase is required, |reason| will be REASON_SET_PASSPHRASE_FAILED. + // + // |pending_keys| is a copy of the cryptographer's pending keys, that may be + // cached by the frontend for subsequent use by the UI. + virtual void OnPassphraseRequired( + PassphraseRequiredReason reason, + const sync_pb::EncryptedData& pending_keys) = 0; + + // Called when the passphrase provided by the user has been accepted and is + // now used to encrypt sync data. + virtual void OnPassphraseAccepted() = 0; + + // |bootstrap_token| is an opaque base64 encoded representation of the key + // generated by the current passphrase, and is provided to the observer for + // persistence purposes and use in a future initialization of sync (e.g. + // after restart). The boostrap token will always be derived from the most + // recent GAIA password (for accounts with implicit passphrases), even if + // the data is still encrypted with an older GAIA password. For accounts + // with explicit passphrases, it will be the most recently seen custom + // passphrase. + virtual void OnBootstrapTokenUpdated( + const std::string& bootstrap_token) = 0; + + // Called when initialization is complete to the point that SyncManager can + // process changes. This does not necessarily mean authentication succeeded + // or that the SyncManager is online. + // IMPORTANT: Creating any type of transaction before receiving this + // notification is illegal! + // WARNING: Calling methods on the SyncManager before receiving this + // message, unless otherwise specified, produces undefined behavior. + // + // |js_backend| is what about:sync interacts with. It can emit + // the following events: + + /** + * @param {{ enabled: boolean }} details A dictionary containing: + * - enabled: whether or not notifications are enabled. + */ + // function onNotificationStateChange(details); + + /** + * @param {{ changedTypes: Array.<string> }} details A dictionary + * containing: + * - changedTypes: a list of types (as strings) for which there + are new updates. + */ + // function onIncomingNotification(details); + + // Also, it responds to the following messages (all other messages + // are ignored): + + /** + * Gets the current notification state. + * + * @param {function(boolean)} callback Called with whether or not + * notifications are enabled. + */ + // function getNotificationState(callback); + + /** + * Gets details about the root node. + * + * @param {function(!Object)} callback Called with details about the + * root node. + */ + // TODO(akalin): Change this to getRootNodeId or eliminate it + // entirely. + // function getRootNodeDetails(callback); + + /** + * Gets summary information for a list of ids. + * + * @param {Array.<string>} idList List of 64-bit ids in decimal + * string form. + * @param {Array.<{id: string, title: string, isFolder: boolean}>} + * callback Called with summaries for the nodes in idList that + * exist. + */ + // function getNodeSummariesById(idList, callback); + + /** + * Gets detailed information for a list of ids. + * + * @param {Array.<string>} idList List of 64-bit ids in decimal + * string form. + * @param {Array.<!Object>} callback Called with detailed + * information for the nodes in idList that exist. + */ + // function getNodeDetailsById(idList, callback); + + /** + * Gets child ids for a given id. + * + * @param {string} id 64-bit id in decimal string form of the parent + * node. + * @param {Array.<string>} callback Called with the (possibly empty) + * list of child ids. + */ + // function getChildNodeIds(id); + + virtual void OnInitializationComplete( + const browser_sync::WeakHandle<browser_sync::JsBackend>& + js_backend, bool success) = 0; + + // We are no longer permitted to communicate with the server. Sync should + // be disabled and state cleaned up at once. This can happen for a number + // of reasons, e.g. swapping from a test instance to production, or a + // global stop syncing operation has wiped the store. + virtual void OnStopSyncingPermanently() = 0; + + // After a request to clear server data, these callbacks are invoked to + // indicate success or failure. + virtual void OnClearServerDataSucceeded() = 0; + virtual void OnClearServerDataFailed() = 0; + + // Called when the set of encrypted types or the encrypt + // everything flag has been changed. Note that encryption isn't + // complete until the OnEncryptionComplete() notification has been + // sent (see below). + // + // |encrypted_types| will always be a superset of + // Cryptographer::SensitiveTypes(). If |encrypt_everything| is + // true, |encrypted_types| will be the set of all known types. + // + // Until this function is called, observers can assume that the + // set of encrypted types is Cryptographer::SensitiveTypes() and + // that the encrypt everything flag is false. + // + // Called from within a transaction. + virtual void OnEncryptedTypesChanged( + syncable::ModelTypeSet encrypted_types, + bool encrypt_everything) = 0; + + // Called after we finish encrypting the current set of encrypted + // types. + // + // Called from within a transaction. + virtual void OnEncryptionComplete() = 0; + + virtual void OnActionableError( + const browser_sync::SyncProtocolError& sync_protocol_error) = 0; + + protected: + virtual ~Observer(); + }; + + enum TestingMode { + NON_TEST, + TEST_ON_DISK, + TEST_IN_MEMORY, + }; + + // Create an uninitialized SyncManager. Callers must Init() before using. + explicit SyncManager(const std::string& name); + virtual ~SyncManager(); + + // Initialize the sync manager. |database_location| specifies the path of + // the directory in which to locate a sqlite repository storing the syncer + // backend state. Initialization will open the database, or create it if it + // does not already exist. Returns false on failure. + // |event_handler| is the JsEventHandler used to propagate events to + // chrome://sync-internals. |event_handler| may be uninitialized. + // |sync_server_and_path| and |sync_server_port| represent the Chrome sync + // server to use, and |use_ssl| specifies whether to communicate securely; + // the default is false. + // |blocking_task_runner| is a TaskRunner to be used for tasks that + // may block on disk I/O. + // |post_factory| will be owned internally and used to create + // instances of an HttpPostProvider. + // |model_safe_worker| ownership is given to the SyncManager. + // |user_agent| is a 7-bit ASCII string suitable for use as the User-Agent + // HTTP header. Used internally when collecting stats to classify clients. + // |sync_notifier| is owned and used to listen for notifications. + // |report_unrecoverable_error_function| may be NULL. + bool Init(const FilePath& database_location, + const browser_sync::WeakHandle<browser_sync::JsEventHandler>& + event_handler, + const std::string& sync_server_and_path, + int sync_server_port, + bool use_ssl, + const scoped_refptr<base::TaskRunner>& blocking_task_runner, + HttpPostProviderFactory* post_factory, + browser_sync::ModelSafeWorkerRegistrar* registrar, + browser_sync::ExtensionsActivityMonitor* + extensions_activity_monitor, + ChangeDelegate* change_delegate, + const std::string& user_agent, + const SyncCredentials& credentials, + bool enable_sync_tabs_for_other_clients, + sync_notifier::SyncNotifier* sync_notifier, + const std::string& restored_key_for_bootstrapping, + TestingMode testing_mode, + browser_sync::Encryptor* encryptor, + browser_sync::UnrecoverableErrorHandler* + unrecoverable_error_handler, + browser_sync::ReportUnrecoverableErrorFunction + report_unrecoverable_error_function); + + // Throw an unrecoverable error from a transaction (mostly used for + // testing). + void ThrowUnrecoverableError(); + + // Check if the database has been populated with a full "initial" download of + // sync items for each data type currently present in the routing info. + // Prerequisite for calling this is that OnInitializationComplete has been + // called. May be called from any thread. + bool InitialSyncEndedForAllEnabledTypes(); + + // Update tokens that we're using in Sync. Email must stay the same. + void UpdateCredentials(const SyncCredentials& credentials); + + // Called when the user disables or enables a sync type. + void UpdateEnabledTypes(); + + // Conditionally sets the flag in the Nigori node which instructs other + // clients to start syncing tabs. + void MaybeSetSyncTabsInNigoriNode(syncable::ModelTypeSet enabled_types); + + // Put the syncer in normal mode ready to perform nudges and polls. + void StartSyncingNormally(); + + // Attempts to re-encrypt encrypted data types using the passphrase provided. + // Notifies observers of the result of the operation via OnPassphraseAccepted + // or OnPassphraseRequired, updates the nigori node, and does re-encryption as + // appropriate. If an explicit password has been set previously, we drop + // subsequent requests to set a passphrase. If the cryptographer has pending + // keys, and a new implicit passphrase is provided, we try decrypting the + // pending keys with it, and if that fails, we cache the passphrase for + // re-encryption once the pending keys are decrypted. + void SetEncryptionPassphrase(const std::string& passphrase, bool is_explicit); + + // Provides a passphrase for decrypting the user's existing sync data. + // Notifies observers of the result of the operation via OnPassphraseAccepted + // or OnPassphraseRequired, updates the nigori node, and does re-encryption as + // appropriate if there is a previously cached encryption passphrase. It is an + // error to call this when we don't have pending keys. + void SetDecryptionPassphrase(const std::string& passphrase); + + // Puts the SyncScheduler into a mode where no normal nudge or poll traffic + // will occur, but calls to RequestConfig will be supported. If |callback| + // is provided, it will be invoked (from the internal SyncScheduler) when + // the thread has changed to configuration mode. + void StartConfigurationMode(const base::Closure& callback); + + // Switches the mode of operation to CONFIGURATION_MODE and + // schedules a config task to fetch updates for |types|. + void RequestConfig(syncable::ModelTypeSet types, + sync_api::ConfigureReason reason); + + void RequestCleanupDisabledTypes(); + + // Request a clearing of all data on the server + void RequestClearServerData(); + + // Adds a listener to be notified of sync events. + // NOTE: It is OK (in fact, it's probably a good idea) to call this before + // having received OnInitializationCompleted. + void AddObserver(Observer* observer); + + // Remove the given observer. Make sure to call this if the + // Observer is being destroyed so the SyncManager doesn't + // potentially dereference garbage. + void RemoveObserver(Observer* observer); + + // Status-related getter. May be called on any thread. + Status GetDetailedStatus() const; + + // Whether or not the Nigori node is encrypted using an explicit passphrase. + // May be called on any thread. + bool IsUsingExplicitPassphrase(); + + // Call periodically from a database-safe thread to persist recent changes + // to the syncapi model. + void SaveChanges(); + + // Initiates shutdown of various components in the sync engine. Must be + // called from the main thread to allow preempting ongoing tasks on the sync + // loop (that may be blocked on I/O). The semantics of |callback| are the + // same as with StartConfigurationMode. If provided and a scheduler / sync + // loop exists, it will be invoked from the sync loop by the scheduler to + // notify that all work has been flushed + cancelled, and it is idle. + // If no scheduler exists, the callback is run immediately (from the loop + // this was created on, which is the sync loop), as sync is effectively + // stopped. + void StopSyncingForShutdown(const base::Closure& callback); + + // Issue a final SaveChanges, and close sqlite handles. + void ShutdownOnSyncThread(); + + // May be called from any thread. + UserShare* GetUserShare() const; + + // Inform the cryptographer of the most recent passphrase and set of + // encrypted types (from nigori node), then ensure all data that + // needs encryption is encrypted with the appropriate passphrase. + // + // May trigger OnPassphraseRequired(). Otherwise, it will trigger + // OnEncryptedTypesChanged() if necessary (see comments for + // OnEncryptedTypesChanged()), and then OnEncryptionComplete(). + // + // Also updates or adds device information to the nigori node. + // + // Note: opens a transaction, so must only be called after syncapi + // has been initialized. + void RefreshNigori(const std::string& chrome_version, + const base::Closure& done_callback); + + // Enable encryption of all sync data. Once enabled, it can never be + // disabled without clearing the server data. + // + // This will trigger OnEncryptedTypesChanged() if necessary (see + // comments for OnEncryptedTypesChanged()). It then may trigger + // OnPassphraseRequired(), but otherwise it will trigger + // OnEncryptionComplete(). + void EnableEncryptEverything(); + + // Returns true if we are currently encrypting all sync data. May + // be called on any thread. + bool EncryptEverythingEnabledForTest() const; + + // Gets the set of encrypted types from the cryptographer + // Note: opens a transaction. May be called from any thread. + syncable::ModelTypeSet GetEncryptedDataTypesForTest() const; + + // Reads the nigori node to determine if any experimental types should be + // enabled. + // Note: opens a transaction. May be called on any thread. + bool ReceivedExperimentalTypes(syncable::ModelTypeSet* to_add) const; + + // Uses a read-only transaction to determine if the directory being synced has + // any remaining unsynced items. May be called on any thread. + bool HasUnsyncedItems() const; + + // Functions used for testing. + + void TriggerOnNotificationStateChangeForTest( + bool notifications_enabled); + + void TriggerOnIncomingNotificationForTest( + syncable::ModelTypeSet model_types); + + static const int kDefaultNudgeDelayMilliseconds; + static const int kPreferencesNudgeDelayMilliseconds; + static const int kPiggybackNudgeDelay; + + static const FilePath::CharType kSyncDatabaseFilename[]; + + private: + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, NudgeDelayTest); + + // For unit tests. + base::TimeDelta GetNudgeDelayTimeDelta(const syncable::ModelType& model_type); + + base::ThreadChecker thread_checker_; + + // An opaque pointer to the nested private class. + SyncInternal* data_; + + DISALLOW_COPY_AND_ASSIGN(SyncManager); +}; + +bool InitialSyncEndedForTypes(syncable::ModelTypeSet types, UserShare* share); + +syncable::ModelTypeSet GetTypesWithEmptyProgressMarkerToken( + syncable::ModelTypeSet types, + sync_api::UserShare* share); + +const char* ConnectionStatusToString(ConnectionStatus status); + +// Returns the string representation of a PassphraseRequiredReason value. +const char* PassphraseRequiredReasonToString(PassphraseRequiredReason reason); + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_SYNC_MANAGER_H_ diff --git a/sync/internal_api/syncapi_internal.cc b/sync/internal_api/syncapi_internal.cc new file mode 100644 index 0000000..0f7a27a --- /dev/null +++ b/sync/internal_api/syncapi_internal.cc @@ -0,0 +1,87 @@ +// Copyright (c) 2012 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 "sync/internal_api/syncapi_internal.h" + +#include "base/memory/scoped_ptr.h" +#include "sync/protocol/password_specifics.pb.h" +#include "sync/protocol/sync.pb.h" +#include "sync/util/cryptographer.h" + +using browser_sync::Cryptographer; + +namespace sync_api { + +sync_pb::PasswordSpecificsData* DecryptPasswordSpecifics( + const sync_pb::EntitySpecifics& specifics, Cryptographer* crypto) { + if (!specifics.has_password()) + return NULL; + const sync_pb::PasswordSpecifics& password_specifics = specifics.password(); + if (!password_specifics.has_encrypted()) + return NULL; + const sync_pb::EncryptedData& encrypted = password_specifics.encrypted(); + scoped_ptr<sync_pb::PasswordSpecificsData> data( + new sync_pb::PasswordSpecificsData); + if (!crypto->Decrypt(encrypted, data.get())) + return NULL; + return data.release(); +} + +// The list of names which are reserved for use by the server. +static const char* kForbiddenServerNames[] = { "", ".", ".." }; + +// When taking a name from the syncapi, append a space if it matches the +// pattern of a server-illegal name followed by zero or more spaces. +void SyncAPINameToServerName(const std::string& sync_api_name, + std::string* out) { + *out = sync_api_name; + if (IsNameServerIllegalAfterTrimming(*out)) + out->append(" "); +} + +// Checks whether |name| is a server-illegal name followed by zero or more space +// characters. The three server-illegal names are the empty string, dot, and +// dot-dot. Very long names (>255 bytes in UTF-8 Normalization Form C) are +// also illegal, but are not considered here. +bool IsNameServerIllegalAfterTrimming(const std::string& name) { + size_t untrimmed_count = name.find_last_not_of(' ') + 1; + for (size_t i = 0; i < arraysize(kForbiddenServerNames); ++i) { + if (name.compare(0, untrimmed_count, kForbiddenServerNames[i]) == 0) + return true; + } + return false; +} + +// Compare the values of two EntitySpecifics, accounting for encryption. +bool AreSpecificsEqual(const browser_sync::Cryptographer* cryptographer, + const sync_pb::EntitySpecifics& left, + const sync_pb::EntitySpecifics& right) { + // Note that we can't compare encrypted strings directly as they are seeded + // with a random value. + std::string left_plaintext, right_plaintext; + if (left.has_encrypted()) { + if (!cryptographer->CanDecrypt(left.encrypted())) { + NOTREACHED() << "Attempting to compare undecryptable data."; + return false; + } + left_plaintext = cryptographer->DecryptToString(left.encrypted()); + } else { + left_plaintext = left.SerializeAsString(); + } + if (right.has_encrypted()) { + if (!cryptographer->CanDecrypt(right.encrypted())) { + NOTREACHED() << "Attempting to compare undecryptable data."; + return false; + } + right_plaintext = cryptographer->DecryptToString(right.encrypted()); + } else { + right_plaintext = right.SerializeAsString(); + } + if (left_plaintext == right_plaintext) { + return true; + } + return false; +} + +} // namespace sync_api diff --git a/sync/internal_api/syncapi_internal.h b/sync/internal_api/syncapi_internal.h new file mode 100644 index 0000000..162d26f --- /dev/null +++ b/sync/internal_api/syncapi_internal.h @@ -0,0 +1,37 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_SYNCAPI_INTERNAL_H_ +#define SYNC_INTERNAL_API_SYNCAPI_INTERNAL_H_ + +// The functions defined are shared among some of the classes that implement +// the internal sync_api. They are not to be used by clients of the API. + +#include <string> + +namespace browser_sync { +class Cryptographer; +} + +namespace sync_pb { +class EntitySpecifics; +class PasswordSpecificsData; +} + +namespace sync_api { +sync_pb::PasswordSpecificsData* DecryptPasswordSpecifics( + const sync_pb::EntitySpecifics& specifics, + browser_sync::Cryptographer* crypto); + +void SyncAPINameToServerName(const std::string& sync_api_name, + std::string* out); + +bool IsNameServerIllegalAfterTrimming(const std::string& name); + +bool AreSpecificsEqual(const browser_sync::Cryptographer* cryptographer, + const sync_pb::EntitySpecifics& left, + const sync_pb::EntitySpecifics& right); +} + +#endif // SYNC_INTERNAL_API_SYNCAPI_INTERNAL_H_ diff --git a/sync/internal_api/syncapi_server_connection_manager.cc b/sync/internal_api/syncapi_server_connection_manager.cc new file mode 100644 index 0000000..ce62d86 --- /dev/null +++ b/sync/internal_api/syncapi_server_connection_manager.cc @@ -0,0 +1,107 @@ +// Copyright (c) 2012 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 "sync/internal_api/syncapi_server_connection_manager.h" + +#include "net/base/net_errors.h" +#include "net/http/http_status_code.h" +#include "sync/internal_api/http_post_provider_factory.h" +#include "sync/internal_api/http_post_provider_interface.h" + +using browser_sync::HttpResponse; + +namespace sync_api { + +SyncAPIBridgedConnection::SyncAPIBridgedConnection( + browser_sync::ServerConnectionManager* scm, + HttpPostProviderFactory* factory) + : Connection(scm), factory_(factory) { + post_provider_ = factory_->Create(); +} + +SyncAPIBridgedConnection::~SyncAPIBridgedConnection() { + DCHECK(post_provider_); + factory_->Destroy(post_provider_); + post_provider_ = NULL; +} + +bool SyncAPIBridgedConnection::Init(const char* path, + const std::string& auth_token, + const std::string& payload, + HttpResponse* response) { + std::string sync_server; + int sync_server_port = 0; + bool use_ssl = false; + GetServerParams(&sync_server, &sync_server_port, &use_ssl); + std::string connection_url = MakeConnectionURL(sync_server, path, use_ssl); + + HttpPostProviderInterface* http = post_provider_; + http->SetUserAgent(scm_->user_agent().c_str()); + http->SetURL(connection_url.c_str(), sync_server_port); + + if (!auth_token.empty()) { + const std::string& headers = + "Authorization: GoogleLogin auth=" + auth_token; + http->SetExtraRequestHeaders(headers.c_str()); + } + + // Must be octet-stream, or the payload may be parsed for a cookie. + http->SetPostPayload("application/octet-stream", payload.length(), + payload.data()); + + // Issue the POST, blocking until it finishes. + int error_code = 0; + int response_code = 0; + if (!http->MakeSynchronousPost(&error_code, &response_code)) { + DVLOG(1) << "Http POST failed, error returns: " << error_code; + response->server_status = error_code == net::ERR_ABORTED ? + HttpResponse::CONNECTION_UNAVAILABLE : HttpResponse::IO_ERROR; + return false; + } + + // We got a server response, copy over response codes and content. + response->response_code = response_code; + response->content_length = + static_cast<int64>(http->GetResponseContentLength()); + response->payload_length = + static_cast<int64>(http->GetResponseContentLength()); + if (response->response_code < 400) + response->server_status = HttpResponse::SERVER_CONNECTION_OK; + else if (response->response_code == net::HTTP_UNAUTHORIZED) + response->server_status = HttpResponse::SYNC_AUTH_ERROR; + else + response->server_status = HttpResponse::SYNC_SERVER_ERROR; + + response->update_client_auth_header = + http->GetResponseHeaderValue("Update-Client-Auth"); + + // Write the content into our buffer. + buffer_.assign(http->GetResponseContent(), http->GetResponseContentLength()); + return true; +} + +void SyncAPIBridgedConnection::Abort() { + DCHECK(post_provider_); + post_provider_->Abort(); +} + +SyncAPIServerConnectionManager::SyncAPIServerConnectionManager( + const std::string& server, + int port, + bool use_ssl, + const std::string& client_version, + HttpPostProviderFactory* factory) + : ServerConnectionManager(server, port, use_ssl, client_version), + post_provider_factory_(factory) { + DCHECK(post_provider_factory_.get()); +} + +SyncAPIServerConnectionManager::~SyncAPIServerConnectionManager() {} + +browser_sync::ServerConnectionManager::Connection* +SyncAPIServerConnectionManager::MakeConnection() { + return new SyncAPIBridgedConnection(this, post_provider_factory_.get()); +} + +} // namespace sync_api diff --git a/sync/internal_api/syncapi_server_connection_manager.h b/sync/internal_api/syncapi_server_connection_manager.h new file mode 100644 index 0000000..fafde1e --- /dev/null +++ b/sync/internal_api/syncapi_server_connection_manager.h @@ -0,0 +1,78 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_SYNCAPI_SERVER_CONNECTION_MANAGER_H_ +#define SYNC_INTERNAL_API_SYNCAPI_SERVER_CONNECTION_MANAGER_H_ +#pragma once + +#include <string> + +#include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" +#include "base/memory/scoped_ptr.h" +#include "sync/engine/net/server_connection_manager.h" + +namespace sync_api { + +class HttpPostProviderFactory; +class HttpPostProviderInterface; + +// This provides HTTP Post functionality through the interface provided +// to the sync API by the application hosting the syncer backend. +class SyncAPIBridgedConnection + : public browser_sync::ServerConnectionManager::Connection { + public: + SyncAPIBridgedConnection(browser_sync::ServerConnectionManager* scm, + HttpPostProviderFactory* factory); + + virtual ~SyncAPIBridgedConnection(); + + virtual bool Init(const char* path, + const std::string& auth_token, + const std::string& payload, + browser_sync::HttpResponse* response) OVERRIDE; + + virtual void Abort() OVERRIDE; + + private: + // Pointer to the factory we use for creating HttpPostProviders. We do not + // own |factory_|. + HttpPostProviderFactory* factory_; + + HttpPostProviderInterface* post_provider_; + + DISALLOW_COPY_AND_ASSIGN(SyncAPIBridgedConnection); +}; + +// A ServerConnectionManager subclass used by the syncapi layer. We use a +// subclass so that we can override MakePost() to generate a POST object using +// an instance of the HttpPostProviderFactory class. +class SyncAPIServerConnectionManager + : public browser_sync::ServerConnectionManager { + public: + // Takes ownership of factory. + SyncAPIServerConnectionManager(const std::string& server, + int port, + bool use_ssl, + const std::string& client_version, + HttpPostProviderFactory* factory); + virtual ~SyncAPIServerConnectionManager(); + + // ServerConnectionManager overrides. + virtual Connection* MakeConnection() OVERRIDE; + + private: + FRIEND_TEST_ALL_PREFIXES(SyncAPIServerConnectionManagerTest, EarlyAbortPost); + FRIEND_TEST_ALL_PREFIXES(SyncAPIServerConnectionManagerTest, AbortPost); + + // A factory creating concrete HttpPostProviders for use whenever we need to + // issue a POST to sync servers. + scoped_ptr<HttpPostProviderFactory> post_provider_factory_; + + DISALLOW_COPY_AND_ASSIGN(SyncAPIServerConnectionManager); +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_SYNCAPI_SERVER_CONNECTION_MANAGER_H_ diff --git a/sync/internal_api/syncapi_server_connection_manager_unittest.cc b/sync/internal_api/syncapi_server_connection_manager_unittest.cc new file mode 100644 index 0000000..a2f5280 --- /dev/null +++ b/sync/internal_api/syncapi_server_connection_manager_unittest.cc @@ -0,0 +1,115 @@ +// Copyright (c) 2012 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 "sync/internal_api/syncapi_server_connection_manager.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/compiler_specific.h" +#include "base/synchronization/waitable_event.h" +#include "base/test/test_timeouts.h" +#include "base/threading/thread.h" +#include "base/time.h" +#include "net/base/net_errors.h" +#include "sync/internal_api/http_post_provider_factory.h" +#include "sync/internal_api/http_post_provider_interface.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::TimeDelta; +using browser_sync::HttpResponse; +using browser_sync::ServerConnectionManager; +using browser_sync::ScopedServerStatusWatcher; + +namespace sync_api { +namespace { + +class BlockingHttpPost : public HttpPostProviderInterface { + public: + BlockingHttpPost() : wait_for_abort_(false, false) {} + virtual ~BlockingHttpPost() {} + + virtual void SetUserAgent(const char* user_agent) OVERRIDE {} + virtual void SetExtraRequestHeaders(const char* headers) OVERRIDE {} + virtual void SetURL(const char* url, int port) OVERRIDE {} + virtual void SetPostPayload(const char* content_type, + int content_length, + const char* content) OVERRIDE {} + virtual bool MakeSynchronousPost(int* error_code, int* response_code) + OVERRIDE { + wait_for_abort_.TimedWait(TimeDelta::FromMilliseconds( + TestTimeouts::action_max_timeout_ms())); + *error_code = net::ERR_ABORTED; + return false; + } + virtual int GetResponseContentLength() const OVERRIDE { + return 0; + } + virtual const char* GetResponseContent() const OVERRIDE { + return ""; + } + virtual const std::string GetResponseHeaderValue( + const std::string& name) const OVERRIDE { + return ""; + } + virtual void Abort() OVERRIDE { + wait_for_abort_.Signal(); + } + private: + base::WaitableEvent wait_for_abort_; +}; + +class BlockingHttpPostFactory : public HttpPostProviderFactory { + public: + virtual ~BlockingHttpPostFactory() {} + virtual HttpPostProviderInterface* Create() OVERRIDE { + return new BlockingHttpPost(); + } + virtual void Destroy(HttpPostProviderInterface* http) OVERRIDE { + delete http; + } +}; + +} // namespace + +TEST(SyncAPIServerConnectionManagerTest, EarlyAbortPost) { + SyncAPIServerConnectionManager server( + "server", 0, true, "1", new BlockingHttpPostFactory()); + + ServerConnectionManager::PostBufferParams params; + ScopedServerStatusWatcher watcher(&server, ¶ms.response); + + server.TerminateAllIO(); + bool result = server.PostBufferToPath( + ¶ms, "/testpath", "testauth", &watcher); + + EXPECT_FALSE(result); + EXPECT_EQ(HttpResponse::CONNECTION_UNAVAILABLE, + params.response.server_status); +} + +TEST(SyncAPIServerConnectionManagerTest, AbortPost) { + SyncAPIServerConnectionManager server( + "server", 0, true, "1", new BlockingHttpPostFactory()); + + ServerConnectionManager::PostBufferParams params; + ScopedServerStatusWatcher watcher(&server, ¶ms.response); + + base::Thread abort_thread("Test_AbortThread"); + ASSERT_TRUE(abort_thread.Start()); + abort_thread.message_loop()->PostDelayedTask( + FROM_HERE, + base::Bind(&ServerConnectionManager::TerminateAllIO, + base::Unretained(&server)), + TestTimeouts::tiny_timeout()); + + bool result = server.PostBufferToPath( + ¶ms, "/testpath", "testauth", &watcher); + + EXPECT_FALSE(result); + EXPECT_EQ(HttpResponse::CONNECTION_UNAVAILABLE, + params.response.server_status); + abort_thread.Stop(); +} + +} // namespace sync_api diff --git a/sync/internal_api/syncapi_unittest.cc b/sync/internal_api/syncapi_unittest.cc new file mode 100644 index 0000000..a9e74ce --- /dev/null +++ b/sync/internal_api/syncapi_unittest.cc @@ -0,0 +1,2519 @@ +// Copyright (c) 2012 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. + +// Unit tests for the SyncApi. Note that a lot of the underlying +// functionality is provided by the Syncable layer, which has its own +// unit tests. We'll test SyncApi specific things in this harness. + +#include <cstddef> +#include <map> + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/format_macros.h" +#include "base/location.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "base/message_loop_proxy.h" +#include "base/scoped_temp_dir.h" +#include "base/string_number_conversions.h" +#include "base/stringprintf.h" +#include "base/test/values_test_util.h" +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "sync/engine/model_safe_worker.h" +#include "sync/engine/nigori_util.h" +#include "sync/engine/polling_constants.h" +#include "sync/internal_api/change_record.h" +#include "sync/internal_api/http_post_provider_factory.h" +#include "sync/internal_api/http_post_provider_interface.h" +#include "sync/internal_api/read_node.h" +#include "sync/internal_api/read_transaction.h" +#include "sync/internal_api/sync_manager.h" +#include "sync/internal_api/syncapi_internal.h" +#include "sync/internal_api/test_user_share.h" +#include "sync/internal_api/write_node.h" +#include "sync/internal_api/write_transaction.h" +#include "sync/js/js_arg_list.h" +#include "sync/js/js_backend.h" +#include "sync/js/js_event_handler.h" +#include "sync/js/js_reply_handler.h" +#include "sync/js/js_test_util.h" +#include "sync/notifier/sync_notifier.h" +#include "sync/notifier/sync_notifier_observer.h" +#include "sync/protocol/bookmark_specifics.pb.h" +#include "sync/protocol/encryption.pb.h" +#include "sync/protocol/extension_specifics.pb.h" +#include "sync/protocol/password_specifics.pb.h" +#include "sync/protocol/preference_specifics.pb.h" +#include "sync/protocol/proto_value_conversions.h" +#include "sync/protocol/sync.pb.h" +#include "sync/sessions/sync_session.h" +#include "sync/syncable/model_type_test_util.h" +#include "sync/syncable/syncable.h" +#include "sync/syncable/syncable_id.h" +#include "sync/test/fake_encryptor.h" +#include "sync/test/fake_extensions_activity_monitor.h" +#include "sync/util/cryptographer.h" +#include "sync/util/extensions_activity_monitor.h" +#include "sync/util/test_unrecoverable_error_handler.h" +#include "sync/util/time.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using base::ExpectDictStringValue; +using browser_sync::Cryptographer; +using browser_sync::FakeEncryptor; +using browser_sync::FakeExtensionsActivityMonitor; +using browser_sync::HasArgsAsList; +using browser_sync::HasDetailsAsDictionary; +using browser_sync::KeyParams; +using browser_sync::kNigoriTag; +using browser_sync::JsArgList; +using browser_sync::JsBackend; +using browser_sync::JsEventHandler; +using browser_sync::JsReplyHandler; +using browser_sync::MockJsEventHandler; +using browser_sync::MockJsReplyHandler; +using browser_sync::ModelSafeRoutingInfo; +using browser_sync::ModelSafeWorker; +using browser_sync::ModelSafeWorkerRegistrar; +using browser_sync::sessions::SyncSessionSnapshot; +using browser_sync::TestUnrecoverableErrorHandler; +using browser_sync::WeakHandle; +using syncable::IS_DEL; +using syncable::IS_UNSYNCED; +using syncable::kEncryptedString; +using syncable::ModelTypeSet; +using syncable::ModelType; +using syncable::NON_UNIQUE_NAME; +using syncable::SPECIFICS; +using testing::_; +using testing::AnyNumber; +using testing::AtLeast; +using testing::InSequence; +using testing::Invoke; +using testing::SaveArg; +using testing::StrictMock; + +namespace sync_api { + +namespace { + +const char kTestChromeVersion[] = "test chrome version"; + +void DoNothing() {} + +void ExpectInt64Value(int64 expected_value, + const DictionaryValue& value, const std::string& key) { + std::string int64_str; + EXPECT_TRUE(value.GetString(key, &int64_str)); + int64 val = 0; + EXPECT_TRUE(base::StringToInt64(int64_str, &val)); + EXPECT_EQ(expected_value, val); +} + +void ExpectTimeValue(const base::Time& expected_value, + const DictionaryValue& value, const std::string& key) { + std::string time_str; + EXPECT_TRUE(value.GetString(key, &time_str)); + EXPECT_EQ(browser_sync::GetTimeDebugString(expected_value), time_str); +} + +// Makes a non-folder child of the root node. Returns the id of the +// newly-created node. +int64 MakeNode(UserShare* share, + ModelType model_type, + const std::string& client_tag) { + WriteTransaction trans(FROM_HERE, share); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + WriteNode node(&trans); + EXPECT_TRUE(node.InitUniqueByCreation(model_type, root_node, client_tag)); + node.SetIsFolder(false); + return node.GetId(); +} + +// Makes a non-folder child of a non-root node. Returns the id of the +// newly-created node. +int64 MakeNodeWithParent(UserShare* share, + ModelType model_type, + const std::string& client_tag, + int64 parent_id) { + WriteTransaction trans(FROM_HERE, share); + ReadNode parent_node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, parent_node.InitByIdLookup(parent_id)); + WriteNode node(&trans); + EXPECT_TRUE(node.InitUniqueByCreation(model_type, parent_node, client_tag)); + node.SetIsFolder(false); + return node.GetId(); +} + +// Makes a folder child of a non-root node. Returns the id of the +// newly-created node. +int64 MakeFolderWithParent(UserShare* share, + ModelType model_type, + int64 parent_id, + BaseNode* predecessor) { + WriteTransaction trans(FROM_HERE, share); + ReadNode parent_node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, parent_node.InitByIdLookup(parent_id)); + WriteNode node(&trans); + EXPECT_TRUE(node.InitByCreation(model_type, parent_node, predecessor)); + node.SetIsFolder(true); + return node.GetId(); +} + +// Creates the "synced" root node for a particular datatype. We use the syncable +// methods here so that the syncer treats these nodes as if they were already +// received from the server. +int64 MakeServerNodeForType(UserShare* share, + ModelType model_type) { + sync_pb::EntitySpecifics specifics; + syncable::AddDefaultFieldValue(model_type, &specifics); + syncable::WriteTransaction trans( + FROM_HERE, syncable::UNITTEST, share->directory.get()); + // Attempt to lookup by nigori tag. + std::string type_tag = syncable::ModelTypeToRootTag(model_type); + syncable::Id node_id = syncable::Id::CreateFromServerId(type_tag); + syncable::MutableEntry entry(&trans, syncable::CREATE_NEW_UPDATE_ITEM, + node_id); + EXPECT_TRUE(entry.good()); + entry.Put(syncable::BASE_VERSION, 1); + entry.Put(syncable::SERVER_VERSION, 1); + entry.Put(syncable::IS_UNAPPLIED_UPDATE, false); + entry.Put(syncable::SERVER_PARENT_ID, syncable::GetNullId()); + entry.Put(syncable::SERVER_IS_DIR, true); + entry.Put(syncable::IS_DIR, true); + entry.Put(syncable::SERVER_SPECIFICS, specifics); + entry.Put(syncable::UNIQUE_SERVER_TAG, type_tag); + entry.Put(syncable::NON_UNIQUE_NAME, type_tag); + entry.Put(syncable::IS_DEL, false); + entry.Put(syncable::SPECIFICS, specifics); + return entry.Get(syncable::META_HANDLE); +} + +// Simulates creating a "synced" node as a child of the root datatype node. +int64 MakeServerNode(UserShare* share, ModelType model_type, + const std::string& client_tag, + const std::string& hashed_tag, + const sync_pb::EntitySpecifics& specifics) { + syncable::WriteTransaction trans( + FROM_HERE, syncable::UNITTEST, share->directory.get()); + syncable::Entry root_entry(&trans, syncable::GET_BY_SERVER_TAG, + syncable::ModelTypeToRootTag(model_type)); + EXPECT_TRUE(root_entry.good()); + syncable::Id root_id = root_entry.Get(syncable::ID); + syncable::Id node_id = syncable::Id::CreateFromServerId(client_tag); + syncable::MutableEntry entry(&trans, syncable::CREATE_NEW_UPDATE_ITEM, + node_id); + EXPECT_TRUE(entry.good()); + entry.Put(syncable::BASE_VERSION, 1); + entry.Put(syncable::SERVER_VERSION, 1); + entry.Put(syncable::IS_UNAPPLIED_UPDATE, false); + entry.Put(syncable::SERVER_PARENT_ID, root_id); + entry.Put(syncable::PARENT_ID, root_id); + entry.Put(syncable::SERVER_IS_DIR, false); + entry.Put(syncable::IS_DIR, false); + entry.Put(syncable::SERVER_SPECIFICS, specifics); + entry.Put(syncable::NON_UNIQUE_NAME, client_tag); + entry.Put(syncable::UNIQUE_CLIENT_TAG, hashed_tag); + entry.Put(syncable::IS_DEL, false); + entry.Put(syncable::SPECIFICS, specifics); + return entry.Get(syncable::META_HANDLE); +} + +} // namespace + +class SyncApiTest : public testing::Test { + public: + virtual void SetUp() { + test_user_share_.SetUp(); + } + + virtual void TearDown() { + test_user_share_.TearDown(); + } + + protected: + MessageLoop message_loop_; + browser_sync::TestUserShare test_user_share_; +}; + +TEST_F(SyncApiTest, SanityCheckTest) { + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + EXPECT_TRUE(trans.GetWrappedTrans() != NULL); + } + { + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + EXPECT_TRUE(trans.GetWrappedTrans() != NULL); + } + { + // No entries but root should exist + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode node(&trans); + // Metahandle 1 can be root, sanity check 2 + EXPECT_EQ(BaseNode::INIT_FAILED_ENTRY_NOT_GOOD, node.InitByIdLookup(2)); + } +} + +TEST_F(SyncApiTest, BasicTagWrite) { + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + EXPECT_EQ(root_node.GetFirstChildId(), 0); + } + + ignore_result(MakeNode(test_user_share_.user_share(), + syncable::BOOKMARKS, "testtag")); + + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, "testtag")); + + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + EXPECT_NE(node.GetId(), 0); + EXPECT_EQ(node.GetId(), root_node.GetFirstChildId()); + } +} + +TEST_F(SyncApiTest, GenerateSyncableHash) { + EXPECT_EQ("OyaXV5mEzrPS4wbogmtKvRfekAI=", + BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, "tag1")); + EXPECT_EQ("iNFQtRFQb+IZcn1kKUJEZDDkLs4=", + BaseNode::GenerateSyncableHash(syncable::PREFERENCES, "tag1")); + EXPECT_EQ("gO1cPZQXaM73sHOvSA+tKCKFs58=", + BaseNode::GenerateSyncableHash(syncable::AUTOFILL, "tag1")); + + EXPECT_EQ("A0eYIHXM1/jVwKDDp12Up20IkKY=", + BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, "tag2")); + EXPECT_EQ("XYxkF7bhS4eItStFgiOIAU23swI=", + BaseNode::GenerateSyncableHash(syncable::PREFERENCES, "tag2")); + EXPECT_EQ("GFiWzo5NGhjLlN+OyCfhy28DJTQ=", + BaseNode::GenerateSyncableHash(syncable::AUTOFILL, "tag2")); +} + +TEST_F(SyncApiTest, ModelTypesSiloed) { + { + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + EXPECT_EQ(root_node.GetFirstChildId(), 0); + } + + ignore_result(MakeNode(test_user_share_.user_share(), + syncable::BOOKMARKS, "collideme")); + ignore_result(MakeNode(test_user_share_.user_share(), + syncable::PREFERENCES, "collideme")); + ignore_result(MakeNode(test_user_share_.user_share(), + syncable::AUTOFILL, "collideme")); + + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + + ReadNode bookmarknode(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + bookmarknode.InitByClientTagLookup(syncable::BOOKMARKS, + "collideme")); + + ReadNode prefnode(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + prefnode.InitByClientTagLookup(syncable::PREFERENCES, + "collideme")); + + ReadNode autofillnode(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + autofillnode.InitByClientTagLookup(syncable::AUTOFILL, + "collideme")); + + EXPECT_NE(bookmarknode.GetId(), prefnode.GetId()); + EXPECT_NE(autofillnode.GetId(), prefnode.GetId()); + EXPECT_NE(bookmarknode.GetId(), autofillnode.GetId()); + } +} + +TEST_F(SyncApiTest, ReadMissingTagsFails) { + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_FAILED_ENTRY_NOT_GOOD, + node.InitByClientTagLookup(syncable::BOOKMARKS, + "testtag")); + } + { + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_FAILED_ENTRY_NOT_GOOD, + node.InitByClientTagLookup(syncable::BOOKMARKS, + "testtag")); + } +} + +// TODO(chron): Hook this all up to the server and write full integration tests +// for update->undelete behavior. +TEST_F(SyncApiTest, TestDeleteBehavior) { + int64 node_id; + int64 folder_id; + std::string test_title("test1"); + + { + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + + // we'll use this spare folder later + WriteNode folder_node(&trans); + EXPECT_TRUE(folder_node.InitByCreation(syncable::BOOKMARKS, + root_node, NULL)); + folder_id = folder_node.GetId(); + + WriteNode wnode(&trans); + EXPECT_TRUE(wnode.InitUniqueByCreation(syncable::BOOKMARKS, + root_node, "testtag")); + wnode.SetIsFolder(false); + wnode.SetTitle(UTF8ToWide(test_title)); + + node_id = wnode.GetId(); + } + + // Ensure we can delete something with a tag. + { + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + WriteNode wnode(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + wnode.InitByClientTagLookup(syncable::BOOKMARKS, + "testtag")); + EXPECT_FALSE(wnode.GetIsFolder()); + EXPECT_EQ(wnode.GetTitle(), test_title); + + wnode.Remove(); + } + + // Lookup of a node which was deleted should return failure, + // but have found some data about the node. + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_FAILED_ENTRY_IS_DEL, + node.InitByClientTagLookup(syncable::BOOKMARKS, + "testtag")); + // Note that for proper function of this API this doesn't need to be + // filled, we're checking just to make sure the DB worked in this test. + EXPECT_EQ(node.GetTitle(), test_title); + } + + { + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode folder_node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, folder_node.InitByIdLookup(folder_id)); + + WriteNode wnode(&trans); + // This will undelete the tag. + EXPECT_TRUE(wnode.InitUniqueByCreation(syncable::BOOKMARKS, + folder_node, "testtag")); + EXPECT_EQ(wnode.GetIsFolder(), false); + EXPECT_EQ(wnode.GetParentId(), folder_node.GetId()); + EXPECT_EQ(wnode.GetId(), node_id); + EXPECT_NE(wnode.GetTitle(), test_title); // Title should be cleared + wnode.SetTitle(UTF8ToWide(test_title)); + } + + // Now look up should work. + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, + "testtag")); + EXPECT_EQ(node.GetTitle(), test_title); + EXPECT_EQ(node.GetModelType(), syncable::BOOKMARKS); + } +} + +TEST_F(SyncApiTest, WriteAndReadPassword) { + KeyParams params = {"localhost", "username", "passphrase"}; + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + trans.GetCryptographer()->AddKey(params); + } + { + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + + WriteNode password_node(&trans); + EXPECT_TRUE(password_node.InitUniqueByCreation(syncable::PASSWORDS, + root_node, "foo")); + sync_pb::PasswordSpecificsData data; + data.set_password_value("secret"); + password_node.SetPasswordSpecifics(data); + } + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + + ReadNode password_node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + password_node.InitByClientTagLookup(syncable::PASSWORDS, + "foo")); + const sync_pb::PasswordSpecificsData& data = + password_node.GetPasswordSpecifics(); + EXPECT_EQ("secret", data.password_value()); + } +} + +TEST_F(SyncApiTest, WriteEncryptedTitle) { + KeyParams params = {"localhost", "username", "passphrase"}; + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + trans.GetCryptographer()->AddKey(params); + trans.GetCryptographer()->set_encrypt_everything(); + } + { + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + + WriteNode bookmark_node(&trans); + EXPECT_TRUE(bookmark_node.InitUniqueByCreation(syncable::BOOKMARKS, + root_node, "foo")); + bookmark_node.SetTitle(UTF8ToWide("foo")); + + WriteNode pref_node(&trans); + EXPECT_TRUE(pref_node.InitUniqueByCreation(syncable::PREFERENCES, + root_node, "bar")); + pref_node.SetTitle(UTF8ToWide("bar")); + } + { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + + ReadNode bookmark_node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + bookmark_node.InitByClientTagLookup(syncable::BOOKMARKS, + "foo")); + EXPECT_EQ("foo", bookmark_node.GetTitle()); + EXPECT_EQ(kEncryptedString, + bookmark_node.GetEntry()->Get(syncable::NON_UNIQUE_NAME)); + + ReadNode pref_node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + pref_node.InitByClientTagLookup(syncable::PREFERENCES, + "bar")); + EXPECT_EQ(kEncryptedString, pref_node.GetTitle()); + } +} + +TEST_F(SyncApiTest, BaseNodeSetSpecifics) { + int64 child_id = MakeNode(test_user_share_.user_share(), + syncable::BOOKMARKS, "testtag"); + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByIdLookup(child_id)); + + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_bookmark()->set_url("http://www.google.com"); + + EXPECT_NE(entity_specifics.SerializeAsString(), + node.GetEntitySpecifics().SerializeAsString()); + node.SetEntitySpecifics(entity_specifics); + EXPECT_EQ(entity_specifics.SerializeAsString(), + node.GetEntitySpecifics().SerializeAsString()); +} + +TEST_F(SyncApiTest, BaseNodeSetSpecificsPreservesUnknownFields) { + int64 child_id = MakeNode(test_user_share_.user_share(), + syncable::BOOKMARKS, "testtag"); + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByIdLookup(child_id)); + EXPECT_TRUE(node.GetEntitySpecifics().unknown_fields().empty()); + + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_bookmark()->set_url("http://www.google.com"); + entity_specifics.mutable_unknown_fields()->AddFixed32(5, 100); + node.SetEntitySpecifics(entity_specifics); + EXPECT_FALSE(node.GetEntitySpecifics().unknown_fields().empty()); + + entity_specifics.mutable_unknown_fields()->Clear(); + node.SetEntitySpecifics(entity_specifics); + EXPECT_FALSE(node.GetEntitySpecifics().unknown_fields().empty()); +} + +namespace { + +void CheckNodeValue(const BaseNode& node, const DictionaryValue& value, + bool is_detailed) { + ExpectInt64Value(node.GetId(), value, "id"); + { + bool is_folder = false; + EXPECT_TRUE(value.GetBoolean("isFolder", &is_folder)); + EXPECT_EQ(node.GetIsFolder(), is_folder); + } + ExpectDictStringValue(node.GetTitle(), value, "title"); + { + ModelType expected_model_type = node.GetModelType(); + std::string type_str; + EXPECT_TRUE(value.GetString("type", &type_str)); + if (expected_model_type >= syncable::FIRST_REAL_MODEL_TYPE) { + ModelType model_type = + syncable::ModelTypeFromString(type_str); + EXPECT_EQ(expected_model_type, model_type); + } else if (expected_model_type == syncable::TOP_LEVEL_FOLDER) { + EXPECT_EQ("Top-level folder", type_str); + } else if (expected_model_type == syncable::UNSPECIFIED) { + EXPECT_EQ("Unspecified", type_str); + } else { + ADD_FAILURE(); + } + } + if (is_detailed) { + ExpectInt64Value(node.GetParentId(), value, "parentId"); + ExpectTimeValue(node.GetModificationTime(), value, "modificationTime"); + ExpectInt64Value(node.GetExternalId(), value, "externalId"); + ExpectInt64Value(node.GetPredecessorId(), value, "predecessorId"); + ExpectInt64Value(node.GetSuccessorId(), value, "successorId"); + ExpectInt64Value(node.GetFirstChildId(), value, "firstChildId"); + { + scoped_ptr<DictionaryValue> expected_entry(node.GetEntry()->ToValue()); + Value* entry = NULL; + EXPECT_TRUE(value.Get("entry", &entry)); + EXPECT_TRUE(Value::Equals(entry, expected_entry.get())); + } + EXPECT_EQ(11u, value.size()); + } else { + EXPECT_EQ(4u, value.size()); + } +} + +} // namespace + +TEST_F(SyncApiTest, BaseNodeGetSummaryAsValue) { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode node(&trans); + node.InitByRootLookup(); + scoped_ptr<DictionaryValue> details(node.GetSummaryAsValue()); + if (details.get()) { + CheckNodeValue(node, *details, false); + } else { + ADD_FAILURE(); + } +} + +TEST_F(SyncApiTest, BaseNodeGetDetailsAsValue) { + ReadTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode node(&trans); + node.InitByRootLookup(); + scoped_ptr<DictionaryValue> details(node.GetDetailsAsValue()); + if (details.get()) { + CheckNodeValue(node, *details, true); + } else { + ADD_FAILURE(); + } +} + +TEST_F(SyncApiTest, EmptyTags) { + WriteTransaction trans(FROM_HERE, test_user_share_.user_share()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + WriteNode node(&trans); + std::string empty_tag; + EXPECT_FALSE(node.InitUniqueByCreation( + syncable::TYPED_URLS, root_node, empty_tag)); + EXPECT_EQ(BaseNode::INIT_FAILED_PRECONDITION, + node.InitByTagLookup(empty_tag)); +} + +namespace { + +class TestHttpPostProviderInterface : public HttpPostProviderInterface { + public: + virtual ~TestHttpPostProviderInterface() {} + + virtual void SetUserAgent(const char* user_agent) OVERRIDE {} + virtual void SetExtraRequestHeaders(const char* headers) OVERRIDE {} + virtual void SetURL(const char* url, int port) OVERRIDE {} + virtual void SetPostPayload(const char* content_type, + int content_length, + const char* content) OVERRIDE {} + virtual bool MakeSynchronousPost(int* error_code, int* response_code) + OVERRIDE { + return false; + } + virtual int GetResponseContentLength() const OVERRIDE { + return 0; + } + virtual const char* GetResponseContent() const OVERRIDE { + return ""; + } + virtual const std::string GetResponseHeaderValue( + const std::string& name) const OVERRIDE { + return ""; + } + virtual void Abort() OVERRIDE {} +}; + +class TestHttpPostProviderFactory : public HttpPostProviderFactory { + public: + virtual ~TestHttpPostProviderFactory() {} + virtual HttpPostProviderInterface* Create() OVERRIDE { + return new TestHttpPostProviderInterface(); + } + virtual void Destroy(HttpPostProviderInterface* http) OVERRIDE { + delete http; + } +}; + +class SyncManagerObserverMock : public SyncManager::Observer { + public: + MOCK_METHOD1(OnSyncCycleCompleted, + void(const SyncSessionSnapshot*)); // NOLINT + MOCK_METHOD2(OnInitializationComplete, + void(const WeakHandle<JsBackend>&, bool)); // NOLINT + MOCK_METHOD1(OnConnectionStatusChange, void(ConnectionStatus)); // NOLINT + MOCK_METHOD2(OnPassphraseRequired, + void(sync_api::PassphraseRequiredReason, + const sync_pb::EncryptedData&)); // NOLINT + MOCK_METHOD0(OnPassphraseAccepted, void()); // NOLINT + MOCK_METHOD1(OnBootstrapTokenUpdated, void(const std::string&)); // NOLINT + MOCK_METHOD0(OnStopSyncingPermanently, void()); // NOLINT + MOCK_METHOD1(OnUpdatedToken, void(const std::string&)); // NOLINT + MOCK_METHOD0(OnClearServerDataFailed, void()); // NOLINT + MOCK_METHOD0(OnClearServerDataSucceeded, void()); // NOLINT + MOCK_METHOD2(OnEncryptedTypesChanged, + void(ModelTypeSet, bool)); // NOLINT + MOCK_METHOD0(OnEncryptionComplete, void()); // NOLINT + MOCK_METHOD1(OnActionableError, + void(const browser_sync::SyncProtocolError&)); // NOLINT +}; + +class SyncNotifierMock : public sync_notifier::SyncNotifier { + public: + MOCK_METHOD1(AddObserver, void(sync_notifier::SyncNotifierObserver*)); + MOCK_METHOD1(RemoveObserver, void(sync_notifier::SyncNotifierObserver*)); + MOCK_METHOD1(SetUniqueId, void(const std::string&)); + MOCK_METHOD1(SetState, void(const std::string&)); + MOCK_METHOD2(UpdateCredentials, + void(const std::string&, const std::string&)); + MOCK_METHOD1(UpdateEnabledTypes, + void(syncable::ModelTypeSet)); + MOCK_METHOD1(SendNotification, void(syncable::ModelTypeSet)); +}; + +} // namespace + +class SyncManagerTest : public testing::Test, + public ModelSafeWorkerRegistrar, + public SyncManager::ChangeDelegate { + protected: + enum NigoriStatus { + DONT_WRITE_NIGORI, + WRITE_TO_NIGORI + }; + + enum EncryptionStatus { + UNINITIALIZED, + DEFAULT_ENCRYPTION, + FULL_ENCRYPTION + }; + + SyncManagerTest() + : sync_notifier_mock_(NULL), + sync_manager_("Test sync manager"), + sync_notifier_observer_(NULL), + update_enabled_types_call_count_(0) {} + + virtual ~SyncManagerTest() { + EXPECT_FALSE(sync_notifier_mock_); + } + + // Test implementation. + void SetUp() { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + + SyncCredentials credentials; + credentials.email = "foo@bar.com"; + credentials.sync_token = "sometoken"; + + sync_notifier_mock_ = new StrictMock<SyncNotifierMock>(); + EXPECT_CALL(*sync_notifier_mock_, AddObserver(_)). + WillOnce(Invoke(this, &SyncManagerTest::SyncNotifierAddObserver)); + EXPECT_CALL(*sync_notifier_mock_, SetUniqueId(_)); + EXPECT_CALL(*sync_notifier_mock_, SetState("")); + EXPECT_CALL(*sync_notifier_mock_, + UpdateCredentials(credentials.email, credentials.sync_token)); + EXPECT_CALL(*sync_notifier_mock_, UpdateEnabledTypes(_)). + Times(AtLeast(1)). + WillRepeatedly( + Invoke(this, &SyncManagerTest::SyncNotifierUpdateEnabledTypes)); + EXPECT_CALL(*sync_notifier_mock_, RemoveObserver(_)). + WillOnce(Invoke(this, &SyncManagerTest::SyncNotifierRemoveObserver)); + + sync_manager_.AddObserver(&observer_); + EXPECT_CALL(observer_, OnInitializationComplete(_, _)). + WillOnce(SaveArg<0>(&js_backend_)); + + EXPECT_FALSE(sync_notifier_observer_); + EXPECT_FALSE(js_backend_.IsInitialized()); + + // Takes ownership of |sync_notifier_mock_|. + sync_manager_.Init(temp_dir_.path(), + WeakHandle<JsEventHandler>(), + "bogus", 0, false, + base::MessageLoopProxy::current(), + new TestHttpPostProviderFactory(), this, + &extensions_activity_monitor_, this, "bogus", + credentials, + false /* enable_sync_tabs_for_other_clients */, + sync_notifier_mock_, "", + sync_api::SyncManager::TEST_IN_MEMORY, + &encryptor_, + &handler_, + NULL); + + EXPECT_TRUE(sync_notifier_observer_); + EXPECT_TRUE(js_backend_.IsInitialized()); + + EXPECT_EQ(1, update_enabled_types_call_count_); + + ModelSafeRoutingInfo routes; + GetModelSafeRoutingInfo(&routes); + for (ModelSafeRoutingInfo::iterator i = routes.begin(); i != routes.end(); + ++i) { + type_roots_[i->first] = MakeServerNodeForType( + sync_manager_.GetUserShare(), i->first); + } + PumpLoop(); + } + + void TearDown() { + sync_manager_.RemoveObserver(&observer_); + sync_manager_.ShutdownOnSyncThread(); + sync_notifier_mock_ = NULL; + EXPECT_FALSE(sync_notifier_observer_); + PumpLoop(); + } + + // ModelSafeWorkerRegistrar implementation. + virtual void GetWorkers(std::vector<ModelSafeWorker*>* out) OVERRIDE { + NOTIMPLEMENTED(); + out->clear(); + } + virtual void GetModelSafeRoutingInfo(ModelSafeRoutingInfo* out) OVERRIDE { + (*out)[syncable::NIGORI] = browser_sync::GROUP_PASSIVE; + (*out)[syncable::BOOKMARKS] = browser_sync::GROUP_PASSIVE; + (*out)[syncable::THEMES] = browser_sync::GROUP_PASSIVE; + (*out)[syncable::SESSIONS] = browser_sync::GROUP_PASSIVE; + (*out)[syncable::PASSWORDS] = browser_sync::GROUP_PASSIVE; + (*out)[syncable::PREFERENCES] = browser_sync::GROUP_PASSIVE; + } + + virtual void OnChangesApplied( + syncable::ModelType model_type, + const BaseTransaction* trans, + const ImmutableChangeRecordList& changes) OVERRIDE {} + + virtual void OnChangesComplete(syncable::ModelType model_type) OVERRIDE {} + + // Helper methods. + bool SetUpEncryption(NigoriStatus nigori_status, + EncryptionStatus encryption_status) { + UserShare* share = sync_manager_.GetUserShare(); + share->directory->set_initial_sync_ended_for_type(syncable::NIGORI, true); + + // We need to create the nigori node as if it were an applied server update. + int64 nigori_id = GetIdForDataType(syncable::NIGORI); + if (nigori_id == kInvalidId) + return false; + + // Set the nigori cryptographer information. + WriteTransaction trans(FROM_HERE, share); + Cryptographer* cryptographer = trans.GetCryptographer(); + if (!cryptographer) + return false; + if (encryption_status != UNINITIALIZED) { + KeyParams params = {"localhost", "dummy", "foobar"}; + cryptographer->AddKey(params); + } else { + DCHECK_NE(nigori_status, WRITE_TO_NIGORI); + } + if (encryption_status == FULL_ENCRYPTION) + cryptographer->set_encrypt_everything(); + if (nigori_status == WRITE_TO_NIGORI) { + sync_pb::NigoriSpecifics nigori; + cryptographer->GetKeys(nigori.mutable_encrypted()); + cryptographer->UpdateNigoriFromEncryptedTypes(&nigori); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByIdLookup(nigori_id)); + node.SetNigoriSpecifics(nigori); + } + return cryptographer->is_ready(); + } + + int64 GetIdForDataType(ModelType type) { + if (type_roots_.count(type) == 0) + return 0; + return type_roots_[type]; + } + + void SyncNotifierAddObserver( + sync_notifier::SyncNotifierObserver* sync_notifier_observer) { + EXPECT_EQ(NULL, sync_notifier_observer_); + sync_notifier_observer_ = sync_notifier_observer; + } + + void SyncNotifierRemoveObserver( + sync_notifier::SyncNotifierObserver* sync_notifier_observer) { + EXPECT_EQ(sync_notifier_observer_, sync_notifier_observer); + sync_notifier_observer_ = NULL; + } + + void SyncNotifierUpdateEnabledTypes(syncable::ModelTypeSet types) { + ModelSafeRoutingInfo routes; + GetModelSafeRoutingInfo(&routes); + const syncable::ModelTypeSet expected_types = + GetRoutingInfoTypes(routes); + EXPECT_TRUE(types.Equals(expected_types)); + ++update_enabled_types_call_count_; + } + + void PumpLoop() { + message_loop_.RunAllPending(); + } + + void SendJsMessage(const std::string& name, const JsArgList& args, + const WeakHandle<JsReplyHandler>& reply_handler) { + js_backend_.Call(FROM_HERE, &JsBackend::ProcessJsMessage, + name, args, reply_handler); + PumpLoop(); + } + + void SetJsEventHandler(const WeakHandle<JsEventHandler>& event_handler) { + js_backend_.Call(FROM_HERE, &JsBackend::SetJsEventHandler, + event_handler); + PumpLoop(); + } + + // Looks up an entry by client tag and resets IS_UNSYNCED value to false. + // Returns true if entry was previously unsynced, false if IS_UNSYNCED was + // already false. + bool ResetUnsyncedEntry(syncable::ModelType type, + const std::string& client_tag) { + UserShare* share = sync_manager_.GetUserShare(); + syncable::WriteTransaction trans( + FROM_HERE, syncable::UNITTEST, share->directory.get()); + const std::string hash = BaseNode::GenerateSyncableHash(type, client_tag); + syncable::MutableEntry entry(&trans, syncable::GET_BY_CLIENT_TAG, + hash); + EXPECT_TRUE(entry.good()); + if (!entry.Get(IS_UNSYNCED)) + return false; + entry.Put(IS_UNSYNCED, false); + return true; + } + + private: + // Needed by |sync_manager_|. + MessageLoop message_loop_; + // Needed by |sync_manager_|. + ScopedTempDir temp_dir_; + // Sync Id's for the roots of the enabled datatypes. + std::map<ModelType, int64> type_roots_; + FakeExtensionsActivityMonitor extensions_activity_monitor_; + StrictMock<SyncNotifierMock>* sync_notifier_mock_; + + protected: + FakeEncryptor encryptor_; + TestUnrecoverableErrorHandler handler_; + SyncManager sync_manager_; + WeakHandle<JsBackend> js_backend_; + StrictMock<SyncManagerObserverMock> observer_; + sync_notifier::SyncNotifierObserver* sync_notifier_observer_; + int update_enabled_types_call_count_; +}; + +TEST_F(SyncManagerTest, UpdateEnabledTypes) { + EXPECT_EQ(1, update_enabled_types_call_count_); + // Triggers SyncNotifierUpdateEnabledTypes. + sync_manager_.UpdateEnabledTypes(); + EXPECT_EQ(2, update_enabled_types_call_count_); +} + +TEST_F(SyncManagerTest, DoNotSyncTabsInNigoriNode) { + const syncable::ModelTypeSet encrypted_types(syncable::TYPED_URLS); + sync_manager_.MaybeSetSyncTabsInNigoriNode(encrypted_types); + + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + ASSERT_EQ(BaseNode::INIT_OK, + node.InitByIdLookup(GetIdForDataType(syncable::NIGORI))); + EXPECT_FALSE(node.GetNigoriSpecifics().sync_tabs()); +} + +TEST_F(SyncManagerTest, SyncTabsInNigoriNode) { + const syncable::ModelTypeSet encrypted_types(syncable::SESSIONS); + sync_manager_.MaybeSetSyncTabsInNigoriNode(encrypted_types); + + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + ASSERT_EQ(BaseNode::INIT_OK, + node.InitByIdLookup(GetIdForDataType(syncable::NIGORI))); + EXPECT_TRUE(node.GetNigoriSpecifics().sync_tabs()); +} + +TEST_F(SyncManagerTest, ProcessJsMessage) { + const JsArgList kNoArgs; + + StrictMock<MockJsReplyHandler> reply_handler; + + ListValue false_args; + false_args.Append(Value::CreateBooleanValue(false)); + + EXPECT_CALL(reply_handler, + HandleJsReply("getNotificationState", + HasArgsAsList(false_args))); + + // This message should be dropped. + SendJsMessage("unknownMessage", kNoArgs, reply_handler.AsWeakHandle()); + + SendJsMessage("getNotificationState", kNoArgs, reply_handler.AsWeakHandle()); +} + +TEST_F(SyncManagerTest, ProcessJsMessageGetRootNodeDetails) { + const JsArgList kNoArgs; + + StrictMock<MockJsReplyHandler> reply_handler; + + JsArgList return_args; + + EXPECT_CALL(reply_handler, + HandleJsReply("getRootNodeDetails", _)) + .WillOnce(SaveArg<1>(&return_args)); + + SendJsMessage("getRootNodeDetails", kNoArgs, reply_handler.AsWeakHandle()); + + EXPECT_EQ(1u, return_args.Get().GetSize()); + DictionaryValue* node_info = NULL; + EXPECT_TRUE(return_args.Get().GetDictionary(0, &node_info)); + if (node_info) { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + node.InitByRootLookup(); + CheckNodeValue(node, *node_info, true); + } else { + ADD_FAILURE(); + } +} + +void CheckGetNodesByIdReturnArgs(const SyncManager& sync_manager, + const JsArgList& return_args, + int64 id, + bool is_detailed) { + EXPECT_EQ(1u, return_args.Get().GetSize()); + ListValue* nodes = NULL; + ASSERT_TRUE(return_args.Get().GetList(0, &nodes)); + ASSERT_TRUE(nodes); + EXPECT_EQ(1u, nodes->GetSize()); + DictionaryValue* node_info = NULL; + EXPECT_TRUE(nodes->GetDictionary(0, &node_info)); + ASSERT_TRUE(node_info); + ReadTransaction trans(FROM_HERE, sync_manager.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByIdLookup(id)); + CheckNodeValue(node, *node_info, is_detailed); +} + +class SyncManagerGetNodesByIdTest : public SyncManagerTest { + protected: + virtual ~SyncManagerGetNodesByIdTest() {} + + void RunGetNodesByIdTest(const char* message_name, bool is_detailed) { + int64 root_id = kInvalidId; + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + root_id = root_node.GetId(); + } + + int64 child_id = + MakeNode(sync_manager_.GetUserShare(), + syncable::BOOKMARKS, "testtag"); + + StrictMock<MockJsReplyHandler> reply_handler; + + JsArgList return_args; + + const int64 ids[] = { root_id, child_id }; + + EXPECT_CALL(reply_handler, + HandleJsReply(message_name, _)) + .Times(arraysize(ids)).WillRepeatedly(SaveArg<1>(&return_args)); + + for (size_t i = 0; i < arraysize(ids); ++i) { + ListValue args; + ListValue* id_values = new ListValue(); + args.Append(id_values); + id_values->Append(Value::CreateStringValue(base::Int64ToString(ids[i]))); + SendJsMessage(message_name, + JsArgList(&args), reply_handler.AsWeakHandle()); + + CheckGetNodesByIdReturnArgs(sync_manager_, return_args, + ids[i], is_detailed); + } + } + + void RunGetNodesByIdFailureTest(const char* message_name) { + StrictMock<MockJsReplyHandler> reply_handler; + + ListValue empty_list_args; + empty_list_args.Append(new ListValue()); + + EXPECT_CALL(reply_handler, + HandleJsReply(message_name, + HasArgsAsList(empty_list_args))) + .Times(6); + + { + ListValue args; + SendJsMessage(message_name, + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + { + ListValue args; + args.Append(new ListValue()); + SendJsMessage(message_name, + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + { + ListValue args; + ListValue* ids = new ListValue(); + args.Append(ids); + ids->Append(Value::CreateStringValue("")); + SendJsMessage(message_name, + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + { + ListValue args; + ListValue* ids = new ListValue(); + args.Append(ids); + ids->Append(Value::CreateStringValue("nonsense")); + SendJsMessage(message_name, + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + { + ListValue args; + ListValue* ids = new ListValue(); + args.Append(ids); + ids->Append(Value::CreateStringValue("0")); + SendJsMessage(message_name, + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + { + ListValue args; + ListValue* ids = new ListValue(); + args.Append(ids); + ids->Append(Value::CreateStringValue("9999")); + SendJsMessage(message_name, + JsArgList(&args), reply_handler.AsWeakHandle()); + } + } +}; + +TEST_F(SyncManagerGetNodesByIdTest, GetNodeSummariesById) { + RunGetNodesByIdTest("getNodeSummariesById", false); +} + +TEST_F(SyncManagerGetNodesByIdTest, GetNodeDetailsById) { + RunGetNodesByIdTest("getNodeDetailsById", true); +} + +TEST_F(SyncManagerGetNodesByIdTest, GetNodeSummariesByIdFailure) { + RunGetNodesByIdFailureTest("getNodeSummariesById"); +} + +TEST_F(SyncManagerGetNodesByIdTest, GetNodeDetailsByIdFailure) { + RunGetNodesByIdFailureTest("getNodeDetailsById"); +} + +TEST_F(SyncManagerTest, GetChildNodeIds) { + StrictMock<MockJsReplyHandler> reply_handler; + + JsArgList return_args; + + EXPECT_CALL(reply_handler, + HandleJsReply("getChildNodeIds", _)) + .Times(1).WillRepeatedly(SaveArg<1>(&return_args)); + + { + ListValue args; + args.Append(Value::CreateStringValue("1")); + SendJsMessage("getChildNodeIds", + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + EXPECT_EQ(1u, return_args.Get().GetSize()); + ListValue* nodes = NULL; + ASSERT_TRUE(return_args.Get().GetList(0, &nodes)); + ASSERT_TRUE(nodes); + EXPECT_EQ(6u, nodes->GetSize()); +} + +TEST_F(SyncManagerTest, GetChildNodeIdsFailure) { + StrictMock<MockJsReplyHandler> reply_handler; + + ListValue empty_list_args; + empty_list_args.Append(new ListValue()); + + EXPECT_CALL(reply_handler, + HandleJsReply("getChildNodeIds", + HasArgsAsList(empty_list_args))) + .Times(5); + + { + ListValue args; + SendJsMessage("getChildNodeIds", + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + { + ListValue args; + args.Append(Value::CreateStringValue("")); + SendJsMessage("getChildNodeIds", + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + { + ListValue args; + args.Append(Value::CreateStringValue("nonsense")); + SendJsMessage("getChildNodeIds", + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + { + ListValue args; + args.Append(Value::CreateStringValue("0")); + SendJsMessage("getChildNodeIds", + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + { + ListValue args; + args.Append(Value::CreateStringValue("9999")); + SendJsMessage("getChildNodeIds", + JsArgList(&args), reply_handler.AsWeakHandle()); + } +} + +TEST_F(SyncManagerTest, GetAllNodesTest) { + StrictMock<MockJsReplyHandler> reply_handler; + JsArgList return_args; + + EXPECT_CALL(reply_handler, + HandleJsReply("getAllNodes", _)) + .Times(1).WillRepeatedly(SaveArg<1>(&return_args)); + + { + ListValue args; + SendJsMessage("getAllNodes", + JsArgList(&args), reply_handler.AsWeakHandle()); + } + + // There's not much value in verifying every attribute on every node here. + // Most of the value of this test has already been achieved: we've verified we + // can call the above function without crashing or leaking memory. + // + // Let's just check the list size and a few of its elements. Anything more + // would make this test brittle without greatly increasing our chances of + // catching real bugs. + + ListValue* node_list; + DictionaryValue* first_result; + + // The resulting argument list should have one argument, a list of nodes. + ASSERT_EQ(1U, return_args.Get().GetSize()); + ASSERT_TRUE(return_args.Get().GetList(0, &node_list)); + + // The database creation logic depends on the routing info. + // Refer to setup methods for more information. + ModelSafeRoutingInfo routes; + GetModelSafeRoutingInfo(&routes); + size_t directory_size = routes.size() + 1; + + ASSERT_EQ(directory_size, node_list->GetSize()); + ASSERT_TRUE(node_list->GetDictionary(0, &first_result)); + EXPECT_TRUE(first_result->HasKey("ID")); + EXPECT_TRUE(first_result->HasKey("NON_UNIQUE_NAME")); +} + +TEST_F(SyncManagerTest, OnNotificationStateChange) { + InSequence dummy; + StrictMock<MockJsEventHandler> event_handler; + + DictionaryValue true_details; + true_details.SetBoolean("enabled", true); + DictionaryValue false_details; + false_details.SetBoolean("enabled", false); + + EXPECT_CALL(event_handler, + HandleJsEvent("onNotificationStateChange", + HasDetailsAsDictionary(true_details))); + EXPECT_CALL(event_handler, + HandleJsEvent("onNotificationStateChange", + HasDetailsAsDictionary(false_details))); + + sync_manager_.TriggerOnNotificationStateChangeForTest(true); + sync_manager_.TriggerOnNotificationStateChangeForTest(false); + + SetJsEventHandler(event_handler.AsWeakHandle()); + sync_manager_.TriggerOnNotificationStateChangeForTest(true); + sync_manager_.TriggerOnNotificationStateChangeForTest(false); + SetJsEventHandler(WeakHandle<JsEventHandler>()); + + sync_manager_.TriggerOnNotificationStateChangeForTest(true); + sync_manager_.TriggerOnNotificationStateChangeForTest(false); + + // Should trigger the replies. + PumpLoop(); +} + +TEST_F(SyncManagerTest, OnIncomingNotification) { + StrictMock<MockJsEventHandler> event_handler; + + const syncable::ModelTypeSet empty_model_types; + const syncable::ModelTypeSet model_types( + syncable::BOOKMARKS, syncable::THEMES); + + // Build expected_args to have a single argument with the string + // equivalents of model_types. + DictionaryValue expected_details; + { + ListValue* model_type_list = new ListValue(); + expected_details.SetString("source", "REMOTE_NOTIFICATION"); + expected_details.Set("changedTypes", model_type_list); + for (syncable::ModelTypeSet::Iterator it = model_types.First(); + it.Good(); it.Inc()) { + model_type_list->Append( + Value::CreateStringValue( + syncable::ModelTypeToString(it.Get()))); + } + } + + EXPECT_CALL(event_handler, + HandleJsEvent("onIncomingNotification", + HasDetailsAsDictionary(expected_details))); + + sync_manager_.TriggerOnIncomingNotificationForTest(empty_model_types); + sync_manager_.TriggerOnIncomingNotificationForTest(model_types); + + SetJsEventHandler(event_handler.AsWeakHandle()); + sync_manager_.TriggerOnIncomingNotificationForTest(model_types); + SetJsEventHandler(WeakHandle<JsEventHandler>()); + + sync_manager_.TriggerOnIncomingNotificationForTest(empty_model_types); + sync_manager_.TriggerOnIncomingNotificationForTest(model_types); + + // Should trigger the replies. + PumpLoop(); +} + +TEST_F(SyncManagerTest, RefreshEncryptionReady) { + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + EXPECT_CALL(observer_, OnEncryptionComplete()); + + sync_manager_.RefreshNigori(kTestChromeVersion, base::Bind(&DoNothing)); + PumpLoop(); + + const syncable::ModelTypeSet encrypted_types = + sync_manager_.GetEncryptedDataTypesForTest(); + EXPECT_TRUE(encrypted_types.Has(syncable::PASSWORDS)); + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByIdLookup(GetIdForDataType(syncable::NIGORI))); + sync_pb::NigoriSpecifics nigori = node.GetNigoriSpecifics(); + EXPECT_TRUE(nigori.has_encrypted()); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + EXPECT_TRUE(cryptographer->CanDecrypt(nigori.encrypted())); + } +} + +// Attempt to refresh encryption when nigori not downloaded. +TEST_F(SyncManagerTest, RefreshEncryptionNotReady) { + // Don't set up encryption (no nigori node created). + + // Should fail. + sync_manager_.RefreshNigori(kTestChromeVersion, base::Bind(&DoNothing)); + PumpLoop(); + + const syncable::ModelTypeSet encrypted_types = + sync_manager_.GetEncryptedDataTypesForTest(); + EXPECT_TRUE(encrypted_types.Has(syncable::PASSWORDS)); // Hardcoded. + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); +} + +// Attempt to refresh encryption when nigori is empty. +TEST_F(SyncManagerTest, RefreshEncryptionEmptyNigori) { + EXPECT_TRUE(SetUpEncryption(DONT_WRITE_NIGORI, DEFAULT_ENCRYPTION)); + EXPECT_CALL(observer_, OnEncryptionComplete()); + + // Should write to nigori. + sync_manager_.RefreshNigori(kTestChromeVersion, base::Bind(&DoNothing)); + PumpLoop(); + + const syncable::ModelTypeSet encrypted_types = + sync_manager_.GetEncryptedDataTypesForTest(); + EXPECT_TRUE(encrypted_types.Has(syncable::PASSWORDS)); // Hardcoded. + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByIdLookup(GetIdForDataType(syncable::NIGORI))); + sync_pb::NigoriSpecifics nigori = node.GetNigoriSpecifics(); + EXPECT_TRUE(nigori.has_encrypted()); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + EXPECT_TRUE(cryptographer->CanDecrypt(nigori.encrypted())); + } +} + +TEST_F(SyncManagerTest, EncryptDataTypesWithNoData) { + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + EXPECT_CALL(observer_, + OnEncryptedTypesChanged( + HasModelTypes(syncable::ModelTypeSet::All()), true)); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.EnableEncryptEverything(); + EXPECT_TRUE(sync_manager_.EncryptEverythingEnabledForTest()); +} + +TEST_F(SyncManagerTest, EncryptDataTypesWithData) { + size_t batch_size = 5; + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + + // Create some unencrypted unsynced data. + int64 folder = MakeFolderWithParent(sync_manager_.GetUserShare(), + syncable::BOOKMARKS, + GetIdForDataType(syncable::BOOKMARKS), + NULL); + // First batch_size nodes are children of folder. + size_t i; + for (i = 0; i < batch_size; ++i) { + MakeNodeWithParent(sync_manager_.GetUserShare(), syncable::BOOKMARKS, + base::StringPrintf("%"PRIuS"", i), folder); + } + // Next batch_size nodes are a different type and on their own. + for (; i < 2*batch_size; ++i) { + MakeNodeWithParent(sync_manager_.GetUserShare(), syncable::SESSIONS, + base::StringPrintf("%"PRIuS"", i), + GetIdForDataType(syncable::SESSIONS)); + } + // Last batch_size nodes are a third type that will not need encryption. + for (; i < 3*batch_size; ++i) { + MakeNodeWithParent(sync_manager_.GetUserShare(), syncable::THEMES, + base::StringPrintf("%"PRIuS"", i), + GetIdForDataType(syncable::THEMES)); + } + + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + EXPECT_TRUE(GetEncryptedTypes(&trans).Equals( + Cryptographer::SensitiveTypes())); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::BOOKMARKS, + false /* not encrypted */)); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::SESSIONS, + false /* not encrypted */)); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::THEMES, + false /* not encrypted */)); + } + + EXPECT_CALL(observer_, + OnEncryptedTypesChanged( + HasModelTypes(syncable::ModelTypeSet::All()), true)); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.EnableEncryptEverything(); + EXPECT_TRUE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + EXPECT_TRUE(GetEncryptedTypes(&trans).Equals( + syncable::ModelTypeSet::All())); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::BOOKMARKS, + true /* is encrypted */)); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::SESSIONS, + true /* is encrypted */)); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::THEMES, + true /* is encrypted */)); + } + + // Trigger's a ReEncryptEverything with new passphrase. + testing::Mock::VerifyAndClearExpectations(&observer_); + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetEncryptionPassphrase("new_passphrase", true); + EXPECT_TRUE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + EXPECT_TRUE(GetEncryptedTypes(&trans).Equals( + syncable::ModelTypeSet::All())); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::BOOKMARKS, + true /* is encrypted */)); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::SESSIONS, + true /* is encrypted */)); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::THEMES, + true /* is encrypted */)); + } + // Calling EncryptDataTypes with an empty encrypted types should not trigger + // a reencryption and should just notify immediately. + // TODO(zea): add logic to ensure nothing was written. + testing::Mock::VerifyAndClearExpectations(&observer_); + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)).Times(0); + EXPECT_CALL(observer_, OnPassphraseAccepted()).Times(0); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.EnableEncryptEverything(); +} + +// Test that when there are no pending keys and the cryptographer is not +// initialized, we add a key based on the current GAIA password. +// (case 1 in SyncManager::SyncInternal::SetEncryptionPassphrase) +TEST_F(SyncManagerTest, SetInitialGaiaPass) { + EXPECT_FALSE(SetUpEncryption(DONT_WRITE_NIGORI, UNINITIALIZED)); + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetEncryptionPassphrase("new_passphrase", false); + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByTagLookup(kNigoriTag)); + sync_pb::NigoriSpecifics nigori = node.GetNigoriSpecifics(); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + EXPECT_TRUE(cryptographer->CanDecrypt(nigori.encrypted())); + } +} + +// Test that when there are no pending keys and we have on the old GAIA +// password, we update and re-encrypt everything with the new GAIA password. +// (case 1 in SyncManager::SyncInternal::SetEncryptionPassphrase) +TEST_F(SyncManagerTest, UpdateGaiaPass) { + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + Cryptographer verifier(&encryptor_); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + std::string bootstrap_token; + cryptographer->GetBootstrapToken(&bootstrap_token); + verifier.Bootstrap(bootstrap_token); + } + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetEncryptionPassphrase("new_passphrase", false); + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + // Verify the default key has changed. + sync_pb::EncryptedData encrypted; + cryptographer->GetKeys(&encrypted); + EXPECT_FALSE(verifier.CanDecrypt(encrypted)); + } +} + +// Sets a new explicit passphrase. This should update the bootstrap token +// and re-encrypt everything. +// (case 2 in SyncManager::SyncInternal::SetEncryptionPassphrase) +TEST_F(SyncManagerTest, SetPassphraseWithPassword) { + Cryptographer verifier(&encryptor_); + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + // Store the default (soon to be old) key. + Cryptographer* cryptographer = trans.GetCryptographer(); + std::string bootstrap_token; + cryptographer->GetBootstrapToken(&bootstrap_token); + verifier.Bootstrap(bootstrap_token); + + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + + WriteNode password_node(&trans); + EXPECT_TRUE(password_node.InitUniqueByCreation(syncable::PASSWORDS, + root_node, "foo")); + sync_pb::PasswordSpecificsData data; + data.set_password_value("secret"); + password_node.SetPasswordSpecifics(data); + } + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetEncryptionPassphrase("new_passphrase", true); + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + // Verify the default key has changed. + sync_pb::EncryptedData encrypted; + cryptographer->GetKeys(&encrypted); + EXPECT_FALSE(verifier.CanDecrypt(encrypted)); + + ReadNode password_node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + password_node.InitByClientTagLookup(syncable::PASSWORDS, + "foo")); + const sync_pb::PasswordSpecificsData& data = + password_node.GetPasswordSpecifics(); + EXPECT_EQ("secret", data.password_value()); + } +} + +// Manually set the pending keys in the cryptographer/nigori to reflect the data +// being encrypted with a new (unprovided) GAIA password, then supply the +// password. +// (case 7 in SyncManager::SyncInternal::SetDecryptionPassphrase) +TEST_F(SyncManagerTest, SupplyPendingGAIAPass) { + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + Cryptographer other_cryptographer(&encryptor_); + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + std::string bootstrap_token; + cryptographer->GetBootstrapToken(&bootstrap_token); + other_cryptographer.Bootstrap(bootstrap_token); + + // Now update the nigori to reflect the new keys, and update the + // cryptographer to have pending keys. + KeyParams params = {"localhost", "dummy", "passphrase2"}; + other_cryptographer.AddKey(params); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByTagLookup(kNigoriTag)); + sync_pb::NigoriSpecifics nigori; + other_cryptographer.GetKeys(nigori.mutable_encrypted()); + cryptographer->Update(nigori); + EXPECT_TRUE(cryptographer->has_pending_keys()); + node.SetNigoriSpecifics(nigori); + } + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetDecryptionPassphrase("passphrase2"); + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + // Verify we're encrypting with the new key. + sync_pb::EncryptedData encrypted; + cryptographer->GetKeys(&encrypted); + EXPECT_TRUE(other_cryptographer.CanDecrypt(encrypted)); + } +} + +// Manually set the pending keys in the cryptographer/nigori to reflect the data +// being encrypted with an old (unprovided) GAIA password. Attempt to supply +// the current GAIA password and verify the bootstrap token is updated. Then +// supply the old GAIA password, and verify we re-encrypt all data with the +// new GAIA password. +// (cases 4 and 5 in SyncManager::SyncInternal::SetEncryptionPassphrase) +TEST_F(SyncManagerTest, SupplyPendingOldGAIAPass) { + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + Cryptographer other_cryptographer(&encryptor_); + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + std::string bootstrap_token; + cryptographer->GetBootstrapToken(&bootstrap_token); + other_cryptographer.Bootstrap(bootstrap_token); + + // Now update the nigori to reflect the new keys, and update the + // cryptographer to have pending keys. + KeyParams params = {"localhost", "dummy", "old_gaia"}; + other_cryptographer.AddKey(params); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByTagLookup(kNigoriTag)); + sync_pb::NigoriSpecifics nigori; + other_cryptographer.GetKeys(nigori.mutable_encrypted()); + node.SetNigoriSpecifics(nigori); + cryptographer->Update(nigori); + + // other_cryptographer now contains all encryption keys, and is encrypting + // with the newest gaia. + KeyParams new_params = {"localhost", "dummy", "new_gaia"}; + other_cryptographer.AddKey(new_params); + } + // The bootstrap token should have been updated. Save it to ensure it's based + // on the new GAIA password. + std::string bootstrap_token; + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)) + .WillOnce(SaveArg<0>(&bootstrap_token)); + EXPECT_CALL(observer_, OnPassphraseRequired(_,_)); + sync_manager_.SetEncryptionPassphrase("new_gaia", false); + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + testing::Mock::VerifyAndClearExpectations(&observer_); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_initialized()); + EXPECT_FALSE(cryptographer->is_ready()); + // Verify we're encrypting with the new key, even though we have pending + // keys. + sync_pb::EncryptedData encrypted; + other_cryptographer.GetKeys(&encrypted); + EXPECT_TRUE(cryptographer->CanDecrypt(encrypted)); + } + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetEncryptionPassphrase("old_gaia", false); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + + // Verify we're encrypting with the new key. + sync_pb::EncryptedData encrypted; + other_cryptographer.GetKeys(&encrypted); + EXPECT_TRUE(cryptographer->CanDecrypt(encrypted)); + + // Verify the saved bootstrap token is based on the new gaia password. + Cryptographer temp_cryptographer(&encryptor_); + temp_cryptographer.Bootstrap(bootstrap_token); + EXPECT_TRUE(temp_cryptographer.CanDecrypt(encrypted)); + } +} + +// Manually set the pending keys in the cryptographer/nigori to reflect the data +// being encrypted with an explicit (unprovided) passphrase, then supply the +// passphrase. +// (case 9 in SyncManager::SyncInternal::SetDecryptionPassphrase) +TEST_F(SyncManagerTest, SupplyPendingExplicitPass) { + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + Cryptographer other_cryptographer(&encryptor_); + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + std::string bootstrap_token; + cryptographer->GetBootstrapToken(&bootstrap_token); + other_cryptographer.Bootstrap(bootstrap_token); + + // Now update the nigori to reflect the new keys, and update the + // cryptographer to have pending keys. + KeyParams params = {"localhost", "dummy", "explicit"}; + other_cryptographer.AddKey(params); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByTagLookup(kNigoriTag)); + sync_pb::NigoriSpecifics nigori; + other_cryptographer.GetKeys(nigori.mutable_encrypted()); + cryptographer->Update(nigori); + EXPECT_TRUE(cryptographer->has_pending_keys()); + nigori.set_using_explicit_passphrase(true); + node.SetNigoriSpecifics(nigori); + } + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetDecryptionPassphrase("explicit"); + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + // Verify we're encrypting with the new key. + sync_pb::EncryptedData encrypted; + cryptographer->GetKeys(&encrypted); + EXPECT_TRUE(other_cryptographer.CanDecrypt(encrypted)); + } +} + +// Manually set the pending keys in the cryptographer/nigori to reflect the data +// being encrypted with a new (unprovided) GAIA password, then supply the +// password as a user-provided password. +// This is the android case 7/8. +TEST_F(SyncManagerTest, SupplyPendingGAIAPassUserProvided) { + EXPECT_FALSE(SetUpEncryption(DONT_WRITE_NIGORI, UNINITIALIZED)); + Cryptographer other_cryptographer(&encryptor_); + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + // Now update the nigori to reflect the new keys, and update the + // cryptographer to have pending keys. + KeyParams params = {"localhost", "dummy", "passphrase"}; + other_cryptographer.AddKey(params); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByTagLookup(kNigoriTag)); + sync_pb::NigoriSpecifics nigori; + other_cryptographer.GetKeys(nigori.mutable_encrypted()); + node.SetNigoriSpecifics(nigori); + cryptographer->Update(nigori); + EXPECT_FALSE(cryptographer->is_ready()); + } + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetEncryptionPassphrase("passphrase", false); + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + } +} + +TEST_F(SyncManagerTest, SetPassphraseWithEmptyPasswordNode) { + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + int64 node_id = 0; + std::string tag = "foo"; + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + + WriteNode password_node(&trans); + EXPECT_TRUE(password_node.InitUniqueByCreation(syncable::PASSWORDS, + root_node, tag)); + node_id = password_node.GetId(); + } + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetEncryptionPassphrase("new_passphrase", true); + EXPECT_FALSE(sync_manager_.EncryptEverythingEnabledForTest()); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode password_node(&trans); + EXPECT_EQ(BaseNode::INIT_FAILED_DECRYPT_IF_NECESSARY, + password_node.InitByClientTagLookup(syncable::PASSWORDS, + tag)); + } + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode password_node(&trans); + EXPECT_EQ(BaseNode::INIT_FAILED_DECRYPT_IF_NECESSARY, + password_node.InitByIdLookup(node_id)); + } +} + +TEST_F(SyncManagerTest, NudgeDelayTest) { + EXPECT_EQ(sync_manager_.GetNudgeDelayTimeDelta(syncable::BOOKMARKS), + base::TimeDelta::FromMilliseconds( + SyncManager::kDefaultNudgeDelayMilliseconds)); + + EXPECT_EQ(sync_manager_.GetNudgeDelayTimeDelta(syncable::AUTOFILL), + base::TimeDelta::FromSeconds( + browser_sync::kDefaultShortPollIntervalSeconds)); + + EXPECT_EQ(sync_manager_.GetNudgeDelayTimeDelta(syncable::PREFERENCES), + base::TimeDelta::FromMilliseconds( + SyncManager::kPreferencesNudgeDelayMilliseconds)); +} + +// Friended by WriteNode, so can't be in an anonymouse namespace. +TEST_F(SyncManagerTest, EncryptBookmarksWithLegacyData) { + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + std::string title; + SyncAPINameToServerName("Google", &title); + std::string url = "http://www.google.com"; + std::string raw_title2 = ".."; // An invalid cosmo title. + std::string title2; + SyncAPINameToServerName(raw_title2, &title2); + std::string url2 = "http://www.bla.com"; + + // Create a bookmark using the legacy format. + int64 node_id1 = MakeNode(sync_manager_.GetUserShare(), + syncable::BOOKMARKS, + "testtag"); + int64 node_id2 = MakeNode(sync_manager_.GetUserShare(), + syncable::BOOKMARKS, + "testtag2"); + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByIdLookup(node_id1)); + + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_bookmark()->set_url(url); + node.SetEntitySpecifics(entity_specifics); + + // Set the old style title. + syncable::MutableEntry* node_entry = node.entry_; + node_entry->Put(syncable::NON_UNIQUE_NAME, title); + + WriteNode node2(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node2.InitByIdLookup(node_id2)); + + sync_pb::EntitySpecifics entity_specifics2; + entity_specifics2.mutable_bookmark()->set_url(url2); + node2.SetEntitySpecifics(entity_specifics2); + + // Set the old style title. + syncable::MutableEntry* node_entry2 = node2.entry_; + node_entry2->Put(syncable::NON_UNIQUE_NAME, title2); + } + + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByIdLookup(node_id1)); + EXPECT_EQ(syncable::BOOKMARKS, node.GetModelType()); + EXPECT_EQ(title, node.GetTitle()); + EXPECT_EQ(title, node.GetBookmarkSpecifics().title()); + EXPECT_EQ(url, node.GetBookmarkSpecifics().url()); + + ReadNode node2(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node2.InitByIdLookup(node_id2)); + EXPECT_EQ(syncable::BOOKMARKS, node2.GetModelType()); + // We should de-canonicalize the title in GetTitle(), but the title in the + // specifics should be stored in the server legal form. + EXPECT_EQ(raw_title2, node2.GetTitle()); + EXPECT_EQ(title2, node2.GetBookmarkSpecifics().title()); + EXPECT_EQ(url2, node2.GetBookmarkSpecifics().url()); + } + + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::BOOKMARKS, + false /* not encrypted */)); + } + + EXPECT_CALL(observer_, + OnEncryptedTypesChanged( + HasModelTypes(syncable::ModelTypeSet::All()), true)); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.EnableEncryptEverything(); + EXPECT_TRUE(sync_manager_.EncryptEverythingEnabledForTest()); + + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + EXPECT_TRUE(GetEncryptedTypes(&trans).Equals( + syncable::ModelTypeSet::All())); + EXPECT_TRUE(syncable::VerifyDataTypeEncryptionForTest( + trans.GetWrappedTrans(), + trans.GetCryptographer(), + syncable::BOOKMARKS, + true /* is encrypted */)); + + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node.InitByIdLookup(node_id1)); + EXPECT_EQ(syncable::BOOKMARKS, node.GetModelType()); + EXPECT_EQ(title, node.GetTitle()); + EXPECT_EQ(title, node.GetBookmarkSpecifics().title()); + EXPECT_EQ(url, node.GetBookmarkSpecifics().url()); + + ReadNode node2(&trans); + EXPECT_EQ(BaseNode::INIT_OK, node2.InitByIdLookup(node_id2)); + EXPECT_EQ(syncable::BOOKMARKS, node2.GetModelType()); + // We should de-canonicalize the title in GetTitle(), but the title in the + // specifics should be stored in the server legal form. + EXPECT_EQ(raw_title2, node2.GetTitle()); + EXPECT_EQ(title2, node2.GetBookmarkSpecifics().title()); + EXPECT_EQ(url2, node2.GetBookmarkSpecifics().url()); + } +} + +// Create a bookmark and set the title/url, then verify the data was properly +// set. This replicates the unique way bookmarks have of creating sync nodes. +// See BookmarkChangeProcessor::PlaceSyncNode(..). +TEST_F(SyncManagerTest, CreateLocalBookmark) { + std::string title = "title"; + GURL url("url"); + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + WriteNode node(&trans); + ASSERT_TRUE(node.InitByCreation(syncable::BOOKMARKS, root_node, NULL)); + node.SetIsFolder(false); + node.SetTitle(UTF8ToWide(title)); + node.SetURL(url); + } + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode root_node(&trans); + root_node.InitByRootLookup(); + int64 child_id = root_node.GetFirstChildId(); + + ReadNode node(&trans); + ASSERT_EQ(BaseNode::INIT_OK, node.InitByIdLookup(child_id)); + EXPECT_FALSE(node.GetIsFolder()); + EXPECT_EQ(title, node.GetTitle()); + EXPECT_EQ(url, node.GetURL()); + } +} + +// Verifies WriteNode::UpdateEntryWithEncryption does not make unnecessary +// changes. +TEST_F(SyncManagerTest, UpdateEntryWithEncryption) { + std::string client_tag = "title"; + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_bookmark()->set_url("url"); + entity_specifics.mutable_bookmark()->set_title("title"); + MakeServerNode(sync_manager_.GetUserShare(), syncable::BOOKMARKS, client_tag, + BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, + client_tag), + entity_specifics); + // New node shouldn't start off unsynced. + EXPECT_FALSE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + // Manually change to the same data. Should not set is_unsynced. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + node.SetEntitySpecifics(entity_specifics); + } + EXPECT_FALSE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Encrypt the datatatype, should set is_unsynced. + EXPECT_CALL(observer_, + OnEncryptedTypesChanged( + HasModelTypes(syncable::ModelTypeSet::All()), true)); + EXPECT_CALL(observer_, OnEncryptionComplete()); + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, FULL_ENCRYPTION)); + + sync_manager_.RefreshNigori(kTestChromeVersion, base::Bind(&DoNothing)); + PumpLoop(); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + const syncable::Entry* node_entry = node.GetEntry(); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + EXPECT_TRUE(cryptographer->CanDecryptUsingDefaultKey( + specifics.encrypted())); + } + EXPECT_TRUE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Set a new passphrase. Should set is_unsynced. + testing::Mock::VerifyAndClearExpectations(&observer_); + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetEncryptionPassphrase("new_passphrase", true); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + const syncable::Entry* node_entry = node.GetEntry(); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->is_ready()); + EXPECT_TRUE(cryptographer->CanDecryptUsingDefaultKey( + specifics.encrypted())); + } + EXPECT_TRUE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Force a re-encrypt everything. Should not set is_unsynced. + testing::Mock::VerifyAndClearExpectations(&observer_); + EXPECT_CALL(observer_, OnEncryptionComplete()); + + sync_manager_.RefreshNigori(kTestChromeVersion, base::Bind(&DoNothing)); + PumpLoop(); + + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + const syncable::Entry* node_entry = node.GetEntry(); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->CanDecryptUsingDefaultKey( + specifics.encrypted())); + } + EXPECT_FALSE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Manually change to the same data. Should not set is_unsynced. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + node.SetEntitySpecifics(entity_specifics); + const syncable::Entry* node_entry = node.GetEntry(); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + EXPECT_FALSE(node_entry->Get(IS_UNSYNCED)); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->CanDecryptUsingDefaultKey( + specifics.encrypted())); + } + EXPECT_FALSE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Manually change to different data. Should set is_unsynced. + { + entity_specifics.mutable_bookmark()->set_url("url2"); + entity_specifics.mutable_bookmark()->set_title("title2"); + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + node.SetEntitySpecifics(entity_specifics); + const syncable::Entry* node_entry = node.GetEntry(); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + EXPECT_TRUE(node_entry->Get(IS_UNSYNCED)); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + Cryptographer* cryptographer = trans.GetCryptographer(); + EXPECT_TRUE(cryptographer->CanDecryptUsingDefaultKey( + specifics.encrypted())); + } +} + +// Passwords have their own handling for encryption. Verify it does not result +// in unnecessary writes via SetEntitySpecifics. +TEST_F(SyncManagerTest, UpdatePasswordSetEntitySpecificsNoChange) { + std::string client_tag = "title"; + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + sync_pb::EntitySpecifics entity_specifics; + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + sync_pb::PasswordSpecificsData data; + data.set_password_value("secret"); + cryptographer->Encrypt( + data, + entity_specifics.mutable_password()-> + mutable_encrypted()); + } + MakeServerNode(sync_manager_.GetUserShare(), syncable::PASSWORDS, client_tag, + BaseNode::GenerateSyncableHash(syncable::PASSWORDS, + client_tag), + entity_specifics); + // New node shouldn't start off unsynced. + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PASSWORDS, client_tag)); + + // Manually change to the same data via SetEntitySpecifics. Should not set + // is_unsynced. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::PASSWORDS, client_tag)); + node.SetEntitySpecifics(entity_specifics); + } + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PASSWORDS, client_tag)); +} + +// Passwords have their own handling for encryption. Verify it does not result +// in unnecessary writes via SetPasswordSpecifics. +TEST_F(SyncManagerTest, UpdatePasswordSetPasswordSpecifics) { + std::string client_tag = "title"; + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + sync_pb::EntitySpecifics entity_specifics; + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + sync_pb::PasswordSpecificsData data; + data.set_password_value("secret"); + cryptographer->Encrypt( + data, + entity_specifics.mutable_password()-> + mutable_encrypted()); + } + MakeServerNode(sync_manager_.GetUserShare(), syncable::PASSWORDS, client_tag, + BaseNode::GenerateSyncableHash(syncable::PASSWORDS, + client_tag), + entity_specifics); + // New node shouldn't start off unsynced. + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PASSWORDS, client_tag)); + + // Manually change to the same data via SetPasswordSpecifics. Should not set + // is_unsynced. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::PASSWORDS, client_tag)); + node.SetPasswordSpecifics(node.GetPasswordSpecifics()); + } + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PASSWORDS, client_tag)); + + // Manually change to different data. Should set is_unsynced. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::PASSWORDS, client_tag)); + Cryptographer* cryptographer = trans.GetCryptographer(); + sync_pb::PasswordSpecificsData data; + data.set_password_value("secret2"); + cryptographer->Encrypt( + data, + entity_specifics.mutable_password()->mutable_encrypted()); + node.SetPasswordSpecifics(data); + const syncable::Entry* node_entry = node.GetEntry(); + EXPECT_TRUE(node_entry->Get(IS_UNSYNCED)); + } +} + +// Passwords have their own handling for encryption. Verify setting a new +// passphrase updates the data. +TEST_F(SyncManagerTest, UpdatePasswordNewPassphrase) { + std::string client_tag = "title"; + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + sync_pb::EntitySpecifics entity_specifics; + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + sync_pb::PasswordSpecificsData data; + data.set_password_value("secret"); + cryptographer->Encrypt( + data, + entity_specifics.mutable_password()->mutable_encrypted()); + } + MakeServerNode(sync_manager_.GetUserShare(), syncable::PASSWORDS, client_tag, + BaseNode::GenerateSyncableHash(syncable::PASSWORDS, + client_tag), + entity_specifics); + // New node shouldn't start off unsynced. + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PASSWORDS, client_tag)); + + // Set a new passphrase. Should set is_unsynced. + testing::Mock::VerifyAndClearExpectations(&observer_); + EXPECT_CALL(observer_, OnBootstrapTokenUpdated(_)); + EXPECT_CALL(observer_, OnPassphraseAccepted()); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.SetEncryptionPassphrase("new_passphrase", true); + EXPECT_TRUE(ResetUnsyncedEntry(syncable::PASSWORDS, client_tag)); +} + +// Passwords have their own handling for encryption. Verify it does not result +// in unnecessary writes via ReencryptEverything. +TEST_F(SyncManagerTest, UpdatePasswordReencryptEverything) { + std::string client_tag = "title"; + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + sync_pb::EntitySpecifics entity_specifics; + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + Cryptographer* cryptographer = trans.GetCryptographer(); + sync_pb::PasswordSpecificsData data; + data.set_password_value("secret"); + cryptographer->Encrypt( + data, + entity_specifics.mutable_password()->mutable_encrypted()); + } + MakeServerNode(sync_manager_.GetUserShare(), syncable::PASSWORDS, client_tag, + BaseNode::GenerateSyncableHash(syncable::PASSWORDS, + client_tag), + entity_specifics); + // New node shouldn't start off unsynced. + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PASSWORDS, client_tag)); + + // Force a re-encrypt everything. Should not set is_unsynced. + testing::Mock::VerifyAndClearExpectations(&observer_); + EXPECT_CALL(observer_, OnEncryptionComplete()); + sync_manager_.RefreshNigori(kTestChromeVersion, base::Bind(&DoNothing)); + PumpLoop(); + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PASSWORDS, client_tag)); +} + +// Verify SetTitle(..) doesn't unnecessarily set IS_UNSYNCED for bookmarks +// when we write the same data, but does set it when we write new data. +TEST_F(SyncManagerTest, SetBookmarkTitle) { + std::string client_tag = "title"; + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_bookmark()->set_url("url"); + entity_specifics.mutable_bookmark()->set_title("title"); + MakeServerNode(sync_manager_.GetUserShare(), syncable::BOOKMARKS, client_tag, + BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, + client_tag), + entity_specifics); + // New node shouldn't start off unsynced. + EXPECT_FALSE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Manually change to the same title. Should not set is_unsynced. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + node.SetTitle(UTF8ToWide(client_tag)); + } + EXPECT_FALSE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Manually change to new title. Should set is_unsynced. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + node.SetTitle(UTF8ToWide("title2")); + } + EXPECT_TRUE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); +} + +// Verify SetTitle(..) doesn't unnecessarily set IS_UNSYNCED for encrypted +// bookmarks when we write the same data, but does set it when we write new +// data. +TEST_F(SyncManagerTest, SetBookmarkTitleWithEncryption) { + std::string client_tag = "title"; + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_bookmark()->set_url("url"); + entity_specifics.mutable_bookmark()->set_title("title"); + MakeServerNode(sync_manager_.GetUserShare(), syncable::BOOKMARKS, client_tag, + BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, + client_tag), + entity_specifics); + // New node shouldn't start off unsynced. + EXPECT_FALSE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Encrypt the datatatype, should set is_unsynced. + EXPECT_CALL(observer_, + OnEncryptedTypesChanged( + HasModelTypes(syncable::ModelTypeSet::All()), true)); + EXPECT_CALL(observer_, OnEncryptionComplete()); + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, FULL_ENCRYPTION)); + sync_manager_.RefreshNigori(kTestChromeVersion, base::Bind(&DoNothing)); + PumpLoop(); + EXPECT_TRUE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Manually change to the same title. Should not set is_unsynced. + // NON_UNIQUE_NAME should be kEncryptedString. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + node.SetTitle(UTF8ToWide(client_tag)); + const syncable::Entry* node_entry = node.GetEntry(); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + } + EXPECT_FALSE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); + + // Manually change to new title. Should set is_unsynced. NON_UNIQUE_NAME + // should still be kEncryptedString. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + node.SetTitle(UTF8ToWide("title2")); + const syncable::Entry* node_entry = node.GetEntry(); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + } + EXPECT_TRUE(ResetUnsyncedEntry(syncable::BOOKMARKS, client_tag)); +} + +// Verify SetTitle(..) doesn't unnecessarily set IS_UNSYNCED for non-bookmarks +// when we write the same data, but does set it when we write new data. +TEST_F(SyncManagerTest, SetNonBookmarkTitle) { + std::string client_tag = "title"; + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_preference()->set_name("name"); + entity_specifics.mutable_preference()->set_value("value"); + MakeServerNode(sync_manager_.GetUserShare(), + syncable::PREFERENCES, + client_tag, + BaseNode::GenerateSyncableHash(syncable::PREFERENCES, + client_tag), + entity_specifics); + // New node shouldn't start off unsynced. + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PREFERENCES, client_tag)); + + // Manually change to the same title. Should not set is_unsynced. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::PREFERENCES, client_tag)); + node.SetTitle(UTF8ToWide(client_tag)); + } + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PREFERENCES, client_tag)); + + // Manually change to new title. Should set is_unsynced. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::PREFERENCES, client_tag)); + node.SetTitle(UTF8ToWide("title2")); + } + EXPECT_TRUE(ResetUnsyncedEntry(syncable::PREFERENCES, client_tag)); +} + +// Verify SetTitle(..) doesn't unnecessarily set IS_UNSYNCED for encrypted +// non-bookmarks when we write the same data or when we write new data +// data (should remained kEncryptedString). +TEST_F(SyncManagerTest, SetNonBookmarkTitleWithEncryption) { + std::string client_tag = "title"; + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_preference()->set_name("name"); + entity_specifics.mutable_preference()->set_value("value"); + MakeServerNode(sync_manager_.GetUserShare(), + syncable::PREFERENCES, + client_tag, + BaseNode::GenerateSyncableHash(syncable::PREFERENCES, + client_tag), + entity_specifics); + // New node shouldn't start off unsynced. + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PREFERENCES, client_tag)); + + // Encrypt the datatatype, should set is_unsynced. + EXPECT_CALL(observer_, + OnEncryptedTypesChanged( + HasModelTypes(syncable::ModelTypeSet::All()), true)); + EXPECT_CALL(observer_, OnEncryptionComplete()); + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, FULL_ENCRYPTION)); + sync_manager_.RefreshNigori(kTestChromeVersion, base::Bind(&DoNothing)); + PumpLoop(); + EXPECT_TRUE(ResetUnsyncedEntry(syncable::PREFERENCES, client_tag)); + + // Manually change to the same title. Should not set is_unsynced. + // NON_UNIQUE_NAME should be kEncryptedString. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::PREFERENCES, client_tag)); + node.SetTitle(UTF8ToWide(client_tag)); + const syncable::Entry* node_entry = node.GetEntry(); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + } + EXPECT_FALSE(ResetUnsyncedEntry(syncable::PREFERENCES, client_tag)); + + // Manually change to new title. Should not set is_unsynced because the + // NON_UNIQUE_NAME should still be kEncryptedString. + { + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::PREFERENCES, client_tag)); + node.SetTitle(UTF8ToWide("title2")); + const syncable::Entry* node_entry = node.GetEntry(); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + EXPECT_FALSE(node_entry->Get(IS_UNSYNCED)); + } +} + +// Create an encrypted entry when the cryptographer doesn't think the type is +// marked for encryption. Ensure reads/writes don't break and don't unencrypt +// the data. +TEST_F(SyncManagerTest, SetPreviouslyEncryptedSpecifics) { + std::string client_tag = "tag"; + std::string url = "url"; + std::string url2 = "new_url"; + std::string title = "title"; + sync_pb::EntitySpecifics entity_specifics; + EXPECT_TRUE(SetUpEncryption(WRITE_TO_NIGORI, DEFAULT_ENCRYPTION)); + { + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + browser_sync::Cryptographer* crypto = trans.GetCryptographer(); + sync_pb::EntitySpecifics bm_specifics; + bm_specifics.mutable_bookmark()->set_title("title"); + bm_specifics.mutable_bookmark()->set_url("url"); + sync_pb::EncryptedData encrypted; + crypto->Encrypt(bm_specifics, &encrypted); + entity_specifics.mutable_encrypted()->CopyFrom(encrypted); + syncable::AddDefaultFieldValue(syncable::BOOKMARKS, &entity_specifics); + } + MakeServerNode(sync_manager_.GetUserShare(), syncable::BOOKMARKS, client_tag, + BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, + client_tag), + entity_specifics); + + { + // Verify the data. + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + EXPECT_EQ(title, node.GetTitle()); + EXPECT_EQ(GURL(url), node.GetURL()); + } + + { + // Overwrite the url (which overwrites the specifics). + WriteTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + WriteNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + node.SetURL(GURL(url2)); + } + + { + // Verify it's still encrypted and it has the most recent url. + ReadTransaction trans(FROM_HERE, sync_manager_.GetUserShare()); + ReadNode node(&trans); + EXPECT_EQ(BaseNode::INIT_OK, + node.InitByClientTagLookup(syncable::BOOKMARKS, client_tag)); + EXPECT_EQ(title, node.GetTitle()); + EXPECT_EQ(GURL(url2), node.GetURL()); + const syncable::Entry* node_entry = node.GetEntry(); + EXPECT_EQ(kEncryptedString, node_entry->Get(NON_UNIQUE_NAME)); + const sync_pb::EntitySpecifics& specifics = node_entry->Get(SPECIFICS); + EXPECT_TRUE(specifics.has_encrypted()); + } +} + +} // namespace browser_sync diff --git a/sync/internal_api/test_user_share.cc b/sync/internal_api/test_user_share.cc new file mode 100644 index 0000000..743f3d0 --- /dev/null +++ b/sync/internal_api/test_user_share.cc @@ -0,0 +1,40 @@ +// Copyright (c) 2012 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 "sync/internal_api/test_user_share.h" + +#include "base/compiler_specific.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace browser_sync { + +TestUserShare::TestUserShare() {} + +TestUserShare::~TestUserShare() { + if (user_share_.get()) + ADD_FAILURE() << "Should have called TestUserShare::TearDown()"; +} + +void TestUserShare::SetUp() { + user_share_.reset(new sync_api::UserShare()); + dir_maker_.SetUp(); + + // The pointer is owned by dir_maker_, we should not be storing it in a + // scoped_ptr. We must be careful to ensure the scoped_ptr never deletes it. + user_share_->directory.reset(dir_maker_.directory()); +} + +void TestUserShare::TearDown() { + // Ensure the scoped_ptr doesn't delete the memory we don't own. + ignore_result(user_share_->directory.release()); + + user_share_.reset(); + dir_maker_.TearDown(); +} + +sync_api::UserShare* TestUserShare::user_share() { + return user_share_.get(); +} + +} // namespace browser_sync diff --git a/sync/internal_api/test_user_share.h b/sync/internal_api/test_user_share.h new file mode 100644 index 0000000..b212e3c --- /dev/null +++ b/sync/internal_api/test_user_share.h @@ -0,0 +1,67 @@ +// Copyright (c) 2012 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. +// +// A handy class that takes care of setting up and destroying a +// sync_api::UserShare instance for unit tests that require one. +// +// The expected usage is to make this a component of your test fixture: +// +// class AwesomenessTest : public testing::Test { +// public: +// virtual void SetUp() { +// test_user_share_.SetUp(); +// } +// virtual void TearDown() { +// test_user_share_.TearDown(); +// } +// protected: +// TestUserShare test_user_share_; +// }; +// +// Then, in your tests: +// +// TEST_F(AwesomenessTest, IsMaximal) { +// sync_api::ReadTransaction trans(test_user_share_.user_share()); +// ... +// } +// + +#ifndef SYNC_INTERNAL_API_TEST_USER_SHARE_H_ +#define SYNC_INTERNAL_API_TEST_USER_SHARE_H_ +#pragma once + +#include "base/basictypes.h" +#include "sync/internal_api/user_share.h" +#include "sync/test/engine/test_directory_setter_upper.h" + +namespace browser_sync { + +class TestUserShare { + public: + TestUserShare(); + ~TestUserShare(); + + // Sets up the UserShare instance. Clears any existing database + // backing files that might exist on disk. + void SetUp(); + + // Undo everything done by SetUp(): closes the UserShare and deletes + // the backing files. Before closing the directory, this will run + // the directory invariant checks and perform the SaveChanges action + // on the user share's directory. + void TearDown(); + + // Non-NULL iff called between a call to SetUp() and TearDown(). + sync_api::UserShare* user_share(); + + private: + TestDirectorySetterUpper dir_maker_; + scoped_ptr<sync_api::UserShare> user_share_; + + DISALLOW_COPY_AND_ASSIGN(TestUserShare); +}; + +} // namespace browser_sync + +#endif // SYNC_INTERNAL_API_TEST_USER_SHARE_H_ diff --git a/sync/internal_api/user_share.cc b/sync/internal_api/user_share.cc new file mode 100644 index 0000000..a0a77ba --- /dev/null +++ b/sync/internal_api/user_share.cc @@ -0,0 +1,15 @@ +// Copyright (c) 2012 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 "sync/internal_api/user_share.h" + +#include "sync/syncable/syncable.h" + +namespace sync_api { + +UserShare::UserShare() {} + +UserShare::~UserShare() {} + +} diff --git a/sync/internal_api/user_share.h b/sync/internal_api/user_share.h new file mode 100644 index 0000000..275fa42 --- /dev/null +++ b/sync/internal_api/user_share.h @@ -0,0 +1,36 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_USER_SHARE_H_ +#define SYNC_INTERNAL_API_USER_SHARE_H_ +#pragma once + +#include <string> + +#include "base/memory/scoped_ptr.h" + +namespace syncable { +class Directory; +} + +namespace sync_api { + +// A UserShare encapsulates the syncable pieces that represent an authenticated +// user and their data (share). +// This encompasses all pieces required to build transaction objects on the +// syncable share. +struct UserShare { + UserShare(); + ~UserShare(); + + // The Directory itself, which is the parent of Transactions. + scoped_ptr<syncable::Directory> directory; + + // The username of the sync user. + std::string name; +}; + +} + +#endif // SYNC_INTERNAL_API_USER_SHARE_H_ diff --git a/sync/internal_api/write_node.cc b/sync/internal_api/write_node.cc new file mode 100644 index 0000000..ce20e31 --- /dev/null +++ b/sync/internal_api/write_node.cc @@ -0,0 +1,503 @@ +// Copyright (c) 2012 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 "sync/internal_api/write_node.h" + +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "sync/engine/nigori_util.h" +#include "sync/internal_api/base_transaction.h" +#include "sync/internal_api/syncapi_internal.h" +#include "sync/internal_api/write_transaction.h" +#include "sync/protocol/app_specifics.pb.h" +#include "sync/protocol/autofill_specifics.pb.h" +#include "sync/protocol/bookmark_specifics.pb.h" +#include "sync/protocol/extension_specifics.pb.h" +#include "sync/protocol/password_specifics.pb.h" +#include "sync/protocol/session_specifics.pb.h" +#include "sync/protocol/theme_specifics.pb.h" +#include "sync/protocol/typed_url_specifics.pb.h" +#include "sync/syncable/syncable.h" +#include "sync/util/cryptographer.h" + +using browser_sync::Cryptographer; +using std::string; +using std::vector; +using syncable::kEncryptedString; +using syncable::SPECIFICS; + +namespace sync_api { + +static const char kDefaultNameForNewNodes[] = " "; + +void WriteNode::SetIsFolder(bool folder) { + if (entry_->Get(syncable::IS_DIR) == folder) + return; // Skip redundant changes. + + entry_->Put(syncable::IS_DIR, folder); + MarkForSyncing(); +} + +void WriteNode::SetTitle(const std::wstring& title) { + DCHECK_NE(GetModelType(), syncable::UNSPECIFIED); + syncable::ModelType type = GetModelType(); + Cryptographer* cryptographer = GetTransaction()->GetCryptographer(); + // It's possible the nigori lost the set of encrypted types. If the current + // specifics are already encrypted, we want to ensure we continue encrypting. + bool needs_encryption = cryptographer->GetEncryptedTypes().Has(type) || + entry_->Get(SPECIFICS).has_encrypted(); + + // If this datatype is encrypted and is not a bookmark, we disregard the + // specified title in favor of kEncryptedString. For encrypted bookmarks the + // NON_UNIQUE_NAME will still be kEncryptedString, but we store the real title + // into the specifics. All strings compared are server legal strings. + std::string new_legal_title; + if (type != syncable::BOOKMARKS && needs_encryption) { + new_legal_title = kEncryptedString; + } else { + SyncAPINameToServerName(WideToUTF8(title), &new_legal_title); + } + + std::string current_legal_title; + if (syncable::BOOKMARKS == type && + entry_->Get(syncable::SPECIFICS).has_encrypted()) { + // Encrypted bookmarks only have their title in the unencrypted specifics. + current_legal_title = GetBookmarkSpecifics().title(); + } else { + // Non-bookmarks and legacy bookmarks (those with no title in their + // specifics) store their title in NON_UNIQUE_NAME. Non-legacy bookmarks + // store their title in specifics as well as NON_UNIQUE_NAME. + current_legal_title = entry_->Get(syncable::NON_UNIQUE_NAME); + } + + bool title_matches = (current_legal_title == new_legal_title); + bool encrypted_without_overwriting_name = (needs_encryption && + entry_->Get(syncable::NON_UNIQUE_NAME) != kEncryptedString); + + // If the title matches and the NON_UNIQUE_NAME is properly overwritten as + // necessary, nothing needs to change. + if (title_matches && !encrypted_without_overwriting_name) { + DVLOG(2) << "Title matches, dropping change."; + return; + } + + // For bookmarks, we also set the title field in the specifics. + // TODO(zea): refactor bookmarks to not need this functionality. + if (GetModelType() == syncable::BOOKMARKS) { + sync_pb::EntitySpecifics specifics = GetEntitySpecifics(); + specifics.mutable_bookmark()->set_title(new_legal_title); + SetEntitySpecifics(specifics); // Does it's own encryption checking. + } + + // For bookmarks, this has to happen after we set the title in the specifics, + // because the presence of a title in the NON_UNIQUE_NAME is what controls + // the logic deciding whether this is an empty node or a legacy bookmark. + // See BaseNode::GetUnencryptedSpecific(..). + if (needs_encryption) + entry_->Put(syncable::NON_UNIQUE_NAME, kEncryptedString); + else + entry_->Put(syncable::NON_UNIQUE_NAME, new_legal_title); + + DVLOG(1) << "Overwriting title of type " + << syncable::ModelTypeToString(type) + << " and marking for syncing."; + MarkForSyncing(); +} + +void WriteNode::SetURL(const GURL& url) { + sync_pb::BookmarkSpecifics new_value = GetBookmarkSpecifics(); + new_value.set_url(url.spec()); + SetBookmarkSpecifics(new_value); +} + +void WriteNode::SetAppSpecifics( + const sync_pb::AppSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_app()->CopyFrom(new_value); + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetAutofillSpecifics( + const sync_pb::AutofillSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_autofill()->CopyFrom(new_value); + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetAutofillProfileSpecifics( + const sync_pb::AutofillProfileSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_autofill_profile()-> + CopyFrom(new_value); + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetBookmarkSpecifics( + const sync_pb::BookmarkSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_bookmark()->CopyFrom(new_value); + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetNigoriSpecifics( + const sync_pb::NigoriSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_nigori()->CopyFrom(new_value); + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetPasswordSpecifics( + const sync_pb::PasswordSpecificsData& data) { + DCHECK_EQ(syncable::PASSWORDS, GetModelType()); + + Cryptographer* cryptographer = GetTransaction()->GetCryptographer(); + + // We have to do the idempotency check here (vs in UpdateEntryWithEncryption) + // because Passwords have their encrypted data within the PasswordSpecifics, + // vs within the EntitySpecifics like all the other types. + const sync_pb::EntitySpecifics& old_specifics = GetEntry()->Get(SPECIFICS); + sync_pb::EntitySpecifics entity_specifics; + // Copy over the old specifics if they exist. + if (syncable::GetModelTypeFromSpecifics(old_specifics) == + syncable::PASSWORDS) { + entity_specifics.CopyFrom(old_specifics); + } else { + syncable::AddDefaultFieldValue(syncable::PASSWORDS, + &entity_specifics); + } + sync_pb::PasswordSpecifics* password_specifics = + entity_specifics.mutable_password(); + // This will only update password_specifics if the underlying unencrypted blob + // was different from |data| or was not encrypted with the proper passphrase. + if (!cryptographer->Encrypt(data, password_specifics->mutable_encrypted())) { + NOTREACHED() << "Failed to encrypt password, possibly due to sync node " + << "corruption"; + return; + } + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetThemeSpecifics( + const sync_pb::ThemeSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_theme()->CopyFrom(new_value); + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetSessionSpecifics( + const sync_pb::SessionSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_session()->CopyFrom(new_value); + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetEntitySpecifics( + const sync_pb::EntitySpecifics& new_value) { + syncable::ModelType new_specifics_type = + syncable::GetModelTypeFromSpecifics(new_value); + DCHECK_NE(new_specifics_type, syncable::UNSPECIFIED); + DVLOG(1) << "Writing entity specifics of type " + << syncable::ModelTypeToString(new_specifics_type); + // GetModelType() can be unspecified if this is the first time this + // node is being initialized (see PutModelType()). Otherwise, it + // should match |new_specifics_type|. + if (GetModelType() != syncable::UNSPECIFIED) { + DCHECK_EQ(new_specifics_type, GetModelType()); + } + browser_sync::Cryptographer* cryptographer = + GetTransaction()->GetCryptographer(); + + // Preserve unknown fields. + const sync_pb::EntitySpecifics& old_specifics = entry_->Get(SPECIFICS); + sync_pb::EntitySpecifics new_specifics; + new_specifics.CopyFrom(new_value); + new_specifics.mutable_unknown_fields()->MergeFrom( + old_specifics.unknown_fields()); + + // Will update the entry if encryption was necessary. + if (!UpdateEntryWithEncryption(cryptographer, new_specifics, entry_)) { + return; + } + if (entry_->Get(SPECIFICS).has_encrypted()) { + // EncryptIfNecessary already updated the entry for us and marked for + // syncing if it was needed. Now we just make a copy of the unencrypted + // specifics so that if this node is updated, we do not have to decrypt the + // old data. Note that this only modifies the node's local data, not the + // entry itself. + SetUnencryptedSpecifics(new_value); + } + + DCHECK_EQ(new_specifics_type, GetModelType()); +} + +void WriteNode::ResetFromSpecifics() { + SetEntitySpecifics(GetEntitySpecifics()); +} + +void WriteNode::SetTypedUrlSpecifics( + const sync_pb::TypedUrlSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_typed_url()->CopyFrom(new_value); + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetExtensionSpecifics( + const sync_pb::ExtensionSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.mutable_extension()->CopyFrom(new_value); + SetEntitySpecifics(entity_specifics); +} + +void WriteNode::SetExternalId(int64 id) { + if (GetExternalId() != id) + entry_->Put(syncable::LOCAL_EXTERNAL_ID, id); +} + +WriteNode::WriteNode(WriteTransaction* transaction) + : entry_(NULL), transaction_(transaction) { + DCHECK(transaction); +} + +WriteNode::~WriteNode() { + delete entry_; +} + +// Find an existing node matching the ID |id|, and bind this WriteNode to it. +// Return true on success. +BaseNode::InitByLookupResult WriteNode::InitByIdLookup(int64 id) { + DCHECK(!entry_) << "Init called twice"; + DCHECK_NE(id, kInvalidId); + entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), + syncable::GET_BY_HANDLE, id); + if (!entry_->good()) + return INIT_FAILED_ENTRY_NOT_GOOD; + if (entry_->Get(syncable::IS_DEL)) + return INIT_FAILED_ENTRY_IS_DEL; + return DecryptIfNecessary() ? INIT_OK : INIT_FAILED_DECRYPT_IF_NECESSARY; +} + +// Find a node by client tag, and bind this WriteNode to it. +// Return true if the write node was found, and was not deleted. +// Undeleting a deleted node is possible by ClientTag. +BaseNode::InitByLookupResult WriteNode::InitByClientTagLookup( + syncable::ModelType model_type, + const std::string& tag) { + DCHECK(!entry_) << "Init called twice"; + if (tag.empty()) + return INIT_FAILED_PRECONDITION; + + const std::string hash = GenerateSyncableHash(model_type, tag); + + entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), + syncable::GET_BY_CLIENT_TAG, hash); + if (!entry_->good()) + return INIT_FAILED_ENTRY_NOT_GOOD; + if (entry_->Get(syncable::IS_DEL)) + return INIT_FAILED_ENTRY_IS_DEL; + return DecryptIfNecessary() ? INIT_OK : INIT_FAILED_DECRYPT_IF_NECESSARY; +} + +BaseNode::InitByLookupResult WriteNode::InitByTagLookup( + const std::string& tag) { + DCHECK(!entry_) << "Init called twice"; + if (tag.empty()) + return INIT_FAILED_PRECONDITION; + entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), + syncable::GET_BY_SERVER_TAG, tag); + if (!entry_->good()) + return INIT_FAILED_ENTRY_NOT_GOOD; + if (entry_->Get(syncable::IS_DEL)) + return INIT_FAILED_ENTRY_IS_DEL; + syncable::ModelType model_type = GetModelType(); + DCHECK_EQ(syncable::NIGORI, model_type); + return INIT_OK; +} + +void WriteNode::PutModelType(syncable::ModelType model_type) { + // Set an empty specifics of the appropriate datatype. The presence + // of the specific field will identify the model type. + DCHECK(GetModelType() == model_type || + GetModelType() == syncable::UNSPECIFIED); // Immutable once set. + + sync_pb::EntitySpecifics specifics; + syncable::AddDefaultFieldValue(model_type, &specifics); + SetEntitySpecifics(specifics); +} + +// Create a new node with default properties, and bind this WriteNode to it. +// Return true on success. +bool WriteNode::InitByCreation(syncable::ModelType model_type, + const BaseNode& parent, + const BaseNode* predecessor) { + DCHECK(!entry_) << "Init called twice"; + // |predecessor| must be a child of |parent| or NULL. + if (predecessor && predecessor->GetParentId() != parent.GetId()) { + DCHECK(false); + return false; + } + + syncable::Id parent_id = parent.GetEntry()->Get(syncable::ID); + + // Start out with a dummy name. We expect + // the caller to set a meaningful name after creation. + string dummy(kDefaultNameForNewNodes); + + entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), + syncable::CREATE, parent_id, dummy); + + if (!entry_->good()) + return false; + + // Entries are untitled folders by default. + entry_->Put(syncable::IS_DIR, true); + + PutModelType(model_type); + + // Now set the predecessor, which sets IS_UNSYNCED as necessary. + return PutPredecessor(predecessor); +} + +// Create a new node with default properties and a client defined unique tag, +// and bind this WriteNode to it. +// Return true on success. If the tag exists in the database, then +// we will attempt to undelete the node. +// TODO(chron): Code datatype into hash tag. +// TODO(chron): Is model type ever lost? +bool WriteNode::InitUniqueByCreation(syncable::ModelType model_type, + const BaseNode& parent, + const std::string& tag) { + DCHECK(!entry_) << "Init called twice"; + if (tag.empty()) { + LOG(WARNING) << "InitUniqueByCreation failed due to empty tag."; + return false; + } + + const std::string hash = GenerateSyncableHash(model_type, tag); + + syncable::Id parent_id = parent.GetEntry()->Get(syncable::ID); + + // Start out with a dummy name. We expect + // the caller to set a meaningful name after creation. + string dummy(kDefaultNameForNewNodes); + + // Check if we have this locally and need to undelete it. + scoped_ptr<syncable::MutableEntry> existing_entry( + new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), + syncable::GET_BY_CLIENT_TAG, hash)); + + if (existing_entry->good()) { + if (existing_entry->Get(syncable::IS_DEL)) { + // Rules for undelete: + // BASE_VERSION: Must keep the same. + // ID: Essential to keep the same. + // META_HANDLE: Must be the same, so we can't "split" the entry. + // IS_DEL: Must be set to false, will cause reindexing. + // This one is weird because IS_DEL is true for "update only" + // items. It should be OK to undelete an update only. + // MTIME/CTIME: Seems reasonable to just leave them alone. + // IS_UNSYNCED: Must set this to true or face database insurrection. + // We do this below this block. + // IS_UNAPPLIED_UPDATE: Either keep it the same or also set BASE_VERSION + // to SERVER_VERSION. We keep it the same here. + // IS_DIR: We'll leave it the same. + // SPECIFICS: Reset it. + + existing_entry->Put(syncable::IS_DEL, false); + + // Client tags are immutable and must be paired with the ID. + // If a server update comes down with an ID and client tag combo, + // and it already exists, always overwrite it and store only one copy. + // We have to undelete entries because we can't disassociate IDs from + // tags and updates. + + existing_entry->Put(syncable::NON_UNIQUE_NAME, dummy); + existing_entry->Put(syncable::PARENT_ID, parent_id); + entry_ = existing_entry.release(); + } else { + return false; + } + } else { + entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), + syncable::CREATE, parent_id, dummy); + if (!entry_->good()) { + return false; + } + + // Only set IS_DIR for new entries. Don't bitflip undeleted ones. + entry_->Put(syncable::UNIQUE_CLIENT_TAG, hash); + } + + // We don't support directory and tag combinations. + entry_->Put(syncable::IS_DIR, false); + + // Will clear specifics data. + PutModelType(model_type); + + // Now set the predecessor, which sets IS_UNSYNCED as necessary. + return PutPredecessor(NULL); +} + +bool WriteNode::SetPosition(const BaseNode& new_parent, + const BaseNode* predecessor) { + // |predecessor| must be a child of |new_parent| or NULL. + if (predecessor && predecessor->GetParentId() != new_parent.GetId()) { + DCHECK(false); + return false; + } + + syncable::Id new_parent_id = new_parent.GetEntry()->Get(syncable::ID); + + // Filter out redundant changes if both the parent and the predecessor match. + if (new_parent_id == entry_->Get(syncable::PARENT_ID)) { + const syncable::Id& old = entry_->Get(syncable::PREV_ID); + if ((!predecessor && old.IsRoot()) || + (predecessor && (old == predecessor->GetEntry()->Get(syncable::ID)))) { + return true; + } + } + + // Atomically change the parent. This will fail if it would + // introduce a cycle in the hierarchy. + if (!entry_->Put(syncable::PARENT_ID, new_parent_id)) + return false; + + // Now set the predecessor, which sets IS_UNSYNCED as necessary. + return PutPredecessor(predecessor); +} + +const syncable::Entry* WriteNode::GetEntry() const { + return entry_; +} + +const BaseTransaction* WriteNode::GetTransaction() const { + return transaction_; +} + +void WriteNode::Remove() { + entry_->Put(syncable::IS_DEL, true); + MarkForSyncing(); +} + +bool WriteNode::PutPredecessor(const BaseNode* predecessor) { + syncable::Id predecessor_id = predecessor ? + predecessor->GetEntry()->Get(syncable::ID) : syncable::Id(); + if (!entry_->PutPredecessor(predecessor_id)) + return false; + // Mark this entry as unsynced, to wake up the syncer. + MarkForSyncing(); + + return true; +} + +void WriteNode::SetFaviconBytes(const vector<unsigned char>& bytes) { + sync_pb::BookmarkSpecifics new_value = GetBookmarkSpecifics(); + new_value.set_favicon(bytes.empty() ? NULL : &bytes[0], bytes.size()); + SetBookmarkSpecifics(new_value); +} + +void WriteNode::MarkForSyncing() { + syncable::MarkForSyncing(entry_); +} + +} // namespace sync_api diff --git a/sync/internal_api/write_node.h b/sync/internal_api/write_node.h new file mode 100644 index 0000000..a070a5c --- /dev/null +++ b/sync/internal_api/write_node.h @@ -0,0 +1,190 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_WRITE_NODE_H_ +#define SYNC_INTERNAL_API_WRITE_NODE_H_ +#pragma once + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "sync/internal_api/base_node.h" +#include "sync/syncable/model_type.h" + +namespace browser_sync { +class Cryptographer; +class TestBookmarkModelAssociator; +} + +namespace syncable { +class Entry; +class MutableEntry; +} + +namespace sync_pb { +class AppSpecifics; +class AutofillSpecifics; +class AutofillProfileSpecifics; +class BookmarkSpecifics; +class EntitySpecifics; +class ExtensionSpecifics; +class SessionSpecifics; +class NigoriSpecifics; +class PasswordSpecificsData; +class ThemeSpecifics; +class TypedUrlSpecifics; +} + +namespace sync_api { + +class WriteTransaction; + +// WriteNode extends BaseNode to add mutation, and wraps +// syncable::MutableEntry. A WriteTransaction is needed to create a WriteNode. +class WriteNode : public BaseNode { + public: + // Create a WriteNode using the given transaction. + explicit WriteNode(WriteTransaction* transaction); + virtual ~WriteNode(); + + // A client must use one (and only one) of the following Init variants to + // populate the node. + + // BaseNode implementation. + virtual InitByLookupResult InitByIdLookup(int64 id) OVERRIDE; + virtual InitByLookupResult InitByClientTagLookup( + syncable::ModelType model_type, + const std::string& tag) OVERRIDE; + + // Create a new node with the specified parent and predecessor. |model_type| + // dictates the type of the item, and controls which EntitySpecifics proto + // extension can be used with this item. Use a NULL |predecessor| + // to indicate that this is to be the first child. + // |predecessor| must be a child of |new_parent| or NULL. Returns false on + // failure. + bool InitByCreation(syncable::ModelType model_type, + const BaseNode& parent, + const BaseNode* predecessor); + + // Create nodes using this function if they're unique items that + // you want to fetch using client_tag. Note that the behavior of these + // items is slightly different than that of normal items. + // Most importantly, if it exists locally, this function will + // actually undelete it + // Client unique tagged nodes must NOT be folders. + bool InitUniqueByCreation(syncable::ModelType model_type, + const BaseNode& parent, + const std::string& client_tag); + + // Each server-created permanent node is tagged with a unique string. + // Look up the node with the particular tag. If it does not exist, + // return false. + InitByLookupResult InitByTagLookup(const std::string& tag); + + // These Set() functions correspond to the Get() functions of BaseNode. + void SetIsFolder(bool folder); + void SetTitle(const std::wstring& title); + + // External ID is a client-only field, so setting it doesn't cause the item to + // be synced again. + void SetExternalId(int64 external_id); + + // Remove this node and its children. + void Remove(); + + // Set a new parent and position. Position is specified by |predecessor|; if + // it is NULL, the node is moved to the first position. |predecessor| must + // be a child of |new_parent| or NULL. Returns false on failure.. + bool SetPosition(const BaseNode& new_parent, const BaseNode* predecessor); + + // Set the bookmark specifics (url and favicon). + // Should only be called if GetModelType() == BOOKMARK. + void SetBookmarkSpecifics(const sync_pb::BookmarkSpecifics& specifics); + + // Legacy, bookmark-specific setters that wrap SetBookmarkSpecifics() above. + // Should only be called if GetModelType() == BOOKMARK. + // TODO(ncarter): Remove these two datatype-specific accessors. + void SetURL(const GURL& url); + void SetFaviconBytes(const std::vector<unsigned char>& bytes); + + // Generic set specifics method. Will extract the model type from |specifics|. + void SetEntitySpecifics(const sync_pb::EntitySpecifics& specifics); + + // Resets the EntitySpecifics for this node based on the unencrypted data. + // Will encrypt if necessary. + void ResetFromSpecifics(); + + // TODO(sync): Remove the setters below when the corresponding data + // types are ported to the new sync service API. + + // Set the app specifics (id, update url, enabled state, etc). + // Should only be called if GetModelType() == APPS. + void SetAppSpecifics(const sync_pb::AppSpecifics& specifics); + + // Set the autofill specifics (name and value). + // Should only be called if GetModelType() == AUTOFILL. + void SetAutofillSpecifics(const sync_pb::AutofillSpecifics& specifics); + + void SetAutofillProfileSpecifics( + const sync_pb::AutofillProfileSpecifics& specifics); + + // Set the nigori specifics. + // Should only be called if GetModelType() == NIGORI. + void SetNigoriSpecifics(const sync_pb::NigoriSpecifics& specifics); + + // Set the password specifics. + // Should only be called if GetModelType() == PASSWORD. + void SetPasswordSpecifics(const sync_pb::PasswordSpecificsData& specifics); + + // Set the theme specifics (name and value). + // Should only be called if GetModelType() == THEME. + void SetThemeSpecifics(const sync_pb::ThemeSpecifics& specifics); + + // Set the typed_url specifics (url, title, typed_count, etc). + // Should only be called if GetModelType() == TYPED_URLS. + void SetTypedUrlSpecifics(const sync_pb::TypedUrlSpecifics& specifics); + + // Set the extension specifics (id, update url, enabled state, etc). + // Should only be called if GetModelType() == EXTENSIONS. + void SetExtensionSpecifics(const sync_pb::ExtensionSpecifics& specifics); + + // Set the session specifics (windows, tabs, navigations etc.). + // Should only be called if GetModelType() == SESSIONS. + void SetSessionSpecifics(const sync_pb::SessionSpecifics& specifics); + + // Implementation of BaseNode's abstract virtual accessors. + virtual const syncable::Entry* GetEntry() const OVERRIDE; + + virtual const BaseTransaction* GetTransaction() const OVERRIDE; + + private: + friend class browser_sync::TestBookmarkModelAssociator; + FRIEND_TEST_ALL_PREFIXES(SyncManagerTest, EncryptBookmarksWithLegacyData); + + void* operator new(size_t size); // Node is meant for stack use only. + + // Helper to set model type. This will clear any specifics data. + void PutModelType(syncable::ModelType model_type); + + // Helper to set the previous node. + bool PutPredecessor(const BaseNode* predecessor) WARN_UNUSED_RESULT; + + // Sets IS_UNSYNCED and SYNCING to ensure this entry is considered in an + // upcoming commit pass. + void MarkForSyncing(); + + // The underlying syncable object which this class wraps. + syncable::MutableEntry* entry_; + + // The sync API transaction that is the parent of this node. + WriteTransaction* transaction_; + + DISALLOW_COPY_AND_ASSIGN(WriteNode); +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_WRITE_NODE_H_ diff --git a/sync/internal_api/write_transaction.cc b/sync/internal_api/write_transaction.cc new file mode 100644 index 0000000..b141b8c --- /dev/null +++ b/sync/internal_api/write_transaction.cc @@ -0,0 +1,29 @@ +// Copyright (c) 2012 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 "sync/internal_api/write_transaction.h" + +#include "sync/syncable/syncable.h" + +namespace sync_api { + +////////////////////////////////////////////////////////////////////////// +// WriteTransaction member definitions +WriteTransaction::WriteTransaction(const tracked_objects::Location& from_here, + UserShare* share) + : BaseTransaction(share), + transaction_(NULL) { + transaction_ = new syncable::WriteTransaction(from_here, syncable::SYNCAPI, + share->directory.get()); +} + +WriteTransaction::~WriteTransaction() { + delete transaction_; +} + +syncable::BaseTransaction* WriteTransaction::GetWrappedTrans() const { + return transaction_; +} + +} // namespace sync_api diff --git a/sync/internal_api/write_transaction.h b/sync/internal_api/write_transaction.h new file mode 100644 index 0000000..4364cdd1 --- /dev/null +++ b/sync/internal_api/write_transaction.h @@ -0,0 +1,57 @@ +// Copyright (c) 2012 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. + +#ifndef SYNC_INTERNAL_API_WRITE_TRANSACTION_H_ +#define SYNC_INTERNAL_API_WRITE_TRANSACTION_H_ + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "sync/internal_api/base_transaction.h" + +namespace syncable { +class BaseTransaction; +class WriteTransaction; +} // namespace syncable + +namespace tracked_objects { +class Location; +} // namespace tracked_objects + +namespace sync_api { + +// Sync API's WriteTransaction is a read/write BaseTransaction. It wraps +// a syncable::WriteTransaction. +// +// NOTE: Only a single model type can be mutated for a given +// WriteTransaction. +class WriteTransaction : public BaseTransaction { + public: + // Start a new read/write transaction. + WriteTransaction(const tracked_objects::Location& from_here, + UserShare* share); + virtual ~WriteTransaction(); + + // Provide access to the syncable.h transaction from the API WriteNode. + virtual syncable::BaseTransaction* GetWrappedTrans() const OVERRIDE; + syncable::WriteTransaction* GetWrappedWriteTrans() { return transaction_; } + + protected: + WriteTransaction() {} + + void SetTransaction(syncable::WriteTransaction* trans) { + transaction_ = trans; + } + + private: + void* operator new(size_t size); // Transaction is meant for stack use only. + + // The underlying syncable object which this class wraps. + syncable::WriteTransaction* transaction_; + + DISALLOW_COPY_AND_ASSIGN(WriteTransaction); +}; + +} // namespace sync_api + +#endif // SYNC_INTERNAL_API_WRITE_TRANSACTION_H_ |