// 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 #include #include #include "base/containers/contains.h" #include "base/logging.h" #include "base/strings/string_util.h" #include "components/variations/variations_seed_processor.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::Seconds(date_time); } // Similar to base::Contains(), but specifically for ASCII strings and // case-insensitive comparison. template 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; return base::Contains(filter.channel(), channel); } bool CheckStudyFormFactor(const Study::Filter& filter, Study::FormFactor form_factor) { // If both filters are empty, match all values. if (filter.form_factor_size() == 0 && filter.exclude_form_factor_size() == 0) return true; // Allow the |form_factor| if it's in the allowlist. // Note if both are specified, the excludelist 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::Contains(filter.form_factor(), form_factor); // Omit if there is a matching excludelist entry. return !base::Contains(filter.exclude_form_factor(), form_factor); } bool CheckStudyCpuArchitecture(const Study::Filter& filter, Study::CpuArchitecture cpu_architecture) { // If both filters are empty, match all values. if (filter.cpu_architecture_size() == 0 && filter.exclude_cpu_architecture_size() == 0) { return true; } // Allow the |cpu_architecture| if it's in the allowlist. // Note if both are specified, the excludelist is ignored. We do not expect // both to be present for Chrome due to server-side checks. if (filter.cpu_architecture_size() > 0) return base::Contains(filter.cpu_architecture(), cpu_architecture); // Omit if there is a matching excludelist entry. return !base::Contains(filter.exclude_cpu_architecture(), cpu_architecture); } bool CheckStudyHardwareClass(const Study::Filter& filter, const std::string& hardware_class) { // If both filters are empty, match all values. 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. // Allow the |hardware_class| if it's in the allowlist. // Note if both are specified, the excludelist is ignored. We do not expect // both to be present for Chrome due to server-side checks. if (filter.hardware_class_size() > 0) { return ContainsStringIgnoreCaseASCII(filter.hardware_class(), hardware_class); } // Omit if there is a matching excludelist entry. return !ContainsStringIgnoreCaseASCII(filter.exclude_hardware_class(), hardware_class); } bool CheckStudyLocale(const Study::Filter& filter, const std::string& locale) { // If both filters are empty, match all values. if (filter.locale_size() == 0 && filter.exclude_locale_size() == 0) return true; // Allow the |locale| if it's in the allowlist. // Note if both are specified, the excludelist is ignored. We do not expect // both to be present for Chrome due to server-side checks. if (filter.locale_size() > 0) return base::Contains(filter.locale(), locale); // Omit if there is a matching excludelist entry. return !base::Contains(filter.exclude_locale(), locale); } bool CheckStudyCountry(const Study::Filter& filter, const std::string& country) { // If both filters are empty, match all values. if (filter.country_size() == 0 && filter.exclude_country_size() == 0) return true; // Allow the |country| if it's in the allowlist. // Note if both are specified, the excludelist is ignored. We do not expect // both to be present for Chrome due to server-side checks. if (filter.country_size() > 0) return base::Contains(filter.country(), country); // Omit if there is a matching excludelist entry. return !base::Contains(filter.exclude_country(), country); } bool CheckStudyPlatform(const Study::Filter& filter, Study::Platform platform) { 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 CheckStudyPolicyRestriction(const Study::Filter& filter, RestrictionPolicy policy_restriction) { switch (policy_restriction) { // If the policy is set to no restrictions let any study that is not // specifically designated for clients requesting critical studies only. case RestrictionPolicy::NO_RESTRICTIONS: return filter.policy_restriction() != Study::CRITICAL_ONLY; // If the policy is set to only allow critical studies than make sure they // have that restriction applied on their Filter. case RestrictionPolicy::CRITICAL_ONLY: return filter.policy_restriction() != Study::NONE; // If the policy is set to not allow any variations then return false // regardless of the actual Filter. case RestrictionPolicy::ALL: return false; } } 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 CheckStudyOSVersion(const Study::Filter& filter, const base::Version& version) { if (filter.has_min_os_version()) { if (!version.IsValid() || version.CompareToWildcardString(filter.min_os_version()) < 0) { return false; } } if (filter.has_max_os_version()) { if (!version.IsValid() || version.CompareToWildcardString(filter.max_os_version()) > 0) { return false; } } return true; } bool CheckStudyEnterprise(const Study::Filter& filter, const ClientFilterableState& client_state) { return !filter.has_is_enterprise() || filter.is_enterprise() == client_state.IsEnterprise(); } 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, const VariationsLayers& layers) { if (study.has_layer()) { if (!layers.IsLayerMemberActive(study.layer().layer_id(), study.layer().layer_member_id())) { DVLOG(1) << "Filtered out study " << study.name() << " due to layer member not being active."; return false; } if (VariationsSeedProcessor::ShouldStudyUseLowEntropy(study) && layers.IsLayerUsingDefaultEntropy(study.layer().layer_id())) { DVLOG(1) << "Filtered out study " << study.name() << " due to requiring a low entropy source yet being a member " "of a layer using the default entropy source."; return false; } } 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 (!CheckStudyCpuArchitecture(study.filter(), client_state.cpu_architecture)) { DVLOG(1) << "Filtered out study " << study.name() << " due to cpu architecture."; 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; } if (!CheckStudyPolicyRestriction(study.filter(), client_state.policy_restriction)) { DVLOG(1) << "Filtered out study " << study.name() << " due to policy restriction."; return false; } if (!CheckStudyOSVersion(study.filter(), client_state.os_version)) { DVLOG(1) << "Filtered out study " << study.name() << " due to os_version."; 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; } // Check for enterprise status last as checking whether the client is // enterprise can be slow. if (!CheckStudyEnterprise(study.filter(), client_state)) { DVLOG(1) << "Filtered out study " << study.name() << " due to enterprise state."; return false; } } DVLOG(1) << "Kept study " << study.name() << "."; return true; } } // namespace internal void FilterAndValidateStudies(const VariationsSeed& seed, const ClientFilterableState& client_state, const VariationsLayers& layers, std::vector* 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 created_studies; std::vector expired_studies; for (int i = 0; i < seed.study_size(); ++i) { const Study& study = seed.study(i); ProcessedStudy processed_study; bool is_expired = internal::IsStudyExpired(study, client_state.reference_date); if (!processed_study.Init(&study, is_expired)) continue; if (!internal::ShouldAddStudy(*processed_study.study(), client_state, layers)) { continue; } if (processed_study.is_expired()) { expired_studies.push_back(processed_study); } else if (!base::Contains(created_studies, processed_study.study()->name())) { filtered_studies->push_back(processed_study); created_studies.insert(processed_study.study()->name()); } } for (auto& expired_study : expired_studies) { if (!base::Contains(created_studies, expired_study.study()->name())) { filtered_studies->push_back(expired_study); } } } } // namespace variations