diff options
Diffstat (limited to 'chrome/browser/profile_resetter')
20 files changed, 5993 insertions, 4 deletions
diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter.cc b/chrome/browser/profile_resetter/automatic_profile_resetter.cc new file mode 100644 index 0000000..51ea44a --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter.cc @@ -0,0 +1,763 @@ +// 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/profile_resetter/automatic_profile_resetter.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/logging.h" +#include "base/memory/ref_counted.h" +#include "base/metrics/field_trial.h" +#include "base/metrics/histogram.h" +#include "base/metrics/sparse_histogram.h" +#include "base/prefs/pref_service.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/task_runner.h" +#include "base/task_runner_util.h" +#include "base/threading/sequenced_worker_pool.h" +#include "base/time/time.h" +#include "base/timer/elapsed_timer.h" +#include "base/values.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter_delegate.h" +#include "chrome/browser/profile_resetter/jtl_interpreter.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/search_engines/template_url_service_factory.h" +#include "components/search_engines/template_url_service.h" +#include "components/variations/variations_associated_data.h" +#include "content/public/browser/browser_thread.h" +#include "grit/browser_resources.h" +#include "ui/base/resource/resource_bundle.h" + + +// Helpers ------------------------------------------------------------------- + +namespace { + +// Name constants for the field trial behind which we enable this feature. +const char kAutomaticProfileResetStudyName[] = "AutomaticProfileReset"; +const char kAutomaticProfileResetStudyDryRunGroupName[] = "DryRun"; +const char kAutomaticProfileResetStudyEnabledGroupName[] = "Enabled"; +#if defined(GOOGLE_CHROME_BUILD) +const char kAutomaticProfileResetStudyProgramParameterName[] = "program"; +const char kAutomaticProfileResetStudyHashSeedParameterName[] = "hash_seed"; +#endif + +// How long to wait after start-up before unleashing the evaluation flow. +const int64 kEvaluationFlowDelayInSeconds = 55; + +// Keys used in the input dictionary of the program. +const char kDefaultSearchProviderKey[] = "default_search_provider"; +const char kDefaultSearchProviderIsUserControlledKey[] = + "default_search_provider_iuc"; +const char kLoadedModuleDigestsKey[] = "loaded_modules"; +const char kLocalStateKey[] = "local_state"; +const char kLocalStateIsUserControlledKey[] = "local_state_iuc"; +const char kSearchProvidersKey[] = "search_providers"; +const char kUserPreferencesKey[] = "preferences"; +const char kUserPreferencesIsUserControlledKey[] = "preferences_iuc"; + +// Keys used in the output dictionary of the program. +const char kCombinedStatusMaskKeyPrefix[] = "combined_status_mask_bit"; +const char kHadPromptedAlreadyKey[] = "had_prompted_already"; +const char kShouldPromptKey[] = "should_prompt"; +const char kSatisfiedCriteriaMaskKeyPrefix[] = "satisfied_criteria_mask_bit"; + +// Keys used in both the input and output dictionary of the program. +const char kMementoValueInFileKey[] = "memento_value_in_file"; +const char kMementoValueInLocalStateKey[] = "memento_value_in_local_state"; +const char kMementoValueInPrefsKey[] = "memento_value_in_prefs"; + +// Number of bits, and maximum value (exclusive) for the mask whose bits +// indicate which of reset criteria were satisfied. +const size_t kSatisfiedCriteriaMaskNumberOfBits = 5u; +const uint32 kSatisfiedCriteriaMaskMaximumValue = + (1u << kSatisfiedCriteriaMaskNumberOfBits); + +// Number of bits, and maximum value (exclusive) for the mask whose bits +// indicate if any of reset criteria were satisfied, and which of the mementos +// were already present. +const size_t kCombinedStatusMaskNumberOfBits = 4u; +const uint32 kCombinedStatusMaskMaximumValue = + (1u << kCombinedStatusMaskNumberOfBits); + +// Returns whether or not a dry-run shall be performed. +bool ShouldPerformDryRun() { + return StartsWithASCII( + base::FieldTrialList::FindFullName(kAutomaticProfileResetStudyName), + kAutomaticProfileResetStudyDryRunGroupName, true); +} + +// Returns whether or not a live-run shall be performed. +bool ShouldPerformLiveRun() { + return StartsWithASCII( + base::FieldTrialList::FindFullName(kAutomaticProfileResetStudyName), + kAutomaticProfileResetStudyEnabledGroupName, true); +} + +// If the currently active experiment group prescribes a |program| and +// |hash_seed| to use instead of the baked-in ones, retrieves those and returns +// true. Otherwise, returns false. +bool GetProgramAndHashSeedOverridesFromExperiment(std::string* program, + std::string* hash_seed) { + DCHECK(program); + DCHECK(hash_seed); +#if defined(GOOGLE_CHROME_BUILD) + std::map<std::string, std::string> params; + variations::GetVariationParams(kAutomaticProfileResetStudyName, ¶ms); + if (params.count(kAutomaticProfileResetStudyProgramParameterName) && + params.count(kAutomaticProfileResetStudyHashSeedParameterName)) { + program->swap(params[kAutomaticProfileResetStudyProgramParameterName]); + hash_seed->swap(params[kAutomaticProfileResetStudyHashSeedParameterName]); + return true; + } +#endif + return false; +} + +// Takes |pref_name_to_value_map|, which shall be a deep-copy of all preferences +// in |source| without path expansion; and (1.) creates a sub-tree from it named +// |value_tree_key| in |target_dictionary| with path expansion, and (2.) also +// creates an isomorphic sub-tree under the key |is_user_controlled_tree_key| +// that contains only Boolean values indicating whether or not the corresponding +// preference is coming from the 'user' PrefStore. +void BuildSubTreesFromPreferences( + scoped_ptr<base::DictionaryValue> pref_name_to_value_map, + const PrefService* source, + const char* value_tree_key, + const char* is_user_controlled_tree_key, + base::DictionaryValue* target_dictionary) { + std::vector<std::string> pref_names; + pref_names.reserve(pref_name_to_value_map->size()); + for (base::DictionaryValue::Iterator it(*pref_name_to_value_map); + !it.IsAtEnd(); it.Advance()) + pref_names.push_back(it.key()); + + base::DictionaryValue* value_tree = new base::DictionaryValue; + base::DictionaryValue* is_user_controlled_tree = new base::DictionaryValue; + for (std::vector<std::string>::const_iterator it = pref_names.begin(); + it != pref_names.end(); ++it) { + scoped_ptr<base::Value> pref_value_owned; + if (pref_name_to_value_map->RemoveWithoutPathExpansion(*it, + &pref_value_owned)) { + value_tree->Set(*it, pref_value_owned.release()); + const PrefService::Preference* pref = source->FindPreference(it->c_str()); + is_user_controlled_tree->Set( + *it, new base::FundamentalValue(pref->IsUserControlled())); + } + } + target_dictionary->Set(value_tree_key, value_tree); + target_dictionary->Set(is_user_controlled_tree_key, is_user_controlled_tree); +} + +} // namespace + + +// AutomaticProfileResetter::InputBuilder ------------------------------------ + +// Collects all the information that is required by the evaluator program to +// assess whether or not the conditions for showing the reset prompt are met. +// +// This necessitates a lot of work that has to be performed on the UI thread, +// such as: accessing the Preferences, Local State, and TemplateURLService. +// In order to keep the browser responsive, the UI thread shall not be blocked +// for long consecutive periods of time. Unfortunately, we cannot reduce the +// total amount of work. Instead, what this class does is to split the work into +// shorter tasks that are posted one-at-a-time to the UI thread in a serial +// fashion, so as to give a chance to run other tasks that have accumulated in +// the meantime. +class AutomaticProfileResetter::InputBuilder + : public base::SupportsWeakPtr<InputBuilder> { + public: + typedef base::Callback<void(scoped_ptr<base::DictionaryValue>)> + ProgramInputCallback; + + // The dependencies must have been initialized through |delegate|, i.e. the + // RequestCallback[...] methods must have already fired before calling this. + InputBuilder(Profile* profile, AutomaticProfileResetterDelegate* delegate) + : profile_(profile), + delegate_(delegate), + memento_in_prefs_(profile_), + memento_in_local_state_(profile_), + memento_in_file_(profile_) {} + ~InputBuilder() {} + + // Assembles the data required by the evaluator program into a dictionary + // format, and posts it back to the UI thread with |callback| once ready. In + // order not to block the UI thread for long consecutive periods of time, the + // work is divided into smaller tasks, see class comment above for details. + // It is safe to destroy |this| immediately from within the |callback|. + void BuildEvaluatorProgramInput(const ProgramInputCallback& callback) { + DCHECK(!data_); + DCHECK(!callback.is_null()); + data_.reset(new base::DictionaryValue); + callback_ = callback; + + AddAsyncTask(base::Bind(&InputBuilder::IncludeMementoValues, AsWeakPtr())); + AddTask(base::Bind(&InputBuilder::IncludeUserPreferences, AsWeakPtr())); + AddTask(base::Bind(&InputBuilder::IncludeLocalState, AsWeakPtr())); + AddTask(base::Bind(&InputBuilder::IncludeSearchEngines, AsWeakPtr())); + AddTask(base::Bind(&InputBuilder::IncludeLoadedModules, AsWeakPtr())); + + // Each task will post the next one. Just trigger the chain reaction. + PostNextTask(); + } + + private: + // Asynchronous task that includes memento values (or empty strings in case + // mementos are not there). + void IncludeMementoValues() { + data_->SetString(kMementoValueInPrefsKey, memento_in_prefs_.ReadValue()); + data_->SetString(kMementoValueInLocalStateKey, + memento_in_local_state_.ReadValue()); + memento_in_file_.ReadValue(base::Bind( + &InputBuilder::IncludeFileBasedMementoCallback, AsWeakPtr())); + } + + // Called back by |memento_in_file_| once the |memento_value| has been read. + void IncludeFileBasedMementoCallback(const std::string& memento_value) { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + data_->SetString(kMementoValueInFileKey, memento_value); + // As an asynchronous task, we need to take care of posting the next task. + PostNextTask(); + } + + // Task that includes all user (i.e. profile-specific) preferences, along with + // information about whether the value is coming from the 'user' PrefStore. + // This is the most expensive operation, so it is itself split into two parts. + void IncludeUserPreferences() { + PrefService* prefs = profile_->GetPrefs(); + DCHECK(prefs); + scoped_ptr<base::DictionaryValue> pref_name_to_value_map( + prefs->GetPreferenceValuesWithoutPathExpansion()); + AddTask(base::Bind(&InputBuilder::IncludeUserPreferencesPartTwo, + AsWeakPtr(), + base::Passed(&pref_name_to_value_map))); + } + + // Second part to above. + void IncludeUserPreferencesPartTwo( + scoped_ptr<base::DictionaryValue> pref_name_to_value_map) { + PrefService* prefs = profile_->GetPrefs(); + DCHECK(prefs); + BuildSubTreesFromPreferences( + pref_name_to_value_map.Pass(), + prefs, + kUserPreferencesKey, + kUserPreferencesIsUserControlledKey, + data_.get()); + } + + // Task that includes all local state (i.e. shared) preferences, along with + // information about whether the value is coming from the 'user' PrefStore. + void IncludeLocalState() { + PrefService* local_state = g_browser_process->local_state(); + DCHECK(local_state); + scoped_ptr<base::DictionaryValue> pref_name_to_value_map( + local_state->GetPreferenceValuesWithoutPathExpansion()); + BuildSubTreesFromPreferences( + pref_name_to_value_map.Pass(), + local_state, + kLocalStateKey, + kLocalStateIsUserControlledKey, + data_.get()); + } + + // Task that includes all information related to search engines. + void IncludeSearchEngines() { + scoped_ptr<base::DictionaryValue> default_search_provider_details( + delegate_->GetDefaultSearchProviderDetails()); + data_->Set(kDefaultSearchProviderKey, + default_search_provider_details.release()); + + scoped_ptr<base::ListValue> search_providers_details( + delegate_->GetPrepopulatedSearchProvidersDetails()); + data_->Set(kSearchProvidersKey, search_providers_details.release()); + + data_->SetBoolean(kDefaultSearchProviderIsUserControlledKey, + !delegate_->IsDefaultSearchProviderManaged()); + } + + // Task that includes information about loaded modules. + void IncludeLoadedModules() { + scoped_ptr<base::ListValue> loaded_module_digests( + delegate_->GetLoadedModuleNameDigests()); + data_->Set(kLoadedModuleDigestsKey, loaded_module_digests.release()); + } + + // ------------------------------------------------------------------------- + + // Adds a |task| that can do as much asynchronous processing as it wants, but + // will need to finally call PostNextTask() on the UI thread when done. + void AddAsyncTask(const base::Closure& task) { + task_queue_.push(task); + } + + // Convenience wrapper for synchronous tasks. + void SynchronousTaskWrapper(const base::Closure& task) { + base::ElapsedTimer timer; + task.Run(); + UMA_HISTOGRAM_CUSTOM_TIMES( + "AutomaticProfileReset.InputBuilder.TaskDuration", + timer.Elapsed(), + base::TimeDelta::FromMilliseconds(1), + base::TimeDelta::FromSeconds(2), + 50); + PostNextTask(); + } + + // Adds a task that needs to finish synchronously. In exchange, PostNextTask() + // is called automatically when the |task| returns, and execution time is + // measured. + void AddTask(const base::Closure& task) { + task_queue_.push( + base::Bind(&InputBuilder::SynchronousTaskWrapper, AsWeakPtr(), task)); + } + + // Posts the next task from the |task_queue_|, unless it is exhausted, in + // which case it posts |callback_| to return with the results. + void PostNextTask() { + base::Closure next_task; + if (task_queue_.empty()) { + next_task = base::Bind(callback_, base::Passed(&data_)); + } else { + next_task = task_queue_.front(); + task_queue_.pop(); + } + content::BrowserThread::PostTask( + content::BrowserThread::UI, FROM_HERE, next_task); + } + + Profile* profile_; + AutomaticProfileResetterDelegate* delegate_; + ProgramInputCallback callback_; + + PreferenceHostedPromptMemento memento_in_prefs_; + LocalStateHostedPromptMemento memento_in_local_state_; + FileHostedPromptMemento memento_in_file_; + + scoped_ptr<base::DictionaryValue> data_; + std::queue<base::Closure> task_queue_; + + DISALLOW_COPY_AND_ASSIGN(InputBuilder); +}; + + +// AutomaticProfileResetter::EvaluationResults ------------------------------- + +// Encapsulates the output values extracted from the evaluator program. +struct AutomaticProfileResetter::EvaluationResults { + EvaluationResults() + : should_prompt(false), + had_prompted_already(false), + satisfied_criteria_mask(0), + combined_status_mask(0) {} + + std::string memento_value_in_prefs; + std::string memento_value_in_local_state; + std::string memento_value_in_file; + + bool should_prompt; + bool had_prompted_already; + uint32 satisfied_criteria_mask; + uint32 combined_status_mask; +}; + + +// AutomaticProfileResetter -------------------------------------------------- + +AutomaticProfileResetter::AutomaticProfileResetter(Profile* profile) + : profile_(profile), + state_(STATE_UNINITIALIZED), + enumeration_of_loaded_modules_ready_(false), + template_url_service_ready_(false), + has_already_dismissed_prompt_(false), + should_show_reset_banner_(false), + weak_ptr_factory_(this) { + DCHECK(profile_); +} + +AutomaticProfileResetter::~AutomaticProfileResetter() {} + +void AutomaticProfileResetter::Initialize() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_UNINITIALIZED); + + if (!ShouldPerformDryRun() && !ShouldPerformLiveRun()) { + state_ = STATE_DISABLED; + return; + } + + if (!GetProgramAndHashSeedOverridesFromExperiment(&program_, &hash_seed_)) { + ui::ResourceBundle& resources(ui::ResourceBundle::GetSharedInstance()); + if (ShouldPerformLiveRun()) { + program_ = resources.GetRawDataResource( + IDR_AUTOMATIC_PROFILE_RESET_RULES).as_string(); + hash_seed_ = resources.GetRawDataResource( + IDR_AUTOMATIC_PROFILE_RESET_HASH_SEED).as_string(); + } else { // ShouldPerformDryRun() + program_ = resources.GetRawDataResource( + IDR_AUTOMATIC_PROFILE_RESET_RULES_DRY).as_string(); + hash_seed_ = resources.GetRawDataResource( + IDR_AUTOMATIC_PROFILE_RESET_HASH_SEED_DRY).as_string(); + } + } + + delegate_.reset(new AutomaticProfileResetterDelegateImpl( + profile_, ProfileResetter::ALL)); + task_runner_for_waiting_ = + content::BrowserThread::GetMessageLoopProxyForThread( + content::BrowserThread::UI); + + state_ = STATE_INITIALIZED; +} + +void AutomaticProfileResetter::Activate() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK(state_ == STATE_INITIALIZED || state_ == STATE_DISABLED); + + if (state_ == STATE_INITIALIZED) { + if (!program_.empty()) { + // Some steps in the flow (e.g. loaded modules, file-based memento) are + // IO-intensive, so defer execution until some time later. + task_runner_for_waiting_->PostDelayedTask( + FROM_HERE, + base::Bind(&AutomaticProfileResetter::PrepareEvaluationFlow, + weak_ptr_factory_.GetWeakPtr()), + base::TimeDelta::FromSeconds(kEvaluationFlowDelayInSeconds)); + } else { + // Terminate early if there is no program included (nor set by tests). + state_ = STATE_DISABLED; + } + } +} + +void AutomaticProfileResetter::TriggerProfileReset(bool send_feedback) { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_HAS_SHOWN_BUBBLE); + + state_ = STATE_PERFORMING_RESET; + should_show_reset_banner_ = false; + + ReportPromptResult(PROMPT_ACTION_RESET); + delegate_->TriggerProfileSettingsReset( + send_feedback, + base::Bind(&AutomaticProfileResetter::OnProfileSettingsResetCompleted, + weak_ptr_factory_.GetWeakPtr())); +} + +void AutomaticProfileResetter::SkipProfileReset() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_HAS_SHOWN_BUBBLE); + + should_show_reset_banner_ = false; + + ReportPromptResult(PROMPT_ACTION_NO_RESET); + delegate_->DismissPrompt(); + FinishResetPromptFlow(); +} + +bool AutomaticProfileResetter::IsResetPromptFlowActive() const { + return state_ == STATE_HAS_TRIGGERED_PROMPT || + state_ == STATE_HAS_SHOWN_BUBBLE; +} + +bool AutomaticProfileResetter::ShouldShowResetBanner() const { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + return should_show_reset_banner_ && ShouldPerformLiveRun(); +} + +void AutomaticProfileResetter::NotifyDidShowResetBubble() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_HAS_TRIGGERED_PROMPT); + + state_ = STATE_HAS_SHOWN_BUBBLE; + + PersistMementos(); + ReportPromptResult(PROMPT_SHOWN_BUBBLE); +} + +void AutomaticProfileResetter::NotifyDidOpenWebUIResetDialog() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + + // This notification is invoked unconditionally by the WebUI, only care about + // it when the prompt flow is currently active (and not yet resetting). + if (state_ == STATE_HAS_TRIGGERED_PROMPT || + state_ == STATE_HAS_SHOWN_BUBBLE) { + has_already_dismissed_prompt_ = true; + delegate_->DismissPrompt(); + } +} + +void AutomaticProfileResetter::NotifyDidCloseWebUIResetDialog( + bool performed_reset) { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + + // This notification is invoked unconditionally by the WebUI, only care about + // it when the prompt flow is currently active (and not yet resetting). + if (state_ == STATE_HAS_TRIGGERED_PROMPT || + state_ == STATE_HAS_SHOWN_BUBBLE) { + if (!has_already_dismissed_prompt_) + delegate_->DismissPrompt(); + if (state_ == STATE_HAS_TRIGGERED_PROMPT) { + PersistMementos(); + ReportPromptResult(performed_reset ? + PROMPT_NOT_SHOWN_BUBBLE_BUT_HAD_WEBUI_RESET : + PROMPT_NOT_SHOWN_BUBBLE_BUT_HAD_WEBUI_NO_RESET); + } else { // if (state_ == STATE_HAS_SHOWN_PROMPT) + ReportPromptResult(performed_reset ? + PROMPT_FOLLOWED_BY_WEBUI_RESET : + PROMPT_FOLLOWED_BY_WEBUI_NO_RESET); + } + FinishResetPromptFlow(); + } +} + +void AutomaticProfileResetter::NotifyDidCloseWebUIResetBanner() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + should_show_reset_banner_ = false; +} + +void AutomaticProfileResetter::SetProgramForTesting( + const std::string& program) { + program_ = program; +} + +void AutomaticProfileResetter::SetHashSeedForTesting( + const std::string& hash_key) { + hash_seed_ = hash_key; +} + +void AutomaticProfileResetter::SetDelegateForTesting( + scoped_ptr<AutomaticProfileResetterDelegate> delegate) { + delegate_ = delegate.Pass(); +} + +void AutomaticProfileResetter::SetTaskRunnerForWaitingForTesting( + const scoped_refptr<base::TaskRunner>& task_runner) { + task_runner_for_waiting_ = task_runner; +} + +void AutomaticProfileResetter::Shutdown() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + + // We better not do anything substantial at this point. The metrics service + // has already been shut down; and local state has already been commited to + // file (in the regular fashion) for the last time. + + state_ = STATE_DISABLED; + + weak_ptr_factory_.InvalidateWeakPtrs(); + delegate_.reset(); +} + +void AutomaticProfileResetter::PrepareEvaluationFlow() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_INITIALIZED); + + state_ = STATE_WAITING_ON_DEPENDENCIES; + + delegate_->RequestCallbackWhenTemplateURLServiceIsLoaded( + base::Bind(&AutomaticProfileResetter::OnTemplateURLServiceIsLoaded, + weak_ptr_factory_.GetWeakPtr())); + delegate_->RequestCallbackWhenLoadedModulesAreEnumerated( + base::Bind(&AutomaticProfileResetter::OnLoadedModulesAreEnumerated, + weak_ptr_factory_.GetWeakPtr())); + delegate_->LoadTemplateURLServiceIfNeeded(); + delegate_->EnumerateLoadedModulesIfNeeded(); +} + +void AutomaticProfileResetter::OnTemplateURLServiceIsLoaded() { + template_url_service_ready_ = true; + OnDependencyIsReady(); +} + +void AutomaticProfileResetter::OnLoadedModulesAreEnumerated() { + enumeration_of_loaded_modules_ready_ = true; + OnDependencyIsReady(); +} + +void AutomaticProfileResetter::OnDependencyIsReady() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_WAITING_ON_DEPENDENCIES); + + if (template_url_service_ready_ && enumeration_of_loaded_modules_ready_) { + state_ = STATE_READY; + content::BrowserThread::PostTask( + content::BrowserThread::UI, + FROM_HERE, + base::Bind(&AutomaticProfileResetter::BeginEvaluationFlow, + weak_ptr_factory_.GetWeakPtr())); + } +} + +void AutomaticProfileResetter::BeginEvaluationFlow() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_READY); + DCHECK(!program_.empty()); + DCHECK(!input_builder_); + + state_ = STATE_EVALUATING_CONDITIONS; + + input_builder_.reset(new InputBuilder(profile_, delegate_.get())); + input_builder_->BuildEvaluatorProgramInput( + base::Bind(&AutomaticProfileResetter::ContinueWithEvaluationFlow, + weak_ptr_factory_.GetWeakPtr())); +} + +void AutomaticProfileResetter::ContinueWithEvaluationFlow( + scoped_ptr<base::DictionaryValue> program_input) { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_EVALUATING_CONDITIONS); + + input_builder_.reset(); + + base::SequencedWorkerPool* blocking_pool = + content::BrowserThread::GetBlockingPool(); + scoped_refptr<base::TaskRunner> task_runner = + blocking_pool->GetTaskRunnerWithShutdownBehavior( + base::SequencedWorkerPool::SKIP_ON_SHUTDOWN); + + base::PostTaskAndReplyWithResult( + task_runner.get(), + FROM_HERE, + base::Bind(&EvaluateConditionsOnWorkerPoolThread, + hash_seed_, + program_, + base::Passed(&program_input)), + base::Bind(&AutomaticProfileResetter::FinishEvaluationFlow, + weak_ptr_factory_.GetWeakPtr())); +} + +// static +scoped_ptr<AutomaticProfileResetter::EvaluationResults> + AutomaticProfileResetter::EvaluateConditionsOnWorkerPoolThread( + const std::string& hash_seed, + const std::string& program, + scoped_ptr<base::DictionaryValue> program_input) { + JtlInterpreter interpreter(hash_seed, program, program_input.get()); + interpreter.Execute(); + UMA_HISTOGRAM_ENUMERATION("AutomaticProfileReset.InterpreterResult", + interpreter.result(), + JtlInterpreter::RESULT_MAX); + UMA_HISTOGRAM_SPARSE_SLOWLY("AutomaticProfileReset.ProgramChecksum", + interpreter.CalculateProgramChecksum()); + + // In each case below, the respective field in result originally contains the + // default, so if the getter fails, we still have the correct value there. + scoped_ptr<EvaluationResults> results(new EvaluationResults); + interpreter.GetOutputBoolean(kShouldPromptKey, &results->should_prompt); + interpreter.GetOutputBoolean(kHadPromptedAlreadyKey, + &results->had_prompted_already); + interpreter.GetOutputString(kMementoValueInPrefsKey, + &results->memento_value_in_prefs); + interpreter.GetOutputString(kMementoValueInLocalStateKey, + &results->memento_value_in_local_state); + interpreter.GetOutputString(kMementoValueInFileKey, + &results->memento_value_in_file); + for (size_t i = 0; i < kCombinedStatusMaskNumberOfBits; ++i) { + bool flag = false; + std::string mask_i_th_bit_key = + kCombinedStatusMaskKeyPrefix + base::IntToString(i + 1); + if (interpreter.GetOutputBoolean(mask_i_th_bit_key, &flag) && flag) + results->combined_status_mask |= (1 << i); + } + for (size_t i = 0; i < kSatisfiedCriteriaMaskNumberOfBits; ++i) { + bool flag = false; + std::string mask_i_th_bit_key = + kSatisfiedCriteriaMaskKeyPrefix + base::IntToString(i + 1); + if (interpreter.GetOutputBoolean(mask_i_th_bit_key, &flag) && flag) + results->satisfied_criteria_mask |= (1 << i); + } + return results.Pass(); +} + +void AutomaticProfileResetter::ReportStatistics(uint32 satisfied_criteria_mask, + uint32 combined_status_mask) { + UMA_HISTOGRAM_ENUMERATION("AutomaticProfileReset.SatisfiedCriteriaMask", + satisfied_criteria_mask, + kSatisfiedCriteriaMaskMaximumValue); + UMA_HISTOGRAM_ENUMERATION("AutomaticProfileReset.CombinedStatusMask", + combined_status_mask, + kCombinedStatusMaskMaximumValue); +} + +void AutomaticProfileResetter::FinishEvaluationFlow( + scoped_ptr<EvaluationResults> results) { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_EVALUATING_CONDITIONS); + + ReportStatistics(results->satisfied_criteria_mask, + results->combined_status_mask); + + if (results->should_prompt) + should_show_reset_banner_ = true; + + if (results->should_prompt && !results->had_prompted_already) { + evaluation_results_ = results.Pass(); + BeginResetPromptFlow(); + } else { + state_ = STATE_DONE; + } +} + +void AutomaticProfileResetter::BeginResetPromptFlow() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_EVALUATING_CONDITIONS); + + state_ = STATE_HAS_TRIGGERED_PROMPT; + + if (ShouldPerformLiveRun() && delegate_->TriggerPrompt()) { + // Start fetching the brandcoded default settings speculatively in the + // background, so as to reduce waiting time if the user chooses to go + // through with the reset. + delegate_->FetchBrandcodedDefaultSettingsIfNeeded(); + } else { + PersistMementos(); + ReportPromptResult(PROMPT_NOT_TRIGGERED); + FinishResetPromptFlow(); + } +} + +void AutomaticProfileResetter::OnProfileSettingsResetCompleted() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK_EQ(state_, STATE_PERFORMING_RESET); + + delegate_->DismissPrompt(); + FinishResetPromptFlow(); +} + +void AutomaticProfileResetter::ReportPromptResult(PromptResult result) { + UMA_HISTOGRAM_ENUMERATION( + "AutomaticProfileReset.PromptResult", result, PROMPT_RESULT_MAX); +} + +void AutomaticProfileResetter::PersistMementos() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK(state_ == STATE_HAS_TRIGGERED_PROMPT || + state_ == STATE_HAS_SHOWN_BUBBLE); + DCHECK(evaluation_results_); + + PreferenceHostedPromptMemento memento_in_prefs(profile_); + LocalStateHostedPromptMemento memento_in_local_state(profile_); + FileHostedPromptMemento memento_in_file(profile_); + + memento_in_prefs.StoreValue(evaluation_results_->memento_value_in_prefs); + memento_in_local_state.StoreValue( + evaluation_results_->memento_value_in_local_state); + memento_in_file.StoreValue(evaluation_results_->memento_value_in_file); + + evaluation_results_.reset(); +} + +void AutomaticProfileResetter::FinishResetPromptFlow() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK(state_ == STATE_HAS_TRIGGERED_PROMPT || + state_ == STATE_HAS_SHOWN_BUBBLE || + state_ == STATE_PERFORMING_RESET); + DCHECK(!evaluation_results_); + + state_ = STATE_DONE; +} diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter.h b/chrome/browser/profile_resetter/automatic_profile_resetter.h new file mode 100644 index 0000000..9db561e --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter.h @@ -0,0 +1,268 @@ +// 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. + +#ifndef CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_H_ +#define CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/strings/string_piece.h" +#include "base/task_runner.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter_mementos.h" +#include "components/keyed_service/core/keyed_service.h" + +class AutomaticProfileResetterDelegate; +class Profile; + +namespace base { +class DictionaryValue; +class ListValue; +} + +// This service is responsible for evaluating whether the criteria for showing +// the one-time profile reset prompt are satisfied, and for potentially +// triggering the prompt. To ensure that the prompt only appears at most once +// for any given profile, a "memento" that the prompt has appeared is written to +// the profile on disk; see automatic_profile_resetter_mementos.h for details. +// The service is created automatically with the Profile and is activated right +// away by its factory. To avoid delaying start-up, however, it will only start +// working after a short delay. +// All methods in this class shall be called on the UI thread, except when noted +// otherwise. +class AutomaticProfileResetter : public KeyedService { + public: + // Enumeration listing the possible outcomes of triggering the profile reset + // prompt. + enum PromptResult { + // The reset prompt was not triggered because only a dry-run was performed, + // or because it was not supported on the current platform. + PROMPT_NOT_TRIGGERED, + // The reset bubble actually got shown. In contrast to the wrench menu item + // that can always be shown, the bubble might be delayed or might never be + // shown if another bubble was shown at the time of triggering the prompt. + // This enumeration value is usually recorded in conjunction with another + // PromptResult, the absence of which indicates that the prompt was ignored. + PROMPT_SHOWN_BUBBLE, + // The user selected "Reset" or "No, thanks" (respectively) directly from + // within the bubble. + PROMPT_ACTION_RESET, + PROMPT_ACTION_NO_RESET, + // The reset bubble was shown, then dismissed without taking definitive + // action. Then, however, the user initiated or refrained from doing a reset + // (respectively) from the conventional, WebUI-based reset dialog. + PROMPT_FOLLOWED_BY_WEBUI_RESET, + PROMPT_FOLLOWED_BY_WEBUI_NO_RESET, + // The reset bubble was suppressed (not shown) because another bubble was + // already being shown at the time. Regardless, however, the user initiated + // or refrained from doing a reset (respectively) from the conventional, + // WebUI-based reset dialog. + PROMPT_NOT_SHOWN_BUBBLE_BUT_HAD_WEBUI_RESET, + PROMPT_NOT_SHOWN_BUBBLE_BUT_HAD_WEBUI_NO_RESET, + PROMPT_RESULT_MAX + }; + + explicit AutomaticProfileResetter(Profile* profile); + virtual ~AutomaticProfileResetter(); + + // Initializes the service if it is enabled in the field trial. Otherwise, + // skips the initialization steps, and also permanently disables the service. + // Called by AutomaticProfileResetterFactory. + void Initialize(); + + // Fires up the service by unleashing the asynchronous evaluation flow, unless + // the service has been already disabled in Initialize() or there is no + // |program_| to run (in which case the service also gets disabled). + // Called by the AutomaticProfileResetterFactory. + void Activate(); + + // Called in case the user chooses to reset their profile settings from inside + // the reset bubble. Will trigger the reset, optionally |send_feedback|, and + // conclude the reset prompt flow. + void TriggerProfileReset(bool send_feedback); + + // Called in case the user chooses from inside the reset bubble that they do + // not want to reset their profile settings. Will conclude the reset prompt + // flow without setting off a reset. + void SkipProfileReset(); + + // Returns whether or not the profile reset prompt flow is currently active, + // that is, we have triggered the prompt and are waiting for the user to take + // definitive action (and we are not yet performing a reset). + bool IsResetPromptFlowActive() const; + + // Returns whether or not the profile reset banner should be shown on the + // WebUI-based settings page. + bool ShouldShowResetBanner() const; + + // Called to give notice that the reset bubble has actually been shown. + void NotifyDidShowResetBubble(); + + // Called to give notice that the conventional, WebUI-based settings reset + // dialog has been opened. This will dismiss the menu item in the wrench menu. + // This should always be followed by a corresponding call to + // NotifyDidCloseWebUIResetDialog(). + void NotifyDidOpenWebUIResetDialog(); + + // Called to give notice that the conventional, WebUI-based settings reset + // dialog has been closed, with |performed_reset| indicating whether or not a + // reset was requested. This is required so that we can record the appropriate + // PromptResult, dismiss the prompt, and conclude the reset prompt flow early + // without setting off any resets in the future. + void NotifyDidCloseWebUIResetDialog(bool performed_reset); + + // Called to give notice that reset banner has been dismissed as a result of + // user action on the WebUI-based settings page itself. + void NotifyDidCloseWebUIResetBanner(); + + base::WeakPtr<AutomaticProfileResetter> AsWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); + } + + // Should be called before Activate(). + void SetProgramForTesting(const std::string& program); + + // Should be called before Activate(). + void SetHashSeedForTesting(const std::string& hash_seed); + + // Should be called before Activate(). + void SetDelegateForTesting( + scoped_ptr<AutomaticProfileResetterDelegate> delegate); + + // Should be called before Activate(). Sets the task runner to be used to post + // task |PrepareEvaluationFlow| in a delayed manner. + void SetTaskRunnerForWaitingForTesting( + const scoped_refptr<base::TaskRunner>& task_runner); + + // KeyedService: + virtual void Shutdown() OVERRIDE; + + private: + class InputBuilder; + struct EvaluationResults; + + enum State { + STATE_UNINITIALIZED, + STATE_INITIALIZED, + STATE_DISABLED, + STATE_WAITING_ON_DEPENDENCIES, + STATE_READY, + STATE_EVALUATING_CONDITIONS, + // The reset prompt has been triggered; but the reset bubble has not yet + // been shown. + STATE_HAS_TRIGGERED_PROMPT, + // The reset prompt has been triggered; the reset bubble has been shown, and + // potentially already dismissed by the user. + STATE_HAS_SHOWN_BUBBLE, + STATE_PERFORMING_RESET, + STATE_DONE + }; + + // Prepares the asynchronous evaluation flow by requesting services that it + // depends on to make themselves ready. + void PrepareEvaluationFlow(); + + // Called back by |resetter_delegate_| when the template URL service is ready. + void OnTemplateURLServiceIsLoaded(); + + // Called back by |resetter_delegate_| when the loaded modules have been + // enumerated. + void OnLoadedModulesAreEnumerated(); + + // Invoked by the above two methods. Kicks off the actual evaluation flow. + void OnDependencyIsReady(); + + // Begins the asynchronous evaluation flow, which will assess whether the + // criteria for showing the reset prompt are met, whether we have already + // shown the prompt; and, in the end, will potentially trigger the prompt. + void BeginEvaluationFlow(); + + // Called by InputBuilder once it has finished assembling the |program_input|, + // and will continue with the evaluation flow by triggering the evaluator + // program on the worker thread. + void ContinueWithEvaluationFlow( + scoped_ptr<base::DictionaryValue> program_input); + + // Performs the bulk of the work. Invokes the JTL interpreter to run the + // |program| that will evaluate whether the conditions are met for showing the + // reset prompt. The program will make this decision based on the state + // information contained in |input| in the form of key-value pairs. The + // program will only see hashed keys and values that are produced using + // |hash_seed| as a key. + static scoped_ptr<EvaluationResults> EvaluateConditionsOnWorkerPoolThread( + const std::string& hash_seed, + const std::string& program, + scoped_ptr<base::DictionaryValue> program_input); + + // Reports the given metrics through UMA. Virtual, so it can be mocked out in + // tests to verify that the correct value are being reported. + virtual void ReportStatistics(uint32 satisfied_criteria_mask, + uint32 combined_status_mask); + + // Called back when EvaluateConditionsOnWorkerPoolThread completes executing + // the program with |results|. Finishes the evaluation flow, and, based on the + // result, potentially initiates the reset prompt flow. + void FinishEvaluationFlow(scoped_ptr<EvaluationResults> results); + + // Begins the reset prompt flow by triggering the reset prompt, which consists + // of two parts: (1.) the profile reset (pop-up) bubble, and (2.) a menu item + // in the wrench menu (provided by a GlobalError). + // The flow lasts until we receive a clear indication from the user about + // whether or not they wish to reset their settings. This indication can come + // in a variety of flavors: + // * taking definitive action (i.e. selecting either "Reset" or "No, thanks") + // in the pop-up reset bubble itself, + // * dismissing the bubble, but then selecting the wrench menu item, which + // takes them to the WebUI reset dialog in chrome://settings, and then the + // user can make their choice there, + // * the user going to the WebUI reset dialog by themself. + // For the most part, the conclusion of the reset flow coincides with when the + // reset prompt is dismissed, with the one exception being that the prompt is + // closed as soon as the WebUI reset dialog is opened, we do not wait until + // the user actually makes a choice in that dialog. + void BeginResetPromptFlow(); + + // Called back by the ProfileResetter once resetting the profile settings has + // been completed, when requested by the user from inside the reset bubble. + // Will dismiss the prompt and conclude the reset prompt flow. + void OnProfileSettingsResetCompleted(); + + // Reports the result of triggering the prompt through UMA. Virtual, so it can + // be mocked out in tests to verify that the correct value is being reported. + virtual void ReportPromptResult(PromptResult result); + + // Writes the memento values returned by the evaluation program to disk, and + // then destroys |evaluation_results_|. + void PersistMementos(); + + // Concludes the reset prompt flow. + void FinishResetPromptFlow(); + + Profile* profile_; + + State state_; + bool enumeration_of_loaded_modules_ready_; + bool template_url_service_ready_; + bool has_already_dismissed_prompt_; + + scoped_ptr<InputBuilder> input_builder_; + std::string hash_seed_; + std::string program_; + + scoped_ptr<EvaluationResults> evaluation_results_; + + bool should_show_reset_banner_; + + scoped_ptr<AutomaticProfileResetterDelegate> delegate_; + scoped_refptr<base::TaskRunner> task_runner_for_waiting_; + + base::WeakPtrFactory<AutomaticProfileResetter> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(AutomaticProfileResetter); +}; + +#endif // CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_H_ diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter_delegate.cc b/chrome/browser/profile_resetter/automatic_profile_resetter_delegate.cc new file mode 100644 index 0000000..3f1c3c7 --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter_delegate.cc @@ -0,0 +1,397 @@ +// 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/profile_resetter/automatic_profile_resetter_delegate.h" + +#include <string> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/callback.h" +#include "base/logging.h" +#include "base/md5.h" +#include "base/memory/scoped_vector.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/values.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/chrome_notification_types.h" +#include "chrome/browser/google/google_brand.h" +#include "chrome/browser/profile_resetter/brandcode_config_fetcher.h" +#include "chrome/browser/profile_resetter/profile_reset_global_error.h" +#include "chrome/browser/profile_resetter/profile_resetter.h" +#include "chrome/browser/profile_resetter/resettable_settings_snapshot.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/search_engines/template_url_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/global_error/global_error_service.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "components/search_engines/template_url_prepopulate_data.h" +#include "components/search_engines/template_url_service.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/notification_service.h" + +#if defined(OS_WIN) +#include "chrome/browser/enumerate_modules_model_win.h" +#endif + +namespace { + +scoped_ptr<base::DictionaryValue> BuildSubTreeFromTemplateURL( + const TemplateURL* template_url) { + // If this value contains a placeholder in the pre-populated data, it will + // have been replaced as it was loaded into a TemplateURL. + // BuildSubTreeFromTemplateURL works with TemplateURL (not TemplateURLData) + // in order to maintain this behaviour. + // TODO(engedy): Confirm the expected behaviour and convert to use + // TemplateURLData if possible." + scoped_ptr<base::DictionaryValue> tree(new base::DictionaryValue); + tree->SetString("name", template_url->short_name()); + tree->SetString("short_name", template_url->short_name()); + tree->SetString("keyword", template_url->keyword()); + tree->SetString("search_url", template_url->url()); + tree->SetString("url", template_url->url()); + tree->SetString("suggestions_url", template_url->suggestions_url()); + tree->SetString("instant_url", template_url->instant_url()); + tree->SetString("image_url", template_url->image_url()); + tree->SetString("new_tab_url", template_url->new_tab_url()); + tree->SetString("search_url_post_params", + template_url->search_url_post_params()); + tree->SetString("suggestions_url_post_params", + template_url->suggestions_url_post_params()); + tree->SetString("instant_url_post_params", + template_url->instant_url_post_params()); + tree->SetString("image_url_post_params", + template_url->image_url_post_params()); + base::ListValue* alternate_urls = new base::ListValue; + alternate_urls->AppendStrings(template_url->alternate_urls()); + tree->Set("alternate_urls", alternate_urls); + tree->SetString("favicon_url", template_url->favicon_url().spec()); + tree->SetString("originating_url", template_url->originating_url().spec()); + tree->SetBoolean("safe_for_autoreplace", + template_url->safe_for_autoreplace()); + base::ListValue* input_encodings = new base::ListValue; + input_encodings->AppendStrings(template_url->input_encodings()); + tree->Set("input_encodings", input_encodings); + tree->SetString("id", base::Int64ToString(template_url->id())); + tree->SetString("date_created", + base::Int64ToString( + template_url->date_created().ToInternalValue())); + tree->SetString("last_modified", + base::Int64ToString( + template_url->last_modified().ToInternalValue())); + tree->SetBoolean("created_by_policy", template_url->created_by_policy()); + tree->SetInteger("usage_count", template_url->usage_count()); + tree->SetInteger("prepopulate_id", template_url->prepopulate_id()); + tree->SetString("search_terms_replacement_key", + template_url->search_terms_replacement_key()); + return tree.Pass(); +} + +#if defined(OS_WIN) +void ExtractLoadedModuleNameDigests( + const base::ListValue& module_list, + base::ListValue* module_name_digests) { + DCHECK(module_name_digests); + + // EnumerateModulesModel produces a list of dictionaries. + // Each dictionary corresponds to a module and exposes a number of properties. + // We care only about 'type' and 'name'. + for (size_t i = 0; i < module_list.GetSize(); ++i) { + const base::DictionaryValue* module_dictionary = NULL; + if (!module_list.GetDictionary(i, &module_dictionary)) + continue; + ModuleEnumerator::ModuleType module_type = + ModuleEnumerator::LOADED_MODULE; + if (!module_dictionary->GetInteger( + "type", reinterpret_cast<int*>(&module_type)) || + module_type != ModuleEnumerator::LOADED_MODULE) { + continue; + } + std::string module_name; + if (!module_dictionary->GetString("name", &module_name)) + continue; + base::StringToLowerASCII(&module_name); + module_name_digests->AppendString(base::MD5String(module_name)); + } +} +#endif + +} // namespace + + +// AutomaticProfileResetterDelegateImpl -------------------------------------- + +AutomaticProfileResetterDelegateImpl::AutomaticProfileResetterDelegateImpl( + Profile* profile, + ProfileResetter::ResettableFlags resettable_aspects) + : profile_(profile), + global_error_service_(GlobalErrorServiceFactory::GetForProfile(profile_)), + template_url_service_(TemplateURLServiceFactory::GetForProfile(profile_)), + resettable_aspects_(resettable_aspects) { + DCHECK(profile_); + if (template_url_service_) { + template_url_service_->AddObserver(this); + // Needed so that |template_url_service_ready_event_| will be signaled even + // when TemplateURLService had been already initialized before this point. + OnTemplateURLServiceChanged(); + } + +#if defined(OS_WIN) + module_list_.reset(EnumerateModulesModel::GetInstance()->GetModuleList()); +#endif + if (module_list_) { + // Having a non-empty module list proves that enumeration had been already + // performed before this point. + modules_have_been_enumerated_event_.Signal(); + } + registrar_.Add(this, + chrome::NOTIFICATION_MODULE_LIST_ENUMERATED, + content::NotificationService::AllSources()); +} + +AutomaticProfileResetterDelegateImpl::~AutomaticProfileResetterDelegateImpl() { + if (template_url_service_) + template_url_service_->RemoveObserver(this); +} + +void AutomaticProfileResetterDelegateImpl::EnumerateLoadedModulesIfNeeded() { + if (!modules_have_been_enumerated_event_.is_signaled()) { +#if defined(OS_WIN) + EnumerateModulesModel::GetInstance()->ScanNow(); +#else + modules_have_been_enumerated_event_.Signal(); +#endif + } +} + +void AutomaticProfileResetterDelegateImpl:: + RequestCallbackWhenLoadedModulesAreEnumerated( + const base::Closure& ready_callback) const { + DCHECK(!ready_callback.is_null()); + modules_have_been_enumerated_event_.Post(FROM_HERE, ready_callback); +} + +void AutomaticProfileResetterDelegateImpl::LoadTemplateURLServiceIfNeeded() { + DCHECK(template_url_service_); + template_url_service_->Load(); // Safe to call even if it has loaded already. +} + +void AutomaticProfileResetterDelegateImpl:: + RequestCallbackWhenTemplateURLServiceIsLoaded( + const base::Closure& ready_callback) const { + DCHECK(!ready_callback.is_null()); + template_url_service_ready_event_.Post(FROM_HERE, ready_callback); +} + +void AutomaticProfileResetterDelegateImpl:: + FetchBrandcodedDefaultSettingsIfNeeded() { + if (brandcoded_config_fetcher_ || + brandcoded_defaults_fetched_event_.is_signaled()) + return; + + std::string brandcode; + google_brand::GetBrand(&brandcode); + if (brandcode.empty()) { + brandcoded_defaults_.reset(new BrandcodedDefaultSettings); + brandcoded_defaults_fetched_event_.Signal(); + } else { + brandcoded_config_fetcher_.reset(new BrandcodeConfigFetcher( + base::Bind( + &AutomaticProfileResetterDelegateImpl::OnBrandcodedDefaultsFetched, + base::Unretained(this)), + GURL("https://tools.google.com/service/update2"), + brandcode)); + } +} + +void AutomaticProfileResetterDelegateImpl:: + RequestCallbackWhenBrandcodedDefaultsAreFetched( + const base::Closure& ready_callback) const { + DCHECK(!ready_callback.is_null()); + brandcoded_defaults_fetched_event_.Post(FROM_HERE, ready_callback); +} + +scoped_ptr<base::ListValue> AutomaticProfileResetterDelegateImpl:: + GetLoadedModuleNameDigests() const { + DCHECK(modules_have_been_enumerated_event_.is_signaled()); + scoped_ptr<base::ListValue> result(new base::ListValue); +#if defined(OS_WIN) + if (module_list_) + ExtractLoadedModuleNameDigests(*module_list_, result.get()); +#endif + return result.Pass(); +} + +scoped_ptr<base::DictionaryValue> AutomaticProfileResetterDelegateImpl:: + GetDefaultSearchProviderDetails() const { + DCHECK(template_url_service_); + DCHECK(template_url_service_->loaded()); + + const TemplateURL* default_search_provider = + template_url_service_->GetDefaultSearchProvider(); + + // Having a NULL default search provider is due to either: + // 1.) default search providers being disabled by policy, + // 2.) directly tampering with the Preferences and/or the SQLite DBs. + // In this state, Omnibox non-keyword search functionality is disabled. + return default_search_provider ? + BuildSubTreeFromTemplateURL(default_search_provider) : + scoped_ptr<base::DictionaryValue>(new base::DictionaryValue); +} + +bool AutomaticProfileResetterDelegateImpl:: + IsDefaultSearchProviderManaged() const { + DCHECK(template_url_service_); + DCHECK(template_url_service_->loaded()); + return template_url_service_->is_default_search_managed(); +} + +scoped_ptr<base::ListValue> AutomaticProfileResetterDelegateImpl:: + GetPrepopulatedSearchProvidersDetails() const { + size_t default_search_index = 0; + ScopedVector<TemplateURLData> engines( + TemplateURLPrepopulateData::GetPrepopulatedEngines( + profile_->GetPrefs(), &default_search_index)); + scoped_ptr<base::ListValue> engines_details_list(new base::ListValue); + for (ScopedVector<TemplateURLData>::const_iterator it = engines.begin(); + it != engines.end(); ++it) { + TemplateURL template_url(**it); + engines_details_list->Append( + BuildSubTreeFromTemplateURL(&template_url).release()); + } + return engines_details_list.Pass(); +} + +bool AutomaticProfileResetterDelegateImpl::TriggerPrompt() { + DCHECK(global_error_service_); + + if (!ProfileResetGlobalError::IsSupportedOnPlatform()) + return false; + + ProfileResetGlobalError* global_error = new ProfileResetGlobalError(profile_); + global_error_service_->AddGlobalError(global_error); + + // Do not try to show bubble if another GlobalError is already showing one. + const GlobalErrorService::GlobalErrorList& global_errors( + global_error_service_->errors()); + GlobalErrorService::GlobalErrorList::const_iterator it; + for (it = global_errors.begin(); it != global_errors.end(); ++it) { + if ((*it)->GetBubbleView()) + break; + } + if (it == global_errors.end()) { + Browser* browser = chrome::FindTabbedBrowser( + profile_, + false /*match_original_profiles*/, + chrome::GetActiveDesktop()); + if (browser) + global_error->ShowBubbleView(browser); + } + return true; +} + +void AutomaticProfileResetterDelegateImpl::TriggerProfileSettingsReset( + bool send_feedback, + const base::Closure& completion) { + DCHECK(!profile_resetter_); + DCHECK(!completion.is_null()); + + profile_resetter_.reset(new ProfileResetter(profile_)); + FetchBrandcodedDefaultSettingsIfNeeded(); + RequestCallbackWhenBrandcodedDefaultsAreFetched(base::Bind( + &AutomaticProfileResetterDelegateImpl::RunProfileSettingsReset, + AsWeakPtr(), + send_feedback, + completion)); +} + +void AutomaticProfileResetterDelegateImpl::OnTemplateURLServiceChanged() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK(template_url_service_); + if (template_url_service_->loaded() && + !template_url_service_ready_event_.is_signaled()) + template_url_service_ready_event_.Signal(); +} + +void AutomaticProfileResetterDelegateImpl::DismissPrompt() { + DCHECK(global_error_service_); + GlobalError* global_error = + global_error_service_->GetGlobalErrorByMenuItemCommandID( + IDC_SHOW_SETTINGS_RESET_BUBBLE); + if (global_error) { + // This will also close/destroy the Bubble UI if it is currently shown. + global_error_service_->RemoveGlobalError(global_error); + delete global_error; + } +} + +void AutomaticProfileResetterDelegateImpl::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + if (type == chrome::NOTIFICATION_MODULE_LIST_ENUMERATED && + !modules_have_been_enumerated_event_.is_signaled()) { +#if defined(OS_WIN) + module_list_.reset(EnumerateModulesModel::GetInstance()->GetModuleList()); +#endif + modules_have_been_enumerated_event_.Signal(); + } +} + +void AutomaticProfileResetterDelegateImpl::SendFeedback( + const std::string& report) const { + SendSettingsFeedback(report, profile_, PROFILE_RESET_PROMPT); +} + +void AutomaticProfileResetterDelegateImpl::RunProfileSettingsReset( + bool send_feedback, + const base::Closure& completion) { + DCHECK(brandcoded_defaults_); + scoped_ptr<ResettableSettingsSnapshot> old_settings_snapshot; + if (send_feedback) { + old_settings_snapshot.reset(new ResettableSettingsSnapshot(profile_)); + old_settings_snapshot->RequestShortcuts(base::Closure()); + } + profile_resetter_->Reset(resettable_aspects_, + brandcoded_defaults_.Pass(), + send_feedback, + base::Bind(&AutomaticProfileResetterDelegateImpl:: + OnProfileSettingsResetCompleted, + AsWeakPtr(), + completion, + base::Passed(&old_settings_snapshot))); +} + +void AutomaticProfileResetterDelegateImpl:: + OnBrandcodedDefaultsFetched() { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + DCHECK(brandcoded_config_fetcher_); + DCHECK(!brandcoded_config_fetcher_->IsActive()); + brandcoded_defaults_ = brandcoded_config_fetcher_->GetSettings(); + if (!brandcoded_defaults_) + brandcoded_defaults_.reset(new BrandcodedDefaultSettings); + brandcoded_defaults_fetched_event_.Signal(); +} + +void AutomaticProfileResetterDelegateImpl::OnProfileSettingsResetCompleted( + const base::Closure& user_callback, + scoped_ptr<ResettableSettingsSnapshot> old_settings_snapshot) { + DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + if (old_settings_snapshot) { + ResettableSettingsSnapshot new_settings_snapshot(profile_); + int difference = + old_settings_snapshot->FindDifferentFields(new_settings_snapshot); + if (difference) { + old_settings_snapshot->Subtract(new_settings_snapshot); + std::string report = + SerializeSettingsReport(*old_settings_snapshot, difference); + SendFeedback(report); + } + } + content::BrowserThread::PostTask( + content::BrowserThread::UI, FROM_HERE, user_callback); +} diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter_delegate.h b/chrome/browser/profile_resetter/automatic_profile_resetter_delegate.h new file mode 100644 index 0000000..015c466 --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter_delegate.h @@ -0,0 +1,239 @@ +// 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. + +// Declares a delegate that interacts with the rest of the browser on behalf of +// the AutomaticProfileResetter. +// +// The reason for this separation is to facilitate unit testing. Factoring out +// the implementation for each interaction step (encapsulated by one method of +// the delegate) allows it to be tested independently in itself. It also becomes +// easier to verify that the state machine inside AutomaticProfileResetter works +// correctly: by mocking out the interaction methods in the delegate, we can, in +// effect, mock out the entire rest of the browser, allowing us to easily +// simulate scenarios that are interesting for testing the state machine. +// +// The delegate is normally instantiated by AutomaticProfileResetter internally, +// while a mock implementation can be injected during unit tests. + +#ifndef CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_DELEGATE_H_ +#define CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_DELEGATE_H_ + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "chrome/browser/profile_resetter/profile_resetter.h" +#include "components/search_engines/template_url_service_observer.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "extensions/common/one_shot_event.h" + +class BrandcodeConfigFetcher; +class GlobalErrorService; +class Profile; +class ResettableSettingsSnapshot; +class TemplateURLService; + +namespace base { +class DictionaryValue; +class ListValue; +} + +// Defines the interface for the delegate that will interact with the rest of +// the browser on behalf of the AutomaticProfileResetter. +class AutomaticProfileResetterDelegate { + public: + virtual ~AutomaticProfileResetterDelegate() {} + + // Requests the module enumerator to start scanning for loaded modules now, if + // it has not done so already. + virtual void EnumerateLoadedModulesIfNeeded() = 0; + + // Requests |ready_callback| to be posted on the UI thread once the module + // enumerator has finished scanning for loaded modules. + virtual void RequestCallbackWhenLoadedModulesAreEnumerated( + const base::Closure& ready_callback) const = 0; + + // Requests the template URL service to load its database (asynchronously). + virtual void LoadTemplateURLServiceIfNeeded() = 0; + + // Requests |ready_callback| to be posted on the UI thread once the template + // URL service has finished loading its database. + virtual void RequestCallbackWhenTemplateURLServiceIsLoaded( + const base::Closure& ready_callback) const = 0; + + // Starts downloading the configuration file that specifies the default + // settings for the current brandcode; or establishes that we are running a + // vanilla (non-branded) build. + virtual void FetchBrandcodedDefaultSettingsIfNeeded() = 0; + + // Requests |ready_callback| to be posted on the UI thread once the brandcoded + // default settings have been downloaded, or once it has been established that + // we are running a vanilla (non-branded) build. + virtual void RequestCallbackWhenBrandcodedDefaultsAreFetched( + const base::Closure& ready_callback) const = 0; + + // Returns a list of loaded module name digests. + virtual scoped_ptr<base::ListValue> GetLoadedModuleNameDigests() const = 0; + + // Returns attributes of the search engine currently set as the default (or + // an empty dictionary if there is none). + // The dictionary is in the same format as persisted to preferences by + // DefaultSearchManager::SetUserSelectedDefaultSearchEngine. + virtual scoped_ptr<base::DictionaryValue> + GetDefaultSearchProviderDetails() const = 0; + + // Returns whether or not the default search provider is set by policy. + virtual bool IsDefaultSearchProviderManaged() const = 0; + + // Returns a list of dictionaries, each containing attributes for each of the + // pre-populated search engines, in the format described above. + virtual scoped_ptr<base::ListValue> + GetPrepopulatedSearchProvidersDetails() const = 0; + + // Triggers showing the one-time profile settings reset prompt, which consists + // of two parts: (1.) the profile reset (pop-up) bubble, and (2.) a menu item + // in the wrench menu. Showing the bubble might be delayed if there is another + // bubble currently shown, in which case the bubble will be shown the first + // time a new browser window is opened. + // Returns true unless the reset prompt is not supported on the current + // platform, in which case it returns false and does nothing. + virtual bool TriggerPrompt() = 0; + + // Triggers the ProfileResetter to reset all supported settings and optionally + // |send_feedback|. Will post |completion| on the UI thread once completed. + // Brandcoded default settings will be fetched if they are not yet available, + // the reset will be performed once the download is complete. + // NOTE: Should only be called at most once during the lifetime of the object. + virtual void TriggerProfileSettingsReset(bool send_feedback, + const base::Closure& completion) = 0; + + // Dismisses the one-time profile settings reset prompt, if it is currently + // triggered. Will synchronously destroy the corresponding GlobalError. + virtual void DismissPrompt() = 0; +}; + +// Implementation for AutomaticProfileResetterDelegate. +class AutomaticProfileResetterDelegateImpl + : public AutomaticProfileResetterDelegate, + public base::SupportsWeakPtr<AutomaticProfileResetterDelegateImpl>, + public TemplateURLServiceObserver, + public content::NotificationObserver { + public: + explicit AutomaticProfileResetterDelegateImpl( + Profile* profile, + ProfileResetter::ResettableFlags resettable_aspects); + virtual ~AutomaticProfileResetterDelegateImpl(); + + // Returns the brandcoded default settings; empty defaults if this is not a + // branded build; or NULL if FetchBrandcodedDefaultSettingsIfNeeded() has not + // yet been called or not yet finished. Exposed only for unit tests. + const BrandcodedDefaultSettings* brandcoded_defaults() const { + return brandcoded_defaults_.get(); + } + + // AutomaticProfileResetterDelegate: + virtual void EnumerateLoadedModulesIfNeeded() OVERRIDE; + virtual void RequestCallbackWhenLoadedModulesAreEnumerated( + const base::Closure& ready_callback) const OVERRIDE; + virtual void LoadTemplateURLServiceIfNeeded() OVERRIDE; + virtual void RequestCallbackWhenTemplateURLServiceIsLoaded( + const base::Closure& ready_callback) const OVERRIDE; + virtual void FetchBrandcodedDefaultSettingsIfNeeded() OVERRIDE; + virtual void RequestCallbackWhenBrandcodedDefaultsAreFetched( + const base::Closure& ready_callback) const OVERRIDE; + virtual scoped_ptr<base::ListValue> + GetLoadedModuleNameDigests() const OVERRIDE; + virtual scoped_ptr<base::DictionaryValue> + GetDefaultSearchProviderDetails() const OVERRIDE; + virtual bool IsDefaultSearchProviderManaged() const OVERRIDE; + virtual scoped_ptr<base::ListValue> + GetPrepopulatedSearchProvidersDetails() const OVERRIDE; + virtual bool TriggerPrompt() OVERRIDE; + virtual void TriggerProfileSettingsReset( + bool send_feedback, + const base::Closure& completion) OVERRIDE; + virtual void DismissPrompt() OVERRIDE; + + // TemplateURLServiceObserver: + virtual void OnTemplateURLServiceChanged() OVERRIDE; + + // content::NotificationObserver: + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + private: + // Sends a feedback |report|, where |report| is supposed to be result of + // ::SerializeSettingsReport(). Virtual, so it can be mocked out for tests. + virtual void SendFeedback(const std::string& report) const; + + // Triggers the ProfileResetter to reset |resettable_aspects_| and optionally + // |send_feedback|. Will invoke |completion| on the UI thread once completed + // The |brandcoded_defaults_| must already be initialized when this is called. + void RunProfileSettingsReset(bool send_feedback, + const base::Closure& completion); + + // Called by |brandcoded_config_fetcher_| when it finishes downloading the + // brandcoded default settings (or the download times out). + void OnBrandcodedDefaultsFetched(); + + // Called back by the ProfileResetter once resetting the profile settings has + // been completed. If |old_settings_snapshot| is non-NULL, will compare it to + // the new settings and send the differences to Google for analysis. Finally, + // will post |user_callback|. + void OnProfileSettingsResetCompleted( + const base::Closure& user_callback, + scoped_ptr<ResettableSettingsSnapshot> old_settings_snapshot); + + // The profile that this delegate operates on. + Profile* profile_; + + // Shortcuts to |profile_| keyed services, to reduce boilerplate. These may be + // NULL in unit tests that do not initialize the respective service(s). + GlobalErrorService* global_error_service_; + TemplateURLService* template_url_service_; + + // Helper to asynchronously download the default settings for the current + // distribution channel (identified by brand code). Instantiated on-demand. + scoped_ptr<BrandcodeConfigFetcher> brandcoded_config_fetcher_; + + // Once |brandcoded_defaults_fetched_event_| has fired, this will contain the + // brandcoded default settings, or empty settings for a non-branded build. + scoped_ptr<BrandcodedDefaultSettings> brandcoded_defaults_; + + // Overwritten to avoid resetting aspects that will not work in unit tests. + const ProfileResetter::ResettableFlags resettable_aspects_; + + // The profile resetter to perform the actual reset. Instantiated on-demand. + scoped_ptr<ProfileResetter> profile_resetter_; + + // Manages registrations/unregistrations for notifications. + content::NotificationRegistrar registrar_; + + // The list of modules found. Even when |modules_have_been_enumerated_event_| + // is signaled, this may still be NULL. + scoped_ptr<base::ListValue> module_list_; + + // This event is signaled once module enumeration has been attempted. If + // during construction, EnumerateModulesModel can supply a non-empty list of + // modules, module enumeration has clearly already happened, so the event will + // be signaled immediately; otherwise, when EnumerateLoadedModulesIfNeeded() + // is called, it will ask the model to scan the modules, and then signal the + // event once this process is completed. + extensions::OneShotEvent modules_have_been_enumerated_event_; + + // This event is signaled once the TemplateURLService has loaded. If the + // TemplateURLService was already loaded prior to the creation of this class, + // the event will be signaled during construction. + extensions::OneShotEvent template_url_service_ready_event_; + + // This event is signaled once brandcoded default settings have been fetched, + // or once it has been established that this is not a branded build. + extensions::OneShotEvent brandcoded_defaults_fetched_event_; + + DISALLOW_COPY_AND_ASSIGN(AutomaticProfileResetterDelegateImpl); +}; + +#endif // CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_DELEGATE_H_ diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter_delegate_unittest.cc b/chrome/browser/profile_resetter/automatic_profile_resetter_delegate_unittest.cc new file mode 100644 index 0000000..560b277 --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter_delegate_unittest.cc @@ -0,0 +1,608 @@ +// 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/profile_resetter/automatic_profile_resetter_delegate.h" + +#include <algorithm> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/prefs/pref_service.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" +#include "base/strings/string_util.h" +#include "base/strings/utf_string_conversions.h" +#include "base/test/values_test_util.h" +#include "base/values.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/chrome_notification_types.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_service_test_base.h" +#include "chrome/browser/google/google_brand.h" +#include "chrome/browser/profile_resetter/brandcoded_default_settings.h" +#include "chrome/browser/profile_resetter/profile_reset_global_error.h" +#include "chrome/browser/search_engines/template_url_service_factory.h" +#include "chrome/browser/search_engines/template_url_service_factory_test_util.h" +#include "chrome/browser/ui/global_error/global_error.h" +#include "chrome/browser/ui/global_error/global_error_service.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/testing_pref_service_syncable.h" +#include "chrome/test/base/testing_profile.h" +#include "components/search_engines/default_search_manager.h" +#include "components/search_engines/template_url_prepopulate_data.h" +#include "components/search_engines/template_url_service.h" +#include "content/public/browser/notification_service.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/test_url_fetcher_factory.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if defined(OS_WIN) +#include "chrome/browser/enumerate_modules_model_win.h" +#endif + +namespace { + +const char kTestBrandcode[] = "FOOBAR"; + +const char kTestHomepage[] = "http://google.com"; +const char kTestBrandedHomepage[] = "http://example.com"; + +const ProfileResetter::ResettableFlags kResettableAspectsForTest = + ProfileResetter::ALL & ~ProfileResetter::COOKIES_AND_SITE_DATA; + +// Helpers ------------------------------------------------------------------- + +// A testing version of the AutomaticProfileResetterDelegate that differs from +// the real one only in that it has its feedback reporting mocked out, and it +// will not reset COOKIES_AND_SITE_DATA, due to difficulties to set up some +// required URLRequestContexts in unit tests. +class AutomaticProfileResetterDelegateUnderTest + : public AutomaticProfileResetterDelegateImpl { + public: + explicit AutomaticProfileResetterDelegateUnderTest(Profile* profile) + : AutomaticProfileResetterDelegateImpl( + profile, kResettableAspectsForTest) {} + virtual ~AutomaticProfileResetterDelegateUnderTest() {} + + MOCK_CONST_METHOD1(SendFeedback, void(const std::string&)); + + private: + DISALLOW_COPY_AND_ASSIGN(AutomaticProfileResetterDelegateUnderTest); +}; + +class MockCallbackTarget { + public: + MockCallbackTarget() {} + ~MockCallbackTarget() {} + + MOCK_CONST_METHOD0(Run, void(void)); + + base::Closure CreateClosure() { + return base::Bind(&MockCallbackTarget::Run, base::Unretained(this)); + } + + private: + DISALLOW_COPY_AND_ASSIGN(MockCallbackTarget); +}; + +// Returns the details of the default search provider from |prefs| in a format +// suitable for usage as |expected_details| in ExpectDetailsMatch(). +const base::DictionaryValue* GetDefaultSearchProviderDetailsFromPrefs( + const PrefService* prefs) { + return prefs->GetDictionary( + DefaultSearchManager::kDefaultSearchProviderDataPrefName); +} + +// Verifies that the |details| of a search engine as provided by the delegate +// are correct in comparison to the |expected_details| coming from the Prefs. +void ExpectDetailsMatch(const base::DictionaryValue& expected_details, + const base::DictionaryValue& details) { + for (base::DictionaryValue::Iterator it(expected_details); !it.IsAtEnd(); + it.Advance()) { + SCOPED_TRACE(testing::Message("Key: ") << it.key()); + if (it.key() == "enabled" || it.key() == "synced_guid") { + // These attributes should not be present. + EXPECT_FALSE(details.HasKey(it.key())); + continue; + } + const base::Value* expected_value = &it.value(); + const base::Value* actual_value = NULL; + ASSERT_TRUE(details.Get(it.key(), &actual_value)); + + // Ignore ID as it is dynamically assigned by the TemplateURLService. + // last_modified may get updated during a run, so ignore value differences. + if (it.key() != "id" && it.key() != "last_modified") { + // Everything else is the same format. + EXPECT_TRUE(actual_value->Equals(expected_value)) + << "Expected: " << *expected_value << ". Actual: " << *actual_value; + } + } +} + +// If |simulate_failure| is false, then replies to the pending request on +// |fetcher| with a brandcoded config that only specifies a home page URL. +// If |simulate_failure| is true, replies with 404. +void ServicePendingBrancodedConfigFetch(net::TestURLFetcher* fetcher, + bool simulate_failure) { + const char kBrandcodedXmlSettings[] = + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<response protocol=\"3.0\" server=\"prod\">" + "<app appid=\"{8A69D345-D564-463C-AFF1-A69D9E530F96}\" status=\"ok\">" + "<data index=\"skipfirstrunui-importsearch-defaultbrowser\" " + "name=\"install\" status=\"ok\">" + "{\"homepage\" : \"$1\"}" + "</data>" + "</app>" + "</response>"; + + fetcher->set_response_code(simulate_failure ? 404 : 200); + scoped_refptr<net::HttpResponseHeaders> response_headers( + new net::HttpResponseHeaders("")); + response_headers->AddHeader("Content-Type: text/xml"); + fetcher->set_response_headers(response_headers); + if (!simulate_failure) { + std::string response(kBrandcodedXmlSettings); + size_t placeholder_index = response.find("$1"); + ASSERT_NE(std::string::npos, placeholder_index); + response.replace(placeholder_index, 2, kTestBrandedHomepage); + fetcher->SetResponseString(response); + } + fetcher->delegate()->OnURLFetchComplete(fetcher); +} + + +// Test fixture -------------------------------------------------------------- + +// ExtensionServiceTestBase sets up a TestingProfile with the ExtensionService, +// we then add the TemplateURLService, so the ProfileResetter can be exercised. +class AutomaticProfileResetterDelegateTest + : public extensions::ExtensionServiceTestBase { + protected: + AutomaticProfileResetterDelegateTest() {} + virtual ~AutomaticProfileResetterDelegateTest() {} + + virtual void SetUp() OVERRIDE { + extensions::ExtensionServiceTestBase::SetUp(); + ExtensionServiceInitParams params = CreateDefaultInitParams(); + params.pref_file.clear(); // Prescribes a TestingPrefService to be created. + InitializeExtensionService(params); + template_url_service_test_util_.reset( + new TemplateURLServiceFactoryTestUtil(profile_.get())); + resetter_delegate_.reset( + new AutomaticProfileResetterDelegateUnderTest(profile())); + } + + virtual void TearDown() OVERRIDE { + resetter_delegate_.reset(); + template_url_service_test_util_.reset(); + extensions::ExtensionServiceTestBase::TearDown(); + } + + scoped_ptr<TemplateURL> CreateTestTemplateURL() { + TemplateURLData data; + + data.SetURL("http://example.com/search?q={searchTerms}"); + data.suggestions_url = "http://example.com/suggest?q={searchTerms}"; + data.instant_url = "http://example.com/instant?q={searchTerms}"; + data.image_url = "http://example.com/image?q={searchTerms}"; + data.search_url_post_params = "search-post-params"; + data.suggestions_url_post_params = "suggest-post-params"; + data.instant_url_post_params = "instant-post-params"; + data.image_url_post_params = "image-post-params"; + + data.favicon_url = GURL("http://example.com/favicon.ico"); + data.new_tab_url = "http://example.com/newtab.html"; + data.alternate_urls.push_back("http://example.com/s?q={searchTerms}"); + + data.short_name = base::ASCIIToUTF16("name"); + data.SetKeyword(base::ASCIIToUTF16("keyword")); + data.search_terms_replacement_key = "search-terms-replacment-key"; + data.prepopulate_id = 42; + data.input_encodings.push_back("UTF-8"); + data.safe_for_autoreplace = true; + + return scoped_ptr<TemplateURL>(new TemplateURL(data)); + } + + void ExpectNoPendingBrandcodedConfigFetch() { + EXPECT_FALSE(test_url_fetcher_factory_.GetFetcherByID(0)); + } + + void ExpectAndServicePendingBrandcodedConfigFetch(bool simulate_failure) { + net::TestURLFetcher* fetcher = test_url_fetcher_factory_.GetFetcherByID(0); + ASSERT_TRUE(fetcher); + EXPECT_THAT(fetcher->upload_data(), + testing::HasSubstr(kTestBrandcode)); + ServicePendingBrancodedConfigFetch(fetcher, simulate_failure); + } + + void ExpectResetPromptState(bool active) { + GlobalErrorService* global_error_service = + GlobalErrorServiceFactory::GetForProfile(profile()); + GlobalError* global_error = global_error_service-> + GetGlobalErrorByMenuItemCommandID(IDC_SHOW_SETTINGS_RESET_BUBBLE); + EXPECT_EQ(active, !!global_error); + } + + AutomaticProfileResetterDelegateUnderTest* resetter_delegate() { + return resetter_delegate_.get(); + } + + TemplateURLServiceFactoryTestUtil* template_url_service_test_util() { + return template_url_service_test_util_.get(); + } + + private: + scoped_ptr<TemplateURLServiceFactoryTestUtil> template_url_service_test_util_; + net::TestURLFetcherFactory test_url_fetcher_factory_; + scoped_ptr<AutomaticProfileResetterDelegateUnderTest> resetter_delegate_; + + DISALLOW_COPY_AND_ASSIGN(AutomaticProfileResetterDelegateTest); +}; + + +// Tests --------------------------------------------------------------------- + +TEST_F(AutomaticProfileResetterDelegateTest, + TriggerAndWaitOnModuleEnumeration) { + // Expect ready_callback to be called just after the modules have been + // enumerated. Fail if it is not called. Note: as the EnumerateModulesModel is + // a global singleton, the callback might be invoked immediately if another + // test-case (e.g. the one below) has already performed module enumeration. + testing::StrictMock<MockCallbackTarget> mock_target; + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->RequestCallbackWhenLoadedModulesAreEnumerated( + mock_target.CreateClosure()); + resetter_delegate()->EnumerateLoadedModulesIfNeeded(); + base::RunLoop().RunUntilIdle(); + + testing::Mock::VerifyAndClearExpectations(&mock_target); + + // Expect ready_callback to be posted immediately when the modules have + // already been enumerated. + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->RequestCallbackWhenLoadedModulesAreEnumerated( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); + +#if defined(OS_WIN) + testing::Mock::VerifyAndClearExpectations(&mock_target); + + // Expect ready_callback to be posted immediately even when the modules had + // already been enumerated when the delegate was constructed. + scoped_ptr<AutomaticProfileResetterDelegate> late_resetter_delegate( + new AutomaticProfileResetterDelegateImpl(profile(), + ProfileResetter::ALL)); + + EXPECT_CALL(mock_target, Run()); + late_resetter_delegate->RequestCallbackWhenLoadedModulesAreEnumerated( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); +#endif +} + +TEST_F(AutomaticProfileResetterDelegateTest, GetLoadedModuleNameDigests) { + resetter_delegate()->EnumerateLoadedModulesIfNeeded(); + base::RunLoop().RunUntilIdle(); + scoped_ptr<base::ListValue> module_name_digests( + resetter_delegate()->GetLoadedModuleNameDigests()); + + // Just verify that each element looks like an MD5 hash in hexadecimal, and + // also that we have at least one element on Win. + ASSERT_TRUE(module_name_digests); + for (base::ListValue::const_iterator it = module_name_digests->begin(); + it != module_name_digests->end(); ++it) { + std::string digest_hex; + std::vector<uint8> digest_raw; + + ASSERT_TRUE((*it)->GetAsString(&digest_hex)); + ASSERT_TRUE(base::HexStringToBytes(digest_hex, &digest_raw)); + EXPECT_EQ(16u, digest_raw.size()); + } +#if defined(OS_WIN) + EXPECT_LE(1u, module_name_digests->GetSize()); +#endif +} + +TEST_F(AutomaticProfileResetterDelegateTest, LoadAndWaitOnTemplateURLService) { + // Expect ready_callback to be called just after the template URL service gets + // initialized. Fail if it is not called, or called too early. + testing::StrictMock<MockCallbackTarget> mock_target; + resetter_delegate()->RequestCallbackWhenTemplateURLServiceIsLoaded( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); + + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->LoadTemplateURLServiceIfNeeded(); + base::RunLoop().RunUntilIdle(); + + testing::Mock::VerifyAndClearExpectations(&mock_target); + + // Expect ready_callback to be posted immediately when the template URL + // service is already initialized. + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->RequestCallbackWhenTemplateURLServiceIsLoaded( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); + + testing::Mock::VerifyAndClearExpectations(&mock_target); + + // Expect ready_callback to be posted immediately even when the template URL + // service had already been initialized when the delegate was constructed. + scoped_ptr<AutomaticProfileResetterDelegate> late_resetter_delegate( + new AutomaticProfileResetterDelegateImpl(profile(), + ProfileResetter::ALL)); + + EXPECT_CALL(mock_target, Run()); + late_resetter_delegate->RequestCallbackWhenTemplateURLServiceIsLoaded( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(AutomaticProfileResetterDelegateTest, + DefaultSearchProviderDataWhenNotManaged) { + TemplateURLService* template_url_service = + TemplateURLServiceFactory::GetForProfile(profile()); + template_url_service_test_util()->VerifyLoad(); + + // Check that the "managed state" and the details returned by the delegate are + // correct. We verify the details against the data stored by + // TemplateURLService into Prefs. + scoped_ptr<TemplateURL> owned_custom_dsp(CreateTestTemplateURL()); + TemplateURL* custom_dsp = owned_custom_dsp.get(); + template_url_service->Add(owned_custom_dsp.release()); + template_url_service->SetUserSelectedDefaultSearchProvider(custom_dsp); + + PrefService* prefs = profile()->GetPrefs(); + ASSERT_TRUE(prefs); + scoped_ptr<base::DictionaryValue> dsp_details( + resetter_delegate()->GetDefaultSearchProviderDetails()); + const base::DictionaryValue* expected_dsp_details = + GetDefaultSearchProviderDetailsFromPrefs(prefs); + + ExpectDetailsMatch(*expected_dsp_details, *dsp_details); + EXPECT_FALSE(resetter_delegate()->IsDefaultSearchProviderManaged()); +} + +TEST_F(AutomaticProfileResetterDelegateTest, + DefaultSearchProviderDataWhenManaged) { + const char kTestSearchURL[] = "http://example.com/search?q={searchTerms}"; + const char kTestName[] = "name"; + const char kTestKeyword[] = "keyword"; + + template_url_service_test_util()->VerifyLoad(); + + EXPECT_FALSE(resetter_delegate()->IsDefaultSearchProviderManaged()); + + // Set managed preferences to emulate a default search provider set by policy. + template_url_service_test_util()->SetManagedDefaultSearchPreferences( + true, kTestName, kTestKeyword, kTestSearchURL, std::string(), + std::string(), std::string(), std::string(), std::string()); + + EXPECT_TRUE(resetter_delegate()->IsDefaultSearchProviderManaged()); + scoped_ptr<base::DictionaryValue> dsp_details( + resetter_delegate()->GetDefaultSearchProviderDetails()); + // Checking that all details are correct is already done by the above test. + // Just make sure details are reported about the correct engine. + base::ExpectDictStringValue(kTestSearchURL, *dsp_details, "search_url"); + + // Set managed preferences to emulate that having a default search provider is + // disabled by policy. + template_url_service_test_util()->RemoveManagedDefaultSearchPreferences(); + template_url_service_test_util()->SetManagedDefaultSearchPreferences( + false, std::string(), std::string(), std::string(), std::string(), + std::string(), std::string(), std::string(), std::string()); + + dsp_details = resetter_delegate()->GetDefaultSearchProviderDetails(); + EXPECT_TRUE(resetter_delegate()->IsDefaultSearchProviderManaged()); + EXPECT_TRUE(dsp_details->empty()); +} + +TEST_F(AutomaticProfileResetterDelegateTest, + GetPrepopulatedSearchProvidersDetails) { + TemplateURLService* template_url_service = + TemplateURLServiceFactory::GetForProfile(profile()); + template_url_service_test_util()->VerifyLoad(); + + scoped_ptr<base::ListValue> search_engines_details( + resetter_delegate()->GetPrepopulatedSearchProvidersDetails()); + + // Do the same kind of verification as for GetDefaultSearchEngineDetails: + // subsequently set each pre-populated engine as the default, so we can verify + // that the details returned by the delegate about one particular engine are + // correct in comparison to what has been stored to the Prefs. + std::vector<TemplateURL*> prepopulated_engines = + template_url_service->GetTemplateURLs(); + + ASSERT_EQ(prepopulated_engines.size(), search_engines_details->GetSize()); + + for (size_t i = 0; i < search_engines_details->GetSize(); ++i) { + const base::DictionaryValue* details = NULL; + ASSERT_TRUE(search_engines_details->GetDictionary(i, &details)); + + std::string keyword; + ASSERT_TRUE(details->GetString("keyword", &keyword)); + TemplateURL* search_engine = + template_url_service->GetTemplateURLForKeyword( + base::ASCIIToUTF16(keyword)); + ASSERT_TRUE(search_engine); + template_url_service->SetUserSelectedDefaultSearchProvider( + prepopulated_engines[i]); + + PrefService* prefs = profile()->GetPrefs(); + ASSERT_TRUE(prefs); + const base::DictionaryValue* expected_dsp_details = + GetDefaultSearchProviderDetailsFromPrefs(prefs); + ExpectDetailsMatch(*expected_dsp_details, *details); + } +} + +TEST_F(AutomaticProfileResetterDelegateTest, + FetchAndWaitOnDefaultSettingsVanilla) { + google_brand::BrandForTesting scoped_brand_for_testing((std::string())); + + // Expect ready_callback to be called just after empty brandcoded settings + // are loaded, given this is a vanilla build. Fail if it is not called, or + // called too early. + testing::StrictMock<MockCallbackTarget> mock_target; + resetter_delegate()->RequestCallbackWhenBrandcodedDefaultsAreFetched( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); + EXPECT_FALSE(resetter_delegate()->brandcoded_defaults()); + + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->FetchBrandcodedDefaultSettingsIfNeeded(); + base::RunLoop().RunUntilIdle(); + ExpectNoPendingBrandcodedConfigFetch(); + + testing::Mock::VerifyAndClearExpectations(&mock_target); + EXPECT_TRUE(resetter_delegate()->brandcoded_defaults()); + + // Expect ready_callback to be posted immediately when the brandcoded settings + // have already been loaded. + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->RequestCallbackWhenBrandcodedDefaultsAreFetched( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); + + // No test for a new instance of AutomaticProfileResetterDelegate. That will + // need to fetch the brandcoded settings again. +} + +TEST_F(AutomaticProfileResetterDelegateTest, + FetchAndWaitOnDefaultSettingsBranded) { + google_brand::BrandForTesting scoped_brand_for_testing(kTestBrandcode); + + // Expect ready_callback to be called just after the brandcoded settings are + // downloaded. Fail if it is not called, or called too early. + testing::StrictMock<MockCallbackTarget> mock_target; + resetter_delegate()->RequestCallbackWhenBrandcodedDefaultsAreFetched( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); + EXPECT_FALSE(resetter_delegate()->brandcoded_defaults()); + + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->FetchBrandcodedDefaultSettingsIfNeeded(); + ExpectAndServicePendingBrandcodedConfigFetch(false /*simulate_failure*/); + base::RunLoop().RunUntilIdle(); + + testing::Mock::VerifyAndClearExpectations(&mock_target); + const BrandcodedDefaultSettings* brandcoded_defaults = + resetter_delegate()->brandcoded_defaults(); + ASSERT_TRUE(brandcoded_defaults); + std::string homepage_url; + EXPECT_TRUE(brandcoded_defaults->GetHomepage(&homepage_url)); + EXPECT_EQ(kTestBrandedHomepage, homepage_url); + + // Expect ready_callback to be posted immediately when the brandcoded settings + // have already been downloaded. + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->RequestCallbackWhenBrandcodedDefaultsAreFetched( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(AutomaticProfileResetterDelegateTest, + FetchAndWaitOnDefaultSettingsBrandedFailure) { + google_brand::BrandForTesting scoped_brand_for_testing(kTestBrandcode); + + // Expect ready_callback to be called just after the brandcoded settings have + // failed to download. Fail if it is not called, or called too early. + testing::StrictMock<MockCallbackTarget> mock_target; + resetter_delegate()->RequestCallbackWhenBrandcodedDefaultsAreFetched( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); + + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->FetchBrandcodedDefaultSettingsIfNeeded(); + ExpectAndServicePendingBrandcodedConfigFetch(true /*simulate_failure*/); + base::RunLoop().RunUntilIdle(); + + testing::Mock::VerifyAndClearExpectations(&mock_target); + EXPECT_TRUE(resetter_delegate()->brandcoded_defaults()); + + // Expect ready_callback to be posted immediately when the brandcoded settings + // have already been attempted to be downloaded, but failed. + EXPECT_CALL(mock_target, Run()); + resetter_delegate()->RequestCallbackWhenBrandcodedDefaultsAreFetched( + mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(AutomaticProfileResetterDelegateTest, TriggerReset) { + google_brand::BrandForTesting scoped_brand_for_testing(kTestBrandcode); + + PrefService* prefs = profile()->GetPrefs(); + DCHECK(prefs); + prefs->SetString(prefs::kHomePage, kTestHomepage); + + testing::StrictMock<MockCallbackTarget> mock_target; + EXPECT_CALL(mock_target, Run()); + EXPECT_CALL(*resetter_delegate(), SendFeedback(testing::_)).Times(0); + resetter_delegate()->TriggerProfileSettingsReset( + false /*send_feedback*/, mock_target.CreateClosure()); + ExpectAndServicePendingBrandcodedConfigFetch(false /*simulate_failure*/); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(kTestBrandedHomepage, prefs->GetString(prefs::kHomePage)); +} + +TEST_F(AutomaticProfileResetterDelegateTest, + TriggerResetWithDefaultSettingsAlreadyLoaded) { + google_brand::BrandForTesting scoped_brand_for_testing(kTestBrandcode); + + PrefService* prefs = profile()->GetPrefs(); + DCHECK(prefs); + prefs->SetString(prefs::kHomePage, kTestHomepage); + + resetter_delegate()->FetchBrandcodedDefaultSettingsIfNeeded(); + ExpectAndServicePendingBrandcodedConfigFetch(false /*simulate_failure*/); + base::RunLoop().RunUntilIdle(); + + testing::StrictMock<MockCallbackTarget> mock_target; + EXPECT_CALL(mock_target, Run()); + EXPECT_CALL(*resetter_delegate(), SendFeedback(testing::_)).Times(0); + resetter_delegate()->TriggerProfileSettingsReset( + false /*send_feedback*/, mock_target.CreateClosure()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ(kTestBrandedHomepage, prefs->GetString(prefs::kHomePage)); +} + +TEST_F(AutomaticProfileResetterDelegateTest, + TriggerResetAndSendFeedback) { + google_brand::BrandForTesting scoped_brand_for_testing(kTestBrandcode); + + PrefService* prefs = profile()->GetPrefs(); + DCHECK(prefs); + prefs->SetString(prefs::kHomePage, kTestHomepage); + + testing::StrictMock<MockCallbackTarget> mock_target; + EXPECT_CALL(mock_target, Run()); + EXPECT_CALL(*resetter_delegate(), + SendFeedback(testing::HasSubstr(kTestHomepage))); + + resetter_delegate()->TriggerProfileSettingsReset( + true /*send_feedback*/, mock_target.CreateClosure()); + ExpectAndServicePendingBrandcodedConfigFetch(false /*simulate_failure*/); + base::RunLoop().RunUntilIdle(); +} + +TEST_F(AutomaticProfileResetterDelegateTest, ShowAndDismissPrompt) { + resetter_delegate()->TriggerPrompt(); + if (ProfileResetGlobalError::IsSupportedOnPlatform()) + ExpectResetPromptState(true /*active*/); + else + ExpectResetPromptState(false /*active*/); + resetter_delegate()->DismissPrompt(); + ExpectResetPromptState(false /*active*/); + resetter_delegate()->DismissPrompt(); +} + +} // namespace diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter_factory.cc b/chrome/browser/profile_resetter/automatic_profile_resetter_factory.cc new file mode 100644 index 0000000..7acdff7 --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter_factory.cc @@ -0,0 +1,72 @@ +// 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/profile_resetter/automatic_profile_resetter_factory.h" + +#include "base/memory/singleton.h" +#include "base/prefs/pref_registry_simple.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/search_engines/template_url_service_factory.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "chrome/common/pref_names.h" +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "content/public/browser/browser_context.h" + +// static +AutomaticProfileResetter* AutomaticProfileResetterFactory::GetForBrowserContext( + content::BrowserContext* context) { + return static_cast<AutomaticProfileResetter*>( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +// static +AutomaticProfileResetterFactory* +AutomaticProfileResetterFactory::GetInstance() { + return Singleton<AutomaticProfileResetterFactory>::get(); +} + +// static +void AutomaticProfileResetterFactory::RegisterPrefs( + PrefRegistrySimple* registry) { + registry->RegisterDictionaryPref( + prefs::kProfileResetPromptMementosInLocalState); +} + +AutomaticProfileResetterFactory::AutomaticProfileResetterFactory() + : BrowserContextKeyedServiceFactory( + "AutomaticProfileResetter", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(TemplateURLServiceFactory::GetInstance()); + DependsOn(GlobalErrorServiceFactory::GetInstance()); +} + +AutomaticProfileResetterFactory::~AutomaticProfileResetterFactory() {} + +KeyedService* AutomaticProfileResetterFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + Profile* profile = Profile::FromBrowserContext(context); + AutomaticProfileResetter* service = new AutomaticProfileResetter(profile); + service->Initialize(); + service->Activate(); + return service; +} + +void AutomaticProfileResetterFactory::RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) { + registry->RegisterStringPref( + prefs::kProfileResetPromptMementoInProfilePrefs, + "", + user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); +} + +bool AutomaticProfileResetterFactory:: + ServiceIsCreatedWithBrowserContext() const { + return true; +} + +bool AutomaticProfileResetterFactory::ServiceIsNULLWhileTesting() const { + return true; +} diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter_factory.h b/chrome/browser/profile_resetter/automatic_profile_resetter_factory.h new file mode 100644 index 0000000..ddd3777 --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter_factory.h @@ -0,0 +1,55 @@ +// 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. + +#ifndef CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_FACTORY_H_ +#define CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_FACTORY_H_ + +#include "base/basictypes.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +template <typename T> +struct DefaultSingletonTraits; + +class PrefRegistrySimple; + +namespace content { +class BrowserContext; +} + +namespace user_prefs { +class PrefRegistrySyncable; +} + +class AutomaticProfileResetter; + +class AutomaticProfileResetterFactory + : public BrowserContextKeyedServiceFactory { + public: + static AutomaticProfileResetter* GetForBrowserContext( + content::BrowserContext* context); + static AutomaticProfileResetterFactory* GetInstance(); + + // Registers Local State preferences. + static void RegisterPrefs(PrefRegistrySimple* registry); + + private: + friend struct DefaultSingletonTraits<AutomaticProfileResetterFactory>; + + AutomaticProfileResetterFactory(); + virtual ~AutomaticProfileResetterFactory(); + + // BrowserContextKeyedServiceFactory: + virtual KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const OVERRIDE; + + // BrowserContextKeyedBaseFactory: + virtual void RegisterProfilePrefs( + user_prefs::PrefRegistrySyncable* registry) OVERRIDE; + virtual bool ServiceIsCreatedWithBrowserContext() const OVERRIDE; + virtual bool ServiceIsNULLWhileTesting() const OVERRIDE; + + DISALLOW_COPY_AND_ASSIGN(AutomaticProfileResetterFactory); +}; + +#endif // CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_FACTORY_H_ diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter_mementos.cc b/chrome/browser/profile_resetter/automatic_profile_resetter_mementos.cc new file mode 100644 index 0000000..a76b98b --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter_mementos.cc @@ -0,0 +1,129 @@ +// 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/profile_resetter/automatic_profile_resetter_mementos.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/prefs/pref_service.h" +#include "base/prefs/scoped_user_pref_update.h" +#include "base/values.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/pref_names.h" +#include "content/public/browser/browser_thread.h" + +using base::DictionaryValue; + + +// AutomaticProfileResetter::PreferenceHostedPromptMemento ------------------- + +PreferenceHostedPromptMemento::PreferenceHostedPromptMemento(Profile* profile) + : profile_(profile) {} + +PreferenceHostedPromptMemento::~PreferenceHostedPromptMemento() {} + +std::string PreferenceHostedPromptMemento::ReadValue() const { + PrefService* prefs = profile_->GetPrefs(); + DCHECK(prefs); + return prefs->GetString(prefs::kProfileResetPromptMementoInProfilePrefs); +} + +void PreferenceHostedPromptMemento::StoreValue(const std::string& value) { + PrefService* prefs = profile_->GetPrefs(); + DCHECK(prefs); + prefs->SetString(prefs::kProfileResetPromptMementoInProfilePrefs, value); +} + + +// AutomaticProfileResetter::LocalStateHostedPromptMemento ------------------- + +LocalStateHostedPromptMemento::LocalStateHostedPromptMemento(Profile* profile) + : profile_(profile) {} + +LocalStateHostedPromptMemento::~LocalStateHostedPromptMemento() {} + +std::string LocalStateHostedPromptMemento::ReadValue() const { + PrefService* local_state = g_browser_process->local_state(); + DCHECK(local_state); + + const base::DictionaryValue* prompt_shown_dict = local_state->GetDictionary( + prefs::kProfileResetPromptMementosInLocalState); + std::string profile_key = GetProfileKey(); + if (!prompt_shown_dict || profile_key.empty()) { + NOTREACHED(); + return std::string(); + } + std::string value; + return prompt_shown_dict->GetStringWithoutPathExpansion(profile_key, &value) ? + value : std::string(); +} + +void LocalStateHostedPromptMemento::StoreValue(const std::string& value) { + PrefService* local_state = g_browser_process->local_state(); + DCHECK(local_state); + + DictionaryPrefUpdate prompt_shown_dict_update( + local_state, prefs::kProfileResetPromptMementosInLocalState); + std::string profile_key = GetProfileKey(); + if (profile_key.empty()) { + NOTREACHED(); + return; + } + prompt_shown_dict_update.Get()->SetStringWithoutPathExpansion(profile_key, + value); +} + +std::string LocalStateHostedPromptMemento::GetProfileKey() const { + return profile_->GetPath().BaseName().MaybeAsASCII(); +} + + +// AutomaticProfileResetter::FileHostedPromptMemento ------------------------- + +FileHostedPromptMemento::FileHostedPromptMemento(Profile* profile) + : profile_(profile) {} + +FileHostedPromptMemento::~FileHostedPromptMemento() {} + +void FileHostedPromptMemento::ReadValue( + const ReadValueCallback& callback) const { + base::FilePath path = GetMementoFilePath(); + content::BrowserThread::PostTaskAndReplyWithResult( + content::BrowserThread::FILE, + FROM_HERE, + base::Bind(&ReadValueOnFileThread, path), + callback); +} + +void FileHostedPromptMemento::StoreValue(const std::string& value) { + base::FilePath path = GetMementoFilePath(); + content::BrowserThread::PostTask( + content::BrowserThread::FILE, + FROM_HERE, + base::Bind(&StoreValueOnFileThread, path, value)); +} + +std::string FileHostedPromptMemento::ReadValueOnFileThread( + const base::FilePath& memento_file_path) { + std::string value; + base::ReadFileToString(memento_file_path, &value); + return value; +} + +void FileHostedPromptMemento::StoreValueOnFileThread( + const base::FilePath& memento_file_path, + const std::string& value) { + int retval = + base::WriteFile(memento_file_path, value.c_str(), value.size()); + DCHECK_EQ(retval, (int)value.size()); +} + +base::FilePath FileHostedPromptMemento::GetMementoFilePath() const { + return profile_->GetPath().Append(chrome::kResetPromptMementoFilename); +} diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter_mementos.h b/chrome/browser/profile_resetter/automatic_profile_resetter_mementos.h new file mode 100644 index 0000000..025ffb2 --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter_mementos.h @@ -0,0 +1,101 @@ +// 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. + +// The classes in this file are alternative implementations of the concept of a +// "prompt memento", a token of some kind that gets stored when we show the +// one-time profile reset prompt, and which then serves as a reminder that we +// should not show the prompt again. +// +// In an ideal world, a single implementation would suffice, however, we expect +// that third party software might accidentally interfere with some of these +// methods. We need this redundancy because we want to make absolutely sure that +// we do not annoy the user with the prompt multiple times. + +#ifndef CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_MEMENTOS_H_ +#define CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_MEMENTOS_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/callback.h" + +namespace base { +class FilePath; +} + +class Profile; + +// This class is a wrapper around the user preference that gets stored when we +// show the one-time profile reset prompt, and which is kept as a reminder that +// we should not show the prompt again. +class PreferenceHostedPromptMemento { + public: + explicit PreferenceHostedPromptMemento(Profile* profile); + ~PreferenceHostedPromptMemento(); + + std::string ReadValue() const; + void StoreValue(const std::string& value); + + private: + Profile* profile_; + + DISALLOW_COPY_AND_ASSIGN(PreferenceHostedPromptMemento); +}; + +// This class is a wrapper around the Local State preference that gets stored +// when we show the one-time profile reset prompt, and which is kept as a +// reminder that we should not show the prompt again. +class LocalStateHostedPromptMemento { + public: + explicit LocalStateHostedPromptMemento(Profile* profile); + ~LocalStateHostedPromptMemento(); + + std::string ReadValue() const; + void StoreValue(const std::string& value); + + private: + // Returns the key that shall be used in the dictionary preference in Local + // State to uniquely identify this profile. + std::string GetProfileKey() const; + + Profile* profile_; + + DISALLOW_COPY_AND_ASSIGN(LocalStateHostedPromptMemento); +}; + +// This class manages a marker file that gets stored when we show the one-time +// profile reset prompt, and which is kept as a reminder that we should not show +// the prompt again. +class FileHostedPromptMemento { + public: + typedef base::Callback<void(const std::string&)> ReadValueCallback; + + explicit FileHostedPromptMemento(Profile* profile); + ~FileHostedPromptMemento(); + + // Posts to the FILE thread to read the value, then returns the value to the + // calling thread. It is safe to destroy this object as soon as this method + // (synchronously) returns. + void ReadValue(const ReadValueCallback& callback) const; + + // Asynchronously stores the value on the FILE thread. However, it is safe to + // destroy this object as soon as this method (synchronously) returns. + void StoreValue(const std::string& value); + + private: + static std::string ReadValueOnFileThread( + const base::FilePath& memento_file_path); + static void StoreValueOnFileThread(const base::FilePath& memento_file_path, + const std::string& value); + + // Returns the path to the file that shall be used to store this kind of + // memento for this profile. + base::FilePath GetMementoFilePath() const; + + Profile* profile_; + + DISALLOW_COPY_AND_ASSIGN(FileHostedPromptMemento); +}; + +#endif // CHROME_BROWSER_PROFILE_RESETTER_AUTOMATIC_PROFILE_RESETTER_MEMENTOS_H_ diff --git a/chrome/browser/profile_resetter/automatic_profile_resetter_unittest.cc b/chrome/browser/profile_resetter/automatic_profile_resetter_unittest.cc new file mode 100644 index 0000000..ad6ed03 --- /dev/null +++ b/chrome/browser/profile_resetter/automatic_profile_resetter_unittest.cc @@ -0,0 +1,1380 @@ +// 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/profile_resetter/automatic_profile_resetter.h" + +#include <string> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/metrics/field_trial.h" +#include "base/prefs/pref_registry_simple.h" +#include "base/prefs/testing_pref_service.h" +#include "base/run_loop.h" +#include "base/test/test_simple_task_runner.h" +#include "base/threading/sequenced_worker_pool.h" +#include "base/values.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter_delegate.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter_factory.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter_mementos.h" +#include "chrome/browser/profile_resetter/jtl_foundation.h" +#include "chrome/browser/profile_resetter/jtl_instructions.h" +#include "chrome/test/base/scoped_testing_local_state.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_pref_service_syncable.h" +#include "chrome/test/base/testing_profile.h" +#include "components/pref_registry/pref_registry_syncable.h" +#include "components/variations/variations_associated_data.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::_; + +namespace { + +const char kAutomaticProfileResetStudyName[] = "AutomaticProfileReset"; +const char kStudyDisabledGroupName[] = "Disabled"; +const char kStudyDryRunGroupName[] = "DryRun"; +const char kStudyEnabledGroupName[] = "Enabled"; + +const char kTestHashSeed[] = "testing-hash-seed"; +const char kTestMementoValue[] = "01234567890123456789012345678901"; +const char kTestInvalidMementoValue[] = "12345678901234567890123456789012"; + +const char kTestPreferencePath[] = "testing.preference"; +const char kTestPreferenceValue[] = "testing-preference-value"; + +const char kSearchURLAttributeKey[] = "search_url"; +const char kTestSearchURL[] = "http://example.com/search?q={searchTerms}"; +const char kTestSearchURL2[] = "http://google.com/?q={searchTerms}"; + +const char kTestModuleDigest[] = "01234567890123456789012345678901"; +const char kTestModuleDigest2[] = "12345678901234567890123456789012"; + +// Helpers ------------------------------------------------------------------ + +// A testing version of the AutomaticProfileResetter that differs from the real +// one only in that it has its statistics reporting mocked out for verification. +class AutomaticProfileResetterUnderTest : public AutomaticProfileResetter { + public: + explicit AutomaticProfileResetterUnderTest(Profile* profile) + : AutomaticProfileResetter(profile) {} + virtual ~AutomaticProfileResetterUnderTest() {} + + MOCK_METHOD2(ReportStatistics, void(uint32, uint32)); + MOCK_METHOD1(ReportPromptResult, + void(AutomaticProfileResetter::PromptResult)); + + private: + DISALLOW_COPY_AND_ASSIGN(AutomaticProfileResetterUnderTest); +}; + +class MockProfileResetterDelegate : public AutomaticProfileResetterDelegate { + public: + MockProfileResetterDelegate() + : emulated_is_default_search_provider_managed_(false) {} + virtual ~MockProfileResetterDelegate() {} + + MOCK_METHOD0(EnumerateLoadedModulesIfNeeded, void()); + MOCK_CONST_METHOD1(RequestCallbackWhenLoadedModulesAreEnumerated, + void(const base::Closure&)); + + MOCK_METHOD0(LoadTemplateURLServiceIfNeeded, void()); + MOCK_CONST_METHOD1(RequestCallbackWhenTemplateURLServiceIsLoaded, + void(const base::Closure&)); + + MOCK_METHOD0(FetchBrandcodedDefaultSettingsIfNeeded, void()); + MOCK_CONST_METHOD1(RequestCallbackWhenBrandcodedDefaultsAreFetched, + void(const base::Closure&)); + + MOCK_CONST_METHOD0(OnGetLoadedModuleNameDigestsCalled, void()); + MOCK_CONST_METHOD0(OnGetDefaultSearchProviderDetailsCalled, void()); + MOCK_CONST_METHOD0(OnIsDefaultSearchProviderManagedCalled, void()); + MOCK_CONST_METHOD0(OnGetPrepopulatedSearchProvidersDetailsCalled, void()); + + MOCK_METHOD0(TriggerPrompt, bool()); + MOCK_METHOD2(TriggerProfileSettingsReset, void(bool, const base::Closure&)); + MOCK_METHOD0(DismissPrompt, void()); + + virtual scoped_ptr<base::ListValue> + GetLoadedModuleNameDigests() const OVERRIDE { + OnGetLoadedModuleNameDigestsCalled(); + return scoped_ptr<base::ListValue>( + emulated_loaded_module_digests_.DeepCopy()); + } + + virtual scoped_ptr<base::DictionaryValue> + GetDefaultSearchProviderDetails() const OVERRIDE { + OnGetDefaultSearchProviderDetailsCalled(); + return scoped_ptr<base::DictionaryValue>( + emulated_default_search_provider_details_.DeepCopy()); + } + + virtual bool IsDefaultSearchProviderManaged() const OVERRIDE { + OnIsDefaultSearchProviderManagedCalled(); + return emulated_is_default_search_provider_managed_; + } + + virtual scoped_ptr<base::ListValue> + GetPrepopulatedSearchProvidersDetails() const OVERRIDE { + OnGetPrepopulatedSearchProvidersDetailsCalled(); + return scoped_ptr<base::ListValue>( + emulated_search_providers_details_.DeepCopy()); + } + + static void ClosureInvoker(const base::Closure& closure) { closure.Run(); } + + void ExpectCallsToDependenciesSetUpMethods() { + EXPECT_CALL(*this, EnumerateLoadedModulesIfNeeded()); + EXPECT_CALL(*this, LoadTemplateURLServiceIfNeeded()); + EXPECT_CALL(*this, RequestCallbackWhenLoadedModulesAreEnumerated(_)) + .WillOnce(testing::Invoke(ClosureInvoker)); + EXPECT_CALL(*this, RequestCallbackWhenTemplateURLServiceIsLoaded(_)) + .WillOnce(testing::Invoke(ClosureInvoker)); + } + + void ExpectCallsToGetterMethods() { + EXPECT_CALL(*this, OnGetLoadedModuleNameDigestsCalled()); + EXPECT_CALL(*this, OnGetDefaultSearchProviderDetailsCalled()); + EXPECT_CALL(*this, OnIsDefaultSearchProviderManagedCalled()); + EXPECT_CALL(*this, OnGetPrepopulatedSearchProvidersDetailsCalled()); + } + + void ExpectCallToShowPrompt() { + EXPECT_CALL(*this, TriggerPrompt()).WillOnce(testing::Return(true)); + EXPECT_CALL(*this, FetchBrandcodedDefaultSettingsIfNeeded()); + } + + void ExpectCallToTriggerReset(bool send_feedback) { + EXPECT_CALL(*this, TriggerProfileSettingsReset(send_feedback, _)) + .WillOnce(testing::SaveArg<1>(&reset_completion_)); + } + + base::DictionaryValue& emulated_default_search_provider_details() { + return emulated_default_search_provider_details_; + } + + base::ListValue& emulated_search_providers_details() { + return emulated_search_providers_details_; + } + + base::ListValue& emulated_loaded_module_digests() { + return emulated_loaded_module_digests_; + } + + void set_emulated_is_default_search_provider_managed(bool value) { + emulated_is_default_search_provider_managed_ = value; + } + + void EmulateProfileResetCompleted() { + reset_completion_.Run(); + } + + private: + base::DictionaryValue emulated_default_search_provider_details_; + base::ListValue emulated_search_providers_details_; + base::ListValue emulated_loaded_module_digests_; + bool emulated_is_default_search_provider_managed_; + base::Closure reset_completion_; + + DISALLOW_COPY_AND_ASSIGN(MockProfileResetterDelegate); +}; + +class FileHostedPromptMementoSynchronous : protected FileHostedPromptMemento { + public: + explicit FileHostedPromptMementoSynchronous(Profile* profile) + : FileHostedPromptMemento(profile) {} + + std::string ReadValue() const { + std::string result; + FileHostedPromptMemento::ReadValue(base::Bind(&AssignArgumentTo, &result)); + base::RunLoop().RunUntilIdle(); + return result; + } + + void StoreValue(const std::string& value) { + FileHostedPromptMemento::StoreValue(value); + base::RunLoop().RunUntilIdle(); + } + + private: + static void AssignArgumentTo(std::string* target, const std::string& value) { + *target = value; + } + + DISALLOW_COPY_AND_ASSIGN(FileHostedPromptMementoSynchronous); +}; + +std::string GetHash(const std::string& input) { + return jtl_foundation::Hasher(kTestHashSeed).GetHash(input); +} + +// Encodes a Boolean argument value into JTL bytecode. +std::string EncodeBool(bool value) { return value ? VALUE_TRUE : VALUE_FALSE; } + +// Constructs a simple evaluation program to test that basic input/output works +// well. It will emulate a scenario in which the reset criteria are satisfied as +// prescribed by |emulate_satisfied_criterion_{1|2}|, and the reset is triggered +// when either of them is true. The bits in the combined status mask will be set +// according to whether or not the memento values received in the input were as +// expected. +// +// More specifically, the output of the program will be as follows: +// { +// "satisfied_criteria_mask_bit1": emulate_satisfied_criterion_1, +// "satisfied_criteria_mask_bit2": emulate_satisfied_criterion_2, +// "combined_status_mask_bit1": +// (emulate_satisfied_criterion_1 || emulate_satisfied_criterion_2), +// "combined_status_mask_bit2": +// (input["memento_value_in_prefs"] == kTestMementoValue), +// "combined_status_mask_bit3": +// (input["memento_value_in_local_state"] == kTestMementoValue), +// "combined_status_mask_bit4": +// (input["memento_value_in_file"] == kTestMementoValue), +// "should_prompt": +// (emulate_satisfied_criterion_1 || emulate_satisfied_criterion_2), +// "had_prompted_already": <OR-combination of above three>, +// "memento_value_in_prefs": kTestMementoValue, +// "memento_value_in_local_state": kTestMementoValue, +// "memento_value_in_file": kTestMementoValue +// } +std::string ConstructProgram(bool emulate_satisfied_criterion_1, + bool emulate_satisfied_criterion_2) { + std::string bytecode; + bytecode += OP_STORE_BOOL(GetHash("satisfied_criteria_mask_bit1"), + EncodeBool(emulate_satisfied_criterion_1)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_BOOL(GetHash("satisfied_criteria_mask_bit2"), + EncodeBool(emulate_satisfied_criterion_2)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_BOOL(GetHash("should_prompt"), + EncodeBool(emulate_satisfied_criterion_1 || + emulate_satisfied_criterion_2)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit1"), + EncodeBool(emulate_satisfied_criterion_1 || + emulate_satisfied_criterion_2)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("memento_value_in_prefs")); + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestMementoValue)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit2"), VALUE_TRUE); + bytecode += OP_STORE_BOOL(GetHash("had_prompted_already"), VALUE_TRUE); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("memento_value_in_local_state")); + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestMementoValue)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit3"), VALUE_TRUE); + bytecode += OP_STORE_BOOL(GetHash("had_prompted_already"), VALUE_TRUE); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("memento_value_in_file")); + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestMementoValue)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit4"), VALUE_TRUE); + bytecode += OP_STORE_BOOL(GetHash("had_prompted_already"), VALUE_TRUE); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_HASH(GetHash("memento_value_in_prefs"), + kTestMementoValue); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_HASH(GetHash("memento_value_in_local_state"), + kTestMementoValue); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_HASH(GetHash("memento_value_in_file"), + kTestMementoValue); + bytecode += OP_END_OF_SENTENCE; + return bytecode; +} + +// Constructs another evaluation program to specifically test that bits of the +// "satisfied_criteria_mask" are correctly assigned, and so is "should_prompt"; +// and that reset is triggered iff the latter is true, regardless of the bits +// in the mask (so as to allow for a non-disjunctive compound criterion). +// +// More specifically, the output of the program will be as follows: +// { +// "satisfied_criteria_mask_bitN": emulate_satisfied_odd_criteria, +// "satisfied_criteria_mask_bitM": emulate_satisfied_even_criteria, +// "combined_status_mask_bit1": emulate_should_prompt, +// "should_prompt": emulate_should_prompt, +// "memento_value_in_prefs": kTestMementoValue, +// "memento_value_in_local_state": kTestMementoValue, +// "memento_value_in_file": kTestMementoValue +// } +// ... such that N is {1,3,5} and M is {2,4}. +std::string ConstructProgramToExerciseCriteria( + bool emulate_should_prompt, + bool emulate_satisfied_odd_criteria, + bool emulate_satisfied_even_criteria) { + std::string bytecode; + bytecode += OP_STORE_BOOL(GetHash("satisfied_criteria_mask_bit1"), + EncodeBool(emulate_satisfied_odd_criteria)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_BOOL(GetHash("satisfied_criteria_mask_bit3"), + EncodeBool(emulate_satisfied_odd_criteria)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_BOOL(GetHash("satisfied_criteria_mask_bit5"), + EncodeBool(emulate_satisfied_odd_criteria)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_BOOL(GetHash("satisfied_criteria_mask_bit2"), + EncodeBool(emulate_satisfied_even_criteria)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_BOOL(GetHash("satisfied_criteria_mask_bit4"), + EncodeBool(emulate_satisfied_even_criteria)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_BOOL(GetHash("should_prompt"), + EncodeBool(emulate_should_prompt)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit1"), + EncodeBool(emulate_should_prompt)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_HASH(GetHash("memento_value_in_prefs"), + kTestMementoValue); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_HASH(GetHash("memento_value_in_local_state"), + kTestMementoValue); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_STORE_HASH(GetHash("memento_value_in_file"), + kTestMementoValue); + bytecode += OP_END_OF_SENTENCE; + return bytecode; +} + +// Constructs another evaluation program to specifically test that local state +// and user preference values are included in the input as expected. We will +// re-purpose the output bitmasks to channel out information about the outcome +// of the checks. +// +// More specifically, the output of the program will be as follows: +// { +// "combined_status_mask_bit1": +// (input["preferences.testing.preference"] == kTestPreferenceValue) +// "combined_status_mask_bit2": +// (input["local_state.testing.preference"] == kTestPreferenceValue) +// "combined_status_mask_bit3": input["preferences_iuc.testing.preference"] +// "combined_status_mask_bit4": input["local_state_iuc.testing.preference"] +// } +std::string ConstructProgramToCheckPreferences() { + std::string bytecode; + bytecode += OP_NAVIGATE(GetHash("preferences")); + bytecode += OP_NAVIGATE(GetHash("testing")); + bytecode += OP_NAVIGATE(GetHash("preference")); + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestPreferenceValue)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit1"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("local_state")); + bytecode += OP_NAVIGATE(GetHash("testing")); + bytecode += OP_NAVIGATE(GetHash("preference")); + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestPreferenceValue)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit2"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("preferences_iuc")); + bytecode += OP_NAVIGATE(GetHash("testing")); + bytecode += OP_NAVIGATE(GetHash("preference")); + bytecode += OP_COMPARE_NODE_BOOL(EncodeBool(true)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit3"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("local_state_iuc")); + bytecode += OP_NAVIGATE(GetHash("testing")); + bytecode += OP_NAVIGATE(GetHash("preference")); + bytecode += OP_COMPARE_NODE_BOOL(EncodeBool(true)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit4"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + return bytecode; +} + +// Legend for the bitmask returned by the above program. +enum CombinedStatusMaskLegendForCheckingPreferences { + HAS_EXPECTED_USER_PREFERENCE = 1 << 0, + HAS_EXPECTED_LOCAL_STATE_PREFERENCE = 1 << 1, + USER_PREFERENCE_IS_USER_CONTROLLED = 1 << 2, + LOCAL_STATE_IS_USER_CONTROLLED = 1 << 3, +}; + +// Constructs yet another evaluation program to specifically test that default +// and pre-populated search engines are included in the input as expected. We +// will re-purpose the output bitmasks to channel out information about the +// outcome of the checks. +// +// More specifically, the output of the program will be as follows: +// { +// "combined_status_mask_bit1": +// (input["default_search_provider.search_url"] == kTestSearchURL) +// "combined_status_mask_bit2": input["default_search_provider_iuc"] +// "combined_status_mask_bit3": +// (input["search_providers.*.search_url"] == kTestSearchURL) +// "combined_status_mask_bit4": +// (input["search_providers.*.search_url"] == kTestSearchURL2) +// } +std::string ConstructProgramToCheckSearchEngines() { + std::string bytecode; + bytecode += OP_NAVIGATE(GetHash("default_search_provider")); + bytecode += OP_NAVIGATE(GetHash("search_url")); + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestSearchURL)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit1"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("default_search_provider_iuc")); + bytecode += OP_COMPARE_NODE_BOOL(EncodeBool(true)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit2"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("search_providers")); + bytecode += OP_NAVIGATE_ANY; + bytecode += OP_NAVIGATE(GetHash("search_url")); + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestSearchURL)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit3"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("search_providers")); + bytecode += OP_NAVIGATE_ANY; + bytecode += OP_NAVIGATE(GetHash("search_url")); + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestSearchURL2)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit4"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + return bytecode; +} + +// Legend for the bitmask returned by the above program. +enum CombinedStatusMaskLegendForCheckingSearchEngines { + HAS_EXPECTED_DEFAULT_SEARCH_PROVIDER = 1 << 0, + DEFAULT_SEARCH_PROVIDER_IS_USER_CONTROLLED = 1 << 1, + HAS_EXPECTED_PREPOPULATED_SEARCH_PROVIDER_1 = 1 << 2, + HAS_EXPECTED_PREPOPULATED_SEARCH_PROVIDER_2 = 1 << 3, +}; + +// Constructs yet another evaluation program to specifically test that loaded +// module digests are included in the input as expected. We will re-purpose the +// output bitmasks to channel out information about the outcome of the checks. +// +// More specifically, the output of the program will be as follows: +// { +// "combined_status_mask_bit1": +// (input["loaded_modules.*"] == kTestModuleDigest) +// "combined_status_mask_bit2": +// (input["loaded_modules.*"] == kTestModuleDigest2) +// } +std::string ConstructProgramToCheckLoadedModuleDigests() { + std::string bytecode; + bytecode += OP_NAVIGATE(GetHash("loaded_modules")); + bytecode += OP_NAVIGATE_ANY; + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestModuleDigest)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit1"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + bytecode += OP_NAVIGATE(GetHash("loaded_modules")); + bytecode += OP_NAVIGATE_ANY; + bytecode += OP_COMPARE_NODE_HASH(GetHash(kTestModuleDigest2)); + bytecode += OP_STORE_BOOL(GetHash("combined_status_mask_bit2"), + EncodeBool(true)); + bytecode += OP_END_OF_SENTENCE; + return bytecode; +} + +// Legend for the bitmask returned by the above program. +enum CombinedStatusMaskLegendForCheckingLoadedModules { + HAS_EXPECTED_MODULE_DIGEST_1 = 1 << 0, + HAS_EXPECTED_MODULE_DIGEST_2 = 1 << 1, +}; + +// Test fixtures ------------------------------------------------------------- + +class AutomaticProfileResetterTestBase : public testing::Test { + protected: + explicit AutomaticProfileResetterTestBase( + const std::string& experiment_group_name) + : waiting_task_runner_(new base::TestSimpleTaskRunner), + local_state_(TestingBrowserProcess::GetGlobal()), + profile_(new TestingProfile()), + field_trials_(new base::FieldTrialList(NULL)), + memento_in_prefs_(new PreferenceHostedPromptMemento(profile())), + memento_in_local_state_(new LocalStateHostedPromptMemento(profile())), + memento_in_file_(new FileHostedPromptMementoSynchronous(profile())), + experiment_group_name_(experiment_group_name), + inject_data_through_variation_params_(false), + mock_delegate_(NULL) { + // Make sure the factory is not optimized away, so whatever preferences it + // wants to register will actually get registered. + AutomaticProfileResetterFactory::GetInstance(); + + // Register some additional local state preferences for testing purposes. + PrefRegistrySimple* local_state_registry = local_state_.Get()->registry(); + DCHECK(local_state_registry); + local_state_registry->RegisterStringPref(kTestPreferencePath, ""); + + // Register some additional user preferences for testing purposes. + user_prefs::PrefRegistrySyncable* user_prefs_registry = + profile_->GetTestingPrefService()->registry(); + DCHECK(user_prefs_registry); + user_prefs_registry->RegisterStringPref( + kTestPreferencePath, std::string(), + user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); + } + + virtual void SetUp() OVERRIDE { + variations::testing::ClearAllVariationParams(); + base::FieldTrialList::CreateFieldTrial(kAutomaticProfileResetStudyName, + experiment_group_name_); + resetter_.reset( + new testing::StrictMock<AutomaticProfileResetterUnderTest>(profile())); + mock_delegate_owned_.reset( + new testing::StrictMock<MockProfileResetterDelegate>()); + mock_delegate_ = mock_delegate_owned_.get(); + + ExpectAllMementoValuesEqualTo(std::string()); + } + + void SetTestingHashSeed(const std::string& hash_seed) { + testing_hash_seed_ = hash_seed; + } + + void SetTestingProgram(const std::string& source_code) { + testing_program_ = source_code; + } + + void AllowInjectingTestDataThroughVariationParams(bool value) { + inject_data_through_variation_params_ = value; + } + + void ExpectAllMementoValuesEqualTo(const std::string& value) { + EXPECT_EQ(value, memento_in_prefs_->ReadValue()); + EXPECT_EQ(value, memento_in_local_state_->ReadValue()); + EXPECT_EQ(value, memento_in_file_->ReadValue()); + } + + void UnleashResetterAndWait() { + if (inject_data_through_variation_params_) { + std::map<std::string, std::string> variation_params; + variation_params["program"] = testing_program_; + variation_params["hash_seed"] = testing_hash_seed_; + ASSERT_TRUE(variations::AssociateVariationParams( + kAutomaticProfileResetStudyName, + experiment_group_name_, + variation_params)); + } + resetter_->Initialize(); + resetter_->SetDelegateForTesting( + mock_delegate_owned_.PassAs<AutomaticProfileResetterDelegate>()); + resetter_->SetTaskRunnerForWaitingForTesting(waiting_task_runner_); + if (!inject_data_through_variation_params_) { + resetter_->SetProgramForTesting(testing_program_); + resetter_->SetHashSeedForTesting(testing_hash_seed_); + } + resetter_->Activate(); + + if (waiting_task_runner_->HasPendingTask()) { + ASSERT_EQ(base::TimeDelta::FromSeconds(55), + waiting_task_runner_->NextPendingTaskDelay()); + waiting_task_runner_->RunPendingTasks(); + } + base::RunLoop().RunUntilIdle(); + content::BrowserThread::GetBlockingPool()->FlushForTesting(); + base::RunLoop().RunUntilIdle(); + } + + // Goes through an evaluation flow such that the reset criteria are satisfied. + // Used to reduce boilerplate for tests that need to verify behavior during + // the reset prompt flow. + void OrchestrateThroughEvaluationFlow() { + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + mock_delegate().ExpectCallToShowPrompt(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x01u)); + + UnleashResetterAndWait(); + + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + testing::Mock::VerifyAndClearExpectations(&resetter()); + testing::Mock::VerifyAndClearExpectations(&mock_delegate()); + } + + // Explicitly shut down the service to double-check that nothing explodes, but + // first, verify expectations to make sure the service makes no more calls to + // any mocked functions during or after shutdown. + void VerifyExpectationsThenShutdownResetter() { + testing::Mock::VerifyAndClearExpectations(&resetter()); + testing::Mock::VerifyAndClearExpectations(&mock_delegate()); + + resetter_->Shutdown(); + resetter_.reset(); + } + + TestingProfile* profile() { return profile_.get(); } + TestingPrefServiceSimple* local_state() { return local_state_.Get(); } + + PreferenceHostedPromptMemento& memento_in_prefs() { + return *memento_in_prefs_; + } + + LocalStateHostedPromptMemento& memento_in_local_state() { + return *memento_in_local_state_; + } + + FileHostedPromptMementoSynchronous& memento_in_file() { + return *memento_in_file_; + } + + MockProfileResetterDelegate& mock_delegate() { return *mock_delegate_; } + AutomaticProfileResetterUnderTest& resetter() { return *resetter_; } + + private: + content::TestBrowserThreadBundle thread_bundle_; + scoped_refptr<base::TestSimpleTaskRunner> waiting_task_runner_; + ScopedTestingLocalState local_state_; + scoped_ptr<TestingProfile> profile_; + scoped_ptr<base::FieldTrialList> field_trials_; + scoped_ptr<PreferenceHostedPromptMemento> memento_in_prefs_; + scoped_ptr<LocalStateHostedPromptMemento> memento_in_local_state_; + scoped_ptr<FileHostedPromptMementoSynchronous> memento_in_file_; + + std::string experiment_group_name_; + std::string testing_program_; + std::string testing_hash_seed_; + bool inject_data_through_variation_params_; + + scoped_ptr<AutomaticProfileResetterUnderTest> resetter_; + scoped_ptr<MockProfileResetterDelegate> mock_delegate_owned_; + MockProfileResetterDelegate* mock_delegate_; + + DISALLOW_COPY_AND_ASSIGN(AutomaticProfileResetterTestBase); +}; + +class AutomaticProfileResetterTest : public AutomaticProfileResetterTestBase { + protected: + AutomaticProfileResetterTest() + : AutomaticProfileResetterTestBase(kStudyEnabledGroupName) {} +}; + +class AutomaticProfileResetterTestDryRun + : public AutomaticProfileResetterTestBase { + protected: + AutomaticProfileResetterTestDryRun() + : AutomaticProfileResetterTestBase(kStudyDryRunGroupName) {} +}; + +class AutomaticProfileResetterTestDisabled + : public AutomaticProfileResetterTestBase { + protected: + AutomaticProfileResetterTestDisabled() + : AutomaticProfileResetterTestBase(kStudyDisabledGroupName) {} +}; + +// Tests --------------------------------------------------------------------- + +TEST_F(AutomaticProfileResetterTestDisabled, NothingIsDoneWhenDisabled) { + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + // No calls are expected to the delegate. + + UnleashResetterAndWait(); + + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + ExpectAllMementoValuesEqualTo(std::string()); +} + +TEST_F(AutomaticProfileResetterTestDryRun, CriteriaNotSatisfied) { + SetTestingProgram(ConstructProgramToExerciseCriteria(false, true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x1fu, 0x00u)); + + UnleashResetterAndWait(); + + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + ExpectAllMementoValuesEqualTo(std::string()); +} + +TEST_F(AutomaticProfileResetterTestDryRun, OddCriteriaSatisfied) { + SetTestingProgram(ConstructProgramToExerciseCriteria(true, true, false)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x15u, 0x01u)); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_NOT_TRIGGERED)); + + UnleashResetterAndWait(); + + ExpectAllMementoValuesEqualTo(kTestMementoValue); + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTestDryRun, EvenCriteriaSatisfied) { + SetTestingProgram(ConstructProgramToExerciseCriteria(true, false, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x0au, 0x01u)); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_NOT_TRIGGERED)); + + UnleashResetterAndWait(); + + ExpectAllMementoValuesEqualTo(kTestMementoValue); + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +#if defined(GOOGLE_CHROME_BUILD) +TEST_F(AutomaticProfileResetterTestDryRun, ProgramSetThroughVariationParams) { + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + AllowInjectingTestDataThroughVariationParams(true); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x01u)); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_NOT_TRIGGERED)); + + UnleashResetterAndWait(); + + ExpectAllMementoValuesEqualTo(kTestMementoValue); + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} +#endif + +TEST_F(AutomaticProfileResetterTestDryRun, + ConditionsSatisfiedAndInvalidMementos) { + memento_in_prefs().StoreValue(kTestInvalidMementoValue); + memento_in_local_state().StoreValue(kTestInvalidMementoValue); + memento_in_file().StoreValue(kTestInvalidMementoValue); + + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x01u)); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_NOT_TRIGGERED)); + + UnleashResetterAndWait(); + + ExpectAllMementoValuesEqualTo(kTestMementoValue); + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTestDryRun, AlreadyHadPrefHostedMemento) { + memento_in_prefs().StoreValue(kTestMementoValue); + + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x03u)); + + UnleashResetterAndWait(); + + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + EXPECT_EQ(kTestMementoValue, memento_in_prefs().ReadValue()); + EXPECT_EQ(std::string(), memento_in_local_state().ReadValue()); + EXPECT_EQ(std::string(), memento_in_file().ReadValue()); +} + +TEST_F(AutomaticProfileResetterTestDryRun, AlreadyHadLocalStateHostedMemento) { + memento_in_local_state().StoreValue(kTestMementoValue); + + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x05u)); + + UnleashResetterAndWait(); + + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + EXPECT_EQ(std::string(), memento_in_prefs().ReadValue()); + EXPECT_EQ(kTestMementoValue, memento_in_local_state().ReadValue()); + EXPECT_EQ(std::string(), memento_in_file().ReadValue()); +} + +TEST_F(AutomaticProfileResetterTestDryRun, AlreadyHadFileHostedMemento) { + memento_in_file().StoreValue(kTestMementoValue); + + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x09u)); + + UnleashResetterAndWait(); + + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + EXPECT_EQ(std::string(), memento_in_prefs().ReadValue()); + EXPECT_EQ(std::string(), memento_in_local_state().ReadValue()); + EXPECT_EQ(kTestMementoValue, memento_in_file().ReadValue()); +} + +TEST_F(AutomaticProfileResetterTestDryRun, DoNothingWhenResourcesAreMissing) { + SetTestingProgram(std::string()); + SetTestingHashSeed(std::string()); + + // No calls are expected to the delegate. + + UnleashResetterAndWait(); + + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + ExpectAllMementoValuesEqualTo(std::string()); +} + +TEST_F(AutomaticProfileResetterTest, CriteriaNotSatisfied) { + SetTestingProgram(ConstructProgramToExerciseCriteria(false, true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x1fu, 0x00u)); + + UnleashResetterAndWait(); + + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + ExpectAllMementoValuesEqualTo(std::string()); +} + +TEST_F(AutomaticProfileResetterTest, OddCriteriaSatisfied) { + SetTestingProgram(ConstructProgramToExerciseCriteria(true, true, false)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + mock_delegate().ExpectCallToShowPrompt(); + EXPECT_CALL(resetter(), ReportStatistics(0x15u, 0x01u)); + + UnleashResetterAndWait(); + + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, EvenCriteriaSatisfied) { + SetTestingProgram(ConstructProgramToExerciseCriteria(true, false, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + mock_delegate().ExpectCallToShowPrompt(); + EXPECT_CALL(resetter(), ReportStatistics(0x0au, 0x01u)); + + UnleashResetterAndWait(); + + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +#if defined(GOOGLE_CHROME_BUILD) +TEST_F(AutomaticProfileResetterTest, ProgramSetThroughVariationParams) { + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + AllowInjectingTestDataThroughVariationParams(true); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + mock_delegate().ExpectCallToShowPrompt(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x01u)); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + + UnleashResetterAndWait(); + resetter().NotifyDidShowResetBubble(); + + ExpectAllMementoValuesEqualTo(kTestMementoValue); + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} +#endif + +TEST_F(AutomaticProfileResetterTest, ConditionsSatisfiedAndInvalidMementos) { + memento_in_prefs().StoreValue(kTestInvalidMementoValue); + memento_in_local_state().StoreValue(kTestInvalidMementoValue); + memento_in_file().StoreValue(kTestInvalidMementoValue); + + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + mock_delegate().ExpectCallToShowPrompt(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x01u)); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + + UnleashResetterAndWait(); + resetter().NotifyDidShowResetBubble(); + + ExpectAllMementoValuesEqualTo(kTestMementoValue); + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PrefHostedMementoPreventsPrompt) { + memento_in_prefs().StoreValue(kTestMementoValue); + + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x03u)); + + UnleashResetterAndWait(); + + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + EXPECT_EQ(kTestMementoValue, memento_in_prefs().ReadValue()); + EXPECT_EQ(std::string(), memento_in_local_state().ReadValue()); + EXPECT_EQ(std::string(), memento_in_file().ReadValue()); +} + +TEST_F(AutomaticProfileResetterTest, LocalStateHostedMementoPreventsPrompt) { + memento_in_local_state().StoreValue(kTestMementoValue); + + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x05u)); + + UnleashResetterAndWait(); + + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + EXPECT_EQ(std::string(), memento_in_prefs().ReadValue()); + EXPECT_EQ(kTestMementoValue, memento_in_local_state().ReadValue()); + EXPECT_EQ(std::string(), memento_in_file().ReadValue()); +} + +TEST_F(AutomaticProfileResetterTest, FileHostedMementoPreventsPrompt) { + memento_in_file().StoreValue(kTestMementoValue); + + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x09u)); + + UnleashResetterAndWait(); + + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + EXPECT_EQ(std::string(), memento_in_prefs().ReadValue()); + EXPECT_EQ(std::string(), memento_in_local_state().ReadValue()); + EXPECT_EQ(kTestMementoValue, memento_in_file().ReadValue()); +} + +TEST_F(AutomaticProfileResetterTest, DoNothingWhenResourcesAreMissing) { + SetTestingProgram(std::string()); + SetTestingHashSeed(std::string()); + + // No calls are expected to the delegate. + + UnleashResetterAndWait(); + + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + ExpectAllMementoValuesEqualTo(std::string()); +} + +TEST_F(AutomaticProfileResetterTest, PromptSuppressed) { + OrchestrateThroughEvaluationFlow(); + + VerifyExpectationsThenShutdownResetter(); + + ExpectAllMementoValuesEqualTo(std::string()); +} + +TEST_F(AutomaticProfileResetterTest, PromptNotSupported) { + SetTestingProgram(ConstructProgram(true, true)); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + EXPECT_CALL(mock_delegate(), TriggerPrompt()) + .WillOnce(testing::Return(false)); + EXPECT_CALL(resetter(), ReportStatistics(0x03u, 0x01u)); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_NOT_TRIGGERED)); + + UnleashResetterAndWait(); + + ExpectAllMementoValuesEqualTo(kTestMementoValue); + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PromptIgnored) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + resetter().NotifyDidShowResetBubble(); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PromptActionReset) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + resetter().NotifyDidShowResetBubble(); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + testing::Mock::VerifyAndClearExpectations(&resetter()); + + mock_delegate().ExpectCallToTriggerReset(false); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_ACTION_RESET)); + resetter().TriggerProfileReset(false /*send_feedback*/); + testing::Mock::VerifyAndClearExpectations(&resetter()); + testing::Mock::VerifyAndClearExpectations(&mock_delegate()); + + EXPECT_CALL(mock_delegate(), DismissPrompt()); + mock_delegate().EmulateProfileResetCompleted(); + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PromptActionResetWithFeedback) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + resetter().NotifyDidShowResetBubble(); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + testing::Mock::VerifyAndClearExpectations(&resetter()); + + mock_delegate().ExpectCallToTriggerReset(true); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_ACTION_RESET)); + resetter().TriggerProfileReset(true /*send_feedback*/); + testing::Mock::VerifyAndClearExpectations(&resetter()); + testing::Mock::VerifyAndClearExpectations(&mock_delegate()); + + EXPECT_CALL(mock_delegate(), DismissPrompt()); + mock_delegate().EmulateProfileResetCompleted(); + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PromptActionNoReset) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + resetter().NotifyDidShowResetBubble(); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + testing::Mock::VerifyAndClearExpectations(&resetter()); + + EXPECT_CALL(mock_delegate(), DismissPrompt()); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_ACTION_NO_RESET)); + resetter().SkipProfileReset(); + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PromptFollowedByWebUIReset) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + resetter().NotifyDidShowResetBubble(); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + testing::Mock::VerifyAndClearExpectations(&resetter()); + + EXPECT_CALL(mock_delegate(), DismissPrompt()); + resetter().NotifyDidOpenWebUIResetDialog(); + testing::Mock::VerifyAndClearExpectations(&mock_delegate()); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_FOLLOWED_BY_WEBUI_RESET)); + resetter().NotifyDidCloseWebUIResetDialog(true); + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PromptFollowedByWebUINoReset) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + resetter().NotifyDidShowResetBubble(); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + testing::Mock::VerifyAndClearExpectations(&resetter()); + + EXPECT_CALL(mock_delegate(), DismissPrompt()); + resetter().NotifyDidOpenWebUIResetDialog(); + testing::Mock::VerifyAndClearExpectations(&mock_delegate()); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_FOLLOWED_BY_WEBUI_NO_RESET)); + resetter().NotifyDidCloseWebUIResetDialog(false); + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PromptFollowedByIncidentalWebUIReset) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + resetter().NotifyDidShowResetBubble(); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + testing::Mock::VerifyAndClearExpectations(&resetter()); + + // Missing NotifyDidOpenWebUIResetDialog(). + // This can arise if a settings page was already opened at the time the prompt + // was triggered, and this already opened dialog was used to initiate a reset + // after having dismissed the prompt. + + EXPECT_CALL(mock_delegate(), DismissPrompt()); + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_FOLLOWED_BY_WEBUI_RESET)); + resetter().NotifyDidCloseWebUIResetDialog(true); + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PromptSuppressedButHadWebUIReset) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(mock_delegate(), DismissPrompt()); + resetter().NotifyDidOpenWebUIResetDialog(); + testing::Mock::VerifyAndClearExpectations(&mock_delegate()); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_NOT_SHOWN_BUBBLE_BUT_HAD_WEBUI_RESET)); + resetter().NotifyDidCloseWebUIResetDialog(true); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, PromptSuppressedButHadWebUINoReset) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(mock_delegate(), DismissPrompt()); + resetter().NotifyDidOpenWebUIResetDialog(); + testing::Mock::VerifyAndClearExpectations(&mock_delegate()); + + EXPECT_CALL(resetter(), ReportPromptResult(AutomaticProfileResetter:: + PROMPT_NOT_SHOWN_BUBBLE_BUT_HAD_WEBUI_NO_RESET)); + resetter().NotifyDidCloseWebUIResetDialog(false); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + EXPECT_TRUE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, BannerDismissed) { + OrchestrateThroughEvaluationFlow(); + + EXPECT_CALL(resetter(), ReportPromptResult( + AutomaticProfileResetter::PROMPT_SHOWN_BUBBLE)); + resetter().NotifyDidShowResetBubble(); + ExpectAllMementoValuesEqualTo(kTestMementoValue); + testing::Mock::VerifyAndClearExpectations(&resetter()); + + resetter().NotifyDidCloseWebUIResetBanner(); + + EXPECT_TRUE(resetter().IsResetPromptFlowActive()); + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + + // Note: we use strict mocks, so this also checks the bubble is not closed. + VerifyExpectationsThenShutdownResetter(); +} + +TEST_F(AutomaticProfileResetterTest, BannerDismissedWhilePromptSuppressed) { + OrchestrateThroughEvaluationFlow(); + + resetter().NotifyDidCloseWebUIResetBanner(); + + EXPECT_TRUE(resetter().IsResetPromptFlowActive()); + EXPECT_FALSE(resetter().ShouldShowResetBanner()); + VerifyExpectationsThenShutdownResetter(); + + ExpectAllMementoValuesEqualTo(std::string()); +} + +// Please see comments above ConstructProgramToCheckPreferences() to understand +// how the following tests work. + +TEST_F(AutomaticProfileResetterTest, InputUserPreferencesCorrect) { + SetTestingProgram(ConstructProgramToCheckPreferences()); + SetTestingHashSeed(kTestHashSeed); + + PrefService* prefs = profile()->GetPrefs(); + prefs->SetString(kTestPreferencePath, kTestPreferenceValue); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + uint32 expected_mask = HAS_EXPECTED_USER_PREFERENCE | + USER_PREFERENCE_IS_USER_CONTROLLED; + EXPECT_CALL(resetter(), ReportStatistics(0x00u, expected_mask)); + + UnleashResetterAndWait(); +} + +TEST_F(AutomaticProfileResetterTest, InputLocalStateCorrect) { + SetTestingProgram(ConstructProgramToCheckPreferences()); + SetTestingHashSeed(kTestHashSeed); + + PrefService* prefs = local_state(); + prefs->SetString(kTestPreferencePath, kTestPreferenceValue); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + uint32 expected_mask = HAS_EXPECTED_LOCAL_STATE_PREFERENCE | + LOCAL_STATE_IS_USER_CONTROLLED; + EXPECT_CALL(resetter(), ReportStatistics(0x00u, expected_mask)); + + UnleashResetterAndWait(); +} + +TEST_F(AutomaticProfileResetterTest, InputManagedUserPreferencesCorrect) { + SetTestingProgram(ConstructProgramToCheckPreferences()); + SetTestingHashSeed(kTestHashSeed); + + TestingPrefServiceSyncable* prefs = profile()->GetTestingPrefService(); + prefs->SetManagedPref(kTestPreferencePath, + new base::StringValue(kTestPreferenceValue)); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + uint32 expected_mask = HAS_EXPECTED_USER_PREFERENCE; + EXPECT_CALL(resetter(), ReportStatistics(0x00u, expected_mask)); + + UnleashResetterAndWait(); +} + +TEST_F(AutomaticProfileResetterTest, InputManagedLocalStateCorrect) { + SetTestingProgram(ConstructProgramToCheckPreferences()); + SetTestingHashSeed(kTestHashSeed); + + TestingPrefServiceSimple* prefs = local_state(); + prefs->SetManagedPref(kTestPreferencePath, + new base::StringValue(kTestPreferenceValue)); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + uint32 expected_mask = HAS_EXPECTED_LOCAL_STATE_PREFERENCE; + EXPECT_CALL(resetter(), ReportStatistics(0x00u, expected_mask)); + + UnleashResetterAndWait(); +} + +// Please see comments above ConstructProgramToCheckSearchEngines() to +// understand how the following tests work. + +TEST_F(AutomaticProfileResetterTest, InputDefaultSearchProviderCorrect) { + SetTestingProgram(ConstructProgramToCheckSearchEngines()); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().emulated_default_search_provider_details().SetString( + kSearchURLAttributeKey, kTestSearchURL); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + uint32 expected_mask = HAS_EXPECTED_DEFAULT_SEARCH_PROVIDER | + DEFAULT_SEARCH_PROVIDER_IS_USER_CONTROLLED; + EXPECT_CALL(resetter(), ReportStatistics(0x00u, expected_mask)); + + UnleashResetterAndWait(); +} + +TEST_F(AutomaticProfileResetterTest, InputSearchProviderManagedCorrect) { + SetTestingProgram(ConstructProgramToCheckSearchEngines()); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().emulated_default_search_provider_details().SetString( + kSearchURLAttributeKey, kTestSearchURL); + mock_delegate().set_emulated_is_default_search_provider_managed(true); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + uint32 expected_mask = HAS_EXPECTED_DEFAULT_SEARCH_PROVIDER; + EXPECT_CALL(resetter(), ReportStatistics(0x00u, expected_mask)); + + UnleashResetterAndWait(); +} + +TEST_F(AutomaticProfileResetterTest, InputSearchProvidersCorrect) { + SetTestingProgram(ConstructProgramToCheckSearchEngines()); + SetTestingHashSeed(kTestHashSeed); + + base::DictionaryValue* search_provider_1 = new base::DictionaryValue; + base::DictionaryValue* search_provider_2 = new base::DictionaryValue; + search_provider_1->SetString(kSearchURLAttributeKey, kTestSearchURL); + search_provider_2->SetString(kSearchURLAttributeKey, kTestSearchURL2); + mock_delegate().emulated_search_providers_details().Append(search_provider_1); + mock_delegate().emulated_search_providers_details().Append(search_provider_2); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + uint32 expected_mask = DEFAULT_SEARCH_PROVIDER_IS_USER_CONTROLLED | + HAS_EXPECTED_PREPOPULATED_SEARCH_PROVIDER_1 | + HAS_EXPECTED_PREPOPULATED_SEARCH_PROVIDER_2; + EXPECT_CALL(resetter(), ReportStatistics(0x00u, expected_mask)); + + UnleashResetterAndWait(); +} + +// Please see comments above ConstructProgramToCheckLoadedModuleDigests() to +// understand how the following tests work. + +TEST_F(AutomaticProfileResetterTest, InputModuleDigestsCorrect) { + SetTestingProgram(ConstructProgramToCheckLoadedModuleDigests()); + SetTestingHashSeed(kTestHashSeed); + + mock_delegate().emulated_loaded_module_digests().AppendString( + kTestModuleDigest); + mock_delegate().emulated_loaded_module_digests().AppendString( + kTestModuleDigest2); + + mock_delegate().ExpectCallsToDependenciesSetUpMethods(); + mock_delegate().ExpectCallsToGetterMethods(); + uint32 expected_mask = + HAS_EXPECTED_MODULE_DIGEST_1 | HAS_EXPECTED_MODULE_DIGEST_2; + EXPECT_CALL(resetter(), ReportStatistics(0x00u, expected_mask)); + + UnleashResetterAndWait(); +} + +} // namespace diff --git a/chrome/browser/profile_resetter/jtl_foundation.cc b/chrome/browser/profile_resetter/jtl_foundation.cc new file mode 100644 index 0000000..0739040 --- /dev/null +++ b/chrome/browser/profile_resetter/jtl_foundation.cc @@ -0,0 +1,48 @@ +// 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/profile_resetter/jtl_foundation.h" + +#include "base/logging.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" + +namespace jtl_foundation { + +Hasher::Hasher(const std::string& seed) : hmac_(crypto::HMAC::SHA256) { + if (!hmac_.Init(seed)) + NOTREACHED(); +} + +Hasher::~Hasher() {} + +std::string Hasher::GetHash(const std::string& input) const { + if (cached_hashes_.find(input) == cached_hashes_.end()) { + // Calculate value. + unsigned char digest[kHashSizeInBytes]; + if (!hmac_.Sign(input, digest, arraysize(digest))) { + NOTREACHED(); + return std::string(); + } + // Instead of using the full SHA256, we only use the hex encoding of the + // first 16 bytes. + cached_hashes_[input] = base::HexEncode(digest, kHashSizeInBytes / 2); + DCHECK_EQ(kHashSizeInBytes, cached_hashes_[input].size()); + } + return cached_hashes_[input]; +} + +// static +bool Hasher::IsHash(const std::string& maybe_hash) { + if (maybe_hash.size() != kHashSizeInBytes) + return false; + for (std::string::const_iterator it = maybe_hash.begin(); + it != maybe_hash.end(); ++it) { + if (!IsHexDigit(*it)) + return false; + } + return true; +} + +} // namespace jtl_foundation diff --git a/chrome/browser/profile_resetter/jtl_foundation.h b/chrome/browser/profile_resetter/jtl_foundation.h new file mode 100644 index 0000000..cc4a5f4 --- /dev/null +++ b/chrome/browser/profile_resetter/jtl_foundation.h @@ -0,0 +1,193 @@ +// 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. + +#ifndef CHROME_BROWSER_PROFILE_RESETTER_JTL_FOUNDATION_H_ +#define CHROME_BROWSER_PROFILE_RESETTER_JTL_FOUNDATION_H_ + +#include <map> +#include <string> + +#include "base/basictypes.h" +#include "crypto/hmac.h" + +namespace jtl_foundation { + +// A JTL (JSON Traversal Language) program is composed of one or more +// sentences. Each sentence consists of a sequence of operations. The input of +// the program is a hierarchical JSON data structure. +// +// The execution of each sentence starts at the root of an input dictionary. The +// operations include navigation in the JSON data structure, as well as +// comparing the current (leaf) node to fixed values. The program also has a +// separate dictionary as working memory, into which it can memorize data, then +// later recall it for comparisons. +// +// Example program: +// NAVIGATE_ANY +// NAVIGATE(hash("bar")) +// COMPARE_NODE_BOOL(1) +// STORE_BOOL(hash("found_foo"), 1) +// STOP_EXECUTING_SENTENCE +// +// Example input: +// { +// 'key1': 1, +// 'key2': {'foo': 0, 'bar': false, 'baz': 2} +// 'key3': {'foo': 0, 'bar': true, 'baz': 2} +// 'key4': {'foo': 0, 'bar': true, 'baz': 2} +// } +// +// This program navigates from the root of the dictionary to all children +// ("key1", "key2", "key3", "key4") and executes the remaining program on each +// of the children. The navigation happens in depth-first pre-order. On each of +// the children it tries to navigate into the child "bar", which fails for +// "key1", so execution stops for this sub-branch. On key2 the program navigates +// to "bar" and moves the execution context to this node which has the value +// "false". Therefore, the following COMPARE_NODE_BOOL is not fulfilled and the +// execution does not continue on this branch, so we back track and proceed with +// "key3" and its "bar" child. For this node, COMPARE_NODE_BOOL is fulfilled and +// the execution continues to store "found_foo = true" into the working memory +// of the interpreter. Next the interpreter executes STOP_EXECUTING_SENTENCE +// which prevents the traversal from descending into the "key4" branch from the +// NAVIGATE_ANY operation and can therefore speedup the processing. +// +// All node names, and node values of type string, integer and double are hashed +// before being compared to hash values listed in |program|. + +// JTL byte code consists of uint8 opcodes followed by parameters. Parameters +// are either boolean (uint8 with value \x00 or \x01), uint32 (in little-endian +// notation), or hash string of 32 bytes. +// The following opcodes are defined: +enum OpCodes { + // Continues execution with the next operation on the element of a + // dictionary that matches the passed key parameter. If no such element + // exists, the command execution returns from the current instruction. + // Parameters: + // - a 32 byte hash of the target dictionary key. + NAVIGATE = 0x00, + // Continues execution with the next operation on each element of a + // dictionary or list. If no such element exists or the current element is + // neither a dictionary or list, the command execution returns from the + // current instruction. + NAVIGATE_ANY = 0x01, + // Continues execution with the next operation on the parent node of the + // current node. If the current node is the root of the input dictionary, the + // program execution fails with a runtime error. + NAVIGATE_BACK = 0x02, + // Stores a boolean value in the working memory. + // Parameters: + // - a 32 byte hash of the parameter name, + // - the value to store (\x00 or \x01) + STORE_BOOL = 0x10, + // Checks whether a boolean stored in working memory matches the expected + // value and continues execution with the next operation in case of a match. + // Parameters: + // - a 32 byte hash of the parameter name, + // - the expected value (\x00 or \x01), + // - the default value in case the working memory contains no corresponding + // entry (\x00 or\x01). + COMPARE_STORED_BOOL = 0x11, + // Same as STORE_BOOL but takes a hash instead of a boolean value as + // parameter. + STORE_HASH = 0x12, + // Same as COMPARE_STORED_BOOL but takes a hash instead of two boolean values + // as parameters. + COMPARE_STORED_HASH = 0x13, + // Stores the current node into the working memory. If the current node is not + // a boolean value, the program execution returns from the current + // instruction. + // Parameters: + // - a 32 byte hash of the parameter name. + STORE_NODE_BOOL = 0x14, + // Stores the hashed value of the current node into the working memory. If + // the current node is not a string, integer or double, the program execution + // returns from the current instruction. + // Parameters: + // - a 32 byte hash of the parameter name. + STORE_NODE_HASH = 0x15, + // Parses the value of the current node as a URL, extracts the subcomponent of + // the domain name that is immediately below the registrar-controlled portion, + // and stores the hash of this subcomponent into working memory. In case the + // domain name ends in a suffix not on the Public Suffix List (i.e. is neither + // an ICANN, nor a private domain suffix), the last subcomponent is assumed to + // be the registry, and the second-to-last subcomponent is extracted. + // If the current node is not a string that represents a URL, or if this URL + // does not have a domain component as described above, the program execution + // returns from the current instruction. + // For example, "foo.com", "www.foo.co.uk", "foo.appspot.com", and "foo.bar" + // will all yield (the hash of) "foo" as a result. + // See the unit test for more details. + // Parameters: + // - a 32 byte hash of the parameter name. + STORE_NODE_REGISTERABLE_DOMAIN_HASH = 0x16, + // Compares the current node against a boolean value and continues execution + // with the next operation in case of a match. If the current node does not + // match or is not a boolean value, the program execution returns from the + // current instruction. + // Parameters: + // - a boolean value (\x00 or \x01). + COMPARE_NODE_BOOL = 0x20, + // Compares the hashed value of the current node against the given hash, and + // continues execution with the next operation in case of a match. If the + // current node is not a string, integer or double, or if it is either, but + // its hash does not match, the program execution returns from the current + // instruction. + // Parameters: + // - a 32 byte hash of the expected value. + COMPARE_NODE_HASH = 0x21, + // The negation of the above. + COMPARE_NODE_HASH_NOT = 0x22, + // Compares the current node against a boolean value stored in working memory, + // and continues with the next operation in case of a match. If the current + // node is not a boolean value, the working memory contains no corresponding + // boolean value, or if they do not match, the program execution returns from + // the current instruction. + // Parameters: + // - a 32 byte hash of the parameter name. + COMPARE_NODE_TO_STORED_BOOL = 0x23, + // Compares the hashed value of the current node against a hash value stored + // in working memory, and continues with the next operation in case of a + // match. If the current node is not a string, integer or double, or if the + // working memory contains no corresponding hash string, or if the hashes do + // not match, the program execution returns from the current instruction. + // Parameters: + // - a 32 byte hash of the parameter name. + COMPARE_NODE_TO_STORED_HASH = 0x24, + // Checks whether the current node is a string value that contains the given + // pattern as a substring, and continues execution with the next operation in + // case it does. If the current node is not a string, or if does not match the + // pattern, the program execution returns from the current instruction. + // Parameters: + // - a 32 byte hash of the pattern, + // - a 4 byte unsigned integer: the length (>0) of the pattern in bytes, + // - a 4 byte unsigned integer: the sum of all bytes in the pattern, serving + // as a heuristic to reduce the number of actual SHA-256 calculations. + COMPARE_NODE_SUBSTRING = 0x25, + // Stop execution in this specific sentence. + STOP_EXECUTING_SENTENCE = 0x30, + // Separator between sentences, starts a new sentence. + END_OF_SENTENCE = 0x31 +}; + +const size_t kHashSizeInBytes = 32u; + +// A class that provides SHA256 hash values for strings using a fixed hash seed. +class Hasher { + public: + explicit Hasher(const std::string& seed); + ~Hasher(); + + std::string GetHash(const std::string& input) const; + + static bool IsHash(const std::string& maybe_hash); + + private: + crypto::HMAC hmac_; + mutable std::map<std::string, std::string> cached_hashes_; + DISALLOW_COPY_AND_ASSIGN(Hasher); +}; + +} // namespace jtl_foundation + +#endif // CHROME_BROWSER_PROFILE_RESETTER_JTL_FOUNDATION_H_ diff --git a/chrome/browser/profile_resetter/jtl_instructions.h b/chrome/browser/profile_resetter/jtl_instructions.h new file mode 100644 index 0000000..76b57a6 --- /dev/null +++ b/chrome/browser/profile_resetter/jtl_instructions.h @@ -0,0 +1,38 @@ +// 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. + +#ifndef CHROME_BROWSER_PROFILE_RESETTER_JTL_INSTRUCTIONS_H_ +#define CHROME_BROWSER_PROFILE_RESETTER_JTL_INSTRUCTIONS_H_ + +#include <string> + +// These are instructions to generate binary code for JTL programs. + +#define VALUE_FALSE std::string(1, '\x00') +#define VALUE_TRUE std::string(1, '\x01') + +#define OP_NAVIGATE(key) std::string(1, '\x00') + key +#define OP_NAVIGATE_ANY std::string(1, '\x01') +#define OP_NAVIGATE_BACK std::string(1, '\x02') +#define OP_STORE_BOOL(name, value) std::string(1, '\x10') + name + value +#define OP_COMPARE_STORED_BOOL(name, value, default_value) \ + std::string(1, '\x11') + name + value + default_value +#define OP_STORE_HASH(name, value) std::string(1, '\x12') + name + value +#define OP_COMPARE_STORED_HASH(name, value, default_value) \ + std::string(1, '\x13') + name + value + default_value +#define OP_STORE_NODE_BOOL(name) std::string(1, '\x14') + name +#define OP_STORE_NODE_HASH(name) std::string(1, '\x15') + name +#define OP_STORE_NODE_REGISTERABLE_DOMAIN_HASH(name) \ + std::string(1, '\x16') + name +#define OP_COMPARE_NODE_BOOL(value) std::string(1, '\x20') + value +#define OP_COMPARE_NODE_HASH(value) std::string(1, '\x21') + value +#define OP_COMPARE_NODE_HASH_NOT(value) std::string(1, '\x22') + value +#define OP_COMPARE_NODE_TO_STORED_BOOL(name) std::string(1, '\x23') + name +#define OP_COMPARE_NODE_TO_STORED_HASH(name) std::string(1, '\x24') + name +#define OP_COMPARE_NODE_SUBSTRING(pattern, pattern_length, pattern_sum) \ + std::string(1, '\x25') + pattern + pattern_length + pattern_sum +#define OP_STOP_EXECUTING_SENTENCE std::string(1, '\x30') +#define OP_END_OF_SENTENCE std::string(1, '\x31') + +#endif // CHROME_BROWSER_PROFILE_RESETTER_JTL_INSTRUCTIONS_H_ diff --git a/chrome/browser/profile_resetter/jtl_interpreter.cc b/chrome/browser/profile_resetter/jtl_interpreter.cc new file mode 100644 index 0000000..e258078 --- /dev/null +++ b/chrome/browser/profile_resetter/jtl_interpreter.cc @@ -0,0 +1,748 @@ +// 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/profile_resetter/jtl_interpreter.h" + +#include <numeric> + +#include "base/memory/scoped_vector.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "chrome/browser/profile_resetter/jtl_foundation.h" +#include "crypto/hmac.h" +#include "crypto/sha2.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.h" +#include "url/gurl.h" + +namespace { + +class ExecutionContext; + +// An operation in an interpreted program. +class Operation { + public: + virtual ~Operation() {} + // Executes the operation on the specified context and instructs the context + // to continue execution with the next instruction if appropriate. + // Returns true if we should continue with any potential backtracking that + // needs to be done. + virtual bool Execute(ExecutionContext* context) = 0; +}; + +// An execution context of operations. +class ExecutionContext { + public: + // |input| is the root of a dictionary that stores the information the + // sentence is evaluated on. + ExecutionContext(const jtl_foundation::Hasher* hasher, + const std::vector<Operation*>& sentence, + const base::DictionaryValue* input, + base::DictionaryValue* working_memory) + : hasher_(hasher), + sentence_(sentence), + next_instruction_index_(0u), + working_memory_(working_memory), + error_(false) { + stack_.push_back(input); + } + ~ExecutionContext() {} + + // Returns true in case of success. + bool ContinueExecution() { + if (error_ || stack_.empty()) { + error_ = true; + return false; + } + if (next_instruction_index_ >= sentence_.size()) + return true; + + Operation* op = sentence_[next_instruction_index_]; + next_instruction_index_++; + bool continue_traversal = op->Execute(this); + next_instruction_index_--; + return continue_traversal; + } + + std::string GetHash(const std::string& input) { + return hasher_->GetHash(input); + } + + // Calculates the |hash| of a string, integer or double |value|, and returns + // true. Returns false otherwise. + bool GetValueHash(const base::Value& value, std::string* hash) { + DCHECK(hash); + std::string value_as_string; + int tmp_int = 0; + double tmp_double = 0.0; + if (value.GetAsInteger(&tmp_int)) + value_as_string = base::IntToString(tmp_int); + else if (value.GetAsDouble(&tmp_double)) + value_as_string = base::DoubleToString(tmp_double); + else if (!value.GetAsString(&value_as_string)) + return false; + *hash = GetHash(value_as_string); + return true; + } + + const base::Value* current_node() const { return stack_.back(); } + std::vector<const base::Value*>* stack() { return &stack_; } + base::DictionaryValue* working_memory() { return working_memory_; } + bool error() const { return error_; } + + private: + // A hasher used to hash node names in a dictionary. + const jtl_foundation::Hasher* hasher_; + // The sentence to be executed. + const std::vector<Operation*> sentence_; + // Position in |sentence_|. + size_t next_instruction_index_; + // A stack of Values, indicating a navigation path from the root node of + // |input| (see constructor) to the current node on which the + // sentence_[next_instruction_index_] is evaluated. + std::vector<const base::Value*> stack_; + // Memory into which values can be stored by the program. + base::DictionaryValue* working_memory_; + // Whether a runtime error occurred. + bool error_; + DISALLOW_COPY_AND_ASSIGN(ExecutionContext); +}; + +class NavigateOperation : public Operation { + public: + explicit NavigateOperation(const std::string& hashed_key) + : hashed_key_(hashed_key) {} + virtual ~NavigateOperation() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + const base::DictionaryValue* dict = NULL; + if (!context->current_node()->GetAsDictionary(&dict)) { + // Just ignore this node gracefully as this navigation is a dead end. + // If this NavigateOperation occurred after a NavigateAny operation, those + // may still be fulfillable, so we allow continuing the execution of the + // sentence on other nodes. + return true; + } + for (base::DictionaryValue::Iterator i(*dict); !i.IsAtEnd(); i.Advance()) { + if (context->GetHash(i.key()) != hashed_key_) + continue; + context->stack()->push_back(&i.value()); + bool continue_traversal = context->ContinueExecution(); + context->stack()->pop_back(); + if (!continue_traversal) + return false; + } + return true; + } + + private: + std::string hashed_key_; + DISALLOW_COPY_AND_ASSIGN(NavigateOperation); +}; + +class NavigateAnyOperation : public Operation { + public: + NavigateAnyOperation() {} + virtual ~NavigateAnyOperation() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + const base::DictionaryValue* dict = NULL; + const base::ListValue* list = NULL; + if (context->current_node()->GetAsDictionary(&dict)) { + for (base::DictionaryValue::Iterator i(*dict); + !i.IsAtEnd(); i.Advance()) { + context->stack()->push_back(&i.value()); + bool continue_traversal = context->ContinueExecution(); + context->stack()->pop_back(); + if (!continue_traversal) + return false; + } + } else if (context->current_node()->GetAsList(&list)) { + for (base::ListValue::const_iterator i = list->begin(); + i != list->end(); ++i) { + context->stack()->push_back(*i); + bool continue_traversal = context->ContinueExecution(); + context->stack()->pop_back(); + if (!continue_traversal) + return false; + } + } else { + // Do nothing, just ignore this node. + } + return true; + } + + private: + DISALLOW_COPY_AND_ASSIGN(NavigateAnyOperation); +}; + +class NavigateBackOperation : public Operation { + public: + NavigateBackOperation() {} + virtual ~NavigateBackOperation() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + const base::Value* current_node = context->current_node(); + context->stack()->pop_back(); + bool continue_traversal = context->ContinueExecution(); + context->stack()->push_back(current_node); + return continue_traversal; + } + + private: + DISALLOW_COPY_AND_ASSIGN(NavigateBackOperation); +}; + +class StoreValue : public Operation { + public: + StoreValue(const std::string& hashed_name, scoped_ptr<base::Value> value) + : hashed_name_(hashed_name), + value_(value.Pass()) { + DCHECK(base::IsStringUTF8(hashed_name)); + DCHECK(value_); + } + virtual ~StoreValue() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + context->working_memory()->Set(hashed_name_, value_->DeepCopy()); + return context->ContinueExecution(); + } + + private: + std::string hashed_name_; + scoped_ptr<base::Value> value_; + DISALLOW_COPY_AND_ASSIGN(StoreValue); +}; + +class CompareStoredValue : public Operation { + public: + CompareStoredValue(const std::string& hashed_name, + scoped_ptr<base::Value> value, + scoped_ptr<base::Value> default_value) + : hashed_name_(hashed_name), + value_(value.Pass()), + default_value_(default_value.Pass()) { + DCHECK(base::IsStringUTF8(hashed_name)); + DCHECK(value_); + DCHECK(default_value_); + } + virtual ~CompareStoredValue() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + const base::Value* actual_value = NULL; + if (!context->working_memory()->Get(hashed_name_, &actual_value)) + actual_value = default_value_.get(); + if (!value_->Equals(actual_value)) + return true; + return context->ContinueExecution(); + } + + private: + std::string hashed_name_; + scoped_ptr<base::Value> value_; + scoped_ptr<base::Value> default_value_; + DISALLOW_COPY_AND_ASSIGN(CompareStoredValue); +}; + +template<bool ExpectedTypeIsBooleanNotHashable> +class StoreNodeValue : public Operation { + public: + explicit StoreNodeValue(const std::string& hashed_name) + : hashed_name_(hashed_name) { + DCHECK(base::IsStringUTF8(hashed_name)); + } + virtual ~StoreNodeValue() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + scoped_ptr<base::Value> value; + if (ExpectedTypeIsBooleanNotHashable) { + if (!context->current_node()->IsType(base::Value::TYPE_BOOLEAN)) + return true; + value.reset(context->current_node()->DeepCopy()); + } else { + std::string hash; + if (!context->GetValueHash(*context->current_node(), &hash)) + return true; + value.reset(new base::StringValue(hash)); + } + context->working_memory()->Set(hashed_name_, value.release()); + return context->ContinueExecution(); + } + + private: + std::string hashed_name_; + DISALLOW_COPY_AND_ASSIGN(StoreNodeValue); +}; + +// Stores the hash of the registerable domain name -- as in, the portion of the +// domain that is registerable, as opposed to controlled by a registrar; without +// subdomains -- of the URL represented by the current node into working memory. +class StoreNodeRegisterableDomain : public Operation { + public: + explicit StoreNodeRegisterableDomain(const std::string& hashed_name) + : hashed_name_(hashed_name) { + DCHECK(base::IsStringUTF8(hashed_name)); + } + virtual ~StoreNodeRegisterableDomain() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + std::string possibly_invalid_url; + std::string domain; + if (!context->current_node()->GetAsString(&possibly_invalid_url) || + !GetRegisterableDomain(possibly_invalid_url, &domain)) + return true; + context->working_memory()->Set( + hashed_name_, new base::StringValue(context->GetHash(domain))); + return context->ContinueExecution(); + } + + private: + // If |possibly_invalid_url| is a valid URL having a registerable domain name + // part, outputs that in |registerable_domain| and returns true. Otherwise, + // returns false. + static bool GetRegisterableDomain(const std::string& possibly_invalid_url, + std::string* registerable_domain) { + namespace domains = net::registry_controlled_domains; + DCHECK(registerable_domain); + GURL url(possibly_invalid_url); + if (!url.is_valid()) + return false; + std::string registry_plus_one = domains::GetDomainAndRegistry( + url.host(), domains::INCLUDE_PRIVATE_REGISTRIES); + size_t registry_length = domains::GetRegistryLength( + url.host(), + domains::INCLUDE_UNKNOWN_REGISTRIES, + domains::INCLUDE_PRIVATE_REGISTRIES); + // Fail unless (1.) the URL has a host part; and (2.) that host part is a + // well-formed domain name consisting of at least one subcomponent; followed + // by either a recognized registry identifier, or exactly one subcomponent, + // which is then assumed to be the unknown registry identifier. + if (registry_length == std::string::npos || registry_length == 0) + return false; + DCHECK_LT(registry_length, registry_plus_one.size()); + // Subtract one to cut off the dot separating the SLD and the registry. + registerable_domain->assign( + registry_plus_one, 0, registry_plus_one.size() - registry_length - 1); + return true; + } + + std::string hashed_name_; + DISALLOW_COPY_AND_ASSIGN(StoreNodeRegisterableDomain); +}; + +class CompareNodeBool : public Operation { + public: + explicit CompareNodeBool(bool value) : value_(value) {} + virtual ~CompareNodeBool() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + bool actual_value = false; + if (!context->current_node()->GetAsBoolean(&actual_value)) + return true; + if (actual_value != value_) + return true; + return context->ContinueExecution(); + } + + private: + bool value_; + DISALLOW_COPY_AND_ASSIGN(CompareNodeBool); +}; + +class CompareNodeHash : public Operation { + public: + explicit CompareNodeHash(const std::string& hashed_value) + : hashed_value_(hashed_value) {} + virtual ~CompareNodeHash() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + std::string actual_hash; + if (!context->GetValueHash(*context->current_node(), &actual_hash) || + actual_hash != hashed_value_) + return true; + return context->ContinueExecution(); + } + + private: + std::string hashed_value_; + DISALLOW_COPY_AND_ASSIGN(CompareNodeHash); +}; + +class CompareNodeHashNot : public Operation { + public: + explicit CompareNodeHashNot(const std::string& hashed_value) + : hashed_value_(hashed_value) {} + virtual ~CompareNodeHashNot() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + std::string actual_hash; + if (context->GetValueHash(*context->current_node(), &actual_hash) && + actual_hash == hashed_value_) + return true; + return context->ContinueExecution(); + } + + private: + std::string hashed_value_; + DISALLOW_COPY_AND_ASSIGN(CompareNodeHashNot); +}; + +template<bool ExpectedTypeIsBooleanNotHashable> +class CompareNodeToStored : public Operation { + public: + explicit CompareNodeToStored(const std::string& hashed_name) + : hashed_name_(hashed_name) {} + virtual ~CompareNodeToStored() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + const base::Value* stored_value = NULL; + if (!context->working_memory()->Get(hashed_name_, &stored_value)) + return true; + if (ExpectedTypeIsBooleanNotHashable) { + if (!context->current_node()->IsType(base::Value::TYPE_BOOLEAN) || + !context->current_node()->Equals(stored_value)) + return true; + } else { + std::string actual_hash; + std::string stored_hash; + if (!context->GetValueHash(*context->current_node(), &actual_hash) || + !stored_value->GetAsString(&stored_hash) || + actual_hash != stored_hash) + return true; + } + return context->ContinueExecution(); + } + + private: + std::string hashed_name_; + DISALLOW_COPY_AND_ASSIGN(CompareNodeToStored); +}; + +class CompareNodeSubstring : public Operation { + public: + explicit CompareNodeSubstring(const std::string& hashed_pattern, + size_t pattern_length, + uint32 pattern_sum) + : hashed_pattern_(hashed_pattern), + pattern_length_(pattern_length), + pattern_sum_(pattern_sum) { + DCHECK(pattern_length_); + } + virtual ~CompareNodeSubstring() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + std::string value_as_string; + if (!context->current_node()->GetAsString(&value_as_string) || + !pattern_length_ || value_as_string.size() < pattern_length_) + return true; + // Go over the string with a sliding window. Meanwhile, maintain the sum in + // an incremental fashion, and only calculate the SHA-256 hash when the sum + // checks out so as to improve performance. + std::string::const_iterator window_begin = value_as_string.begin(); + std::string::const_iterator window_end = window_begin + pattern_length_ - 1; + uint32 window_sum = + std::accumulate(window_begin, window_end, static_cast<uint32>(0u)); + while (window_end != value_as_string.end()) { + window_sum += *window_end++; + if (window_sum == pattern_sum_ && context->GetHash(std::string( + window_begin, window_end)) == hashed_pattern_) + return context->ContinueExecution(); + window_sum -= *window_begin++; + } + return true; + } + + private: + std::string hashed_pattern_; + size_t pattern_length_; + uint32 pattern_sum_; + DISALLOW_COPY_AND_ASSIGN(CompareNodeSubstring); +}; + +class StopExecutingSentenceOperation : public Operation { + public: + StopExecutingSentenceOperation() {} + virtual ~StopExecutingSentenceOperation() {} + virtual bool Execute(ExecutionContext* context) OVERRIDE { + return false; + } + + private: + DISALLOW_COPY_AND_ASSIGN(StopExecutingSentenceOperation); +}; + +class Parser { + public: + explicit Parser(const std::string& program) + : program_(program), + next_instruction_index_(0u) {} + ~Parser() {} + bool ParseNextSentence(ScopedVector<Operation>* output) { + ScopedVector<Operation> operators; + bool sentence_ended = false; + while (next_instruction_index_ < program_.size() && !sentence_ended) { + uint8 op_code = 0; + if (!ReadOpCode(&op_code)) + return false; + switch (static_cast<jtl_foundation::OpCodes>(op_code)) { + case jtl_foundation::NAVIGATE: { + std::string hashed_key; + if (!ReadHash(&hashed_key)) + return false; + operators.push_back(new NavigateOperation(hashed_key)); + break; + } + case jtl_foundation::NAVIGATE_ANY: + operators.push_back(new NavigateAnyOperation); + break; + case jtl_foundation::NAVIGATE_BACK: + operators.push_back(new NavigateBackOperation); + break; + case jtl_foundation::STORE_BOOL: { + std::string hashed_name; + if (!ReadHash(&hashed_name) || !base::IsStringUTF8(hashed_name)) + return false; + bool value = false; + if (!ReadBool(&value)) + return false; + operators.push_back(new StoreValue( + hashed_name, + scoped_ptr<base::Value>(new base::FundamentalValue(value)))); + break; + } + case jtl_foundation::COMPARE_STORED_BOOL: { + std::string hashed_name; + if (!ReadHash(&hashed_name) || !base::IsStringUTF8(hashed_name)) + return false; + bool value = false; + if (!ReadBool(&value)) + return false; + bool default_value = false; + if (!ReadBool(&default_value)) + return false; + operators.push_back(new CompareStoredValue( + hashed_name, + scoped_ptr<base::Value>(new base::FundamentalValue(value)), + scoped_ptr<base::Value>( + new base::FundamentalValue(default_value)))); + break; + } + case jtl_foundation::STORE_HASH: { + std::string hashed_name; + if (!ReadHash(&hashed_name) || !base::IsStringUTF8(hashed_name)) + return false; + std::string hashed_value; + if (!ReadHash(&hashed_value)) + return false; + operators.push_back(new StoreValue( + hashed_name, + scoped_ptr<base::Value>(new base::StringValue(hashed_value)))); + break; + } + case jtl_foundation::COMPARE_STORED_HASH: { + std::string hashed_name; + if (!ReadHash(&hashed_name) || !base::IsStringUTF8(hashed_name)) + return false; + std::string hashed_value; + if (!ReadHash(&hashed_value)) + return false; + std::string hashed_default_value; + if (!ReadHash(&hashed_default_value)) + return false; + operators.push_back(new CompareStoredValue( + hashed_name, + scoped_ptr<base::Value>(new base::StringValue(hashed_value)), + scoped_ptr<base::Value>( + new base::StringValue(hashed_default_value)))); + break; + } + case jtl_foundation::STORE_NODE_BOOL: { + std::string hashed_name; + if (!ReadHash(&hashed_name) || !base::IsStringUTF8(hashed_name)) + return false; + operators.push_back(new StoreNodeValue<true>(hashed_name)); + break; + } + case jtl_foundation::STORE_NODE_HASH: { + std::string hashed_name; + if (!ReadHash(&hashed_name) || !base::IsStringUTF8(hashed_name)) + return false; + operators.push_back(new StoreNodeValue<false>(hashed_name)); + break; + } + case jtl_foundation::STORE_NODE_REGISTERABLE_DOMAIN_HASH: { + std::string hashed_name; + if (!ReadHash(&hashed_name) || !base::IsStringUTF8(hashed_name)) + return false; + operators.push_back(new StoreNodeRegisterableDomain(hashed_name)); + break; + } + case jtl_foundation::COMPARE_NODE_BOOL: { + bool value = false; + if (!ReadBool(&value)) + return false; + operators.push_back(new CompareNodeBool(value)); + break; + } + case jtl_foundation::COMPARE_NODE_HASH: { + std::string hashed_value; + if (!ReadHash(&hashed_value)) + return false; + operators.push_back(new CompareNodeHash(hashed_value)); + break; + } + case jtl_foundation::COMPARE_NODE_HASH_NOT: { + std::string hashed_value; + if (!ReadHash(&hashed_value)) + return false; + operators.push_back(new CompareNodeHashNot(hashed_value)); + break; + } + case jtl_foundation::COMPARE_NODE_TO_STORED_BOOL: { + std::string hashed_name; + if (!ReadHash(&hashed_name) || !base::IsStringUTF8(hashed_name)) + return false; + operators.push_back(new CompareNodeToStored<true>(hashed_name)); + break; + } + case jtl_foundation::COMPARE_NODE_TO_STORED_HASH: { + std::string hashed_name; + if (!ReadHash(&hashed_name) || !base::IsStringUTF8(hashed_name)) + return false; + operators.push_back(new CompareNodeToStored<false>(hashed_name)); + break; + } + case jtl_foundation::COMPARE_NODE_SUBSTRING: { + std::string hashed_pattern; + uint32 pattern_length = 0, pattern_sum = 0; + if (!ReadHash(&hashed_pattern)) + return false; + if (!ReadUint32(&pattern_length) || pattern_length == 0) + return false; + if (!ReadUint32(&pattern_sum)) + return false; + operators.push_back(new CompareNodeSubstring( + hashed_pattern, pattern_length, pattern_sum)); + break; + } + case jtl_foundation::STOP_EXECUTING_SENTENCE: + operators.push_back(new StopExecutingSentenceOperation); + break; + case jtl_foundation::END_OF_SENTENCE: + sentence_ended = true; + break; + default: + return false; + } + } + output->swap(operators); + return true; + } + + bool HasNextSentence() const { + return next_instruction_index_ < program_.size(); + } + + private: + // Reads an uint8 and returns whether this operation was successful. + bool ReadUint8(uint8* out) { + DCHECK(out); + if (next_instruction_index_ + 1u > program_.size()) + return false; + *out = static_cast<uint8>(program_[next_instruction_index_]); + ++next_instruction_index_; + return true; + } + + // Reads an uint32 and returns whether this operation was successful. + bool ReadUint32(uint32* out) { + DCHECK(out); + if (next_instruction_index_ + 4u > program_.size()) + return false; + *out = 0u; + for (int i = 0; i < 4; ++i) { + *out >>= 8; + *out |= static_cast<uint8>(program_[next_instruction_index_]) << 24; + ++next_instruction_index_; + } + return true; + } + + // Reads an operator code and returns whether this operation was successful. + bool ReadOpCode(uint8* out) { return ReadUint8(out); } + + bool ReadHash(std::string* out) { + DCHECK(out); + if (next_instruction_index_ + jtl_foundation::kHashSizeInBytes > + program_.size()) + return false; + *out = program_.substr(next_instruction_index_, + jtl_foundation::kHashSizeInBytes); + next_instruction_index_ += jtl_foundation::kHashSizeInBytes; + DCHECK(jtl_foundation::Hasher::IsHash(*out)); + return true; + } + + bool ReadBool(bool* out) { + DCHECK(out); + uint8 value = 0; + if (!ReadUint8(&value)) + return false; + if (value == 0) + *out = false; + else if (value == 1) + *out = true; + else + return false; + return true; + } + + std::string program_; + size_t next_instruction_index_; + DISALLOW_COPY_AND_ASSIGN(Parser); +}; + +} // namespace + +JtlInterpreter::JtlInterpreter( + const std::string& hasher_seed, + const std::string& program, + const base::DictionaryValue* input) + : hasher_seed_(hasher_seed), + program_(program), + input_(input), + working_memory_(new base::DictionaryValue), + result_(OK) { + DCHECK(input->IsType(base::Value::TYPE_DICTIONARY)); +} + +JtlInterpreter::~JtlInterpreter() {} + +void JtlInterpreter::Execute() { + jtl_foundation::Hasher hasher(hasher_seed_); + Parser parser(program_); + while (parser.HasNextSentence()) { + ScopedVector<Operation> sentence; + if (!parser.ParseNextSentence(&sentence)) { + result_ = PARSE_ERROR; + return; + } + ExecutionContext context( + &hasher, sentence.get(), input_, working_memory_.get()); + context.ContinueExecution(); + if (context.error()) { + result_ = RUNTIME_ERROR; + return; + } + } +} + +bool JtlInterpreter::GetOutputBoolean(const std::string& unhashed_key, + bool* output) const { + std::string hashed_key = + jtl_foundation::Hasher(hasher_seed_).GetHash(unhashed_key); + return working_memory_->GetBoolean(hashed_key, output); +} + +bool JtlInterpreter::GetOutputString(const std::string& unhashed_key, + std::string* output) const { + std::string hashed_key = + jtl_foundation::Hasher(hasher_seed_).GetHash(unhashed_key); + return working_memory_->GetString(hashed_key, output); +} + +int JtlInterpreter::CalculateProgramChecksum() const { + uint8 digest[3] = {}; + crypto::SHA256HashString(program_, digest, arraysize(digest)); + return static_cast<uint32>(digest[0]) << 16 | + static_cast<uint32>(digest[1]) << 8 | + static_cast<uint32>(digest[2]); +} diff --git a/chrome/browser/profile_resetter/jtl_interpreter.h b/chrome/browser/profile_resetter/jtl_interpreter.h new file mode 100644 index 0000000..cafb7fd --- /dev/null +++ b/chrome/browser/profile_resetter/jtl_interpreter.h @@ -0,0 +1,61 @@ +// 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. + +#ifndef CHROME_BROWSER_PROFILE_RESETTER_JTL_INTERPRETER_H_ +#define CHROME_BROWSER_PROFILE_RESETTER_JTL_INTERPRETER_H_ + +#include <string> + +#include "base/basictypes.h" +#include "base/values.h" + +// Executes a JTL program on a given dictionary. +// +// JTL (Json Traversal Language) programs are defined in jtl_foundation.h +class JtlInterpreter { + public: + enum Result { + OK, + PARSE_ERROR, + RUNTIME_ERROR, + RESULT_MAX, + }; + + // |hasher_seed| is a value used in jtl_foundation::Hasher. All node names, + // strings, integers and doubles are hashed before being compared to hash + // values listed in |program|. + // |program| is a byte array containing a JTL program. + // |input| is a dictionary on which the program is evaluated. + JtlInterpreter(const std::string& hasher_seed, + const std::string& program, + const base::DictionaryValue* input); + ~JtlInterpreter(); + + void Execute(); + + Result result() const { return result_; } + const base::DictionaryValue* working_memory() const { + return working_memory_.get(); + } + bool GetOutputBoolean(const std::string& unhashed_key, bool* output) const; + bool GetOutputString(const std::string& unhashed_key, + std::string* output) const; + + // Generates a checksum of the loaded program, defined as the first 3 bytes of + // the program's SHA-256 hash interpreted as a big-endian integer. + int CalculateProgramChecksum() const; + + private: + // Input. + std::string hasher_seed_; + std::string program_; + const base::DictionaryValue* input_; + // Output. + scoped_ptr<base::DictionaryValue> working_memory_; + Result result_; + + DISALLOW_COPY_AND_ASSIGN(JtlInterpreter); +}; + +#endif // CHROME_BROWSER_PROFILE_RESETTER_JTL_INTERPRETER_H_ diff --git a/chrome/browser/profile_resetter/jtl_interpreter_unittest.cc b/chrome/browser/profile_resetter/jtl_interpreter_unittest.cc new file mode 100644 index 0000000..67d89df --- /dev/null +++ b/chrome/browser/profile_resetter/jtl_interpreter_unittest.cc @@ -0,0 +1,689 @@ +// Copyright 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/profile_resetter/jtl_interpreter.h" + +#include <numeric> + +#include "base/strings/string_util.h" +#include "base/test/values_test_util.h" +#include "chrome/browser/profile_resetter/jtl_foundation.h" +#include "chrome/browser/profile_resetter/jtl_instructions.h" +#include "crypto/hmac.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char seed[] = "foobar"; + +#define KEY_HASH_1 GetHash("KEY_HASH_1") +#define KEY_HASH_2 GetHash("KEY_HASH_2") +#define KEY_HASH_3 GetHash("KEY_HASH_3") +#define KEY_HASH_4 GetHash("KEY_HASH_4") + +#define VALUE_HASH_1 GetHash("VALUE_HASH_1") +#define VALUE_HASH_2 GetHash("VALUE_HASH_2") + +#define VAR_HASH_1 "01234567890123456789012345678901" +#define VAR_HASH_2 "12345678901234567890123456789012" + +std::string GetHash(const std::string& input) { + return jtl_foundation::Hasher(seed).GetHash(input); +} + +std::string EncodeUint32(uint32 value) { + std::string bytecode; + for (int i = 0; i < 4; ++i) { + bytecode.push_back(static_cast<char>(value & 0xFFu)); + value >>= 8; + } + return bytecode; +} + +// escaped_json_param may contain ' characters that are replaced with ". This +// makes the code more readable because we need less escaping. +#define INIT_INTERPRETER(program_param, escaped_json_param) \ + const char* escaped_json = escaped_json_param; \ + std::string json; \ + base::ReplaceChars(escaped_json, "'", "\"", &json); \ + scoped_ptr<base::Value> json_value(ParseJson(json)); \ + JtlInterpreter interpreter( \ + seed, \ + program_param, \ + static_cast<const base::DictionaryValue*>(json_value.get())); \ + interpreter.Execute() + +using base::test::ParseJson; + +TEST(JtlInterpreter, Store) { + INIT_INTERPRETER( + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + "{ 'KEY_HASH_1': 'VALUE_HASH_1' }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); +} + +TEST(JtlInterpreter, NavigateAndStore) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + "{ 'KEY_HASH_1': 'VALUE_HASH_1' }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); +} + +TEST(JtlInterpreter, FailNavigate) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_2) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + "{ 'KEY_HASH_1': 'VALUE_HASH_1' }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_1)); +} + +TEST(JtlInterpreter, ConsecutiveNavigate) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE(KEY_HASH_2) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); +} + +TEST(JtlInterpreter, FailConsecutiveNavigate) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE(KEY_HASH_2) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + "{ 'KEY_HASH_1': 'foo' }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_1)); +} + +TEST(JtlInterpreter, NavigateAnyInDictionary) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE_ANY + + OP_NAVIGATE(KEY_HASH_4) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + "{ 'KEY_HASH_1':" + " { 'KEY_HASH_2': {'KEY_HASH_3': 'VALUE_HASH_1' }," + " 'KEY_HASH_3': {'KEY_HASH_4': 'VALUE_HASH_1' }" + " } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); +} + +TEST(JtlInterpreter, NavigateAnyInList) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE_ANY + + OP_NAVIGATE(KEY_HASH_4) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + "{ 'KEY_HASH_1':" + " [ {'KEY_HASH_3': 'VALUE_HASH_1' }," + " {'KEY_HASH_4': 'VALUE_HASH_1' }" + " ] }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); +} + +TEST(JtlInterpreter, NavigateBack) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE(KEY_HASH_2) + + OP_NAVIGATE_BACK + + OP_NAVIGATE(KEY_HASH_3) + + OP_NAVIGATE(KEY_HASH_4) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + "{ 'KEY_HASH_1':" + " { 'KEY_HASH_2': {'KEY_HASH_3': 'VALUE_HASH_1' }," + " 'KEY_HASH_3': {'KEY_HASH_4': 'VALUE_HASH_1' }" + " } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); +} + +TEST(JtlInterpreter, StoreTwoValues) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE(KEY_HASH_2) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE) + + OP_STORE_HASH(VAR_HASH_2, VALUE_HASH_1), + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); + base::ExpectDictStringValue(VALUE_HASH_1, *interpreter.working_memory(), + VAR_HASH_2); +} + +TEST(JtlInterpreter, CompareStoredMatch) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE) + + OP_NAVIGATE(KEY_HASH_2) + + OP_COMPARE_STORED_BOOL(VAR_HASH_1, VALUE_TRUE, VALUE_FALSE) + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_2); +} + +TEST(JtlInterpreter, CompareStoredMismatch) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE) + + OP_NAVIGATE(KEY_HASH_2) + + OP_COMPARE_STORED_BOOL(VAR_HASH_1, VALUE_FALSE, VALUE_TRUE) + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_2)); +} + +TEST(JtlInterpreter, CompareStoredNoValueMatchingDefault) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE(KEY_HASH_2) + + OP_COMPARE_STORED_BOOL(VAR_HASH_1, VALUE_TRUE, VALUE_TRUE) + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_2); +} + +TEST(JtlInterpreter, CompareStoredNoValueMismatchingDefault) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE(KEY_HASH_2) + + OP_COMPARE_STORED_BOOL(VAR_HASH_1, VALUE_TRUE, VALUE_FALSE) + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_2)); +} + +TEST(JtlInterpreter, CompareBool) { + struct TestCase { + std::string expected_value; + const char* json; + bool expected_success; + } cases[] = { + { VALUE_TRUE, "{ 'KEY_HASH_1': true }", true }, + { VALUE_FALSE, "{ 'KEY_HASH_1': false }", true }, + { VALUE_TRUE, "{ 'KEY_HASH_1': false }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': 'abc' }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': 1 }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': 1.2 }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': [1] }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(testing::Message() << "Iteration " << i); + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_COMPARE_NODE_BOOL(cases[i].expected_value) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + cases[i].json); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + if (cases[i].expected_success) { + base::ExpectDictBooleanValue( + true, *interpreter.working_memory(), VAR_HASH_1); + } else { + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_1)); + } + } +} + +TEST(JtlInterpreter, CompareHashString) { + struct TestCase { + std::string expected_value; + const char* json; + bool expected_success; + } cases[] = { + { VALUE_HASH_1, "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", true }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': 'VALUE_HASH_2' }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': true }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': 1 }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': 1.1 }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': [1] }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + + { GetHash("1.2"), "{ 'KEY_HASH_1': 1.2 }", true }, + { GetHash("1.2"), "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': true }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': 1 }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': 1.3 }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': [1] }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + + { GetHash("1"), "{ 'KEY_HASH_1': 1 }", true }, + { GetHash("1"), "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': true }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': 2 }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': 1.1 }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': [1] }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(testing::Message() << "Iteration " << i); + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_COMPARE_NODE_HASH(cases[i].expected_value) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + cases[i].json); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + if (cases[i].expected_success) { + base::ExpectDictBooleanValue( + true, *interpreter.working_memory(), VAR_HASH_1); + } else { + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_1)); + } + } + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(testing::Message() << "Negated, Iteration " << i); + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_COMPARE_NODE_HASH_NOT(cases[i].expected_value) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + cases[i].json); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + if (!cases[i].expected_success) { + base::ExpectDictBooleanValue( + true, *interpreter.working_memory(), VAR_HASH_1); + } else { + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_1)); + } + } +} + +TEST(JtlInterpreter, StoreNodeBool) { + struct TestCase { + bool expected_value; + const char* json; + bool expected_success; + } cases[] = { + { true, "{ 'KEY_HASH_1': true }", true }, + { false, "{ 'KEY_HASH_1': false }", true }, + { false, "{ 'KEY_HASH_1': 'abc' }", false }, + { false, "{ 'KEY_HASH_1': 1 }", false }, + { false, "{ 'KEY_HASH_1': 1.2 }", false }, + { false, "{ 'KEY_HASH_1': [1] }", false }, + { false, "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(testing::Message() << "Iteration " << i); + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_STORE_NODE_BOOL(VAR_HASH_1) + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + cases[i].json); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + if (cases[i].expected_success) { + base::ExpectDictBooleanValue( + cases[i].expected_value, *interpreter.working_memory(), VAR_HASH_1); + base::ExpectDictBooleanValue( + true, *interpreter.working_memory(), VAR_HASH_2); + } else { + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_1)); + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_2)); + } + } +} + +TEST(JtlInterpreter, CompareNodeToStoredBool) { + struct TestCase { + std::string stored_value; + const char* json; + bool expected_success; + } cases[] = { + { VALUE_TRUE, "{ 'KEY_HASH_1': true }", true }, + { VALUE_FALSE, "{ 'KEY_HASH_1': false }", true }, + { VALUE_FALSE, "{ 'KEY_HASH_1': true }", false }, + { std::string(), "{ 'KEY_HASH_1': true }", false }, + + { VALUE_HASH_1, "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': 1 }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': 1.2 }", false }, + + { VALUE_HASH_1, "{ 'KEY_HASH_1': true }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': true }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': true }", false }, + + { VALUE_TRUE, "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': 1 }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': 1.2 }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': [1] }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(testing::Message() << "Iteration " << i); + std::string store_op; + if (cases[i].stored_value == VALUE_TRUE || + cases[i].stored_value == VALUE_FALSE) + store_op = OP_STORE_BOOL(VAR_HASH_1, cases[i].stored_value); + else if (!cases[i].stored_value.empty()) + store_op = OP_STORE_HASH(VAR_HASH_1, cases[i].stored_value); + INIT_INTERPRETER( + store_op + + OP_NAVIGATE(KEY_HASH_1) + + OP_COMPARE_NODE_TO_STORED_BOOL(VAR_HASH_1) + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + cases[i].json); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + if (cases[i].expected_success) { + base::ExpectDictBooleanValue( + true, *interpreter.working_memory(), VAR_HASH_2); + } else { + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_2)); + } + } +} + +TEST(JtlInterpreter, StoreNodeHash) { + struct TestCase { + std::string expected_value; + const char* json; + bool expected_success; + } cases[] = { + { VALUE_HASH_1, "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", true }, + { VALUE_HASH_2, "{ 'KEY_HASH_1': 'VALUE_HASH_2' }", true }, + { GetHash("1"), "{ 'KEY_HASH_1': 1 }", true }, + { GetHash("1.2"), "{ 'KEY_HASH_1': 1.2 }", true }, + { std::string(), "{ 'KEY_HASH_1': true }", false }, + { std::string(), "{ 'KEY_HASH_1': [1] }", false }, + { std::string(), "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(testing::Message() << "Iteration " << i); + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_STORE_NODE_HASH(VAR_HASH_1) + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + cases[i].json); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + if (cases[i].expected_success) { + base::ExpectDictStringValue( + cases[i].expected_value, *interpreter.working_memory(), VAR_HASH_1); + base::ExpectDictBooleanValue( + true, *interpreter.working_memory(), VAR_HASH_2); + } else { + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_1)); + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_2)); + } + } +} + +TEST(JtlInterpreter, CompareNodeToStoredHash) { + struct TestCase { + std::string stored_value; + const char* json; + bool expected_success; + } cases[] = { + { VALUE_HASH_1, "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", true }, + { VALUE_HASH_2, "{ 'KEY_HASH_1': 'VALUE_HASH_2' }", true }, + { std::string(), "{ 'KEY_HASH_1': 'VALUE_HASH_2' }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': 'VALUE_HASH_2' }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': true }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': 1 }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': 1.1 }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': [1] }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + + { GetHash("1.2"), "{ 'KEY_HASH_1': 1.2 }", true }, + { GetHash("1.3"), "{ 'KEY_HASH_1': 1.3 }", true }, + { std::string(), "{ 'KEY_HASH_1': 1.2 }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': true }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': 1 }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': 1.3 }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': [1] }", false }, + { GetHash("1.2"), "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + + { GetHash("1"), "{ 'KEY_HASH_1': 1 }", true }, + { GetHash("2"), "{ 'KEY_HASH_1': 2 }", true }, + { std::string(), "{ 'KEY_HASH_1': 2 }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': true }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': 2 }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': 1.1 }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': [1] }", false }, + { GetHash("1"), "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + + { VALUE_TRUE, "{ 'KEY_HASH_1': 'VALUE_HASH_1' }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': 1 }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': 1.3 }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': [1] }", false }, + { VALUE_TRUE, "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + + { VALUE_TRUE, "{ 'KEY_HASH_1': true }", false }, + { VALUE_FALSE, "{ 'KEY_HASH_1': false }", false }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(testing::Message() << "Iteration " << i); + std::string store_op; + if (cases[i].stored_value == VALUE_TRUE || + cases[i].stored_value == VALUE_FALSE) + store_op = OP_STORE_BOOL(VAR_HASH_1, cases[i].stored_value); + else if (!cases[i].stored_value.empty()) + store_op = OP_STORE_HASH(VAR_HASH_1, cases[i].stored_value); + INIT_INTERPRETER( + store_op + + OP_NAVIGATE(KEY_HASH_1) + + OP_COMPARE_NODE_TO_STORED_HASH(VAR_HASH_1) + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + cases[i].json); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + if (cases[i].expected_success) { + base::ExpectDictBooleanValue( + true, *interpreter.working_memory(), VAR_HASH_2); + } else { + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_2)); + } + } +} + +TEST(JtlInterpreter, CompareSubstring) { + struct TestCase { + std::string pattern; + const char* json; + bool expected_success; + } cases[] = { + { "abc", "{ 'KEY_HASH_1': 'abcdefghijklmnopqrstuvwxyz' }", true }, + { "xyz", "{ 'KEY_HASH_1': 'abcdefghijklmnopqrstuvwxyz' }", true }, + { "m", "{ 'KEY_HASH_1': 'abcdefghijklmnopqrstuvwxyz' }", true }, + { "abc", "{ 'KEY_HASH_1': 'abc' }", true }, + { "cba", "{ 'KEY_HASH_1': 'abcdefghijklmnopqrstuvwxyz' }", false }, + { "acd", "{ 'KEY_HASH_1': 'abcdefghijklmnopqrstuvwxyz' }", false }, + { "waaaaaaay_too_long", "{ 'KEY_HASH_1': 'abc' }", false }, + + { VALUE_HASH_1, "{ 'KEY_HASH_1': true }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': 1 }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': 1.1 }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': [1] }", false }, + { VALUE_HASH_1, "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(testing::Message() << "Iteration " << i); + std::string pattern = cases[i].pattern; + uint32 pattern_sum = std::accumulate( + pattern.begin(), pattern.end(), static_cast<uint32>(0u)); + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_COMPARE_NODE_SUBSTRING(GetHash(pattern), + EncodeUint32(pattern.size()), + EncodeUint32(pattern_sum)) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE), + cases[i].json); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + if (cases[i].expected_success) { + base::ExpectDictBooleanValue( + true, *interpreter.working_memory(), VAR_HASH_1); + } else { + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_1)); + } + } +} + +TEST(JtlInterpreter, StoreNodeRegisterableDomainHash) { + struct TestCase { + std::string expected_value; + const char* json; + bool expected_success; + } cases[] = { + { GetHash("google"), "{ 'KEY_HASH_1': 'http://google.com/path' }", true }, + { GetHash("google"), "{ 'KEY_HASH_1': 'http://mail.google.com/' }", true }, + { GetHash("google"), "{ 'KEY_HASH_1': 'http://google.co.uk/' }", true }, + { GetHash("google"), "{ 'KEY_HASH_1': 'http://google.com./' }", true }, + { GetHash("google"), "{ 'KEY_HASH_1': 'http://..google.com/' }", true }, + + { GetHash("foo"), "{ 'KEY_HASH_1': 'http://foo.bar/path' }", true }, + { GetHash("foo"), "{ 'KEY_HASH_1': 'http://sub.foo.bar' }", true }, + { GetHash("foo"), "{ 'KEY_HASH_1': 'http://foo.appspot.com/' }", true }, + { GetHash("foo"), "{ 'KEY_HASH_1': 'http://sub.foo.appspot.com' }", true }, + + { std::string(), "{ 'KEY_HASH_1': 'http://google.com../' }", false }, + + { std::string(), "{ 'KEY_HASH_1': 'http://bar/path' }", false }, + { std::string(), "{ 'KEY_HASH_1': 'http://co.uk/path' }", false }, + { std::string(), "{ 'KEY_HASH_1': 'http://appspot.com/path' }", false }, + { std::string(), "{ 'KEY_HASH_1': 'http://127.0.0.1/path' }", false }, + { std::string(), "{ 'KEY_HASH_1': 'file:///C:/bar.html' }", false }, + + { std::string(), "{ 'KEY_HASH_1': 1 }", false }, + { std::string(), "{ 'KEY_HASH_1': 1.2 }", false }, + { std::string(), "{ 'KEY_HASH_1': true }", false }, + { std::string(), "{ 'KEY_HASH_1': [1] }", false }, + { std::string(), "{ 'KEY_HASH_1': {'a': 'b'} }", false }, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(testing::Message() << "Iteration " << i); + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_STORE_NODE_REGISTERABLE_DOMAIN_HASH(VAR_HASH_1) + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + cases[i].json); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + if (cases[i].expected_success) { + base::ExpectDictStringValue( + cases[i].expected_value, *interpreter.working_memory(), VAR_HASH_1); + base::ExpectDictBooleanValue( + true, *interpreter.working_memory(), VAR_HASH_2); + } else { + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_1)); + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_2)); + } + } +} + +TEST(JtlInterpreter, Stop) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE(KEY_HASH_2) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE) + + OP_STOP_EXECUTING_SENTENCE + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); + EXPECT_FALSE(interpreter.working_memory()->HasKey(VAR_HASH_2)); +} + +TEST(JtlInterpreter, EndOfSentence) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE(KEY_HASH_2) + + OP_STORE_BOOL(VAR_HASH_1, VALUE_TRUE) + + OP_END_OF_SENTENCE + + OP_STORE_BOOL(VAR_HASH_2, VALUE_TRUE), + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::OK, interpreter.result()); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_1); + base::ExpectDictBooleanValue(true, *interpreter.working_memory(), VAR_HASH_2); +} + +TEST(JtlInterpreter, InvalidBack) { + INIT_INTERPRETER( + OP_NAVIGATE(KEY_HASH_1) + + OP_NAVIGATE_BACK + + OP_NAVIGATE_BACK, + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::RUNTIME_ERROR, interpreter.result()); +} + +TEST(JtlInterpreter, IncorrectPrograms) { + std::string missing_hash; + std::string missing_bool; + std::string invalid_hash("123"); + std::string invalid_bool("\x02", 1); + std::string invalid_operation("\x99", 1); + std::string programs[] = { + OP_NAVIGATE(missing_hash), + OP_NAVIGATE(invalid_hash), + OP_STORE_BOOL(VAR_HASH_1, invalid_bool), + OP_STORE_BOOL(missing_hash, VALUE_TRUE), + OP_STORE_BOOL(invalid_hash, VALUE_TRUE), + OP_COMPARE_STORED_BOOL(invalid_hash, VALUE_TRUE, VALUE_TRUE), + OP_COMPARE_STORED_BOOL(VAR_HASH_1, invalid_bool, VALUE_TRUE), + OP_COMPARE_STORED_BOOL(VAR_HASH_1, VALUE_TRUE, invalid_bool), + OP_COMPARE_STORED_BOOL(VAR_HASH_1, VALUE_TRUE, missing_bool), + OP_STORE_NODE_BOOL(missing_hash), + OP_STORE_NODE_BOOL(invalid_hash), + OP_STORE_NODE_HASH(missing_hash), + OP_STORE_NODE_HASH(invalid_hash), + OP_COMPARE_NODE_BOOL(missing_bool), + OP_COMPARE_NODE_BOOL(invalid_bool), + OP_COMPARE_NODE_HASH(missing_hash), + OP_COMPARE_NODE_HASH(invalid_hash), + OP_COMPARE_NODE_TO_STORED_BOOL(missing_hash), + OP_COMPARE_NODE_TO_STORED_BOOL(invalid_hash), + OP_COMPARE_NODE_TO_STORED_HASH(missing_hash), + OP_COMPARE_NODE_TO_STORED_HASH(invalid_hash), + invalid_operation, + }; + for (size_t i = 0; i < arraysize(programs); ++i) { + SCOPED_TRACE(testing::Message() << "Iteration " << i); + INIT_INTERPRETER(programs[i], + "{ 'KEY_HASH_1': { 'KEY_HASH_2': 'VALUE_HASH_1' } }"); + EXPECT_EQ(JtlInterpreter::PARSE_ERROR, interpreter.result()); + } +} + +TEST(JtlInterpreter, GetOutput) { + INIT_INTERPRETER( + OP_STORE_BOOL(GetHash("output1"), VALUE_TRUE) + + OP_STORE_HASH(GetHash("output2"), VALUE_HASH_1), + "{}"); + bool output1 = false; + std::string output2; + EXPECT_TRUE(interpreter.GetOutputBoolean("output1", &output1)); + EXPECT_EQ(true, output1); + EXPECT_TRUE(interpreter.GetOutputString("output2", &output2)); + EXPECT_EQ(VALUE_HASH_1, output2); + EXPECT_FALSE(interpreter.GetOutputBoolean("outputxx", &output1)); + EXPECT_FALSE(interpreter.GetOutputString("outputxx", &output2)); +} + +TEST(JtlInterpreter, CalculateProgramChecksum) { + const char kTestSeed[] = "Irrelevant seed value."; + const char kTestProgram[] = "The quick brown fox jumps over the lazy dog."; + // This program is invalid, but we are not actually executing it. + base::DictionaryValue input; + JtlInterpreter interpreter(kTestSeed, kTestProgram, &input); + EXPECT_EQ(0xef537f, interpreter.CalculateProgramChecksum()); +} + +} // namespace diff --git a/chrome/browser/profile_resetter/profile_reset_global_error.cc b/chrome/browser/profile_resetter/profile_reset_global_error.cc new file mode 100644 index 0000000..40228007 --- /dev/null +++ b/chrome/browser/profile_resetter/profile_reset_global_error.cc @@ -0,0 +1,110 @@ +// 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/profile_resetter/profile_reset_global_error.h" + +#include "base/metrics/histogram.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter_factory.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/chrome_pages.h" +#include "chrome/browser/ui/global_error/global_error_service.h" +#include "chrome/browser/ui/global_error/global_error_service_factory.h" +#include "chrome/browser/ui/profile_reset_bubble.h" +#include "chrome/common/url_constants.h" +#include "grit/chromium_strings.h" +#include "grit/generated_resources.h" +#include "ui/base/l10n/l10n_util.h" + +namespace { + +base::TimeDelta GetPromptDelayHistogramMaximum() { + return base::TimeDelta::FromDays(7); +} + +// Records the delay between when the reset prompt is triggered and when the +// bubble can actually be shown. +void RecordPromptDelay(const base::TimeDelta& delay) { + UMA_HISTOGRAM_CUSTOM_TIMES( + "AutomaticProfileReset.PromptDelay", delay, + base::TimeDelta::FromSeconds(1), GetPromptDelayHistogramMaximum(), 50); +} + +} // namespace + + +// ProfileResetGlobalError --------------------------------------------------- + +ProfileResetGlobalError::ProfileResetGlobalError(Profile* profile) + : profile_(profile), has_shown_bubble_view_(false), bubble_view_(NULL) { + AutomaticProfileResetter* automatic_profile_resetter = + AutomaticProfileResetterFactory::GetForBrowserContext(profile_); + if (automatic_profile_resetter) + automatic_profile_resetter_ = automatic_profile_resetter->AsWeakPtr(); +} + +ProfileResetGlobalError::~ProfileResetGlobalError() { + if (!has_shown_bubble_view_) + RecordPromptDelay(GetPromptDelayHistogramMaximum()); +} + +// static +bool ProfileResetGlobalError::IsSupportedOnPlatform() { + return IsProfileResetBubbleSupported(); +} + +bool ProfileResetGlobalError::HasMenuItem() { return true; } + +int ProfileResetGlobalError::MenuItemCommandID() { + return IDC_SHOW_SETTINGS_RESET_BUBBLE; +} + +base::string16 ProfileResetGlobalError::MenuItemLabel() { + return l10n_util::GetStringFUTF16( + IDS_RESET_SETTINGS_MENU_ITEM, + l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)); +} + +void ProfileResetGlobalError::ExecuteMenuItem(Browser* browser) { + chrome::ShowSettingsSubPage(browser, chrome::kResetProfileSettingsSubPage); +} + +bool ProfileResetGlobalError::HasBubbleView() { return true; } + +bool ProfileResetGlobalError::HasShownBubbleView() { + return has_shown_bubble_view_; +} + +void ProfileResetGlobalError::ShowBubbleView(Browser* browser) { + if (has_shown_bubble_view_) + return; + + has_shown_bubble_view_ = true; + bubble_view_ = ShowProfileResetBubble(AsWeakPtr(), browser); + + if (automatic_profile_resetter_) + automatic_profile_resetter_->NotifyDidShowResetBubble(); + RecordPromptDelay(timer_.Elapsed()); +} + +void ProfileResetGlobalError::OnBubbleViewDidClose() { + bubble_view_ = NULL; +} + +void ProfileResetGlobalError::OnBubbleViewResetButtonPressed( + bool send_feedback) { + if (automatic_profile_resetter_) + automatic_profile_resetter_->TriggerProfileReset(send_feedback); +} + +void ProfileResetGlobalError::OnBubbleViewNoThanksButtonPressed() { + if (automatic_profile_resetter_) + automatic_profile_resetter_->SkipProfileReset(); +} + +GlobalErrorBubbleViewBase* ProfileResetGlobalError::GetBubbleView() { + return bubble_view_; +} diff --git a/chrome/browser/profile_resetter/profile_reset_global_error.h b/chrome/browser/profile_resetter/profile_reset_global_error.h new file mode 100644 index 0000000..6529df49 --- /dev/null +++ b/chrome/browser/profile_resetter/profile_reset_global_error.h @@ -0,0 +1,72 @@ +// 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. + +#ifndef CHROME_BROWSER_PROFILE_RESETTER_PROFILE_RESET_GLOBAL_ERROR_H_ +#define CHROME_BROWSER_PROFILE_RESETTER_PROFILE_RESET_GLOBAL_ERROR_H_ + +#include "base/basictypes.h" +#include "base/memory/weak_ptr.h" +#include "base/timer/elapsed_timer.h" +#include "chrome/browser/ui/global_error/global_error.h" + +class AutomaticProfileResetter; +class GlobalErrorBubbleViewBase; +class Profile; + +// Encapsulates UI-related functionality for the one-time profile settings reset +// prompt. The UI consists of two parts: (1.) the profile reset (pop-up) bubble, +// and (2.) a menu item in the wrench menu (provided by us being a GlobalError). +class ProfileResetGlobalError + : public GlobalError, + public base::SupportsWeakPtr<ProfileResetGlobalError> { + public: + explicit ProfileResetGlobalError(Profile* profile); + virtual ~ProfileResetGlobalError(); + + // Returns whether or not the reset prompt is supported on this platform. + static bool IsSupportedOnPlatform(); + + // Called by the bubble view when it is closed. + void OnBubbleViewDidClose(); + + // Called when the user clicks on the 'Reset' button. The user can choose to + // send feedback containing the old settings that are now being reset, this is + // indicated by |send_feedback|. + void OnBubbleViewResetButtonPressed(bool send_feedback); + + // Called when the user clicks the 'No, thanks' button. + void OnBubbleViewNoThanksButtonPressed(); + + // GlobalError: + virtual bool HasMenuItem() OVERRIDE; + virtual int MenuItemCommandID() OVERRIDE; + virtual base::string16 MenuItemLabel() OVERRIDE; + virtual void ExecuteMenuItem(Browser* browser) OVERRIDE; + virtual bool HasBubbleView() OVERRIDE; + virtual bool HasShownBubbleView() OVERRIDE; + virtual void ShowBubbleView(Browser* browser) OVERRIDE; + virtual GlobalErrorBubbleViewBase* GetBubbleView() OVERRIDE; + + private: + Profile* profile_; + + // GlobalErrorService owns us, on which AutomaticProfileResetter depends, so + // during shutdown, it may get destroyed before we are. + // Note: the AutomaticProfileResetter expects call-backs from us to always be + // synchronous, so that there will be no call-backs once we are destroyed. + base::WeakPtr<AutomaticProfileResetter> automatic_profile_resetter_; + + // Used to measure the delay before the bubble actually gets shown. + base::ElapsedTimer timer_; + + // Whether or not we have already shown the bubble. + bool has_shown_bubble_view_; + + // The reset bubble, if we're currently showing one. + GlobalErrorBubbleViewBase* bubble_view_; + + DISALLOW_COPY_AND_ASSIGN(ProfileResetGlobalError); +}; + +#endif // CHROME_BROWSER_PROFILE_RESETTER_PROFILE_RESET_GLOBAL_ERROR_H_ diff --git a/chrome/browser/profile_resetter/resettable_settings_snapshot.cc b/chrome/browser/profile_resetter/resettable_settings_snapshot.cc index 439153c..fb58ee3 100644 --- a/chrome/browser/profile_resetter/resettable_settings_snapshot.cc +++ b/chrome/browser/profile_resetter/resettable_settings_snapshot.cc @@ -29,7 +29,8 @@ using feedback::FeedbackData; namespace { -// Feedback bucket label. +// Feedback bucket labels. +const char kProfileResetPromptBucket[] = "SamplingOfSettingsResetPrompt"; const char kProfileResetWebUIBucket[] = "ProfileResetReport"; // Dictionary keys for feedback report. @@ -219,9 +220,19 @@ std::string SerializeSettingsReport(const ResettableSettingsSnapshot& snapshot, } void SendSettingsFeedback(const std::string& report, - Profile* profile) { + Profile* profile, + SnapshotCaller caller) { scoped_refptr<FeedbackData> feedback_data = new FeedbackData(); - feedback_data->set_category_tag(kProfileResetWebUIBucket); + std::string bucket; + switch (caller) { + case PROFILE_RESET_WEBUI: + bucket = kProfileResetWebUIBucket; + break; + case PROFILE_RESET_PROMPT: + bucket = kProfileResetPromptBucket; + break; + } + feedback_data->set_category_tag(bucket); feedback_data->set_description(report); feedback_data->set_image(make_scoped_ptr(new std::string)); diff --git a/chrome/browser/profile_resetter/resettable_settings_snapshot.h b/chrome/browser/profile_resetter/resettable_settings_snapshot.h index 84aa6e2..04b4541 100644 --- a/chrome/browser/profile_resetter/resettable_settings_snapshot.h +++ b/chrome/browser/profile_resetter/resettable_settings_snapshot.h @@ -114,6 +114,12 @@ class ResettableSettingsSnapshot { DISALLOW_COPY_AND_ASSIGN(ResettableSettingsSnapshot); }; +// The caller of ResettableSettingsSnapshot. +enum SnapshotCaller { + PROFILE_RESET_WEBUI = 0, + PROFILE_RESET_PROMPT, +}; + // Serializes specified |snapshot| members to JSON format. |field_mask| is a bit // mask of ResettableSettingsSnapshot::Field values. std::string SerializeSettingsReport(const ResettableSettingsSnapshot& snapshot, @@ -122,7 +128,8 @@ std::string SerializeSettingsReport(const ResettableSettingsSnapshot& snapshot, // Sends |report| as a feedback. |report| is supposed to be result of // SerializeSettingsReport(). void SendSettingsFeedback(const std::string& report, - Profile* profile); + Profile* profile, + SnapshotCaller caller); // Returns list of key/value pairs for all available reported information // from the |profile| and some additional fields. |