diff options
author | Balazs Engedy <engedy@chromium.org> | 2014-09-18 12:40:18 +0200 |
---|---|---|
committer | Balazs Engedy <engedy@chromium.org> | 2014-09-18 10:42:59 +0000 |
commit | 9d0221cd251502231877f943fa2872a2f92a5621 (patch) | |
tree | 7a12b382678a523747bfe4b25583d416a67d6042 | |
parent | 96297a73c48ff299e45eac62b967a8e333569602 (diff) | |
download | chromium_src-9d0221cd251502231877f943fa2872a2f92a5621.zip chromium_src-9d0221cd251502231877f943fa2872a2f92a5621.tar.gz chromium_src-9d0221cd251502231877f943fa2872a2f92a5621.tar.bz2 |
Revert "Eliminate all code related to the AutomaticProfileResetter."
This reverts commit:
https://chromium.googlesource.com/chromium/src/+/93ea39da5423d641ecbc5687764f6b12972e302a.
Additionally, it also changes the |first_id| for the string resource group chrome/browser/browser_resources.grd / "includes", because the group has ran out of IDs.
BUG=370966
NOPRESUBMIT=true
R=asvitkine@chromium.org, dbeam@chromium.org, erg@chromium.org, gab@chromium.org, sky@chromium.org, vasilii@chromium.org
Review URL: https://codereview.chromium.org/533183002
Cr-Commit-Position: refs/heads/master@{#295450}
61 files changed, 8200 insertions, 76 deletions
diff --git a/build/all.gyp b/build/all.gyp index 4e71a39..eff9af4 100644 --- a/build/all.gyp +++ b/build/all.gyp @@ -98,6 +98,7 @@ 'dependencies': [ '../third_party/re2/re2.gyp:re2', '../chrome/chrome.gyp:*', + '../chrome/tools/profile_reset/jtl_compiler.gyp:*', '../cc/blink/cc_blink_tests.gyp:*', '../cc/cc_tests.gyp:*', '../device/bluetooth/bluetooth.gyp:*', diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index 8556823..2e97de6 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -8032,6 +8032,33 @@ Keep your key file in a safe place. You will need it to create new versions of y Report an issue </message> + <!-- settings reset bubble messages --> + <message name="IDS_RESETTING" desc="Text for the button, once the user clicked to reset settings."> + Resetting... + </message> + <message name="IDS_NO_THANKS" desc="Text for the button the user clicks to refuse settings reset."> + No thanks + </message> + <if expr="use_titlecase"> + <message name="IDS_RESET_SETTINGS_MENU_ITEM" desc="In Title Case: Text for the Chrome menu option replacing Update required."> + Reset Altered <ph name="IDS_SHORT_PRODUCT_NAME">$1<ex>Chrome</ex></ph> Settings + </message> + </if> + <if expr="not use_titlecase"> + <message name="IDS_RESET_SETTINGS_MENU_ITEM" desc="Text for the Chrome menu option replacing Update required."> + Reset altered <ph name="IDS_SHORT_PRODUCT_NAME">$1<ex>Chrome</ex></ph> settings + </message> + </if> + <message name="IDS_RESET_BUBBLE_TITLE" desc="Text for the title of the settings reset bubble view."> + Reset altered <ph name="IDS_SHORT_PRODUCT_NAME">$1<ex>Chrome</ex></ph> settings? + </message> + <message name="IDS_RESET_BUBBLE_TEXT" desc="Text for the settings reset bubble view full description."> + <ph name="IDS_SHORT_PRODUCT_NAME">$1<ex>Chrome</ex></ph> detected that your browser settings may have been changed without your knowledge. Would you like to reset them to their original defaults? + </message> + <message name="IDS_REPORT_BUBBLE_TEXT" desc="Text for the settings reset bubble reporting checkbox."> + Help make Google Chrome better by reporting the current settings + </message> + <!-- Upgrade bubble messages --> <message name="IDS_REENABLE_UPDATES" desc="Text for the button the user clicks to re-enable automatic updates."> Enable autoupdate @@ -10309,6 +10336,9 @@ Chrome ran out of memory. </message> <!-- Reset Profile Settings strings --> + <message name="IDS_RESET_PROFILE_SETTINGS_BANNER_TEXT" desc="The text to show in a banner at the top of the chrome://settings page. The banner is displayed when Chrome detects that the settings might have been changed without the user's knowledge."> + Some of your settings may have been changed without your knowledge. + </message> <message name="IDS_RESET_PROFILE_SETTINGS_SECTION_TITLE" desc="The title of the section in chrome://settings that allows resetting some settings in a profile"> Reset settings </message> diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd index 7c244f0..22e40fe 100644 --- a/chrome/browser/browser_resources.grd +++ b/chrome/browser/browser_resources.grd @@ -390,6 +390,10 @@ </if> <if expr="_google_chrome"> <include name="IDR_PREF_HASH_SEED_BIN" file="resources\settings_internal\pref_hash_seed.bin" type="BINDATA" /> + <include name="IDR_AUTOMATIC_PROFILE_RESET_RULES" file="internal\resources\profile_reset\automatic_profile_reset_rules.dat" type="BINDATA" /> + <include name="IDR_AUTOMATIC_PROFILE_RESET_RULES_DRY" file="internal\resources\profile_reset\automatic_profile_reset_rules_dry.dat" type="BINDATA" /> + <include name="IDR_AUTOMATIC_PROFILE_RESET_HASH_SEED" file="internal\resources\profile_reset\automatic_profile_reset_rules.seed" type="BINDATA" /> + <include name="IDR_AUTOMATIC_PROFILE_RESET_HASH_SEED_DRY" file="internal\resources\profile_reset\automatic_profile_reset_rules_dry.seed" type="BINDATA" /> <include name="IDR_ADDITIONAL_MODULE_IDS" file="${additional_modules_list_file}" use_base_dir="false" type="BINDATA" /> </if> <if expr="chromeos"> diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc index 2dc4c325e..3d27e95 100644 --- a/chrome/browser/prefs/browser_prefs.cc +++ b/chrome/browser/prefs/browser_prefs.cc @@ -134,6 +134,7 @@ #include "chrome/browser/android/new_tab_page_prefs.h" #else #include "chrome/browser/notifications/sync_notifier/chrome_notifier_service.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter_factory.h" #include "chrome/browser/ui/autofill/generated_credit_card_bubble_controller.h" #endif @@ -220,11 +221,12 @@ const char kBackupPref[] = "backup"; // be cleared from user data. const char kSyncPromoErrorMessage[] = "sync_promo.error_message"; -// The AutomaticProfileResetter service, which has since been unimplemented, -// used this preference to save that the profile reset prompt had already been -// shown. We keep the name here for now so that we can clear out legacy values. +// The AutomaticProfileResetter service used this preference to save that the +// profile reset prompt had already been shown, however, the preference has been +// renamed in Local State. We keep the name here for now so that we can clear +// out legacy values. // TODO(engedy): Remove this and usages in M42 or later. See crbug.com/398813. -const char kProfileResetPromptMemento[] = "profile.reset_prompt_memento"; +const char kLegacyProfileResetPromptMemento[] = "profile.reset_prompt_memento"; #endif } // namespace @@ -288,6 +290,7 @@ void RegisterLocalState(PrefRegistrySimple* registry) { #endif // defined(ENABLE_TASK_MANAGER) #if !defined(OS_ANDROID) + AutomaticProfileResetterFactory::RegisterPrefs(registry); BackgroundModeManager::RegisterPrefs(registry); RegisterBrowserPrefs(registry); #if !defined(OS_CHROMEOS) @@ -349,7 +352,7 @@ void RegisterLocalState(PrefRegistrySimple* registry) { // Preferences registered only for migration (clearing or moving to a new key) // go here. #if !defined(OS_ANDROID) - registry->RegisterDictionaryPref(kProfileResetPromptMemento); + registry->RegisterDictionaryPref(kLegacyProfileResetPromptMemento); #endif // !defined(OS_ANDROID) } @@ -601,7 +604,7 @@ void MigrateBrowserPrefs(Profile* profile, PrefService* local_state) { } #if !defined(OS_ANDROID) - local_state->ClearPref(kProfileResetPromptMemento); + local_state->ClearPref(kLegacyProfileResetPromptMemento); #endif #if defined(OS_CHROMEOS) diff --git a/chrome/browser/prefs/chrome_pref_service_factory.cc b/chrome/browser/prefs/chrome_pref_service_factory.cc index 57a184c..733491a 100644 --- a/chrome/browser/prefs/chrome_pref_service_factory.cc +++ b/chrome/browser/prefs/chrome_pref_service_factory.cc @@ -153,6 +153,11 @@ const PrefHashFilter::TrackedPreferenceMetadata kTrackedPrefs[] = { PrefHashFilter::ENFORCE_ON_LOAD, PrefHashFilter::TRACKING_STRATEGY_ATOMIC }, + { + 13, prefs::kProfileResetPromptMementoInProfilePrefs, + PrefHashFilter::ENFORCE_ON_LOAD, + PrefHashFilter::TRACKING_STRATEGY_ATOMIC + }, #endif { 14, DefaultSearchManager::kDefaultSearchProviderDataPrefName, diff --git a/chrome/browser/prefs/pref_hash_filter.cc b/chrome/browser/prefs/pref_hash_filter.cc index 3aceb62..fe216e8 100644 --- a/chrome/browser/prefs/pref_hash_filter.cc +++ b/chrome/browser/prefs/pref_hash_filter.cc @@ -31,10 +31,6 @@ void CleanupDeprecatedTrackedPreferences( static const char* kDeprecatedTrackedPreferences[] = { // TODO(gab): Remove in M41+. "extensions.known_disabled", -#if !defined(OS_ANDROID) - // TODO(engedy): Remove this in M42 or later. See crbug.com/398813. - "profile.reset_prompt_memento", -#endif }; for (size_t i = 0; i < arraysize(kDeprecatedTrackedPreferences); ++i) { 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. diff --git a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc index a31257b..d7c72b0 100644 --- a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc +++ b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc @@ -101,6 +101,7 @@ #include "chrome/browser/media/protected_media_identifier_permission_context_factory.h" #else #include "chrome/browser/notifications/sync_notifier/chrome_notifier_service_factory.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter_factory.h" #endif #if defined(ENABLE_SPELLCHECK) @@ -154,6 +155,9 @@ EnsureBrowserContextKeyedServiceFactoriesBuilt() { AboutSigninInternalsFactory::GetInstance(); AccountTrackerServiceFactory::GetInstance(); autofill::PersonalDataManagerFactory::GetInstance(); +#if !defined(OS_ANDROID) + AutomaticProfileResetterFactory::GetInstance(); +#endif #if defined(ENABLE_BACKGROUND) BackgroundContentsServiceFactory::GetInstance(); #endif diff --git a/chrome/browser/profiles/profile_impl.cc b/chrome/browser/profiles/profile_impl.cc index 9cfe535..ce44ca2 100644 --- a/chrome/browser/profiles/profile_impl.cc +++ b/chrome/browser/profiles/profile_impl.cc @@ -282,17 +282,6 @@ PrefStore* CreateExtensionPrefStore(Profile* profile, #endif } -#if !defined(OS_ANDROID) -// Deletes the file that was used by the AutomaticProfileResetter service, which -// has since been removed, to store that the prompt had already been shown. -// TODO(engedy): Remove this and caller in M42 or later. See crbug.com/398813. -void DeleteResetPromptMementoFile(const base::FilePath& profile_dir) { - base::FilePath memento_path = - profile_dir.Append(FILE_PATH_LITERAL("Reset Prompt Memento")); - base::DeleteFile(memento_path, false); -} -#endif - } // namespace // static @@ -724,13 +713,6 @@ void ProfileImpl::DoFinalInit() { // as a URLDataSource early. RegisterDomDistillerViewerSource(this); -#if !defined(OS_ANDROID) - BrowserThread::GetBlockingPool()->PostDelayedWorkerTask( - FROM_HERE, - base::Bind(&DeleteResetPromptMementoFile, GetPath()), - base::TimeDelta::FromMilliseconds(2 * create_readme_delay_ms)); -#endif - // Creation has been finished. TRACE_EVENT_END1("browser", "Profile::CreateProfile", diff --git a/chrome/browser/resources/options/automatic_settings_reset_banner.js b/chrome/browser/resources/options/automatic_settings_reset_banner.js index 3da6f5d..b16f6cb 100644 --- a/chrome/browser/resources/options/automatic_settings_reset_banner.js +++ b/chrome/browser/resources/options/automatic_settings_reset_banner.js @@ -25,12 +25,12 @@ cr.define('options', function() { * Initializes the banner's event handlers. */ initialize: function() { - this.showMetricName_ = 'AutomaticSettingsReset_WebUIBanner_BannerShown'; + this.showMetricName = 'AutomaticSettingsReset_WebUIBanner_BannerShown'; - this.dismissNativeCallbackName_ = + this.dismissNativeCallbackName = 'onDismissedAutomaticSettingsResetBanner'; - this.setVisibilibyDomElement_ = $('automatic-settings-reset-banner'); + this.visibilityDomElement = $('automatic-settings-reset-banner'); $('automatic-settings-reset-banner-close').onclick = function(event) { chrome.send('metricsHandler:recordAction', @@ -48,36 +48,18 @@ cr.define('options', function() { PageManager.showPageByName('resetProfileSettings'); }; }, - - /** - * Called by the native code to show the banner if needed. - * @private - */ - show_: function() { - if (!this.hadBeenDismissed_) { - chrome.send('metricsHandler:recordAction', [this.showMetricName_]); - this.setVisibility(true); - } - }, - - /** - * Called when the banner should be closed as a result of something taking - * place on the WebUI page, i.e. when its close button is pressed, or when - * the confirmation dialog for the profile settings reset feature is opened. - * @private - */ - dismiss_: function() { - chrome.send(this.dismissNativeCallbackName_); - this.hadBeenDismissed_ = true; - this.setVisibility(false); - }, }; - // Forward public APIs to private implementations. - cr.makePublic(AutomaticSettingsResetBanner, [ + // Forward public APIs to protected implementations. + [ 'show', 'dismiss', - ]); + ].forEach(function(name) { + AutomaticSettingsResetBanner[name] = function() { + var instance = AutomaticSettingsResetBanner.getInstance(); + return instance[name].apply(instance, arguments); + }; + }); // Export return { diff --git a/chrome/browser/resources/options/browser_options.html b/chrome/browser/resources/options/browser_options.html index 452ddf8..da3641b 100644 --- a/chrome/browser/resources/options/browser_options.html +++ b/chrome/browser/resources/options/browser_options.html @@ -2,6 +2,7 @@ <header> <h1 i18n-content="settingsTitle"></h1> </header> + <include src="reset_profile_settings_banner.html"> <include src="automatic_settings_reset_banner.html"> <if expr="chromeos"> <include src="secondary_user_banner.html"> diff --git a/chrome/browser/resources/options/options.js b/chrome/browser/resources/options/options.js index c90dba53..9d62c10 100644 --- a/chrome/browser/resources/options/options.js +++ b/chrome/browser/resources/options/options.js @@ -35,6 +35,7 @@ var PageManager = cr.ui.pageManager.PageManager; var PasswordManager = options.PasswordManager; var Preferences = options.Preferences; var PreferredNetworks = options.PreferredNetworks; +var ResetProfileSettingsBanner = options.ResetProfileSettingsBanner; var ResetProfileSettingsOverlay = options.ResetProfileSettingsOverlay; var SearchEngineManager = options.SearchEngineManager; var SearchPage = options.SearchPage; @@ -238,6 +239,7 @@ function load() { cr.ui.FocusManager.disableMouseFocusOnButtons(); OptionsFocusManager.getInstance().initialize(); Preferences.getInstance().initialize(); + ResetProfileSettingsBanner.getInstance().initialize(); AutomaticSettingsResetBanner.getInstance().initialize(); OptionsPage.initialize(); PageManager.initialize(BrowserOptions.getInstance()); diff --git a/chrome/browser/resources/options/options_bundle.js b/chrome/browser/resources/options/options_bundle.js index 34ede49..bfb23ed 100644 --- a/chrome/browser/resources/options/options_bundle.js +++ b/chrome/browser/resources/options/options_bundle.js @@ -103,6 +103,7 @@ var CertificateImportErrorOverlay = options.CertificateImportErrorOverlay; <include src="password_manager.js"> <include src="password_manager_list.js"> <include src="profiles_icon_grid.js"> +<include src="reset_profile_settings_banner.js"> <include src="reset_profile_settings_overlay.js"> <include src="search_engine_manager.js"> <include src="search_engine_manager_engine_list.js"> diff --git a/chrome/browser/resources/options/reset_profile_settings_banner.html b/chrome/browser/resources/options/reset_profile_settings_banner.html new file mode 100644 index 0000000..fd95c3c --- /dev/null +++ b/chrome/browser/resources/options/reset_profile_settings_banner.html @@ -0,0 +1,18 @@ +<div id="reset-profile-settings-banner" class="settings-banner" hidden> + <div id="reset-profile-settings-banner-close" class="close-button"></div> + <div class="content-area"> + <div class="badge"></div> + <div class="text"> + <p> + <span i18n-values=".innerHTML:resetProfileSettingsBannerText"> + </span> + <a class="nowrap" i18n-values="href:resetProfileSettingsLearnMoreUrl" + i18n-content="learnMore" target="_blank"></a> + </p> + </div> + <div class="button-area"> + <button id="reset-profile-settings-banner-activate" + i18n-content="resetProfileSettings"></button> + </div> + </div> +</div> diff --git a/chrome/browser/resources/options/reset_profile_settings_banner.js b/chrome/browser/resources/options/reset_profile_settings_banner.js new file mode 100644 index 0000000..e54be25 --- /dev/null +++ b/chrome/browser/resources/options/reset_profile_settings_banner.js @@ -0,0 +1,62 @@ +// 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. + +// Note: the native-side handler for this is ResetProfileSettingsHandler. + +cr.define('options', function() { + /** @const */ var PageManager = cr.ui.pageManager.PageManager; + /** @const */ var SettingsBannerBase = options.SettingsBannerBase; + + /** + * ResetProfileSettingsBanner class + * Provides encapsulated handling of the Reset Profile Settings banner. + * @constructor + */ + function ResetProfileSettingsBanner() {} + + cr.addSingletonGetter(ResetProfileSettingsBanner); + + ResetProfileSettingsBanner.prototype = { + __proto__: SettingsBannerBase.prototype, + + /** + * Initializes the banner's event handlers. + */ + initialize: function() { + this.showMetricName = 'AutomaticReset_WebUIBanner_BannerShown'; + + this.dismissNativeCallbackName = + 'onDismissedResetProfileSettingsBanner'; + + this.visibilityDomElement = $('reset-profile-settings-banner'); + + $('reset-profile-settings-banner-close').onclick = function(event) { + chrome.send('metricsHandler:recordAction', + ['AutomaticReset_WebUIBanner_ManuallyClosed']); + ResetProfileSettingsBanner.dismiss(); + }; + $('reset-profile-settings-banner-activate').onclick = function(event) { + chrome.send('metricsHandler:recordAction', + ['AutomaticReset_WebUIBanner_ResetClicked']); + PageManager.showPageByName('resetProfileSettings'); + }; + }, + }; + + // Forward public APIs to protected implementations. + [ + 'show', + 'dismiss', + ].forEach(function(name) { + ResetProfileSettingsBanner[name] = function() { + var instance = ResetProfileSettingsBanner.getInstance(); + return instance[name].apply(instance, arguments); + }; + }); + + // Export + return { + ResetProfileSettingsBanner: ResetProfileSettingsBanner + }; +}); diff --git a/chrome/browser/resources/options/reset_profile_settings_overlay.js b/chrome/browser/resources/options/reset_profile_settings_overlay.js index 3dd1b79..60cf57d 100644 --- a/chrome/browser/resources/options/reset_profile_settings_overlay.js +++ b/chrome/browser/resources/options/reset_profile_settings_overlay.js @@ -6,6 +6,7 @@ cr.define('options', function() { var Page = cr.ui.pageManager.Page; var AutomaticSettingsResetBanner = options.AutomaticSettingsResetBanner; + var ResetProfileSettingsBanner = options.ResetProfileSettingsBanner; /** * ResetProfileSettingsOverlay class @@ -45,6 +46,7 @@ cr.define('options', function() { /** @override */ didShowPage: function() { + ResetProfileSettingsBanner.dismiss(); chrome.send('onShowResetProfileDialog'); }, diff --git a/chrome/browser/resources/options/settings_banner.css b/chrome/browser/resources/options/settings_banner.css index 82a84d7..33ac8c5 100644 --- a/chrome/browser/resources/options/settings_banner.css +++ b/chrome/browser/resources/options/settings_banner.css @@ -2,8 +2,8 @@ * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ -/* TODO(engedy): These styles are used by automatic_settings_reset_banner.html - * only. Rename/refactor this into automatic_settings_reset_banner.css. */ +/* These styles are used by both reset_profile_settings_banner.html and + * automatic_settings_reset_banner.html. */ .settings-banner { background-color: #f5f5f5; diff --git a/chrome/browser/resources/options/settings_banner.js b/chrome/browser/resources/options/settings_banner.js index 5d030a4..666aaf2 100644 --- a/chrome/browser/resources/options/settings_banner.js +++ b/chrome/browser/resources/options/settings_banner.js @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// TODO(engedy): AutomaticSettingsResetBanner is the sole class to derive from -// SettingsBannerBase. Refactor this into automatic_settings_reset_banner.js. - cr.define('options', function() { /** @@ -36,26 +33,52 @@ cr.define('options', function() { /** * Metric name to send when a show event occurs. + * @protected */ - showMetricName_: '', + showMetricName: '', /** * Name of the native callback invoked when the banner is dismised. + * @protected */ - dismissNativeCallbackName_: '', + dismissNativeCallbackName: '', /** * DOM element whose visibility is set when setVisibility_ is called. + * @protected + */ + visibilityDomElement: null, + + /** + * Called by the native code to show the banner if needed. + * @protected */ - setVisibilibyDomElement_: null, + show: function() { + if (!this.hadBeenDismissed_) { + chrome.send('metricsHandler:recordAction', [this.showMetricName]); + this.setVisibility_(true); + } + }, + + /** + * Called when the banner should be closed as a result of something taking + * place on the WebUI page, i.e. when its close button is pressed, or when + * the confirmation dialog for the profile settings reset feature is opened. + * @protected + */ + dismiss: function() { + chrome.send(this.dismissNativeCallbackName); + this.hadBeenDismissed_ = true; + this.setVisibility_(false); + }, /** * Sets whether or not the reset profile settings banner shall be visible. * @param {boolean} show Whether or not to show the banner. - * @protected + * @private */ - setVisibility: function(show) { - this.setVisibilibyDomElement_.hidden = !show; + setVisibility_: function(show) { + this.visibilityDomElement.hidden = !show; }, }; diff --git a/chrome/browser/ui/profile_reset_bubble.h b/chrome/browser/ui/profile_reset_bubble.h new file mode 100644 index 0000000..ad0bb2e --- /dev/null +++ b/chrome/browser/ui/profile_reset_bubble.h @@ -0,0 +1,23 @@ +// 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_UI_PROFILE_RESET_BUBBLE_H_ +#define CHROME_BROWSER_UI_PROFILE_RESET_BUBBLE_H_ + +#include "base/memory/weak_ptr.h" + +class Browser; +class GlobalErrorBubbleViewBase; +class ProfileResetGlobalError; + +// Returns whether or not the profile reset bubble is supported on this +// platform. +bool IsProfileResetBubbleSupported(); + +// Shows the profile reset bubble on the platforms that support it. +GlobalErrorBubbleViewBase* ShowProfileResetBubble( + const base::WeakPtr<ProfileResetGlobalError>& global_error, + Browser* browser); + +#endif // CHROME_BROWSER_UI_PROFILE_RESET_BUBBLE_H_ diff --git a/chrome/browser/ui/profile_reset_bubble_stub.cc b/chrome/browser/ui/profile_reset_bubble_stub.cc new file mode 100644 index 0000000..d009660 --- /dev/null +++ b/chrome/browser/ui/profile_reset_bubble_stub.cc @@ -0,0 +1,21 @@ +// 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. + +// Definitions for chrome/browser/ui/profile_reset_bubble.h, for platforms where +// the profile reset bubble is not implemented yet. + +#include "chrome/browser/ui/profile_reset_bubble.h" + +#include "base/logging.h" + +bool IsProfileResetBubbleSupported() { + return false; +} + +GlobalErrorBubbleViewBase* ShowProfileResetBubble( + const base::WeakPtr<ProfileResetGlobalError>& global_error, + Browser* browser) { + NOTREACHED(); + return NULL; +} diff --git a/chrome/browser/ui/views/profiles/profile_reset_bubble_view.cc b/chrome/browser/ui/views/profiles/profile_reset_bubble_view.cc new file mode 100644 index 0000000..2b039f2 --- /dev/null +++ b/chrome/browser/ui/views/profiles/profile_reset_bubble_view.cc @@ -0,0 +1,447 @@ +// Copyright 2014 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/ui/views/profiles/profile_reset_bubble_view.h" + +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/profile_resetter/profile_reset_global_error.h" +#include "chrome/browser/profile_resetter/resettable_settings_snapshot.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/browser/ui/views/frame/browser_view.h" +#include "chrome/browser/ui/views/toolbar/toolbar_view.h" +#include "chrome/common/url_constants.h" +#include "components/google/core/browser/google_util.h" +#include "content/public/browser/page_navigator.h" +#include "content/public/browser/user_metrics.h" +#include "grit/chromium_strings.h" +#include "grit/components_strings.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/resource/resource_bundle.h" +#include "ui/gfx/image/image_skia_operations.h" +#include "ui/views/background.h" +#include "ui/views/controls/button/checkbox.h" +#include "ui/views/controls/button/image_button.h" +#include "ui/views/controls/button/label_button.h" +#include "ui/views/controls/label.h" +#include "ui/views/controls/link.h" +#include "ui/views/controls/scroll_view.h" +#include "ui/views/controls/separator.h" +#include "ui/views/layout/grid_layout.h" +#include "ui/views/layout/layout_constants.h" + +using views::GridLayout; + +namespace { + +// Fixed width of the column holding the description label of the bubble. +const int kWidthOfDescriptionText = 370; + +// Margins width for the top rows to compensate for the bottom panel for which +// we don't want any margin. +const int kMarginWidth = 12; +const int kMarginHeight = kMarginWidth; + +// Width of a colum in the FeedbackView. +const int kFeedbackViewColumnWidth = kWidthOfDescriptionText / 2 + kMarginWidth; + +// Width of the column used to disaplay the help button. +const int kHelpButtonColumnWidth = 30; + +// Width of the reporting checkbox column. +const int kReportingCheckboxColumnWidth = + kWidthOfDescriptionText + 2 * kMarginWidth; + +// Full width including all columns. +const int kAllColumnsWidth = + kReportingCheckboxColumnWidth + kHelpButtonColumnWidth; + +// Maximum height of the scrollable feedback view. +const int kMaxFeedbackViewHeight = 450; + +// The vertical padding between two values in the feedback view. +const int kInterFeedbackValuePadding = 4; + +// We subtract 2 to account for the natural button padding, and +// to bring the separation visually in line with the row separation +// height. +const int kButtonPadding = views::kRelatedButtonHSpacing - 2; + +// The color of the background of the sub panel to report current settings. +const SkColor kLightGrayBackgroundColor = 0xFFF5F5F5; + +// This view is used to contain the scrollable contents that are shown the user +// to expose what feedback will be sent back to Google. +class FeedbackView : public views::View { + public: + FeedbackView() {} + + // Setup the layout manager of the Feedback view using the content of the + // |feedback| ListValue which contains a list of key/value pairs stored in + // DictionaryValues. The key is to be displayed right aligned on the left, and + // the value as a left aligned multiline text on the right. + void SetupLayoutManager(const base::ListValue& feedback) { + RemoveAllChildViews(true); + set_background(views::Background::CreateSolidBackground( + kLightGrayBackgroundColor)); + + GridLayout* layout = new GridLayout(this); + SetLayoutManager(layout); + + // We only need a single column set for left/right text and middle margin. + views::ColumnSet* cs = layout->AddColumnSet(0); + cs->AddColumn(GridLayout::FILL, GridLayout::LEADING, 1, + GridLayout::FIXED, kFeedbackViewColumnWidth, 0); + cs->AddPaddingColumn(0, kMarginWidth); + cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 1, + GridLayout::FIXED, kFeedbackViewColumnWidth, 0); + for (size_t i = 0; i < feedback.GetSize(); ++i) { + const base::DictionaryValue* dictionary = NULL; + if (!feedback.GetDictionary(i, &dictionary) || !dictionary) + continue; + + base::string16 key; + if (!dictionary->GetString("key", &key)) + continue; + + base::string16 value; + if (!dictionary->GetString("value", &value)) + continue; + + // The key is shown on the left, multi-line (required to allow wrapping in + // case the key name does not fit), and right-aligned. + views::Label* left_text_label = new views::Label(key); + left_text_label->SetMultiLine(true); + left_text_label->SetEnabledColor(SK_ColorGRAY); + left_text_label->SetHorizontalAlignment(gfx::ALIGN_RIGHT); + + // The value is shown on the right, multi-line, left-aligned. + views::Label* right_text_label = new views::Label(value); + right_text_label->SetMultiLine(true); + right_text_label->SetEnabledColor(SK_ColorDKGRAY); + right_text_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); + + layout->StartRow(0, 0); + layout->AddView(left_text_label); + layout->AddView(right_text_label); + layout->AddPaddingRow(0, kInterFeedbackValuePadding); + } + + // We need to set our size to our preferred size because our parent is a + // scroll view and doesn't know which size to set us to. Also since our + // parent scrolls, we are not bound to its size. So our size is based on the + // size computed by the our layout manager, which is what + // SizeToPreferredSize() does. + SizeToPreferredSize(); + } + + private: + DISALLOW_COPY_AND_ASSIGN(FeedbackView); +}; + +} // namespace + +// ProfileResetBubbleView --------------------------------------------------- + +// static +ProfileResetBubbleView* ProfileResetBubbleView::ShowBubble( + const base::WeakPtr<ProfileResetGlobalError>& global_error, + Browser* browser) { + views::View* anchor_view = + BrowserView::GetBrowserViewForBrowser(browser)->toolbar()->app_menu(); + ProfileResetBubbleView* reset_bubble = new ProfileResetBubbleView( + global_error, anchor_view, browser, browser->profile()); + views::BubbleDelegateView::CreateBubble(reset_bubble)->Show(); + content::RecordAction(base::UserMetricsAction("SettingsResetBubble.Show")); + return reset_bubble; +} + +ProfileResetBubbleView::~ProfileResetBubbleView() {} + +views::View* ProfileResetBubbleView::GetInitiallyFocusedView() { + return controls_.reset_button; +} + +void ProfileResetBubbleView::WindowClosing() { + if (global_error_) + global_error_->OnBubbleViewDidClose(); +} + +ProfileResetBubbleView::ProfileResetBubbleView( + const base::WeakPtr<ProfileResetGlobalError>& global_error, + views::View* anchor_view, + content::PageNavigator* navigator, + Profile* profile) + : BubbleDelegateView(anchor_view, views::BubbleBorder::TOP_RIGHT), + navigator_(navigator), + profile_(profile), + global_error_(global_error), + resetting_(false), + chose_to_reset_(false), + show_help_pane_(false), + weak_factory_(this) { +} + +void ProfileResetBubbleView::ResetAllChildren() { + controls_.Reset(); + SetLayoutManager(NULL); + RemoveAllChildViews(true); +} + +void ProfileResetBubbleView::Init() { + set_margins(gfx::Insets(kMarginHeight, 0, 0, 0)); + // Start requesting the feedback data. + snapshot_.reset(new ResettableSettingsSnapshot(profile_)); + snapshot_->RequestShortcuts( + base::Bind(&ProfileResetBubbleView::UpdateFeedbackDetails, + weak_factory_.GetWeakPtr())); + SetupLayoutManager(true); +} + +void ProfileResetBubbleView::SetupLayoutManager(bool report_checked) { + ResetAllChildren(); + + base::string16 product_name( + l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)); + ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); + + // Bubble title label. + views::Label* title_label = new views::Label( + l10n_util::GetStringFUTF16(IDS_RESET_BUBBLE_TITLE, product_name), + rb.GetFontList(ui::ResourceBundle::BoldFont)); + title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); + + // Description text label. + views::Label* text_label = new views::Label( + l10n_util::GetStringFUTF16(IDS_RESET_BUBBLE_TEXT, product_name)); + text_label->SetMultiLine(true); + text_label->SetLineHeight(20); + text_label->SetEnabledColor(SK_ColorDKGRAY); + text_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); + + // Learn more link. + views::Link* learn_more_link = new views::Link( + l10n_util::GetStringUTF16(IDS_LEARN_MORE)); + learn_more_link->SetHorizontalAlignment(gfx::ALIGN_LEFT); + learn_more_link->set_listener(this); + learn_more_link->SetUnderline(false); + + // Reset button's name is based on |resetting_| state. + int reset_button_string_id = IDS_RESET_PROFILE_SETTINGS_COMMIT_BUTTON; + if (resetting_) + reset_button_string_id = IDS_RESETTING; + controls_.reset_button = new views::LabelButton( + this, l10n_util::GetStringUTF16(reset_button_string_id)); + controls_.reset_button->SetStyle(views::Button::STYLE_BUTTON); + controls_.reset_button->SetIsDefault(true); + controls_.reset_button->SetFontList( + rb.GetFontList(ui::ResourceBundle::BoldFont)); + controls_.reset_button->SetEnabled(!resetting_); + // For the Resetting... text to fit. + gfx::Size reset_button_size = controls_.reset_button->GetPreferredSize(); + reset_button_size.set_width(100); + controls_.reset_button->SetMinSize(reset_button_size); + + // No thanks button. + controls_.no_thanks_button = new views::LabelButton( + this, l10n_util::GetStringUTF16(IDS_NO_THANKS)); + controls_.no_thanks_button->SetStyle(views::Button::STYLE_BUTTON); + controls_.no_thanks_button->SetEnabled(!resetting_); + + // Checkbox for reporting settings or not. + controls_.report_settings_checkbox = new views::Checkbox( + l10n_util::GetStringUTF16(IDS_REPORT_BUBBLE_TEXT)); + controls_.report_settings_checkbox->SetTextColor( + views::Button::STATE_NORMAL, SK_ColorGRAY); + controls_.report_settings_checkbox->SetChecked(report_checked); + controls_.report_settings_checkbox->SetTextMultiLine(true); + controls_.report_settings_checkbox->set_background( + views::Background::CreateSolidBackground(kLightGrayBackgroundColor)); + // Have a smaller margin on the right, to have the |controls_.help_button| + // closer to the edge. + controls_.report_settings_checkbox->SetBorder( + views::Border::CreateSolidSidedBorder(kMarginWidth, + kMarginWidth, + kMarginWidth, + kMarginWidth / 2, + kLightGrayBackgroundColor)); + + // Help button to toggle the bottom panel on or off. + controls_.help_button = new views::ImageButton(this); + const gfx::ImageSkia* help_image = rb.GetImageSkiaNamed(IDR_QUESTION_MARK); + color_utils::HSL hsl_shift = { -1, 0, 0.8 }; + brighter_help_image_ = gfx::ImageSkiaOperations::CreateHSLShiftedImage( + *help_image, hsl_shift); + controls_.help_button->SetImage( + views::Button::STATE_NORMAL, &brighter_help_image_); + controls_.help_button->SetImage(views::Button::STATE_HOVERED, help_image); + controls_.help_button->SetImage(views::Button::STATE_PRESSED, help_image); + controls_.help_button->set_background( + views::Background::CreateSolidBackground(kLightGrayBackgroundColor)); + controls_.help_button->SetImageAlignment(views::ImageButton::ALIGN_CENTER, + views::ImageButton::ALIGN_MIDDLE); + + GridLayout* layout = new GridLayout(this); + SetLayoutManager(layout); + + // Title row. + const int kTitleColumnSetId = 0; + views::ColumnSet* cs = layout->AddColumnSet(kTitleColumnSetId); + cs->AddPaddingColumn(0, kMarginWidth); + cs->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0, + GridLayout::USE_PREF, 0, 0); + cs->AddPaddingColumn(0, kMarginWidth); + + // Text row. + const int kTextColumnSetId = 1; + cs = layout->AddColumnSet(kTextColumnSetId); + cs->AddPaddingColumn(0, kMarginWidth); + cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 0, + GridLayout::FIXED, kWidthOfDescriptionText, 0); + cs->AddPaddingColumn(0, kMarginWidth); + + // Learn more link & buttons row. + const int kButtonsColumnSetId = 2; + cs = layout->AddColumnSet(kButtonsColumnSetId); + cs->AddPaddingColumn(0, kMarginWidth); + cs->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0, + GridLayout::USE_PREF, 0, 0); + cs->AddPaddingColumn(1, views::kRelatedControlHorizontalSpacing); + cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0, + GridLayout::USE_PREF, 0, 0); + cs->AddPaddingColumn(0, kButtonPadding); + cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0, + GridLayout::USE_PREF, 0, 0); + cs->AddPaddingColumn(0, kMarginWidth); + + // Separator. + const int kSeparatorColumnSetId = 3; + cs = layout->AddColumnSet(kSeparatorColumnSetId); + cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 0, + GridLayout::FIXED, kAllColumnsWidth, 0); + + // Reporting row. + const int kReportColumnSetId = 4; + cs = layout->AddColumnSet(kReportColumnSetId); + cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 0, + GridLayout::FIXED, kReportingCheckboxColumnWidth, 0); + cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 0, + GridLayout::FIXED, kHelpButtonColumnWidth, 0); + + layout->StartRow(0, kTitleColumnSetId); + layout->AddView(title_label); + layout->AddPaddingRow(0, kMarginHeight); + + layout->StartRow(0, kTextColumnSetId); + layout->AddView(text_label); + layout->AddPaddingRow(0, kMarginHeight); + + layout->StartRow(0, kButtonsColumnSetId); + layout->AddView(learn_more_link); + layout->AddView(controls_.reset_button); + layout->AddView(controls_.no_thanks_button); + layout->AddPaddingRow(0, kMarginHeight); + + layout->StartRow(0, kSeparatorColumnSetId); + layout->AddView(new views::Separator(views::Separator::HORIZONTAL)); + + layout->StartRow(0, kReportColumnSetId); + layout->AddView(controls_.report_settings_checkbox); + layout->AddView(controls_.help_button); + + if (show_help_pane_ && snapshot_) { + // We need a single row to add the scroll view containing the feedback. + const int kReportDetailsColumnSetId = 5; + cs = layout->AddColumnSet(kReportDetailsColumnSetId); + cs->AddColumn(GridLayout::FILL, GridLayout::FILL, 1, + GridLayout::USE_PREF, 0, 0); + + FeedbackView* feedback_view = new FeedbackView(); + scoped_ptr<base::ListValue> feedback_data = + GetReadableFeedbackForSnapshot(profile_, *snapshot_); + feedback_view->SetupLayoutManager(*feedback_data); + + views::ScrollView* scroll_view = new views::ScrollView(); + scroll_view->set_background(views::Background::CreateSolidBackground( + kLightGrayBackgroundColor)); + scroll_view->SetContents(feedback_view); + + layout->StartRow(1, kReportDetailsColumnSetId); + layout->AddView(scroll_view, 1, 1, GridLayout::FILL, + GridLayout::FILL, kAllColumnsWidth, + std::min(feedback_view->height() + kMarginHeight, + kMaxFeedbackViewHeight)); + } + + Layout(); + AddAccelerator(ui::Accelerator(ui::VKEY_RETURN, ui::EF_NONE)); +} + +void ProfileResetBubbleView::ButtonPressed(views::Button* sender, + const ui::Event& event) { + if (sender == controls_.reset_button) { + DCHECK(!resetting_); + content::RecordAction( + base::UserMetricsAction("SettingsResetBubble.Reset")); + + // Remember that the user chose to reset, and that resetting is underway. + chose_to_reset_ = true; + resetting_ = true; + + controls_.reset_button->SetText(l10n_util::GetStringUTF16(IDS_RESETTING)); + controls_.reset_button->SetEnabled(false); + controls_.no_thanks_button->SetEnabled(false); + SchedulePaint(); + + if (global_error_) { + global_error_->OnBubbleViewResetButtonPressed( + controls_.report_settings_checkbox->checked()); + } + } else if (sender == controls_.no_thanks_button) { + DCHECK(!resetting_); + content::RecordAction( + base::UserMetricsAction("SettingsResetBubble.NoThanks")); + + if (global_error_) + global_error_->OnBubbleViewNoThanksButtonPressed(); + GetWidget()->Close(); + return; + } else if (sender == controls_.help_button) { + show_help_pane_ = !show_help_pane_; + + SetupLayoutManager(controls_.report_settings_checkbox->checked()); + SizeToContents(); + } +} + +void ProfileResetBubbleView::LinkClicked(views::Link* source, int flags) { + content::RecordAction( + base::UserMetricsAction("SettingsResetBubble.LearnMore")); + navigator_->OpenURL(content::OpenURLParams( + GURL(chrome::kResetProfileSettingsLearnMoreURL), content::Referrer(), + NEW_FOREGROUND_TAB, content::PAGE_TRANSITION_LINK, false)); +} + +void ProfileResetBubbleView::CloseBubbleView() { + resetting_ = false; + GetWidget()->Close(); +} + +void ProfileResetBubbleView::UpdateFeedbackDetails() { + if (show_help_pane_) + SetupLayoutManager(controls_.report_settings_checkbox->checked()); +} + +bool IsProfileResetBubbleSupported() { + return true; +} + +GlobalErrorBubbleViewBase* ShowProfileResetBubble( + const base::WeakPtr<ProfileResetGlobalError>& global_error, + Browser* browser) { + return ProfileResetBubbleView::ShowBubble(global_error, browser); +} diff --git a/chrome/browser/ui/views/profiles/profile_reset_bubble_view.h b/chrome/browser/ui/views/profiles/profile_reset_bubble_view.h new file mode 100644 index 0000000..454bb2f --- /dev/null +++ b/chrome/browser/ui/views/profiles/profile_reset_bubble_view.h @@ -0,0 +1,138 @@ +// Copyright 2014 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_UI_VIEWS_PROFILES_PROFILE_RESET_BUBBLE_VIEW_H_ +#define CHROME_BROWSER_UI_VIEWS_PROFILES_PROFILE_RESET_BUBBLE_VIEW_H_ + +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/values.h" +#include "chrome/browser/ui/global_error/global_error_bubble_view_base.h" +#include "ui/gfx/image/image_skia.h" +#include "ui/views/bubble/bubble_delegate.h" +#include "ui/views/controls/button/button.h" +#include "ui/views/controls/link_listener.h" + +namespace content { +class PageNavigator; +} + +namespace views { +class Checkbox; +class ImageButton; +class LabelButton; +class Link; +} + +class Browser; +class Profile; +class ProfileResetGlobalError; +class ResettableSettingsSnapshot; + +// ProfileResetBubbleView warns the user that a settings reset might be needed. +// It is intended to be used as the content of a bubble anchored off of the +// Chrome toolbar. +class ProfileResetBubbleView : public views::BubbleDelegateView, + public views::ButtonListener, + public views::LinkListener, + public GlobalErrorBubbleViewBase { + public: + static ProfileResetBubbleView* ShowBubble( + const base::WeakPtr<ProfileResetGlobalError>& global_error, + Browser* browser); + + // views::BubbleDelegateView methods. + virtual views::View* GetInitiallyFocusedView() OVERRIDE; + virtual void Init() OVERRIDE; + + // views::WidgetDelegate method. + virtual void WindowClosing() OVERRIDE; + + // GlobalErrorBubbleViewBase: + virtual void CloseBubbleView() OVERRIDE; + + private: + ProfileResetBubbleView( + const base::WeakPtr<ProfileResetGlobalError>& global_error, + views::View* anchor_view, + content::PageNavigator* navigator, + Profile* profile); + + virtual ~ProfileResetBubbleView(); + + // Reset all child views members and remove children from view hierarchy. + void ResetAllChildren(); + + // Sets up the layout manager and set the report checkbox to the value passed + // in |report_checked|. + void SetupLayoutManager(bool report_checked); + + // views::ButtonListener method. + virtual void ButtonPressed(views::Button* sender, + const ui::Event& event) OVERRIDE; + + // views::LinkListener method. + virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE; + + // Sets the fully populated feedback data. + void UpdateFeedbackDetails(); + + struct Controls { + Controls() { + Reset(); + } + void Reset() { + reset_button = NULL; + no_thanks_button = NULL; + help_button = NULL; + report_settings_checkbox = NULL; + } + + // Button for the user to confirm a settings reset. + views::LabelButton* reset_button; + + // Button for the user to refuse a settings reset. + views::LabelButton* no_thanks_button; + + // Button for the user to get more info about reporting settings. + views::ImageButton* help_button; + + // Checkbox for the user to choose to report the settings or not. + views::Checkbox* report_settings_checkbox; + } controls_; + + // The snapshot is used to show user feedback information. + scoped_ptr<ResettableSettingsSnapshot> snapshot_; + + // A version of the help image that is brighter. + gfx::ImageSkia brighter_help_image_; + + // Used for opening the learn more link. + content::PageNavigator* navigator_; + + // Used to access profile specific stuff like the global error or readable + // feedback. + Profile* profile_; + + // The GlobalError this Bubble belongs to. + base::WeakPtr<ProfileResetGlobalError> global_error_; + + // Remembers if we are currently resetting or not. + bool resetting_; + + // Remembers if the reset button was hit before closing the bubble. + bool chose_to_reset_; + + // Toggles when the user clicks on the |help_button_| to identify if we should + // show the help pane or not. + bool show_help_pane_; + + // To cancel pending callbacks after destruction. + base::WeakPtrFactory<ProfileResetBubbleView> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(ProfileResetBubbleView); +}; + +#endif // CHROME_BROWSER_UI_VIEWS_PROFILES_PROFILE_RESET_BUBBLE_VIEW_H_ diff --git a/chrome/browser/ui/webui/options/reset_profile_settings_handler.cc b/chrome/browser/ui/webui/options/reset_profile_settings_handler.cc index cd4db5e..86bacf3 100644 --- a/chrome/browser/ui/webui/options/reset_profile_settings_handler.cc +++ b/chrome/browser/ui/webui/options/reset_profile_settings_handler.cc @@ -11,6 +11,8 @@ #include "base/strings/string16.h" #include "base/values.h" #include "chrome/browser/google/google_brand.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter.h" +#include "chrome/browser/profile_resetter/automatic_profile_resetter_factory.h" #include "chrome/browser/profile_resetter/brandcode_config_fetcher.h" #include "chrome/browser/profile_resetter/brandcoded_default_settings.h" #include "chrome/browser/profile_resetter/profile_resetter.h" @@ -24,7 +26,9 @@ namespace options { -ResetProfileSettingsHandler::ResetProfileSettingsHandler() { +ResetProfileSettingsHandler::ResetProfileSettingsHandler() + : automatic_profile_resetter_(NULL), + has_shown_confirmation_dialog_(false) { google_brand::GetBrand(&brandcode_); } @@ -33,12 +37,25 @@ ResetProfileSettingsHandler::~ResetProfileSettingsHandler() {} void ResetProfileSettingsHandler::InitializeHandler() { Profile* profile = Profile::FromWebUI(web_ui()); resetter_.reset(new ProfileResetter(profile)); + automatic_profile_resetter_ = + AutomaticProfileResetterFactory::GetForBrowserContext(profile); } void ResetProfileSettingsHandler::InitializePage() { web_ui()->CallJavascriptFunction( "ResetProfileSettingsOverlay.setResettingState", base::FundamentalValue(resetter_->IsActive())); + if (automatic_profile_resetter_ && + automatic_profile_resetter_->ShouldShowResetBanner()) { + web_ui()->CallJavascriptFunction("ResetProfileSettingsBanner.show"); + } +} + +void ResetProfileSettingsHandler::Uninitialize() { + if (has_shown_confirmation_dialog_ && automatic_profile_resetter_) { + automatic_profile_resetter_->NotifyDidCloseWebUIResetDialog( + false /*performed_reset*/); + } } void ResetProfileSettingsHandler::GetLocalizedValues( @@ -46,6 +63,8 @@ void ResetProfileSettingsHandler::GetLocalizedValues( DCHECK(localized_strings); static OptionsStringResource resources[] = { + { "resetProfileSettingsBannerText", + IDS_RESET_PROFILE_SETTINGS_BANNER_TEXT }, { "resetProfileSettingsCommit", IDS_RESET_PROFILE_SETTINGS_COMMIT_BUTTON }, { "resetProfileSettingsExplanation", IDS_RESET_PROFILE_SETTINGS_EXPLANATION }, @@ -71,6 +90,10 @@ void ResetProfileSettingsHandler::RegisterMessages() { web_ui()->RegisterMessageCallback("onHideResetProfileDialog", base::Bind(&ResetProfileSettingsHandler::OnHideResetProfileDialog, base::Unretained(this))); + web_ui()->RegisterMessageCallback("onDismissedResetProfileSettingsBanner", + base::Bind(&ResetProfileSettingsHandler:: + OnDismissedResetProfileSettingsBanner, + base::Unretained(this))); } void ResetProfileSettingsHandler::HandleResetProfileSettings( @@ -102,10 +125,17 @@ void ResetProfileSettingsHandler::OnResetProfileSettingsDone( setting_snapshot_->Subtract(current_snapshot); std::string report = SerializeSettingsReport(*setting_snapshot_, difference); - SendSettingsFeedback(report, profile); + bool is_reset_prompt_active = automatic_profile_resetter_ && + automatic_profile_resetter_->IsResetPromptFlowActive(); + SendSettingsFeedback(report, profile, is_reset_prompt_active ? + PROFILE_RESET_PROMPT : PROFILE_RESET_WEBUI); } } setting_snapshot_.reset(); + if (automatic_profile_resetter_) { + automatic_profile_resetter_->NotifyDidCloseWebUIResetDialog( + true /*performed_reset*/); + } } void ResetProfileSettingsHandler::OnShowResetProfileDialog( @@ -118,6 +148,10 @@ void ResetProfileSettingsHandler::OnShowResetProfileDialog( UpdateFeedbackUI(); } + if (automatic_profile_resetter_) + automatic_profile_resetter_->NotifyDidOpenWebUIResetDialog(); + has_shown_confirmation_dialog_ = true; + if (brandcode_.empty()) return; config_fetcher_.reset(new BrandcodeConfigFetcher( @@ -133,6 +167,12 @@ void ResetProfileSettingsHandler::OnHideResetProfileDialog( setting_snapshot_.reset(); } +void ResetProfileSettingsHandler::OnDismissedResetProfileSettingsBanner( + const base::ListValue* args) { + if (automatic_profile_resetter_) + automatic_profile_resetter_->NotifyDidCloseWebUIResetBanner(); +} + void ResetProfileSettingsHandler::OnSettingsFetched() { DCHECK(config_fetcher_); DCHECK(!config_fetcher_->IsActive()); diff --git a/chrome/browser/ui/webui/options/reset_profile_settings_handler.h b/chrome/browser/ui/webui/options/reset_profile_settings_handler.h index 8c1528c..997f681 100644 --- a/chrome/browser/ui/webui/options/reset_profile_settings_handler.h +++ b/chrome/browser/ui/webui/options/reset_profile_settings_handler.h @@ -17,6 +17,7 @@ class DictionaryValue; class ListValue; } // namespace base +class AutomaticProfileResetter; class BrandcodeConfigFetcher; class ProfileResetter; class ResettableSettingsSnapshot; @@ -37,6 +38,7 @@ class ResetProfileSettingsHandler base::DictionaryValue* localized_strings) OVERRIDE; virtual void InitializeHandler() OVERRIDE; virtual void InitializePage() OVERRIDE; + virtual void Uninitialize() OVERRIDE; // WebUIMessageHandler implementation. virtual void RegisterMessages() OVERRIDE; @@ -54,6 +56,9 @@ class ResetProfileSettingsHandler // Called when the confirmation box disappears. void OnHideResetProfileDialog(const base::ListValue* value); + // Called when the reset banner is dismissed from the WebUI. + void OnDismissedResetProfileSettingsBanner(const base::ListValue* args); + // Called when BrandcodeConfigFetcher completed fetching settings. void OnSettingsFetched(); @@ -64,6 +69,14 @@ class ResetProfileSettingsHandler // Sets new values for the feedback area. void UpdateFeedbackUI(); + // Destroyed with the Profile, thus it should outlive us. This will be NULL if + // the underlying profile is off-the-record (e.g. in Guest mode on Chrome OS). + AutomaticProfileResetter* automatic_profile_resetter_; + + // Records whether or not the Profile Reset confirmation dialog was opened at + // least once during the lifetime of the settings page. + bool has_shown_confirmation_dialog_; + scoped_ptr<ProfileResetter> resetter_; scoped_ptr<BrandcodeConfigFetcher> config_fetcher_; diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 3dd8d103..bed6356d 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -2093,10 +2093,25 @@ 'browser/process_singleton_posix.cc', 'browser/process_singleton_startup_lock.cc', 'browser/process_singleton_startup_lock.h', + 'browser/profile_resetter/automatic_profile_resetter.cc', + 'browser/profile_resetter/automatic_profile_resetter_delegate.cc', + 'browser/profile_resetter/automatic_profile_resetter_delegate.h', + 'browser/profile_resetter/automatic_profile_resetter_factory.cc', + 'browser/profile_resetter/automatic_profile_resetter_factory.h', + 'browser/profile_resetter/automatic_profile_resetter.h', + 'browser/profile_resetter/automatic_profile_resetter_mementos.cc', + 'browser/profile_resetter/automatic_profile_resetter_mementos.h', 'browser/profile_resetter/brandcode_config_fetcher.cc', 'browser/profile_resetter/brandcode_config_fetcher.h', 'browser/profile_resetter/brandcoded_default_settings.cc', 'browser/profile_resetter/brandcoded_default_settings.h', + 'browser/profile_resetter/jtl_foundation.cc', + 'browser/profile_resetter/jtl_foundation.h', + 'browser/profile_resetter/jtl_instructions.h', + 'browser/profile_resetter/jtl_interpreter.cc', + 'browser/profile_resetter/jtl_interpreter.h', + 'browser/profile_resetter/profile_reset_global_error.cc', + 'browser/profile_resetter/profile_reset_global_error.h', 'browser/profile_resetter/profile_resetter.cc', 'browser/profile_resetter/profile_resetter.h', 'browser/profile_resetter/resettable_settings_snapshot.cc', diff --git a/chrome/chrome_browser_ui.gypi b/chrome/chrome_browser_ui.gypi index 5fddda7..6fc17b9 100644 --- a/chrome/chrome_browser_ui.gypi +++ b/chrome/chrome_browser_ui.gypi @@ -864,6 +864,7 @@ 'browser/ui/settings_window_manager.cc', 'browser/ui/settings_window_manager.h', 'browser/ui/settings_window_manager_observer.h', + 'browser/ui/profile_reset_bubble.h', 'browser/ui/simple_message_box.h', 'browser/ui/status_bubble.h', 'browser/ui/sync/inline_login_dialog.cc', @@ -2175,6 +2176,8 @@ 'browser/ui/views/profiles/new_avatar_button.h', 'browser/ui/views/profiles/profile_chooser_view.cc', 'browser/ui/views/profiles/profile_chooser_view.h', + 'browser/ui/views/profiles/profile_reset_bubble_view.cc', + 'browser/ui/views/profiles/profile_reset_bubble_view.h', 'browser/ui/views/profiles/user_manager_view.cc', 'browser/ui/views/profiles/user_manager_view.h', 'browser/ui/views/renderer_context_menu/render_view_context_menu_views.cc', @@ -2313,6 +2316,9 @@ 'browser/ui/webui/gesture_config_ui.h', 'browser/ui/window_sizer/window_sizer_aura.cc', ], + 'chrome_browser_ui_non_aura_sources': [ + 'browser/ui/profile_reset_bubble_stub.cc', + ], # Aura but not ChromeOS. 'chrome_browser_ui_aura_non_chromeos': [ 'browser/ui/aura/active_desktop_monitor.cc', @@ -2766,6 +2772,8 @@ 'sources': [ '<@(chrome_browser_ui_aura_non_chromeos)' ], }], ], + }, { # else: use_aura==0 + 'sources': [ '<@(chrome_browser_ui_non_aura_sources)' ], }], ['ui_compositor_image_transport==1', { 'dependencies': [ diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index 605eced..7b4222a 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -2528,6 +2528,9 @@ ], }], ['OS!="android" and OS!="ios"', { + 'dependencies': [ + 'tools/profile_reset/jtl_compiler.gyp:jtl_compiler_lib', + ], 'sources!': [ 'browser/metrics/variations/variations_request_scheduler_mobile_unittest.cc', 'browser/net/spdyproxy/data_reduction_proxy_settings_unittest.cc', diff --git a/chrome/common/chrome_constants.cc b/chrome/common/chrome_constants.cc index 523acc7..1b0b5cc 100644 --- a/chrome/common/chrome_constants.cc +++ b/chrome/common/chrome_constants.cc @@ -167,6 +167,8 @@ const base::FilePath::CharType kPreferencesFilename[] = FPL("Preferences"); const base::FilePath::CharType kProtectedPreferencesFilenameDeprecated[] = FPL("Protected Preferences"); const base::FilePath::CharType kReadmeFilename[] = FPL("README"); +const base::FilePath::CharType kResetPromptMementoFilename[] = + FPL("Reset Prompt Memento"); const base::FilePath::CharType kSafeBrowsingBaseFilename[] = FPL("Safe Browsing"); const base::FilePath::CharType kSecurePreferencesFilename[] = diff --git a/chrome/common/chrome_constants.h b/chrome/common/chrome_constants.h index a750bed..3d2fd7b5 100644 --- a/chrome/common/chrome_constants.h +++ b/chrome/common/chrome_constants.h @@ -86,6 +86,7 @@ extern const base::FilePath::CharType kNewTabThumbnailsFilename[]; extern const base::FilePath::CharType kPreferencesFilename[]; extern const base::FilePath::CharType kProtectedPreferencesFilenameDeprecated[]; extern const base::FilePath::CharType kReadmeFilename[]; +extern const base::FilePath::CharType kResetPromptMementoFilename[]; extern const base::FilePath::CharType kSafeBrowsingBaseFilename[]; extern const base::FilePath::CharType kSecurePreferencesFilename[]; extern const base::FilePath::CharType kServiceStateFileName[]; diff --git a/chrome/common/pref_names.cc b/chrome/common/pref_names.cc index 8a104aa..52560a9 100644 --- a/chrome/common/pref_names.cc +++ b/chrome/common/pref_names.cc @@ -1176,6 +1176,11 @@ const char kSupervisedUserCreationAllowed[] = // List pref containing the users supervised by this user. const char kSupervisedUsers[] = "profile.managed_users"; +// String that indicates that the profile reset prompt has already been shown to +// the user (profile). +const char kProfileResetPromptMementoInProfilePrefs[] = + "profile.reset_prompt_memento"; + // List pref containing the extension ids which are not allowed to send // notifications to the message center. const char kMessageCenterDisabledExtensionIds[] = @@ -1272,6 +1277,14 @@ const char kProfileCreatedByVersion[] = "profile.created_by_version"; // them. const char kProfileInfoCache[] = "profile.info_cache"; +// Dictionary that maps profile keys to strings that indicate that the profile +// reset prompt has already been shown to the corresponding user (profile). +// This is semantically similar to kProfileResetPromptMementoInProfilePrefs, see +// chrome/browser/profile_resetter/automatic_profile_resetter_mementos.h for an +// explanation of why this redundancy is needed. +const char kProfileResetPromptMementosInLocalState[] = + "profile.reset_prompt_mementos"; + // Prefs for SSLConfigServicePref. const char kCertRevocationCheckingEnabled[] = "ssl.rev_checking.enabled"; const char kCertRevocationCheckingRequiredLocalAnchors[] = diff --git a/chrome/common/pref_names.h b/chrome/common/pref_names.h index 83e6cac..c3f142d 100644 --- a/chrome/common/pref_names.h +++ b/chrome/common/pref_names.h @@ -374,6 +374,8 @@ extern const char kDefaultSupervisedUserFilteringBehavior[]; extern const char kSupervisedUserCreationAllowed[]; extern const char kSupervisedUsers[]; +extern const char kProfileResetPromptMementoInProfilePrefs[]; + extern const char kMessageCenterDisabledExtensionIds[]; extern const char kMessageCenterDisabledSystemComponentIds[]; extern const char kWelcomeNotificationDismissed[]; @@ -436,6 +438,7 @@ extern const char kProfilesLastActive[]; extern const char kProfilesNumCreated[]; extern const char kProfileInfoCache[]; extern const char kProfileCreatedByVersion[]; +extern const char kProfileResetPromptMementosInLocalState[]; extern const char kStabilityPageLoadCount[]; extern const char kStabilityRendererCrashCount[]; diff --git a/chrome/tools/profile_reset/OWNERS b/chrome/tools/profile_reset/OWNERS new file mode 100644 index 0000000..de6863e --- /dev/null +++ b/chrome/tools/profile_reset/OWNERS @@ -0,0 +1,3 @@ +battre@chromium.org +engedy@chromium.org + diff --git a/chrome/tools/profile_reset/jtl_compiler.cc b/chrome/tools/profile_reset/jtl_compiler.cc new file mode 100644 index 0000000..ffa5ec2 --- /dev/null +++ b/chrome/tools/profile_reset/jtl_compiler.cc @@ -0,0 +1,252 @@ +// 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/tools/profile_reset/jtl_compiler.h" + +#include <limits> +#include <map> +#include <numeric> + +#include "base/logging.h" +#include "chrome/browser/profile_resetter/jtl_foundation.h" +#include "chrome/tools/profile_reset/jtl_parser.h" + +namespace jtl = jtl_foundation; + +namespace { + +// Serializes symbols into byte-code in a streaming manner. +class ByteCodeWriter { + public: + explicit ByteCodeWriter(std::string* output) : output_(output) {} + ~ByteCodeWriter() {} + + void WriteUint8(uint8 value) { output_->push_back(static_cast<char>(value)); } + void WriteUint32(uint32 value) { + for (int i = 0; i < 4; ++i) { + output_->push_back(static_cast<char>(value & 0xFFu)); + value >>= 8; + } + } + void WriteOpCode(uint8 op_code) { WriteUint8(op_code); } + void WriteHash(const std::string& hash) { + CHECK(jtl::Hasher::IsHash(hash)); + *output_ += hash; + } + void WriteBool(bool value) { WriteUint8(value ? 1u : 0u); } + + private: + std::string* output_; + + DISALLOW_COPY_AND_ASSIGN(ByteCodeWriter); +}; + +// Encapsulates meta-data about all instructions, and is capable of transcoding +// each instruction from a parsed text-based format to byte-code. +class InstructionSet { + public: + InstructionSet() { + // Define each instruction in this list. + // Note: + // - Instructions ending in "hash" will write their 'HashString' arguments + // directly into the byte-code. + // - Instructions ending in "hashed" will first hash their 'String' + // arguments, and will write this hash to the byte-code. + Add(Instruction("go", jtl::NAVIGATE, Arguments(String))); + Add(Instruction("any", jtl::NAVIGATE_ANY, Arguments())); + Add(Instruction("back", jtl::NAVIGATE_BACK, Arguments())); + Add(Instruction("store_bool", jtl::STORE_BOOL, Arguments(String, Bool))); + Add(Instruction("store_hash", + jtl::STORE_HASH, Arguments(String, HashString))); + Add(Instruction("store_hashed", + jtl::STORE_HASH, Arguments(String, String))); + Add(Instruction("store_node_bool", + jtl::STORE_NODE_BOOL, Arguments(String))); + Add(Instruction("store_node_hash", + jtl::STORE_NODE_HASH, Arguments(String))); + Add(Instruction("store_node_registerable_domain_hash", + jtl::STORE_NODE_REGISTERABLE_DOMAIN_HASH, + Arguments(String))); + Add(Instruction("compare_bool", jtl::COMPARE_NODE_BOOL, Arguments(Bool))); + Add(Instruction("compare_hashed", + jtl::COMPARE_NODE_HASH, Arguments(String))); + Add(Instruction("compare_hashed_not", + jtl::COMPARE_NODE_HASH_NOT, Arguments(String))); + Add(Instruction("compare_stored_bool", + jtl::COMPARE_STORED_BOOL, + Arguments(String, Bool, Bool))); + Add(Instruction("compare_stored_hashed", + jtl::COMPARE_STORED_HASH, + Arguments(String, String, String))); + Add(Instruction("compare_to_stored_bool", + jtl::COMPARE_NODE_TO_STORED_BOOL, + Arguments(String))); + Add(Instruction("compare_to_stored_hash", + jtl::COMPARE_NODE_TO_STORED_HASH, + Arguments(String))); + Add(Instruction("compare_substring_hashed", + jtl::COMPARE_NODE_SUBSTRING, + Arguments(StringPattern))); + Add(Instruction("break", jtl::STOP_EXECUTING_SENTENCE, Arguments())); + } + + JtlCompiler::CompileError::ErrorCode TranscodeInstruction( + const std::string& name, + const base::ListValue& arguments, + bool ends_sentence, + const jtl::Hasher& hasher, + ByteCodeWriter* target) const { + if (instruction_map_.count(name) == 0) + return JtlCompiler::CompileError::INVALID_OPERATION_NAME; + const Instruction& instruction(instruction_map_.at(name)); + if (instruction.argument_types.size() != arguments.GetSize()) + return JtlCompiler::CompileError::INVALID_ARGUMENT_COUNT; + target->WriteOpCode(instruction.op_code); + for (size_t i = 0; i < arguments.GetSize(); ++i) { + switch (instruction.argument_types[i]) { + case Bool: { + bool value = false; + if (!arguments.GetBoolean(i, &value)) + return JtlCompiler::CompileError::INVALID_ARGUMENT_TYPE; + target->WriteBool(value); + break; + } + case String: { + std::string value; + if (!arguments.GetString(i, &value)) + return JtlCompiler::CompileError::INVALID_ARGUMENT_TYPE; + target->WriteHash(hasher.GetHash(value)); + break; + } + case StringPattern: { + std::string value; + if (!arguments.GetString(i, &value)) + return JtlCompiler::CompileError::INVALID_ARGUMENT_TYPE; + if (value.empty() || + value.size() > std::numeric_limits<uint32>::max()) + return JtlCompiler::CompileError::INVALID_ARGUMENT_VALUE; + target->WriteHash(hasher.GetHash(value)); + target->WriteUint32(static_cast<uint32>(value.size())); + uint32 pattern_sum = std::accumulate( + value.begin(), value.end(), static_cast<uint32>(0u)); + target->WriteUint32(pattern_sum); + break; + } + case HashString: { + std::string hash_value; + if (!arguments.GetString(i, &hash_value) || + !jtl::Hasher::IsHash(hash_value)) + return JtlCompiler::CompileError::INVALID_ARGUMENT_TYPE; + target->WriteHash(hash_value); + break; + } + default: + NOTREACHED(); + return JtlCompiler::CompileError::INVALID_ARGUMENT_TYPE; + } + } + if (ends_sentence) + target->WriteOpCode(jtl::END_OF_SENTENCE); + return JtlCompiler::CompileError::ERROR_NONE; + } + + private: + // The possible types of an operation's argument. + enum ArgumentType { + None, + Bool, + String, + StringPattern, + HashString + }; + + // Encapsulates meta-data about one instruction. + struct Instruction { + Instruction() : op_code(jtl::END_OF_SENTENCE) {} + Instruction(const char* name, + jtl_foundation::OpCodes op_code, + const std::vector<ArgumentType>& argument_types) + : name(name), op_code(op_code), argument_types(argument_types) {} + + std::string name; + jtl::OpCodes op_code; + std::vector<ArgumentType> argument_types; + }; + + static std::vector<ArgumentType> Arguments(ArgumentType arg1_type = None, + ArgumentType arg2_type = None, + ArgumentType arg3_type = None) { + std::vector<ArgumentType> result; + if (arg1_type != None) + result.push_back(arg1_type); + if (arg2_type != None) + result.push_back(arg2_type); + if (arg3_type != None) + result.push_back(arg3_type); + return result; + } + + void Add(const Instruction& instruction) { + instruction_map_[instruction.name] = instruction; + } + + std::map<std::string, Instruction> instruction_map_; + + DISALLOW_COPY_AND_ASSIGN(InstructionSet); +}; + +} // namespace + +bool JtlCompiler::Compile(const std::string& source_code, + const std::string& hash_seed, + std::string* output_bytecode, + CompileError* error_details) { + DCHECK(output_bytecode); + InstructionSet instruction_set; + ByteCodeWriter bytecode_writer(output_bytecode); + jtl::Hasher hasher(hash_seed); + + std::string compacted_source_code; + std::vector<size_t> newline_indices; + size_t mismatched_quotes_line; + if (!JtlParser::RemoveCommentsAndAllWhitespace(source_code, + &compacted_source_code, + &newline_indices, + &mismatched_quotes_line)) { + if (error_details) { + error_details->context = ""; // No meaningful intra-line context here. + error_details->line_number = mismatched_quotes_line; + error_details->error_code = CompileError::MISMATCHED_DOUBLE_QUOTES; + } + return false; + } + + JtlParser parser(compacted_source_code, newline_indices); + while (!parser.HasFinished()) { + std::string operation_name; + base::ListValue arguments; + bool ends_sentence = false; + if (!parser.ParseNextOperation( + &operation_name, &arguments, &ends_sentence)) { + if (error_details) { + error_details->context = parser.GetLastContext(); + error_details->line_number = parser.GetLastLineNumber(); + error_details->error_code = CompileError::PARSING_ERROR; + } + return false; + } + CompileError::ErrorCode error_code = instruction_set.TranscodeInstruction( + operation_name, arguments, ends_sentence, hasher, &bytecode_writer); + if (error_code != CompileError::ERROR_NONE) { + if (error_details) { + error_details->context = parser.GetLastContext(); + error_details->line_number = parser.GetLastLineNumber(); + error_details->error_code = error_code; + } + return false; + } + } + + return true; +} diff --git a/chrome/tools/profile_reset/jtl_compiler.gyp b/chrome/tools/profile_reset/jtl_compiler.gyp new file mode 100644 index 0000000..6d21015 --- /dev/null +++ b/chrome/tools/profile_reset/jtl_compiler.gyp @@ -0,0 +1,38 @@ +{ + 'variables': { + 'chromium_code': 1, + }, + 'targets': [ + { + 'target_name': 'jtl_compiler', + 'type': 'executable', + 'dependencies': [ + '../../../base/base.gyp:base', + '../../../crypto/crypto.gyp:crypto', + 'jtl_compiler_lib', + ], + 'sources': [ + '../../browser/profile_resetter/jtl_foundation.cc', + '../../browser/profile_resetter/jtl_foundation.h', + 'jtl_compiler_frontend.cc', + ], + }, + { + 'target_name': 'jtl_compiler_lib', + 'type': 'static_library', + 'product_name': 'jtl_compiler', + 'dependencies': [ + '../../../third_party/re2/re2.gyp:re2', + '../../../base/base.gyp:base', + ], + 'sources': [ + '../../browser/profile_resetter/jtl_foundation.h', + '../../browser/profile_resetter/jtl_instructions.h', + 'jtl_compiler.h', + 'jtl_compiler.cc', + 'jtl_parser.h', + 'jtl_parser.cc', + ], + }, + ], +} diff --git a/chrome/tools/profile_reset/jtl_compiler.h b/chrome/tools/profile_reset/jtl_compiler.h new file mode 100644 index 0000000..cd81b81 --- /dev/null +++ b/chrome/tools/profile_reset/jtl_compiler.h @@ -0,0 +1,74 @@ +// 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_TOOLS_PROFILE_RESET_JTL_COMPILER_H_ +#define CHROME_TOOLS_PROFILE_RESET_JTL_COMPILER_H_ + +#include <string> + +#include "base/basictypes.h" + +// Compiles text-based JTL source code into JTL byte-code. +// +// For an overview of JTL (JSON Traversal Language), and the exhaustive list of +// instructions, please see "chrome/browser/profile_resetter/jtl_foundation.h". +// +// The text-based JTL syntax itself much resembles C/C++. A program consists of +// zero or more sentences. Each sentence is terminated by a semi-colon (;), and +// is composed of *one* or more operations, separated by periods (.). +// +// Each operation resembles a C/C++ function call and consists of an instruction +// name, and an optional argument list, which takes Boolean values and/or string +// literals. The text-based instruction names are defined in "jtl_compiler.cc". +// +// Whitespace does not matter, except for inside string literals. C++-style, +// double-slash-introduced comments are also supported. +// +// Example source code: +// +// // Store "x"=true if path "foo.bar" is found. +// go("foo").go("bar").store_bool("x", true); +// +// // Store "y"="1" if the above value is set. +// compare_stored_bool("x", true, false).store_hash("y", "1"); +// +class JtlCompiler { + public: + struct CompileError { + enum ErrorCode { + ERROR_NONE, + MISMATCHED_DOUBLE_QUOTES, + PARSING_ERROR, + INVALID_OPERATION_NAME, + INVALID_ARGUMENT_COUNT, + INVALID_ARGUMENT_TYPE, + INVALID_ARGUMENT_VALUE + }; + + CompileError() : line_number(0), error_code(ERROR_NONE) {} + CompileError(size_t line_number, + const std::string& context, + ErrorCode error_code) + : line_number(line_number), context(context), error_code(error_code) {} + + size_t line_number; // 0-based. + std::string context; + ErrorCode error_code; + }; + + // Compiles text-based JTL source code contained in |source_code| into JTL + // byte-code to |output_bytecode|. Variable, node names, and string literals + // will be hashed using the seed in |hash_seed|. + // On success, returns true. Otherwise, returns false and fills |error| with + // more information (if it is non-NULL). + static bool Compile(const std::string& source_code, + const std::string& hash_seed, + std::string* output_bytecode, + CompileError* error); + + private: + DISALLOW_IMPLICIT_CONSTRUCTORS(JtlCompiler); +}; + +#endif // CHROME_TOOLS_PROFILE_RESET_JTL_COMPILER_H_ diff --git a/chrome/tools/profile_reset/jtl_compiler_frontend.cc b/chrome/tools/profile_reset/jtl_compiler_frontend.cc new file mode 100644 index 0000000..b172205 --- /dev/null +++ b/chrome/tools/profile_reset/jtl_compiler_frontend.cc @@ -0,0 +1,112 @@ +// 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. +// +// A simple command-line compiler for JTL (JSON Traversal Language). +// +// Translates rules from a text-based, human-readable format to an easy-to-parse +// byte-code format, which then can be interpreted by JtlInterpreter. +// +// Example usage: +// jtl_compiler --input=blah.txt --hash-seed="foobar" --output=blah.dat + +#include <iostream> +#include <string> + +#include "base/command_line.h" +#include "base/file_util.h" +#include "base/files/file_path.h" +#include "chrome/tools/profile_reset/jtl_compiler.h" + +namespace { + +// Command-line argument name: path to the input text-based JTL source code. +const char kInputPath[] = "input"; + +// Command-line argument name: path to the output byte-code. +const char kOutputPath[] = "output"; + +// Command-line argument name: the hash seed to use. +const char kHashSeed[] = "hash-seed"; + +// Error codes. +const char kMismatchedDoubleQuotes[] = "Mismatched double-quotes before EOL."; +const char kParsingError[] = "Parsing error. Input is ill-formed."; +const char kArgumentCountError[] = "Wrong number of arguments for operation."; +const char kArgumentTypeError[] = "Wrong argument type(s) for operation."; +const char kArgumentValueError[] = "Wrong argument value(s) for operation."; +const char kUnknownOperationError[] = "No operation by this name."; +const char kUnknownError[] = "Unknown error."; + +const char* ResolveErrorCode(JtlCompiler::CompileError::ErrorCode code) { + switch (code) { + case JtlCompiler::CompileError::MISMATCHED_DOUBLE_QUOTES: + return kMismatchedDoubleQuotes; + case JtlCompiler::CompileError::PARSING_ERROR: + return kParsingError; + case JtlCompiler::CompileError::INVALID_ARGUMENT_COUNT: + return kArgumentCountError; + case JtlCompiler::CompileError::INVALID_ARGUMENT_TYPE: + return kArgumentTypeError; + case JtlCompiler::CompileError::INVALID_ARGUMENT_VALUE: + return kArgumentValueError; + case JtlCompiler::CompileError::INVALID_OPERATION_NAME: + return kUnknownOperationError; + default: + return kUnknownError; + } +} + +} // namespace + +int main(int argc, char* argv[]) { + CommandLine::Init(argc, argv); + CommandLine* cmd_line = CommandLine::ForCurrentProcess(); + if (!cmd_line->HasSwitch(kInputPath) || !cmd_line->HasSwitch(kHashSeed) || + !cmd_line->HasSwitch(kOutputPath)) { + std::cerr << "Usage: " << argv[0] << " <required switches>" << std::endl; + std::cerr << "\nRequired switches are:" << std::endl; + std::cerr << " --" << kInputPath << "=<file>" + << "\t\tPath to the input text-based JTL source code." + << std::endl; + std::cerr << " --" << kOutputPath << "=<file>" + << "\t\tPath to the output byte-code." << std::endl; + std::cerr << " --" << kHashSeed << "=<value>" + << "\t\tThe hash seed to use." << std::endl; + return -1; + } + + base::FilePath source_code_path = + MakeAbsoluteFilePath(cmd_line->GetSwitchValuePath(kInputPath)); + std::string source_code; + if (!base::ReadFileToString(source_code_path, &source_code)) { + std::cerr << "ERROR: Cannot read input file." << std::endl; + return -3; + } + + std::string bytecode; + JtlCompiler::CompileError error; + std::string hash_seed = cmd_line->GetSwitchValueASCII(kHashSeed); + if (!JtlCompiler::Compile(source_code, hash_seed, &bytecode, &error)) { + std::cerr << "COMPILE ERROR: " << ResolveErrorCode(error.error_code) + << std::endl; + std::cerr << " Line number: " << (error.line_number + 1) << std::endl; + std::cerr << " Context: " << (error.context.size() > 63 + ? error.context.substr(0, 60) + "..." + : error.context) << std::endl; + return -2; + } + + base::FilePath bytecode_path = + MakeAbsoluteFilePath(cmd_line->GetSwitchValuePath(kOutputPath)); + int bytes_written = + base::WriteFile(cmd_line->GetSwitchValuePath(kOutputPath), + bytecode.data(), + static_cast<int>(bytecode.size())); + if (bytes_written != static_cast<int>(bytecode.size())) { + std::cerr << "ERROR: Cannot write output file." << std::endl; + return -3; + } + + return 0; +} diff --git a/chrome/tools/profile_reset/jtl_compiler_unittest.cc b/chrome/tools/profile_reset/jtl_compiler_unittest.cc new file mode 100644 index 0000000..3dbf886 --- /dev/null +++ b/chrome/tools/profile_reset/jtl_compiler_unittest.cc @@ -0,0 +1,217 @@ +// 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/tools/profile_reset/jtl_compiler.h" + +#include <string> + +#include "base/values.h" +#include "chrome/browser/profile_resetter/jtl_foundation.h" +#include "chrome/browser/profile_resetter/jtl_instructions.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +const char kTestHashSeed[] = "test-hash-seed"; + +// Helpers ------------------------------------------------------------------- + +std::string GetHash(const std::string& input) { + return jtl_foundation::Hasher(kTestHashSeed).GetHash(input); +} + +static 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; +} + +// Tests --------------------------------------------------------------------- + +// Note: Parsing and parsing-related errors are unit-tested separately in more +// detail in "jtl_parser_unittest.cc". Here, most of the time, we assume that +// creating the parse tree works. + +TEST(JtlCompiler, CompileSingleInstructions) { + struct TestCase { + std::string source_code; + std::string expected_bytecode; + } cases[] = { + {"go(\"foo\").", OP_NAVIGATE(GetHash("foo"))}, + {"go(\"has whitespace\t\").", OP_NAVIGATE(GetHash("has whitespace\t"))}, + {"any.", OP_NAVIGATE_ANY}, + {"back.", OP_NAVIGATE_BACK}, + {"store_bool(\"name\", true).", + OP_STORE_BOOL(GetHash("name"), VALUE_TRUE)}, + {"compare_stored_bool(\"name\", true, false).", + OP_COMPARE_STORED_BOOL(GetHash("name"), VALUE_TRUE, VALUE_FALSE)}, + {"store_hash(\"name\", \"" + GetHash("value") + "\").", + OP_STORE_HASH(GetHash("name"), GetHash("value"))}, + {"store_hashed(\"name\", \"value\").", + OP_STORE_HASH(GetHash("name"), GetHash("value"))}, + {"store_node_bool(\"name\").", OP_STORE_NODE_BOOL(GetHash("name"))}, + {"store_node_hash(\"name\").", OP_STORE_NODE_HASH(GetHash("name"))}, + {"store_node_registerable_domain_hash(\"name\").", + OP_STORE_NODE_REGISTERABLE_DOMAIN_HASH(GetHash("name"))}, + {"compare_stored_hashed(\"name\", \"value\", \"default\").", + OP_COMPARE_STORED_HASH( + GetHash("name"), GetHash("value"), GetHash("default"))}, + {"compare_bool(false).", OP_COMPARE_NODE_BOOL(VALUE_FALSE)}, + {"compare_bool(true).", OP_COMPARE_NODE_BOOL(VALUE_TRUE)}, + {"compare_hashed(\"foo\").", OP_COMPARE_NODE_HASH(GetHash("foo"))}, + {"compare_hashed_not(\"foo\").", + OP_COMPARE_NODE_HASH_NOT(GetHash("foo"))}, + {"compare_to_stored_bool(\"name\").", + OP_COMPARE_NODE_TO_STORED_BOOL(GetHash("name"))}, + {"compare_to_stored_hash(\"name\").", + OP_COMPARE_NODE_TO_STORED_HASH(GetHash("name"))}, + {"compare_substring_hashed(\"pattern\").", + OP_COMPARE_NODE_SUBSTRING( + GetHash("pattern"), EncodeUint32(7), EncodeUint32(766))}, + {"break.", OP_STOP_EXECUTING_SENTENCE}, + {"break;", OP_STOP_EXECUTING_SENTENCE + OP_END_OF_SENTENCE}}; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(cases[i].source_code); + std::string bytecode; + EXPECT_TRUE(JtlCompiler::Compile( + cases[i].source_code, kTestHashSeed, &bytecode, NULL)); + EXPECT_EQ(cases[i].expected_bytecode, bytecode); + } +} + +TEST(JtlCompiler, CompileEntireProgram) { + const char kSourceCode[] = + "// Store \"x\"=true if path is found.\n" + "go(\"foo\").go(\"bar\").store_bool(\"x\", true);\n" + "// ...\n" + "// Store \"y\"=\"1\" if above value is set.\n" + "compare_stored_bool(\"x\", true, false).store_hashed(\"y\", \"1\");\n"; + + std::string expected_bytecode = + OP_NAVIGATE(GetHash("foo")) + + OP_NAVIGATE(GetHash("bar")) + + OP_STORE_BOOL(GetHash("x"), VALUE_TRUE) + OP_END_OF_SENTENCE + + OP_COMPARE_STORED_BOOL(GetHash("x"), VALUE_TRUE, VALUE_FALSE) + + OP_STORE_HASH(GetHash("y"), GetHash("1")) + OP_END_OF_SENTENCE; + + std::string bytecode; + EXPECT_TRUE( + JtlCompiler::Compile(kSourceCode, kTestHashSeed, &bytecode, NULL)); + EXPECT_EQ(expected_bytecode, bytecode); +} + +TEST(JtlCompiler, InvalidOperationName) { + const char kSourceCode[] = "any()\n.\nnon_existent_instruction\n(\n)\n;\n"; + + std::string bytecode; + JtlCompiler::CompileError error; + EXPECT_FALSE( + JtlCompiler::Compile(kSourceCode, kTestHashSeed, &bytecode, &error)); + EXPECT_THAT(error.context, testing::StartsWith("non_existent_instruction")); + EXPECT_EQ(2u, error.line_number); + EXPECT_EQ(JtlCompiler::CompileError::INVALID_OPERATION_NAME, + error.error_code); +} + +TEST(JtlCompiler, InvalidArgumentsCount) { + const char* kSourceCodes[] = { + "any().\nstore_bool(\"name\", true, \"superfluous argument\");\n", + "any().\nstore_bool(\"name\");"}; // missing argument + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kSourceCodes); ++i) { + SCOPED_TRACE(kSourceCodes[i]); + std::string bytecode; + JtlCompiler::CompileError error; + EXPECT_FALSE(JtlCompiler::Compile( + kSourceCodes[i], kTestHashSeed, &bytecode, &error)); + EXPECT_THAT(error.context, testing::StartsWith("store_bool")); + EXPECT_EQ(1u, error.line_number); + EXPECT_EQ(JtlCompiler::CompileError::INVALID_ARGUMENT_COUNT, + error.error_code); + } +} + +TEST(JtlCompiler, InvalidArgumentType) { + struct TestCase { + std::string expected_context_prefix; + std::string source_code; + } cases[] = { + {"compare_bool", "any()\n.\ncompare_bool(\"foo\");"}, + {"compare_bool", + "any()\n.\ncompare_bool(\"01234567890123456789012345678901\");"}, + {"compare_hashed", "any()\n.\ncompare_hashed(false);"}, + {"store_hash", "any()\n.\nstore_hash(\"name\", false);"}, + {"store_hash", "any()\n.\nstore_hash(\"name\", \"foo\");"}, + {"compare_stored_bool", + "any()\n.\ncompare_stored_bool(\"name\", \"need a bool\", false);"}, + {"compare_stored_bool", + "any()\n.\ncompare_stored_bool(\"name\", false, \"need a bool\");"}, + {"compare_substring_hashed", + "any()\n.\ncompare_substring_hashed(true);"}}; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(cases[i].source_code); + std::string bytecode; + JtlCompiler::CompileError error; + EXPECT_FALSE(JtlCompiler::Compile( + cases[i].source_code, kTestHashSeed, &bytecode, &error)); + EXPECT_THAT(error.context, + testing::StartsWith(cases[i].expected_context_prefix)); + EXPECT_EQ(2u, error.line_number); + EXPECT_EQ(JtlCompiler::CompileError::INVALID_ARGUMENT_TYPE, + error.error_code); + } +} + +TEST(JtlCompiler, InvalidArgumentValue) { + struct TestCase { + std::string expected_context_prefix; + std::string source_code; + } cases[] = { + {"compare_substring_hashed", "compare_substring_hashed(\"\");"}}; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(cases[i].source_code); + std::string bytecode; + JtlCompiler::CompileError error; + EXPECT_FALSE(JtlCompiler::Compile( + cases[i].source_code, kTestHashSeed, &bytecode, &error)); + EXPECT_THAT(error.context, + testing::StartsWith(cases[i].expected_context_prefix)); + EXPECT_EQ(0u, error.line_number); + EXPECT_EQ(JtlCompiler::CompileError::INVALID_ARGUMENT_VALUE, + error.error_code); + } +} + +TEST(JtlCompiler, MistmatchedDoubleQuotes) { + const char kSourceCode[] = "any().\ngo(\"ok\", \"stray quote).break();"; + + std::string bytecode; + JtlCompiler::CompileError error; + EXPECT_FALSE( + JtlCompiler::Compile(kSourceCode, kTestHashSeed, &bytecode, &error)); + EXPECT_EQ(1u, error.line_number); + EXPECT_EQ(JtlCompiler::CompileError::MISMATCHED_DOUBLE_QUOTES, + error.error_code); +} + +TEST(JtlCompiler, ParsingError) { + const char kSourceCode[] = "any().\ngo()missing_separator();"; + + std::string bytecode; + JtlCompiler::CompileError error; + EXPECT_FALSE( + JtlCompiler::Compile(kSourceCode, kTestHashSeed, &bytecode, &error)); + EXPECT_THAT(error.context, testing::StartsWith("go")); + EXPECT_EQ(1u, error.line_number); + EXPECT_EQ(JtlCompiler::CompileError::PARSING_ERROR, error.error_code); +} + +} // namespace diff --git a/chrome/tools/profile_reset/jtl_parser.cc b/chrome/tools/profile_reset/jtl_parser.cc new file mode 100644 index 0000000..c1d9c15 --- /dev/null +++ b/chrome/tools/profile_reset/jtl_parser.cc @@ -0,0 +1,167 @@ +// 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/tools/profile_reset/jtl_parser.h" + +#include <algorithm> + +#include "base/logging.h" +#include "third_party/re2/re2/re2.h" + +namespace { + +// RegEx that matches the first line of a text. Will throw away any potential +// double-slash-introduced comments and the potential trailing EOL character. +// Note: will fail in case the first line contains an unmatched double-quote +// outside of comments. +const char kSingleLineWithMaybeCommentsRE[] = + // Non-greedily match and capture sequences of 1.) string literals inside + // correctly matched double-quotes, or 2.) any other character. + "^((?:\"[^\"\\n]*\"|[^\"\\n])*?)" + // Greedily match and throw away the potential comment. + "(?://.*)?" + // Match and throw away EOL, or match end-of-string. + "(?:\n|$)"; + +// RegEx to match either a double-quote-enclosed string literal or a whitespace. +// Applied repeatedly and without overlapping, can be used to remove whitespace +// outside of string literals. +const char kRemoveWhitespaceRE[] = "(\"[^\"]*\")|\\s"; + +// The substitution pattern to use together with the above when replacing. As +// the whitespace is not back-referenced here, it will get removed. +const char kRemoveWhitespaceRewrite[] = "\\1"; + +// Separator to terminate a sentence. +const char kEndOfSentenceSeparator[] = ";"; + +// The 'true' Boolean keyword. +const char kTrueKeyword[] = "true"; + +// RegEx that matches and captures one argument, which is either a double-quote +// enclosed string, or a Boolean value. Will throw away a trailing comma. +const char kSingleArgumentRE[] = "(?:(?:\"([^\"]*)\"|(true|false))(?:,|$))"; + +// RegEx-es that, when concatenated, will match a single operation, and capture +// the: operation name, the optional arguments, and the separator that follows. +const char kOperationNameRE[] = "([[:word:]]+)"; +const char kMaybeArgumentListRE[] = + "(?:\\(" // Opening parenthesis. + "((?:\"[^\"]*\"|[^\")])*)" // Capture: anything inside, quote-aware. + "\\))?"; // Closing parenthesis + everything optional. +const char kOperationSeparatorRE[] = "(;|\\.)"; + +} // namespace + +struct JtlParser::ParsingState { + explicit ParsingState(const re2::StringPiece& compacted_source) + : single_operation_regex(std::string(kOperationNameRE) + + kMaybeArgumentListRE + + kOperationSeparatorRE), + single_argument_regex(kSingleArgumentRE), + remaining_compacted_source(compacted_source), + last_line_number(0) {} + + RE2 single_operation_regex; + RE2 single_argument_regex; + re2::StringPiece remaining_compacted_source; + re2::StringPiece last_context; + size_t last_line_number; +}; + +JtlParser::JtlParser(const std::string& compacted_source_code, + const std::vector<size_t>& newline_indices) + : compacted_source_(compacted_source_code), + newline_indices_(newline_indices) { + state_.reset(new ParsingState(compacted_source_)); +} + +JtlParser::~JtlParser() {} + +// static +bool JtlParser::RemoveCommentsAndAllWhitespace( + const std::string& verbose_text, + std::string* compacted_text, + std::vector<size_t>* newline_indices, + size_t* error_line_number) { + DCHECK(compacted_text); + DCHECK(newline_indices); + std::string line; + RE2 single_line_regex(kSingleLineWithMaybeCommentsRE); + RE2 remove_whitespace_regex(kRemoveWhitespaceRE); + re2::StringPiece verbose_text_piece(verbose_text); + compacted_text->clear(); + newline_indices->clear(); + while (!verbose_text_piece.empty()) { + if (!RE2::Consume(&verbose_text_piece, single_line_regex, &line)) { + if (error_line_number) + *error_line_number = newline_indices->size(); + return false; + } + RE2::GlobalReplace( + &line, remove_whitespace_regex, kRemoveWhitespaceRewrite); + *compacted_text += line; + newline_indices->push_back(compacted_text->size()); + } + return true; +} + +bool JtlParser::HasFinished() { + return state_->remaining_compacted_source.empty(); +} + +bool JtlParser::ParseNextOperation(std::string* name, + base::ListValue* argument_list, + bool* ends_sentence) { + DCHECK(name); + DCHECK(argument_list); + DCHECK(ends_sentence); + + state_->last_context = state_->remaining_compacted_source; + state_->last_line_number = GetOriginalLineNumber( + compacted_source_.size() - state_->remaining_compacted_source.length()); + + std::string arguments, separator; + if (!RE2::Consume(&state_->remaining_compacted_source, + state_->single_operation_regex, + name, + &arguments, + &separator)) + return false; + + *ends_sentence = (separator == kEndOfSentenceSeparator); + state_->last_context.remove_suffix(state_->remaining_compacted_source.size()); + + re2::StringPiece arguments_piece(arguments); + std::string string_value, boolean_value; + while (!arguments_piece.empty()) { + if (!RE2::Consume(&arguments_piece, + state_->single_argument_regex, + &string_value, + &boolean_value)) + return false; + + if (!boolean_value.empty()) { + argument_list->Append( + new base::FundamentalValue(boolean_value == kTrueKeyword)); + } else { + // |string_value| might be empty for an empty string + argument_list->Append(new base::StringValue(string_value)); + } + } + return true; +} + +size_t JtlParser::GetOriginalLineNumber(size_t compacted_index) const { + return static_cast<size_t>(std::upper_bound(newline_indices_.begin(), + newline_indices_.end(), + compacted_index) - + newline_indices_.begin()); +} + +size_t JtlParser::GetLastLineNumber() const { return state_->last_line_number; } + +std::string JtlParser::GetLastContext() const { + return state_->last_context.ToString(); +} diff --git a/chrome/tools/profile_reset/jtl_parser.h b/chrome/tools/profile_reset/jtl_parser.h new file mode 100644 index 0000000..e758211 --- /dev/null +++ b/chrome/tools/profile_reset/jtl_parser.h @@ -0,0 +1,83 @@ +// 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_TOOLS_PROFILE_RESET_JTL_PARSER_H_ +#define CHROME_TOOLS_PROFILE_RESET_JTL_PARSER_H_ + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/values.h" + +// Parses text-based JTL source code into a stream of operation names, arguments +// and separator kinds. +class JtlParser { + public: + // Creates a new parser to parse |compacted_source_code|, which should already + // be stripped of all comments and whitespace (except inside string literals). + // Use RemoveCommentsAndAllWhitespace() to manufacture these arguments, also + // see its comments for a description of |newline_indices|. + JtlParser(const std::string& compacted_source_code, + const std::vector<size_t>& newline_indices); + ~JtlParser(); + + // Removes comments from |verbose_text| and compacts it into whitespace-free + // format (except inside string literals). Elements in |newline_indices| will + // be monotonically increasing and will refer to positions in |compacted_text| + // such that a new line has been removed before that position. + // Example: + // verbose_text = "H e l l o // my\n" + // " dear \n" + // "\n" + // "world\" ! \"" + // compacted_text = "Hellodearworld\" ! \"" + // 01234567890123... + // newline_indices = {5, 9, 9} + // Returns true on success, false if there were unmatched quotes in a line, in + // which case |error_line_number| will be set accordingly if it is non-NULL. + static bool RemoveCommentsAndAllWhitespace( + const std::string& verbose_text, + std::string* compacted_text, + std::vector<size_t>* newline_indices, + size_t* error_line_number); + + // Returns true if the entire input has been successfully consumed. Note that + // even when this returns false, a subsequent call to ParseNextOperation() + // might still fail if the next operation cannot be parsed. + bool HasFinished(); + + // Fetches the |name| and the |argument_list| of the next operation, and also + // whether or not it |ends_the_sentence|, i.e. it is followed by the + // end-of-sentence separator. + // Returns false if there is a parsing error, in which case the values for the + // output parameters are undefined, and |this| parser shall no longer be used. + bool ParseNextOperation(std::string* name, + base::ListValue* argument_list, + bool* ends_the_sentence); + + // Returns the compacted source code that was passed in to the constructor. + const std::string& compacted_source() const { return compacted_source_; } + + // Returns at which line the character at position |compacted_index| in the + // |compacted_source()| was originally located. + size_t GetOriginalLineNumber(size_t compacted_index) const; + + size_t GetLastLineNumber() const; + std::string GetLastContext() const; + + private: + // Contains pre-compiled regular expressions and related state. Factored out + // to avoid this header depending on RE2 headers. + struct ParsingState; + + std::string compacted_source_; + std::vector<size_t> newline_indices_; + scoped_ptr<ParsingState> state_; + + DISALLOW_COPY_AND_ASSIGN(JtlParser); +}; + +#endif // CHROME_TOOLS_PROFILE_RESET_JTL_PARSER_H_ diff --git a/chrome/tools/profile_reset/jtl_parser_unittest.cc b/chrome/tools/profile_reset/jtl_parser_unittest.cc new file mode 100644 index 0000000..1868df8 --- /dev/null +++ b/chrome/tools/profile_reset/jtl_parser_unittest.cc @@ -0,0 +1,346 @@ +// 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/tools/profile_reset/jtl_parser.h" + +#include "base/json/json_writer.h" +#include "base/memory/scoped_ptr.h" +#include "base/values.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// Helpers ------------------------------------------------------------------- + +void ExpectNextOperation(JtlParser* parser, + const char* expected_name, + const char* expected_args_json, + bool expected_ends_sentence) { + std::string actual_name; + base::ListValue actual_args; + std::string actual_args_json; + bool actual_ends_sentence; + + EXPECT_FALSE(parser->HasFinished()); + EXPECT_TRUE(parser->ParseNextOperation( + &actual_name, &actual_args, &actual_ends_sentence)); + EXPECT_EQ(expected_name, actual_name); + base::JSONWriter::Write(&actual_args, &actual_args_json); + EXPECT_EQ(expected_args_json, actual_args_json); + EXPECT_EQ(expected_ends_sentence, actual_ends_sentence); +} + +void ExpectNextOperationToFail(JtlParser* parser, + size_t expected_line_number, + const char* expected_context_prefix) { + std::string actual_name; + base::ListValue actual_args; + bool actual_ends_sentence; + + EXPECT_FALSE(parser->HasFinished()); + EXPECT_FALSE(parser->ParseNextOperation( + &actual_name, &actual_args, &actual_ends_sentence)); + EXPECT_THAT(parser->GetLastContext(), + testing::StartsWith(expected_context_prefix)); + EXPECT_EQ(expected_line_number, parser->GetLastLineNumber()); +} + +JtlParser* CreateParserFromVerboseText(const std::string& verbose_text) { + std::string compacted_source_code; + std::vector<size_t> newline_indices; + EXPECT_TRUE(JtlParser::RemoveCommentsAndAllWhitespace( + verbose_text, &compacted_source_code, &newline_indices, NULL)); + return new JtlParser(compacted_source_code, newline_indices); +} + +// Tests --------------------------------------------------------------------- + +TEST(JtlParser, CompactingEmpty) { + const char kSourceCode[] = ""; + const char kCompactedSourceCode[] = ""; + + scoped_ptr<JtlParser> parser(CreateParserFromVerboseText(kSourceCode)); + EXPECT_EQ(kCompactedSourceCode, parser->compacted_source()); +} + +TEST(JtlParser, CompactingTrivial) { + const char kSourceCode[] = "foo"; + const char kCompactedSourceCode[] = "foo"; + + scoped_ptr<JtlParser> parser(CreateParserFromVerboseText(kSourceCode)); + EXPECT_EQ(kCompactedSourceCode, parser->compacted_source()); +} + +TEST(JtlParser, CompactingOneLine) { + const char kSourceCode[] = " \r f\to o ( true ) "; + const char kCompactedSourceCode[] = "foo(true)"; + + scoped_ptr<JtlParser> parser(CreateParserFromVerboseText(kSourceCode)); + EXPECT_EQ(kCompactedSourceCode, parser->compacted_source()); + for (size_t i = 0; i < arraysize(kCompactedSourceCode) - 1; ++i) { + SCOPED_TRACE(testing::Message("Position ") << i); + EXPECT_EQ(0u, parser->GetOriginalLineNumber(i)); + } +} + +TEST(JtlParser, CompactingMultipleLines) { + const char kSourceCode[] = "a\nbb\n \nccc \n\n d( \n e \n )"; + const char kCompactedSourceCode[] = "abbcccd(e)"; + const size_t kLineNumbers[] = {0u, 1u, 1u, 3u, 3u, 3u, 5u, 5u, 6u, 7u}; + COMPILE_ASSERT(arraysize(kCompactedSourceCode) == arraysize(kLineNumbers) + 1, + mismatched_test_data); + + scoped_ptr<JtlParser> parser(CreateParserFromVerboseText(kSourceCode)); + EXPECT_EQ(kCompactedSourceCode, parser->compacted_source()); + for (size_t i = 0; i < arraysize(kLineNumbers); ++i) { + SCOPED_TRACE(testing::Message("Position ") << i); + EXPECT_EQ(kLineNumbers[i], parser->GetOriginalLineNumber(i)); + } +} + +TEST(JtlParser, CompactingMultipleLinesWithComments) { + const char kSourceCode[] = + "a/ /b//Comment \n" + "//\n" + "// Full line comment\n" + " cd //"; + const char kCompactedSourceCode[] = "a//bcd"; + const size_t kLineNumbers[] = {0u, 0u, 0u, 0u, 3u, 3u}; + COMPILE_ASSERT(arraysize(kCompactedSourceCode) == arraysize(kLineNumbers) + 1, + mismatched_test_data); + + scoped_ptr<JtlParser> parser(CreateParserFromVerboseText(kSourceCode)); + EXPECT_EQ(kCompactedSourceCode, parser->compacted_source()); + for (size_t i = 0; i < arraysize(kLineNumbers); ++i) { + SCOPED_TRACE(testing::Message("Position ") << i); + EXPECT_EQ(kLineNumbers[i], parser->GetOriginalLineNumber(i)); + } +} + +TEST(JtlParser, HandlingCommentsAndStringLiterals) { + struct TestCase { + const char* source_code; + const char* compacted_source_code; + } cases[] = { + {"//", ""}, + {"//comment", ""}, + {"foo // comment", "foo"}, + {"foo // // comment", "foo"}, + {"foo //", "foo"}, + {"\"literal\"", "\"literal\""}, + {"\"literal with space\"", "\"literal with space\""}, + {"\"\"", "\"\""}, + {"\"\"\"\"", "\"\"\"\""}, + {"\"\" // comment", "\"\""}, + {"\"literal\" // comment", "\"literal\""}, + {"\"literal\" \"literal\" // comment", "\"literal\"\"literal\""}, + {"foo // \"not a literal\"", "foo"}, + {"foo // \"not even matched", "foo"}, + {"foo // \"not a literal\" \"not even matched", "foo"}, + {"\"literal\" // \"not a literal\"", "\"literal\""}, + {"\"literal\" // \"not even matched", "\"literal\""}, + {"\"//not a comment//\"", "\"//not a comment//\""}, + {"\"//not a comment//\" // comment", "\"//not a comment//\""}, + {"// \"//not a literal//\" // comment", ""}, + {"\"literal\" // \"//not a literal//\" // comment", "\"literal\""}, + {"\"//not a comment//\" // \"//not a literal//\" // comment", + "\"//not a comment//\""}, + {"\"literal // \"not a literal nor a comment", + "\"literal // \"notaliteralnoracomment"}, + {"\"literal // \"not a literal nor a comment//\"", + "\"literal // \"notaliteralnoracomment"} + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(cases[i].source_code); + scoped_ptr<JtlParser> parser( + CreateParserFromVerboseText(cases[i].source_code)); + EXPECT_EQ(cases[i].compacted_source_code, parser->compacted_source()); + } +} + +TEST(JtlParser, MismatchedDoubleQuotesBeforeEndOfLine) { + struct TestCase { + const char* source_code; + size_t error_line_number; + } cases[] = { + {"\"", 0}, + {"\"mismatched literal", 0}, + {"\n\"", 1}, + {"\"\n\"", 0}, + {"\"\"\"", 0}, + {"\"\"\n\"", 1}, + {"\"\"\n\"\n\"", 1}, + {"\" // not a comment", 0}, + {"\" // not a comment\n\"", 0}, + {"\"\" // comment\n\"", 1}, + {"\"\"\" // not a comment\n\"", 0}, + {"\"\"\" // \" neither a literal nor a comment\"\n\"", 0}, + {"\"\" // comment\n\"// not a comment", 1}, + {"\" // not a comment\"\n\"// not a comment", 1}, + {"foo(\"bar\");\nfoo(\"mismatched);", 1}, + {"foo(\n\"bar\", \"mismatched);", 1}, + {"foo(\n\"bar\", \"mismatched); //comment", 1}, + {"foo(\n\"bar\", \"mismatched);\ngood(\"bar\")", 1} + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(cases[i].source_code); + std::string compacted_source_code; + std::vector<size_t> newline_indices; + size_t error_line_number; + EXPECT_FALSE(JtlParser::RemoveCommentsAndAllWhitespace( + cases[i].source_code, + &compacted_source_code, + &newline_indices, + &error_line_number)); + EXPECT_EQ(cases[i].error_line_number, error_line_number); + } +} + +TEST(JtlParser, ParsingEmpty) { + const char kSourceCode[] = ""; + + scoped_ptr<JtlParser> parser(CreateParserFromVerboseText(kSourceCode)); + EXPECT_TRUE(parser->HasFinished()); +} + +TEST(JtlParser, ParsingOneWellFormedOperation) { + struct TestCase { + const char* source_code; + const char* expected_name; + const char* expected_args; + const bool expected_ends_sentence; + } cases[] = { + {"foo1;", "foo1", "[]", true}, + {"foo2().", "foo2", "[]", false}, + {"foo3(true);", "foo3", "[true]", true}, + {"foo4(false).", "foo4", "[false]", false}, + {"foo5(\"bar\").", "foo5", "[\"bar\"]", false}, + {"foo6(\" b a r \").", "foo6", "[\" b a r \"]", false}, + {"foo7(true, \"bar\").", "foo7", "[true,\"bar\"]", false}, + {"foo8(\"bar\", false, true);", "foo8", "[\"bar\",false,true]", true}, + {"foo9(\"bar\", \" b a r \");", "foo9", "[\"bar\",\" b a r \"]", true} + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(cases[i].expected_name); + scoped_ptr<JtlParser> parser( + CreateParserFromVerboseText(cases[i].source_code)); + ExpectNextOperation(parser.get(), + cases[i].expected_name, + cases[i].expected_args, + cases[i].expected_ends_sentence); + EXPECT_TRUE(parser->HasFinished()); + } +} + +TEST(JtlParser, ParsingMultipleWellFormedOperations) { + const char kSourceCode[] = + "foo1(true).foo2.foo3(\"bar\");" + "foo4(\"bar\", false);"; + + scoped_ptr<JtlParser> parser(CreateParserFromVerboseText(kSourceCode)); + ExpectNextOperation(parser.get(), "foo1", "[true]", false); + ExpectNextOperation(parser.get(), "foo2", "[]", false); + ExpectNextOperation(parser.get(), "foo3", "[\"bar\"]", true); + ExpectNextOperation(parser.get(), "foo4", "[\"bar\",false]", true); + EXPECT_TRUE(parser->HasFinished()); +} + +TEST(JtlParser, ParsingTrickyStringLiterals) { + struct TestCase { + const char* source_code; + const char* expected_name; + const char* expected_args; + const bool expected_ends_sentence; + } cases[] = { + {"prev().foo1(\"\");next(true);", "foo1", "[\"\"]", true}, + {"prev().foo2(\" \");next(true);", "foo2", "[\" \"]", true}, + {"prev().foo3(\",\",true);next(true);", "foo3", "[\",\",true]", true}, + {"prev().foo4(\")\",true);next(true);", "foo4", "[\")\",true]", true}, + {"prev().foo5(\";\",true);next(true);", "foo5", "[\";\",true]", true}, + {"prev().foo6(\"/\",true).next(true);", "foo6", "[\"/\",true]", false}, + {"prev().foo7(\"//\",true).next(true);", "foo7", "[\"//\",true]", false}, + {"prev().foo8(\".\",true).next(true);", "foo8", "[\".\",true]", false}, + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(cases[i].expected_name); + scoped_ptr<JtlParser> parser( + CreateParserFromVerboseText(cases[i].source_code)); + ExpectNextOperation(parser.get(), "prev", "[]", false); + ExpectNextOperation(parser.get(), + cases[i].expected_name, + cases[i].expected_args, + cases[i].expected_ends_sentence); + ExpectNextOperation(parser.get(), "next", "[true]", true); + EXPECT_TRUE(parser->HasFinished()); + } +} + +TEST(JtlParser, FirstOperationIsIllFormed) { + struct TestCase { + const char* source_code; + const char* operation_name; + } cases[] = { + {";;", ";"}, + {"bad_args1(not a boolean value);", "bad_args1"}, + {"bad_args2(,);", "bad_args2"}, + {"bad_args3(...);", "bad_args3"}, + {"bad_args4(1);", "bad_args4"}, + {"bad_args5(1.2);", "bad_args5"}, + {"bad_args6([\"bar\"]);", "bad_args6"}, + {"bad_args7(False);", "bad_args7"}, + {"bad_args8(True);", "bad_args8"}, + {"bad_quotes1(missing both, true).good();", "bad_quotes1"}, + {"bad_quotes2(true, \"missing one).good(); //\"", "bad_quotes2"}, + {"bad_quotes3(\"too\" \"much\", true).good();", "bad_quotes3"}, + {"bad_missing_separator1", "bad_missing_separator1"}, + {"bad_missing_separator2()good();", "bad_missing_separator2"}, + {"bad_parenthesis1(true.good();", "bad_parenthesis1"}, + {"bad_parenthesis2(true.good());", "bad_parenthesis2"}, + {"bad_parenthesis3).good();", "bad_parenthesis3"} + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(cases[i].operation_name); + scoped_ptr<JtlParser> parser( + CreateParserFromVerboseText(cases[i].source_code)); + ExpectNextOperationToFail(parser.get(), 0, cases[i].operation_name); + } +} + +TEST(JtlParser, SecondOperationIsIllFormed) { + struct TestCase { + const char* source_code; + const char* bad_operation_name; + } cases[] = { + {"\ngood(true,false)\n.bad_args(,);", "bad_args"}, + {"\ngood(true,false)\n.bad_quotes1(missing both, true).good();", + "bad_quotes1"}, + {"\ngood(true,false)\n.bad_quotes2(\"missing one, true).good(); //\"", + "bad_quotes2"}, + {"\ngood(true,false)\n.bad_quotes3(\"too\" \"many\", true).good();", + "bad_quotes3"}, + {"\ngood(true,false)\n.bad_separator1()/good();", "bad_separator1"}, + {"\ngood(true,false)\n.missing_separator1", "missing_separator1"}, + {"\ngood(true,false)\n.missing_separator2()good();", + "missing_separator2"}, + {"\ngood(true,false)\n.bad_parens1(true.good();", "bad_parens1"}, + {"\ngood(true,false)\n.bad_parens2(true.good());", "bad_parens2"}, + {"\ngood(true,false)\n.bad_parens3).good();", "bad_parens3"} + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) { + SCOPED_TRACE(cases[i].bad_operation_name); + scoped_ptr<JtlParser> parser( + CreateParserFromVerboseText(cases[i].source_code)); + ExpectNextOperation(parser.get(), "good", "[true,false]", false); + ExpectNextOperationToFail(parser.get(), 2, cases[i].bad_operation_name); + } +} + +} // namespace diff --git a/tools/gritsettings/resource_ids b/tools/gritsettings/resource_ids index 1c20615..9cd707c 100644 --- a/tools/gritsettings/resource_ids +++ b/tools/gritsettings/resource_ids @@ -17,7 +17,7 @@ "SRCDIR": "../..", "chrome/browser/browser_resources.grd": { - "includes": [500], + "includes": [400], "structures": [750], }, "chrome/browser/resources/component_extension_resources.grd": { diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml index dc8dd6a..365d7b2 100644 --- a/tools/metrics/histograms/histograms.xml +++ b/tools/metrics/histograms/histograms.xml @@ -51739,7 +51739,7 @@ To add a new entry, add it with any value and run test to compute valid value. <int value="11" label="prefs::kPinnedTabs"/> <int value="12" label="extensions::pref_names::kKnownDisabled (Obsolete 07/2014)"/> - <int value="13" label="prefs::kProfileResetPromptMemento (Obsolete 07/2014)"/> + <int value="13" label="prefs::kProfileResetPromptMemento"/> <int value="14" label="DefaultSearchManager::kDefaultSearchProviderDataPrefName"/> <int value="15" label="prefs::kPreferenceResetTime"/> |