diff options
Diffstat (limited to 'chrome/browser/sync/engine/syncapi.cc')
-rw-r--r-- | chrome/browser/sync/engine/syncapi.cc | 2142 |
1 files changed, 2142 insertions, 0 deletions
diff --git a/chrome/browser/sync/engine/syncapi.cc b/chrome/browser/sync/engine/syncapi.cc new file mode 100644 index 0000000..d9c5a9f --- /dev/null +++ b/chrome/browser/sync/engine/syncapi.cc @@ -0,0 +1,2142 @@ +// Copyright (c) 2010 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 "chrome/browser/sync/engine/syncapi.h" + +#include "build/build_config.h" + +#include <iomanip> +#include <list> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/base64.h" +#include "base/lock.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "base/platform_thread.h" +#include "base/scoped_ptr.h" +#include "base/sha1.h" +#include "base/string_util.h" +#include "base/task.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/sync/sync_constants.h" +#include "chrome/browser/sync/engine/all_status.h" +#include "chrome/browser/sync/engine/auth_watcher.h" +#include "chrome/browser/sync/engine/change_reorder_buffer.h" +#include "chrome/browser/sync/engine/model_safe_worker.h" +#include "chrome/browser/sync/engine/net/server_connection_manager.h" +#include "chrome/browser/sync/engine/net/syncapi_server_connection_manager.h" +#include "chrome/browser/sync/engine/syncer.h" +#include "chrome/browser/sync/engine/syncer_thread.h" +#include "chrome/browser/sync/notifier/server_notifier_thread.h" +#include "chrome/browser/sync/protocol/autofill_specifics.pb.h" +#include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" +#include "chrome/browser/sync/protocol/extension_specifics.pb.h" +#include "chrome/browser/sync/protocol/nigori_specifics.pb.h" +#include "chrome/browser/sync/protocol/password_specifics.pb.h" +#include "chrome/browser/sync/protocol/preference_specifics.pb.h" +#include "chrome/browser/sync/protocol/service_constants.h" +#include "chrome/browser/sync/protocol/sync.pb.h" +#include "chrome/browser/sync/protocol/theme_specifics.pb.h" +#include "chrome/browser/sync/protocol/typed_url_specifics.pb.h" +#include "chrome/browser/sync/sessions/sync_session_context.h" +#include "chrome/browser/sync/syncable/directory_manager.h" +#include "chrome/browser/sync/syncable/syncable.h" +#include "chrome/browser/sync/util/crypto_helpers.h" +#include "chrome/browser/sync/util/user_settings.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/deprecated/event_sys.h" +#include "chrome/common/net/gaia/gaia_authenticator.h" +#include "jingle/notifier/listener/mediator_thread_impl.h" +#include "jingle/notifier/listener/notification_constants.h" +#include "jingle/notifier/listener/talk_mediator.h" +#include "jingle/notifier/listener/talk_mediator_impl.h" +#include "net/base/network_change_notifier.h" + +using browser_sync::AllStatus; +using browser_sync::AllStatusEvent; +using browser_sync::AuthWatcher; +using browser_sync::AuthWatcherEvent; +using browser_sync::Cryptographer; +using browser_sync::KeyParams; +using browser_sync::ModelSafeRoutingInfo; +using browser_sync::ModelSafeWorker; +using browser_sync::ModelSafeWorkerRegistrar; +using browser_sync::Syncer; +using browser_sync::SyncerEvent; +using browser_sync::SyncerThread; +using browser_sync::UserSettings; +using browser_sync::kNigoriTag; +using browser_sync::sessions::SyncSessionContext; +using notifier::TalkMediator; +using notifier::TalkMediatorImpl; +using std::list; +using std::hex; +using std::string; +using std::vector; +using syncable::Directory; +using syncable::DirectoryManager; +using syncable::Entry; +using syncable::SPECIFICS; + +typedef GoogleServiceAuthError AuthError; + +static const int kThreadExitTimeoutMsec = 60000; +static const int kSSLPort = 443; + +// We manage the lifetime of sync_api::SyncManager::SyncInternal ourselves. +DISABLE_RUNNABLE_METHOD_REFCOUNT(sync_api::SyncManager::SyncInternal); + +namespace sync_api { + +static const FilePath::CharType kBookmarkSyncUserSettingsDatabase[] = + FILE_PATH_LITERAL("BookmarkSyncSettings.sqlite3"); +static const char kDefaultNameForNewNodes[] = " "; + +// The list of names which are reserved for use by the server. +static const char* kForbiddenServerNames[] = { "", ".", ".." }; + +////////////////////////////////////////////////////////////////////////// +// Static helper functions. + +// 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); +} + +// 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. +static 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; +} + +static bool EndsWithSpace(const std::string& string) { + return !string.empty() && *string.rbegin() == ' '; +} + +// 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. +static void SyncAPINameToServerName(const std::wstring& sync_api_name, + std::string* out) { + *out = WideToUTF8(sync_api_name); + if (IsNameServerIllegalAfterTrimming(*out)) + out->append(" "); +} + +// 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::wstring* out) { + int length_to_copy = server_name.length(); + if (IsNameServerIllegalAfterTrimming(server_name) && + EndsWithSpace(server_name)) + --length_to_copy; + if (!UTF8ToWide(server_name.c_str(), length_to_copy, out)) { + NOTREACHED() << "Could not convert server name from UTF8 to wide"; + } +} + +//////////////////////////////////// +// BaseNode member definitions. + +BaseNode::BaseNode() {} + +BaseNode::~BaseNode() {} + +std::string BaseNode::GenerateSyncableHash( + syncable::ModelType model_type, const std::string& client_tag) { + // blank PB with just the extension in it has termination symbol, + // handy for delimiter + sync_pb::EntitySpecifics serialized_type; + syncable::AddDefaultExtensionValue(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(Entry* entry) { + if (GetIsFolder()) return true; // Ignore the top-level password folder. + const sync_pb::EntitySpecifics& specifics = + entry->Get(syncable::SPECIFICS); + if (specifics.HasExtension(sync_pb::password)) { + const sync_pb::EncryptedData& encrypted = + specifics.GetExtension(sync_pb::password).encrypted(); + scoped_ptr<sync_pb::PasswordSpecificsData> data( + new sync_pb::PasswordSpecificsData); + if (!GetTransaction()->GetCryptographer()->Decrypt(encrypted, + data.get())) + return false; + password_data_.swap(data); + } + return true; +} + +int64 BaseNode::GetParentId() const { + return IdToMetahandle(GetTransaction()->GetWrappedTrans(), + GetEntry()->Get(syncable::PARENT_ID)); +} + +int64 BaseNode::GetId() const { + return GetEntry()->Get(syncable::META_HANDLE); +} + +bool BaseNode::GetIsFolder() const { + return GetEntry()->Get(syncable::IS_DIR); +} + +std::wstring BaseNode::GetTitle() const { + std::wstring result; + ServerNameToSyncAPIName(GetEntry()->Get(syncable::NON_UNIQUE_NAME), &result); + return result; +} + +GURL BaseNode::GetURL() const { + return GURL(GetBookmarkSpecifics().url()); +} + +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()->GetLookup(); + syncable::BaseTransaction* trans = GetTransaction()->GetWrappedTrans(); + syncable::Id id_string = + dir->GetFirstChildId(trans, GetEntry()->Get(syncable::ID)); + if (id_string.IsRoot()) + return kInvalidId; + return IdToMetahandle(GetTransaction()->GetWrappedTrans(), id_string); +} + +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::AutofillSpecifics& BaseNode::GetAutofillSpecifics() const { + DCHECK(GetModelType() == syncable::AUTOFILL); + return GetEntry()->Get(SPECIFICS).GetExtension(sync_pb::autofill); +} + +const sync_pb::BookmarkSpecifics& BaseNode::GetBookmarkSpecifics() const { + DCHECK(GetModelType() == syncable::BOOKMARKS); + return GetEntry()->Get(SPECIFICS).GetExtension(sync_pb::bookmark); +} + +const sync_pb::NigoriSpecifics& BaseNode::GetNigoriSpecifics() const { + DCHECK(GetModelType() == syncable::NIGORI); + return GetEntry()->Get(SPECIFICS).GetExtension(sync_pb::nigori); +} + +const sync_pb::PasswordSpecificsData& BaseNode::GetPasswordSpecifics() const { + DCHECK(GetModelType() == syncable::PASSWORDS); + DCHECK(password_data_.get()); + return *password_data_; +} + +const sync_pb::PreferenceSpecifics& BaseNode::GetPreferenceSpecifics() const { + DCHECK(GetModelType() == syncable::PREFERENCES); + return GetEntry()->Get(SPECIFICS).GetExtension(sync_pb::preference); +} + +const sync_pb::ThemeSpecifics& BaseNode::GetThemeSpecifics() const { + DCHECK(GetModelType() == syncable::THEMES); + return GetEntry()->Get(SPECIFICS).GetExtension(sync_pb::theme); +} + +const sync_pb::TypedUrlSpecifics& BaseNode::GetTypedUrlSpecifics() const { + DCHECK(GetModelType() == syncable::TYPED_URLS); + return GetEntry()->Get(SPECIFICS).GetExtension(sync_pb::typed_url); +} + +const sync_pb::ExtensionSpecifics& BaseNode::GetExtensionSpecifics() const { + DCHECK(GetModelType() == syncable::EXTENSIONS); + return GetEntry()->Get(SPECIFICS).GetExtension(sync_pb::extension); +} + +syncable::ModelType BaseNode::GetModelType() const { + return GetEntry()->GetModelType(); +} + +//////////////////////////////////// +// WriteNode member definitions +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) { + std::string server_legal_name; + SyncAPINameToServerName(title, &server_legal_name); + + string old_name = entry_->Get(syncable::NON_UNIQUE_NAME); + + if (server_legal_name == old_name) + return; // Skip redundant changes. + + entry_->Put(syncable::NON_UNIQUE_NAME, server_legal_name); + MarkForSyncing(); +} + +void WriteNode::SetURL(const GURL& url) { + sync_pb::BookmarkSpecifics new_value = GetBookmarkSpecifics(); + new_value.set_url(url.spec()); + SetBookmarkSpecifics(new_value); +} + +void WriteNode::SetAutofillSpecifics( + const sync_pb::AutofillSpecifics& new_value) { + DCHECK(GetModelType() == syncable::AUTOFILL); + PutAutofillSpecificsAndMarkForSyncing(new_value); +} + +void WriteNode::PutAutofillSpecificsAndMarkForSyncing( + const sync_pb::AutofillSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.MutableExtension(sync_pb::autofill)->CopyFrom(new_value); + PutSpecificsAndMarkForSyncing(entity_specifics); +} + +void WriteNode::SetBookmarkSpecifics( + const sync_pb::BookmarkSpecifics& new_value) { + DCHECK(GetModelType() == syncable::BOOKMARKS); + PutBookmarkSpecificsAndMarkForSyncing(new_value); +} + +void WriteNode::PutBookmarkSpecificsAndMarkForSyncing( + const sync_pb::BookmarkSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.MutableExtension(sync_pb::bookmark)->CopyFrom(new_value); + PutSpecificsAndMarkForSyncing(entity_specifics); +} + +void WriteNode::SetNigoriSpecifics( + const sync_pb::NigoriSpecifics& new_value) { + DCHECK(GetModelType() == syncable::NIGORI); + PutNigoriSpecificsAndMarkForSyncing(new_value); +} + +void WriteNode::PutNigoriSpecificsAndMarkForSyncing( + const sync_pb::NigoriSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.MutableExtension(sync_pb::nigori)->CopyFrom(new_value); + PutSpecificsAndMarkForSyncing(entity_specifics); +} + +void WriteNode::SetPasswordSpecifics( + const sync_pb::PasswordSpecificsData& data) { + DCHECK(GetModelType() == syncable::PASSWORDS); + std::string serialized_data; + data.SerializeToString(&serialized_data); + sync_pb::PasswordSpecifics new_value; + if (!GetTransaction()->GetCryptographer()->Encrypt( + data, + new_value.mutable_encrypted())) + NOTREACHED(); + + PutPasswordSpecificsAndMarkForSyncing(new_value); +} + +void WriteNode::SetPreferenceSpecifics( + const sync_pb::PreferenceSpecifics& new_value) { + DCHECK(GetModelType() == syncable::PREFERENCES); + PutPreferenceSpecificsAndMarkForSyncing(new_value); +} + +void WriteNode::SetThemeSpecifics( + const sync_pb::ThemeSpecifics& new_value) { + DCHECK(GetModelType() == syncable::THEMES); + PutThemeSpecificsAndMarkForSyncing(new_value); +} + +void WriteNode::PutPasswordSpecificsAndMarkForSyncing( + const sync_pb::PasswordSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.MutableExtension(sync_pb::password)->CopyFrom(new_value); + PutSpecificsAndMarkForSyncing(entity_specifics); +} + +void WriteNode::PutPreferenceSpecificsAndMarkForSyncing( + const sync_pb::PreferenceSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.MutableExtension(sync_pb::preference)->CopyFrom(new_value); + PutSpecificsAndMarkForSyncing(entity_specifics); +} + +void WriteNode::SetTypedUrlSpecifics( + const sync_pb::TypedUrlSpecifics& new_value) { + DCHECK(GetModelType() == syncable::TYPED_URLS); + PutTypedUrlSpecificsAndMarkForSyncing(new_value); +} + +void WriteNode::SetExtensionSpecifics( + const sync_pb::ExtensionSpecifics& new_value) { + DCHECK(GetModelType() == syncable::EXTENSIONS); + PutExtensionSpecificsAndMarkForSyncing(new_value); +} + +void WriteNode::PutThemeSpecificsAndMarkForSyncing( + const sync_pb::ThemeSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.MutableExtension(sync_pb::theme)->CopyFrom(new_value); + PutSpecificsAndMarkForSyncing(entity_specifics); +} + +void WriteNode::PutTypedUrlSpecificsAndMarkForSyncing( + const sync_pb::TypedUrlSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.MutableExtension(sync_pb::typed_url)->CopyFrom(new_value); + PutSpecificsAndMarkForSyncing(entity_specifics); +} + +void WriteNode::PutExtensionSpecificsAndMarkForSyncing( + const sync_pb::ExtensionSpecifics& new_value) { + sync_pb::EntitySpecifics entity_specifics; + entity_specifics.MutableExtension(sync_pb::extension)->CopyFrom(new_value); + PutSpecificsAndMarkForSyncing(entity_specifics); +} + +void WriteNode::PutSpecificsAndMarkForSyncing( + const sync_pb::EntitySpecifics& specifics) { + // Skip redundant changes. + if (specifics.SerializeAsString() == + entry_->Get(SPECIFICS).SerializeAsString()) { + return; + } + entry_->Put(SPECIFICS, specifics); + MarkForSyncing(); +} + +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. +bool WriteNode::InitByIdLookup(int64 id) { + DCHECK(!entry_) << "Init called twice"; + DCHECK_NE(id, kInvalidId); + entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), + syncable::GET_BY_HANDLE, id); + return (entry_->good() && !entry_->Get(syncable::IS_DEL) && + DecryptIfNecessary(entry_)); +} + +// 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. +bool WriteNode::InitByClientTagLookup(syncable::ModelType model_type, + const std::string& tag) { + DCHECK(!entry_) << "Init called twice"; + if (tag.empty()) + return false; + + const std::string hash = GenerateSyncableHash(model_type, tag); + + entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), + syncable::GET_BY_CLIENT_TAG, hash); + return (entry_->good() && !entry_->Get(syncable::IS_DEL) && + DecryptIfNecessary(entry_)); +} + +bool WriteNode::InitByTagLookup(const std::string& tag) { + DCHECK(!entry_) << "Init called twice"; + if (tag.empty()) + return false; + entry_ = new syncable::MutableEntry(transaction_->GetWrappedWriteTrans(), + syncable::GET_BY_SERVER_TAG, tag); + if (!entry_->good()) + return false; + if (entry_->Get(syncable::IS_DEL)) + return false; + syncable::ModelType model_type = GetModelType(); + DCHECK(model_type == syncable::NIGORI); + return true; +} + +void WriteNode::PutModelType(syncable::ModelType model_type) { + // Set an empty specifics of the appropriate datatype. The presence + // of the specific extension will identify the model type. + DCHECK(GetModelType() == model_type || + GetModelType() == syncable::UNSPECIFIED); // Immutable once set. + + sync_pb::EntitySpecifics specifics; + syncable::AddDefaultExtensionValue(model_type, &specifics); + PutSpecificsAndMarkForSyncing(specifics); + DCHECK(GetModelType() == model_type); +} + +// 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. + PutPredecessor(predecessor); + + return true; +} + +// 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"; + + 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. + PutPredecessor(NULL); + + return true; +} + +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. + PutPredecessor(predecessor); + + return true; +} + +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(); +} + +void WriteNode::PutPredecessor(const BaseNode* predecessor) { + syncable::Id predecessor_id = predecessor ? + predecessor->GetEntry()->Get(syncable::ID) : syncable::Id(); + entry_->PutPredecessor(predecessor_id); + // Mark this entry as unsynced, to wake up the syncer. + MarkForSyncing(); +} + +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_); +} + +////////////////////////////////////////////////////////////////////////// +// ReadNode member definitions +ReadNode::ReadNode(const BaseTransaction* transaction) + : entry_(NULL), transaction_(transaction) { + DCHECK(transaction); +} + +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."; +} + +bool 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 false; + if (entry_->Get(syncable::IS_DEL)) + return false; + 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(entry_); +} + +bool ReadNode::InitByClientTagLookup(syncable::ModelType model_type, + const std::string& tag) { + DCHECK(!entry_) << "Init called twice"; + if (tag.empty()) + return false; + + const std::string hash = GenerateSyncableHash(model_type, tag); + + entry_ = new syncable::Entry(transaction_->GetWrappedTrans(), + syncable::GET_BY_CLIENT_TAG, hash); + return (entry_->good() && !entry_->Get(syncable::IS_DEL) && + DecryptIfNecessary(entry_)); +} + +const syncable::Entry* ReadNode::GetEntry() const { + return entry_; +} + +const BaseTransaction* ReadNode::GetTransaction() const { + return transaction_; +} + +bool ReadNode::InitByTagLookup(const std::string& tag) { + DCHECK(!entry_) << "Init called twice"; + if (tag.empty()) + return false; + syncable::BaseTransaction* trans = transaction_->GetWrappedTrans(); + entry_ = new syncable::Entry(trans, syncable::GET_BY_SERVER_TAG, tag); + if (!entry_->good()) + return false; + if (entry_->Get(syncable::IS_DEL)) + return false; + 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(entry_); +} + +////////////////////////////////////////////////////////////////////////// +// ReadTransaction member definitions +ReadTransaction::ReadTransaction(UserShare* share) + : BaseTransaction(share), + transaction_(NULL), + close_transaction_(true) { + transaction_ = new syncable::ReadTransaction(GetLookup(), __FILE__, __LINE__); +} + +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_; +} + +////////////////////////////////////////////////////////////////////////// +// WriteTransaction member definitions +WriteTransaction::WriteTransaction(UserShare* share) + : BaseTransaction(share), + transaction_(NULL) { + transaction_ = new syncable::WriteTransaction(GetLookup(), syncable::SYNCAPI, + __FILE__, __LINE__); +} + +WriteTransaction::~WriteTransaction() { + delete transaction_; +} + +syncable::BaseTransaction* WriteTransaction::GetWrappedTrans() const { + return transaction_; +} + +// A GaiaAuthenticator that uses HttpPostProviders instead of CURL. +class BridgedGaiaAuthenticator : public gaia::GaiaAuthenticator { + public: + BridgedGaiaAuthenticator(const string& user_agent, const string& service_id, + const string& gaia_url, + HttpPostProviderFactory* factory) + : GaiaAuthenticator(user_agent, service_id, gaia_url), + gaia_source_(user_agent), post_factory_(factory) { + } + + virtual ~BridgedGaiaAuthenticator() { + } + + virtual bool Post(const GURL& url, const string& post_body, + unsigned long* response_code, string* response_body) { + string connection_url = "https://"; + connection_url += url.host(); + connection_url += url.path(); + HttpPostProviderInterface* http = post_factory_->Create(); + http->SetUserAgent(gaia_source_.c_str()); + // SSL is on 443 for Gaia Posts always. + http->SetURL(connection_url.c_str(), kSSLPort); + http->SetPostPayload("application/x-www-form-urlencoded", + post_body.length(), post_body.c_str()); + + int os_error_code = 0; + int int_response_code = 0; + if (!http->MakeSynchronousPost(&os_error_code, &int_response_code)) { + LOG(INFO) << "Http POST failed, error returns: " << os_error_code; + return false; + } + *response_code = static_cast<int>(int_response_code); + response_body->assign(http->GetResponseContent(), + http->GetResponseContentLength()); + post_factory_->Destroy(http); + return true; + } + + virtual int GetBackoffDelaySeconds(int current_backoff_delay) { + return AllStatus::GetRecommendedDelaySeconds(current_backoff_delay); + } + private: + const std::string gaia_source_; + scoped_ptr<HttpPostProviderFactory> post_factory_; + DISALLOW_COPY_AND_ASSIGN(BridgedGaiaAuthenticator); +}; + +////////////////////////////////////////////////////////////////////////// +// SyncManager's implementation: SyncManager::SyncInternal +class SyncManager::SyncInternal + : public net::NetworkChangeNotifier::Observer, + public TalkMediator::Delegate, + public browser_sync::ChannelEventHandler<syncable::DirectoryChangeEvent>, + public browser_sync::ChannelEventHandler<SyncerEvent>{ + static const int kDefaultNudgeDelayMilliseconds; + static const int kPreferencesNudgeDelayMilliseconds; + public: + explicit SyncInternal(SyncManager* sync_manager) + : core_message_loop_(NULL), + observer_(NULL), + auth_problem_(AuthError::NONE), + sync_manager_(sync_manager), + registrar_(NULL), + notification_pending_(false), + initialized_(false), + use_chrome_async_socket_(false), + notification_method_(browser_sync::kDefaultNotificationMethod) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + } + + ~SyncInternal() { + DCHECK(!core_message_loop_); + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + } + + bool Init(const FilePath& database_location, + const std::string& sync_server_and_path, + int port, + const char* gaia_service_id, + const char* gaia_source, + bool use_ssl, + HttpPostProviderFactory* post_factory, + HttpPostProviderFactory* auth_post_factory, + ModelSafeWorkerRegistrar* model_safe_worker_registrar, + bool attempt_last_user_authentication, + bool invalidate_last_user_auth_token, + bool invalidate_xmpp_auth_token, + const char* user_agent, + const std::string& lsid, + const bool use_chrome_async_socket, + browser_sync::NotificationMethod notification_method); + + // Tell sync engine to submit credentials to GAIA for verification. + // Successful GAIA authentication will kick off the following chain of events: + // 1. Cause sync engine to open the syncer database. + // 2. Trigger the AuthWatcher to create a Syncer for the directory and call + // SyncerThread::SyncDirectory; the SyncerThread will block until (4). + // 3. Tell the ServerConnectionManager to pass the newly received GAIA auth + // token to a sync server to obtain a sync token. + // 4. On receipt of this token, the ServerConnectionManager broadcasts + // a server-reachable event, which will unblock the SyncerThread. + // 5. When StartSyncing is called, the Syncer will begin the sync process, by + // downloading from or uploading to the server. + // + // If authentication fails, an event will be broadcast all the way up to + // the SyncManager::Observer. It may, in turn, decide to try again with new + // credentials. Calling this method again is the appropriate course of action + // to "retry". + void Authenticate(const std::string& username, const std::string& password, + const std::string& captcha); + + // Tell the sync engine to start the syncing process. + void StartSyncing(); + + void SetPassphrase(const std::string& passphrase); + + // Call periodically from a database-safe thread to persist recent changes + // to the syncapi model. + void SaveChanges(); + + // 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 HandleChannelEvent(const syncable::DirectoryChangeEvent& event); + void HandleTransactionEndingChangeEvent( + const syncable::DirectoryChangeEvent& event); + void HandleCalculateChangesChangeEventFromSyncApi( + const syncable::DirectoryChangeEvent& event); + void HandleCalculateChangesChangeEventFromSyncer( + const syncable::DirectoryChangeEvent& event); + + // This listener is called by the syncer channel for all syncer events. + virtual void HandleChannelEvent(const SyncerEvent& event); + + // We have a direct hookup to the authwatcher to be notified for auth failures + // on startup, to serve our UI needs. + void HandleAuthWatcherEvent(const AuthWatcherEvent& event); + + // Listen here for directory opened events. + void HandleDirectoryManagerEvent( + const syncable::DirectoryManagerEvent& event); + + // Login to the talk mediator with the given credentials. + void TalkMediatorLogin( + const std::string& email, const std::string& token); + + // TalkMediator::Delegate implementation. + + virtual void OnNotificationStateChange( + bool notifications_enabled); + + virtual void OnIncomingNotification( + const IncomingNotificationData& notification_data); + + virtual void OnOutgoingNotification(); + + // Accessors for the private members. + DirectoryManager* dir_manager() { return share_.dir_manager.get(); } + SyncAPIServerConnectionManager* connection_manager() { + return connection_manager_.get(); + } + SyncerThread* syncer_thread() { return syncer_thread_.get(); } + TalkMediator* talk_mediator() { return talk_mediator_.get(); } + AuthWatcher* auth_watcher() { return auth_watcher_.get(); } + void set_observer(SyncManager::Observer* observer) { observer_ = observer; } + UserShare* GetUserShare() { return &share_; } + + // Return the currently active (validated) username for use with syncable + // types. + const std::string& username_for_share() const { + return share_.authenticated_name; + } + + // Note about SyncManager::Status implementation: Status is a trimmed + // down AllStatus::Status, augmented with authentication failure information + // gathered from the internal AuthWatcher. The sync UI itself hooks up to + // various sources like the AuthWatcher individually, but with syncapi we try + // to keep everything status-related in one place. This means we have to + // privately manage state about authentication failures, and whenever the + // status or status summary is requested we aggregate this state with + // AllStatus::Status information. + Status ComputeAggregatedStatus(); + Status::Summary ComputeAggregatedStatusSummary(); + + // See SyncManager::SetupForTestMode for information. + void SetupForTestMode(const std::wstring& test_username); + + // See SyncManager::Shutdown for information. + void Shutdown(); + + // Whether we're initialized to the point of being able to accept changes + // (and hence allow transaction creation). See initialized_ for details. + bool initialized() const { + AutoLock lock(initialized_mutex_); + return initialized_; + } + + void SetExtraChangeRecordData(int64 id, + syncable::ModelType type, + ChangeReorderBuffer* buffer, + const syncable::EntryKernel& original, + bool existed_before, + bool exists_now); + + // Called only by our NetworkChangeNotifier. + virtual void OnIPAddressChanged(); + + private: + // Try to authenticate using a LSID cookie. + void AuthenticateWithLsid(const std::string& lsid); + + // Try to authenticate using persisted credentials from a previous successful + // authentication. If no such credentials exist, calls OnAuthError on the + // client to collect credentials. Otherwise, there exist local credentials + // that were once used for a successful auth, so we'll try to re-use these. + // Failure of that attempt will be communicated as normal using OnAuthError. + // Since this entry point will bypass normal GAIA authentication and try to + // authenticate directly with the sync service using a cached token, + // authentication failure will generally occur due to expired credentials, or + // possibly because of a password change. + bool AuthenticateForUser(const std::string& username, + const std::string& auth_token); + + bool InitialSyncEndedForAllEnabledTypes() { + syncable::ScopedDirLookup lookup(dir_manager(), username_for_share()); + if (!lookup.good()) { + DCHECK(false) << "ScopedDirLookup failed when checking initial sync"; + return false; + } + + ModelSafeRoutingInfo enabled_types; + registrar_->GetModelSafeRoutingInfo(&enabled_types); + for (ModelSafeRoutingInfo::const_iterator i = enabled_types.begin(); + i != enabled_types.end(); ++i) { + if (!lookup->initial_sync_ended_for_type(i->first)) + return false; + } + return true; + } + + // Helper to call OnAuthError when no authentication credentials are + // available. + void RaiseAuthNeededEvent(); + + // Helper to set initialized_ to true and raise an event to clients to notify + // that initialization is complete and it is safe to send us changes. If + // already initialized, this is a no-op. + void MarkAndNotifyInitializationComplete(); + + // If there's a pending notification to be sent, either from the + // new_pending_notification flag or a previous unsuccessfully sent + // notification, tries to send a notification. + void SendPendingXMPPNotification(bool new_pending_notification); + + // 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::EntryKernel& a, + const syncable::Entry& b) { + // 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 (!b.ShouldMaintainPosition()) + return false; + if (a.ref(syncable::NEXT_ID) != b.Get(syncable::NEXT_ID)) + return true; + if (a.ref(syncable::PARENT_ID) != b.Get(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::EntryKernel& a, + const syncable::Entry& b) { + syncable::ModelType model_type = b.GetModelType(); + // Suppress updates to items that aren't tracked by any browser model. + if (model_type == syncable::UNSPECIFIED || + model_type == syncable::TOP_LEVEL_FOLDER) { + return false; + } + if (a.ref(syncable::NON_UNIQUE_NAME) != b.Get(syncable::NON_UNIQUE_NAME)) + return true; + if (a.ref(syncable::IS_DIR) != b.Get(syncable::IS_DIR)) + return true; + if (a.ref(SPECIFICS).SerializeAsString() != + b.Get(SPECIFICS).SerializeAsString()) { + return true; + } + if (VisiblePositionsDiffer(a, b)) + 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; + } + + // We couple the DirectoryManager and username together in a UserShare member + // so we can return a handle to share_ to clients of the API for use when + // constructing any transaction type. + UserShare share_; + + // A wrapper around a sqlite store used for caching authentication data, + // last user information, current sync-related URLs, and more. + scoped_ptr<UserSettings> user_settings_; + + MessageLoop* core_message_loop_; + + // Observer registered via SetObserver/RemoveObserver. + // WARNING: This can be NULL! + SyncManager::Observer* observer_; + + // The ServerConnectionManager used to abstract communication between the + // client (the Syncer) and the sync server. + scoped_ptr<SyncAPIServerConnectionManager> connection_manager_; + + // The thread that runs the Syncer. Needs to be explicitly Start()ed. + scoped_refptr<SyncerThread> syncer_thread_; + + // Notification (xmpp) handler. + scoped_ptr<TalkMediator> talk_mediator_; + + // A multi-purpose status watch object that aggregates stats from various + // sync components. + AllStatus allstatus_; + + // AuthWatcher kicks off the authentication process and follows it through + // phase 1 (GAIA) to phase 2 (sync engine). As part of this work it determines + // the initial connectivity and causes the server connection event to be + // broadcast, which signals the syncer thread to start syncing. + // It has a heavy duty constructor requiring boilerplate so we heap allocate. + scoped_refptr<AuthWatcher> auth_watcher_; + + // 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. + ChangeReorderBuffer change_buffers_[syncable::MODEL_TYPE_COUNT]; + + // The event listener hookup that is registered for HandleChangeEvent. + scoped_ptr<browser_sync::ChannelHookup<syncable::DirectoryChangeEvent> > + dir_change_hookup_; + + // The event listener hookup registered for HandleSyncerEvent. + scoped_ptr<browser_sync::ChannelHookup<SyncerEvent> > syncer_event_; + + // The event listener hookup registered for HandleAuthWatcherEvent. + scoped_ptr<EventListenerHookup> authwatcher_hookup_; + + // The event listener hookup registered for the DirectoryManager (OPENED). + scoped_ptr<EventListenerHookup> directory_manager_hookup_; + + // Our cache of a recent authentication problem. If no authentication problem + // occurred, or if the last problem encountered has been cleared (by a + // subsequent AuthWatcherEvent), this is set to NONE. + AuthError::State auth_problem_; + + // The sync dir_manager to which we belong. + SyncManager* const sync_manager_; + + // The entity that provides us with information about which types to sync. + // The instance is shared between the SyncManager and the Syncer. + ModelSafeWorkerRegistrar* registrar_; + + // True if the next SyncCycle should notify peers of an update. + bool notification_pending_; + + // Set to true once Init has been called, and we know of an authenticated + // valid) username either from a fresh authentication attempt (as in + // first-use case) or from a previous attempt stored in our UserSettings + // (as in the steady-state), and the syncable::Directory has been opened, + // meaning we are ready to accept changes. Protected by initialized_mutex_ + // as it can get read/set by both the SyncerThread and the AuthWatcherThread. + bool initialized_; + mutable Lock initialized_mutex_; + + bool use_chrome_async_socket_; + browser_sync::NotificationMethod notification_method_; +}; +const int SyncManager::SyncInternal::kDefaultNudgeDelayMilliseconds = 200; +const int SyncManager::SyncInternal::kPreferencesNudgeDelayMilliseconds = 2000; + +SyncManager::SyncManager() { + data_ = new SyncInternal(this); +} + +bool SyncManager::Init(const FilePath& database_location, + const char* sync_server_and_path, + int sync_server_port, + const char* gaia_service_id, + const char* gaia_source, + bool use_ssl, + HttpPostProviderFactory* post_factory, + HttpPostProviderFactory* auth_post_factory, + ModelSafeWorkerRegistrar* registrar, + bool attempt_last_user_authentication, + bool invalidate_last_user_auth_token, + bool invalidate_xmpp_auth_token, + const char* user_agent, + const char* lsid, + bool use_chrome_async_socket, + browser_sync::NotificationMethod notification_method) { + DCHECK(post_factory); + LOG(INFO) << "SyncManager starting Init..."; + string server_string(sync_server_and_path); + return data_->Init(database_location, + server_string, + sync_server_port, + gaia_service_id, + gaia_source, + use_ssl, + post_factory, + auth_post_factory, + registrar, + attempt_last_user_authentication, + invalidate_last_user_auth_token, + invalidate_xmpp_auth_token, + user_agent, + lsid, + use_chrome_async_socket, + notification_method); +} + +void SyncManager::Authenticate(const char* username, const char* password, + const char* captcha) { + data_->Authenticate(std::string(username), std::string(password), + std::string(captcha)); +} + +void SyncManager::StartSyncing() { + data_->StartSyncing(); +} + +void SyncManager::SetPassphrase(const std::string& passphrase) { + data_->SetPassphrase(passphrase); +} + +bool SyncManager::RequestPause() { + return data_->syncer_thread()->RequestPause(); +} + +bool SyncManager::RequestResume() { + return data_->syncer_thread()->RequestResume(); +} + +void SyncManager::RequestNudge() { + data_->syncer_thread()->NudgeSyncer(0, SyncerThread::kLocal); +} + +const std::string& SyncManager::GetAuthenticatedUsername() { + DCHECK(data_); + return data_->username_for_share(); +} + +bool SyncManager::SyncInternal::Init( + const FilePath& database_location, + const std::string& sync_server_and_path, + int port, + const char* gaia_service_id, + const char* gaia_source, + bool use_ssl, + HttpPostProviderFactory* post_factory, + HttpPostProviderFactory* auth_post_factory, + ModelSafeWorkerRegistrar* model_safe_worker_registrar, + bool attempt_last_user_authentication, + bool invalidate_last_user_auth_token, + bool invalidate_xmpp_auth_token, + const char* user_agent, + const std::string& lsid, + bool use_chrome_async_socket, + browser_sync::NotificationMethod notification_method) { + + LOG(INFO) << "Starting SyncInternal initialization."; + + core_message_loop_ = MessageLoop::current(); + DCHECK(core_message_loop_); + notification_method_ = notification_method; + // Set up UserSettings, creating the db if necessary. We need this to + // instantiate a URLFactory to give to the Syncer. + FilePath settings_db_file = + database_location.Append(FilePath(kBookmarkSyncUserSettingsDatabase)); + user_settings_.reset(new UserSettings()); + if (!user_settings_->Init(settings_db_file)) + return false; + + registrar_ = model_safe_worker_registrar; + + LOG(INFO) << "Initialized sync user settings. Starting DirectoryManager."; + + share_.dir_manager.reset(new DirectoryManager(database_location)); + directory_manager_hookup_.reset(NewEventListenerHookup( + share_.dir_manager->channel(), this, + &SyncInternal::HandleDirectoryManagerEvent)); + + string client_id = user_settings_->GetClientId(); + connection_manager_.reset(new SyncAPIServerConnectionManager( + sync_server_and_path, port, use_ssl, user_agent, client_id, + post_factory)); + + // Watch various objects for aggregated status. + allstatus_.WatchConnectionManager(connection_manager()); + + net::NetworkChangeNotifier::AddObserver(this); + // TODO(akalin): CheckServerReachable() can block, which may cause jank if we + // try to shut down sync. Fix this. + connection_manager()->CheckServerReachable(); + + // NOTIFICATION_SERVER uses a substantially different notification method, so + // it has its own MediatorThread implementation. Everything else just uses + // MediatorThreadImpl. + notifier::MediatorThread* mediator_thread = + (notification_method == browser_sync::NOTIFICATION_SERVER) ? + new sync_notifier::ServerNotifierThread(use_chrome_async_socket) : + new notifier::MediatorThreadImpl(use_chrome_async_socket); + const bool kInitializeSsl = true; + const bool kConnectImmediately = false; + talk_mediator_.reset(new TalkMediatorImpl(mediator_thread, kInitializeSsl, + kConnectImmediately, invalidate_xmpp_auth_token)); + if (notification_method != browser_sync::NOTIFICATION_LEGACY && + notification_method != browser_sync::NOTIFICATION_SERVER) { + if (notification_method == browser_sync::NOTIFICATION_TRANSITIONAL) { + talk_mediator_->AddSubscribedServiceUrl( + browser_sync::kSyncLegacyServiceUrl); + } + talk_mediator_->AddSubscribedServiceUrl(browser_sync::kSyncServiceUrl); + } + + // Listen to TalkMediator events ourselves + talk_mediator_->SetDelegate(this); + + std::string gaia_url = gaia::kGaiaUrl; + const char* service_id = gaia_service_id ? + gaia_service_id : SYNC_SERVICE_NAME; + + BridgedGaiaAuthenticator* gaia_auth = new BridgedGaiaAuthenticator( + gaia_source, service_id, gaia_url, auth_post_factory); + + LOG(INFO) << "Sync is bringing up authwatcher and SyncSessionContext."; + + auth_watcher_ = new AuthWatcher(dir_manager(), + connection_manager(), + gaia_source, + service_id, + gaia_url, + user_settings_.get(), + gaia_auth); + + authwatcher_hookup_.reset(NewEventListenerHookup(auth_watcher_->channel(), + this, &SyncInternal::HandleAuthWatcherEvent)); + + // Build a SyncSessionContext and store the worker in it. + SyncSessionContext* context = new SyncSessionContext( + connection_manager_.get(), auth_watcher(), + dir_manager(), model_safe_worker_registrar); + + // The SyncerThread takes ownership of |context|. + syncer_thread_ = new SyncerThread(context, &allstatus_); + allstatus_.WatchSyncerThread(syncer_thread()); + + // Subscribe to the syncer thread's channel. + syncer_event_.reset(syncer_thread()->relay_channel()->AddObserver(this)); + + bool attempting_auth = false; + std::string username, auth_token; + if (attempt_last_user_authentication && + auth_watcher()->settings()->GetLastUserAndServiceToken( + SYNC_SERVICE_NAME, &username, &auth_token)) { + if (invalidate_last_user_auth_token) { + auth_token += "bogus"; + } + attempting_auth = AuthenticateForUser(username, auth_token); + } else if (!lsid.empty()) { + attempting_auth = true; + AuthenticateWithLsid(lsid); + } + if (attempt_last_user_authentication && !attempting_auth) + RaiseAuthNeededEvent(); + return true; +} + +void SyncManager::SyncInternal::StartSyncing() { + if (syncer_thread()) // NULL during certain unittests. + syncer_thread()->Start(); // Start the syncer thread. This won't actually + // result in any syncing until at least the + // DirectoryManager broadcasts the OPENED event, + // and a valid server connection is detected. +} + +void SyncManager::SyncInternal::MarkAndNotifyInitializationComplete() { + // There is only one real time we need this mutex. If we get an auth + // success, and before the initial sync ends we get an auth failure. In this + // case we'll be listening to both the AuthWatcher and Syncer, and it's a race + // between their respective threads to call MarkAndNotify. We need to make + // sure the observer is notified once and only once. + { + AutoLock lock(initialized_mutex_); + if (initialized_) + return; + initialized_ = true; + } + + // Notify that initialization is complete. + if (observer_) + observer_->OnInitializationComplete(); +} + +void SyncManager::SyncInternal::SendPendingXMPPNotification( + bool new_pending_notification) { + DCHECK_EQ(MessageLoop::current(), core_message_loop_); + DCHECK_NE(notification_method_, browser_sync::NOTIFICATION_SERVER); + notification_pending_ = notification_pending_ || new_pending_notification; + if (!notification_pending_) { + LOG(INFO) << "Not sending notification: no pending notification"; + return; + } + if (!talk_mediator_.get()) { + LOG(INFO) << "Not sending notification: shutting down " + << "(talk_mediator_ is NULL)"; + return; + } + LOG(INFO) << "Sending XMPP notification..."; + OutgoingNotificationData notification_data; + if (notification_method_ == browser_sync::NOTIFICATION_LEGACY) { + notification_data.service_id = browser_sync::kSyncLegacyServiceId; + notification_data.service_url = browser_sync::kSyncLegacyServiceUrl; + notification_data.send_content = false; + } else { + notification_data.service_id = browser_sync::kSyncServiceId; + notification_data.service_url = browser_sync::kSyncServiceUrl; + notification_data.send_content = true; + notification_data.priority = browser_sync::kSyncPriority; + notification_data.write_to_cache_only = true; + if (notification_method_ == browser_sync::NOTIFICATION_NEW) { + notification_data.service_specific_data = + browser_sync::kSyncServiceSpecificData; + notification_data.require_subscription = true; + } else { + notification_data.require_subscription = false; + } + } + bool success = talk_mediator_->SendNotification(notification_data); + if (success) { + notification_pending_ = false; + LOG(INFO) << "Sent XMPP notification"; + } else { + LOG(INFO) << "Could not send XMPP notification"; + } +} + +void SyncManager::SyncInternal::Authenticate(const std::string& username, + const std::string& password, + const std::string& captcha) { + DCHECK(username_for_share().empty() || username == username_for_share()) + << "Username change from valid username detected"; + if (allstatus_.status().authenticated) + return; + if (password.empty()) { + // TODO(timsteele): Seems like this shouldn't be needed, but auth_watcher + // currently drops blank password attempts on the floor and doesn't update + // state; it only LOGs an error in this case. We want to make sure we set + // our GoogleServiceAuthError state to denote an error. + RaiseAuthNeededEvent(); + } + auth_watcher()->Authenticate(username, password, std::string(), + captcha); +} + +void SyncManager::SyncInternal::AuthenticateWithLsid(const string& lsid) { + DCHECK(!lsid.empty()); + auth_watcher()->AuthenticateWithLsid(lsid); +} + +bool SyncManager::SyncInternal::AuthenticateForUser( + const std::string& username, const std::string& auth_token) { + share_.authenticated_name = username; + + // We optimize by opening the directory before the "fresh" authentication + // attempt completes so that we can immediately begin processing changes. + if (!dir_manager()->Open(username_for_share())) { + DCHECK(false) << "Had last known user but could not open directory"; + return false; + } + + // Load the last-known good auth token into the connection manager and send + // it off to the AuthWatcher for validation. The result of the validation + // will update the connection manager if necessary. + connection_manager()->set_auth_token(auth_token); + auth_watcher()->AuthenticateWithToken(username, auth_token); + return true; +} + +void SyncManager::SyncInternal::RaiseAuthNeededEvent() { + auth_problem_ = AuthError::INVALID_GAIA_CREDENTIALS; + if (observer_) + observer_->OnAuthError(AuthError(auth_problem_)); +} + +void SyncManager::SyncInternal::SetPassphrase( + const std::string& passphrase) { + Cryptographer* cryptographer = dir_manager()->cryptographer(); + KeyParams params = {"localhost", "dummy", passphrase}; + if (cryptographer->has_pending_keys()) { + if (!cryptographer->DecryptPendingKeys(params)) { + observer_->OnPassphraseRequired(); + return; + } + // Nudge the syncer so that passwords updates that were waiting for this + // passphrase get applied as soon as possible. + sync_manager_->RequestNudge(); + } else { + WriteTransaction trans(GetUserShare()); + WriteNode node(&trans); + if (!node.InitByTagLookup(kNigoriTag)) { + // TODO(albertb): Plumb an UnrecoverableError all the way back to the PSS. + NOTREACHED(); + return; + } + cryptographer->AddKey(params); + + sync_pb::NigoriSpecifics specifics; + cryptographer->GetKeys(specifics.mutable_encrypted()); + node.SetNigoriSpecifics(specifics); + } + observer_->OnPassphraseAccepted(); +} + +SyncManager::~SyncManager() { + delete data_; +} + +void SyncManager::SetObserver(Observer* observer) { + data_->set_observer(observer); +} + +void SyncManager::RemoveObserver() { + data_->set_observer(NULL); +} + +void SyncManager::Shutdown() { + data_->Shutdown(); +} + +void SyncManager::SyncInternal::Shutdown() { + // We NULL out talk_mediator_ so that any tasks pumped below do not + // trigger further XMPP actions. + // + // TODO(akalin): NULL the other member variables defensively, too. + scoped_ptr<TalkMediator> talk_mediator(talk_mediator_.release()); + + // First reset the AuthWatcher in case an auth attempt is in progress so that + // it terminates gracefully before we shutdown and close other components. + // Otherwise the attempt can complete after we've closed the directory, for + // example, and cause initialization to continue, which is bad. + if (auth_watcher_) { + auth_watcher_->Shutdown(); + auth_watcher_ = NULL; + authwatcher_hookup_.reset(); + } + + if (syncer_thread()) { + if (!syncer_thread()->Stop(kThreadExitTimeoutMsec)) { + LOG(FATAL) << "Unable to stop the syncer, it won't be happy..."; + } + syncer_event_.reset(); + syncer_thread_ = NULL; + } + + // Shutdown the xmpp buzz connection. + if (talk_mediator.get()) { + LOG(INFO) << "P2P: Mediator logout started."; + talk_mediator->Logout(); + LOG(INFO) << "P2P: Mediator logout completed."; + talk_mediator.reset(); + LOG(INFO) << "P2P: Mediator destroyed."; + } + + // Pump any messages the auth watcher, syncer thread, or talk + // mediator posted before they shut down. (See HandleSyncerEvent(), + // HandleAuthWatcherEvent(), and HandleTalkMediatorEvent() for the + // events that may be posted.) + { + CHECK(core_message_loop_); + bool old_state = core_message_loop_->NestableTasksAllowed(); + core_message_loop_->SetNestableTasksAllowed(true); + core_message_loop_->RunAllPending(); + core_message_loop_->SetNestableTasksAllowed(old_state); + } + + net::NetworkChangeNotifier::RemoveObserver(this); + + if (dir_manager()) { + dir_manager()->FinalSaveChangesForAll(); + dir_manager()->Close(username_for_share()); + } + + // Reset the DirectoryManager and UserSettings so they relinquish sqlite + // handles to backing files. + share_.dir_manager.reset(); + user_settings_.reset(); + + // We don't want to process any more events. + dir_change_hookup_.reset(); + + core_message_loop_ = NULL; +} + +void SyncManager::SyncInternal::OnIPAddressChanged() { + LOG(INFO) << "IP address change detected"; + // TODO(akalin): CheckServerReachable() can block, which may cause + // jank if we try to shut down sync. Fix this. + connection_manager()->CheckServerReachable(); +} + +void SyncManager::SyncInternal::HandleDirectoryManagerEvent( + const syncable::DirectoryManagerEvent& event) { + LOG(INFO) << "Sync internal handling a directory manager event"; + if (syncable::DirectoryManagerEvent::OPENED == event.what_happened) { + DCHECK(!initialized()) << "Should only happen once"; + MarkAndNotifyInitializationComplete(); + } +} + +// Listen to model changes, filter out ones initiated by the sync API, and +// saves the rest (hopefully just backend Syncer changes resulting from +// ApplyUpdates) to data_->changelist. +void SyncManager::SyncInternal::HandleChannelEvent( + const syncable::DirectoryChangeEvent& event) { + if (event.todo == syncable::DirectoryChangeEvent::TRANSACTION_ENDING) { + HandleTransactionEndingChangeEvent(event); + return; + } else if (event.todo == syncable::DirectoryChangeEvent::CALCULATE_CHANGES) { + if (event.writer == syncable::SYNCAPI) { + HandleCalculateChangesChangeEventFromSyncApi(event); + return; + } + HandleCalculateChangesChangeEventFromSyncer(event); + return; + } else if (event.todo == syncable::DirectoryChangeEvent::SHUTDOWN) { + dir_change_hookup_.reset(); + } +} + +void SyncManager::SyncInternal::HandleTransactionEndingChangeEvent( + const syncable::DirectoryChangeEvent& event) { + // 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. + DCHECK_EQ(event.todo, syncable::DirectoryChangeEvent::TRANSACTION_ENDING); + if (!observer_ || ChangeBuffersAreEmpty()) + return; + + // 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 trans(GetUserShare(), event.trans); + + for (int i = 0; i < syncable::MODEL_TYPE_COUNT; ++i) { + if (change_buffers_[i].IsEmpty()) + continue; + + vector<ChangeRecord> ordered_changes; + change_buffers_[i].GetAllChangesInTreeOrder(&trans, &ordered_changes); + if (!ordered_changes.empty()) { + observer_->OnChangesApplied(syncable::ModelTypeFromInt(i), &trans, + &ordered_changes[0], ordered_changes.size()); + } + change_buffers_[i].Clear(); + } +} + +void SyncManager::SyncInternal::HandleCalculateChangesChangeEventFromSyncApi( + const syncable::DirectoryChangeEvent& event) { + // We have been notified about a user action changing the bookmark model. + DCHECK_EQ(event.todo, syncable::DirectoryChangeEvent::CALCULATE_CHANGES); + DCHECK(event.writer == syncable::SYNCAPI || + event.writer == syncable::UNITTEST); + LOG_IF(WARNING, !ChangeBuffersAreEmpty()) << + "CALCULATE_CHANGES called with unapplied old changes."; + + bool exists_unsynced_items = false; + bool only_preference_changes = true; + for (syncable::OriginalEntries::const_iterator i = event.originals->begin(); + i != event.originals->end() && !exists_unsynced_items; + ++i) { + int64 id = i->ref(syncable::META_HANDLE); + syncable::Entry e(event.trans, syncable::GET_BY_HANDLE, id); + DCHECK(e.good()); + + syncable::ModelType model_type = e.GetModelType(); + + if (e.Get(syncable::IS_UNSYNCED)) { + if (model_type == syncable::TOP_LEVEL_FOLDER || + model_type == syncable::UNSPECIFIED) { + NOTREACHED() << "Permanent or underspecified item changed via syncapi."; + continue; + } + // Unsynced items will cause us to nudge the the syncer. + exists_unsynced_items = true; + + if (model_type != syncable::PREFERENCES) + only_preference_changes = false; + } + } + if (exists_unsynced_items && syncer_thread()) { + int nudge_delay = only_preference_changes ? + kPreferencesNudgeDelayMilliseconds : kDefaultNudgeDelayMilliseconds; + syncer_thread()->NudgeSyncer(nudge_delay, SyncerThread::kLocal); + } +} + +void SyncManager::SyncInternal::SetExtraChangeRecordData(int64 id, + syncable::ModelType type, ChangeReorderBuffer* buffer, + const syncable::EntryKernel& original, bool existed_before, + bool exists_now) { + // If this is a deletion, attach the entity specifics as extra data + // so that the delete can be processed. + if (!exists_now && existed_before) { + buffer->SetSpecificsForId(id, original.ref(SPECIFICS)); + } +} + +void SyncManager::SyncInternal::HandleCalculateChangesChangeEventFromSyncer( + const syncable::DirectoryChangeEvent& event) { + // We only expect one notification per sync step, so change_buffers_ should + // contain no pending entries. + DCHECK_EQ(event.todo, syncable::DirectoryChangeEvent::CALCULATE_CHANGES); + DCHECK(event.writer == syncable::SYNCER || + event.writer == syncable::UNITTEST); + LOG_IF(WARNING, !ChangeBuffersAreEmpty()) << + "CALCULATE_CHANGES called with unapplied old changes."; + + for (syncable::OriginalEntries::const_iterator i = event.originals->begin(); + i != event.originals->end(); ++i) { + int64 id = i->ref(syncable::META_HANDLE); + syncable::Entry e(event.trans, syncable::GET_BY_HANDLE, id); + bool existed_before = !i->ref(syncable::IS_DEL); + bool exists_now = e.good() && !e.Get(syncable::IS_DEL); + DCHECK(e.good()); + + // Omit items that aren't associated with a model. + syncable::ModelType type = e.GetModelType(); + if (type == syncable::TOP_LEVEL_FOLDER || type == syncable::UNSPECIFIED) + continue; + + if (exists_now && !existed_before) + change_buffers_[type].PushAddedItem(id); + else if (!exists_now && existed_before) + change_buffers_[type].PushDeletedItem(id); + else if (exists_now && existed_before && VisiblePropertiesDiffer(*i, e)) + change_buffers_[type].PushUpdatedItem(id, VisiblePositionsDiffer(*i, e)); + + SetExtraChangeRecordData(id, type, &change_buffers_[type], *i, + existed_before, exists_now); + } +} + +SyncManager::Status::Summary +SyncManager::SyncInternal::ComputeAggregatedStatusSummary() { + switch (allstatus_.status().icon) { + case AllStatus::OFFLINE: + return Status::OFFLINE; + case AllStatus::OFFLINE_UNSYNCED: + return Status::OFFLINE_UNSYNCED; + case AllStatus::SYNCING: + return Status::SYNCING; + case AllStatus::READY: + return Status::READY; + case AllStatus::CONFLICT: + return Status::CONFLICT; + case AllStatus::OFFLINE_UNUSABLE: + return Status::OFFLINE_UNUSABLE; + default: + return Status::INVALID; + } +} + +SyncManager::Status SyncManager::SyncInternal::ComputeAggregatedStatus() { + Status return_status = + { ComputeAggregatedStatusSummary(), + allstatus_.status().authenticated, + allstatus_.status().server_up, + allstatus_.status().server_reachable, + allstatus_.status().server_broken, + allstatus_.status().notifications_enabled, + allstatus_.status().notifications_received, + allstatus_.status().notifications_sent, + allstatus_.status().unsynced_count, + allstatus_.status().conflicting_count, + allstatus_.status().syncing, + allstatus_.status().initial_sync_ended, + allstatus_.status().syncer_stuck, + allstatus_.status().updates_available, + allstatus_.status().updates_received, + allstatus_.status().disk_full, + false, // TODO(ncarter): invalid store? + allstatus_.status().max_consecutive_errors}; + return return_status; +} + +void SyncManager::SyncInternal::HandleChannelEvent(const SyncerEvent& event) { + if (!observer_) + return; + + // 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 == SyncerEvent::SYNC_CYCLE_ENDED) { + + ModelSafeRoutingInfo enabled_types; + registrar_->GetModelSafeRoutingInfo(&enabled_types); + if (enabled_types.count(syncable::PASSWORDS) > 0) { + Cryptographer* cryptographer = + GetUserShare()->dir_manager->cryptographer(); + if (!cryptographer->is_ready() && !cryptographer->has_pending_keys()) { + sync_api::ReadTransaction trans(GetUserShare()); + sync_api::ReadNode node(&trans); + if (!node.InitByTagLookup(kNigoriTag)) { + NOTREACHED(); + return; + } + const sync_pb::NigoriSpecifics& nigori = node.GetNigoriSpecifics(); + if (!nigori.encrypted().blob().empty()) { + if (cryptographer->CanDecrypt(nigori.encrypted())) { + cryptographer->SetKeys(nigori.encrypted()); + } else { + cryptographer->SetPendingKeys(nigori.encrypted()); + } + } + } + // If we've completed a sync cycle and the cryptographer isn't ready yet, + // prompt the user for a passphrase. + if (!cryptographer->is_ready()) { + observer_->OnPassphraseRequired(); + } + } + + if (!initialized()) + return; + + if (!event.snapshot->has_more_to_sync) { + observer_->OnSyncCycleCompleted(event.snapshot); + } + + if (notification_method_ != browser_sync::NOTIFICATION_SERVER) { + // TODO(chron): Consider changing this back to track has_more_to_sync + // only notify peers if a successful commit has occurred. + bool new_pending_notification = + (event.snapshot->syncer_status.num_successful_commits > 0); + core_message_loop_->PostTask( + FROM_HERE, + NewRunnableMethod( + this, + &SyncManager::SyncInternal::SendPendingXMPPNotification, + new_pending_notification)); + } + } + + if (event.what_happened == SyncerEvent::PAUSED) { + observer_->OnPaused(); + return; + } + + if (event.what_happened == SyncerEvent::RESUMED) { + observer_->OnResumed(); + return; + } + + if (event.what_happened == SyncerEvent::STOP_SYNCING_PERMANENTLY) { + observer_->OnStopSyncingPermanently(); + return; + } +} + +void SyncManager::SyncInternal::HandleAuthWatcherEvent( + const AuthWatcherEvent& event) { + allstatus_.HandleAuthWatcherEvent(event); + // We don't care about an authentication attempt starting event, and we + // don't want to reset our state to GoogleServiceAuthError::NONE because the + // fact that an _attempt_ is starting doesn't change the fact that we have an + // auth problem. + if (event.what_happened == AuthWatcherEvent::AUTHENTICATION_ATTEMPT_START) + return; + // We clear our last auth problem cache on new auth watcher events, and only + // set it to indicate a problem state for certain AuthWatcherEvent types. + auth_problem_ = AuthError::NONE; + switch (event.what_happened) { + case AuthWatcherEvent::AUTH_SUCCEEDED: + DCHECK(!event.user_email.empty()); + // We now know the supplied username and password were valid. If this + // wasn't the first sync, authenticated_name should already be assigned. + if (username_for_share().empty()) { + share_.authenticated_name = event.user_email; + } + + DCHECK(LowerCaseEqualsASCII(username_for_share(), + StringToLowerASCII(event.user_email).c_str())) + << "username_for_share= " << username_for_share() + << ", event.user_email= " << event.user_email; + + if (observer_) + observer_->OnAuthError(AuthError::None()); + + // Hook up the DirectoryChangeEvent listener, HandleChangeEvent. + { + syncable::ScopedDirLookup lookup(dir_manager(), username_for_share()); + if (!lookup.good()) { + DCHECK(false) << "ScopedDirLookup creation failed; unable to hook " + << "up directory change event listener!"; + return; + } + + // Note that we can end up here multiple times, for example if the + // user had to re-login and we got a second AUTH_SUCCEEDED event. Take + // care not to add ourselves as an observer a second time. + if (!dir_change_hookup_.get()) + dir_change_hookup_.reset(lookup->AddChangeObserver(this)); + } + + if (!event.auth_token.empty()) { + core_message_loop_->PostTask( + FROM_HERE, + NewRunnableMethod( + this, &SyncManager::SyncInternal::TalkMediatorLogin, + event.user_email, event.auth_token)); + } + return; + case AuthWatcherEvent::AUTH_RENEWED: + DCHECK(!event.user_email.empty()); + DCHECK(!event.auth_token.empty()); + core_message_loop_->PostTask( + FROM_HERE, + NewRunnableMethod( + this, &SyncManager::SyncInternal::TalkMediatorLogin, + event.user_email, event.auth_token)); + return; + // Authentication failures translate to GoogleServiceAuthError events. + case AuthWatcherEvent::GAIA_AUTH_FAILED: // Invalid GAIA credentials. + if (event.auth_results->auth_error == gaia::CaptchaRequired) { + auth_problem_ = AuthError::CAPTCHA_REQUIRED; + std::string url_string("https://www.google.com/accounts/"); + url_string += event.auth_results->captcha_url; + GURL captcha(url_string); + observer_->OnAuthError(AuthError::FromCaptchaChallenge( + event.auth_results->captcha_token, captcha, + GURL(event.auth_results->auth_error_url))); + return; + } else if (event.auth_results->auth_error == + gaia::ConnectionUnavailable) { + auth_problem_ = AuthError::CONNECTION_FAILED; + } else { + auth_problem_ = AuthError::INVALID_GAIA_CREDENTIALS; + } + break; + case AuthWatcherEvent::SERVICE_AUTH_FAILED: // Expired GAIA credentials. + auth_problem_ = AuthError::INVALID_GAIA_CREDENTIALS; + break; + case AuthWatcherEvent::SERVICE_USER_NOT_SIGNED_UP: + auth_problem_ = AuthError::USER_NOT_SIGNED_UP; + break; + case AuthWatcherEvent::SERVICE_CONNECTION_FAILED: + auth_problem_ = AuthError::CONNECTION_FAILED; + break; + default: // We don't care about the many other AuthWatcherEvent types. + return; + } + + + // Fire notification that the status changed due to an authentication error. + if (observer_) + observer_->OnAuthError(AuthError(auth_problem_)); +} + +void SyncManager::SyncInternal::OnNotificationStateChange( + bool notifications_enabled) { + LOG(INFO) << "P2P: Notifications enabled = " + << (notifications_enabled ? "true" : "false"); + allstatus_.SetNotificationsEnabled(notifications_enabled); + if (syncer_thread()) { + syncer_thread()->SetNotificationsEnabled(notifications_enabled); + } + if ((notification_method_ != browser_sync::NOTIFICATION_SERVER) && + notifications_enabled) { + // Nudge the syncer thread when notifications are enabled, in case there is + // any data that has not yet been synced. If we are listening to + // server-issued notifications, we are already guaranteed to receive a + // notification on a successful connection. + if (syncer_thread()) { + syncer_thread()->NudgeSyncer(0, SyncerThread::kLocal); + } + + // Send a notification as soon as subscriptions are on + // (see http://code.google.com/p/chromium/issues/detail?id=38563 ). + core_message_loop_->PostTask( + FROM_HERE, + NewRunnableMethod( + this, + &SyncManager::SyncInternal::SendPendingXMPPNotification, + true)); + } +} + +void SyncManager::SyncInternal::TalkMediatorLogin( + const std::string& email, const std::string& token) { + DCHECK_EQ(MessageLoop::current(), core_message_loop_); + DCHECK(!email.empty()); + DCHECK(!token.empty()); + if (!talk_mediator_.get()) { + LOG(INFO) << "Not logging in: shutting down " + << "(talk_mediator_ is NULL)"; + return; + } + // TODO(akalin): Make talk_mediator automatically login on + // auth token change. + talk_mediator_->SetAuthToken(email, token, SYNC_SERVICE_NAME); + talk_mediator_->Login(); +} + +void SyncManager::SyncInternal::OnIncomingNotification( + const IncomingNotificationData& notification_data) { + // Check if the service url is a sync URL. An empty service URL is + // treated as a legacy sync notification. If we're listening to + // server-issued notifications, no need to check the service_url. + if ((notification_method_ == browser_sync::NOTIFICATION_SERVER) || + notification_data.service_url.empty() || + (notification_data.service_url == + browser_sync::kSyncLegacyServiceUrl) || + (notification_data.service_url == + browser_sync::kSyncServiceUrl)) { + LOG(INFO) << "P2P: Updates on server, pushing syncer"; + if (syncer_thread()) { + syncer_thread()->NudgeSyncer(0, SyncerThread::kNotification); + } + allstatus_.IncrementNotificationsReceived(); + } else { + LOG(WARNING) << "Notification fron unexpected source: " + << notification_data.service_url; + } +} + +void SyncManager::SyncInternal::OnOutgoingNotification() { + DCHECK_NE(notification_method_, browser_sync::NOTIFICATION_SERVER); + allstatus_.IncrementNotificationsSent(); +} + +SyncManager::Status::Summary SyncManager::GetStatusSummary() const { + return data_->ComputeAggregatedStatusSummary(); +} + +SyncManager::Status SyncManager::GetDetailedStatus() const { + return data_->ComputeAggregatedStatus(); +} + +SyncManager::SyncInternal* SyncManager::GetImpl() const { return data_; } + +void SyncManager::SaveChanges() { + data_->SaveChanges(); +} + +void SyncManager::SyncInternal::SaveChanges() { + syncable::ScopedDirLookup lookup(dir_manager(), username_for_share()); + if (!lookup.good()) { + DCHECK(false) << "ScopedDirLookup creation failed; Unable to SaveChanges"; + return; + } + lookup->SaveChanges(); +} + +void SyncManager::SetupForTestMode(const std::wstring& test_username) { + DCHECK(data_) << "SetupForTestMode requires initialization"; + data_->SetupForTestMode(test_username); +} + +void SyncManager::SyncInternal::SetupForTestMode( + const std::wstring& test_username) { + share_.authenticated_name = WideToUTF8(test_username); + + // Some tests are targeting only local db operations & integrity, and don't + // want syncer thread interference. + syncer_event_.reset(); + allstatus_.WatchSyncerThread(NULL); + syncer_thread_ = NULL; + + if (!dir_manager()->Open(username_for_share())) + DCHECK(false) << "Could not open directory when running in test mode"; + + // Hook up the DirectoryChangeEvent listener, HandleChangeEvent. + { + syncable::ScopedDirLookup lookup(dir_manager(), username_for_share()); + if (!lookup.good()) { + DCHECK(false) << "ScopedDirLookup creation failed; unable to hook " + << "up directory change event listener!"; + return; + } + dir_change_hookup_.reset(lookup->AddChangeObserver(this)); + } +} + +////////////////////////////////////////////////////////////////////////// +// BaseTransaction member definitions +BaseTransaction::BaseTransaction(UserShare* share) + : lookup_(NULL) { + DCHECK(share && share->dir_manager.get()); + lookup_ = new syncable::ScopedDirLookup(share->dir_manager.get(), + share->authenticated_name); + cryptographer_ = share->dir_manager->cryptographer(); + if (!(lookup_->good())) + DCHECK(false) << "ScopedDirLookup failed on valid DirManager."; +} +BaseTransaction::~BaseTransaction() { + delete lookup_; +} + +UserShare* SyncManager::GetUserShare() const { + DCHECK(data_->initialized()) << "GetUserShare requires initialization!"; + return data_->GetUserShare(); +} + +} // namespace sync_api |