| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/devtools/devtools_navigation_gating_rule_manager.h" |
| |
| #include <optional> |
| #include <string> |
| |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/json/json_reader.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/no_destructor.h" |
| #include "base/notreached.h" |
| #include "base/values.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "components/content_settings/core/common/content_settings.h" |
| #include "components/content_settings/core/common/content_settings_pattern.h" |
| #include "components/content_settings/core/common/content_settings_utils.h" |
| |
| namespace { |
| |
| constexpr char kAllowlistKey[] = "allowlist"; |
| constexpr char kBlocklistKey[] = "blocklist"; |
| |
| } // namespace |
| |
| // static |
| DevToolsNavigationGatingRuleManager& |
| DevToolsNavigationGatingRuleManager::Get() { |
| static base::NoDestructor<DevToolsNavigationGatingRuleManager> instance( |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( |
| switches::kDevToolsNavigationGatingRules)); |
| return *instance; |
| } |
| |
| // static |
| DevToolsNavigationGatingRuleManager |
| DevToolsNavigationGatingRuleManager::CreateForTesting( |
| std::string_view rules_json) { |
| return DevToolsNavigationGatingRuleManager(rules_json); |
| } |
| |
| DevToolsNavigationGatingRuleManager::DevToolsNavigationGatingRuleManager( |
| std::string_view rules_json) |
| : origin_gating_checker_( |
| *this, |
| origin_gating::OriginGatingConfiguration( |
| { |
| {origin_gating::CustomPredicate( |
| base::BindRepeating( |
| &DevToolsNavigationGatingRuleManager::EvaluateRules, |
| // Safe because `origin_gating_checker_` is owned by |
| // `this`, so the callback is guaranteed to be |
| // destroyed before `this` is. |
| base::Unretained(this)), |
| "DevToolsNavigationGatingRules"), |
| origin_gating::GateableEventSet::All()}, |
| }, |
| /*use_site_keyed_cache=*/false)) { |
| if (rules_json.empty()) { |
| return; |
| } |
| |
| auto parsed_json = base::JSONReader::ReadAndReturnValueWithError( |
| rules_json, base::JSON_PARSE_RFC); |
| if (!parsed_json.has_value()) { |
| LOG(ERROR) << "Failed to parse DevTools navigation gating rules: " |
| << parsed_json.error().message; |
| return; |
| } |
| |
| const base::DictValue* dict = parsed_json->GetIfDict(); |
| if (!dict) { |
| LOG(ERROR) << "DevTools navigation gating rules must be a JSON dictionary."; |
| return; |
| } |
| |
| auto parse_and_add_rules = [&](std::string_view key, ContentSetting setting) { |
| const base::Value* val = dict->Find(key); |
| if (!val) { |
| return; |
| } |
| const base::ListValue* list = val->GetIfList(); |
| if (!list) { |
| LOG(ERROR) << "DevTools navigation gating rules key '" << key |
| << "' must be a list."; |
| return; |
| } |
| |
| for (const auto& item : *list) { |
| if (!item.is_string()) { |
| LOG(ERROR) << "DevTools navigation gating list '" << key |
| << "' contains a non-string element."; |
| continue; |
| } |
| ContentSettingsPattern pattern = |
| ContentSettingsPattern::FromString(item.GetString()); |
| if (!pattern.IsValid()) { |
| LOG(ERROR) << "DevTools navigation gating list '" << key |
| << "' contains an invalid pattern: '" << item.GetString() |
| << "'"; |
| continue; |
| } |
| rules_.SetValue(ContentSettingsPattern::Wildcard(), pattern, |
| content_settings::ContentSettingToValue(setting), {}); |
| } |
| return; |
| }; |
| |
| has_allowlist_ = !!dict->FindList(kAllowlistKey); |
| parse_and_add_rules(kAllowlistKey, CONTENT_SETTING_ALLOW); |
| parse_and_add_rules(kBlocklistKey, CONTENT_SETTING_BLOCK); |
| } |
| |
| DevToolsNavigationGatingRuleManager::~DevToolsNavigationGatingRuleManager() = |
| default; |
| |
| void DevToolsNavigationGatingRuleManager::IsNavigationAllowed( |
| const GURL& url, |
| base::OnceCallback<void(bool)> callback) { |
| origin_gating_checker_.ComputeGatingDecision( |
| /*context=*/nullptr, origin_gating::GateableEvent::kNavigationRequest, |
| /*source=*/GURL(), /*destination=*/url, |
| base::BindOnce([](std::unique_ptr<origin_gating::GatingDecisionContext> |
| context, |
| origin_gating::GatingDecision decision) { |
| return decision.is_allowed; |
| }).Then(std::move(callback))); |
| } |
| |
| bool DevToolsNavigationGatingRuleManager::MayBlockNavigation() const { |
| return !rules_.empty() || has_allowlist_; |
| } |
| |
| void DevToolsNavigationGatingRuleManager::DoesOriginRequireUserConfirmation( |
| origin_gating::GatingDecisionContext* context, |
| origin_gating::GateableEvent event, |
| const GURL& source, |
| const GURL& destination, |
| DoesOriginRequireUserConfirmationCallback callback) const { |
| NOTREACHED(); |
| } |
| |
| void DevToolsNavigationGatingRuleManager::EvaluateEnterprisePolicy( |
| const GURL& destination, |
| EvaluateEnterprisePolicyCallback callback) const { |
| NOTREACHED(); |
| } |
| |
| void DevToolsNavigationGatingRuleManager::OnNoVerdict( |
| origin_gating::GatingDecisionContext* context, |
| origin_gating::GateableEvent event, |
| const GURL& source, |
| const GURL& destination, |
| bool requires_user_confirmation, |
| base::OnceCallback<void(NoVerdictResult)> callback) { |
| NOTREACHED(); |
| } |
| |
| origin_gating::Decision DevToolsNavigationGatingRuleManager::EvaluateRules( |
| const origin_gating::GatingDecisionContext* context, |
| const GURL& source, |
| const GURL& destination) const { |
| if (!MayBlockNavigation()) { |
| return origin_gating::Decision::kAllowed; |
| } |
| |
| // GURL() is passed as primary_url to represent the wildcard source pattern. |
| const content_settings::RuleEntry* rule_entry = |
| rules_.Find(GURL(), destination); |
| |
| if (!rule_entry) { |
| return has_allowlist_ ? origin_gating::Decision::kBlocked |
| : origin_gating::Decision::kAllowed; |
| } |
| |
| std::optional<ContentSetting> setting = |
| content_settings::ParseContentSettingValue(rule_entry->second.value); |
| CHECK(setting.has_value()); |
| switch (setting.value()) { |
| case CONTENT_SETTING_ALLOW: |
| return origin_gating::Decision::kAllowed; |
| case CONTENT_SETTING_BLOCK: |
| return origin_gating::Decision::kBlocked; |
| case CONTENT_SETTING_DEFAULT: |
| case CONTENT_SETTING_ASK: |
| case CONTENT_SETTING_SESSION_ONLY: |
| case CONTENT_SETTING_NUM_SETTINGS: |
| NOTREACHED(); |
| } |
| |
| NOTREACHED(); |
| } |