| // Copyright (c) 2011 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/internal_api/write_node.h" |
| |
| #include "base/json/json_writer.h" |
| #include "base/utf_string_conversions.h" |
| #include "base/values.h" |
| #include "chrome/browser/sync/engine/nigori_util.h" |
| #include "chrome/browser/sync/engine/syncapi_internal.h" |
| #include "chrome/browser/sync/internal_api/base_transaction.h" |
| #include "chrome/browser/sync/internal_api/write_transaction.h" |
| #include "chrome/browser/sync/protocol/app_specifics.pb.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/password_specifics.pb.h" |
| #include "chrome/browser/sync/protocol/session_specifics.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/syncable/syncable.h" |
| #include "chrome/browser/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[] = " "; |
| |
| bool WriteNode::UpdateEntryWithEncryption( |
| browser_sync::Cryptographer* cryptographer, |
| const sync_pb::EntitySpecifics& new_specifics, |
| syncable::MutableEntry* entry) { |
| syncable::ModelType type = syncable::GetModelTypeFromSpecifics(new_specifics); |
| DCHECK_GE(type, syncable::FIRST_REAL_MODEL_TYPE); |
| syncable::ModelTypeSet encrypted_types = cryptographer->GetEncryptedTypes(); |
| sync_pb::EntitySpecifics generated_specifics; |
| if (!SpecificsNeedsEncryption(encrypted_types, new_specifics) || |
| !cryptographer->is_initialized()) { |
| // No encryption required or we are unable to encrypt. |
| generated_specifics.CopyFrom(new_specifics); |
| } else { |
| // Encrypt new_specifics into generated_specifics. |
| if (VLOG_IS_ON(2)) { |
| scoped_ptr<DictionaryValue> value(entry->ToValue()); |
| std::string info; |
| base::JSONWriter::Write(value.get(), true, &info); |
| VLOG(2) << "Encrypting specifics of type " |
| << syncable::ModelTypeToString(type) |
| << " with content: " |
| << info; |
| } |
| syncable::AddDefaultExtensionValue(type, &generated_specifics); |
| if (!cryptographer->Encrypt(new_specifics, |
| generated_specifics.mutable_encrypted())) { |
| NOTREACHED() << "Could not encrypt data for node of type " |
| << syncable::ModelTypeToString(type); |
| return false; |
| } |
| } |
| |
| const sync_pb::EntitySpecifics& old_specifics = entry->Get(SPECIFICS); |
| if (AreSpecificsEqual(cryptographer, old_specifics, generated_specifics) && |
| (entry->Get(syncable::NON_UNIQUE_NAME) == kEncryptedString || |
| !generated_specifics.has_encrypted())) { |
| // Even if the data is the same but the old specifics are encrypted with an |
| // old key, we should go ahead and re-encrypt with the new key. |
| if ((!old_specifics.has_encrypted() && |
| !generated_specifics.has_encrypted()) || |
| cryptographer->CanDecryptUsingDefaultKey(old_specifics.encrypted())) { |
| VLOG(2) << "Specifics of type " << syncable::ModelTypeToString(type) |
| << " already match, dropping change."; |
| return true; |
| } |
| // TODO(zea): Add some way to keep track of how often we're reencrypting |
| // because of a passphrase change. |
| } |
| |
| if (generated_specifics.has_encrypted()) { |
| // Overwrite the possibly sensitive non-specifics data. |
| entry->Put(syncable::NON_UNIQUE_NAME, kEncryptedString); |
| // For bookmarks we actually put bogus data into the unencrypted specifics, |
| // else the server will try to do it for us. |
| if (type == syncable::BOOKMARKS) { |
| sync_pb::BookmarkSpecifics* bookmark_specifics = |
| generated_specifics.MutableExtension(sync_pb::bookmark); |
| if (!entry->Get(syncable::IS_DIR)) |
| bookmark_specifics->set_url(kEncryptedString); |
| bookmark_specifics->set_title(kEncryptedString); |
| } |
| } |
| entry->Put(syncable::SPECIFICS, generated_specifics); |
| syncable::MarkForSyncing(entry); |
| return true; |
| } |
| |
| 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) { |
| sync_pb::EntitySpecifics specifics = GetEntitySpecifics(); |
| std::string server_legal_name; |
| SyncAPINameToServerName(WideToUTF8(title), &server_legal_name); |
| |
| string old_name = entry_->Get(syncable::NON_UNIQUE_NAME); |
| |
| if (server_legal_name == old_name) |
| return; // Skip redundant changes. |
| |
| // Only set NON_UNIQUE_NAME to the title if we're not encrypted. |
| Cryptographer* cryptographer = GetTransaction()->GetCryptographer(); |
| if (cryptographer->GetEncryptedTypes().count(GetModelType()) > 0) { |
| if (old_name != kEncryptedString) |
| entry_->Put(syncable::NON_UNIQUE_NAME, kEncryptedString); |
| } else { |
| entry_->Put(syncable::NON_UNIQUE_NAME, server_legal_name); |
| } |
| |
| // For bookmarks, we also set the title field in the specifics. |
| // TODO(zea): refactor bookmarks to not need this functionality. |
| if (GetModelType() == syncable::BOOKMARKS) { |
| specifics.MutableExtension(sync_pb::bookmark)->set_title(server_legal_name); |
| SetEntitySpecifics(specifics); // Does it's own encryption checking. |
| } |
| |
| 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.MutableExtension(sync_pb::app)->CopyFrom(new_value); |
| SetEntitySpecifics(entity_specifics); |
| } |
| |
| void WriteNode::SetAutofillSpecifics( |
| const sync_pb::AutofillSpecifics& new_value) { |
| sync_pb::EntitySpecifics entity_specifics; |
| entity_specifics.MutableExtension(sync_pb::autofill)->CopyFrom(new_value); |
| SetEntitySpecifics(entity_specifics); |
| } |
| |
| void WriteNode::SetAutofillProfileSpecifics( |
| const sync_pb::AutofillProfileSpecifics& new_value) { |
| sync_pb::EntitySpecifics entity_specifics; |
| entity_specifics.MutableExtension(sync_pb::autofill_profile)-> |
| CopyFrom(new_value); |
| SetEntitySpecifics(entity_specifics); |
| } |
| |
| void WriteNode::SetBookmarkSpecifics( |
| const sync_pb::BookmarkSpecifics& new_value) { |
| sync_pb::EntitySpecifics entity_specifics; |
| entity_specifics.MutableExtension(sync_pb::bookmark)->CopyFrom(new_value); |
| SetEntitySpecifics(entity_specifics); |
| } |
| |
| void WriteNode::SetNigoriSpecifics( |
| const sync_pb::NigoriSpecifics& new_value) { |
| sync_pb::EntitySpecifics entity_specifics; |
| entity_specifics.MutableExtension(sync_pb::nigori)->CopyFrom(new_value); |
| SetEntitySpecifics(entity_specifics); |
| } |
| |
| void WriteNode::SetPasswordSpecifics( |
| const sync_pb::PasswordSpecificsData& data) { |
| DCHECK_EQ(syncable::PASSWORDS, GetModelType()); |
| |
| Cryptographer* cryptographer = GetTransaction()->GetCryptographer(); |
| |
| // Idempotency check to prevent unnecessary syncing: if the plaintexts match |
| // and the old ciphertext is encrypted with the most current key, there's |
| // nothing to do here. Because each encryption is seeded with a different |
| // random value, checking for equivalence post-encryption doesn't suffice. |
| const sync_pb::EncryptedData& old_ciphertext = |
| GetEntry()->Get(SPECIFICS).GetExtension(sync_pb::password).encrypted(); |
| scoped_ptr<sync_pb::PasswordSpecificsData> old_plaintext( |
| DecryptPasswordSpecifics(GetEntry()->Get(SPECIFICS), cryptographer)); |
| if (old_plaintext.get() && |
| old_plaintext->SerializeAsString() == data.SerializeAsString() && |
| cryptographer->CanDecryptUsingDefaultKey(old_ciphertext)) { |
| return; |
| } |
| |
| sync_pb::PasswordSpecifics new_value; |
| if (!cryptographer->Encrypt(data, new_value.mutable_encrypted())) { |
| NOTREACHED() << "Failed to encrypt password, possibly due to sync node " |
| << "corruption"; |
| return; |
| } |
| |
| sync_pb::EntitySpecifics entity_specifics; |
| entity_specifics.MutableExtension(sync_pb::password)->CopyFrom(new_value); |
| SetEntitySpecifics(entity_specifics); |
| } |
| |
| void WriteNode::SetThemeSpecifics( |
| const sync_pb::ThemeSpecifics& new_value) { |
| sync_pb::EntitySpecifics entity_specifics; |
| entity_specifics.MutableExtension(sync_pb::theme)->CopyFrom(new_value); |
| SetEntitySpecifics(entity_specifics); |
| } |
| |
| void WriteNode::SetSessionSpecifics( |
| const sync_pb::SessionSpecifics& new_value) { |
| sync_pb::EntitySpecifics entity_specifics; |
| entity_specifics.MutableExtension(sync_pb::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); |
| VLOG(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.MutableExtension(sync_pb::typed_url)->CopyFrom(new_value); |
| SetEntitySpecifics(entity_specifics); |
| } |
| |
| void WriteNode::SetExtensionSpecifics( |
| const sync_pb::ExtensionSpecifics& new_value) { |
| sync_pb::EntitySpecifics entity_specifics; |
| entity_specifics.MutableExtension(sync_pb::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. |
| 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()); |
| } |
| |
| // 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()); |
| } |
| |
| 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_EQ(syncable::NIGORI, model_type); |
| 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); |
| 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 |