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 <stddef.h>
8 #include <stdint.h>
9
10 #include <set>
11
12 #include "base/stl_util.h"
13 #include "base/strings/string_util.h"
14
15 namespace variations {
16 namespace {
17
18 // Converts |date_time| in Study date format to base::Time.
ConvertStudyDateToBaseTime(int64_t date_time)19 base::Time ConvertStudyDateToBaseTime(int64_t date_time) {
20 return base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(date_time);
21 }
22
23 // Similar to base::Contains(), but specifically for ASCII strings and
24 // case-insensitive comparison.
25 template <typename Collection>
ContainsStringIgnoreCaseASCII(const Collection & collection,const std::string & value)26 bool ContainsStringIgnoreCaseASCII(const Collection& collection,
27 const std::string& value) {
28 return std::find_if(std::begin(collection), std::end(collection),
29 [&value](const std::string& s) -> bool {
30 return base::EqualsCaseInsensitiveASCII(s, value);
31 }) != std::end(collection);
32 }
33
34 } // namespace
35
36 namespace internal {
37
CheckStudyChannel(const Study::Filter & filter,Study::Channel channel)38 bool CheckStudyChannel(const Study::Filter& filter, Study::Channel channel) {
39 // An empty channel list matches all channels.
40 if (filter.channel_size() == 0)
41 return true;
42
43 for (int i = 0; i < filter.channel_size(); ++i) {
44 if (filter.channel(i) == channel)
45 return true;
46 }
47 return false;
48 }
49
CheckStudyFormFactor(const Study::Filter & filter,Study::FormFactor form_factor)50 bool CheckStudyFormFactor(const Study::Filter& filter,
51 Study::FormFactor form_factor) {
52 // Empty whitelist and blacklist signifies matching any form factor.
53 if (filter.form_factor_size() == 0 && filter.exclude_form_factor_size() == 0)
54 return true;
55
56 // Allow the form_factor if it matches the whitelist.
57 // Note if both a whitelist and blacklist are specified, the blacklist is
58 // ignored. We do not expect both to be present for Chrome due to server-side
59 // checks.
60 if (filter.form_factor_size() > 0)
61 return base::Contains(filter.form_factor(), form_factor);
62
63 // Omit if we match the blacklist.
64 return !base::Contains(filter.exclude_form_factor(), form_factor);
65 }
66
CheckStudyHardwareClass(const Study::Filter & filter,const std::string & hardware_class)67 bool 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 // Note: This logic changed in M66. Prior to M66, this used substring
76 // comparison logic to match hardware classes. In M66, it was made consistent
77 // with other filters.
78
79 // Checks if we are supposed to filter for a specified set of
80 // hardware_classes. Note that this means this overrides the
81 // exclude_hardware_class in case that ever occurs (which it shouldn't).
82 if (filter.hardware_class_size() > 0) {
83 return ContainsStringIgnoreCaseASCII(filter.hardware_class(),
84 hardware_class);
85 }
86
87 // Omit if we match the blacklist.
88 return !ContainsStringIgnoreCaseASCII(filter.exclude_hardware_class(),
89 hardware_class);
90 }
91
CheckStudyLocale(const Study::Filter & filter,const std::string & locale)92 bool CheckStudyLocale(const Study::Filter& filter, const std::string& locale) {
93 // Empty locale and exclude_locale lists matches all locales.
94 if (filter.locale_size() == 0 && filter.exclude_locale_size() == 0)
95 return true;
96
97 // Check if we are supposed to filter for a specified set of countries. Note
98 // that this means this overrides the exclude_locale in case that ever occurs
99 // (which it shouldn't).
100 if (filter.locale_size() > 0)
101 return base::Contains(filter.locale(), locale);
102
103 // Omit if matches any of the exclude entries.
104 return !base::Contains(filter.exclude_locale(), locale);
105 }
106
CheckStudyPlatform(const Study::Filter & filter,Study::Platform platform)107 bool CheckStudyPlatform(const Study::Filter& filter, Study::Platform platform) {
108 for (int i = 0; i < filter.platform_size(); ++i) {
109 if (filter.platform(i) == platform)
110 return true;
111 }
112 return false;
113 }
114
CheckStudyLowEndDevice(const Study::Filter & filter,bool is_low_end_device)115 bool CheckStudyLowEndDevice(const Study::Filter& filter,
116 bool is_low_end_device) {
117 return !filter.has_is_low_end_device() ||
118 filter.is_low_end_device() == is_low_end_device;
119 }
120
CheckStudyEnterprise(const Study::Filter & filter,const ClientFilterableState & client_state)121 bool CheckStudyEnterprise(const Study::Filter& filter,
122 const ClientFilterableState& client_state) {
123 return !filter.has_is_enterprise() ||
124 filter.is_enterprise() == client_state.IsEnterprise();
125 }
126
CheckStudyPolicyRestriction(const Study::Filter & filter,RestrictionPolicy policy_restriction)127 bool CheckStudyPolicyRestriction(const Study::Filter& filter,
128 RestrictionPolicy policy_restriction) {
129 switch (policy_restriction) {
130 // If the policy is set to no restrictions let any study that is not
131 // specifically designated for clients requesting critical studies only.
132 case RestrictionPolicy::NO_RESTRICTIONS:
133 return filter.policy_restriction() != Study::CRITICAL_ONLY;
134 // If the policy is set to only allow critical studies than make sure they
135 // have that restriction applied on their Filter.
136 case RestrictionPolicy::CRITICAL_ONLY:
137 return filter.policy_restriction() != Study::NONE;
138 // If the policy is set to not allow any variations then return false
139 // regardless of the actual Filter.
140 case RestrictionPolicy::ALL:
141 return false;
142 }
143 }
144
CheckStudyStartDate(const Study::Filter & filter,const base::Time & date_time)145 bool CheckStudyStartDate(const Study::Filter& filter,
146 const base::Time& date_time) {
147 if (filter.has_start_date()) {
148 const base::Time start_date =
149 ConvertStudyDateToBaseTime(filter.start_date());
150 return date_time >= start_date;
151 }
152
153 return true;
154 }
155
CheckStudyEndDate(const Study::Filter & filter,const base::Time & date_time)156 bool CheckStudyEndDate(const Study::Filter& filter,
157 const base::Time& date_time) {
158 if (filter.has_end_date()) {
159 const base::Time end_date = ConvertStudyDateToBaseTime(filter.end_date());
160 return end_date >= date_time;
161 }
162
163 return true;
164 }
165
CheckStudyVersion(const Study::Filter & filter,const base::Version & version)166 bool CheckStudyVersion(const Study::Filter& filter,
167 const base::Version& version) {
168 if (filter.has_min_version()) {
169 if (version.CompareToWildcardString(filter.min_version()) < 0)
170 return false;
171 }
172
173 if (filter.has_max_version()) {
174 if (version.CompareToWildcardString(filter.max_version()) > 0)
175 return false;
176 }
177
178 return true;
179 }
180
CheckStudyOSVersion(const Study::Filter & filter,const base::Version & version)181 bool CheckStudyOSVersion(const Study::Filter& filter,
182 const base::Version& version) {
183 if (filter.has_min_os_version()) {
184 if (!version.IsValid() ||
185 version.CompareToWildcardString(filter.min_os_version()) < 0) {
186 return false;
187 }
188 }
189
190 if (filter.has_max_os_version()) {
191 if (!version.IsValid() ||
192 version.CompareToWildcardString(filter.max_os_version()) > 0) {
193 return false;
194 }
195 }
196
197 return true;
198 }
199
CheckStudyCountry(const Study::Filter & filter,const std::string & country)200 bool CheckStudyCountry(const Study::Filter& filter,
201 const std::string& country) {
202 // Empty country and exclude_country matches all.
203 if (filter.country_size() == 0 && filter.exclude_country_size() == 0)
204 return true;
205
206 // Checks if we are supposed to filter for a specified set of countries. Note
207 // that this means this overrides the exclude_country in case that ever occurs
208 // (which it shouldn't).
209 if (filter.country_size() > 0)
210 return base::Contains(filter.country(), country);
211
212 // Omit if matches any of the exclude entries.
213 return !base::Contains(filter.exclude_country(), country);
214 }
215
GetClientCountryForStudy(const Study & study,const ClientFilterableState & client_state)216 const std::string& GetClientCountryForStudy(
217 const Study& study,
218 const ClientFilterableState& client_state) {
219 switch (study.consistency()) {
220 case Study::SESSION:
221 return client_state.session_consistency_country;
222 case Study::PERMANENT:
223 // Use the saved country for permanent consistency studies. This allows
224 // Chrome to use the same country for filtering permanent consistency
225 // studies between Chrome upgrades. Since some studies have user-visible
226 // effects, this helps to avoid annoying users with experimental group
227 // churn while traveling.
228 return client_state.permanent_consistency_country;
229 }
230
231 // Unless otherwise specified, use an empty country that won't pass any
232 // filters that specifically include countries, but will pass any filters
233 // that specifically exclude countries.
234 return base::EmptyString();
235 }
236
IsStudyExpired(const Study & study,const base::Time & date_time)237 bool IsStudyExpired(const Study& study, const base::Time& date_time) {
238 if (study.has_expiry_date()) {
239 const base::Time expiry_date =
240 ConvertStudyDateToBaseTime(study.expiry_date());
241 return date_time >= expiry_date;
242 }
243
244 return false;
245 }
246
ShouldAddStudy(const Study & study,const ClientFilterableState & client_state)247 bool ShouldAddStudy(const Study& study,
248 const ClientFilterableState& client_state) {
249 if (study.has_filter()) {
250 if (!CheckStudyChannel(study.filter(), client_state.channel)) {
251 DVLOG(1) << "Filtered out study " << study.name() << " due to channel.";
252 return false;
253 }
254
255 if (!CheckStudyFormFactor(study.filter(), client_state.form_factor)) {
256 DVLOG(1) << "Filtered out study " << study.name() <<
257 " due to form factor.";
258 return false;
259 }
260
261 if (!CheckStudyLocale(study.filter(), client_state.locale)) {
262 DVLOG(1) << "Filtered out study " << study.name() << " due to locale.";
263 return false;
264 }
265
266 if (!CheckStudyPlatform(study.filter(), client_state.platform)) {
267 DVLOG(1) << "Filtered out study " << study.name() << " due to platform.";
268 return false;
269 }
270
271 if (!CheckStudyVersion(study.filter(), client_state.version)) {
272 DVLOG(1) << "Filtered out study " << study.name() << " due to version.";
273 return false;
274 }
275
276 if (!CheckStudyStartDate(study.filter(), client_state.reference_date)) {
277 DVLOG(1) << "Filtered out study " << study.name() <<
278 " due to start date.";
279 return false;
280 }
281
282 if (!CheckStudyEndDate(study.filter(), client_state.reference_date)) {
283 DVLOG(1) << "Filtered out study " << study.name() << " due to end date.";
284 return false;
285 }
286
287 if (!CheckStudyHardwareClass(study.filter(), client_state.hardware_class)) {
288 DVLOG(1) << "Filtered out study " << study.name() <<
289 " due to hardware_class.";
290 return false;
291 }
292
293 if (!CheckStudyLowEndDevice(study.filter(),
294 client_state.is_low_end_device)) {
295 DVLOG(1) << "Filtered out study " << study.name()
296 << " due to is_low_end_device.";
297 return false;
298 }
299
300 if (!CheckStudyEnterprise(study.filter(), client_state)) {
301 DVLOG(1) << "Filtered out study " << study.name()
302 << " due to enterprise state.";
303 return false;
304 }
305
306 if (!CheckStudyPolicyRestriction(study.filter(),
307 client_state.policy_restriction)) {
308 DVLOG(1) << "Filtered out study " << study.name()
309 << " due to policy restriction.";
310 return false;
311 }
312
313 if (!CheckStudyOSVersion(study.filter(), client_state.os_version)) {
314 DVLOG(1) << "Filtered out study " << study.name()
315 << " due to os_version.";
316 return false;
317 }
318
319 const std::string& country = GetClientCountryForStudy(study, client_state);
320 if (!CheckStudyCountry(study.filter(), country)) {
321 DVLOG(1) << "Filtered out study " << study.name() << " due to country.";
322 return false;
323 }
324 }
325
326 DVLOG(1) << "Kept study " << study.name() << ".";
327 return true;
328 }
329
330 } // namespace internal
331
FilterAndValidateStudies(const VariationsSeed & seed,const ClientFilterableState & client_state,std::vector<ProcessedStudy> * filtered_studies)332 void FilterAndValidateStudies(const VariationsSeed& seed,
333 const ClientFilterableState& client_state,
334 std::vector<ProcessedStudy>* filtered_studies) {
335 DCHECK(client_state.version.IsValid());
336
337 // Add expired studies (in a disabled state) only after all the non-expired
338 // studies have been added (and do not add an expired study if a corresponding
339 // non-expired study got added). This way, if there's both an expired and a
340 // non-expired study that applies, the non-expired study takes priority.
341 std::set<std::string> created_studies;
342 std::vector<const Study*> expired_studies;
343
344 for (int i = 0; i < seed.study_size(); ++i) {
345 const Study& study = seed.study(i);
346 if (!internal::ShouldAddStudy(study, client_state))
347 continue;
348
349 if (internal::IsStudyExpired(study, client_state.reference_date)) {
350 expired_studies.push_back(&study);
351 } else if (!base::Contains(created_studies, study.name())) {
352 ProcessedStudy::ValidateAndAppendStudy(&study, false, filtered_studies);
353 created_studies.insert(study.name());
354 }
355 }
356
357 for (size_t i = 0; i < expired_studies.size(); ++i) {
358 if (!base::Contains(created_studies, expired_studies[i]->name())) {
359 ProcessedStudy::ValidateAndAppendStudy(expired_studies[i], true,
360 filtered_studies);
361 }
362 }
363 }
364
365 } // namespace variations
366