pref_metrics_service.cc revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/prefs/pref_metrics_service.h"
6
7#include "base/bind.h"
8#include "base/command_line.h"
9#include "base/json/json_string_value_serializer.h"
10#include "base/metrics/histogram.h"
11#include "base/prefs/pref_registry_simple.h"
12#include "base/prefs/pref_service.h"
13#include "base/strings/string_number_conversions.h"
14#include "chrome/browser/browser_process.h"
15#include "chrome/browser/browser_shutdown.h"
16// Accessing the Device ID API here is a layering violation.
17// TODO(bbudge) Move the API so it's usable here.
18// http://crbug.com/276485
19#include "chrome/browser/extensions/api/music_manager_private/device_id.h"
20#include "chrome/browser/prefs/pref_service_syncable.h"
21#include "chrome/browser/prefs/scoped_user_pref_update.h"
22#include "chrome/browser/prefs/session_startup_pref.h"
23#include "chrome/browser/prefs/synced_pref_change_registrar.h"
24#include "chrome/browser/profiles/incognito_helpers.h"
25#include "chrome/browser/profiles/profile.h"
26#include "chrome/browser/search_engines/template_url_prepopulate_data.h"
27#include "chrome/browser/ui/tabs/pinned_tab_codec.h"
28#include "chrome/common/chrome_switches.h"
29#include "chrome/common/pref_names.h"
30#include "components/browser_context_keyed_service/browser_context_dependency_manager.h"
31#include "crypto/hmac.h"
32#include "grit/browser_resources.h"
33#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
34#include "ui/base/resource/resource_bundle.h"
35
36namespace {
37
38const int kSessionStartupPrefValueMax = SessionStartupPref::kPrefValueMax;
39
40// An unregistered preference to fill in indices in kTrackedPrefs below for
41// preferences that aren't defined on every platform. This is fine as the code
42// below (e.g. CheckTrackedPreferences()) skips unregistered preferences and
43// should thus never report any data about that index on the platforms where
44// that preference is unimplemented.
45const char kUnregisteredPreference[] = "_";
46
47// These preferences must be kept in sync with the TrackedPreference enum in
48// tools/metrics/histograms/histograms.xml. To add a new preference, append it
49// to the array and add a corresponding value to the histogram enum.
50const char* kTrackedPrefs[] = {
51  prefs::kShowHomeButton,
52  prefs::kHomePageIsNewTabPage,
53  prefs::kHomePage,
54  prefs::kRestoreOnStartup,
55  prefs::kURLsToRestoreOnStartup,
56  prefs::kExtensionsPref,
57  prefs::kGoogleServicesLastUsername,
58  prefs::kSearchProviderOverrides,
59  prefs::kDefaultSearchProviderSearchURL,
60  prefs::kDefaultSearchProviderKeyword,
61  prefs::kDefaultSearchProviderName,
62#if !defined(OS_ANDROID)
63  prefs::kPinnedTabs,
64#else
65  kUnregisteredPreference,
66#endif
67  prefs::kExtensionKnownDisabled,
68};
69
70const size_t kSHA256DigestSize = 32;
71
72}  // namespace
73
74PrefMetricsService::PrefMetricsService(Profile* profile)
75    : profile_(profile),
76      prefs_(profile_->GetPrefs()),
77      local_state_(g_browser_process->local_state()),
78      profile_name_(profile_->GetPath().AsUTF8Unsafe()),
79      tracked_pref_paths_(kTrackedPrefs),
80      tracked_pref_path_count_(arraysize(kTrackedPrefs)),
81      checked_tracked_prefs_(false),
82      weak_factory_(this) {
83  pref_hash_seed_ = ResourceBundle::GetSharedInstance().GetRawDataResource(
84      IDR_PREF_HASH_SEED_BIN).as_string();
85
86  RecordLaunchPrefs();
87
88  PrefServiceSyncable* prefs = PrefServiceSyncable::FromProfile(profile_);
89  synced_pref_change_registrar_.reset(new SyncedPrefChangeRegistrar(prefs));
90
91  RegisterSyncedPrefObservers();
92
93  // The following code might cause callbacks into this instance before we exit
94  // the constructor. This instance should be initialized at this point.
95#if defined(OS_WIN) || defined(OS_MACOSX)
96  // We need the machine id to compute pref value hashes. Fetch that, and then
97  // call CheckTrackedPreferences in the callback.
98  extensions::api::DeviceId::GetDeviceId(
99      "PrefMetricsService",  // non-empty string to obfuscate the device id.
100      Bind(&PrefMetricsService::GetDeviceIdCallback,
101           weak_factory_.GetWeakPtr()));
102#endif  // defined(OS_WIN) || defined(OS_MACOSX)
103}
104
105// For unit testing only.
106PrefMetricsService::PrefMetricsService(Profile* profile,
107                                       PrefService* local_state,
108                                       const std::string& device_id,
109                                       const char** tracked_pref_paths,
110                                       int tracked_pref_path_count)
111    : profile_(profile),
112      prefs_(profile->GetPrefs()),
113      local_state_(local_state),
114      profile_name_(profile_->GetPath().AsUTF8Unsafe()),
115      pref_hash_seed_(kSHA256DigestSize, 0),
116      device_id_(device_id),
117      tracked_pref_paths_(tracked_pref_paths),
118      tracked_pref_path_count_(tracked_pref_path_count),
119      checked_tracked_prefs_(false),
120      weak_factory_(this) {
121  CheckTrackedPreferences();
122}
123
124PrefMetricsService::~PrefMetricsService() {
125}
126
127void PrefMetricsService::RecordLaunchPrefs() {
128  bool show_home_button = prefs_->GetBoolean(prefs::kShowHomeButton);
129  bool home_page_is_ntp = prefs_->GetBoolean(prefs::kHomePageIsNewTabPage);
130  UMA_HISTOGRAM_BOOLEAN("Settings.ShowHomeButton", show_home_button);
131  if (show_home_button) {
132    UMA_HISTOGRAM_BOOLEAN("Settings.GivenShowHomeButton_HomePageIsNewTabPage",
133                          home_page_is_ntp);
134  }
135
136  // For non-NTP homepages, see if the URL comes from the same TLD+1 as a known
137  // search engine.  Note that this is only an approximation of search engine
138  // use, due to both false negatives (pages that come from unknown TLD+1 X but
139  // consist of a search box that sends to known TLD+1 Y) and false positives
140  // (pages that share a TLD+1 with a known engine but aren't actually search
141  // pages, e.g. plus.google.com).
142  if (!home_page_is_ntp) {
143    GURL homepage_url(prefs_->GetString(prefs::kHomePage));
144    if (homepage_url.is_valid()) {
145      UMA_HISTOGRAM_ENUMERATION(
146          "Settings.HomePageEngineType",
147          TemplateURLPrepopulateData::GetEngineType(homepage_url),
148          SEARCH_ENGINE_MAX);
149    }
150  }
151
152  int restore_on_startup = prefs_->GetInteger(prefs::kRestoreOnStartup);
153  UMA_HISTOGRAM_ENUMERATION("Settings.StartupPageLoadSettings",
154                            restore_on_startup, kSessionStartupPrefValueMax);
155  if (restore_on_startup == SessionStartupPref::kPrefValueURLs) {
156    const ListValue* url_list = prefs_->GetList(prefs::kURLsToRestoreOnStartup);
157    UMA_HISTOGRAM_CUSTOM_COUNTS("Settings.StartupPageLoadURLs",
158                                url_list->GetSize(), 1, 50, 20);
159    // Similarly, check startup pages for known search engine TLD+1s.
160    std::string url_text;
161    for (size_t i = 0; i < url_list->GetSize(); ++i) {
162      if (url_list->GetString(i, &url_text)) {
163        GURL start_url(url_text);
164        if (start_url.is_valid()) {
165          UMA_HISTOGRAM_ENUMERATION(
166              "Settings.StartupPageEngineTypes",
167              TemplateURLPrepopulateData::GetEngineType(start_url),
168              SEARCH_ENGINE_MAX);
169        }
170      }
171    }
172  }
173
174#if !defined(OS_ANDROID)
175  StartupTabs startup_tabs = PinnedTabCodec::ReadPinnedTabs(profile_);
176  UMA_HISTOGRAM_CUSTOM_COUNTS("Settings.PinnedTabs",
177                              startup_tabs.size(), 1, 50, 20);
178  for (size_t i = 0; i < startup_tabs.size(); ++i) {
179    GURL start_url(startup_tabs.at(i).url);
180    if (start_url.is_valid()) {
181      UMA_HISTOGRAM_ENUMERATION(
182          "Settings.PinnedTabEngineTypes",
183          TemplateURLPrepopulateData::GetEngineType(start_url),
184          SEARCH_ENGINE_MAX);
185    }
186  }
187#endif
188}
189
190// static
191void PrefMetricsService::RegisterPrefs(PrefRegistrySimple* registry) {
192  // Register the top level dictionary to map profile names to dictionaries of
193  // tracked preferences.
194  registry->RegisterDictionaryPref(prefs::kProfilePreferenceHashes);
195}
196
197void PrefMetricsService::RegisterSyncedPrefObservers() {
198  LogHistogramValueCallback booleanHandler = base::Bind(
199      &PrefMetricsService::LogBooleanPrefChange, base::Unretained(this));
200
201  AddPrefObserver(prefs::kShowHomeButton, "ShowHomeButton", booleanHandler);
202  AddPrefObserver(prefs::kHomePageIsNewTabPage, "HomePageIsNewTabPage",
203                  booleanHandler);
204
205  AddPrefObserver(prefs::kRestoreOnStartup, "StartupPageLoadSettings",
206                  base::Bind(&PrefMetricsService::LogIntegerPrefChange,
207                             base::Unretained(this),
208                             kSessionStartupPrefValueMax));
209}
210
211void PrefMetricsService::AddPrefObserver(
212    const std::string& path,
213    const std::string& histogram_name_prefix,
214    const LogHistogramValueCallback& callback) {
215  synced_pref_change_registrar_->Add(path.c_str(),
216      base::Bind(&PrefMetricsService::OnPrefChanged,
217                 base::Unretained(this),
218                 histogram_name_prefix, callback));
219}
220
221void PrefMetricsService::OnPrefChanged(
222    const std::string& histogram_name_prefix,
223    const LogHistogramValueCallback& callback,
224    const std::string& path,
225    bool from_sync) {
226  PrefServiceSyncable* prefs = PrefServiceSyncable::FromProfile(profile_);
227  const PrefService::Preference* pref = prefs->FindPreference(path.c_str());
228  DCHECK(pref);
229  std::string source_name(
230      from_sync ? ".PulledFromSync" : ".PushedToSync");
231  std::string histogram_name("Settings." + histogram_name_prefix + source_name);
232  callback.Run(histogram_name, pref->GetValue());
233};
234
235void PrefMetricsService::LogBooleanPrefChange(const std::string& histogram_name,
236                                              const Value* value) {
237  bool boolean_value = false;
238  if (!value->GetAsBoolean(&boolean_value))
239    return;
240  base::HistogramBase* histogram = base::BooleanHistogram::FactoryGet(
241      histogram_name, base::HistogramBase::kUmaTargetedHistogramFlag);
242  histogram->Add(boolean_value);
243}
244
245void PrefMetricsService::LogIntegerPrefChange(int boundary_value,
246                                              const std::string& histogram_name,
247                                              const Value* value) {
248  int integer_value = 0;
249  if (!value->GetAsInteger(&integer_value))
250    return;
251  base::HistogramBase* histogram = base::LinearHistogram::FactoryGet(
252      histogram_name,
253      1,
254      boundary_value,
255      boundary_value + 1,
256      base::HistogramBase::kUmaTargetedHistogramFlag);
257  histogram->Add(integer_value);
258}
259
260void PrefMetricsService::LogListPrefChange(
261    const LogHistogramValueCallback& item_callback,
262    const std::string& histogram_name,
263    const Value* value) {
264  const ListValue* items = NULL;
265  if (!value->GetAsList(&items))
266    return;
267  for (size_t i = 0; i < items->GetSize(); ++i) {
268    const Value *item_value = NULL;
269    if (items->Get(i, &item_value))
270      item_callback.Run(histogram_name, item_value);
271  }
272}
273
274void PrefMetricsService::GetDeviceIdCallback(const std::string& device_id) {
275  device_id_ = device_id;
276  // On Aura, this seems to be called twice.
277  if (!checked_tracked_prefs_)
278    CheckTrackedPreferences();
279}
280
281// To detect changes to Preferences that happen outside of Chrome, we hash
282// selected pref values and save them in local state. CheckTrackedPreferences
283// compares the saved values to the values observed in the profile's prefs. A
284// dictionary of dictionaries in local state holds the hashed values, grouped by
285// profile. To make the system more resistant to spoofing, pref values are
286// hashed with the pref path and the device id.
287void PrefMetricsService::CheckTrackedPreferences() {
288  DCHECK(!checked_tracked_prefs_);
289
290  const base::DictionaryValue* pref_hash_dicts =
291      local_state_->GetDictionary(prefs::kProfilePreferenceHashes);
292  // Get the hashed prefs dictionary if it exists. If it doesn't, it will be
293  // created if we set preference values below.
294  const base::DictionaryValue* hashed_prefs = NULL;
295  pref_hash_dicts->GetDictionaryWithoutPathExpansion(profile_name_,
296                                                     &hashed_prefs);
297  for (int i = 0; i < tracked_pref_path_count_; ++i) {
298    // Skip prefs that haven't been registered.
299    if (!prefs_->FindPreference(tracked_pref_paths_[i]))
300      continue;
301
302    bool changed = false;
303    const base::Value* value = prefs_->GetUserPrefValue(tracked_pref_paths_[i]);
304    if (value) {
305      std::string value_hash =
306          GetHashedPrefValue(tracked_pref_paths_[i], value);
307      std::string last_hash;
308      if (hashed_prefs &&
309          hashed_prefs->GetString(tracked_pref_paths_[i], &last_hash)) {
310        if (value_hash != last_hash) {
311          changed = true;
312          // Record that the preference changed from its last value.
313          UMA_HISTOGRAM_ENUMERATION("Settings.TrackedPreferenceChanged",
314              i, tracked_pref_path_count_);
315          UpdateTrackedPreference(tracked_pref_paths_[i]);
316        }
317      } else {
318        changed = true;
319        // Record that we haven't tracked this preference yet, or the hash in
320        // local state was removed.
321        UMA_HISTOGRAM_ENUMERATION("Settings.TrackedPreferenceInitialized",
322            i, tracked_pref_path_count_);
323        UpdateTrackedPreference(tracked_pref_paths_[i]);
324      }
325    } else {
326      // There is no preference set. Remove any hashed value from local state
327      // and if one was present, record that a pref was cleared.
328      if (RemoveTrackedPreference(tracked_pref_paths_[i])) {
329        changed = true;
330        UMA_HISTOGRAM_ENUMERATION("Settings.TrackedPreferenceCleared",
331            i, tracked_pref_path_count_);
332      }
333    }
334    if (!changed) {
335      UMA_HISTOGRAM_ENUMERATION("Settings.TrackedPreferenceUnchanged",
336          i, tracked_pref_path_count_);
337    }
338  }
339
340  checked_tracked_prefs_ = true;
341
342  // Now that we've checked the incoming preferences, register for change
343  // notifications, unless this is test code.
344  // TODO(bbudge) Fix failing browser_tests and so we can remove this test.
345  // Several tests fail when they shutdown before they can write local state.
346  if (!CommandLine::ForCurrentProcess()->HasSwitch(switches::kTestType) &&
347      !CommandLine::ForCurrentProcess()->HasSwitch(switches::kChromeFrame)) {
348    InitializePrefObservers();
349  }
350}
351
352void PrefMetricsService::UpdateTrackedPreference(const char* path) {
353  const base::Value* value = prefs_->GetUserPrefValue(path);
354  // If the pref value is now the default, remove the hash.
355  const ListValue* list_value;
356  const DictionaryValue* dict_value;
357  if (!value ||
358      (value->GetAsList(&list_value) && list_value->GetSize() == 0) ||
359      (value->GetAsDictionary(&dict_value) && dict_value->size() == 0)) {
360    RemoveTrackedPreference(path);
361  } else {
362    DictionaryPrefUpdate update(local_state_, prefs::kProfilePreferenceHashes);
363    DictionaryValue* child_dictionary = NULL;
364
365    // Get the dictionary corresponding to the profile name,
366    // which may have a '.'
367    if (!update->GetDictionaryWithoutPathExpansion(profile_name_,
368                                                   &child_dictionary)) {
369      child_dictionary = new DictionaryValue;
370      update->SetWithoutPathExpansion(profile_name_, child_dictionary);
371    }
372    child_dictionary->SetString(path, GetHashedPrefValue(path, value));
373  }
374}
375
376bool PrefMetricsService::RemoveTrackedPreference(const char* path) {
377  DictionaryPrefUpdate update(local_state_, prefs::kProfilePreferenceHashes);
378  DictionaryValue* child_dictionary = NULL;
379
380  if (!update->GetDictionaryWithoutPathExpansion(profile_name_,
381                                                 &child_dictionary)) {
382    return false;
383  }
384  return child_dictionary->Remove(path, NULL);
385}
386
387std::string PrefMetricsService::GetHashedPrefValue(
388    const char* path,
389    const base::Value* value) {
390  DCHECK(value);
391
392  // Dictionary values may contain empty lists and sub-dictionaries. Make a
393  // deep copy with those removed to make the hash more stable.
394  const DictionaryValue* dict_value;
395  scoped_ptr<DictionaryValue> canonical_dict_value;
396  if (value->GetAsDictionary(&dict_value)) {
397    canonical_dict_value.reset(dict_value->DeepCopyWithoutEmptyChildren());
398    value = canonical_dict_value.get();
399  }
400
401  std::string string_to_hash(device_id_);
402  string_to_hash.append(path);
403  JSONStringValueSerializer serializer(&string_to_hash);
404  serializer.Serialize(*value);
405
406  crypto::HMAC hmac(crypto::HMAC::SHA256);
407  unsigned char digest[kSHA256DigestSize];
408  if (!hmac.Init(pref_hash_seed_) ||
409      !hmac.Sign(string_to_hash, digest, kSHA256DigestSize)) {
410    NOTREACHED();
411    return std::string();
412  }
413
414  return base::HexEncode(digest, kSHA256DigestSize);
415}
416
417void PrefMetricsService::InitializePrefObservers() {
418  pref_registrar_.Init(prefs_);
419  for (int i = 0; i < tracked_pref_path_count_; ++i) {
420    // Skip prefs that haven't been registered.
421    if (!prefs_->FindPreference(tracked_pref_paths_[i]))
422      continue;
423
424    pref_registrar_.Add(
425        tracked_pref_paths_[i],
426        base::Bind(&PrefMetricsService::UpdateTrackedPreference,
427                   weak_factory_.GetWeakPtr(),
428                   tracked_pref_paths_[i]));
429  }
430}
431
432// static
433PrefMetricsService::Factory* PrefMetricsService::Factory::GetInstance() {
434  return Singleton<PrefMetricsService::Factory>::get();
435}
436
437// static
438PrefMetricsService* PrefMetricsService::Factory::GetForProfile(
439    Profile* profile) {
440  return static_cast<PrefMetricsService*>(
441      GetInstance()->GetServiceForBrowserContext(profile, true));
442}
443
444PrefMetricsService::Factory::Factory()
445    : BrowserContextKeyedServiceFactory(
446        "PrefMetricsService",
447        BrowserContextDependencyManager::GetInstance()) {
448}
449
450PrefMetricsService::Factory::~Factory() {
451}
452
453BrowserContextKeyedService*
454PrefMetricsService::Factory::BuildServiceInstanceFor(
455    content::BrowserContext* profile) const {
456  return new PrefMetricsService(static_cast<Profile*>(profile));
457}
458
459bool PrefMetricsService::Factory::ServiceIsCreatedWithBrowserContext() const {
460  return true;
461}
462
463bool PrefMetricsService::Factory::ServiceIsNULLWhileTesting() const {
464  return false;
465}
466
467content::BrowserContext* PrefMetricsService::Factory::GetBrowserContextToUse(
468    content::BrowserContext* context) const {
469  return chrome::GetBrowserContextRedirectedInIncognito(context);
470}
471