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