diff options
Diffstat (limited to 'chromium/components/blocklist')
18 files changed, 3208 insertions, 0 deletions
diff --git a/chromium/components/blocklist/OWNERS b/chromium/components/blocklist/OWNERS new file mode 100644 index 00000000000..75baba8b67f --- /dev/null +++ b/chromium/components/blocklist/OWNERS @@ -0,0 +1,3 @@ +file://components/data_reduction_proxy/OWNERS + +# COMPONENT: Blink>Previews
\ No newline at end of file diff --git a/chromium/components/blocklist/README.md b/chromium/components/blocklist/README.md new file mode 100644 index 00000000000..5d354e7b985 --- /dev/null +++ b/chromium/components/blocklist/README.md @@ -0,0 +1,58 @@ +# Blocklist component # + +The goal of the blocklist component is to provide various blocklists that allow +different policies for features to consume. Currently, the only implemented +blocklist is the opt out blocklist. + +## Opt out blocklist ## +The opt out blocklist makes decisions based on user history actions. Each user +action is evaluated based on action type, time of the evaluation, host name of +the action (can be any string representation), and previous action history. + +### Expected feature behavior ### +When a feature action is allowed, the feature may perform said action. After +performing the action, the user interaction should be determined to be an opt +out (the user did not like the action) or a non-opt out (the user was not +opposed to the action). The action, type, host name, and whether it was an opt +out should be reported back to the blocklist to build user action history. + +For example, a feature may wish to show an InfoBar (or different types of +InfoBars) displaying information about the page a user is on. After querying the +opt out blocklist for action eligibility, an InfoBar may be allowed to be shown. +If it is shown, the user may interact with it in a number of ways. If the user +dismisses the InfoBar, that could be considered an opt out; if the user does +not dismiss the InfoBar that could be considered a non-opt out. All of the +information related to that action should be reported to the blocklist. + +### Supported evaluation policies ### +In general, policies follow a specific form: the most recent _n_ actions are +evaluated, and if _t_ or more of them are opt outs the action will not be +allowed for a specified duration, _d_. For each policy, the feature specifies +whether the policy is enabled, and, if it is, the feature specifies _n_ +(history), _t_ (threshold), and _d_ (duration) for each policy. + +* Session policy: This policy only applies across all types and host names, but +is limited to actions that happened within the current session. The beginning of +a session is defined as the creation of the blocklist object or when the +blocklist is cleared (see below for details on clearing the blocklist). + +* Persistent policy: This policy applies across all sessions, types and host +names. + +* Host policy: This policy applies across all session and types, but keeps a +separate history for each host names. This rule allows specific host names to be +prevented from having an action performed for the specific user. When this +policy is enabled, the feature specifies a number of hosts that are stored in +memory (to limit memory footprint, query time, etc.) + +* Type policy: This policy applies across all session and host names, but keeps +a separate history for each type. This rule allows specific types to be +prevented from having an action performed for the specific user. The feature +specifies a set of enabled types and versions for each type. This allows +removing past versions of types to be removed from the backing store. + +### Clearing the blocklist ### +Because many actions should be cleared when user clears history, the opt out +blocklist allows clearing history in certain time ranges. All entries are +cleared for the specified time range, and the data in memory is repopulated +from the backing store. diff --git a/chromium/components/blocklist/opt_out_blocklist/BUILD.gn b/chromium/components/blocklist/opt_out_blocklist/BUILD.gn new file mode 100644 index 00000000000..9b8f3d38182 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/BUILD.gn @@ -0,0 +1,34 @@ +# Copyright 2018 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. + +static_library("opt_out_blocklist") { + sources = [ + "opt_out_blocklist.cc", + "opt_out_blocklist.h", + "opt_out_blocklist_data.cc", + "opt_out_blocklist_data.h", + "opt_out_blocklist_delegate.h", + "opt_out_blocklist_item.cc", + "opt_out_blocklist_item.h", + "opt_out_store.h", + ] + + deps = [ "//base" ] +} + +source_set("unit_tests") { + testonly = true + sources = [ + "opt_out_blocklist_item_unittest.cc", + "opt_out_blocklist_unittest.cc", + ] + + deps = [ + ":opt_out_blocklist", + "//base", + "//base/test:test_support", + "//testing/gmock", + "//testing/gtest", + ] +} diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist.cc b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist.cc new file mode 100644 index 00000000000..8a451845af9 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist.cc @@ -0,0 +1,221 @@ +// Copyright 2018 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/blocklist/opt_out_blocklist/opt_out_blocklist.h" + +#include "base/bind.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram.h" +#include "base/optional.h" +#include "base/strings/stringprintf.h" +#include "base/time/clock.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_delegate.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h" +#include "components/blocklist/opt_out_blocklist/opt_out_store.h" + +namespace blocklist { + +OptOutBlocklist::OptOutBlocklist(std::unique_ptr<OptOutStore> opt_out_store, + base::Clock* clock, + OptOutBlocklistDelegate* blocklist_delegate) + : loaded_(false), + opt_out_store_(std::move(opt_out_store)), + clock_(clock), + blocklist_delegate_(blocklist_delegate) { + DCHECK(clock_); + DCHECK(blocklist_delegate_); +} + +OptOutBlocklist::~OptOutBlocklist() = default; + +void OptOutBlocklist::Init() { + DCHECK(!loaded_); + DCHECK(!blocklist_data_); + base::TimeDelta duration; + size_t history = 0; + int threshold = 0; + + std::unique_ptr<BlocklistData::Policy> session_policy; + if (ShouldUseSessionPolicy(&duration, &history, &threshold)) { + session_policy = + std::make_unique<BlocklistData::Policy>(duration, history, threshold); + } + + std::unique_ptr<BlocklistData::Policy> persistent_policy; + if (ShouldUsePersistentPolicy(&duration, &history, &threshold)) { + persistent_policy = + std::make_unique<BlocklistData::Policy>(duration, history, threshold); + } + + size_t max_hosts = 0; + std::unique_ptr<BlocklistData::Policy> host_policy; + if (ShouldUseHostPolicy(&duration, &history, &threshold, &max_hosts)) { + host_policy = + std::make_unique<BlocklistData::Policy>(duration, history, threshold); + } + + std::unique_ptr<BlocklistData::Policy> type_policy; + if (ShouldUseTypePolicy(&duration, &history, &threshold)) { + type_policy = + std::make_unique<BlocklistData::Policy>(duration, history, threshold); + } + + auto blocklist_data = std::make_unique<BlocklistData>( + std::move(session_policy), std::move(persistent_policy), + std::move(host_policy), std::move(type_policy), max_hosts, + GetAllowedTypes()); + + if (opt_out_store_) { + opt_out_store_->LoadBlockList( + std::move(blocklist_data), + base::BindOnce(&OptOutBlocklist::LoadBlockListDone, + weak_factory_.GetWeakPtr())); + } else { + LoadBlockListDone(std::move(blocklist_data)); + } +} + +base::Time OptOutBlocklist::AddEntry(const std::string& host_name, + bool opt_out, + int type) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + base::Time now = clock_->Now(); + + // If the |blocklist_data| has been loaded from |opt_out_store_|, synchronous + // operations will be accurate. Otherwise, queue the task to run + // asynchronously. + if (loaded_) { + AddEntrySync(host_name, opt_out, type, now); + } else { + QueuePendingTask(base::BindOnce(&OptOutBlocklist::AddEntrySync, + base::Unretained(this), host_name, opt_out, + type, now)); + } + + return now; +} + +void OptOutBlocklist::AddEntrySync(const std::string& host_name, + bool opt_out, + int type, + base::Time time) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(loaded_); + + bool host_was_blocklisted = + blocklist_data_->IsHostBlocklisted(host_name, time); + bool user_was_blocklisted = blocklist_data_->IsUserOptedOutInGeneral(time); + blocklist_data_->AddEntry(host_name, opt_out, type, time, false); + + if (!host_was_blocklisted && + blocklist_data_->IsHostBlocklisted(host_name, time)) { + // Notify |blocklist_delegate_| about a new blocklisted host. + blocklist_delegate_->OnNewBlocklistedHost(host_name, time); + } + + if (user_was_blocklisted != blocklist_data_->IsUserOptedOutInGeneral(time)) { + // Notify |blocklist_delegate_| about a new blocklisted host. + blocklist_delegate_->OnUserBlocklistedStatusChange( + blocklist_data_->IsUserOptedOutInGeneral(time)); + } + + if (!opt_out_store_) + return; + opt_out_store_->AddEntry(opt_out, host_name, type, time); +} + +BlocklistReason OptOutBlocklist::IsLoadedAndAllowed( + const std::string& host_name, + int type, + bool ignore_long_term_block_list_rules, + std::vector<BlocklistReason>* passed_reasons) const { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!loaded_) + return BlocklistReason::kBlocklistNotLoaded; + passed_reasons->push_back(BlocklistReason::kBlocklistNotLoaded); + + return blocklist_data_->IsAllowed(host_name, type, + ignore_long_term_block_list_rules, + clock_->Now(), passed_reasons); +} + +void OptOutBlocklist::ClearBlockList(base::Time begin_time, + base::Time end_time) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK_LE(begin_time, end_time); + // If the |blocklist_data| has been loaded from |opt_out_store_|, + // synchronous operations will be accurate. Otherwise, queue the task to run + // asynchronously. + if (loaded_) { + ClearBlockListSync(begin_time, end_time); + } else { + QueuePendingTask(base::BindOnce(&OptOutBlocklist::ClearBlockListSync, + base::Unretained(this), begin_time, + end_time)); + } +} + +void OptOutBlocklist::ClearBlockListSync(base::Time begin_time, + base::Time end_time) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(loaded_); + DCHECK_LE(begin_time, end_time); + + // Clear the in-memory rules entirely. + blocklist_data_->ClearData(); + loaded_ = false; + + // Notify |blocklist_delegate_| that the blocklist is cleared. + blocklist_delegate_->OnBlocklistCleared(clock_->Now()); + + // Delete relevant entries and reload the blocklist into memory. + if (opt_out_store_) { + opt_out_store_->ClearBlockList(begin_time, end_time); + opt_out_store_->LoadBlockList( + std::move(blocklist_data_), + base::BindOnce(&OptOutBlocklist::LoadBlockListDone, + weak_factory_.GetWeakPtr())); + } else { + LoadBlockListDone(std::move(blocklist_data_)); + } +} + +void OptOutBlocklist::QueuePendingTask(base::OnceClosure callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!loaded_); + DCHECK(!callback.is_null()); + pending_callbacks_.push(std::move(callback)); +} + +void OptOutBlocklist::LoadBlockListDone( + std::unique_ptr<BlocklistData> blocklist_data) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(blocklist_data); + DCHECK(!loaded_); + DCHECK(!blocklist_data_); + loaded_ = true; + blocklist_data_ = std::move(blocklist_data); + + // Notify |blocklist_delegate_| on current user blocklisted status. + blocklist_delegate_->OnUserBlocklistedStatusChange( + blocklist_data_->IsUserOptedOutInGeneral(clock_->Now())); + + // Notify the |blocklist_delegate_| on historical blocklisted hosts. + for (const auto& entry : blocklist_data_->block_list_item_host_map()) { + if (blocklist_data_->IsHostBlocklisted(entry.first, clock_->Now())) { + blocklist_delegate_->OnNewBlocklistedHost( + entry.first, entry.second.most_recent_opt_out_time().value()); + } + } + + // Run all pending tasks. |loaded_| may change if ClearBlockList is queued. + while (pending_callbacks_.size() > 0 && loaded_) { + std::move(pending_callbacks_.front()).Run(); + pending_callbacks_.pop(); + } +} + +} // namespace blocklist diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist.h b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist.h new file mode 100644 index 00000000000..ece381483fe --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist.h @@ -0,0 +1,183 @@ +// Copyright 2018 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. + +#ifndef COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_H_ +#define COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_H_ + +#include <stdint.h> + +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/containers/queue.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/optional.h" +#include "base/sequence_checker.h" +#include "base/time/time.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_data.h" + +namespace base { +class Clock; +} + +namespace blocklist { + +class BlocklistData; +class OptOutBlocklistDelegate; +class OptOutStore; + +class OptOutBlocklist { + public: + // |opt_out_store| is the backing store to retrieve and store blocklist + // information, and can be null. When |opt_out_store| is null, the in-memory + // data will be immediately loaded to empty. If |opt_out_store| is non-null, + // it will be used to load the in-memory map asynchronously. + // |blocklist_delegate| is a single object listening for blocklist events, and + // it is guaranteed to outlive the life time of |this|. + OptOutBlocklist(std::unique_ptr<OptOutStore> opt_out_store, + base::Clock* clock, + OptOutBlocklistDelegate* blocklist_delegate); + virtual ~OptOutBlocklist(); + + // Creates the BlocklistData that backs the blocklist. + void Init(); + + // Asynchronously deletes all entries in the in-memory blocklist. Informs + // the backing store to delete entries between |begin_time| and |end_time|, + // and reloads entries into memory from the backing store. If the embedder + // passed in a null store, resets all history in the in-memory blocklist. + void ClearBlockList(base::Time begin_time, base::Time end_time); + + // Asynchronously adds a new navigation to to the in-memory blocklist and + // backing store. |opt_out| is whether the user opted out of the action. If + // the in memory map has reached the max number of hosts allowed, and + // |host_name| is a new host, a host will be evicted based on recency of the + // hosts most recent opt out. It returns the time used for recording the + // moment when the navigation is added for logging. + base::Time AddEntry(const std::string& host_name, bool opt_out, int type); + + // Synchronously determines if the action should be allowed for |host_name| + // and |type|. Returns the reason the blocklist disallowed the action, or + // kAllowed if the action is allowed. Record checked reasons in + // |passed_reasons|. |ignore_long_term_block_list_rules| will cause session, + // type, and host rules, but the session rule will still be queried. + BlocklistReason IsLoadedAndAllowed( + const std::string& host_name, + int type, + bool ignore_long_term_block_list_rules, + std::vector<BlocklistReason>* passed_reasons) const; + + protected: + // Whether the session rule should be enabled. |duration| specifies how long a + // user remains blocklisted. |history| specifies how many entries should be + // evaluated; |threshold| specifies how many opt outs would cause + // blocklisting. I.e., the most recent |history| are looked at and if + // |threshold| (or more) of them are opt outs, the user is considered + // blocklisted unless the most recent opt out was longer than |duration| ago. + // This rule only considers entries within this session (it does not use the + // data that was persisted in previous sessions). When the blocklist is + // cleared, this rule is reset as if it were a new session. Queried in Init(). + virtual bool ShouldUseSessionPolicy(base::TimeDelta* duration, + size_t* history, + int* threshold) const = 0; + + // Whether the persistent rule should be enabled. |duration| specifies how + // long a user remains blocklisted. |history| specifies how many entries + // should be evaluated; |threshold| specifies how many opt outs would cause + // blocklisting. I.e., the most recent |history| are looked at and if + // |threshold| (or more) of them are opt outs, the user is considered + // blocklisted unless the most recent opt out was longer than |duration| ago. + // Queried in Init(). + virtual bool ShouldUsePersistentPolicy(base::TimeDelta* duration, + size_t* history, + int* threshold) const = 0; + + // Whether the host rule should be enabled. |duration| specifies how long a + // host remains blocklisted. |history| specifies how many entries should be + // evaluated per host; |threshold| specifies how many opt outs would cause + // blocklisting. I.e., the most recent |history| entries per host are looked + // at and if |threshold| (or more) of them are opt outs, the host is + // considered blocklisted unless the most recent opt out was longer than + // |duration| ago. |max_hosts| will limit the number of hosts stored in this + // class when non-zero. + // Queried in Init(). + virtual bool ShouldUseHostPolicy(base::TimeDelta* duration, + size_t* history, + int* threshold, + size_t* max_hosts) const = 0; + + // Whether the type rule should be enabled. |duration| specifies how long a + // type remains blocklisted. |history| specifies how many entries should be + // evaluated per type; |threshold| specifies how many opt outs would cause + // blocklisting. + // I.e., the most recent |history| entries per type are looked at and if + // |threshold| (or more) of them are opt outs, the type is considered + // blocklisted unless the most recent opt out was longer than |duration| ago. + // Queried in Init(). + virtual bool ShouldUseTypePolicy(base::TimeDelta* duration, + size_t* history, + int* threshold) const = 0; + + // The allowed types and what version they are. Should be empty unless the + // caller will not be using the blocklist in the session. It is used to remove + // stale entries from the database and to DCHECK that other methods are not + // using disallowed types. Queried in Init(). + virtual BlocklistData::AllowedTypesAndVersions GetAllowedTypes() const = 0; + + private: + // Synchronous version of AddEntry method. |time| is the time + // stamp of when the navigation was determined to be an opt-out or non-opt + // out. + void AddEntrySync(const std::string& host_name, + bool opt_out, + int type, + base::Time time); + + // Synchronous version of ClearBlockList method. + void ClearBlockListSync(base::Time begin_time, base::Time end_time); + + // Callback passed to the backing store when loading block list information. + // Takes ownership of |blocklist_data|. + void LoadBlockListDone(std::unique_ptr<BlocklistData> blocklist_data); + + // Called while waiting for the blocklist to be loaded from the backing + // store. + // Enqueues a task to run when when loading blocklist information has + // completed. Maintains the order that tasks were called in. + void QueuePendingTask(base::OnceClosure callback); + + // An in-memory representation of the various rules of the blocklist. This is + // null while reading from the backing store. + std::unique_ptr<BlocklistData> blocklist_data_; + + // Whether the blocklist is done being loaded from the backing store. + bool loaded_; + + // The backing store of the blocklist information. + std::unique_ptr<OptOutStore> opt_out_store_; + + // Callbacks to be run after loading information from the backing store has + // completed. + base::queue<base::OnceClosure> pending_callbacks_; + + base::Clock* clock_; + + // The delegate listening to this blocklist. |blocklist_delegate_| lifetime is + // guaranteed to outlive |this|. + OptOutBlocklistDelegate* blocklist_delegate_; + + SEQUENCE_CHECKER(sequence_checker_); + + base::WeakPtrFactory<OptOutBlocklist> weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(OptOutBlocklist); +}; + +} // namespace blocklist + +#endif // COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_H_ diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_data.cc b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_data.cc new file mode 100644 index 00000000000..711e409fd42 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_data.cc @@ -0,0 +1,173 @@ +// Copyright 2018 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/blocklist/opt_out_blocklist/opt_out_blocklist_data.h" + +#include "base/memory/ptr_util.h" + +namespace blocklist { + +BlocklistData::BlocklistData(std::unique_ptr<Policy> session_policy, + std::unique_ptr<Policy> persistent_policy, + std::unique_ptr<Policy> host_policy, + std::unique_ptr<Policy> type_policy, + size_t max_hosts, + AllowedTypesAndVersions allowed_types) + : session_policy_(std::move(session_policy)), + persistent_policy_(std::move(persistent_policy)), + host_policy_(std::move(host_policy)), + max_hosts_(max_hosts), + type_policy_(std::move(type_policy)), + allowed_types_(std::move(allowed_types)) { + DCHECK_GE(100u, max_hosts); +} +BlocklistData::~BlocklistData() = default; + +void BlocklistData::ClearData() { + session_block_list_item_.reset(); + persistent_block_list_item_.reset(); + block_list_item_host_map_.clear(); + block_list_item_type_map_.clear(); +} + +void BlocklistData::AddEntry(const std::string& host_name, + bool opt_out, + int type, + base::Time time, + bool is_from_persistent_storage) { + // Add to the session based rule if it is enabled. + if (session_policy_ && !is_from_persistent_storage) { + if (!session_block_list_item_) { + session_block_list_item_ = std::make_unique<OptOutBlocklistItem>( + session_policy_->history, session_policy_->threshold, + session_policy_->duration); + } + session_block_list_item_->AddEntry(opt_out, time); + } + + // Add to the persistent rule if it is enabled. + if (persistent_policy_) { + if (!persistent_block_list_item_) { + persistent_block_list_item_ = std::make_unique<OptOutBlocklistItem>( + persistent_policy_->history, persistent_policy_->threshold, + persistent_policy_->duration); + } + persistent_block_list_item_->AddEntry(opt_out, time); + } + + // Add to the host rule if it is enabled. Remove hosts if there are more than + // |max_hosts_| in the map. + if (host_policy_) { + auto item = block_list_item_host_map_.find(host_name); + if (item == block_list_item_host_map_.end()) { + auto value = block_list_item_host_map_.emplace( + std::piecewise_construct, std::forward_as_tuple(host_name), + std::forward_as_tuple(host_policy_->history, host_policy_->threshold, + host_policy_->duration)); + DCHECK(value.second); + item = value.first; + } + item->second.AddEntry(opt_out, time); + if (max_hosts_ > 0 && block_list_item_host_map_.size() > max_hosts_) + EvictOldestHost(); + } + + if (type_policy_) { + auto item = block_list_item_type_map_.find(type); + if (item == block_list_item_type_map_.end()) { + auto value = block_list_item_type_map_.emplace( + std::piecewise_construct, std::forward_as_tuple(type), + std::forward_as_tuple(type_policy_->history, type_policy_->threshold, + type_policy_->duration)); + DCHECK(value.second); + item = value.first; + } + item->second.AddEntry(opt_out, time); + } +} + +BlocklistReason BlocklistData::IsAllowed( + const std::string& host_name, + int type, + bool ignore_long_term_block_list_rules, + base::Time time, + std::vector<BlocklistReason>* passed_reasons) const { + // Check the session rule. + if (session_policy_) { + if (session_block_list_item_ && + session_block_list_item_->IsBlockListed(time)) { + return BlocklistReason::kUserOptedOutInSession; + } + passed_reasons->push_back(BlocklistReason::kUserOptedOutInSession); + } + + // Check whether the persistent rules should be checked this time. + if (ignore_long_term_block_list_rules) + return BlocklistReason::kAllowed; + + // Check the persistent rule. + if (persistent_policy_) { + if (IsUserOptedOutInGeneral(time)) { + return BlocklistReason::kUserOptedOutInGeneral; + } + passed_reasons->push_back(BlocklistReason::kUserOptedOutInGeneral); + } + + // Check the host rule. + if (host_policy_) { + if (IsHostBlocklisted(host_name, time)) + return BlocklistReason::kUserOptedOutOfHost; + passed_reasons->push_back(BlocklistReason::kUserOptedOutOfHost); + } + + // Only allowed types should be recorded. + DCHECK(allowed_types_.find(type) != allowed_types_.end()); + + // Check the type rule. + if (type_policy_) { + auto item = block_list_item_type_map_.find(type); + if (item != block_list_item_type_map_.end() && + item->second.IsBlockListed(time)) { + return BlocklistReason::kUserOptedOutOfType; + } + passed_reasons->push_back(BlocklistReason::kUserOptedOutOfType); + } + + return BlocklistReason::kAllowed; +} + +void BlocklistData::EvictOldestHost() { + DCHECK_LT(max_hosts_, block_list_item_host_map_.size()); + base::Optional<base::Time> oldest_opt_out; + std::string key_to_delete; + for (auto& item : block_list_item_host_map_) { + base::Optional<base::Time> most_recent_opt_out = + item.second.most_recent_opt_out_time(); + if (!most_recent_opt_out) { + // If there is no opt out time, this is a good choice to evict. + key_to_delete = item.first; + break; + } + if (!oldest_opt_out || + most_recent_opt_out.value() < oldest_opt_out.value()) { + oldest_opt_out = most_recent_opt_out.value(); + key_to_delete = item.first; + } + } + block_list_item_host_map_.erase(key_to_delete); +} + +bool BlocklistData::IsHostBlocklisted(const std::string& host_name, + base::Time time) const { + auto item = block_list_item_host_map_.find(host_name); + return item != block_list_item_host_map_.end() && + item->second.IsBlockListed(time); +} + +bool BlocklistData::IsUserOptedOutInGeneral(base::Time time) const { + return persistent_block_list_item_ && + persistent_block_list_item_->IsBlockListed(time); +} + +} // namespace blocklist diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_data.h b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_data.h new file mode 100644 index 00000000000..659d1e26c74 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_data.h @@ -0,0 +1,176 @@ +// Copyright 2018 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. + +#ifndef COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_DATA_H_ +#define COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_DATA_H_ + +#include <stdint.h> + +#include <map> +#include <memory> +#include <set> +#include <string> + +#include "base/macros.h" +#include "base/optional.h" +#include "base/time/time.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h" + +namespace blocklist { + +// The various reasons the Blocklist may tell that the user is blocklisted. +// This should remain synchronized with enums.xml +enum class BlocklistReason { + // The blocklist may not be loaded very early in the session or when the user + // has cleared the blocklist history (usually by clearing their browsing + // history). + kBlocklistNotLoaded = 0, + kUserOptedOutInSession = 1, + kUserOptedOutInGeneral = 2, + kUserOptedOutOfHost = 3, + kUserOptedOutOfType = 4, + kAllowed = 5, + kMaxValue = kAllowed, + +}; + +// This class describes all of the data used to determine whether an action is +// allowed based on four possible rules: Session: if the user has opted out +// of j of the last k entries this session, the action will be blocklisted for a +// set duration. Persistent: if the user has opted out of j of the last k +// entries, the action will be blocklisted for a set duration. Host: if the user +// has opted out of threshold of the last history entries for a specific host, +// the action will be blocklisted for a set duration. Type: if the user has +// opted out of j of the last k entries for a specific type, the action will be +// blocklisted for a set duration. This is the in-memory version of the block +// list policy. This object is moved from the embedder thread to a background +// thread, It is not safe to access concurrently on two threads. +class BlocklistData { + public: + // A struct describing the general blocklisting pattern used by all of the + // blocklisting rules. + // The most recent |history| entries are looked at and if |threshold| (or + // more) of them are opt outs, new actions are considered blocklisted unless + // the most recent opt out was longer than |duration| ago. + struct Policy { + Policy(base::TimeDelta duration, size_t history, int threshold) + : duration(duration), history(history), threshold(threshold) {} + + ~Policy() = default; + + // Specifies how long the blocklisting rule lasts after the most recent opt + // out. + const base::TimeDelta duration; + // Amount of entries evaluated for the rule. + const size_t history; + // The number of opt outs that will trigger blocklisting for the rule. + const int threshold; + }; + + // A map of types that are allowed to be used in the blocklist as well as the + // version that those types are in. Versioning allows removals from persistent + // memory at session start. + using AllowedTypesAndVersions = std::map<int, int>; + + // |session_policy| if non-null, is the policy that is not persisted across + // sessions and is not specific to host or type. |persistent_policy| if + // non-null, is the policy that is persisted across sessions and is not + // specific to host or type. |host_policy| if non-null, is the policy that is + // persisted across sessions applies at the per-host level. |host_policy| if + // non-null, is the policy that is persisted across sessions and applies at + // the per-type level. |max_hosts| is the maximum number of hosts stored in + // memory. |allowed_types| contains the action types that are allowed in the + // session and their corresponding versions. Conversioning is used to clear + // stale data from the persistent storage. + BlocklistData(std::unique_ptr<Policy> session_policy, + std::unique_ptr<Policy> persistent_policy, + std::unique_ptr<Policy> host_policy, + std::unique_ptr<Policy> type_policy, + size_t max_hosts, + AllowedTypesAndVersions allowed_types); + ~BlocklistData(); + + // Adds a new entry for all rules to use when evaluating blocklisting state. + // |is_from_persistent_storage| is used to delineate between data added from + // this session, and previous sessions. + void AddEntry(const std::string& host_name, + bool opt_out, + int type, + base::Time time, + bool is_from_persistent_storage); + + // Whether the user is opted out when considering all enabled rules. if + // |ignore_long_term_block_list_rules| is true, this will only check the + // session rule. For every reason that is checked, but does not trigger + // blocklisting, a new reason will be appended to the end |passed_reasons|. + // |time| is the time that decision should be evaluated at (usually now). + BlocklistReason IsAllowed(const std::string& host_name, + int type, + bool ignore_long_term_block_list_rules, + base::Time time, + std::vector<BlocklistReason>* passed_reasons) const; + + // This clears all data in all rules. + void ClearData(); + + // The allowed types and what version they are. If it is non-empty, it is used + // to remove stale entries from the database and to DCHECK that other methods + // are not using disallowed types. + const AllowedTypesAndVersions& allowed_types() const { + return allowed_types_; + } + + // Whether the specific |host_name| is blocklisted based only on the host + // rule. + bool IsHostBlocklisted(const std::string& host_name, base::Time time) const; + + // Whether the user is opted out based solely on the persistent blocklist + // rule. + bool IsUserOptedOutInGeneral(base::Time time) const; + + // Exposed for logging purposes only. + const std::map<std::string, OptOutBlocklistItem>& block_list_item_host_map() + const { + return block_list_item_host_map_; + } + + private: + // Removes the oldest (or safest) host item from |block_list_item_host_map_|. + // Oldest is defined by most recent opt out time, and safest is defined as an + // item with no opt outs. + void EvictOldestHost(); + + // The session rule policy. If non-null the session rule is enforced. + std::unique_ptr<Policy> session_policy_; + // The session rule history. + std::unique_ptr<OptOutBlocklistItem> session_block_list_item_; + + // The persistent rule policy. If non-null the persistent rule is enforced. + std::unique_ptr<Policy> persistent_policy_; + // The persistent rule history. + std::unique_ptr<OptOutBlocklistItem> persistent_block_list_item_; + + // The host rule policy. If non-null the host rule is enforced. + std::unique_ptr<Policy> host_policy_; + // The maximum number of hosts allowed in the host blocklist. + size_t max_hosts_; + // The host rule history. Each host is stored as a separate blocklist history. + std::map<std::string, OptOutBlocklistItem> block_list_item_host_map_; + + // The type rule policy. If non-null the type rule is enforced. + std::unique_ptr<Policy> type_policy_; + // The type rule history. Each type is stored as a separate blocklist history. + std::map<int, OptOutBlocklistItem> block_list_item_type_map_; + + // The allowed types and what version they are. If it is non-empty, it is used + // to remove stale entries from the database and to DCHECK that other methods + // are not using disallowed types. + AllowedTypesAndVersions allowed_types_; + + DISALLOW_COPY_AND_ASSIGN(BlocklistData); +}; + +} // namespace blocklist + +#endif // COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_DATA_H_ diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_delegate.h b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_delegate.h new file mode 100644 index 00000000000..14b767aa3a3 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_delegate.h @@ -0,0 +1,41 @@ +// Copyright 2017 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. + +#ifndef COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_DELEGATE_H_ +#define COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_DELEGATE_H_ + +#include <string> + +#include "base/macros.h" +#include "base/time/time.h" + +namespace blocklist { + +// An interface for a delegate to the opt out blocklist. This interface is for +// responding to events occurring in the opt out blocklist (e.g. New blocklisted +// host and user is blocklisted). +class OptOutBlocklistDelegate { + public: + OptOutBlocklistDelegate() = default; + virtual ~OptOutBlocklistDelegate() = default; + + // Notifies |this| that |host| has been blocklisted at |time|. This method is + // guaranteed to be called when a previously allowlisted host is now + // blocklisted. + virtual void OnNewBlocklistedHost(const std::string& host, base::Time time) {} + + // Notifies |this| that the user blocklisted has changed, and it is + // guaranteed to be called when the user blocklisted status is changed. + // + // TODO(crbug/1099030): Update the comment and interface to support providing + // a signal that the blocklist is loaded and available. + virtual void OnUserBlocklistedStatusChange(bool blocklisted) {} + + // Notifies |this| that the blocklist is cleared at |time|. + virtual void OnBlocklistCleared(base::Time time) {} +}; + +} // namespace blocklist + +#endif // COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_DELEGATE_H_ diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.cc b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.cc new file mode 100644 index 00000000000..e8645277147 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.cc @@ -0,0 +1,74 @@ +// Copyright 2016 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/blocklist/opt_out_blocklist/opt_out_blocklist_item.h" + +#include <algorithm> +#include <tuple> + +#include "components/blocklist/opt_out_blocklist/opt_out_store.h" + +namespace blocklist { + +OptOutBlocklistItem::OptOutRecord::OptOutRecord(base::Time entry_time, + bool opt_out) + : entry_time_(entry_time), opt_out_(opt_out) {} + +OptOutBlocklistItem::OptOutRecord::~OptOutRecord() = default; + +OptOutBlocklistItem::OptOutRecord::OptOutRecord(OptOutRecord&&) noexcept = + default; +OptOutBlocklistItem::OptOutRecord& OptOutBlocklistItem::OptOutRecord::operator=( + OptOutRecord&&) noexcept = default; + +bool OptOutBlocklistItem::OptOutRecord::operator<( + const OptOutRecord& other) const { + // Fresher entries are lower priority to evict, as are non-opt-outs. + return std::tie(entry_time_, opt_out_) > + std::tie(other.entry_time_, other.opt_out_); +} + +OptOutBlocklistItem::OptOutBlocklistItem(size_t stored_history_length, + int opt_out_block_list_threshold, + base::TimeDelta block_list_duration) + : max_stored_history_length_(stored_history_length), + opt_out_block_list_threshold_(opt_out_block_list_threshold), + max_block_list_duration_(block_list_duration), + total_opt_out_(0) {} + +OptOutBlocklistItem::~OptOutBlocklistItem() = default; + +void OptOutBlocklistItem::AddEntry(bool opt_out, base::Time entry_time) { + DCHECK_LE(opt_out_records_.size(), max_stored_history_length_); + + opt_out_records_.emplace(entry_time, opt_out); + + if (opt_out && (!most_recent_opt_out_time_ || + entry_time > most_recent_opt_out_time_.value())) { + most_recent_opt_out_time_ = entry_time; + } + total_opt_out_ += opt_out ? 1 : 0; + + // Remove the oldest entry if the size exceeds the max history size. + if (opt_out_records_.size() > max_stored_history_length_) { + DCHECK_EQ(opt_out_records_.size(), max_stored_history_length_ + 1); + DCHECK_LE(opt_out_records_.top().entry_time(), entry_time); + total_opt_out_ -= opt_out_records_.top().opt_out() ? 1 : 0; + opt_out_records_.pop(); + } + DCHECK_LE(opt_out_records_.size(), max_stored_history_length_); +} + +bool OptOutBlocklistItem::IsBlockListed(base::Time now) const { + DCHECK_LE(opt_out_records_.size(), max_stored_history_length_); + return most_recent_opt_out_time_ && + now - most_recent_opt_out_time_.value() < max_block_list_duration_ && + total_opt_out_ >= opt_out_block_list_threshold_; +} + +size_t OptOutBlocklistItem::OptOutRecordsSizeForTesting() const { + return opt_out_records_.size(); +} + +} // namespace blocklist diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h new file mode 100644 index 00000000000..1ad6c01c1e5 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h @@ -0,0 +1,99 @@ +// Copyright 2016 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. + +#ifndef COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_ITEM_H_ +#define COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_ITEM_H_ + +#include <stdint.h> + +#include <map> +#include <memory> +#include <queue> +#include <string> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/optional.h" +#include "base/time/time.h" + +namespace blocklist { + +// Stores the recent block list history for a single host. Stores +// |stored_history_length| of the most recent actions. To determine action +// eligibility fewer than |opt_out_block_list_threshold| out of the past +// |stored_history_length| navigations must be opt outs. |block_list_duration| +// is the amount of time that elapses until the host is no longer on the block +// list. +class OptOutBlocklistItem { + public: + OptOutBlocklistItem(size_t stored_history_length, + int opt_out_block_list_threshold, + base::TimeDelta block_list_duration); + + ~OptOutBlocklistItem(); + + // Adds a new navigation at the specified |entry_time|. + void AddEntry(bool opt_out, base::Time entry_time); + + // Whether the action corresponding to |this| should be disallowed. + bool IsBlockListed(base::Time now) const; + + base::Optional<base::Time> most_recent_opt_out_time() const { + return most_recent_opt_out_time_; + } + + size_t OptOutRecordsSizeForTesting() const; + + private: + // An action to |this| is represented by time and whether the action was an + // opt out. + class OptOutRecord { + public: + OptOutRecord(base::Time entry_time, bool opt_out); + ~OptOutRecord(); + OptOutRecord(OptOutRecord&&) noexcept; + OptOutRecord& operator=(OptOutRecord&&) noexcept; + + // Used to determine eviction priority. + bool operator<(const OptOutRecord& other) const; + + // The time that the opt out state was determined. + base::Time entry_time() const { return entry_time_; } + + // Whether the user opted out of the action. + bool opt_out() const { return opt_out_; } + + private: + // The time that the opt out state was determined. + base::Time entry_time_; + // Whether the user opted out of the action. + bool opt_out_; + + DISALLOW_COPY_AND_ASSIGN(OptOutRecord); + }; + + // The number of entries to store to determine action eligibility. + const size_t max_stored_history_length_; + // The number opt outs in recent history that will trigger blocklisting. + const int opt_out_block_list_threshold_; + // The amount of time to block list a domain after the most recent opt out. + const base::TimeDelta max_block_list_duration_; + + // The |max_stored_history_length_| most recent action. Is maintained as a + // priority queue that has high priority for items that should be evicted + // (i.e., they are old). + std::priority_queue<OptOutRecord> opt_out_records_; + + // Time of the most recent opt out. + base::Optional<base::Time> most_recent_opt_out_time_; + + // The total number of opt outs currently in |opt_out_records_|. + int total_opt_out_; + + DISALLOW_COPY_AND_ASSIGN(OptOutBlocklistItem); +}; + +} // namespace blocklist + +#endif // COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_BLOCKLIST_ITEM_H_ diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_item_unittest.cc b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_item_unittest.cc new file mode 100644 index 00000000000..c54bc22dfea --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_item_unittest.cc @@ -0,0 +1,80 @@ +// Copyright 2016 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/blocklist/opt_out_blocklist/opt_out_blocklist_item.h" + +#include <memory> + +#include "base/optional.h" +#include "base/time/time.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +using OptOutBlocklistItemTest = testing::Test; + +} // namespace + +namespace blocklist { + +TEST_F(OptOutBlocklistItemTest, BlockListState) { + const int history = 4; + const int threshold = 2; + const base::TimeDelta max_blocklist_duration = + base::TimeDelta::FromSeconds(30); + const base::Time now = base::Time::UnixEpoch(); + const base::TimeDelta delay_between_entries = base::TimeDelta::FromSeconds(1); + const base::Time later = + now + max_blocklist_duration + (delay_between_entries * 3); + + OptOutBlocklistItem block_list_item(history, threshold, + max_blocklist_duration); + + // Empty block list item should report that the host is allowed. + EXPECT_FALSE(block_list_item.IsBlockListed(now)); + EXPECT_FALSE(block_list_item.IsBlockListed(later)); + + EXPECT_FALSE(block_list_item.most_recent_opt_out_time()); + block_list_item.AddEntry(false, now); + EXPECT_FALSE(block_list_item.most_recent_opt_out_time()); + + block_list_item.AddEntry(true, now); + EXPECT_TRUE(block_list_item.most_recent_opt_out_time()); + EXPECT_EQ(now, block_list_item.most_recent_opt_out_time().value()); + // Block list item of size less that |threshold| should report that the host + // is allowed. + EXPECT_FALSE(block_list_item.IsBlockListed(now)); + EXPECT_FALSE(block_list_item.IsBlockListed(later)); + + block_list_item.AddEntry(true, now + delay_between_entries); + // Block list item with |threshold| fresh entries should report the host as + // disallowed. + EXPECT_TRUE(block_list_item.IsBlockListed(now)); + // Block list item with only entries from longer than |duration| ago should + // report the host is allowed. + EXPECT_FALSE(block_list_item.IsBlockListed(later)); + block_list_item.AddEntry(true, later - (delay_between_entries * 2)); + // Block list item with a fresh opt out and total number of opt outs larger + // than |threshold| should report the host is disallowed. + EXPECT_TRUE(block_list_item.IsBlockListed(later)); + + // The block list item should maintain entries based on time, so adding + // |history| entries should not push out newer entries. + block_list_item.AddEntry(true, later - delay_between_entries * 2); + block_list_item.AddEntry(false, later - delay_between_entries * 3); + block_list_item.AddEntry(false, later - delay_between_entries * 3); + block_list_item.AddEntry(false, later - delay_between_entries * 3); + block_list_item.AddEntry(false, later - delay_between_entries * 3); + EXPECT_TRUE(block_list_item.IsBlockListed(later)); + + // The block list item should maintain entries based on time, so adding + // |history| newer entries should push out older entries. + block_list_item.AddEntry(false, later - delay_between_entries * 1); + block_list_item.AddEntry(false, later - delay_between_entries * 1); + block_list_item.AddEntry(false, later - delay_between_entries * 1); + block_list_item.AddEntry(false, later - delay_between_entries * 1); + EXPECT_FALSE(block_list_item.IsBlockListed(later)); +} + +} // namespace blocklist diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_unittest.cc b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_unittest.cc new file mode 100644 index 00000000000..a5d5cee9cb8 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_blocklist_unittest.cc @@ -0,0 +1,1194 @@ +// Copyright 2018 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/blocklist/opt_out_blocklist/opt_out_blocklist.h" + +#include <algorithm> +#include <map> +#include <memory> +#include <string> +#include <unordered_map> +#include <utility> +#include <vector> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/memory/ptr_util.h" +#include "base/run_loop.h" +#include "base/stl_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/simple_test_clock.h" +#include "base/test/task_environment.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_delegate.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h" +#include "components/blocklist/opt_out_blocklist/opt_out_store.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace blocklist { + +namespace { + +const char kTestHost1[] = "testhost1.com"; +const char kTestHost2[] = "testhost2.com"; + +// Mock class to test that OptOutBlocklist notifies the delegate with correct +// events (e.g. New host blocklisted, user blocklisted, and blocklist cleared). +class TestOptOutBlocklistDelegate : public OptOutBlocklistDelegate { + public: + TestOptOutBlocklistDelegate() : blocklist_cleared_time_(base::Time::Now()) {} + + // OptOutBlocklistDelegate: + void OnNewBlocklistedHost(const std::string& host, base::Time time) override { + blocklisted_hosts_[host] = time; + } + void OnUserBlocklistedStatusChange(bool blocklisted) override { + user_blocklisted_ = blocklisted; + } + void OnBlocklistCleared(base::Time time) override { + blocklist_cleared_ = true; + blocklist_cleared_time_ = time; + } + + // Gets the set of blocklisted hosts recorded. + const std::unordered_map<std::string, base::Time>& blocklisted_hosts() const { + return blocklisted_hosts_; + } + + // Gets the state of user blocklisted status. + bool user_blocklisted() const { return user_blocklisted_; } + + // Gets the state of blocklisted cleared status of |this| for testing. + bool blocklist_cleared() const { return blocklist_cleared_; } + + // Gets the event time of blocklist is as cleared. + base::Time blocklist_cleared_time() const { return blocklist_cleared_time_; } + + private: + // The user blocklisted status of |this| blocklist_delegate. + bool user_blocklisted_ = false; + + // Check if the blocklist is notified as cleared on |this| blocklist_delegate. + bool blocklist_cleared_ = false; + + // The time when blocklist is cleared. + base::Time blocklist_cleared_time_; + + // |this| blocklist_delegate's collection of blocklisted hosts. + std::unordered_map<std::string, base::Time> blocklisted_hosts_; +}; + +class TestOptOutStore : public OptOutStore { + public: + TestOptOutStore() = default; + ~TestOptOutStore() override = default; + + int clear_blocklist_count() { return clear_blocklist_count_; } + + void SetBlocklistData(std::unique_ptr<BlocklistData> data) { + data_ = std::move(data); + } + + private: + // OptOutStore implementation: + void AddEntry(bool opt_out, + const std::string& host_name, + int type, + base::Time now) override {} + + void LoadBlockList(std::unique_ptr<BlocklistData> blocklist_data, + LoadBlockListCallback callback) override { + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce(std::move(callback), + data_ ? std::move(data_) : std::move(blocklist_data))); + } + + void ClearBlockList(base::Time begin_time, base::Time end_time) override { + ++clear_blocklist_count_; + } + + int clear_blocklist_count_ = 0; + + std::unique_ptr<BlocklistData> data_; +}; + +class TestOptOutBlocklist : public OptOutBlocklist { + public: + TestOptOutBlocklist(std::unique_ptr<OptOutStore> opt_out_store, + base::Clock* clock, + OptOutBlocklistDelegate* blocklist_delegate) + : OptOutBlocklist(std::move(opt_out_store), clock, blocklist_delegate) {} + ~TestOptOutBlocklist() override = default; + + void SetSessionRule(std::unique_ptr<BlocklistData::Policy> policy) { + session_policy_ = std::move(policy); + } + + void SetPersistentRule(std::unique_ptr<BlocklistData::Policy> policy) { + persistent_policy_ = std::move(policy); + } + + void SetHostRule(std::unique_ptr<BlocklistData::Policy> policy, + size_t max_hosts) { + host_policy_ = std::move(policy); + max_hosts_ = max_hosts; + } + + void SetTypeRule(std::unique_ptr<BlocklistData::Policy> policy) { + type_policy_ = std::move(policy); + } + + void SetAllowedTypes(BlocklistData::AllowedTypesAndVersions allowed_types) { + allowed_types_ = std::move(allowed_types); + } + + private: + bool ShouldUseSessionPolicy(base::TimeDelta* duration, + size_t* history, + int* threshold) const override { + if (!session_policy_) + return false; + *duration = session_policy_->duration; + *history = session_policy_->history; + *threshold = session_policy_->threshold; + + return true; + } + + bool ShouldUsePersistentPolicy(base::TimeDelta* duration, + size_t* history, + int* threshold) const override { + if (!persistent_policy_) + return false; + *duration = persistent_policy_->duration; + *history = persistent_policy_->history; + *threshold = persistent_policy_->threshold; + + return true; + } + + bool ShouldUseHostPolicy(base::TimeDelta* duration, + size_t* history, + int* threshold, + size_t* max_hosts) const override { + if (!host_policy_) + return false; + *duration = host_policy_->duration; + *history = host_policy_->history; + *threshold = host_policy_->threshold; + *max_hosts = max_hosts_; + + return true; + } + + bool ShouldUseTypePolicy(base::TimeDelta* duration, + size_t* history, + int* threshold) const override { + if (!type_policy_) + return false; + *duration = type_policy_->duration; + *history = type_policy_->history; + *threshold = type_policy_->threshold; + + return true; + } + + BlocklistData::AllowedTypesAndVersions GetAllowedTypes() const override { + return allowed_types_; + } + + std::unique_ptr<BlocklistData::Policy> session_policy_; + std::unique_ptr<BlocklistData::Policy> persistent_policy_; + std::unique_ptr<BlocklistData::Policy> host_policy_; + std::unique_ptr<BlocklistData::Policy> type_policy_; + + size_t max_hosts_ = 0; + + BlocklistData::AllowedTypesAndVersions allowed_types_; +}; + +class OptOutBlocklistTest : public testing::Test { + public: + OptOutBlocklistTest() = default; + ~OptOutBlocklistTest() override = default; + + void StartTest(bool null_opt_out_store) { + std::unique_ptr<TestOptOutStore> opt_out_store = + null_opt_out_store ? nullptr : std::make_unique<TestOptOutStore>(); + opt_out_store_ = opt_out_store.get(); + + block_list_ = std::make_unique<TestOptOutBlocklist>( + std::move(opt_out_store), &test_clock_, &blocklist_delegate_); + if (session_policy_) { + block_list_->SetSessionRule(std::move(session_policy_)); + } + if (persistent_policy_) { + block_list_->SetPersistentRule(std::move(persistent_policy_)); + } + if (host_policy_) { + block_list_->SetHostRule(std::move(host_policy_), max_hosts_); + } + if (type_policy_) { + block_list_->SetTypeRule(std::move(type_policy_)); + } + + block_list_->SetAllowedTypes(std::move(allowed_types_)); + block_list_->Init(); + + start_ = test_clock_.Now(); + + passed_reasons_ = {}; + } + + void SetSessionRule(std::unique_ptr<BlocklistData::Policy> policy) { + session_policy_ = std::move(policy); + } + + void SetPersistentRule(std::unique_ptr<BlocklistData::Policy> policy) { + persistent_policy_ = std::move(policy); + } + + void SetHostRule(std::unique_ptr<BlocklistData::Policy> policy, + size_t max_hosts) { + host_policy_ = std::move(policy); + max_hosts_ = max_hosts; + } + + void SetTypeRule(std::unique_ptr<BlocklistData::Policy> policy) { + type_policy_ = std::move(policy); + } + + void SetAllowedTypes(BlocklistData::AllowedTypesAndVersions allowed_types) { + allowed_types_ = std::move(allowed_types); + } + + protected: + base::test::SingleThreadTaskEnvironment task_environment_; + + // Observer to |block_list_|. + TestOptOutBlocklistDelegate blocklist_delegate_; + + base::SimpleTestClock test_clock_; + TestOptOutStore* opt_out_store_; + base::Time start_; + + std::unique_ptr<TestOptOutBlocklist> block_list_; + std::vector<BlocklistReason> passed_reasons_; + + private: + std::unique_ptr<BlocklistData::Policy> session_policy_; + std::unique_ptr<BlocklistData::Policy> persistent_policy_; + std::unique_ptr<BlocklistData::Policy> host_policy_; + std::unique_ptr<BlocklistData::Policy> type_policy_; + + size_t max_hosts_ = 0; + + BlocklistData::AllowedTypesAndVersions allowed_types_; + + DISALLOW_COPY_AND_ASSIGN(OptOutBlocklistTest); +}; + +TEST_F(OptOutBlocklistTest, HostBlockListNoStore) { + // Tests the block list behavior when a null OptOutStore is passed in. + auto host_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 4u, 2); + SetHostRule(std::move(host_policy), 5); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, &passed_reasons_)); + + block_list_->AddEntry(kTestHost1, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfHost, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, true, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, &passed_reasons_)); + + block_list_->AddEntry(kTestHost2, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost2, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfHost, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, true, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfHost, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, &passed_reasons_)); + + block_list_->AddEntry(kTestHost2, false, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost2, false, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost2, false, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfHost, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, true, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, &passed_reasons_)); + + block_list_->ClearBlockList(start_, test_clock_.Now()); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, &passed_reasons_)); +} + +TEST_F(OptOutBlocklistTest, TypeBlockListWithStore) { + // Tests the block list behavior when a non-null OptOutStore is passed in. + + auto type_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 4u, 2); + SetTypeRule(std::move(type_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + allowed_types.insert({2, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(false /* null_opt_out */); + + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kBlocklistNotLoaded, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kBlocklistNotLoaded, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kBlocklistNotLoaded, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + block_list_->AddEntry(kTestHost1, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, true, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + block_list_->AddEntry(kTestHost1, true, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, true, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + block_list_->AddEntry(kTestHost1, false, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, false, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, false, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + EXPECT_EQ(0, opt_out_store_->clear_blocklist_count()); + block_list_->ClearBlockList(start_, base::Time::Now()); + EXPECT_EQ(1, opt_out_store_->clear_blocklist_count()); + + EXPECT_EQ( + BlocklistReason::kBlocklistNotLoaded, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kBlocklistNotLoaded, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kBlocklistNotLoaded, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(1, opt_out_store_->clear_blocklist_count()); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); +} + +TEST_F(OptOutBlocklistTest, TypeBlockListNoStore) { + // Tests the block list behavior when a null OptOutStore is passed in. + auto type_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 4u, 2); + SetTypeRule(std::move(type_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + allowed_types.insert({2, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + block_list_->AddEntry(kTestHost1, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, true, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + block_list_->AddEntry(kTestHost1, true, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, true, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, true, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + block_list_->AddEntry(kTestHost1, false, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, false, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, false, 2); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfType, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, true, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); + + block_list_->ClearBlockList(start_, test_clock_.Now()); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 2, false, &passed_reasons_)); +} + +TEST_F(OptOutBlocklistTest, HostIndifferentBlocklist) { + // Tests the block list behavior when a null OptOutStore is passed in. + const std::string hosts[] = { + "url_0.com", + "url_1.com", + "url_2.com", + "url_3.com", + }; + + int host_indifferent_threshold = 4; + + auto persistent_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 4u, host_indifferent_threshold); + SetPersistentRule(std::move(persistent_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(hosts[0], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(hosts[1], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(hosts[2], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(hosts[3], 1, false, &passed_reasons_)); + + for (int i = 0; i < host_indifferent_threshold; i++) { + block_list_->AddEntry(hosts[i], true, 1); + EXPECT_EQ( + i != 3 ? BlocklistReason::kAllowed + : BlocklistReason::kUserOptedOutInGeneral, + block_list_->IsLoadedAndAllowed(hosts[0], 1, false, &passed_reasons_)); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + } + + EXPECT_EQ( + BlocklistReason::kUserOptedOutInGeneral, + block_list_->IsLoadedAndAllowed(hosts[0], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutInGeneral, + block_list_->IsLoadedAndAllowed(hosts[1], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutInGeneral, + block_list_->IsLoadedAndAllowed(hosts[2], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutInGeneral, + block_list_->IsLoadedAndAllowed(hosts[3], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(hosts[3], 1, true, &passed_reasons_)); + + block_list_->AddEntry(hosts[3], false, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + // New non-opt-out entry will cause these to be allowed now. + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(hosts[0], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(hosts[1], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(hosts[2], 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(hosts[3], 1, false, &passed_reasons_)); +} + +TEST_F(OptOutBlocklistTest, QueueBehavior) { + // Tests the block list asynchronous queue behavior. Methods called while + // loading the opt-out store are queued and should run in the order they were + // queued. + + std::vector<bool> test_opt_out{true, false}; + + for (auto opt_out : test_opt_out) { + auto host_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 4u, 2); + SetHostRule(std::move(host_policy), 5); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(false /* null_opt_out */); + + EXPECT_EQ(BlocklistReason::kBlocklistNotLoaded, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, + &passed_reasons_)); + block_list_->AddEntry(kTestHost1, opt_out, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, opt_out, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + EXPECT_EQ(BlocklistReason::kBlocklistNotLoaded, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, + &passed_reasons_)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(opt_out ? BlocklistReason::kUserOptedOutOfHost + : BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, + &passed_reasons_)); + block_list_->AddEntry(kTestHost1, opt_out, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, opt_out, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + EXPECT_EQ(0, opt_out_store_->clear_blocklist_count()); + block_list_->ClearBlockList( + start_, test_clock_.Now() + base::TimeDelta::FromSeconds(1)); + EXPECT_EQ(1, opt_out_store_->clear_blocklist_count()); + block_list_->AddEntry(kTestHost2, opt_out, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost2, opt_out, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + base::RunLoop().RunUntilIdle(); + EXPECT_EQ(1, opt_out_store_->clear_blocklist_count()); + + EXPECT_EQ(BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, + &passed_reasons_)); + EXPECT_EQ(opt_out ? BlocklistReason::kUserOptedOutOfHost + : BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, + &passed_reasons_)); + } +} + +TEST_F(OptOutBlocklistTest, MaxHosts) { + // Test that the block list only stores n hosts, and it stores the correct n + // hosts. + const std::string test_host_3("host3.com"); + const std::string test_host_4("host4.com"); + const std::string test_host_5("host5.com"); + + auto host_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 1u, 1); + SetHostRule(std::move(host_policy), 2); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + + block_list_->AddEntry(kTestHost1, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost2, false, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(test_host_3, false, 1); + // kTestHost1 should stay in the map, since it has an opt out time. + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfHost, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(test_host_3, 1, false, &passed_reasons_)); + + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(test_host_4, true, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(test_host_5, true, 1); + // test_host_4 and test_host_5 should remain in the map, but host should be + // evicted. + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfHost, + block_list_->IsLoadedAndAllowed(test_host_4, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfHost, + block_list_->IsLoadedAndAllowed(test_host_5, 1, false, &passed_reasons_)); +} + +TEST_F(OptOutBlocklistTest, SingleOptOut) { + // Test that when a user opts out of an action, actions won't be allowed until + // |single_opt_out_duration| has elapsed. + int single_opt_out_duration = 5; + const std::string test_host_3("host3.com"); + + auto session_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromSeconds(single_opt_out_duration), 1u, 1); + SetSessionRule(std::move(session_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + + block_list_->AddEntry(kTestHost1, false, 1); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(test_host_3, 1, false, &passed_reasons_)); + + test_clock_.Advance( + base::TimeDelta::FromSeconds(single_opt_out_duration + 1)); + + block_list_->AddEntry(kTestHost2, true, 1); + EXPECT_EQ( + BlocklistReason::kUserOptedOutInSession, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutInSession, + block_list_->IsLoadedAndAllowed(test_host_3, 1, false, &passed_reasons_)); + + test_clock_.Advance( + base::TimeDelta::FromSeconds(single_opt_out_duration - 1)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutInSession, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kUserOptedOutInSession, + block_list_->IsLoadedAndAllowed(test_host_3, 1, false, &passed_reasons_)); + + test_clock_.Advance( + base::TimeDelta::FromSeconds(single_opt_out_duration + 1)); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost2, 1, false, &passed_reasons_)); + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(test_host_3, 1, false, &passed_reasons_)); +} + +TEST_F(OptOutBlocklistTest, ClearingBlockListClearsRecentNavigation) { + // Tests that clearing the block list for a long amount of time (relative to + // "single_opt_out_duration_in_seconds") resets the blocklist's recent opt out + // rule. + + auto session_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromSeconds(5), 1u, 1); + SetSessionRule(std::move(session_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(false /* null_opt_out */); + + block_list_->AddEntry(kTestHost1, true /* opt_out */, 1); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->ClearBlockList(start_, test_clock_.Now()); + base::RunLoop().RunUntilIdle(); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); +} + +TEST_F(OptOutBlocklistTest, ObserverIsNotifiedOnHostBlocklisted) { + // Tests the block list behavior when a null OptOutStore is passed in. + + auto host_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 4u, 2); + SetHostRule(std::move(host_policy), 5); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + + // Observer is not notified as blocklisted when the threshold does not met. + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, true, 1); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(blocklist_delegate_.blocklisted_hosts(), ::testing::SizeIs(0)); + + // Observer is notified as blocklisted when the threshold is met. + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, true, 1); + base::RunLoop().RunUntilIdle(); + const base::Time blocklisted_time = test_clock_.Now(); + EXPECT_THAT(blocklist_delegate_.blocklisted_hosts(), ::testing::SizeIs(1)); + EXPECT_EQ(blocklisted_time, + blocklist_delegate_.blocklisted_hosts().find(kTestHost1)->second); + + // Observer is not notified when the host is already blocklisted. + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(kTestHost1, true, 1); + base::RunLoop().RunUntilIdle(); + EXPECT_THAT(blocklist_delegate_.blocklisted_hosts(), ::testing::SizeIs(1)); + EXPECT_EQ(blocklisted_time, + blocklist_delegate_.blocklisted_hosts().find(kTestHost1)->second); + + // Observer is notified when blocklist is cleared. + EXPECT_FALSE(blocklist_delegate_.blocklist_cleared()); + + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->ClearBlockList(start_, test_clock_.Now()); + base::RunLoop().RunUntilIdle(); + + EXPECT_TRUE(blocklist_delegate_.blocklist_cleared()); + EXPECT_EQ(test_clock_.Now(), blocklist_delegate_.blocklist_cleared_time()); +} + +TEST_F(OptOutBlocklistTest, ObserverIsNotifiedOnUserBlocklisted) { + // Tests the block list behavior when a null OptOutStore is passed in. + const std::string hosts[] = { + "url_0.com", + "url_1.com", + "url_2.com", + "url_3.com", + }; + + int host_indifferent_threshold = 4; + + auto persistent_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(30), 4u, host_indifferent_threshold); + SetPersistentRule(std::move(persistent_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + + // Initially no host is blocklisted, and user is not blocklisted. + EXPECT_THAT(blocklist_delegate_.blocklisted_hosts(), ::testing::SizeIs(0)); + EXPECT_FALSE(blocklist_delegate_.user_blocklisted()); + + for (int i = 0; i < host_indifferent_threshold; ++i) { + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(hosts[i], true, 1); + base::RunLoop().RunUntilIdle(); + + EXPECT_THAT(blocklist_delegate_.blocklisted_hosts(), ::testing::SizeIs(0)); + // Observer is notified when number of recently opt out meets + // |host_indifferent_threshold|. + EXPECT_EQ(i >= host_indifferent_threshold - 1, + blocklist_delegate_.user_blocklisted()); + } + + // Observer is notified when the user is no longer blocklisted. + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + block_list_->AddEntry(hosts[3], false, 1); + base::RunLoop().RunUntilIdle(); + + EXPECT_FALSE(blocklist_delegate_.user_blocklisted()); +} + +TEST_F(OptOutBlocklistTest, ObserverIsNotifiedWhenLoadBlocklistDone) { + int host_indifferent_threshold = 4; + size_t host_indifferent_history = 4u; + auto persistent_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(30), host_indifferent_history, + host_indifferent_threshold); + SetPersistentRule(std::move(persistent_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(false /* null_opt_out */); + + allowed_types.clear(); + allowed_types[0] = 0; + std::unique_ptr<BlocklistData> data = std::make_unique<BlocklistData>( + nullptr, + std::make_unique<BlocklistData::Policy>(base::TimeDelta::FromSeconds(365), + host_indifferent_history, + host_indifferent_threshold), + nullptr, nullptr, 0, std::move(allowed_types)); + base::SimpleTestClock test_clock; + + for (int i = 0; i < host_indifferent_threshold; ++i) { + test_clock.Advance(base::TimeDelta::FromSeconds(1)); + data->AddEntry(kTestHost1, true, 0, test_clock.Now(), true); + } + + std::unique_ptr<TestOptOutStore> opt_out_store = + std::make_unique<TestOptOutStore>(); + opt_out_store->SetBlocklistData(std::move(data)); + + EXPECT_FALSE(blocklist_delegate_.user_blocklisted()); + allowed_types.clear(); + allowed_types[1] = 0; + auto block_list = std::make_unique<TestOptOutBlocklist>( + std::move(opt_out_store), &test_clock, &blocklist_delegate_); + block_list->SetAllowedTypes(std::move(allowed_types)); + + persistent_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(30), host_indifferent_history, + host_indifferent_threshold); + block_list->SetPersistentRule(std::move(persistent_policy)); + + block_list->Init(); + base::RunLoop().RunUntilIdle(); + EXPECT_TRUE(blocklist_delegate_.user_blocklisted()); +} + +TEST_F(OptOutBlocklistTest, ObserverIsNotifiedOfHistoricalBlocklistedHosts) { + // Tests the block list behavior when a non-null OptOutStore is passed in. + int host_indifferent_threshold = 2; + size_t host_indifferent_history = 4u; + auto host_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), host_indifferent_history, + host_indifferent_threshold); + SetHostRule(std::move(host_policy), 5); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(false /* null_opt_out */); + + base::SimpleTestClock test_clock; + + allowed_types.clear(); + allowed_types[static_cast<int>(1)] = 0; + std::unique_ptr<BlocklistData> data = std::make_unique<BlocklistData>( + nullptr, nullptr, + std::make_unique<BlocklistData::Policy>(base::TimeDelta::FromDays(365), + host_indifferent_history, + host_indifferent_threshold), + nullptr, 2, std::move(allowed_types)); + + test_clock.Advance(base::TimeDelta::FromSeconds(1)); + data->AddEntry(kTestHost1, true, static_cast<int>(1), test_clock.Now(), true); + test_clock.Advance(base::TimeDelta::FromSeconds(1)); + data->AddEntry(kTestHost1, true, static_cast<int>(1), test_clock.Now(), true); + base::Time blocklisted_time = test_clock.Now(); + + base::RunLoop().RunUntilIdle(); + std::vector<BlocklistReason> reasons; + EXPECT_NE(BlocklistReason::kAllowed, + data->IsAllowed(kTestHost1, static_cast<int>(1), false, + test_clock.Now(), &reasons)); + + // Host |url_b| is not blocklisted. + test_clock.Advance(base::TimeDelta::FromSeconds(1)); + data->AddEntry(kTestHost2, true, static_cast<int>(1), test_clock.Now(), true); + + std::unique_ptr<TestOptOutStore> opt_out_store = + std::make_unique<TestOptOutStore>(); + opt_out_store->SetBlocklistData(std::move(data)); + + allowed_types.clear(); + allowed_types[static_cast<int>(1)] = 0; + auto block_list = std::make_unique<TestOptOutBlocklist>( + std::move(opt_out_store), &test_clock, &blocklist_delegate_); + block_list->SetAllowedTypes(std::move(allowed_types)); + + host_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(30), host_indifferent_history, + host_indifferent_threshold); + block_list->SetPersistentRule(std::move(host_policy)); + + block_list->Init(); + + base::RunLoop().RunUntilIdle(); + + ASSERT_THAT(blocklist_delegate_.blocklisted_hosts(), ::testing::SizeIs(1)); + EXPECT_EQ(blocklisted_time, + blocklist_delegate_.blocklisted_hosts().find(kTestHost1)->second); +} + +TEST_F(OptOutBlocklistTest, PassedReasonsWhenBlocklistDataNotLoaded) { + // Test that IsLoadedAndAllow, push checked BlocklistReasons to the + // |passed_reasons| vector. + + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + StartTest(false /* null_opt_out */); + + EXPECT_EQ( + BlocklistReason::kBlocklistNotLoaded, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + + EXPECT_EQ(0UL, passed_reasons_.size()); +} + +TEST_F(OptOutBlocklistTest, PassedReasonsWhenUserRecentlyOptedOut) { + // Test that IsLoadedAndAllow, push checked BlocklistReasons to the + // |passed_reasons| vector. + + auto session_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromSeconds(5), 1u, 1); + SetSessionRule(std::move(session_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + + block_list_->AddEntry(kTestHost1, true, 1); + EXPECT_EQ( + BlocklistReason::kUserOptedOutInSession, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + EXPECT_EQ(1UL, passed_reasons_.size()); + EXPECT_EQ(BlocklistReason::kBlocklistNotLoaded, passed_reasons_[0]); +} + +TEST_F(OptOutBlocklistTest, PassedReasonsWhenUserBlocklisted) { + // Test that IsLoadedAndAllow, push checked BlocklistReasons to the + // |passed_reasons| vector. + const std::string hosts[] = { + "http://www.url_0.com", + "http://www.url_1.com", + "http://www.url_2.com", + "http://www.url_3.com", + }; + + auto session_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromSeconds(1), 1u, 1); + SetSessionRule(std::move(session_policy)); + auto persistent_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 4u, 4); + SetPersistentRule(std::move(persistent_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + test_clock_.Advance(base::TimeDelta::FromSeconds(1)); + + for (auto host : hosts) { + block_list_->AddEntry(host, true, 1); + } + + test_clock_.Advance(base::TimeDelta::FromSeconds(2)); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutInGeneral, + block_list_->IsLoadedAndAllowed(hosts[0], 1, false, &passed_reasons_)); + + BlocklistReason expected_reasons[] = { + BlocklistReason::kBlocklistNotLoaded, + BlocklistReason::kUserOptedOutInSession, + }; + EXPECT_EQ(base::size(expected_reasons), passed_reasons_.size()); + for (size_t i = 0; i < passed_reasons_.size(); i++) { + EXPECT_EQ(expected_reasons[i], passed_reasons_[i]); + } +} + +TEST_F(OptOutBlocklistTest, PassedReasonsWhenHostBlocklisted) { + // Test that IsLoadedAndAllow, push checked BlocklistReasons to the + // |passed_reasons| vector. + + auto session_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(5), 3u, 3); + SetSessionRule(std::move(session_policy)); + auto persistent_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 4u, 4); + SetPersistentRule(std::move(persistent_policy)); + auto host_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(30), 4u, 2); + SetHostRule(std::move(host_policy), 2); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + + block_list_->AddEntry(kTestHost1, true, 1); + block_list_->AddEntry(kTestHost1, true, 1); + + EXPECT_EQ( + BlocklistReason::kUserOptedOutOfHost, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + + BlocklistReason expected_reasons[] = { + BlocklistReason::kBlocklistNotLoaded, + BlocklistReason::kUserOptedOutInSession, + BlocklistReason::kUserOptedOutInGeneral, + }; + EXPECT_EQ(base::size(expected_reasons), passed_reasons_.size()); + for (size_t i = 0; i < passed_reasons_.size(); i++) { + EXPECT_EQ(expected_reasons[i], passed_reasons_[i]); + } +} + +TEST_F(OptOutBlocklistTest, PassedReasonsWhenAllowed) { + // Test that IsLoadedAndAllow, push checked BlocklistReasons to the + // |passed_reasons| vector. + + auto session_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromSeconds(1), 1u, 1); + SetSessionRule(std::move(session_policy)); + auto persistent_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(365), 4u, 4); + SetPersistentRule(std::move(persistent_policy)); + auto host_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(30), 4u, 4); + SetHostRule(std::move(host_policy), 1); + auto type_policy = std::make_unique<BlocklistData::Policy>( + base::TimeDelta::FromDays(30), 4u, 4); + SetTypeRule(std::move(type_policy)); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetAllowedTypes(std::move(allowed_types)); + + StartTest(true /* null_opt_out */); + + EXPECT_EQ( + BlocklistReason::kAllowed, + block_list_->IsLoadedAndAllowed(kTestHost1, 1, false, &passed_reasons_)); + + BlocklistReason expected_reasons[] = { + BlocklistReason::kBlocklistNotLoaded, + BlocklistReason::kUserOptedOutInSession, + BlocklistReason::kUserOptedOutInGeneral, + BlocklistReason::kUserOptedOutOfHost, + BlocklistReason::kUserOptedOutOfType, + }; + EXPECT_EQ(base::size(expected_reasons), passed_reasons_.size()); + for (size_t i = 0; i < passed_reasons_.size(); i++) { + EXPECT_EQ(expected_reasons[i], passed_reasons_[i]); + } +} + +} // namespace + +} // namespace blocklist diff --git a/chromium/components/blocklist/opt_out_blocklist/opt_out_store.h b/chromium/components/blocklist/opt_out_blocklist/opt_out_store.h new file mode 100644 index 00000000000..4bfbe4c44b9 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/opt_out_store.h @@ -0,0 +1,49 @@ +// Copyright 2016 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. + +#ifndef COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_STORE_H_ +#define COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_STORE_H_ + +#include <stdint.h> + +#include <memory> +#include <string> + +#include "base/callback.h" +#include "base/time/time.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_data.h" + +namespace blocklist { + +typedef base::OnceCallback<void(std::unique_ptr<BlocklistData>)> + LoadBlockListCallback; + +// OptOutStore keeps opt out information for the blocklist. +// Ability to create multiple instances of the store as well as behavior of +// asynchronous operations when the object is being destroyed, before such +// operation finishes will depend on implementation. It is possible to issue +// multiple asynchronous operations in parallel and maintain ordering. +class OptOutStore { + public: + virtual ~OptOutStore() {} + + // Adds a new navigation to the store. |opt_out| is whether the user opted out + // of the action. + virtual void AddEntry(bool opt_out, + const std::string& host_name, + int type, + base::Time now) = 0; + + // Asynchronously loads a map of host names to OptOutBlocklistItem for that + // host from the store. And runs |callback| once loading is finished. + virtual void LoadBlockList(std::unique_ptr<BlocklistData> blocklist_data, + LoadBlockListCallback callback) = 0; + + // Deletes all history in the store between |begin_time| and |end_time|. + virtual void ClearBlockList(base::Time begin_time, base::Time end_time) = 0; +}; + +} // namespace blocklist + +#endif // COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_OPT_OUT_STORE_H_ diff --git a/chromium/components/blocklist/opt_out_blocklist/sql/BUILD.gn b/chromium/components/blocklist/opt_out_blocklist/sql/BUILD.gn new file mode 100644 index 00000000000..cb270b6888f --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/sql/BUILD.gn @@ -0,0 +1,32 @@ +# Copyright 2018 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. + +static_library("opt_out_blocklist_sql") { + sources = [ + "opt_out_store_sql.cc", + "opt_out_store_sql.h", + ] + + deps = [ + "//base", + "//components/blocklist/opt_out_blocklist:opt_out_blocklist", + "//sql", + ] +} + +source_set("unit_tests") { + testonly = true + sources = [ "opt_out_store_sql_unittest.cc" ] + + deps = [ + ":opt_out_blocklist_sql", + "//base", + "//base/test:test_support", + "//components/blocklist/opt_out_blocklist:opt_out_blocklist", + "//sql", + "//sql:test_support", + "//testing/gmock", + "//testing/gtest", + ] +} diff --git a/chromium/components/blocklist/opt_out_blocklist/sql/DEPS b/chromium/components/blocklist/opt_out_blocklist/sql/DEPS new file mode 100644 index 00000000000..6fff87d325a --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/sql/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+sql", +] diff --git a/chromium/components/blocklist/opt_out_blocklist/sql/opt_out_store_sql.cc b/chromium/components/blocklist/opt_out_blocklist/sql/opt_out_store_sql.cc new file mode 100644 index 00000000000..f13d2014a9e --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/sql/opt_out_store_sql.cc @@ -0,0 +1,410 @@ +// Copyright 2016 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/blocklist/opt_out_blocklist/sql/opt_out_store_sql.h" + +#include <map> +#include <string> +#include <utility> + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/command_line.h" +#include "base/files/file_util.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/sequenced_task_runner.h" +#include "base/single_thread_task_runner.h" +#include "base/strings/string_number_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_data.h" +#include "sql/database.h" +#include "sql/recovery.h" +#include "sql/statement.h" +#include "sql/transaction.h" + +namespace blocklist { + +namespace { + +// Command line switch to change the entry per host DB size. +const char kMaxRowsPerHost[] = "max-opt-out-rows-per-host"; + +// Command line switch to change the DB size. +const char kMaxRows[] = "max-opt-out-rows"; + +// Returns the maximum number of table rows allowed per host for the sql +// opt out store. This is enforced during insertion of new navigation entries. +int MaxRowsPerHostInOptOutDB() { + std::string max_rows = + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + kMaxRowsPerHost); + int value; + return base::StringToInt(max_rows, &value) ? value : 32; +} + +// Returns the maximum number of table rows allowed for the blocklist opt out +// store. This is enforced during load time; thus the database can grow +// larger than this temporarily. +int MaxRowsInOptOutDB() { + std::string max_rows = + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(kMaxRows); + int value; + return base::StringToInt(max_rows, &value) ? value : 3200; +} + +// Table names use a macro instead of a const, so they can be used inline in +// other SQL statements below. + +// The Opt Out table holds entries for hosts that should not use a specified +// type. Historically, this was named previews_v1. +#define OPT_OUT_TABLE_NAME "previews_v1" + +// The Enabled types table hold the list of enabled types +// treatments with a version for that enabled treatment. If the version +// changes or the type becomes disabled, then any entries in the Opt Out +// table for that treatment type should be cleared. Historically, this was named +// enabled_previews_v1. +#define ENABLED_TYPES_TABLE_NAME "enabled_previews_v1" + +void CreateSchema(sql::Database* db) { + static const char kSqlCreateTable[] = + "CREATE TABLE IF NOT EXISTS " OPT_OUT_TABLE_NAME + " (host_name VARCHAR NOT NULL," + " time INTEGER NOT NULL," + " opt_out INTEGER NOT NULL," + " type INTEGER NOT NULL," + " PRIMARY KEY(host_name, time DESC, opt_out, type))"; + if (!db->Execute(kSqlCreateTable)) + return; + + static const char kSqlCreateEnabledTypeVersionTable[] = + "CREATE TABLE IF NOT EXISTS " ENABLED_TYPES_TABLE_NAME + " (type INTEGER NOT NULL," + " version INTEGER NOT NULL," + " PRIMARY KEY(type))"; + if (!db->Execute(kSqlCreateEnabledTypeVersionTable)) + return; +} + +void DatabaseErrorCallback(sql::Database* db, + const base::FilePath& db_path, + int extended_error, + sql::Statement* stmt) { + if (sql::Recovery::ShouldRecover(extended_error)) { + // Prevent reentrant calls. + db->reset_error_callback(); + + // After this call, the |db| handle is poisoned so that future calls will + // return errors until the handle is re-opened. + sql::Recovery::RecoverDatabase(db, db_path); + + // The DLOG(WARNING) below is intended to draw immediate attention to errors + // in newly-written code. Database corruption is generally a result of OS + // or hardware issues, not coding errors at the client level, so displaying + // the error would probably lead to confusion. The ignored call signals the + // test-expectation framework that the error was handled. + ignore_result(sql::Database::IsExpectedSqliteError(extended_error)); + return; + } +} + +void InitDatabase(sql::Database* db, base::FilePath path) { + // The entry size should be between 11 and 10 + x bytes, where x is the the + // length of the host name string in bytes. + // The total number of entries per host is bounded at 32, and the total number + // of hosts is currently unbounded (but typically expected to be under 100). + // Assuming average of 100 bytes per entry, and 100 hosts, the total size will + // be 4096 * 78. 250 allows room for extreme cases such as many host names + // or very long host names. + // The average case should be much smaller as users rarely visit hosts that + // are not in their top 20 hosts. It should be closer to 32 * 100 * 20 for + // most users, which is about 4096 * 15. + // The total size of the database will be capped at 3200 entries. + db->set_page_size(4096); + db->set_cache_size(250); + // TODO(crbug.com/1092101): Migrate to OptOutBlocklist and update any backend + // code that may depend on this tag. + db->set_histogram_tag("OptOutBlacklist"); + db->set_exclusive_locking(); + + db->set_error_callback(base::BindRepeating(&DatabaseErrorCallback, db, path)); + + base::File::Error err; + if (!base::CreateDirectoryAndGetError(path.DirName(), &err)) { + return; + } + if (!db->Open(path)) { + return; + } + + CreateSchema(db); +} + +// Adds a new OptOut entry to the data base. +void AddEntryToDataBase(sql::Database* db, + bool opt_out, + const std::string& host_name, + int type, + base::Time now) { + // Adds the new entry. + static const char kSqlInsert[] = "INSERT INTO " OPT_OUT_TABLE_NAME + " (host_name, time, opt_out, type)" + " VALUES " + " (?, ?, ?, ?)"; + + sql::Statement statement_insert( + db->GetCachedStatement(SQL_FROM_HERE, kSqlInsert)); + statement_insert.BindString(0, host_name); + statement_insert.BindInt64(1, (now - base::Time()).InMicroseconds()); + statement_insert.BindBool(2, opt_out); + statement_insert.BindInt(3, type); + statement_insert.Run(); +} + +// Removes OptOut entries for |host_name| if the per-host row limit is exceeded. +// Removes OptOut entries if per data base row limit is exceeded. +void MaybeEvictHostEntryFromDataBase(sql::Database* db, + const std::string& host_name) { + // Delete the oldest entries if there are more than |MaxRowsPerHostInOptOutDB| + // for |host_name|. + // DELETE ... LIMIT -1 OFFSET x means delete all but the first x entries. + static const char kSqlDeleteByHost[] = + "DELETE FROM " OPT_OUT_TABLE_NAME + " WHERE ROWID IN" + " (SELECT ROWID from " OPT_OUT_TABLE_NAME + " WHERE host_name == ?" + " ORDER BY time DESC" + " LIMIT -1 OFFSET ?)"; + + sql::Statement statement_delete_by_host( + db->GetCachedStatement(SQL_FROM_HERE, kSqlDeleteByHost)); + statement_delete_by_host.BindString(0, host_name); + statement_delete_by_host.BindInt(1, MaxRowsPerHostInOptOutDB()); + statement_delete_by_host.Run(); +} + +// Deletes every entry for |type|. +void ClearBlocklistForTypeInDataBase(sql::Database* db, int type) { + static const char kSql[] = + "DELETE FROM " OPT_OUT_TABLE_NAME " WHERE type == ?"; + sql::Statement statement(db->GetUniqueStatement(kSql)); + statement.BindInt(0, type); + statement.Run(); +} + +// Retrieves the list of previously enabled types with their version from the +// Enabled table. +BlocklistData::AllowedTypesAndVersions GetStoredEntries(sql::Database* db) { + static const char kSqlLoadEnabledTypesVersions[] = + "SELECT type, version FROM " ENABLED_TYPES_TABLE_NAME; + + sql::Statement statement( + db->GetUniqueStatement(kSqlLoadEnabledTypesVersions)); + + BlocklistData::AllowedTypesAndVersions stored_entries; + while (statement.Step()) { + int type = statement.ColumnInt(0); + int version = statement.ColumnInt(1); + stored_entries.insert({type, version}); + } + return stored_entries; +} + +// Adds a newly enabled |type| with its |version| to the Enabled types table. +void InsertEnabledTypesInDataBase(sql::Database* db, int type, int version) { + static const char kSqlInsert[] = "INSERT INTO " ENABLED_TYPES_TABLE_NAME + " (type, version)" + " VALUES " + " (?, ?)"; + + sql::Statement statement_insert(db->GetUniqueStatement(kSqlInsert)); + statement_insert.BindInt(0, type); + statement_insert.BindInt(1, version); + statement_insert.Run(); +} + +// Updates the |version| of an enabled |type| in the Enabled table. +void UpdateEnabledTypesInDataBase(sql::Database* db, int type, int version) { + static const char kSqlUpdate[] = "UPDATE " ENABLED_TYPES_TABLE_NAME + " SET version = ?" + " WHERE type = ?"; + + sql::Statement statement_update( + db->GetCachedStatement(SQL_FROM_HERE, kSqlUpdate)); + statement_update.BindInt(0, version); + statement_update.BindInt(1, type); + statement_update.Run(); +} + +// Checks the current set of enabled types (with their current version) +// and where a type is now disabled or has a different version, cleans up +// any associated blocklist entries. +void CheckAndReconcileEnabledTypesWithDataBase( + sql::Database* db, + const BlocklistData::AllowedTypesAndVersions& allowed_types) { + BlocklistData::AllowedTypesAndVersions stored_entries = GetStoredEntries(db); + + for (auto enabled_it : allowed_types) { + int type = enabled_it.first; + int current_version = enabled_it.second; + auto stored_it = stored_entries.find(type); + if (stored_it == stored_entries.end()) { + InsertEnabledTypesInDataBase(db, type, current_version); + } else { + if (stored_it->second != current_version) { + DCHECK_GE(current_version, stored_it->second); + ClearBlocklistForTypeInDataBase(db, type); + UpdateEnabledTypesInDataBase(db, type, current_version); + } + } + } + // Do not delete types that are not in |allowed_types|. They will get cleaned + // up eventually when they expire if the type is truly gone. However, if the + // type has been removed temporarily (like in a holdback experiment), then + // it'll still be around for the next time it is used. +} + +void LoadBlockListFromDataBase( + sql::Database* db, + std::unique_ptr<BlocklistData> blocklist_data, + scoped_refptr<base::SingleThreadTaskRunner> runner, + LoadBlockListCallback callback) { + // First handle any update needed wrt enabled types and their versions. + CheckAndReconcileEnabledTypesWithDataBase(db, + blocklist_data->allowed_types()); + + // Gets the table sorted by host and time. Limits the number of hosts using + // most recent opt_out time as the limiting function. Sorting is free due to + // the table structure, and it improves performance in the loop below. + static const char kSql[] = + "SELECT host_name, time, opt_out, type" + " FROM " OPT_OUT_TABLE_NAME " ORDER BY host_name, time DESC"; + + sql::Statement statement(db->GetUniqueStatement(kSql)); + + int count = 0; + while (statement.Step()) { + ++count; + blocklist_data->AddEntry(statement.ColumnString(0), statement.ColumnBool(2), + statement.ColumnInt64(3), + base::Time() + base::TimeDelta::FromMicroseconds( + statement.ColumnInt64(1)), + true); + } + + if (count > MaxRowsInOptOutDB()) { + // Delete the oldest entries if there are more than |kMaxEntriesInDB|. + // DELETE ... LIMIT -1 OFFSET x means delete all but the first x entries. + static const char kSqlDeleteByDBSize[] = + "DELETE FROM " OPT_OUT_TABLE_NAME + " WHERE ROWID IN" + " (SELECT ROWID from " OPT_OUT_TABLE_NAME + " ORDER BY time DESC" + " LIMIT -1 OFFSET ?)"; + + sql::Statement statement_delete( + db->GetCachedStatement(SQL_FROM_HERE, kSqlDeleteByDBSize)); + statement_delete.BindInt(0, MaxRowsInOptOutDB()); + statement_delete.Run(); + } + + runner->PostTask(FROM_HERE, base::BindOnce(std::move(callback), + std::move(blocklist_data))); +} + +// Synchronous implementations, these are run on the background thread +// and actually do the work to access the SQL data base. +void LoadBlockListSync(sql::Database* db, + const base::FilePath& path, + std::unique_ptr<BlocklistData> blocklist_data, + scoped_refptr<base::SingleThreadTaskRunner> runner, + LoadBlockListCallback callback) { + if (!db->is_open()) + InitDatabase(db, path); + + LoadBlockListFromDataBase(db, std::move(blocklist_data), runner, + std::move(callback)); +} + +// Deletes every row in the table that has entry time between |begin_time| and +// |end_time|. +void ClearBlockListSync(sql::Database* db, + base::Time begin_time, + base::Time end_time) { + static const char kSql[] = + "DELETE FROM " OPT_OUT_TABLE_NAME " WHERE time >= ? and time <= ?"; + + sql::Statement statement(db->GetUniqueStatement(kSql)); + statement.BindInt64(0, (begin_time - base::Time()).InMicroseconds()); + statement.BindInt64(1, (end_time - base::Time()).InMicroseconds()); + statement.Run(); +} + +void AddEntrySync(bool opt_out, + const std::string& host_name, + int type, + base::Time now, + sql::Database* db) { + sql::Transaction transaction(db); + if (!transaction.Begin()) + return; + AddEntryToDataBase(db, opt_out, host_name, type, now); + MaybeEvictHostEntryFromDataBase(db, host_name); + transaction.Commit(); +} + +} // namespace + +OptOutStoreSQL::OptOutStoreSQL( + scoped_refptr<base::SingleThreadTaskRunner> io_task_runner, + scoped_refptr<base::SequencedTaskRunner> background_task_runner, + const base::FilePath& path) + : io_task_runner_(io_task_runner), + background_task_runner_(background_task_runner), + db_file_path_(path) {} + +OptOutStoreSQL::~OptOutStoreSQL() { + DCHECK(io_task_runner_->BelongsToCurrentThread()); + if (db_) { + background_task_runner_->DeleteSoon(FROM_HERE, db_.release()); + } +} + +void OptOutStoreSQL::AddEntry(bool opt_out, + const std::string& host_name, + int type, + base::Time now) { + DCHECK(io_task_runner_->BelongsToCurrentThread()); + DCHECK(db_); + background_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&AddEntrySync, opt_out, host_name, type, now, db_.get())); +} + +void OptOutStoreSQL::ClearBlockList(base::Time begin_time, + base::Time end_time) { + DCHECK(io_task_runner_->BelongsToCurrentThread()); + DCHECK(db_); + background_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&ClearBlockListSync, db_.get(), begin_time, end_time)); +} + +void OptOutStoreSQL::LoadBlockList( + std::unique_ptr<BlocklistData> blocklist_data, + LoadBlockListCallback callback) { + DCHECK(io_task_runner_->BelongsToCurrentThread()); + if (!db_) + db_ = std::make_unique<sql::Database>(); + background_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&LoadBlockListSync, db_.get(), db_file_path_, + std::move(blocklist_data), + base::ThreadTaskRunnerHandle::Get(), std::move(callback))); +} + +} // namespace blocklist diff --git a/chromium/components/blocklist/opt_out_blocklist/sql/opt_out_store_sql.h b/chromium/components/blocklist/opt_out_blocklist/sql/opt_out_store_sql.h new file mode 100644 index 00000000000..4e2c90ae264 --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/sql/opt_out_store_sql.h @@ -0,0 +1,68 @@ +// Copyright 2016 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. + +#ifndef COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_SQL_OPT_OUT_STORE_SQL_H_ +#define COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_SQL_OPT_OUT_STORE_SQL_H_ + +#include <stdint.h> + +#include <memory> +#include <string> +#include <vector> + +#include "base/files/file_path.h" +#include "base/macros.h" +#include "base/threading/thread_checker.h" +#include "base/time/time.h" +#include "components/blocklist/opt_out_blocklist/opt_out_store.h" + +namespace base { +class SequencedTaskRunner; +class SingleThreadTaskRunner; +} // namespace base + +namespace sql { +class Database; +} // namespace sql + +namespace blocklist { + +// OptOutStoreSQL is an instance of OptOutStore +// which is implemented using a SQLite database. +class OptOutStoreSQL : public OptOutStore { + public: + OptOutStoreSQL( + scoped_refptr<base::SingleThreadTaskRunner> io_task_runner, + scoped_refptr<base::SequencedTaskRunner> background_task_runner, + const base::FilePath& database_dir); + ~OptOutStoreSQL() override; + + // OptOutStore implementation: + void AddEntry(bool opt_out, + const std::string& host_name, + int type, + base::Time now) override; + void ClearBlockList(base::Time begin_time, base::Time end_time) override; + void LoadBlockList(std::unique_ptr<BlocklistData> blocklist_data, + LoadBlockListCallback callback) override; + + private: + // Thread this object is accessed on. + scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_; + + // Background thread where all SQL access should be run. + scoped_refptr<base::SequencedTaskRunner> background_task_runner_; + + // Path to the database on disk. + const base::FilePath db_file_path_; + + // SQL connection to the SQLite database. + std::unique_ptr<sql::Database> db_; + + DISALLOW_COPY_AND_ASSIGN(OptOutStoreSQL); +}; + +} // namespace blocklist + +#endif // COMPONENTS_BLOCKLIST_OPT_OUT_BLOCKLIST_SQL_OPT_OUT_STORE_SQL_H_ diff --git a/chromium/components/blocklist/opt_out_blocklist/sql/opt_out_store_sql_unittest.cc b/chromium/components/blocklist/opt_out_blocklist/sql/opt_out_store_sql_unittest.cc new file mode 100644 index 00000000000..4aac3cc5a9a --- /dev/null +++ b/chromium/components/blocklist/opt_out_blocklist/sql/opt_out_store_sql_unittest.cc @@ -0,0 +1,310 @@ +// Copyright 2018 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/blocklist/opt_out_blocklist/sql/opt_out_store_sql.h" + +#include <map> +#include <memory> +#include <string> + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/files/scoped_temp_dir.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/simple_test_clock.h" +#include "base/test/task_environment.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_data.h" +#include "components/blocklist/opt_out_blocklist/opt_out_blocklist_item.h" +#include "components/blocklist/opt_out_blocklist/opt_out_store.h" +#include "sql/test/test_helpers.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace blocklist { + +namespace { + +const base::FilePath::CharType kOptOutFilename[] = FILE_PATH_LITERAL("OptOut"); + +} // namespace + +class OptOutStoreSQLTest : public testing::Test { + public: + OptOutStoreSQLTest() {} + ~OptOutStoreSQLTest() override {} + + // Called when |store_| is done loading. + void OnLoaded(std::unique_ptr<BlocklistData> blocklist_data) { + blocklist_data_ = std::move(blocklist_data); + } + + // Initializes the store and get the data from it. + void Load() { + // Choose reasonable constants. + std::unique_ptr<BlocklistData> data = std::make_unique<BlocklistData>( + std::make_unique<BlocklistData::Policy>(base::TimeDelta::FromMinutes(5), + 1, 1), + std::make_unique<BlocklistData::Policy>(base::TimeDelta::FromDays(30), + 10, 6u), + std::make_unique<BlocklistData::Policy>(base::TimeDelta::FromDays(30), + 4, 2u), + nullptr, 10, allowed_types_); + + store_->LoadBlockList( + std::move(data), + base::BindOnce(&OptOutStoreSQLTest::OnLoaded, base::Unretained(this))); + base::RunLoop().RunUntilIdle(); + } + + // Destroys the database connection and |store_|. + void DestroyStore() { + store_.reset(); + base::RunLoop().RunUntilIdle(); + } + + // Creates a store that operates on one thread. + void Create() { + store_ = std::make_unique<OptOutStoreSQL>( + base::ThreadTaskRunnerHandle::Get(), + base::ThreadTaskRunnerHandle::Get(), + temp_dir_.GetPath().Append(kOptOutFilename)); + } + + // Sets up initialization of |store_|. + void CreateAndLoad() { + Create(); + Load(); + } + + void SetEnabledTypes(BlocklistData::AllowedTypesAndVersions allowed_types) { + allowed_types_ = std::move(allowed_types); + } + + // Creates a directory for the test. + void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); } + + // Delete |store_| if it hasn't been deleted. + void TearDown() override { DestroyStore(); } + + protected: + base::test::SingleThreadTaskEnvironment task_environment_; + + // The backing SQL store. + std::unique_ptr<OptOutStoreSQL> store_; + + // The map returned from |store_|. + std::unique_ptr<BlocklistData> blocklist_data_; + + // The directory for the database. + base::ScopedTempDir temp_dir_; + + private: + BlocklistData::AllowedTypesAndVersions allowed_types_; +}; + +TEST_F(OptOutStoreSQLTest, TestErrorRecovery) { + // Creates the database and corrupt to test the recovery method. + std::string test_host = "host.com"; + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + store_->AddEntry(true, test_host, 1, base::Time::Now()); + base::RunLoop().RunUntilIdle(); + DestroyStore(); + + // Corrupts the database by adjusting the header size. + EXPECT_TRUE(sql::test::CorruptSizeInHeader( + temp_dir_.GetPath().Append(kOptOutFilename))); + base::RunLoop().RunUntilIdle(); + + allowed_types.clear(); + allowed_types.insert({1, 0}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + // The data should be recovered. + EXPECT_EQ(1U, blocklist_data_->block_list_item_host_map().size()); + const auto& iter = + blocklist_data_->block_list_item_host_map().find(test_host); + + EXPECT_NE(blocklist_data_->block_list_item_host_map().end(), iter); + EXPECT_EQ(1U, iter->second.OptOutRecordsSizeForTesting()); +} + +TEST_F(OptOutStoreSQLTest, TestPersistance) { + // Tests if data is stored as expected in the SQLite database. + std::string test_host = "host.com"; + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + base::Time now = base::Time::Now(); + store_->AddEntry(true, test_host, 1, now); + base::RunLoop().RunUntilIdle(); + + // Replace the store effectively destroying the current one and forcing it + // to write its data to disk. + DestroyStore(); + + // Reload and test for persistence + allowed_types.clear(); + allowed_types.insert({1, 0}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + EXPECT_EQ(1U, blocklist_data_->block_list_item_host_map().size()); + const auto& iter = + blocklist_data_->block_list_item_host_map().find(test_host); + + EXPECT_NE(blocklist_data_->block_list_item_host_map().end(), iter); + EXPECT_EQ(1U, iter->second.OptOutRecordsSizeForTesting()); + EXPECT_EQ(now, iter->second.most_recent_opt_out_time().value()); +} + +TEST_F(OptOutStoreSQLTest, TestMaxRows) { + // Tests that the number of rows are culled down to the row limit at each + // load. + std::string test_host_a = "host_a.com"; + std::string test_host_b = "host_b.com"; + std::string test_host_c = "host_c.com"; + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + size_t row_limit = 2; + std::string row_limit_string = base::NumberToString(row_limit); + command_line->AppendSwitchASCII("max-opt-out-rows", row_limit_string); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + base::SimpleTestClock clock; + + // Create three different entries with different hosts. + store_->AddEntry(true, test_host_a, 1, clock.Now()); + clock.Advance(base::TimeDelta::FromSeconds(1)); + + store_->AddEntry(true, test_host_b, 1, clock.Now()); + base::Time host_b_time = clock.Now(); + clock.Advance(base::TimeDelta::FromSeconds(1)); + + store_->AddEntry(false, test_host_c, 1, clock.Now()); + base::RunLoop().RunUntilIdle(); + // Replace the store effectively destroying the current one and forcing it + // to write its data to disk. + DestroyStore(); + + // Reload and test for persistence + allowed_types.clear(); + allowed_types.insert({1, 0}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + // The delete happens after the load, so it is possible to load more than + // |row_limit| into the in memory map. + EXPECT_EQ(row_limit + 1, blocklist_data_->block_list_item_host_map().size()); + + DestroyStore(); + allowed_types.clear(); + allowed_types.insert({1, 0}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + + EXPECT_EQ(row_limit, blocklist_data_->block_list_item_host_map().size()); + const auto& iter_host_b = + blocklist_data_->block_list_item_host_map().find(test_host_b); + const auto& iter_host_c = + blocklist_data_->block_list_item_host_map().find(test_host_c); + + EXPECT_EQ(blocklist_data_->block_list_item_host_map().end(), + blocklist_data_->block_list_item_host_map().find(test_host_a)); + EXPECT_NE(blocklist_data_->block_list_item_host_map().end(), iter_host_b); + EXPECT_NE(blocklist_data_->block_list_item_host_map().end(), iter_host_c); + EXPECT_EQ(host_b_time, + iter_host_b->second.most_recent_opt_out_time().value()); + EXPECT_EQ(1U, iter_host_b->second.OptOutRecordsSizeForTesting()); +} + +TEST_F(OptOutStoreSQLTest, TestMaxRowsPerHost) { + // Tests that each host is limited to |row_limit| rows. + std::string test_host = "host.com"; + base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); + size_t row_limit = 2; + std::string row_limit_string = base::NumberToString(row_limit); + command_line->AppendSwitchASCII("max-opt-out-rows-per-host", + row_limit_string); + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 0}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + base::SimpleTestClock clock; + + base::Time last_opt_out_time; + for (size_t i = 0; i < row_limit; i++) { + store_->AddEntry(true, test_host, 1, clock.Now()); + last_opt_out_time = clock.Now(); + clock.Advance(base::TimeDelta::FromSeconds(1)); + } + + clock.Advance(base::TimeDelta::FromSeconds(1)); + store_->AddEntry(false, test_host, 1, clock.Now()); + + base::RunLoop().RunUntilIdle(); + // Replace the store effectively destroying the current one and forcing it + // to write its data to disk. + DestroyStore(); + + // Reload and test for persistence. + allowed_types.clear(); + allowed_types.insert({1, 0}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + + EXPECT_EQ(1U, blocklist_data_->block_list_item_host_map().size()); + const auto& iter = + blocklist_data_->block_list_item_host_map().find(test_host); + + EXPECT_NE(blocklist_data_->block_list_item_host_map().end(), iter); + EXPECT_EQ(last_opt_out_time, iter->second.most_recent_opt_out_time().value()); + EXPECT_EQ(row_limit, iter->second.OptOutRecordsSizeForTesting()); + clock.Advance(base::TimeDelta::FromSeconds(1)); + // If both entries' opt out states are stored correctly, then this should not + // be block listed. + EXPECT_FALSE(iter->second.IsBlockListed(clock.Now())); +} + +TEST_F(OptOutStoreSQLTest, TestTypesVersionUpdateClearsBlocklistEntry) { + // Tests if data is cleared for new version of type. + std::string test_host = "host.com"; + BlocklistData::AllowedTypesAndVersions allowed_types; + allowed_types.insert({1, 1}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + base::Time now = base::Time::Now(); + store_->AddEntry(true, test_host, 1, now); + base::RunLoop().RunUntilIdle(); + + // Force data write to database then reload it and verify block list entry + // is present. + DestroyStore(); + allowed_types.clear(); + allowed_types.insert({1, 1}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + const auto& iter = + blocklist_data_->block_list_item_host_map().find(test_host); + EXPECT_NE(blocklist_data_->block_list_item_host_map().end(), iter); + EXPECT_EQ(1U, iter->second.OptOutRecordsSizeForTesting()); + + DestroyStore(); + allowed_types.clear(); + allowed_types.insert({1, 2}); + SetEnabledTypes(std::move(allowed_types)); + CreateAndLoad(); + const auto& iter2 = + blocklist_data_->block_list_item_host_map().find(test_host); + EXPECT_EQ(blocklist_data_->block_list_item_host_map().end(), iter2); +} + +} // namespace blocklist |