| // Copyright 2013 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/extensions/extension_sync_service.h" |
| |
| #include <iterator> |
| |
| #include "base/basictypes.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/threading/sequenced_worker_pool.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "chrome/browser/extensions/app_sync_data.h" |
| #include "chrome/browser/extensions/bookmark_app_helper.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/extension_sync_data.h" |
| #include "chrome/browser/extensions/extension_sync_service_factory.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/extensions/launch_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sync/glue/sync_start_util.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/common/extensions/manifest_handlers/app_launch_info.h" |
| #include "chrome/common/extensions/sync_helper.h" |
| #include "chrome/common/web_application_info.h" |
| #include "components/sync_driver/sync_prefs.h" |
| #include "extensions/browser/app_sorting.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_icon_set.h" |
| #include "extensions/common/feature_switch.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/manifest_handlers/icons_handler.h" |
| #include "sync/api/sync_change.h" |
| #include "sync/api/sync_error_factory.h" |
| #include "ui/gfx/image/image_family.h" |
| |
| using extensions::Extension; |
| using extensions::ExtensionPrefs; |
| using extensions::ExtensionRegistry; |
| using extensions::FeatureSwitch; |
| |
| namespace { |
| |
| void OnWebApplicationInfoLoaded( |
| WebApplicationInfo synced_info, |
| base::WeakPtr<ExtensionService> extension_service, |
| const WebApplicationInfo& loaded_info) { |
| DCHECK_EQ(synced_info.app_url, loaded_info.app_url); |
| |
| if (!extension_service) |
| return; |
| |
| // Use the old icons if they exist. |
| synced_info.icons = loaded_info.icons; |
| CreateOrUpdateBookmarkApp(extension_service.get(), synced_info); |
| } |
| |
| } // namespace |
| |
| ExtensionSyncService::ExtensionSyncService(Profile* profile, |
| ExtensionPrefs* extension_prefs, |
| ExtensionService* extension_service) |
| : profile_(profile), |
| extension_prefs_(extension_prefs), |
| extension_service_(extension_service), |
| app_sync_bundle_(this), |
| extension_sync_bundle_(this), |
| pending_app_enables_(make_scoped_ptr(new sync_driver::SyncPrefs( |
| extension_prefs_->pref_service())), |
| &app_sync_bundle_, |
| syncer::APPS), |
| pending_extension_enables_(make_scoped_ptr(new sync_driver::SyncPrefs( |
| extension_prefs_->pref_service())), |
| &extension_sync_bundle_, |
| syncer::EXTENSIONS) { |
| SetSyncStartFlare(sync_start_util::GetFlareForSyncableService( |
| profile_->GetPath())); |
| |
| extension_service_->set_extension_sync_service(this); |
| extension_prefs_->app_sorting()->SetExtensionSyncService(this); |
| } |
| |
| ExtensionSyncService::~ExtensionSyncService() {} |
| |
| // static |
| ExtensionSyncService* ExtensionSyncService::Get(Profile* profile) { |
| return ExtensionSyncServiceFactory::GetForProfile(profile); |
| } |
| |
| syncer::SyncChange ExtensionSyncService::PrepareToSyncUninstallExtension( |
| const extensions::Extension* extension, bool extensions_ready) { |
| // Extract the data we need for sync now, but don't actually sync until we've |
| // completed the uninstallation. |
| // TODO(tim): If we get here and IsSyncing is false, this will cause |
| // "back from the dead" style bugs, because sync will add-back the extension |
| // that was uninstalled here when MergeDataAndStartSyncing is called. |
| // See crbug.com/256795. |
| if (extensions::util::ShouldSyncApp(extension, profile_)) { |
| if (app_sync_bundle_.IsSyncing()) |
| return app_sync_bundle_.CreateSyncChangeToDelete(extension); |
| else if (extensions_ready && !flare_.is_null()) |
| flare_.Run(syncer::APPS); // Tell sync to start ASAP. |
| } else if (extensions::sync_helper::IsSyncableExtension(extension)) { |
| if (extension_sync_bundle_.IsSyncing()) |
| return extension_sync_bundle_.CreateSyncChangeToDelete(extension); |
| else if (extensions_ready && !flare_.is_null()) |
| flare_.Run(syncer::EXTENSIONS); // Tell sync to start ASAP. |
| } |
| |
| return syncer::SyncChange(); |
| } |
| |
| void ExtensionSyncService::ProcessSyncUninstallExtension( |
| const std::string& extension_id, |
| const syncer::SyncChange& sync_change) { |
| if (app_sync_bundle_.HasExtensionId(extension_id) && |
| sync_change.sync_data().GetDataType() == syncer::APPS) { |
| app_sync_bundle_.ProcessDeletion(extension_id, sync_change); |
| } else if (extension_sync_bundle_.HasExtensionId(extension_id) && |
| sync_change.sync_data().GetDataType() == syncer::EXTENSIONS) { |
| extension_sync_bundle_.ProcessDeletion(extension_id, sync_change); |
| } |
| } |
| |
| void ExtensionSyncService::SyncEnableExtension( |
| const extensions::Extension& extension) { |
| |
| // Syncing may not have started yet, so handle pending enables. |
| if (extensions::util::ShouldSyncApp(&extension, profile_)) |
| pending_app_enables_.OnExtensionEnabled(extension.id()); |
| |
| if (extensions::util::ShouldSyncExtension(&extension, profile_)) |
| pending_extension_enables_.OnExtensionEnabled(extension.id()); |
| |
| SyncExtensionChangeIfNeeded(extension); |
| } |
| |
| void ExtensionSyncService::SyncDisableExtension( |
| const extensions::Extension& extension) { |
| |
| // Syncing may not have started yet, so handle pending enables. |
| if (extensions::util::ShouldSyncApp(&extension, profile_)) |
| pending_app_enables_.OnExtensionDisabled(extension.id()); |
| |
| if (extensions::util::ShouldSyncExtension(&extension, profile_)) |
| pending_extension_enables_.OnExtensionDisabled(extension.id()); |
| |
| SyncExtensionChangeIfNeeded(extension); |
| } |
| |
| syncer::SyncMergeResult ExtensionSyncService::MergeDataAndStartSyncing( |
| syncer::ModelType type, |
| const syncer::SyncDataList& initial_sync_data, |
| scoped_ptr<syncer::SyncChangeProcessor> sync_processor, |
| scoped_ptr<syncer::SyncErrorFactory> sync_error_factory) { |
| CHECK(sync_processor.get()); |
| CHECK(sync_error_factory.get()); |
| |
| switch (type) { |
| case syncer::EXTENSIONS: |
| extension_sync_bundle_.SetupSync(sync_processor.release(), |
| sync_error_factory.release(), |
| initial_sync_data); |
| pending_extension_enables_.OnSyncStarted(extension_service_); |
| break; |
| |
| case syncer::APPS: |
| app_sync_bundle_.SetupSync(sync_processor.release(), |
| sync_error_factory.release(), |
| initial_sync_data); |
| pending_app_enables_.OnSyncStarted(extension_service_); |
| break; |
| |
| default: |
| LOG(FATAL) << "Got " << type << " ModelType"; |
| } |
| |
| // Process local extensions. |
| // TODO(yoz): Determine whether pending extensions should be considered too. |
| // See crbug.com/104399. |
| syncer::SyncDataList sync_data_list = GetAllSyncData(type); |
| syncer::SyncChangeList sync_change_list; |
| for (syncer::SyncDataList::const_iterator i = sync_data_list.begin(); |
| i != sync_data_list.end(); |
| ++i) { |
| switch (type) { |
| case syncer::EXTENSIONS: |
| sync_change_list.push_back( |
| extension_sync_bundle_.CreateSyncChange(*i)); |
| break; |
| case syncer::APPS: |
| sync_change_list.push_back(app_sync_bundle_.CreateSyncChange(*i)); |
| break; |
| default: |
| LOG(FATAL) << "Got " << type << " ModelType"; |
| } |
| } |
| |
| |
| if (type == syncer::EXTENSIONS) { |
| extension_sync_bundle_.ProcessSyncChangeList(sync_change_list); |
| } else if (type == syncer::APPS) { |
| app_sync_bundle_.ProcessSyncChangeList(sync_change_list); |
| } |
| |
| return syncer::SyncMergeResult(type); |
| } |
| |
| void ExtensionSyncService::StopSyncing(syncer::ModelType type) { |
| if (type == syncer::APPS) { |
| app_sync_bundle_.Reset(); |
| } else if (type == syncer::EXTENSIONS) { |
| extension_sync_bundle_.Reset(); |
| } |
| } |
| |
| syncer::SyncDataList ExtensionSyncService::GetAllSyncData( |
| syncer::ModelType type) const { |
| if (type == syncer::EXTENSIONS) |
| return extension_sync_bundle_.GetAllSyncData(); |
| if (type == syncer::APPS) |
| return app_sync_bundle_.GetAllSyncData(); |
| |
| // We should only get sync data for extensions and apps. |
| NOTREACHED(); |
| |
| return syncer::SyncDataList(); |
| } |
| |
| syncer::SyncError ExtensionSyncService::ProcessSyncChanges( |
| const tracked_objects::Location& from_here, |
| const syncer::SyncChangeList& change_list) { |
| for (syncer::SyncChangeList::const_iterator i = change_list.begin(); |
| i != change_list.end(); |
| ++i) { |
| syncer::ModelType type = i->sync_data().GetDataType(); |
| if (type == syncer::EXTENSIONS) { |
| extension_sync_bundle_.ProcessSyncChange( |
| extensions::ExtensionSyncData(*i)); |
| } else if (type == syncer::APPS) { |
| app_sync_bundle_.ProcessSyncChange(extensions::AppSyncData(*i)); |
| } |
| } |
| |
| extension_prefs_->app_sorting()->FixNTPOrdinalCollisions(); |
| |
| return syncer::SyncError(); |
| } |
| |
| extensions::ExtensionSyncData ExtensionSyncService::GetExtensionSyncData( |
| const Extension& extension) const { |
| return extensions::ExtensionSyncData( |
| extension, |
| extension_service_->IsExtensionEnabled(extension.id()), |
| extensions::util::IsIncognitoEnabled(extension.id(), profile_), |
| extension_prefs_->HasDisableReason(extension.id(), |
| Extension::DISABLE_REMOTE_INSTALL)); |
| } |
| |
| extensions::AppSyncData ExtensionSyncService::GetAppSyncData( |
| const Extension& extension) const { |
| return extensions::AppSyncData( |
| extension, |
| extension_service_->IsExtensionEnabled(extension.id()), |
| extensions::util::IsIncognitoEnabled(extension.id(), profile_), |
| extension_prefs_->HasDisableReason(extension.id(), |
| Extension::DISABLE_REMOTE_INSTALL), |
| extension_prefs_->app_sorting()->GetAppLaunchOrdinal(extension.id()), |
| extension_prefs_->app_sorting()->GetPageOrdinal(extension.id()), |
| extensions::GetLaunchTypePrefValue(extension_prefs_, extension.id())); |
| } |
| |
| std::vector<extensions::ExtensionSyncData> |
| ExtensionSyncService::GetExtensionSyncDataList() const { |
| ExtensionRegistry* registry = ExtensionRegistry::Get(profile_); |
| std::vector<extensions::ExtensionSyncData> extension_sync_list; |
| extension_sync_bundle_.GetExtensionSyncDataListHelper( |
| registry->enabled_extensions(), &extension_sync_list); |
| extension_sync_bundle_.GetExtensionSyncDataListHelper( |
| registry->disabled_extensions(), &extension_sync_list); |
| extension_sync_bundle_.GetExtensionSyncDataListHelper( |
| registry->terminated_extensions(), &extension_sync_list); |
| |
| std::vector<extensions::ExtensionSyncData> pending_extensions = |
| extension_sync_bundle_.GetPendingData(); |
| extension_sync_list.insert(extension_sync_list.begin(), |
| pending_extensions.begin(), |
| pending_extensions.end()); |
| |
| return extension_sync_list; |
| } |
| |
| std::vector<extensions::AppSyncData> ExtensionSyncService::GetAppSyncDataList() |
| const { |
| ExtensionRegistry* registry = ExtensionRegistry::Get(profile_); |
| std::vector<extensions::AppSyncData> app_sync_list; |
| app_sync_bundle_.GetAppSyncDataListHelper( |
| registry->enabled_extensions(), &app_sync_list); |
| app_sync_bundle_.GetAppSyncDataListHelper( |
| registry->disabled_extensions(), &app_sync_list); |
| app_sync_bundle_.GetAppSyncDataListHelper( |
| registry->terminated_extensions(), &app_sync_list); |
| |
| std::vector<extensions::AppSyncData> pending_apps = |
| app_sync_bundle_.GetPendingData(); |
| app_sync_list.insert(app_sync_list.begin(), |
| pending_apps.begin(), |
| pending_apps.end()); |
| |
| return app_sync_list; |
| } |
| |
| bool ExtensionSyncService::ProcessExtensionSyncData( |
| const extensions::ExtensionSyncData& extension_sync_data) { |
| if (!ProcessExtensionSyncDataHelper(extension_sync_data, |
| syncer::EXTENSIONS)) { |
| extension_sync_bundle_.AddPendingExtension(extension_sync_data.id(), |
| extension_sync_data); |
| extension_service_->CheckForUpdatesSoon(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool ExtensionSyncService::ProcessAppSyncData( |
| const extensions::AppSyncData& app_sync_data) { |
| const std::string& id = app_sync_data.id(); |
| |
| if (app_sync_data.app_launch_ordinal().IsValid() && |
| app_sync_data.page_ordinal().IsValid()) { |
| extension_prefs_->app_sorting()->SetAppLaunchOrdinal( |
| id, |
| app_sync_data.app_launch_ordinal()); |
| extension_prefs_->app_sorting()->SetPageOrdinal( |
| id, |
| app_sync_data.page_ordinal()); |
| } |
| |
| // The corresponding validation of this value during AppSyncData population |
| // is in AppSyncData::PopulateAppSpecifics. |
| if (app_sync_data.launch_type() >= extensions::LAUNCH_TYPE_FIRST && |
| app_sync_data.launch_type() < extensions::NUM_LAUNCH_TYPES) { |
| extensions::SetLaunchType(extension_service_, id, |
| app_sync_data.launch_type()); |
| } |
| |
| if (!app_sync_data.bookmark_app_url().empty()) |
| ProcessBookmarkAppSyncData(app_sync_data); |
| |
| if (!ProcessExtensionSyncDataHelper(app_sync_data.extension_sync_data(), |
| syncer::APPS)) { |
| app_sync_bundle_.AddPendingApp(id, app_sync_data); |
| extension_service_->CheckForUpdatesSoon(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void ExtensionSyncService::ProcessBookmarkAppSyncData( |
| const extensions::AppSyncData& app_sync_data) { |
| // Process bookmark app sync if necessary. |
| GURL bookmark_app_url(app_sync_data.bookmark_app_url()); |
| if (!bookmark_app_url.is_valid() || |
| app_sync_data.extension_sync_data().uninstalled()) { |
| return; |
| } |
| |
| const extensions::Extension* extension = |
| extension_service_->GetInstalledExtension( |
| app_sync_data.extension_sync_data().id()); |
| |
| // Return if there are no bookmark app details that need updating. |
| if (extension && extension->non_localized_name() == |
| app_sync_data.extension_sync_data().name() && |
| extension->description() == app_sync_data.bookmark_app_description()) { |
| return; |
| } |
| |
| WebApplicationInfo web_app_info; |
| web_app_info.app_url = bookmark_app_url; |
| web_app_info.title = |
| base::UTF8ToUTF16(app_sync_data.extension_sync_data().name()); |
| web_app_info.description = |
| base::UTF8ToUTF16(app_sync_data.bookmark_app_description()); |
| |
| // If the bookmark app already exists, keep the old icons. |
| if (!extension) { |
| CreateOrUpdateBookmarkApp(extension_service_, web_app_info); |
| } else { |
| app_sync_data.extension_sync_data().name(); |
| GetWebApplicationInfoFromApp(profile_, |
| extension, |
| base::Bind(&OnWebApplicationInfoLoaded, |
| web_app_info, |
| extension_service_->AsWeakPtr())); |
| } |
| } |
| |
| void ExtensionSyncService::SyncOrderingChange(const std::string& extension_id) { |
| const extensions::Extension* ext = extension_service_->GetInstalledExtension( |
| extension_id); |
| |
| if (ext) |
| SyncExtensionChangeIfNeeded(*ext); |
| } |
| |
| void ExtensionSyncService::SetSyncStartFlare( |
| const syncer::SyncableService::StartSyncFlare& flare) { |
| flare_ = flare; |
| } |
| |
| bool ExtensionSyncService::IsCorrectSyncType(const Extension& extension, |
| syncer::ModelType type) const { |
| if (type == syncer::EXTENSIONS && |
| extensions::sync_helper::IsSyncableExtension(&extension)) { |
| return true; |
| } |
| |
| if (type == syncer::APPS && |
| extensions::sync_helper::IsSyncableApp(&extension)) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool ExtensionSyncService::IsPendingEnable( |
| const std::string& extension_id) const { |
| return pending_app_enables_.Contains(extension_id) || |
| pending_extension_enables_.Contains(extension_id); |
| } |
| |
| bool ExtensionSyncService::ProcessExtensionSyncDataHelper( |
| const extensions::ExtensionSyncData& extension_sync_data, |
| syncer::ModelType type) { |
| const std::string& id = extension_sync_data.id(); |
| const Extension* extension = extension_service_->GetInstalledExtension(id); |
| |
| // TODO(bolms): we should really handle this better. The particularly bad |
| // case is where an app becomes an extension or vice versa, and we end up with |
| // a zombie extension that won't go away. |
| if (extension && !IsCorrectSyncType(*extension, type)) |
| return true; |
| |
| // Handle uninstalls first. |
| if (extension_sync_data.uninstalled()) { |
| if (!extension_service_->UninstallExtensionHelper(extension_service_, id)) { |
| LOG(WARNING) << "Could not uninstall extension " << id |
| << " for sync"; |
| } |
| return true; |
| } |
| |
| // Extension from sync was uninstalled by the user as external extensions. |
| // Honor user choice and skip installation/enabling. |
| if (extensions::ExtensionPrefs::Get(profile_) |
| ->IsExternalExtensionUninstalled(id)) { |
| LOG(WARNING) << "Extension with id " << id |
| << " from sync was uninstalled as external extension"; |
| return true; |
| } |
| |
| // Set user settings. |
| // If the extension has been disabled from sync, it may not have |
| // been installed yet, so we don't know if the disable reason was a |
| // permissions increase. That will be updated once CheckPermissionsIncrease |
| // is called for it. |
| // However if the extension is marked as a remote install in sync, we know |
| // what the disable reason is, so set it to that directly. Note that when |
| // CheckPermissionsIncrease runs, it might still add permissions increase |
| // as a disable reason for the extension. |
| if (extension_sync_data.enabled()) { |
| extension_service_->EnableExtension(id); |
| } else if (!IsPendingEnable(id)) { |
| if (extension_sync_data.remote_install()) { |
| extension_service_->DisableExtension(id, |
| Extension::DISABLE_REMOTE_INSTALL); |
| } else { |
| extension_service_->DisableExtension( |
| id, Extension::DISABLE_UNKNOWN_FROM_SYNC); |
| } |
| } |
| |
| // We need to cache some version information here because setting the |
| // incognito flag invalidates the |extension| pointer (it reloads the |
| // extension). |
| bool extension_installed = (extension != NULL); |
| int version_compare_result = extension ? |
| extension->version()->CompareTo(extension_sync_data.version()) : 0; |
| |
| // If the target extension has already been installed ephemerally, it can |
| // be promoted to a regular installed extension and downloading from the Web |
| // Store is not necessary. |
| if (extension && extensions::util::IsEphemeralApp(id, profile_)) |
| extension_service_->PromoteEphemeralApp(extension, true); |
| |
| // Update the incognito flag. |
| extensions::util::SetIsIncognitoEnabled( |
| id, profile_, extension_sync_data.incognito_enabled()); |
| extension = NULL; // No longer safe to use. |
| |
| if (extension_installed) { |
| // If the extension is already installed, check if it's outdated. |
| if (version_compare_result < 0) { |
| // Extension is outdated. |
| return false; |
| } |
| } else { |
| // TODO(akalin): Replace silent update with a list of enabled |
| // permissions. |
| const bool kInstallSilently = true; |
| |
| CHECK(type == syncer::EXTENSIONS || type == syncer::APPS); |
| extensions::PendingExtensionInfo::ShouldAllowInstallPredicate filter = |
| (type == syncer::APPS) ? extensions::sync_helper::IsSyncableApp : |
| extensions::sync_helper::IsSyncableExtension; |
| |
| if (!extension_service_->pending_extension_manager()->AddFromSync( |
| id, |
| extension_sync_data.update_url(), |
| filter, |
| kInstallSilently, |
| extension_sync_data.remote_install())) { |
| LOG(WARNING) << "Could not add pending extension for " << id; |
| // This means that the extension is already pending installation, with a |
| // non-INTERNAL location. Add to pending_sync_data, even though it will |
| // never be removed (we'll never install a syncable version of the |
| // extension), so that GetAllSyncData() continues to send it. |
| } |
| // Track pending extensions so that we can return them in GetAllSyncData(). |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void ExtensionSyncService::SyncExtensionChangeIfNeeded( |
| const Extension& extension) { |
| if (extensions::util::ShouldSyncApp(&extension, profile_)) { |
| if (app_sync_bundle_.IsSyncing()) |
| app_sync_bundle_.SyncChangeIfNeeded(extension); |
| else if (extension_service_->is_ready() && !flare_.is_null()) |
| flare_.Run(syncer::APPS); |
| } else if (extensions::util::ShouldSyncExtension(&extension, profile_)) { |
| if (extension_sync_bundle_.IsSyncing()) |
| extension_sync_bundle_.SyncChangeIfNeeded(extension); |
| else if (extension_service_->is_ready() && !flare_.is_null()) |
| flare_.Run(syncer::EXTENSIONS); |
| } |
| } |