blob: e7ccd25888f272b6ad9d4946c33943e3b2c243ff [file] [log] [blame]
// 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"
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);
}
} // 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;
}
// 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) {
for (int i = 0; i < filter.hardware_class_size(); ++i) {
// Check if the entry is a substring of |hardware_class|.
size_t position = hardware_class.find(filter.hardware_class(i));
if (position != std::string::npos)
return true;
}
// None of the requested hardware_classes match.
return false;
}
// Omit if matches any of the exclude entries.
for (int i = 0; i < filter.exclude_hardware_class_size(); ++i) {
// Check if the entry is a substring of |hardware_class|.
size_t position = hardware_class.find(
filter.exclude_hardware_class(i));
if (position != std::string::npos)
return false;
}
// None of the exclusions match, so this accepts.
return true;
}
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 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;
}
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;
}
}
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