diff options
author | akalin@chromium.org <akalin@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-06-14 19:59:15 +0000 |
---|---|---|
committer | akalin@chromium.org <akalin@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-06-14 19:59:15 +0000 |
commit | c05d2cb72ed6f2dcbe6d359665841a0d1b481390 (patch) | |
tree | f2f71c08c0f66b7ae8e118eef83e7556a4c2d429 | |
parent | 4f3d77cd54c36802154cb8c483bed0abdcf75b8d (diff) | |
download | chromium_src-c05d2cb72ed6f2dcbe6d359665841a0d1b481390.zip chromium_src-c05d2cb72ed6f2dcbe6d359665841a0d1b481390.tar.gz chromium_src-c05d2cb72ed6f2dcbe6d359665841a0d1b481390.tar.bz2 |
Implemented initial version of extension syncing.
BUG=32413
TEST=unit tests, trybots, manual
Review URL: http://codereview.chromium.org/2752007
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@49712 0039d316-1c4b-4281-b951-d872f2087c98
22 files changed, 2188 insertions, 7 deletions
diff --git a/chrome/browser/sync/engine/syncapi.cc b/chrome/browser/sync/engine/syncapi.cc index 7018d74..1a2da8b 100644 --- a/chrome/browser/sync/engine/syncapi.cc +++ b/chrome/browser/sync/engine/syncapi.cc @@ -1613,13 +1613,26 @@ void SyncManager::SyncInternal::SetExtraChangeRecordData(int64 id, const syncable::EntryKernel& original, bool existed_before, bool exists_now) { // Extra data for autofill deletions. - if (type == syncable::AUTOFILL) { - if (!exists_now && existed_before) { - sync_pb::AutofillSpecifics* s = new sync_pb::AutofillSpecifics; - s->CopyFrom(original.ref(SPECIFICS).GetExtension(sync_pb::autofill)); - ExtraChangeRecordData* extra = new ExtraAutofillChangeRecordData(s); - buffer->SetExtraDataForId(id, extra); - } + switch (type) { + case syncable::AUTOFILL: + if (!exists_now && existed_before) { + sync_pb::AutofillSpecifics* s = new sync_pb::AutofillSpecifics; + s->CopyFrom(original.ref(SPECIFICS).GetExtension(sync_pb::autofill)); + ExtraChangeRecordData* extra = new ExtraAutofillChangeRecordData(s); + buffer->SetExtraDataForId(id, extra); + } + break; + case syncable::EXTENSIONS: + if (!exists_now && existed_before) { + const std::string& extension_id = + original.ref(SPECIFICS).GetExtension(sync_pb::extension).id(); + ExtraChangeRecordData* extra = + new ExtraExtensionChangeRecordData(extension_id); + buffer->SetExtraDataForId(id, extra); + } + break; + default: + break; } } diff --git a/chrome/browser/sync/engine/syncapi.h b/chrome/browser/sync/engine/syncapi.h index b363188..71ca150 100644 --- a/chrome/browser/sync/engine/syncapi.h +++ b/chrome/browser/sync/engine/syncapi.h @@ -529,6 +529,15 @@ class SyncManager { const sync_pb::AutofillSpecifics* pre_deletion_data; }; + // Extra data used only for extension DELETE changes. + class ExtraExtensionChangeRecordData : public ExtraChangeRecordData { + public: + explicit ExtraExtensionChangeRecordData(const std::string& extension_id) + : extension_id(extension_id) {} + virtual ~ExtraExtensionChangeRecordData() {} + const std::string extension_id; + }; + // Status encapsulates detailed state about the internals of the SyncManager. struct Status { // Summary is a distilled set of important information that the end-user may diff --git a/chrome/browser/sync/glue/extension_change_processor.cc b/chrome/browser/sync/glue/extension_change_processor.cc new file mode 100644 index 0000000..36a7dc1 --- /dev/null +++ b/chrome/browser/sync/glue/extension_change_processor.cc @@ -0,0 +1,165 @@ +// 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/glue/extension_change_processor.h" + +#include <sstream> +#include <string> + +#include "base/logging.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/sync/engine/syncapi.h" +#include "chrome/browser/sync/glue/extension_model_associator.h" +#include "chrome/browser/sync/glue/extension_util.h" +#include "chrome/common/extensions/extension.h" +#include "chrome/common/notification_details.h" +#include "chrome/common/notification_source.h" + +typedef sync_api::SyncManager::ExtraExtensionChangeRecordData + ExtraExtensionChangeRecordData; + +namespace browser_sync { + +ExtensionChangeProcessor::ExtensionChangeProcessor( + UnrecoverableErrorHandler* error_handler, + ExtensionModelAssociator* extension_model_associator) + : ChangeProcessor(error_handler), + extension_model_associator_(extension_model_associator), + profile_(NULL) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + DCHECK(error_handler); + DCHECK(extension_model_associator_); +} + +ExtensionChangeProcessor::~ExtensionChangeProcessor() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); +} + +// TODO(akalin): We need to make sure events we receive from either +// the browser or the syncapi are done in order; this is tricky since +// some events (e.g., extension installation) are done asynchronously. + +void ExtensionChangeProcessor::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + DCHECK(running()); + DCHECK(profile_); + switch (type.value) { + case NotificationType::EXTENSION_LOADED: + case NotificationType::EXTENSION_UNLOADED: { + DCHECK_EQ(Source<Profile>(source).ptr(), profile_); + Extension* extension = Details<Extension>(details).ptr(); + CHECK(extension); + const std::string& id = extension->id(); + LOG(INFO) << "Got change notification of type " << type.value + << " for extension " << id; + if (!extension_model_associator_->OnClientUpdate(id)) { + std::string error = std::string("Client update failed for ") + id; + error_handler()->OnUnrecoverableError(FROM_HERE, error); + return; + } + break; + } + default: + LOG(DFATAL) << "Received unexpected notification of type " + << type.value; + break; + } + + return; +} + +void ExtensionChangeProcessor::ApplyChangesFromSyncModel( + const sync_api::BaseTransaction* trans, + const sync_api::SyncManager::ChangeRecord* changes, + int change_count) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + if (!running()) { + return; + } + for (int i = 0; i < change_count; ++i) { + const sync_api::SyncManager::ChangeRecord& change = changes[i]; + switch (change.action) { + case sync_api::SyncManager::ChangeRecord::ACTION_ADD: + case sync_api::SyncManager::ChangeRecord::ACTION_UPDATE: { + sync_api::ReadNode node(trans); + if (!node.InitByIdLookup(change.id)) { + std::stringstream error; + error << "Extension node lookup failed for change " << change.id + << " of action type " << change.action; + error_handler()->OnUnrecoverableError(FROM_HERE, error.str()); + return; + } + DCHECK_EQ(node.GetModelType(), syncable::EXTENSIONS); + const sync_pb::ExtensionSpecifics& specifics = + node.GetExtensionSpecifics(); + if (!IsExtensionSpecificsValid(specifics)) { + std::string error = + std::string("Invalid server specifics: ") + + ExtensionSpecificsToString(specifics); + error_handler()->OnUnrecoverableError(FROM_HERE, error); + return; + } + StopObserving(); + extension_model_associator_->OnServerUpdate(specifics); + StartObserving(); + break; + } + case sync_api::SyncManager::ChangeRecord::ACTION_DELETE: { + StopObserving(); + scoped_ptr<ExtraExtensionChangeRecordData> + data(static_cast<ExtraExtensionChangeRecordData*>(change.extra)); + if (data.get()) { + extension_model_associator_->OnServerRemove(data->extension_id); + } else { + std::stringstream error; + error << "Could not get extension ID for deleted node " + << change.id; + error_handler()->OnUnrecoverableError(FROM_HERE, error.str()); + LOG(DFATAL) << error.str(); + } + StartObserving(); + break; + } + } + } +} + +void ExtensionChangeProcessor::StartImpl(Profile* profile) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + DCHECK(profile); + profile_ = profile; + StartObserving(); +} + +void ExtensionChangeProcessor::StopImpl() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + StopObserving(); + profile_ = NULL; +} + +void ExtensionChangeProcessor::StartObserving() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + DCHECK(profile_); + LOG(INFO) << "Observing EXTENSION_LOADED and EXTENSION_UNLOADED"; + // TODO(akalin): We miss notifications when we uninstall a disabled + // extension since EXTENSION_UNLOADED isn't sent in that case. Add + // an EXTENSION_UNINSTALLED notification and listen to it. + notification_registrar_.Add( + this, NotificationType::EXTENSION_LOADED, + Source<Profile>(profile_)); + notification_registrar_.Add( + this, NotificationType::EXTENSION_UNLOADED, + Source<Profile>(profile_)); +} + +void ExtensionChangeProcessor::StopObserving() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + DCHECK(profile_); + LOG(INFO) << "Unobserving all notifications"; + notification_registrar_.RemoveAll(); +} + +} // namespace browser_sync diff --git a/chrome/browser/sync/glue/extension_change_processor.h b/chrome/browser/sync/glue/extension_change_processor.h new file mode 100644 index 0000000..7a10f9d --- /dev/null +++ b/chrome/browser/sync/glue/extension_change_processor.h @@ -0,0 +1,72 @@ +// 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. + +#ifndef CHROME_BROWSER_SYNC_GLUE_EXTENSION_CHANGE_PROCESSOR_H_ +#define CHROME_BROWSER_SYNC_GLUE_EXTENSION_CHANGE_PROCESSOR_H_ + +#include "base/basictypes.h" +#include "chrome/browser/sync/engine/syncapi.h" +#include "chrome/browser/sync/glue/change_processor.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_type.h" +#include "chrome/common/notification_registrar.h" + +class NotificationDetails; +class NotificationSource; +class Profile; + +namespace browser_sync { + +class ExtensionModelAssociator; +class UnrecoverableErrorHandler; + +// This class is responsible for taking changes from the +// ExtensionService and applying them to the sync_api 'syncable' +// model, and vice versa. All operations and use of this class are +// from the UI thread. +class ExtensionChangeProcessor : public ChangeProcessor, + public NotificationObserver { + public: + // Does not take ownership of either argument. + // + // TODO(akalin): Create a Delegate interface and take that instead. + // That'll enable us to unit test this class. + ExtensionChangeProcessor( + UnrecoverableErrorHandler* error_handler, + ExtensionModelAssociator* extension_model_associator); + virtual ~ExtensionChangeProcessor(); + + // NotificationObserver implementation. + // BrowserExtensionProvider -> sync_api model change application. + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); + + // ChangeProcessor implementation. + // sync_api model -> BrowserExtensionProvider change application. + virtual void ApplyChangesFromSyncModel( + const sync_api::BaseTransaction* trans, + const sync_api::SyncManager::ChangeRecord* changes, + int change_count); + + protected: + // ChangeProcessor implementation. + virtual void StartImpl(Profile* profile); + virtual void StopImpl(); + + private: + void StartObserving(); + void StopObserving(); + + ExtensionModelAssociator* extension_model_associator_; + NotificationRegistrar notification_registrar_; + // Owner of the ExtensionService. Non-NULL iff |running()| is true. + Profile* profile_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionChangeProcessor); +}; + +} // namespace browser_sync + +#endif // CHROME_BROWSER_SYNC_GLUE_EXTENSION_CHANGE_PROCESSOR_H_ diff --git a/chrome/browser/sync/glue/extension_data.cc b/chrome/browser/sync/glue/extension_data.cc new file mode 100644 index 0000000..eb4fea6 --- /dev/null +++ b/chrome/browser/sync/glue/extension_data.cc @@ -0,0 +1,50 @@ +// 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/glue/extension_data.h" + +#include "base/logging.h" +#include "chrome/browser/sync/glue/extension_util.h" + +namespace browser_sync { + +ExtensionData ExtensionData::FromData( + Source source, const sync_pb::ExtensionSpecifics& data) { + DcheckIsExtensionSpecificsValid(data); + ExtensionData extension_data; + extension_data.merged_data_ = extension_data.source_data_[source] = data; + DCHECK(AreExtensionSpecificsEqual(extension_data.merged_data(), data)); + DCHECK(!extension_data.NeedsUpdate(source)); + return extension_data; +} + +const sync_pb::ExtensionSpecifics& ExtensionData::merged_data() const { + DcheckIsExtensionSpecificsValid(merged_data_); + return merged_data_; +} + +bool ExtensionData::NeedsUpdate(Source source) const { + SourceDataMap::const_iterator it = source_data_.find(source); + return + (it == source_data_.end()) || + !AreExtensionSpecificsEqual(it->second, merged_data_); +} + +void ExtensionData::SetData( + Source source, bool merge_user_properties, + const sync_pb::ExtensionSpecifics& data) { + DcheckIsExtensionSpecificsValid(data); + source_data_[source] = data; + MergeExtensionSpecifics(data, merge_user_properties, &merged_data_); + DcheckIsExtensionSpecificsValid(merged_data_); +} + +void ExtensionData::ResolveData(Source source) { + source_data_[source] = merged_data_; + DCHECK(!NeedsUpdate(source)); +} + +ExtensionData::ExtensionData() {} + +} // namespace browser_sync diff --git a/chrome/browser/sync/glue/extension_data.h b/chrome/browser/sync/glue/extension_data.h new file mode 100644 index 0000000..f67a48f --- /dev/null +++ b/chrome/browser/sync/glue/extension_data.h @@ -0,0 +1,62 @@ +// 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. + +#ifndef CHROME_BROWSER_SYNC_GLUE_EXTENSION_DATA_H_ +#define CHROME_BROWSER_SYNC_GLUE_EXTENSION_DATA_H_ + +// ExtensionData is the class used to manage the client and server +// versions of the data for a particular extension. + +#include <map> + +#include "chrome/browser/sync/protocol/extension_specifics.pb.h" + +namespace browser_sync { + +class ExtensionData { + public: + enum Source { + CLIENT, + SERVER, + }; + + // Returns an ExtensionData constructed from the given data from the + // given source. merged_data() will be equal to |data| and + // NeedsUpdate(source) will return false. + static ExtensionData FromData( + Source source, const sync_pb::ExtensionSpecifics& data); + + // Implicit copy constructor and assignment operator welcome. + + // Returns the version of the data that all sources should + // eventually have. + const sync_pb::ExtensionSpecifics& merged_data() const; + + // Returns whether or not the data from the given source needs to be + // updated from merged_data(). + bool NeedsUpdate(Source source) const; + + // Stores the given data as being from the given source, merging it + // into merged_data(). May change what NeedsUpdate() returns for + // any source. + void SetData( + Source source, bool merge_user_properties, + const sync_pb::ExtensionSpecifics& data); + + // Marks the data from the given data as updated, + // i.e. NeedsUpdate(source) will return false. + void ResolveData(Source source); + + private: + typedef std::map<Source, sync_pb::ExtensionSpecifics> SourceDataMap; + SourceDataMap source_data_; + sync_pb::ExtensionSpecifics merged_data_; + + // This is private; clients must use FromData(). + ExtensionData(); +}; + +} // namespace + +#endif // CHROME_BROWSER_SYNC_GLUE_EXTENSION_DATA_H_ diff --git a/chrome/browser/sync/glue/extension_data_type_controller.cc b/chrome/browser/sync/glue/extension_data_type_controller.cc new file mode 100644 index 0000000..c71ce3c --- /dev/null +++ b/chrome/browser/sync/glue/extension_data_type_controller.cc @@ -0,0 +1,111 @@ +// 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 "base/histogram.h" +#include "base/logging.h" +#include "base/time.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/sync/glue/extension_change_processor.h" +#include "chrome/browser/sync/glue/extension_data_type_controller.h" +#include "chrome/browser/sync/glue/extension_model_associator.h" +#include "chrome/browser/sync/profile_sync_service.h" +#include "chrome/browser/sync/profile_sync_factory.h" +#include "chrome/browser/sync/unrecoverable_error_handler.h" + +namespace browser_sync { + +ExtensionDataTypeController::ExtensionDataTypeController( + ProfileSyncFactory* profile_sync_factory, + Profile* profile, + ProfileSyncService* sync_service) + : profile_sync_factory_(profile_sync_factory), + profile_(profile), + sync_service_(sync_service), + state_(NOT_RUNNING) { + DCHECK(profile_sync_factory); + DCHECK(sync_service); +} + +ExtensionDataTypeController::~ExtensionDataTypeController() { +} + +void ExtensionDataTypeController::Start(StartCallback* start_callback) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + DCHECK(start_callback); + if (state_ != NOT_RUNNING) { + start_callback->Run(BUSY); + delete start_callback; + return; + } + + start_callback_.reset(start_callback); + + profile_->InitExtensions(); + ProfileSyncFactory::SyncComponents sync_components = + profile_sync_factory_->CreateExtensionSyncComponents(sync_service_, + this); + model_associator_.reset(sync_components.model_associator); + change_processor_.reset(sync_components.change_processor); + + bool sync_has_nodes = false; + if (!model_associator_->SyncModelHasUserCreatedNodes(&sync_has_nodes)) { + StartFailed(UNRECOVERABLE_ERROR); + return; + } + + base::TimeTicks start_time = base::TimeTicks::Now(); + bool merge_success = model_associator_->AssociateModels(); + UMA_HISTOGRAM_TIMES("Sync.ExtensionAssociationTime", + base::TimeTicks::Now() - start_time); + if (!merge_success) { + StartFailed(ASSOCIATION_FAILED); + return; + } + + sync_service_->ActivateDataType(this, change_processor_.get()); + state_ = RUNNING; + FinishStart(!sync_has_nodes ? OK_FIRST_RUN : OK); +} + +void ExtensionDataTypeController::Stop() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + + if (change_processor_ != NULL) + sync_service_->DeactivateDataType(this, change_processor_.get()); + + if (model_associator_ != NULL) + model_associator_->DisassociateModels(); + + change_processor_.reset(); + model_associator_.reset(); + start_callback_.reset(); + + state_ = NOT_RUNNING; +} + +void ExtensionDataTypeController::OnUnrecoverableError( + const tracked_objects::Location& from_here, + const std::string& message) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + UMA_HISTOGRAM_COUNTS("Sync.ExtensionRunFailures", 1); + sync_service_->OnUnrecoverableError(from_here, message); +} + +void ExtensionDataTypeController::FinishStart(StartResult result) { + start_callback_->Run(result); + start_callback_.reset(); +} + +void ExtensionDataTypeController::StartFailed(StartResult result) { + model_associator_.reset(); + change_processor_.reset(); + start_callback_->Run(result); + start_callback_.reset(); + UMA_HISTOGRAM_ENUMERATION("Sync.ExtensionStartFailures", + result, + MAX_START_RESULT); +} + +} // namespace browser_sync diff --git a/chrome/browser/sync/glue/extension_data_type_controller.h b/chrome/browser/sync/glue/extension_data_type_controller.h new file mode 100644 index 0000000..c364ac2 --- /dev/null +++ b/chrome/browser/sync/glue/extension_data_type_controller.h @@ -0,0 +1,84 @@ +// 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. + +#ifndef CHROME_BROWSER_SYNC_GLUE_EXTENSION_DATA_TYPE_CONTROLLER_H_ +#define CHROME_BROWSER_SYNC_GLUE_EXTENSION_DATA_TYPE_CONTROLLER_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/scoped_ptr.h" +#include "chrome/browser/sync/glue/data_type_controller.h" + +class Profile; +class ProfileSyncFactory; +class ProfileSyncService; + +namespace browser_sync { + +class AssociatorInterface; +class ChangeProcessor; + +class ExtensionDataTypeController : public DataTypeController { + public: + ExtensionDataTypeController( + ProfileSyncFactory* profile_sync_factory, + Profile* profile, + ProfileSyncService* sync_service); + virtual ~ExtensionDataTypeController(); + + // DataTypeController implementation. + virtual void Start(StartCallback* start_callback); + + virtual void Stop(); + + virtual bool enabled() { + return true; + } + + virtual syncable::ModelType type() { + return syncable::EXTENSIONS; + } + + virtual browser_sync::ModelSafeGroup model_safe_group() { + return browser_sync::GROUP_UI; + } + + virtual const char* name() const { + // For logging only. + return "extension"; + } + + virtual State state() { + return state_; + } + + // UnrecoverableErrorHandler interface. + virtual void OnUnrecoverableError( + const tracked_objects::Location& from_here, + const std::string& message); + + private: + // Helper method to run the stashed start callback with a given result. + void FinishStart(StartResult result); + + // Cleans up state and calls callback when start fails. + void StartFailed(StartResult result); + + ProfileSyncFactory* profile_sync_factory_; + Profile* profile_; + ProfileSyncService* sync_service_; + + State state_; + + scoped_ptr<StartCallback> start_callback_; + scoped_ptr<AssociatorInterface> model_associator_; + scoped_ptr<ChangeProcessor> change_processor_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionDataTypeController); +}; + +} // namespace browser_sync + +#endif // CHROME_BROWSER_SYNC_GLUE_EXTENSION_DATA_TYPE_CONTROLLER_H_ diff --git a/chrome/browser/sync/glue/extension_data_type_controller_unittest.cc b/chrome/browser/sync/glue/extension_data_type_controller_unittest.cc new file mode 100644 index 0000000..a8d4979 --- /dev/null +++ b/chrome/browser/sync/glue/extension_data_type_controller_unittest.cc @@ -0,0 +1,175 @@ +// 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 "testing/gtest/include/gtest/gtest.h" + +#include "base/callback.h" +#include "base/message_loop.h" +#include "base/scoped_ptr.h" +#include "base/task.h" +#include "chrome/browser/chrome_thread.h" +#include "chrome/browser/sync/glue/extension_data_type_controller.h" +#include "chrome/browser/sync/glue/change_processor_mock.h" +#include "chrome/browser/sync/glue/model_associator_mock.h" +#include "chrome/browser/sync/profile_sync_factory_mock.h" +#include "chrome/browser/sync/profile_sync_service_mock.h" +#include "chrome/test/profile_mock.h" + +using browser_sync::ExtensionDataTypeController; +using browser_sync::ChangeProcessorMock; +using browser_sync::DataTypeController; +using browser_sync::ModelAssociatorMock; +using testing::_; +using testing::DoAll; +using testing::InvokeWithoutArgs; +using testing::Return; +using testing::SetArgumentPointee; + +class StartCallback { + public: + MOCK_METHOD1(Run, void(DataTypeController::StartResult result)); +}; + +class ExtensionDataTypeControllerTest : public testing::Test { + public: + ExtensionDataTypeControllerTest() + : ui_thread_(ChromeThread::UI, &message_loop_) {} + + virtual void SetUp() { + profile_sync_factory_.reset(new ProfileSyncFactoryMock()); + extension_dtc_ = + new ExtensionDataTypeController(profile_sync_factory_.get(), + &profile_, &service_); + } + + protected: + void SetStartExpectations() { + model_associator_ = new ModelAssociatorMock(); + change_processor_ = new ChangeProcessorMock(); + EXPECT_CALL(*profile_sync_factory_, CreateExtensionSyncComponents(_, _)). + WillOnce(Return( + ProfileSyncFactory::SyncComponents(model_associator_, + change_processor_))); + } + + void SetAssociateExpectations() { + EXPECT_CALL(*model_associator_, ChromeModelHasUserCreatedNodes(_)). + WillRepeatedly(DoAll(SetArgumentPointee<0>(false), Return(true))); + EXPECT_CALL(*model_associator_, SyncModelHasUserCreatedNodes(_)). + WillRepeatedly(DoAll(SetArgumentPointee<0>(true), Return(true))); + EXPECT_CALL(*model_associator_, AssociateModels()). + WillRepeatedly(Return(true)); + } + + void SetActivateExpectations() { + EXPECT_CALL(service_, ActivateDataType(_, _)); + } + + void SetStopExpectations() { + EXPECT_CALL(service_, DeactivateDataType(_, _)); + EXPECT_CALL(*model_associator_, DisassociateModels()); + } + + MessageLoopForUI message_loop_; + ChromeThread ui_thread_; + scoped_refptr<ExtensionDataTypeController> extension_dtc_; + scoped_ptr<ProfileSyncFactoryMock> profile_sync_factory_; + ProfileMock profile_; + ProfileSyncServiceMock service_; + ModelAssociatorMock* model_associator_; + ChangeProcessorMock* change_processor_; + StartCallback start_callback_; +}; + +TEST_F(ExtensionDataTypeControllerTest, Start) { + SetStartExpectations(); + SetAssociateExpectations(); + SetActivateExpectations(); + EXPECT_EQ(DataTypeController::NOT_RUNNING, extension_dtc_->state()); + EXPECT_CALL(start_callback_, Run(DataTypeController::OK)); + extension_dtc_->Start(NewCallback(&start_callback_, &StartCallback::Run)); + EXPECT_EQ(DataTypeController::RUNNING, extension_dtc_->state()); +} + +TEST_F(ExtensionDataTypeControllerTest, StartFirstRun) { + SetStartExpectations(); + SetAssociateExpectations(); + SetActivateExpectations(); + EXPECT_CALL(*model_associator_, SyncModelHasUserCreatedNodes(_)). + WillRepeatedly(DoAll(SetArgumentPointee<0>(false), Return(true))); + EXPECT_CALL(start_callback_, Run(DataTypeController::OK_FIRST_RUN)); + extension_dtc_->Start(NewCallback(&start_callback_, &StartCallback::Run)); +} + +TEST_F(ExtensionDataTypeControllerTest, StartOk) { + SetStartExpectations(); + SetAssociateExpectations(); + SetActivateExpectations(); + EXPECT_CALL(*model_associator_, ChromeModelHasUserCreatedNodes(_)). + WillRepeatedly(DoAll(SetArgumentPointee<0>(true), Return(true))); + EXPECT_CALL(*model_associator_, SyncModelHasUserCreatedNodes(_)). + WillRepeatedly(DoAll(SetArgumentPointee<0>(true), Return(true))); + + EXPECT_CALL(start_callback_, Run(DataTypeController::OK)); + extension_dtc_->Start(NewCallback(&start_callback_, &StartCallback::Run)); +} + +TEST_F(ExtensionDataTypeControllerTest, StartAssociationFailed) { + SetStartExpectations(); + SetAssociateExpectations(); + EXPECT_CALL(*model_associator_, AssociateModels()). + WillRepeatedly(Return(false)); + + EXPECT_CALL(start_callback_, Run(DataTypeController::ASSOCIATION_FAILED)); + extension_dtc_->Start(NewCallback(&start_callback_, &StartCallback::Run)); + EXPECT_EQ(DataTypeController::NOT_RUNNING, extension_dtc_->state()); +} + +TEST_F(ExtensionDataTypeControllerTest, + StartAssociationTriggersUnrecoverableError) { + SetStartExpectations(); + // Set up association to fail with an unrecoverable error. + EXPECT_CALL(*model_associator_, ChromeModelHasUserCreatedNodes(_)). + WillRepeatedly(DoAll(SetArgumentPointee<0>(false), Return(true))); + EXPECT_CALL(*model_associator_, SyncModelHasUserCreatedNodes(_)). + WillRepeatedly(DoAll(SetArgumentPointee<0>(false), Return(false))); + EXPECT_CALL(start_callback_, Run(DataTypeController::UNRECOVERABLE_ERROR)); + extension_dtc_->Start(NewCallback(&start_callback_, &StartCallback::Run)); + EXPECT_EQ(DataTypeController::NOT_RUNNING, extension_dtc_->state()); +} + +TEST_F(ExtensionDataTypeControllerTest, Stop) { + SetStartExpectations(); + SetAssociateExpectations(); + SetActivateExpectations(); + SetStopExpectations(); + + EXPECT_EQ(DataTypeController::NOT_RUNNING, extension_dtc_->state()); + + EXPECT_CALL(start_callback_, Run(DataTypeController::OK)); + extension_dtc_->Start(NewCallback(&start_callback_, &StartCallback::Run)); + EXPECT_EQ(DataTypeController::RUNNING, extension_dtc_->state()); + extension_dtc_->Stop(); + EXPECT_EQ(DataTypeController::NOT_RUNNING, extension_dtc_->state()); +} + +// TODO(akalin): Add this test to all the other DTCs. +TEST_F(ExtensionDataTypeControllerTest, OnUnrecoverableError) { + SetStartExpectations(); + SetAssociateExpectations(); + SetActivateExpectations(); + EXPECT_CALL(*model_associator_, ChromeModelHasUserCreatedNodes(_)). + WillRepeatedly(DoAll(SetArgumentPointee<0>(true), Return(true))); + EXPECT_CALL(*model_associator_, SyncModelHasUserCreatedNodes(_)). + WillRepeatedly(DoAll(SetArgumentPointee<0>(true), Return(true))); + EXPECT_CALL(service_, OnUnrecoverableError(_, _)). + WillOnce(InvokeWithoutArgs(extension_dtc_.get(), + &ExtensionDataTypeController::Stop)); + SetStopExpectations(); + + EXPECT_CALL(start_callback_, Run(DataTypeController::OK)); + extension_dtc_->Start(NewCallback(&start_callback_, &StartCallback::Run)); + // This should cause extension_dtc_->Stop() to be called. + extension_dtc_->OnUnrecoverableError(FROM_HERE, "Test"); +} diff --git a/chrome/browser/sync/glue/extension_data_unittest.cc b/chrome/browser/sync/glue/extension_data_unittest.cc new file mode 100644 index 0000000..a5fdddd --- /dev/null +++ b/chrome/browser/sync/glue/extension_data_unittest.cc @@ -0,0 +1,93 @@ +// 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/glue/extension_data.h" + +#include "chrome/browser/sync/glue/extension_util.h" +#include "chrome/browser/sync/protocol/extension_specifics.pb.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace browser_sync { + +namespace { + +class ExtensionDataTest : public testing::Test { +}; + +const char kValidId[] = "abcdefghijklmnopabcdefghijklmnop"; +const char kValidUpdateUrl1[] = "http://www.google.com"; +const char kValidUpdateUrl2[] = "http://www.bing.com"; +const char kValidVersion1[] = "1.0.0.0"; +const char kValidVersion2[] = "1.1.0.0"; + +TEST_F(ExtensionDataTest, FromData) { + sync_pb::ExtensionSpecifics client_data; + client_data.set_id(kValidId); + client_data.set_update_url(kValidUpdateUrl1); + client_data.set_version(kValidVersion1); + ExtensionData extension_data = + ExtensionData::FromData(ExtensionData::CLIENT, client_data); + EXPECT_FALSE(extension_data.NeedsUpdate(ExtensionData::CLIENT)); + EXPECT_TRUE(extension_data.NeedsUpdate(ExtensionData::SERVER)); + EXPECT_TRUE(AreExtensionSpecificsEqual( + client_data, extension_data.merged_data())); +} + +TEST_F(ExtensionDataTest, SetData) { + sync_pb::ExtensionSpecifics client_data; + client_data.set_id(kValidId); + client_data.set_update_url(kValidUpdateUrl1); + client_data.set_version(kValidVersion1); + ExtensionData extension_data = + ExtensionData::FromData(ExtensionData::CLIENT, client_data); + + { + sync_pb::ExtensionSpecifics server_data; + server_data.set_id(kValidId); + server_data.set_update_url(kValidUpdateUrl2); + server_data.set_version(kValidVersion2); + server_data.set_enabled(true); + extension_data.SetData(ExtensionData::SERVER, false, server_data); + EXPECT_TRUE(extension_data.NeedsUpdate(ExtensionData::CLIENT)); + EXPECT_TRUE(extension_data.NeedsUpdate(ExtensionData::SERVER)); + } + + { + sync_pb::ExtensionSpecifics server_data; + server_data.set_id(kValidId); + server_data.set_update_url(kValidUpdateUrl2); + server_data.set_version(kValidVersion2); + server_data.set_enabled(true); + extension_data.SetData(ExtensionData::SERVER, true, server_data); + EXPECT_TRUE(extension_data.NeedsUpdate(ExtensionData::CLIENT)); + EXPECT_FALSE(extension_data.NeedsUpdate(ExtensionData::SERVER)); + EXPECT_TRUE(AreExtensionSpecificsEqual( + server_data, extension_data.merged_data())); + } +} + +TEST_F(ExtensionDataTest, ResolveData) { + sync_pb::ExtensionSpecifics client_data; + client_data.set_id(kValidId); + client_data.set_update_url(kValidUpdateUrl1); + client_data.set_version(kValidVersion1); + ExtensionData extension_data = + ExtensionData::FromData(ExtensionData::CLIENT, client_data); + + sync_pb::ExtensionSpecifics server_data; + server_data.set_id(kValidId); + server_data.set_update_url(kValidUpdateUrl2); + server_data.set_version(kValidVersion2); + extension_data.SetData(ExtensionData::SERVER, true, server_data); + + extension_data.ResolveData(ExtensionData::CLIENT); + EXPECT_FALSE(extension_data.NeedsUpdate(ExtensionData::CLIENT)); + EXPECT_FALSE(extension_data.NeedsUpdate(ExtensionData::SERVER)); + EXPECT_TRUE(AreExtensionSpecificsEqual( + server_data, extension_data.merged_data())); +} + +} // namespace + +} // namespace browser_sync diff --git a/chrome/browser/sync/glue/extension_model_associator.cc b/chrome/browser/sync/glue/extension_model_associator.cc new file mode 100644 index 0000000..e3facec --- /dev/null +++ b/chrome/browser/sync/glue/extension_model_associator.cc @@ -0,0 +1,420 @@ +// 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/glue/extension_model_associator.h" + +#include <map> +#include <utility> + +#include "base/logging.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/extensions/extension_updater.h" +#include "chrome/browser/extensions/extensions_service.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/sync/engine/syncapi.h" +#include "chrome/browser/sync/glue/extension_util.h" +#include "chrome/browser/sync/profile_sync_service.h" +#include "chrome/browser/sync/protocol/extension_specifics.pb.h" +#include "chrome/common/extensions/extension.h" + +namespace browser_sync { + +namespace { + +static const char kExtensionsTag[] = "google_chrome_extensions"; + +static const char kNoExtensionsFolderError[] = + "Server did not create the top-level extensions node. We " + "might be running against an out-of-date server."; + +typedef std::map<std::string, ExtensionData> ExtensionDataMap; + +ExtensionData* SetOrCreateData( + ExtensionDataMap* extension_data_map, + ExtensionData::Source source, + bool merge_user_properties, + const sync_pb::ExtensionSpecifics& data) { + DcheckIsExtensionSpecificsValid(data); + const std::string& extension_id = data.id(); + std::pair<ExtensionDataMap::iterator, bool> result = + extension_data_map->insert( + std::make_pair(extension_id, + ExtensionData::FromData(source, data))); + ExtensionData* extension_data = &result.first->second; + if (result.second) { + // The value was just inserted, so it shouldn't need an update + // from source. + DCHECK(!extension_data->NeedsUpdate(source)); + } else { + extension_data->SetData(source, merge_user_properties, data); + } + return extension_data; +} + +void GetSyncableExtensionsClientData( + const ExtensionList& extensions, + ExtensionsService* extensions_service, + ExtensionDataMap* extension_data_map) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + for (ExtensionList::const_iterator it = extensions.begin(); + it != extensions.end(); ++it) { + CHECK(*it); + const Extension& extension = **it; + if (IsExtensionSyncable(extension)) { + sync_pb::ExtensionSpecifics client_specifics; + GetExtensionSpecifics(extension, extensions_service, + &client_specifics); + DcheckIsExtensionSpecificsValid(client_specifics); + const ExtensionData& extension_data = + *SetOrCreateData(extension_data_map, + ExtensionData::CLIENT, true, client_specifics); + DcheckIsExtensionSpecificsValid(extension_data.merged_data()); + // Assumes this is called before any server data is read. + DCHECK(extension_data.NeedsUpdate(ExtensionData::SERVER)); + DCHECK(!extension_data.NeedsUpdate(ExtensionData::CLIENT)); + } + } +} + +} // namespace + +ExtensionModelAssociator::ExtensionModelAssociator( + ProfileSyncService* sync_service) : sync_service_(sync_service) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + DCHECK(sync_service_); +} + +ExtensionModelAssociator::~ExtensionModelAssociator() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); +} + +bool ExtensionModelAssociator::AssociateModels() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + sync_api::WriteTransaction trans( + sync_service_->backend()->GetUserShareHandle()); + sync_api::ReadNode root(&trans); + if (!root.InitByTagLookup(kExtensionsTag)) { + LOG(ERROR) << kNoExtensionsFolderError; + return false; + } + + ExtensionDataMap extension_data_map; + + // Read client-side data. Do this first so server data takes + // precedence. + { + ExtensionsService* extensions_service = GetExtensionsService(); + + const ExtensionList* extensions = extensions_service->extensions(); + CHECK(extensions); + GetSyncableExtensionsClientData( + *extensions, extensions_service, &extension_data_map); + + const ExtensionList* disabled_extensions = + extensions_service->disabled_extensions(); + CHECK(disabled_extensions); + GetSyncableExtensionsClientData( + *disabled_extensions, extensions_service, &extension_data_map); + } + + // Read server-side data. + { + int64 id = root.GetFirstChildId(); + while (id != sync_api::kInvalidId) { + sync_api::ReadNode sync_node(&trans); + if (!sync_node.InitByIdLookup(id)) { + LOG(ERROR) << "Failed to fetch sync node for id " << id; + return false; + } + const sync_pb::ExtensionSpecifics& server_data = + sync_node.GetExtensionSpecifics(); + if (!IsExtensionSpecificsValid(server_data)) { + LOG(ERROR) << "Invalid extensions specifics for id " << id; + return false; + } + // Pass in false for merge_user_properties so client user + // settings always take precedence. + const ExtensionData& extension_data = + *SetOrCreateData(&extension_data_map, + ExtensionData::SERVER, false, server_data); + DcheckIsExtensionSpecificsValid(extension_data.merged_data()); + id = sync_node.GetSuccessorId(); + } + } + + // Update server and client as necessary. + bool should_nudge_extension_updater = false; + for (ExtensionDataMap::iterator it = extension_data_map.begin(); + it != extension_data_map.end(); ++it) { + ExtensionData* extension_data = &it->second; + // Update server first. + if (extension_data->NeedsUpdate(ExtensionData::SERVER)) { + if (!UpdateServer(extension_data, &trans, root)) { + LOG(ERROR) << "Could not update server data for extension " + << it->first; + return false; + } + } + DCHECK(!extension_data->NeedsUpdate(ExtensionData::SERVER)); + if (extension_data->NeedsUpdate(ExtensionData::CLIENT)) { + TryUpdateClient(extension_data); + if (extension_data->NeedsUpdate(ExtensionData::CLIENT)) { + should_nudge_extension_updater = true; + } + } + DCHECK(!extension_data->NeedsUpdate(ExtensionData::SERVER)); + } + + if (should_nudge_extension_updater) { + NudgeExtensionUpdater(); + } + + return true; +} + +bool ExtensionModelAssociator::DisassociateModels() { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + // Nothing to do. + return true; +} + +bool ExtensionModelAssociator::SyncModelHasUserCreatedNodes(bool* has_nodes) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + CHECK(has_nodes); + *has_nodes = false; + sync_api::ReadTransaction trans( + sync_service_->backend()->GetUserShareHandle()); + sync_api::ReadNode root(&trans); + if (!root.InitByTagLookup(kExtensionsTag)) { + LOG(ERROR) << kNoExtensionsFolderError; + return false; + } + // The sync model has user created nodes iff the extensions folder has + // any children. + *has_nodes = root.GetFirstChildId() != sync_api::kInvalidId; + return true; +} + +bool ExtensionModelAssociator::ChromeModelHasUserCreatedNodes( + bool* has_nodes) { + DCHECK(ChromeThread::CurrentlyOn(ChromeThread::UI)); + CHECK(has_nodes); + // This is wrong, but this function is unused, anyway. + *has_nodes = true; + return true; +} + +bool ExtensionModelAssociator::OnClientUpdate(const std::string& id) { + sync_api::WriteTransaction trans( + sync_service_->backend()->GetUserShareHandle()); + sync_api::ReadNode root(&trans); + if (!root.InitByTagLookup(kExtensionsTag)) { + LOG(ERROR) << kNoExtensionsFolderError; + return false; + } + + sync_pb::ExtensionSpecifics client_data; + if (GetExtensionDataFromClient(id, &client_data)) { + sync_pb::ExtensionSpecifics server_data; + if (!GetExtensionDataFromServer(id, &trans, root, &server_data)) { + LOG(ERROR) << "Could not get server data for extension " << id; + return false; + } + ExtensionData extension_data = + ExtensionData::FromData(ExtensionData::SERVER, server_data); + extension_data.SetData(ExtensionData::CLIENT, true, client_data); + if (extension_data.NeedsUpdate(ExtensionData::SERVER)) { + if (!UpdateServer(&extension_data, &trans, root)) { + LOG(ERROR) << "Could not update server data for extension " << id; + return false; + } + } + DCHECK(!extension_data.NeedsUpdate(ExtensionData::SERVER)); + // Client may still need updating, e.g. if we disable an extension + // while it's being auto-updated. If so, then we'll be called + // again once the auto-update is finished. + // + // TODO(akalin): Figure out a way to tell when the above happens, + // so we know exactly what NeedsUpdate(CLIENT) should return. + } else { + sync_api::WriteNode write_node(&trans); + if (write_node.InitByClientTagLookup(syncable::EXTENSIONS, id)) { + write_node.Remove(); + } else { + LOG(ERROR) << "Trying to remove server data for " + << "nonexistent extension " << id; + } + } + return true; +} + +void ExtensionModelAssociator::OnServerUpdate( + const sync_pb::ExtensionSpecifics& server_data) { + DcheckIsExtensionSpecificsValid(server_data); + sync_pb::ExtensionSpecifics client_data; + if (!GetExtensionDataFromClient(server_data.id(), &client_data)) { + client_data = sync_pb::ExtensionSpecifics(); + } + ExtensionData extension_data = + ExtensionData::FromData(ExtensionData::CLIENT, client_data); + extension_data.SetData(ExtensionData::SERVER, true, server_data); + DCHECK(!extension_data.NeedsUpdate(ExtensionData::SERVER)); + if (extension_data.NeedsUpdate(ExtensionData::CLIENT)) { + TryUpdateClient(&extension_data); + if (extension_data.NeedsUpdate(ExtensionData::CLIENT)) { + NudgeExtensionUpdater(); + } + } + DCHECK(!extension_data.NeedsUpdate(ExtensionData::SERVER)); +} + +void ExtensionModelAssociator::OnServerRemove(const std::string& id) { + ExtensionsService* extensions_service = GetExtensionsService(); + Extension* extension = extensions_service->GetExtensionById(id, true); + if (extension) { + extensions_service->UninstallExtension(id, false); + } else { + LOG(ERROR) << "Trying to uninstall nonexistent extension " << id; + } +} + +ExtensionsService* ExtensionModelAssociator::GetExtensionsService() { + CHECK(sync_service_); + Profile* profile = sync_service_->profile(); + CHECK(profile); + ExtensionsService* extensions_service = profile->GetExtensionsService(); + CHECK(extensions_service); + return extensions_service; +} + +bool ExtensionModelAssociator::GetExtensionDataFromClient( + const std::string& id, sync_pb::ExtensionSpecifics* client_data) { + ExtensionsService* extensions_service = GetExtensionsService(); + Extension* extension = extensions_service->GetExtensionById(id, true); + if (!extension) { + return false; + } + GetExtensionSpecifics(*extension, extensions_service, client_data); + DcheckIsExtensionSpecificsValid(*client_data); + return true; +} + +bool ExtensionModelAssociator::GetExtensionDataFromServer( + const std::string& id, sync_api::WriteTransaction* trans, + const sync_api::ReadNode& root, + sync_pb::ExtensionSpecifics* server_data) { + sync_api::ReadNode sync_node(trans); + if (!sync_node.InitByClientTagLookup(syncable::EXTENSIONS, id)) { + LOG(ERROR) << "Failed to fetch sync node for id " << id; + return false; + } + const sync_pb::ExtensionSpecifics& read_server_data = + sync_node.GetExtensionSpecifics(); + if (!IsExtensionSpecificsValid(read_server_data)) { + LOG(ERROR) << "Invalid extensions specifics for id " << id; + return false; + } + *server_data = read_server_data; + return true; +} + +namespace { + +void SetNodeData(const sync_pb::ExtensionSpecifics& specifics, + sync_api::WriteNode* node) { + node->SetTitle(UTF8ToWide(specifics.name())); + node->SetExtensionSpecifics(specifics); +} + +} // namespace + +bool ExtensionModelAssociator::UpdateServer( + ExtensionData* extension_data, + sync_api::WriteTransaction* trans, + const sync_api::ReadNode& root) { + DCHECK(extension_data->NeedsUpdate(ExtensionData::SERVER)); + const sync_pb::ExtensionSpecifics& specifics = + extension_data->merged_data(); + const std::string& id = specifics.id(); + sync_api::WriteNode write_node(trans); + if (write_node.InitByClientTagLookup(syncable::EXTENSIONS, id)) { + SetNodeData(specifics, &write_node); + } else { + sync_api::WriteNode create_node(trans); + if (!create_node.InitUniqueByCreation(syncable::EXTENSIONS, root, id)) { + LOG(ERROR) << "Could not create node for extension " << id; + return false; + } + SetNodeData(specifics, &create_node); + } + bool old_client_needs_update = + extension_data->NeedsUpdate(ExtensionData::CLIENT); + extension_data->ResolveData(ExtensionData::SERVER); + DCHECK(!extension_data->NeedsUpdate(ExtensionData::SERVER)); + DCHECK_EQ(extension_data->NeedsUpdate(ExtensionData::CLIENT), + old_client_needs_update); + return true; +} + +void ExtensionModelAssociator::TryUpdateClient( + ExtensionData* extension_data) { + DCHECK(!extension_data->NeedsUpdate(ExtensionData::SERVER)); + DCHECK(extension_data->NeedsUpdate(ExtensionData::CLIENT)); + const sync_pb::ExtensionSpecifics& specifics = + extension_data->merged_data(); + DcheckIsExtensionSpecificsValid(specifics); + ExtensionsService* extensions_service = GetExtensionsService(); + const std::string& id = specifics.id(); + Extension* extension = extensions_service->GetExtensionById(id, true); + if (extension) { + SetExtensionProperties(specifics, extensions_service, extension); + { + sync_pb::ExtensionSpecifics extension_specifics; + GetExtensionSpecifics(*extension, extensions_service, + &extension_specifics); + DCHECK(AreExtensionSpecificsUserPropertiesEqual( + specifics, extension_specifics)) + << ExtensionSpecificsToString(specifics) << ", " + << ExtensionSpecificsToString(extension_specifics); + } + if (!IsExtensionOutdated(*extension, specifics)) { + extension_data->ResolveData(ExtensionData::CLIENT); + DCHECK(!extension_data->NeedsUpdate(ExtensionData::CLIENT)); + } + } else { + GURL update_url(specifics.update_url()); + // TODO(akalin): The version number should be used only to + // determine auto-updating permissions, not to send to the + // auto-update server. + scoped_ptr<Version> version( + Version::GetVersionFromString("0.0.0.0")); + CHECK(version.get()); + // TODO(akalin): Replace silent update with a list of enabled + // permissions. + // + // TODO(akalin): Pass through the enabled/incognito_enabled state. + // We can't do it on OnClientUpdate() because we'd run into + // problems with triggering notifications while we're in a + // notification handler. The bug that this causes is that syncing + // a fresh client (i.e., no extensions) re-enables disabled + // extensions. + extensions_service->AddPendingExtension( + id, update_url, *version, false, true); + } + DCHECK(!extension_data->NeedsUpdate(ExtensionData::SERVER)); +} + +void ExtensionModelAssociator::NudgeExtensionUpdater() { + ExtensionUpdater* extension_updater = GetExtensionsService()->updater(); + // Auto-updates should now be on always (see the construction of the + // ExtensionsService in ProfileImpl::InitExtensions()). + if (extension_updater) { + extension_updater->CheckNow(); + } else { + LOG(DFATAL) << "Extension updater unexpectedly NULL; " + << "auto-updates may be turned off"; + } +} + +} // namespace browser_sync diff --git a/chrome/browser/sync/glue/extension_model_associator.h b/chrome/browser/sync/glue/extension_model_associator.h new file mode 100644 index 0000000..98a16fb --- /dev/null +++ b/chrome/browser/sync/glue/extension_model_associator.h @@ -0,0 +1,110 @@ +// 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. + +#ifndef CHROME_BROWSER_SYNC_GLUE_EXTENSION_MODEL_ASSOCIATOR_H_ +#define CHROME_BROWSER_SYNC_GLUE_EXTENSION_MODEL_ASSOCIATOR_H_ + +#include <string> + +#include "base/basictypes.h" +#include "chrome/browser/sync/glue/extension_data.h" +#include "chrome/browser/sync/glue/model_associator.h" +#include "chrome/browser/sync/syncable/model_type.h" +#include "chrome/common/extensions/extension.h" + +class ExtensionsService; +class Profile; +class ProfileSyncService; + +namespace sync_api { +class ReadNode; +class WriteTransaction; +} // namespace sync_api + +namespace sync_pb { +class ExtensionSpecifics; +} // namespace sync_pb + +namespace browser_sync { + +// Contains all logic for associating the Chrome extensions model and +// the sync extensions model. +class ExtensionModelAssociator : public AssociatorInterface { + public: + // Does not take ownership of sync_service. + explicit ExtensionModelAssociator(ProfileSyncService* sync_service); + virtual ~ExtensionModelAssociator(); + + // Used by profile_sync_test_util.h. + static syncable::ModelType model_type() { return syncable::EXTENSIONS; } + + // AssociatorInterface implementation. + virtual bool AssociateModels(); + virtual bool DisassociateModels(); + virtual bool SyncModelHasUserCreatedNodes(bool* has_nodes); + // TODO(akalin): Remove this unused function. + virtual bool ChromeModelHasUserCreatedNodes(bool* has_nodes); + virtual void AbortAssociation() { + // No implementation needed, this associator runs on the main + // thread. + } + + // Used by ExtensionChangeProcessor. + // + // TODO(akalin): These functions can actually be moved to the + // ChangeProcessor after some refactoring. + + // TODO(akalin): Return an error string instead of just a bool. + bool OnClientUpdate(const std::string& id); + void OnServerUpdate(const sync_pb::ExtensionSpecifics& server_data); + void OnServerRemove(const std::string& id); + + private: + // Returns the extension service from |sync_service_|. Never + // returns NULL. + ExtensionsService* GetExtensionsService(); + + bool GetExtensionDataFromClient( + const std::string& id, sync_pb::ExtensionSpecifics* client_data); + + bool GetExtensionDataFromServer( + const std::string& id, sync_api::WriteTransaction* trans, + const sync_api::ReadNode& root, + sync_pb::ExtensionSpecifics* server_data); + + // Updates the server data from the given extension data. + // extension_data->ServerNeedsUpdate() must hold before this + // function is called. Returns whether or not the update was + // successful. If the update was successful, + // extension_data->ServerNeedsUpdate() will be false after this + // function is called. This function leaves + // extension_data->ClientNeedsUpdate() unchanged. + bool UpdateServer(ExtensionData* extension_data, + sync_api::WriteTransaction* trans, + const sync_api::ReadNode& root); + + // Tries to update the client data from the given extension data. + // extension_data->ServerNeedsUpdate() must not hold and + // extension_data->ClientNeedsUpdate() must hold before this + // function is called. If the update was successful, + // extension_data->ClientNeedsUpdate() will be false after this + // function is called. Otherwise, the extension needs updating to a + // new version. + void TryUpdateClient(ExtensionData* extension_data); + + // Kick off a run of the extension updater. + // + // TODO(akalin): Combine this with the similar function in + // theme_util.cc. + void NudgeExtensionUpdater(); + + // Weak pointer. + ProfileSyncService* sync_service_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionModelAssociator); +}; + +} // namespace browser_sync + +#endif // CHROME_BROWSER_SYNC_GLUE_EXTENSION_MODEL_ASSOCIATOR_H_ diff --git a/chrome/browser/sync/glue/extension_util.cc b/chrome/browser/sync/glue/extension_util.cc new file mode 100644 index 0000000..e2b7505 --- /dev/null +++ b/chrome/browser/sync/glue/extension_util.cc @@ -0,0 +1,232 @@ +// 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/glue/extension_util.h" + +#include <sstream> + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/version.h" +#include "chrome/browser/extensions/extensions_service.h" +#include "chrome/browser/sync/protocol/extension_specifics.pb.h" +#include "chrome/common/extensions/extension.h" +#include "googleurl/src/gurl.h" + +namespace browser_sync { + +bool IsExtensionSyncable(const Extension& extension) { + if (extension.IsTheme()) { + return false; + } + + // TODO(akalin): Add Extensions::IsApp(). + // TODO(akalin): Figure out if we want to treat extensions and apps + // identically after all. + if (!extension.GetFullLaunchURL().is_empty()) { + // We have an app. + return false; + } + + // TODO(akalin): Figure out if we need to allow some other types. + if (extension.location() != Extension::INTERNAL) { + // We have a non-standard location. + return false; + } + + return true; +} + +std::string ExtensionSpecificsToString( + const sync_pb::ExtensionSpecifics& specifics) { + std::stringstream ss; + ss << "{ "; + ss << "id: " << specifics.id() << ", "; + ss << "version: " << specifics.version() << ", "; + ss << "update_url: " << specifics.update_url() << ", "; + ss << "enabled: " << specifics.enabled() << ", "; + ss << "incognito_enabled: " << specifics.incognito_enabled() << ", "; + ss << "name: " << specifics.name(); + ss << " }"; + return ss.str(); +} + +bool IsExtensionSpecificsValid( + const sync_pb::ExtensionSpecifics& specifics) { + if (!Extension::IdIsValid(specifics.id())) { + return false; + } + + scoped_ptr<Version> version( + Version::GetVersionFromString(specifics.version())); + if (!version.get()) { + return false; + } + + // The update URL must be either empty or valid. + GURL update_url(specifics.update_url()); + if (!update_url.is_empty() && !update_url.is_valid()) { + return false; + } + + return true; +} + +void DcheckIsExtensionSpecificsValid( + const sync_pb::ExtensionSpecifics& specifics) { + DCHECK(IsExtensionSpecificsValid(specifics)) + << ExtensionSpecificsToString(specifics); +} + +bool AreExtensionSpecificsEqual(const sync_pb::ExtensionSpecifics& a, + const sync_pb::ExtensionSpecifics& b) { + // TODO(akalin): Figure out if we have to worry about version/URL + // strings that are not identical but map to the same object. + return ((a.id() == b.id()) && + (a.version() == b.version()) && + (a.update_url() == b.update_url()) && + (a.enabled() == b.enabled()) && + (a.incognito_enabled() == b.incognito_enabled()) && + (a.name() == b.name())); +} + +bool IsExtensionSpecificsUnset( + const sync_pb::ExtensionSpecifics& specifics) { + return AreExtensionSpecificsEqual(specifics, + sync_pb::ExtensionSpecifics()); +} + +void CopyUserProperties( + const sync_pb::ExtensionSpecifics& specifics, + sync_pb::ExtensionSpecifics* dest_specifics) { + DCHECK(dest_specifics); + dest_specifics->set_enabled(specifics.enabled()); + dest_specifics->set_incognito_enabled(specifics.incognito_enabled()); +} + +void CopyNonUserProperties( + const sync_pb::ExtensionSpecifics& specifics, + sync_pb::ExtensionSpecifics* dest_specifics) { + DCHECK(dest_specifics); + sync_pb::ExtensionSpecifics old_dest_specifics(*dest_specifics); + *dest_specifics = specifics; + CopyUserProperties(old_dest_specifics, dest_specifics); +} + +bool AreExtensionSpecificsUserPropertiesEqual( + const sync_pb::ExtensionSpecifics& a, + const sync_pb::ExtensionSpecifics& b) { + sync_pb::ExtensionSpecifics a_user_properties, b_user_properties; + CopyUserProperties(a, &a_user_properties); + CopyUserProperties(b, &b_user_properties); + return AreExtensionSpecificsEqual(a_user_properties, b_user_properties); +} + +bool AreExtensionSpecificsNonUserPropertiesEqual( + const sync_pb::ExtensionSpecifics& a, + const sync_pb::ExtensionSpecifics& b) { + sync_pb::ExtensionSpecifics a_non_user_properties, b_non_user_properties; + CopyNonUserProperties(a, &a_non_user_properties); + CopyNonUserProperties(b, &b_non_user_properties); + return AreExtensionSpecificsEqual( + a_non_user_properties, b_non_user_properties); +} + +void GetExtensionSpecifics(const Extension& extension, + ExtensionsService* extensions_service, + sync_pb::ExtensionSpecifics* specifics) { + const std::string& id = extension.id(); + bool enabled = + extensions_service->GetExtensionById(id, false) != NULL; + bool incognito_enabled = + extensions_service->IsIncognitoEnabled(&extension); + GetExtensionSpecificsHelper(extension, enabled, incognito_enabled, + specifics); +} + +void GetExtensionSpecificsHelper(const Extension& extension, + bool enabled, bool incognito_enabled, + sync_pb::ExtensionSpecifics* specifics) { + DCHECK(IsExtensionSyncable(extension)); + const std::string& id = extension.id(); + specifics->set_id(id); + specifics->set_version(extension.VersionString()); + specifics->set_update_url(extension.update_url().spec()); + specifics->set_enabled(enabled); + specifics->set_incognito_enabled(incognito_enabled); + specifics->set_name(extension.name()); + DcheckIsExtensionSpecificsValid(*specifics); +} + +bool IsExtensionOutdated(const Extension& extension, + const sync_pb::ExtensionSpecifics& specifics) { + DCHECK(IsExtensionSyncable(extension)); + DcheckIsExtensionSpecificsValid(specifics); + scoped_ptr<Version> specifics_version( + Version::GetVersionFromString(specifics.version())); + if (!specifics_version.get()) { + // If version is invalid, assume we're up-to-date. + return false; + } + return extension.version()->CompareTo(*specifics_version) < 0; +} + +void SetExtensionProperties( + const sync_pb::ExtensionSpecifics& specifics, + ExtensionsService* extensions_service, Extension* extension) { + DcheckIsExtensionSpecificsValid(specifics); + CHECK(extensions_service); + CHECK(extension); + DCHECK(IsExtensionSyncable(*extension)); + const std::string& id = extension->id(); + GURL update_url(specifics.update_url()); + if (update_url != extension->update_url()) { + LOG(WARNING) << "specifics for extension " << id + << "has a different update URL than the extension: " + << update_url.spec() << " vs. " << extension->update_url(); + } + bool enabled = extensions_service->GetExtensionById(id, false) != NULL; + if (enabled && !specifics.enabled()) { + extensions_service->DisableExtension(id); + } else if (!enabled && specifics.enabled()) { + extensions_service->EnableExtension(id); + } + bool incognito_enabled = + extensions_service->IsIncognitoEnabled(extension); + if (incognito_enabled != specifics.incognito_enabled()) { + extensions_service->SetIsIncognitoEnabled( + extension, specifics.incognito_enabled()); + } + if (specifics.name() != extension->name()) { + LOG(WARNING) << "specifics for extension " << id + << "has a different name than the extension: " + << specifics.name() << " vs. " << extension->name(); + } +} + +void MergeExtensionSpecifics( + const sync_pb::ExtensionSpecifics& specifics, + bool merge_user_properties, + sync_pb::ExtensionSpecifics* merged_specifics) { + DcheckIsExtensionSpecificsValid(*merged_specifics); + DcheckIsExtensionSpecificsValid(specifics); + DCHECK_EQ(specifics.id(), merged_specifics->id()); + // TODO(akalin): Merge enabled permissions when we sync those. + scoped_ptr<Version> version( + Version::GetVersionFromString(specifics.version())); + CHECK(version.get()); + scoped_ptr<Version> merged_version( + Version::GetVersionFromString(merged_specifics->version())); + CHECK(merged_version.get()); + if (version->CompareTo(*merged_version) >= 0) { + // |specifics| has a more recent or the same version, so merge it + // in. + CopyNonUserProperties(specifics, merged_specifics); + if (merge_user_properties) { + CopyUserProperties(specifics, merged_specifics); + } + } +} + +} // namespace browser_sync diff --git a/chrome/browser/sync/glue/extension_util.h b/chrome/browser/sync/glue/extension_util.h new file mode 100644 index 0000000..0eef52f --- /dev/null +++ b/chrome/browser/sync/glue/extension_util.h @@ -0,0 +1,112 @@ +// 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. + +#ifndef CHROME_BROWSER_SYNC_GLUE_EXTENSION_UTIL_H_ +#define CHROME_BROWSER_SYNC_GLUE_EXTENSION_UTIL_H_ + +#include <string> + +class Extension; +class ExtensionsService; + +namespace sync_pb { +class ExtensionSpecifics; +} // sync_pb + +namespace browser_sync { + +// Returns whether or not the given extension is one we want to sync. +bool IsExtensionSyncable(const Extension& extension); + +// Stringifies the given ExtensionSpecifics. +std::string ExtensionSpecificsToString( + const sync_pb::ExtensionSpecifics& specifics); + +// Returns whether or not the values of the given specifics are valid, +// in particular the id, version, and update URL. +bool IsExtensionSpecificsValid( + const sync_pb::ExtensionSpecifics& specifics); + +// Equivalent to DCHECK(IsExtensionSpecificsValid(specifics)) << +// ExtensionSpecificsToString(specifics); +void DcheckIsExtensionSpecificsValid( + const sync_pb::ExtensionSpecifics& specifics); + +// Returns true iff two ExtensionSpecifics denote the same extension +// state. Neither |a| nor |b| need to be valid. +bool AreExtensionSpecificsEqual(const sync_pb::ExtensionSpecifics& a, + const sync_pb::ExtensionSpecifics& b); + +// Returns true iff the given ExtensionSpecifics is equal to the empty +// ExtensionSpecifics object. |specifics| does not have to be valid +// and indeed, IsExtensionSpecificsValid(specifics) -> +// !IsExtensionSpecificsUnset(specifics). +bool IsExtensionSpecificsUnset( + const sync_pb::ExtensionSpecifics& specifics); + +// Copies the user properties from |specifics| into |dest_specifics|. +// User properties are properties that are set by the user, i.e. not +// inherent to the extension. Currently they include |enabled| and +// |incognito_enabled|. Neither parameter need be valid. +void CopyUserProperties( + const sync_pb::ExtensionSpecifics& specifics, + sync_pb::ExtensionSpecifics* dest_specifics); + +// Copies everything but non-user properties. Neither parameter need +// be valid. +void CopyNonUserProperties( + const sync_pb::ExtensionSpecifics& specifics, + sync_pb::ExtensionSpecifics* dest_specifics); + +// Returns true iff two ExtensionSpecifics have the same user +// properties. Neither |a| nor |b| need to be valid. +bool AreExtensionSpecificsUserPropertiesEqual( + const sync_pb::ExtensionSpecifics& a, + const sync_pb::ExtensionSpecifics& b); + +// Returns true iff two ExtensionSpecifics have the same non-user +// properties. Neither |a| nor |b| need to be valid. +bool AreExtensionSpecificsNonUserPropertiesEqual( + const sync_pb::ExtensionSpecifics& a, + const sync_pb::ExtensionSpecifics& b); + +// Fills |specifics| with information taken from |extension|, which +// must be a syncable extension. |specifics| will be valid after this +// function is called. +void GetExtensionSpecifics(const Extension& extension, + ExtensionsService* extensions_service, + sync_pb::ExtensionSpecifics* specifics); + +// Exposed only for testing. Pre- and post-conditions are the same as +// GetExtensionSpecifics(). +void GetExtensionSpecificsHelper(const Extension& extension, + bool enabled, bool incognito_enabled, + sync_pb::ExtensionSpecifics* specifics); + +// Returns whether or not the extension should be updated according to +// the specifics. |extension| must be syncable and |specifics| must +// be valid. +bool IsExtensionOutdated(const Extension& extension, + const sync_pb::ExtensionSpecifics& specifics); + +// Sets properties of |extension| according to the information in +// specifics. |extension| must be syncable and |specifics| must be +// valid. +void SetExtensionProperties( + const sync_pb::ExtensionSpecifics& specifics, + ExtensionsService* extensions_service, Extension* extension); + +// Merge |specifics| into |merged_specifics|. Both must be valid and +// have the same ID. The merge policy is currently to copy the +// non-user properties of |specifics| into |merged_specifics| (and the +// user properties if |merge_user_properties| is set) if |specifics| +// has a more recent or the same version as |merged_specifics|. +void MergeExtensionSpecifics( + const sync_pb::ExtensionSpecifics& specifics, + bool merge_user_properties, + sync_pb::ExtensionSpecifics* merged_specifics); + +} // namespace browser_sync + +#endif // CHROME_BROWSER_SYNC_GLUE_EXTENSION_UTIL_H_ diff --git a/chrome/browser/sync/glue/extension_util_unittest.cc b/chrome/browser/sync/glue/extension_util_unittest.cc new file mode 100644 index 0000000..9094838 --- /dev/null +++ b/chrome/browser/sync/glue/extension_util_unittest.cc @@ -0,0 +1,420 @@ +// 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/glue/extension_util.h" + +#include "base/file_path.h" +#include "base/version.h" +#include "chrome/browser/sync/protocol/extension_specifics.pb.h" +#include "chrome/common/extensions/extension.h" +#include "chrome/common/extensions/extension_constants.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace browser_sync { + +namespace { + +#if defined(OS_WIN) +const FilePath::CharType kExtensionFilePath[] = FILE_PATH_LITERAL("c:\\foo"); +#elif defined(OS_POSIX) +const FilePath::CharType kExtensionFilePath[] = FILE_PATH_LITERAL("/foo"); +#endif + +const char kValidId[] = "abcdefghijklmnopabcdefghijklmnop"; +const char kValidVersion[] = "0.0.0.0"; +const char kVersion1[] = "1.0.0.1"; +const char kVersion2[] = "1.0.1.0"; +const char kVersion3[] = "1.1.0.0"; +const char kValidUpdateUrl[] = "http://www.google.com/"; +const char kValidUpdateUrl1[] = "http://www.1.com/"; +const char kValidUpdateUrl2[] = "http://www.2.com/"; +const char kName[] = "MyExtension"; +const char kName2[] = "MyExtension2"; + +class ExtensionUtilTest : public testing::Test { +}; + +void MakePossiblySyncableExtension(bool is_theme, + const GURL& launch_url, + Extension::Location location, + Extension* extension) { + DictionaryValue source; + source.SetString(extension_manifest_keys::kName, + "PossiblySyncableExtension"); + source.SetString(extension_manifest_keys::kVersion, "0.0.0.0"); + if (is_theme) { + source.Set(extension_manifest_keys::kTheme, new DictionaryValue()); + } + if (!launch_url.is_empty()) { + source.SetString(extension_manifest_keys::kLaunchWebURL, + launch_url.spec()); + } + std::string error; + EXPECT_TRUE(extension->InitFromValue(source, false, &error)); + EXPECT_EQ("", error); + extension->set_location(location); +} + +TEST_F(ExtensionUtilTest, IsSyncableExtension) { + { + FilePath file_path(kExtensionFilePath); + Extension extension(file_path); + MakePossiblySyncableExtension(false, GURL(), Extension::INTERNAL, + &extension); + EXPECT_TRUE(IsExtensionSyncable(extension)); + } + { + FilePath file_path(kExtensionFilePath); + Extension extension(file_path); + MakePossiblySyncableExtension(true, GURL(), Extension::INTERNAL, + &extension); + EXPECT_FALSE(IsExtensionSyncable(extension)); + } + // TODO(akalin): Test with a non-empty launch_url once apps are + // enabled by default. + { + FilePath file_path(kExtensionFilePath); + Extension extension(file_path); + MakePossiblySyncableExtension(false, GURL(), Extension::EXTERNAL_PREF, + &extension); + EXPECT_FALSE(IsExtensionSyncable(extension)); + } +} + +TEST_F(ExtensionUtilTest, IsExtensionSpecificsUnset) { + { + sync_pb::ExtensionSpecifics specifics; + EXPECT_TRUE(IsExtensionSpecificsUnset(specifics)); + } + + { + sync_pb::ExtensionSpecifics specifics; + specifics.set_id("a"); + EXPECT_FALSE(IsExtensionSpecificsUnset(specifics)); + } + + { + sync_pb::ExtensionSpecifics specifics; + specifics.set_version("a"); + EXPECT_FALSE(IsExtensionSpecificsUnset(specifics)); + } + + { + sync_pb::ExtensionSpecifics specifics; + specifics.set_update_url("a"); + EXPECT_FALSE(IsExtensionSpecificsUnset(specifics)); + } + + { + sync_pb::ExtensionSpecifics specifics; + specifics.set_enabled(true); + EXPECT_FALSE(IsExtensionSpecificsUnset(specifics)); + } + + { + sync_pb::ExtensionSpecifics specifics; + specifics.set_incognito_enabled(true); + EXPECT_FALSE(IsExtensionSpecificsUnset(specifics)); + } + + { + sync_pb::ExtensionSpecifics specifics; + specifics.set_name("a"); + EXPECT_FALSE(IsExtensionSpecificsUnset(specifics)); + } +} + +TEST_F(ExtensionUtilTest, IsExtensionSpecificsValid) { + sync_pb::ExtensionSpecifics specifics; + EXPECT_FALSE(IsExtensionSpecificsValid(specifics)); + specifics.set_id(kValidId); + EXPECT_FALSE(IsExtensionSpecificsValid(specifics)); + specifics.set_version(kValidVersion); + EXPECT_TRUE(IsExtensionSpecificsValid(specifics)); + EXPECT_FALSE(IsExtensionSpecificsUnset(specifics)); + specifics.set_update_url(kValidUpdateUrl); + EXPECT_TRUE(IsExtensionSpecificsValid(specifics)); + EXPECT_FALSE(IsExtensionSpecificsUnset(specifics)); + + { + sync_pb::ExtensionSpecifics specifics_copy(specifics); + specifics_copy.set_id("invalid"); + EXPECT_FALSE(IsExtensionSpecificsValid(specifics_copy)); + } + + { + sync_pb::ExtensionSpecifics specifics_copy(specifics); + specifics_copy.set_version("invalid"); + EXPECT_FALSE(IsExtensionSpecificsValid(specifics_copy)); + } + + { + sync_pb::ExtensionSpecifics specifics_copy(specifics); + specifics_copy.set_update_url("http:invalid.com:invalid"); + EXPECT_FALSE(IsExtensionSpecificsValid(specifics_copy)); + } +} + +TEST_F(ExtensionUtilTest, AreExtensionSpecificsEqual) { + sync_pb::ExtensionSpecifics a, b; + EXPECT_TRUE(AreExtensionSpecificsEqual(a, b)); + + a.set_id("a"); + EXPECT_FALSE(AreExtensionSpecificsEqual(a, b)); + b.set_id("a"); + EXPECT_TRUE(AreExtensionSpecificsEqual(a, b)); + + a.set_version("1.5"); + EXPECT_FALSE(AreExtensionSpecificsEqual(a, b)); + b.set_version("1.5"); + EXPECT_TRUE(AreExtensionSpecificsEqual(a, b)); + + a.set_update_url("http://www.foo.com"); + EXPECT_FALSE(AreExtensionSpecificsEqual(a, b)); + b.set_update_url("http://www.foo.com"); + EXPECT_TRUE(AreExtensionSpecificsEqual(a, b)); + + a.set_enabled(true); + EXPECT_FALSE(AreExtensionSpecificsEqual(a, b)); + b.set_enabled(true); + EXPECT_TRUE(AreExtensionSpecificsEqual(a, b)); + + a.set_incognito_enabled(true); + EXPECT_FALSE(AreExtensionSpecificsEqual(a, b)); + b.set_incognito_enabled(true); + EXPECT_TRUE(AreExtensionSpecificsEqual(a, b)); + + a.set_name("name"); + EXPECT_FALSE(AreExtensionSpecificsEqual(a, b)); + b.set_name("name"); + EXPECT_TRUE(AreExtensionSpecificsEqual(a, b)); +} + +TEST_F(ExtensionUtilTest, CopyUserProperties) { + sync_pb::ExtensionSpecifics dest_specifics; + dest_specifics.set_version(kVersion2); + dest_specifics.set_update_url(kValidUpdateUrl1); + dest_specifics.set_enabled(true); + dest_specifics.set_incognito_enabled(false); + dest_specifics.set_name(kName); + + sync_pb::ExtensionSpecifics specifics; + specifics.set_id(kValidId); + specifics.set_version(kVersion3); + specifics.set_update_url(kValidUpdateUrl2); + specifics.set_enabled(false); + specifics.set_incognito_enabled(true); + specifics.set_name(kName2); + + CopyUserProperties(specifics, &dest_specifics); + EXPECT_EQ("", dest_specifics.id()); + EXPECT_EQ(kVersion2, dest_specifics.version()); + EXPECT_EQ(kValidUpdateUrl1, dest_specifics.update_url()); + EXPECT_FALSE(dest_specifics.enabled()); + EXPECT_TRUE(dest_specifics.incognito_enabled()); + EXPECT_EQ(kName, dest_specifics.name()); +} + +TEST_F(ExtensionUtilTest, CopyNonUserProperties) { + sync_pb::ExtensionSpecifics dest_specifics; + dest_specifics.set_id(kValidId); + dest_specifics.set_version(kVersion2); + dest_specifics.set_update_url(kValidUpdateUrl1); + dest_specifics.set_enabled(true); + dest_specifics.set_incognito_enabled(false); + dest_specifics.set_name(kName); + + sync_pb::ExtensionSpecifics specifics; + specifics.set_id(""); + specifics.set_version(kVersion3); + specifics.set_update_url(kValidUpdateUrl2); + specifics.set_enabled(false); + specifics.set_incognito_enabled(true); + specifics.set_name(kName2); + + CopyNonUserProperties(specifics, &dest_specifics); + EXPECT_EQ("", dest_specifics.id()); + EXPECT_EQ(kVersion3, dest_specifics.version()); + EXPECT_EQ(kValidUpdateUrl2, dest_specifics.update_url()); + EXPECT_TRUE(dest_specifics.enabled()); + EXPECT_FALSE(dest_specifics.incognito_enabled()); + EXPECT_EQ(kName2, dest_specifics.name()); +} + +TEST_F(ExtensionUtilTest, AreExtensionSpecificsUserPropertiesEqual) { + sync_pb::ExtensionSpecifics a, b; + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual(a, b)); + + a.set_id("a"); + b.set_id("b"); + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual(a, b)); + + a.set_version("1.5"); + b.set_version("1.6"); + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual(a, b)); + + a.set_name("name"); + b.set_name("name2"); + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual(a, b)); + + a.set_update_url("http://www.foo.com"); + b.set_update_url("http://www.foo2.com"); + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual(a, b)); + + a.set_enabled(true); + EXPECT_FALSE(AreExtensionSpecificsUserPropertiesEqual(a, b)); + b.set_enabled(true); + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual(a, b)); + + a.set_incognito_enabled(true); + EXPECT_FALSE(AreExtensionSpecificsUserPropertiesEqual(a, b)); + b.set_incognito_enabled(true); + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual(a, b)); +} + +TEST_F(ExtensionUtilTest, AreExtensionSpecificsNonUserPropertiesEqual) { + sync_pb::ExtensionSpecifics a, b; + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + + a.set_enabled(true); + b.set_enabled(false); + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + + a.set_incognito_enabled(true); + b.set_incognito_enabled(false); + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + + a.set_id("a"); + EXPECT_FALSE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + b.set_id("a"); + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + + a.set_version("1.5"); + EXPECT_FALSE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + b.set_version("1.5"); + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + + a.set_update_url("http://www.foo.com"); + EXPECT_FALSE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + b.set_update_url("http://www.foo.com"); + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + + a.set_name("name"); + EXPECT_FALSE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); + b.set_name("name"); + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual(a, b)); +} + +void MakeSyncableExtension(const std::string& version_string, + const std::string& update_url_spec, + const std::string& name, + Extension* extension) { + DictionaryValue source; + source.SetString(extension_manifest_keys::kVersion, version_string); + source.SetString(extension_manifest_keys::kUpdateURL, update_url_spec); + source.SetString(extension_manifest_keys::kName, name); + std::string error; + EXPECT_TRUE(extension->InitFromValue(source, false, &error)); + EXPECT_EQ("", error); + extension->set_location(Extension::INTERNAL); +} + +TEST_F(ExtensionUtilTest, GetExtensionSpecificsHelper) { + FilePath file_path(kExtensionFilePath); + Extension extension(file_path); + MakeSyncableExtension(kValidVersion, kValidUpdateUrl, kName, + &extension); + sync_pb::ExtensionSpecifics specifics; + GetExtensionSpecificsHelper(extension, true, false, &specifics); + EXPECT_EQ(extension.id(), specifics.id()); + EXPECT_EQ(extension.VersionString(), kValidVersion); + EXPECT_EQ(extension.update_url().spec(), kValidUpdateUrl); + EXPECT_TRUE(specifics.enabled()); + EXPECT_FALSE(specifics.incognito_enabled()); + EXPECT_EQ(kName, specifics.name()); +} + +TEST_F(ExtensionUtilTest, IsExtensionOutdated) { + FilePath file_path(kExtensionFilePath); + Extension extension(file_path); + MakeSyncableExtension(kVersion2, kValidUpdateUrl, kName, + &extension); + sync_pb::ExtensionSpecifics specifics; + specifics.set_id(kValidId); + specifics.set_update_url(kValidUpdateUrl); + + specifics.set_version(kVersion1); + EXPECT_FALSE(IsExtensionOutdated(extension, specifics)); + specifics.set_version(kVersion2); + EXPECT_FALSE(IsExtensionOutdated(extension, specifics)); + specifics.set_version(kVersion3); + EXPECT_TRUE(IsExtensionOutdated(extension, specifics)); +} + +// TODO(akalin): Make ExtensionsService/ExtensionUpdater testable +// enough to be able to write a unittest for SetExtensionProperties(). + +TEST_F(ExtensionUtilTest, MergeExtensionSpecificsWithUserProperties) { + sync_pb::ExtensionSpecifics merged_specifics; + merged_specifics.set_id(kValidId); + merged_specifics.set_update_url(kValidUpdateUrl1); + merged_specifics.set_enabled(true); + merged_specifics.set_incognito_enabled(false); + merged_specifics.set_version(kVersion2); + + sync_pb::ExtensionSpecifics specifics; + specifics.set_id(kValidId); + specifics.set_update_url(kValidUpdateUrl2); + merged_specifics.set_enabled(false); + merged_specifics.set_incognito_enabled(true); + + specifics.set_version(kVersion1); + { + sync_pb::ExtensionSpecifics result = merged_specifics; + MergeExtensionSpecifics(specifics, false, &result); + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual( + result, merged_specifics)); + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual( + result, merged_specifics)); + } + { + sync_pb::ExtensionSpecifics result = merged_specifics; + MergeExtensionSpecifics(specifics, true, &result); + EXPECT_TRUE(AreExtensionSpecificsEqual(result, merged_specifics)); + } + + specifics.set_version(kVersion2); + { + sync_pb::ExtensionSpecifics result = merged_specifics; + MergeExtensionSpecifics(specifics, false, &result); + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual( + result, merged_specifics)); + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual( + result, specifics)); + } + { + sync_pb::ExtensionSpecifics result = merged_specifics; + MergeExtensionSpecifics(specifics, true, &result); + EXPECT_TRUE(AreExtensionSpecificsEqual(result, specifics)); + } + + specifics.set_version(kVersion3); + { + sync_pb::ExtensionSpecifics result = merged_specifics; + MergeExtensionSpecifics(specifics, false, &result); + EXPECT_TRUE(AreExtensionSpecificsUserPropertiesEqual( + result, merged_specifics)); + EXPECT_TRUE(AreExtensionSpecificsNonUserPropertiesEqual( + result, specifics)); + } + { + sync_pb::ExtensionSpecifics result = merged_specifics; + MergeExtensionSpecifics(specifics, true, &result); + EXPECT_TRUE(AreExtensionSpecificsEqual(result, specifics)); + } +} + +} // namespace + +} // namespace browser_sync diff --git a/chrome/browser/sync/profile_sync_factory.h b/chrome/browser/sync/profile_sync_factory.h index d2cb6e5..f8b16e7 100644 --- a/chrome/browser/sync/profile_sync_factory.h +++ b/chrome/browser/sync/profile_sync_factory.h @@ -73,6 +73,13 @@ class ProfileSyncFactory { browser_sync::UnrecoverableErrorHandler* error_handler) = 0; // Instantiates both a model associator and change processor for the + // extension data type. The pointers in the return struct are + // owned by the caller. + virtual SyncComponents CreateExtensionSyncComponents( + ProfileSyncService* profile_sync_service, + browser_sync::UnrecoverableErrorHandler* error_handler) = 0; + + // Instantiates both a model associator and change processor for the // password data type. The pointers in the return struct are // owned by the caller. virtual SyncComponents CreatePasswordSyncComponents( diff --git a/chrome/browser/sync/profile_sync_factory_impl.cc b/chrome/browser/sync/profile_sync_factory_impl.cc index 0c75936..f0d698f 100644 --- a/chrome/browser/sync/profile_sync_factory_impl.cc +++ b/chrome/browser/sync/profile_sync_factory_impl.cc @@ -13,6 +13,9 @@ #include "chrome/browser/sync/glue/bookmark_data_type_controller.h" #include "chrome/browser/sync/glue/bookmark_model_associator.h" #include "chrome/browser/sync/glue/data_type_manager_impl.h" +#include "chrome/browser/sync/glue/extension_change_processor.h" +#include "chrome/browser/sync/glue/extension_data_type_controller.h" +#include "chrome/browser/sync/glue/extension_model_associator.h" #include "chrome/browser/sync/glue/password_change_processor.h" #include "chrome/browser/sync/glue/password_data_type_controller.h" #include "chrome/browser/sync/glue/password_model_associator.h" @@ -40,6 +43,9 @@ using browser_sync::BookmarkModelAssociator; using browser_sync::DataTypeController; using browser_sync::DataTypeManager; using browser_sync::DataTypeManagerImpl; +using browser_sync::ExtensionChangeProcessor; +using browser_sync::ExtensionDataTypeController; +using browser_sync::ExtensionModelAssociator; using browser_sync::PasswordChangeProcessor; using browser_sync::PasswordDataTypeController; using browser_sync::PasswordModelAssociator; @@ -88,6 +94,13 @@ ProfileSyncService* ProfileSyncFactoryImpl::CreateProfileSyncService() { new BookmarkDataTypeController(this, profile_, pss)); } + // Extension sync is disabled by default. Register only if + // explicitly enabled. + if (command_line_->HasSwitch(switches::kEnableSyncExtensions)) { + pss->RegisterDataTypeController( + new ExtensionDataTypeController(this, profile_, pss)); + } + // Password sync is disabled by default. Register only if // explicitly enabled. if (command_line_->HasSwitch(switches::kEnableSyncPasswords)) { @@ -156,6 +169,17 @@ ProfileSyncFactoryImpl::CreateBookmarkSyncComponents( } ProfileSyncFactory::SyncComponents +ProfileSyncFactoryImpl::CreateExtensionSyncComponents( + ProfileSyncService* profile_sync_service, + UnrecoverableErrorHandler* error_handler) { + ExtensionModelAssociator* model_associator = + new ExtensionModelAssociator(profile_sync_service); + ExtensionChangeProcessor* change_processor = + new ExtensionChangeProcessor(error_handler, model_associator); + return SyncComponents(model_associator, change_processor); +} + +ProfileSyncFactory::SyncComponents ProfileSyncFactoryImpl::CreatePasswordSyncComponents( ProfileSyncService* profile_sync_service, PasswordStore* password_store, diff --git a/chrome/browser/sync/profile_sync_factory_impl.h b/chrome/browser/sync/profile_sync_factory_impl.h index bbce500..c26ba4f 100644 --- a/chrome/browser/sync/profile_sync_factory_impl.h +++ b/chrome/browser/sync/profile_sync_factory_impl.h @@ -41,6 +41,10 @@ class ProfileSyncFactoryImpl : public ProfileSyncFactory { ProfileSyncService* profile_sync_service, browser_sync::UnrecoverableErrorHandler* error_handler); + virtual SyncComponents CreateExtensionSyncComponents( + ProfileSyncService* profile_sync_service, + browser_sync::UnrecoverableErrorHandler* error_handler); + virtual SyncComponents CreatePasswordSyncComponents( ProfileSyncService* profile_sync_service, PasswordStore* password_store, diff --git a/chrome/browser/sync/profile_sync_factory_mock.h b/chrome/browser/sync/profile_sync_factory_mock.h index df760c5..a323b34 100644 --- a/chrome/browser/sync/profile_sync_factory_mock.h +++ b/chrome/browser/sync/profile_sync_factory_mock.h @@ -37,6 +37,9 @@ class ProfileSyncFactoryMock : public ProfileSyncFactory { MOCK_METHOD2(CreateBookmarkSyncComponents, SyncComponents(ProfileSyncService* profile_sync_service, browser_sync::UnrecoverableErrorHandler* error_handler)); + MOCK_METHOD2(CreateExtensionSyncComponents, + SyncComponents(ProfileSyncService* profile_sync_service, + browser_sync::UnrecoverableErrorHandler* error_handler)); MOCK_METHOD3(CreatePasswordSyncComponents, SyncComponents( ProfileSyncService* profile_sync_service, diff --git a/chrome/browser/sync/syncable/model_type.cc b/chrome/browser/sync/syncable/model_type.cc index 204f65d..8aefe40 100644 --- a/chrome/browser/sync/syncable/model_type.cc +++ b/chrome/browser/sync/syncable/model_type.cc @@ -113,6 +113,8 @@ std::string ModelTypeToString(ModelType model_type) { return "Themes"; case TYPED_URLS: return "Typed URLs"; + case EXTENSIONS: + return "Extensions"; default: NOTREACHED() << "No known extension for model type."; return "INVALID"; diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 9e80c17..2535b6d 100755 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -2105,6 +2105,16 @@ 'browser/sync/glue/data_type_manager_impl.h', 'browser/sync/glue/database_model_worker.cc', 'browser/sync/glue/database_model_worker.h', + 'browser/sync/glue/extension_change_processor.cc', + 'browser/sync/glue/extension_change_processor.h', + 'browser/sync/glue/extension_data.cc', + 'browser/sync/glue/extension_data.h', + 'browser/sync/glue/extension_data_type_controller.cc', + 'browser/sync/glue/extension_data_type_controller.h', + 'browser/sync/glue/extension_model_associator.cc', + 'browser/sync/glue/extension_model_associator.h', + 'browser/sync/glue/extension_util.cc', + 'browser/sync/glue/extension_util.h', 'browser/sync/glue/history_model_worker.cc', 'browser/sync/glue/history_model_worker.h', 'browser/sync/glue/password_model_worker.cc', diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 6c61324..7786f58 100755 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -918,6 +918,9 @@ 'browser/sync/glue/data_type_manager_impl_unittest.cc', 'browser/sync/glue/data_type_manager_mock.h', 'browser/sync/glue/database_model_worker_unittest.cc', + 'browser/sync/glue/extension_data_unittest.cc', + 'browser/sync/glue/extension_data_type_controller_unittest.cc', + 'browser/sync/glue/extension_util_unittest.cc', 'browser/sync/glue/http_bridge_unittest.cc', 'browser/sync/glue/preference_data_type_controller_unittest.cc', 'browser/sync/glue/sync_backend_host_mock.h', |