// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/sync/sync_setup_flow.h" #include "base/callback.h" #include "base/command_line.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/metrics/histogram.h" #include "base/string_util.h" #include "base/utf_string_conversions.h" #include "base/values.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/sync/profile_sync_service.h" #include "chrome/browser/sync/sync_setup_flow_handler.h" #include "chrome/browser/sync/syncable/model_type.h" #include "chrome/browser/sync/util/oauth.h" #include "chrome/browser/ui/browser_list.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/net/gaia/google_service_auth_error.h" #include "chrome/common/pref_names.h" #include "chrome/common/url_constants.h" #include "grit/generated_resources.h" namespace { // Helper function to disable password sync. void DisablePasswordSync(ProfileSyncService* service) { syncable::ModelTypeSet types; service->GetPreferredDataTypes(&types); types.erase(syncable::PASSWORDS); service->OnUserChoseDatatypes(false, types); } // Returns the next step for the non-fatal error case. SyncSetupWizard::State GetStepForNonFatalError(ProfileSyncService* service) { // TODO(sync): Update this error handling to allow different platforms to // display the error appropriately (http://crbug.com/92722) instead of // navigating to a LOGIN state that is not supported on every platform. if (service->IsPassphraseRequired()) { if (service->IsUsingSecondaryPassphrase()) return SyncSetupWizard::ENTER_PASSPHRASE; return SyncSetupWizard::GetLoginState(); } const GoogleServiceAuthError& error = service->GetAuthError(); if (error.state() == GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS || error.state() == GoogleServiceAuthError::CAPTCHA_REQUIRED || error.state() == GoogleServiceAuthError::ACCOUNT_DELETED || error.state() == GoogleServiceAuthError::ACCOUNT_DISABLED || error.state() == GoogleServiceAuthError::SERVICE_UNAVAILABLE) return SyncSetupWizard::GetLoginState(); NOTREACHED(); return SyncSetupWizard::FATAL_ERROR; } } // namespace SyncConfiguration::SyncConfiguration() : encrypt_all(false), sync_everything(false), set_secondary_passphrase(false), set_gaia_passphrase(false) { } SyncConfiguration::~SyncConfiguration() {} SyncSetupFlow::~SyncSetupFlow() { flow_handler_->SetFlow(NULL); } // static SyncSetupFlow* SyncSetupFlow::Run(ProfileSyncService* service, SyncSetupFlowContainer* container, SyncSetupWizard::State start, SyncSetupWizard::State end) { if (start == SyncSetupWizard::NONFATAL_ERROR) start = GetStepForNonFatalError(service); if ((start == SyncSetupWizard::CONFIGURE || start == SyncSetupWizard::SYNC_EVERYTHING || start == SyncSetupWizard::ENTER_PASSPHRASE) && !service->sync_initialized()) { // We are trying to open configuration window, but the backend isn't ready. // We just return NULL. This has the effect of the flow getting reset, and // the user's action has no effect. LOG(ERROR) << "Attempted to show sync configure before backend ready."; return NULL; } return new SyncSetupFlow(start, end, container, service); } // static void SyncSetupFlow::GetArgsForGaiaLogin(const ProfileSyncService* service, DictionaryValue* args) { const GoogleServiceAuthError& error = service->GetAuthError(); if (!service->last_attempted_user_email().empty()) { args->SetString("user", service->last_attempted_user_email()); args->SetInteger("error", error.state()); args->SetBoolean("editable_user", true); } else { string16 user; if (!service->cros_user().empty()) user = UTF8ToUTF16(service->cros_user()); else user = service->GetAuthenticatedUsername(); args->SetString("user", user); args->SetInteger("error", 0); args->SetBoolean("editable_user", user.empty()); } args->SetString("captchaUrl", error.captcha().image_url.spec()); } void SyncSetupFlow::GetArgsForConfigure(ProfileSyncService* service, DictionaryValue* args) { // The SYNC_EVERYTHING case will set this to true. args->SetBoolean("showSyncEverythingPage", false); args->SetBoolean("syncAllDataTypes", service->profile()->GetPrefs()->GetBoolean( prefs::kSyncKeepEverythingSynced)); // Bookmarks, Preferences, and Themes are launched for good, there's no // going back now. Check if the other data types are registered though. syncable::ModelTypeSet registered_types; service->GetRegisteredDataTypes(®istered_types); args->SetBoolean("passwordsRegistered", registered_types.count(syncable::PASSWORDS) > 0); args->SetBoolean("autofillRegistered", registered_types.count(syncable::AUTOFILL) > 0); args->SetBoolean("extensionsRegistered", registered_types.count(syncable::EXTENSIONS) > 0); args->SetBoolean("typedUrlsRegistered", registered_types.count(syncable::TYPED_URLS) > 0); args->SetBoolean("appsRegistered", registered_types.count(syncable::APPS) > 0); args->SetBoolean("searchEnginesRegistered", registered_types.count(syncable::SEARCH_ENGINES) > 0); args->SetBoolean("sessionsRegistered", registered_types.count(syncable::SESSIONS) > 0); args->SetBoolean("appNotificationsRegistered", registered_types.count(syncable::APP_NOTIFICATIONS) > 0); args->SetBoolean("syncBookmarks", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncBookmarks)); args->SetBoolean("syncPreferences", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncPreferences)); args->SetBoolean("syncThemes", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncThemes)); args->SetBoolean("syncPasswords", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncPasswords)); args->SetBoolean("syncAutofill", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncAutofill)); args->SetBoolean("syncExtensions", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncExtensions)); args->SetBoolean("syncSearchEngines", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncSearchEngines)); args->SetBoolean("syncSessions", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncSessions)); args->SetBoolean("syncTypedUrls", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncTypedUrls)); args->SetBoolean("syncApps", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncApps)); args->SetBoolean("syncAppNotifications", service->profile()->GetPrefs()->GetBoolean(prefs::kSyncAppNotifications)); args->SetBoolean("encryptionEnabled", !CommandLine::ForCurrentProcess()->HasSwitch( switches::kDisableSyncEncryption)); bool encrypt_all = service->EncryptEverythingEnabled(); if (service->encryption_pending()) encrypt_all = true; args->SetBoolean("encryptAllData", encrypt_all); // Load the parameters for the encryption tab. args->SetBoolean("usePassphrase", service->IsUsingSecondaryPassphrase()); // Determine if we need a passphrase or not, and if so, prompt the user. if (service->IsPassphraseRequiredForDecryption() && (service->IsUsingSecondaryPassphrase() || cached_passphrase_.empty())) { // We need a passphrase, and either it's an explicit passphrase, or we // don't have a cached gaia passphrase, so we have to prompt the user. args->SetBoolean("show_passphrase", true); // Tell the UI layer what kind of passphrase we need. args->SetBoolean("need_google_passphrase", !service->IsUsingSecondaryPassphrase()); args->SetBoolean("passphrase_creation_rejected", user_tried_creating_explicit_passphrase_); args->SetBoolean("passphrase_setting_rejected", user_tried_setting_passphrase_); } } bool SyncSetupFlow::AttachSyncSetupHandler(SyncSetupFlowHandler* handler) { if (flow_handler_) return false; flow_handler_ = handler; handler->SetFlow(this); ActivateState(current_state_); return true; } bool SyncSetupFlow::IsAttached() const { return flow_handler_ != NULL; } void SyncSetupFlow::Advance(SyncSetupWizard::State advance_state) { if (!ShouldAdvance(advance_state)) { LOG(WARNING) << "Invalid state change from " << current_state_ << " to " << advance_state; return; } if (flow_handler_) ActivateState(advance_state); } void SyncSetupFlow::Focus() { // This gets called from SyncSetupWizard::Focus(), and might get called // before flow_handler_ is set in AttachSyncSetupHandler() (which gets // called asynchronously after the UI initializes). if (flow_handler_) flow_handler_->Focus(); } // A callback to notify the delegate that the dialog closed. // TODO(rickcam): Bug 90713: Handle OAUTH_LOGIN case here void SyncSetupFlow::OnDialogClosed(const std::string& json_retval) { DCHECK(json_retval.empty()); container_->set_flow(NULL); // Sever ties from the wizard. // If we've reached the end, mark it. This could be a discrete run, in which // case it's already set, but it simplifes the logic to do it this way. if (current_state_ == end_state_) service_->SetSyncSetupCompleted(); // Record the state at which the user cancelled the signon dialog. switch (current_state_) { case SyncSetupWizard::GAIA_LOGIN: ProfileSyncService::SyncEvent( ProfileSyncService::CANCEL_FROM_SIGNON_WITHOUT_AUTH); break; case SyncSetupWizard::GAIA_SUCCESS: ProfileSyncService::SyncEvent( ProfileSyncService::CANCEL_DURING_SIGNON); break; case SyncSetupWizard::CONFIGURE: case SyncSetupWizard::ENTER_PASSPHRASE: case SyncSetupWizard::SETTING_UP: // TODO(atwilson): Treat a close during ENTER_PASSPHRASE like a // Cancel + Skip (i.e. call OnPassphraseCancel()). http://crbug.com/74645 ProfileSyncService::SyncEvent( ProfileSyncService::CANCEL_DURING_CONFIGURE); break; case SyncSetupWizard::DONE: // TODO(sync): rename this histogram; it's tracking authorization AND // initial sync download time. UMA_HISTOGRAM_MEDIUM_TIMES("Sync.UserPerceivedAuthorizationTime", base::TimeTicks::Now() - login_start_time_); break; default: break; } service_->OnUserCancelledDialog(); delete this; } void SyncSetupFlow::OnUserSubmittedAuth(const std::string& username, const std::string& password, const std::string& captcha, const std::string& access_code) { // It's possible to receive an empty password (e.g. for ASP's), in which case // we don't want to overwrite any previously cached password. if (!password.empty()) cached_passphrase_ = password; service_->OnUserSubmittedAuth(username, password, captcha, access_code); } void SyncSetupFlow::OnUserSubmittedOAuth( const std::string& oauth1_request_token) { service_->OnUserSubmittedOAuth(oauth1_request_token); } void SyncSetupFlow::OnUserConfigured(const SyncConfiguration& configuration) { // Go to the "loading..." screen. Advance(SyncSetupWizard::SETTING_UP); // Note: encryption will not occur until OnUserChoseDatatypes is called. service_->SetEncryptEverything(configuration.encrypt_all); bool set_new_decryption_passphrase = false; if (configuration.set_gaia_passphrase) { // Caller passed a gaia passphrase. This is illegal if we are currently // using a secondary passphrase. DCHECK(!service_->IsUsingSecondaryPassphrase()); service_->SetPassphrase(configuration.gaia_passphrase, false); // Since the user entered the passphrase manually, set this flag so we can // report an error if the passphrase setting failed. user_tried_setting_passphrase_ = true; set_new_decryption_passphrase = true; } else if (!service_->IsUsingSecondaryPassphrase() && !cached_passphrase_.empty()) { // Service needs a GAIA passphrase and we have one cached, so try it. service_->SetPassphrase(cached_passphrase_, false); cached_passphrase_.clear(); set_new_decryption_passphrase = true; } else { // We can get here if the user changes their GAIA passphrase but still has // data encrypted with the old passphrase. The UI prompts the user for their // passphrase, but they might just leave it blank/disable the encrypted // types. // No gaia passphrase cached or set, so make sure the ProfileSyncService // wasn't expecting one. DLOG_IF(WARNING, !service_->IsUsingSecondaryPassphrase() && service_->IsPassphraseRequiredForDecryption()) << "Google passphrase required but not provided by UI"; } // Set the secondary passphrase, either as a decryption passphrase, or // as an attempt to encrypt the user's data using this new passphrase. if (configuration.set_secondary_passphrase) { service_->SetPassphrase(configuration.secondary_passphrase, true); if (service_->IsUsingSecondaryPassphrase()) { user_tried_setting_passphrase_ = true; set_new_decryption_passphrase = true; } else { user_tried_creating_explicit_passphrase_ = true; } } service_->OnUserChoseDatatypes(configuration.sync_everything, configuration.data_types); // See if we are done configuring (if we don't need a passphrase, and don't // need to hang around waiting for encryption to happen, just exit). This call // to IsPassphraseRequiredForDecryption() takes into account the data types // we just enabled/disabled. if (!service_->IsPassphraseRequiredForDecryption() && !service_->encryption_pending()) { Advance(SyncSetupWizard::DONE); } else if (!set_new_decryption_passphrase) { // We need a passphrase, but the user did not provide one, so transition // directly to ENTER_PASSPHRASE (otherwise we'll have to wait until // the sync engine generates another OnPassphraseRequired() at the end of // the sync cycle which can take a long time). Advance(SyncSetupWizard::ENTER_PASSPHRASE); } } void SyncSetupFlow::OnPassphraseEntry(const std::string& passphrase) { Advance(SyncSetupWizard::SETTING_UP); service_->SetPassphrase(passphrase, true); user_tried_setting_passphrase_ = true; } void SyncSetupFlow::OnPassphraseCancel() { // If the user cancels when being asked for the passphrase, // just disable encrypted sync and continue setting up. if (current_state_ == SyncSetupWizard::ENTER_PASSPHRASE) DisablePasswordSync(service_); Advance(SyncSetupWizard::SETTING_UP); } // Use static Run method to get an instance. SyncSetupFlow::SyncSetupFlow(SyncSetupWizard::State start_state, SyncSetupWizard::State end_state, SyncSetupFlowContainer* container, ProfileSyncService* service) : container_(container), current_state_(start_state), end_state_(end_state), login_start_time_(base::TimeTicks::Now()), flow_handler_(NULL), service_(service), user_tried_creating_explicit_passphrase_(false), user_tried_setting_passphrase_(false) { } // Returns true if the flow should advance to |state| based on |current_state_|. bool SyncSetupFlow::ShouldAdvance(SyncSetupWizard::State state) { switch (state) { case SyncSetupWizard::OAUTH_LOGIN: return current_state_ == SyncSetupWizard::FATAL_ERROR || current_state_ == SyncSetupWizard::OAUTH_LOGIN || current_state_ == SyncSetupWizard::SETTING_UP; case SyncSetupWizard::GAIA_LOGIN: return current_state_ == SyncSetupWizard::FATAL_ERROR || current_state_ == SyncSetupWizard::GAIA_LOGIN || current_state_ == SyncSetupWizard::SETTING_UP; case SyncSetupWizard::GAIA_SUCCESS: return current_state_ == SyncSetupWizard::GAIA_LOGIN || current_state_ == SyncSetupWizard::OAUTH_LOGIN; case SyncSetupWizard::SYNC_EVERYTHING: // Intentionally fall through. case SyncSetupWizard::CONFIGURE: return current_state_ == SyncSetupWizard::GAIA_SUCCESS; case SyncSetupWizard::ENTER_PASSPHRASE: return current_state_ == SyncSetupWizard::SYNC_EVERYTHING || current_state_ == SyncSetupWizard::CONFIGURE || current_state_ == SyncSetupWizard::SETTING_UP; case SyncSetupWizard::SETUP_ABORTED_BY_PENDING_CLEAR: return current_state_ != SyncSetupWizard::ABORT; case SyncSetupWizard::SETTING_UP: return current_state_ == SyncSetupWizard::SYNC_EVERYTHING || current_state_ == SyncSetupWizard::CONFIGURE || current_state_ == SyncSetupWizard::ENTER_PASSPHRASE; case SyncSetupWizard::NONFATAL_ERROR: // Intentionally fall through. case SyncSetupWizard::FATAL_ERROR: return current_state_ != SyncSetupWizard::ABORT; case SyncSetupWizard::ABORT: return true; case SyncSetupWizard::DONE: return current_state_ == SyncSetupWizard::SETTING_UP || current_state_ == SyncSetupWizard::ENTER_PASSPHRASE; default: NOTREACHED() << "Unhandled State: " << state; return false; } } void SyncSetupFlow::ActivateState(SyncSetupWizard::State state) { DCHECK(flow_handler_); if (state == SyncSetupWizard::NONFATAL_ERROR) state = GetStepForNonFatalError(service_); current_state_ = state; switch (state) { case SyncSetupWizard::OAUTH_LOGIN: { flow_handler_->ShowOAuthLogin(); break; } case SyncSetupWizard::GAIA_LOGIN: { DictionaryValue args; SyncSetupFlow::GetArgsForGaiaLogin(service_, &args); flow_handler_->ShowGaiaLogin(args); break; } case SyncSetupWizard::GAIA_SUCCESS: if (end_state_ == SyncSetupWizard::GAIA_SUCCESS) { flow_handler_->ShowGaiaSuccessAndClose(); break; } flow_handler_->ShowGaiaSuccessAndSettingUp(); break; case SyncSetupWizard::SYNC_EVERYTHING: { DictionaryValue args; SyncSetupFlow::GetArgsForConfigure(service_, &args); args.SetBoolean("showSyncEverythingPage", true); flow_handler_->ShowConfigure(args); break; } case SyncSetupWizard::CONFIGURE: { DictionaryValue args; SyncSetupFlow::GetArgsForConfigure(service_, &args); flow_handler_->ShowConfigure(args); break; } case SyncSetupWizard::ENTER_PASSPHRASE: { DictionaryValue args; SyncSetupFlow::GetArgsForConfigure(service_, &args); GetArgsForConfigure(service_, &args); // TODO(atwilson): Remove ShowPassphraseEntry in favor of using // ShowConfigure() - http://crbug.com/90786. flow_handler_->ShowPassphraseEntry(args); break; } case SyncSetupWizard::SETUP_ABORTED_BY_PENDING_CLEAR: { // TODO(sync): We should expose a real "display an error" API on // SyncSetupFlowHandler (crbug.com/92722) but for now just transition // to the login state with a special error code. DictionaryValue args; SyncSetupFlow::GetArgsForGaiaLogin(service_, &args); args.SetInteger("error", GoogleServiceAuthError::SERVICE_UNAVAILABLE); current_state_ = SyncSetupWizard::GAIA_LOGIN; flow_handler_->ShowGaiaLogin(args); break; } case SyncSetupWizard::SETTING_UP: { flow_handler_->ShowSettingUp(); break; } case SyncSetupWizard::FATAL_ERROR: { // This shows the user the "Could not connect to server" error. // TODO(sync): Update this error handling to allow different platforms to // display the error appropriately (http://crbug.com/92722). DictionaryValue args; SyncSetupFlow::GetArgsForGaiaLogin(service_, &args); args.SetBoolean("fatalError", true); current_state_ = SyncSetupWizard::GAIA_LOGIN; flow_handler_->ShowGaiaLogin(args); break; } case SyncSetupWizard::DONE: case SyncSetupWizard::ABORT: flow_handler_->ShowSetupDone( UTF16ToWide(service_->GetAuthenticatedUsername())); break; default: NOTREACHED() << "Invalid advance state: " << state; } }