| // 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 "components/variations/study_filtering.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <set> |
| |
| #include "base/stl_util.h" |
| #include "base/strings/string_util.h" |
| #include "components/variations/client_filterable_state.h" |
| #include "components/variations/proto/study.pb.h" |
| |
| namespace variations { |
| namespace { |
| |
| // Converts |date_time| in Study date format to base::Time. |
| base::Time ConvertStudyDateToBaseTime(int64_t date_time) { |
| return base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(date_time); |
| } |
| |
| // Similar to base::ContainsValue(), but specifically for ASCII strings and |
| // case-insensitive comparison. |
| template <typename Collection> |
| bool ContainsStringIgnoreCaseASCII(const Collection& collection, |
| const std::string& value) { |
| return std::find_if(std::begin(collection), std::end(collection), |
| [&value](const std::string& s) -> bool { |
| return base::EqualsCaseInsensitiveASCII(s, value); |
| }) != std::end(collection); |
| } |
| |
| } // namespace |
| |
| namespace internal { |
| |
| bool CheckStudyChannel(const Study::Filter& filter, Study::Channel channel) { |
| // An empty channel list matches all channels. |
| if (filter.channel_size() == 0) |
| return true; |
| |
| for (int i = 0; i < filter.channel_size(); ++i) { |
| if (filter.channel(i) == channel) |
| return true; |
| } |
| return false; |
| } |
| |
| bool CheckStudyFormFactor(const Study::Filter& filter, |
| Study::FormFactor form_factor) { |
| // Empty whitelist and blacklist signifies matching any form factor. |
| if (filter.form_factor_size() == 0 && filter.exclude_form_factor_size() == 0) |
| return true; |
| |
| // Allow the form_factor if it matches the whitelist. |
| // Note if both a whitelist and blacklist are specified, the blacklist is |
| // ignored. We do not expect both to be present for Chrome due to server-side |
| // checks. |
| if (filter.form_factor_size() > 0) |
| return base::ContainsValue(filter.form_factor(), form_factor); |
| |
| // Omit if we match the blacklist. |
| return !base::ContainsValue(filter.exclude_form_factor(), form_factor); |
| } |
| |
| bool CheckStudyHardwareClass(const Study::Filter& filter, |
| const std::string& hardware_class) { |
| // Empty hardware_class and exclude_hardware_class matches all. |
| if (filter.hardware_class_size() == 0 && |
| filter.exclude_hardware_class_size() == 0) { |
| return true; |
| } |
| |
| // Note: This logic changed in M66. Prior to M66, this used substring |
| // comparison logic to match hardware classes. In M66, it was made consistent |
| // with other filters. |
| |
| // Checks if we are supposed to filter for a specified set of |
| // hardware_classes. Note that this means this overrides the |
| // exclude_hardware_class in case that ever occurs (which it shouldn't). |
| if (filter.hardware_class_size() > 0) { |
| return ContainsStringIgnoreCaseASCII(filter.hardware_class(), |
| hardware_class); |
| } |
| |
| // Omit if we match the blacklist. |
| return !ContainsStringIgnoreCaseASCII(filter.exclude_hardware_class(), |
| hardware_class); |
| } |
| |
| bool CheckStudyLocale(const Study::Filter& filter, const std::string& locale) { |
| // Empty locale and exclude_locale lists matches all locales. |
| if (filter.locale_size() == 0 && filter.exclude_locale_size() == 0) |
| return true; |
| |
| // Check if we are supposed to filter for a specified set of countries. Note |
| // that this means this overrides the exclude_locale in case that ever occurs |
| // (which it shouldn't). |
| if (filter.locale_size() > 0) |
| return base::ContainsValue(filter.locale(), locale); |
| |
| // Omit if matches any of the exclude entries. |
| return !base::ContainsValue(filter.exclude_locale(), locale); |
| } |
| |
| bool CheckStudyPlatform(const Study::Filter& filter, Study::Platform platform) { |
| // An empty platform list matches all platforms. |
| if (filter.platform_size() == 0) |
| return true; |
| |
| for (int i = 0; i < filter.platform_size(); ++i) { |
| if (filter.platform(i) == platform) |
| return true; |
| } |
| return false; |
| } |
| |
| bool CheckStudyLowEndDevice(const Study::Filter& filter, |
| bool is_low_end_device) { |
| return !filter.has_is_low_end_device() || |
| filter.is_low_end_device() == is_low_end_device; |
| } |
| |
| bool CheckStudyStartDate(const Study::Filter& filter, |
| const base::Time& date_time) { |
| if (filter.has_start_date()) { |
| const base::Time start_date = |
| ConvertStudyDateToBaseTime(filter.start_date()); |
| return date_time >= start_date; |
| } |
| |
| return true; |
| } |
| |
| bool CheckStudyEndDate(const Study::Filter& filter, |
| const base::Time& date_time) { |
| if (filter.has_end_date()) { |
| const base::Time end_date = ConvertStudyDateToBaseTime(filter.end_date()); |
| return end_date >= date_time; |
| } |
| |
| return true; |
| } |
| |
| bool CheckStudyVersion(const Study::Filter& filter, |
| const base::Version& version) { |
| if (filter.has_min_version()) { |
| if (version.CompareToWildcardString(filter.min_version()) < 0) |
| return false; |
| } |
| |
| if (filter.has_max_version()) { |
| if (version.CompareToWildcardString(filter.max_version()) > 0) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool CheckStudyCountry(const Study::Filter& filter, |
| const std::string& country) { |
| // Empty country and exclude_country matches all. |
| if (filter.country_size() == 0 && filter.exclude_country_size() == 0) |
| return true; |
| |
| // Checks if we are supposed to filter for a specified set of countries. Note |
| // that this means this overrides the exclude_country in case that ever occurs |
| // (which it shouldn't). |
| if (filter.country_size() > 0) |
| return base::ContainsValue(filter.country(), country); |
| |
| // Omit if matches any of the exclude entries. |
| return !base::ContainsValue(filter.exclude_country(), country); |
| } |
| |
| const std::string& GetClientCountryForStudy( |
| const Study& study, |
| const ClientFilterableState& client_state) { |
| switch (study.consistency()) { |
| case Study::SESSION: |
| return client_state.session_consistency_country; |
| case Study::PERMANENT: |
| // Use the saved country for permanent consistency studies. This allows |
| // Chrome to use the same country for filtering permanent consistency |
| // studies between Chrome upgrades. Since some studies have user-visible |
| // effects, this helps to avoid annoying users with experimental group |
| // churn while traveling. |
| return client_state.permanent_consistency_country; |
| } |
| |
| // Unless otherwise specified, use an empty country that won't pass any |
| // filters that specifically include countries, but will pass any filters |
| // that specifically exclude countries. |
| return base::EmptyString(); |
| } |
| |
| bool IsStudyExpired(const Study& study, const base::Time& date_time) { |
| if (study.has_expiry_date()) { |
| const base::Time expiry_date = |
| ConvertStudyDateToBaseTime(study.expiry_date()); |
| return date_time >= expiry_date; |
| } |
| |
| return false; |
| } |
| |
| bool ShouldAddStudy(const Study& study, |
| const ClientFilterableState& client_state) { |
| if (study.has_filter()) { |
| if (!CheckStudyChannel(study.filter(), client_state.channel)) { |
| DVLOG(1) << "Filtered out study " << study.name() << " due to channel."; |
| return false; |
| } |
| |
| if (!CheckStudyFormFactor(study.filter(), client_state.form_factor)) { |
| DVLOG(1) << "Filtered out study " << study.name() << |
| " due to form factor."; |
| return false; |
| } |
| |
| if (!CheckStudyLocale(study.filter(), client_state.locale)) { |
| DVLOG(1) << "Filtered out study " << study.name() << " due to locale."; |
| return false; |
| } |
| |
| if (!CheckStudyPlatform(study.filter(), client_state.platform)) { |
| DVLOG(1) << "Filtered out study " << study.name() << " due to platform."; |
| return false; |
| } |
| |
| if (!CheckStudyVersion(study.filter(), client_state.version)) { |
| DVLOG(1) << "Filtered out study " << study.name() << " due to version."; |
| return false; |
| } |
| |
| if (!CheckStudyStartDate(study.filter(), client_state.reference_date)) { |
| DVLOG(1) << "Filtered out study " << study.name() << |
| " due to start date."; |
| return false; |
| } |
| |
| if (!CheckStudyEndDate(study.filter(), client_state.reference_date)) { |
| DVLOG(1) << "Filtered out study " << study.name() << " due to end date."; |
| return false; |
| } |
| |
| if (!CheckStudyHardwareClass(study.filter(), client_state.hardware_class)) { |
| DVLOG(1) << "Filtered out study " << study.name() << |
| " due to hardware_class."; |
| return false; |
| } |
| |
| if (!CheckStudyLowEndDevice(study.filter(), |
| client_state.is_low_end_device)) { |
| DVLOG(1) << "Filtered out study " << study.name() |
| << " due to is_low_end_device."; |
| return false; |
| } |
| |
| const std::string& country = GetClientCountryForStudy(study, client_state); |
| if (!CheckStudyCountry(study.filter(), country)) { |
| DVLOG(1) << "Filtered out study " << study.name() << " due to country."; |
| return false; |
| } |
| } |
| |
| // TODO(paulmiller): Remove this once https://crbug.com/866722 is resolved. |
| if (study.consistency() == Study_Consistency_PERMANENT && |
| !client_state.supports_permanent_consistency) { |
| DVLOG(1) << "Filtered out study " << study.name() |
| << " due to supports_permanent_consistency."; |
| return false; |
| } |
| |
| DVLOG(1) << "Kept study " << study.name() << "."; |
| return true; |
| } |
| |
| } // namespace internal |
| |
| void FilterAndValidateStudies(const VariationsSeed& seed, |
| const ClientFilterableState& client_state, |
| std::vector<ProcessedStudy>* filtered_studies) { |
| DCHECK(client_state.version.IsValid()); |
| |
| // Add expired studies (in a disabled state) only after all the non-expired |
| // studies have been added (and do not add an expired study if a corresponding |
| // non-expired study got added). This way, if there's both an expired and a |
| // non-expired study that applies, the non-expired study takes priority. |
| std::set<std::string> created_studies; |
| std::vector<const Study*> expired_studies; |
| |
| for (int i = 0; i < seed.study_size(); ++i) { |
| const Study& study = seed.study(i); |
| if (!internal::ShouldAddStudy(study, client_state)) |
| continue; |
| |
| if (internal::IsStudyExpired(study, client_state.reference_date)) { |
| expired_studies.push_back(&study); |
| } else if (!base::ContainsKey(created_studies, study.name())) { |
| ProcessedStudy::ValidateAndAppendStudy(&study, false, filtered_studies); |
| created_studies.insert(study.name()); |
| } |
| } |
| |
| for (size_t i = 0; i < expired_studies.size(); ++i) { |
| if (!base::ContainsKey(created_studies, expired_studies[i]->name())) { |
| ProcessedStudy::ValidateAndAppendStudy(expired_studies[i], true, |
| filtered_studies); |
| } |
| } |
| } |
| |
| } // namespace variations |