1// Copyright 2014 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 "components/variations/study_filtering.h"
6
7#include <set>
8
9namespace variations {
10
11namespace {
12
13Study_Platform GetCurrentPlatform() {
14#if defined(OS_WIN)
15  return Study_Platform_PLATFORM_WINDOWS;
16#elif defined(OS_IOS)
17  return Study_Platform_PLATFORM_IOS;
18#elif defined(OS_MACOSX)
19  return Study_Platform_PLATFORM_MAC;
20#elif defined(OS_CHROMEOS)
21  return Study_Platform_PLATFORM_CHROMEOS;
22#elif defined(OS_ANDROID)
23  return Study_Platform_PLATFORM_ANDROID;
24#elif defined(OS_LINUX) || defined(OS_BSD) || defined(OS_SOLARIS)
25  // Default BSD and SOLARIS to Linux to not break those builds, although these
26  // platforms are not officially supported by Chrome.
27  return Study_Platform_PLATFORM_LINUX;
28#else
29#error Unknown platform
30#endif
31}
32
33// Converts |date_time| in Study date format to base::Time.
34base::Time ConvertStudyDateToBaseTime(int64 date_time) {
35  return base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(date_time);
36}
37
38}  // namespace
39
40namespace internal {
41
42bool CheckStudyChannel(const Study_Filter& filter, Study_Channel channel) {
43  // An empty channel list matches all channels.
44  if (filter.channel_size() == 0)
45    return true;
46
47  for (int i = 0; i < filter.channel_size(); ++i) {
48    if (filter.channel(i) == channel)
49      return true;
50  }
51  return false;
52}
53
54bool CheckStudyFormFactor(const Study_Filter& filter,
55                          Study_FormFactor form_factor) {
56  // An empty form factor list matches all form factors.
57  if (filter.form_factor_size() == 0)
58    return true;
59
60  for (int i = 0; i < filter.form_factor_size(); ++i) {
61    if (filter.form_factor(i) == form_factor)
62      return true;
63  }
64  return false;
65}
66
67bool CheckStudyHardwareClass(const Study_Filter& filter,
68                             const std::string& hardware_class) {
69  // Empty hardware_class and exclude_hardware_class matches all.
70  if (filter.hardware_class_size() == 0 &&
71      filter.exclude_hardware_class_size() == 0) {
72    return true;
73  }
74
75  // Checks if we are supposed to filter for a specified set of
76  // hardware_classes. Note that this means this overrides the
77  // exclude_hardware_class in case that ever occurs (which it shouldn't).
78  if (filter.hardware_class_size() > 0) {
79    for (int i = 0; i < filter.hardware_class_size(); ++i) {
80      // Check if the entry is a substring of |hardware_class|.
81      size_t position = hardware_class.find(filter.hardware_class(i));
82      if (position != std::string::npos)
83        return true;
84    }
85    // None of the requested hardware_classes match.
86    return false;
87  }
88
89  // Omit if matches any of the exclude entries.
90  for (int i = 0; i < filter.exclude_hardware_class_size(); ++i) {
91    // Check if the entry is a substring of |hardware_class|.
92    size_t position = hardware_class.find(
93        filter.exclude_hardware_class(i));
94    if (position != std::string::npos)
95      return false;
96  }
97
98  // None of the exclusions match, so this accepts.
99  return true;
100}
101
102bool CheckStudyLocale(const Study_Filter& filter, const std::string& locale) {
103  // An empty locale list matches all locales.
104  if (filter.locale_size() == 0)
105    return true;
106
107  for (int i = 0; i < filter.locale_size(); ++i) {
108    if (filter.locale(i) == locale)
109      return true;
110  }
111  return false;
112}
113
114bool CheckStudyPlatform(const Study_Filter& filter, Study_Platform platform) {
115  // An empty platform list matches all platforms.
116  if (filter.platform_size() == 0)
117    return true;
118
119  for (int i = 0; i < filter.platform_size(); ++i) {
120    if (filter.platform(i) == platform)
121      return true;
122  }
123  return false;
124}
125
126bool CheckStudyStartDate(const Study_Filter& filter,
127                         const base::Time& date_time) {
128  if (filter.has_start_date()) {
129    const base::Time start_date =
130        ConvertStudyDateToBaseTime(filter.start_date());
131    return date_time >= start_date;
132  }
133
134  return true;
135}
136
137bool CheckStudyVersion(const Study_Filter& filter,
138                       const base::Version& version) {
139  if (filter.has_min_version()) {
140    if (version.CompareToWildcardString(filter.min_version()) < 0)
141      return false;
142  }
143
144  if (filter.has_max_version()) {
145    if (version.CompareToWildcardString(filter.max_version()) > 0)
146      return false;
147  }
148
149  return true;
150}
151
152bool IsStudyExpired(const Study& study, const base::Time& date_time) {
153  if (study.has_expiry_date()) {
154    const base::Time expiry_date =
155        ConvertStudyDateToBaseTime(study.expiry_date());
156    return date_time >= expiry_date;
157  }
158
159  return false;
160}
161
162bool ShouldAddStudy(
163    const Study& study,
164    const std::string& locale,
165    const base::Time& reference_date,
166    const base::Version& version,
167    Study_Channel channel,
168    Study_FormFactor form_factor,
169    const std::string& hardware_class) {
170  if (study.has_filter()) {
171    if (!CheckStudyChannel(study.filter(), channel)) {
172      DVLOG(1) << "Filtered out study " << study.name() << " due to channel.";
173      return false;
174    }
175
176    if (!CheckStudyFormFactor(study.filter(), form_factor)) {
177      DVLOG(1) << "Filtered out study " << study.name() <<
178                  " due to form factor.";
179      return false;
180    }
181
182    if (!CheckStudyLocale(study.filter(), locale)) {
183      DVLOG(1) << "Filtered out study " << study.name() << " due to locale.";
184      return false;
185    }
186
187    if (!CheckStudyPlatform(study.filter(), GetCurrentPlatform())) {
188      DVLOG(1) << "Filtered out study " << study.name() << " due to platform.";
189      return false;
190    }
191
192    if (!CheckStudyVersion(study.filter(), version)) {
193      DVLOG(1) << "Filtered out study " << study.name() << " due to version.";
194      return false;
195    }
196
197    if (!CheckStudyStartDate(study.filter(), reference_date)) {
198      DVLOG(1) << "Filtered out study " << study.name() <<
199                  " due to start date.";
200      return false;
201    }
202
203    if (!CheckStudyHardwareClass(study.filter(), hardware_class)) {
204      DVLOG(1) << "Filtered out study " << study.name() <<
205                  " due to hardware_class.";
206      return false;
207    }
208  }
209
210  DVLOG(1) << "Kept study " << study.name() << ".";
211  return true;
212}
213
214}  // namespace internal
215
216void FilterAndValidateStudies(
217    const VariationsSeed& seed,
218    const std::string& locale,
219    const base::Time& reference_date,
220    const base::Version& version,
221    Study_Channel channel,
222    Study_FormFactor form_factor,
223    const std::string& hardware_class,
224    std::vector<ProcessedStudy>* filtered_studies) {
225  DCHECK(version.IsValid());
226
227  // Add expired studies (in a disabled state) only after all the non-expired
228  // studies have been added (and do not add an expired study if a corresponding
229  // non-expired study got added). This way, if there's both an expired and a
230  // non-expired study that applies, the non-expired study takes priority.
231  std::set<std::string> created_studies;
232  std::vector<const Study*> expired_studies;
233
234  for (int i = 0; i < seed.study_size(); ++i) {
235    const Study& study = seed.study(i);
236    if (!internal::ShouldAddStudy(study, locale, reference_date, version,
237                                  channel, form_factor, hardware_class)) {
238      continue;
239    }
240
241    if (internal::IsStudyExpired(study, reference_date)) {
242      expired_studies.push_back(&study);
243    } else if (!ContainsKey(created_studies, study.name())) {
244      ProcessedStudy::ValidateAndAppendStudy(&study, false, filtered_studies);
245      created_studies.insert(study.name());
246    }
247  }
248
249  for (size_t i = 0; i < expired_studies.size(); ++i) {
250    if (!ContainsKey(created_studies, expired_studies[i]->name())) {
251      ProcessedStudy::ValidateAndAppendStudy(expired_studies[i], true,
252                                             filtered_studies);
253    }
254  }
255}
256
257}  // namespace variations
258