// Copyright 2012 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/extensions/api/declarative_content/chrome_content_rules_registry.h"

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/api/declarative/rules_registry_service.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/rules_registry_ids.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/api/declarative/declarative_constants.h"
#include "extensions/common/extension_id.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

//
// EvaluationScope
//

// Used to coalesce multiple requests for evaluation into a zero or one actual
// evaluations (depending on the EvaluationDisposition).  This is required for
// correctness when multiple trackers respond to the same event. Otherwise,
// executing the request from the first tracker will be done before the tracked
// state has been updated for the other trackers.
class ChromeContentRulesRegistry::EvaluationScope {
 public:
  // Default disposition is PERFORM_EVALUATION.
  explicit EvaluationScope(ChromeContentRulesRegistry* registry);
  EvaluationScope(ChromeContentRulesRegistry* registry,
                  EvaluationDisposition disposition);

  EvaluationScope(const EvaluationScope&) = delete;
  EvaluationScope& operator=(const EvaluationScope&) = delete;

  ~EvaluationScope();

 private:
  const raw_ptr<ChromeContentRulesRegistry> registry_;
  const EvaluationDisposition previous_disposition_;
};

ChromeContentRulesRegistry::EvaluationScope::EvaluationScope(
    ChromeContentRulesRegistry* registry)
    : EvaluationScope(registry, DEFER_REQUESTS) {}

ChromeContentRulesRegistry::EvaluationScope::EvaluationScope(
    ChromeContentRulesRegistry* registry,
    EvaluationDisposition disposition)
    : registry_(registry),
      previous_disposition_(registry_->evaluation_disposition_) {
  DCHECK_NE(EVALUATE_REQUESTS, disposition);
  registry_->evaluation_disposition_ = disposition;
}

ChromeContentRulesRegistry::EvaluationScope::~EvaluationScope() {
  registry_->evaluation_disposition_ = previous_disposition_;
  if (registry_->evaluation_disposition_ == EVALUATE_REQUESTS) {
    for (content::WebContents* tab : registry_->evaluation_pending_) {
      registry_->EvaluateConditionsForTab(tab);
    }
    registry_->evaluation_pending_.clear();
  }
}

//
// ChromeContentRulesRegistry
//

ChromeContentRulesRegistry::ChromeContentRulesRegistry(
    content::BrowserContext* browser_context,
    RulesCacheDelegate* cache_delegate,
    PredicateEvaluatorsFactory evaluators_factory)
    : ContentRulesRegistry(browser_context,
                           declarative_content_constants::kOnPageChanged,
                           cache_delegate,
                           rules_registry_ids::kDefaultRulesRegistryID),
      evaluators_(std::move(evaluators_factory).Run(this)),
      evaluation_disposition_(EVALUATE_REQUESTS) {}

void ChromeContentRulesRegistry::NotifyPredicateStateUpdated(
    content::WebContents* contents) {
  switch (evaluation_disposition_) {
    case EVALUATE_REQUESTS:
      EvaluateConditionsForTab(contents);
      break;
    case DEFER_REQUESTS:
      evaluation_pending_.insert(contents);
      break;
    case IGNORE_REQUESTS:
      break;
  }
}

bool ChromeContentRulesRegistry::ShouldManagePredicatesForBrowserContext(
    content::BrowserContext* context) {
  return ManagingRulesForBrowserContext(context);
}

void ChromeContentRulesRegistry::MonitorWebContentsForRuleEvaluation(
    content::WebContents* contents) {
  // We rely on active_rules_ to have a key-value pair for |contents| to know
  // which WebContents we are working with.
  active_rules_[contents] =
      std::set<raw_ptr<const ContentRule, SetExperimental>>();

  EvaluationScope evaluation_scope(this);
  for (const std::unique_ptr<ContentPredicateEvaluator>& evaluator :
       evaluators_)
    evaluator->TrackForWebContents(contents);
}

void ChromeContentRulesRegistry::DidFinishNavigation(
    content::WebContents* contents,
    content::NavigationHandle* navigation_handle) {
  if (active_rules_.contains(contents)) {
    EvaluationScope evaluation_scope(this);
    for (const std::unique_ptr<ContentPredicateEvaluator>& evaluator :
         evaluators_)
      evaluator->OnWebContentsNavigation(contents, navigation_handle);
  }
}

void ChromeContentRulesRegistry::WebContentsDestroyed(
    content::WebContents* web_contents) {
  active_rules_.erase(web_contents);
  evaluation_pending_.erase(web_contents);
}

void ChromeContentRulesRegistry::OnWatchedPageChanged(
    content::WebContents* contents,
    const std::vector<std::string>& css_selectors) {
  if (active_rules_.contains(contents)) {
    EvaluationScope evaluation_scope(this);
    for (const std::unique_ptr<ContentPredicateEvaluator>& evaluator :
         evaluators_) {
      evaluator->OnWatchedPageChanged(contents, css_selectors);
    }
  }
}

ChromeContentRulesRegistry::ContentRule::ContentRule(
    const Extension* extension,
    std::vector<std::unique_ptr<const ContentCondition>> conditions,
    std::vector<std::unique_ptr<const ContentAction>> actions,
    int priority)
    : extension(extension),
      conditions(std::move(conditions)),
      actions(std::move(actions)),
      priority(priority) {}

ChromeContentRulesRegistry::ContentRule::~ContentRule() = default;

std::unique_ptr<const ChromeContentRulesRegistry::ContentRule>
ChromeContentRulesRegistry::CreateRule(
    const Extension* extension,
    const std::map<std::string, ContentPredicateFactory*>& predicate_factories,
    const api::events::Rule& api_rule,
    std::string* error) {
  std::vector<std::unique_ptr<const ContentCondition>> conditions;
  for (const base::Value& value : api_rule.conditions) {
    conditions.push_back(
        CreateContentCondition(extension, predicate_factories, value, error));
    if (!error->empty()) {
      return nullptr;
    }
  }

  std::vector<std::unique_ptr<const ContentAction>> actions;
  for (const base::Value& value : api_rule.actions) {
    // TODO(crbug.com/40832669): Migrate api_rule to use base::DictValue to
    // avoid conversion.
    if (!value.is_dict()) {
      return nullptr;
    }

    actions.push_back(ContentAction::Create(browser_context(), extension,
                                            value.GetDict(), error));
    if (!error->empty()) {
      return nullptr;
    }
  }

  // Note: |api_rule| may contain tags, but these are ignored.

  return std::make_unique<ContentRule>(extension, std::move(conditions),
                                       std::move(actions), *api_rule.priority);
}

bool ChromeContentRulesRegistry::ManagingRulesForBrowserContext(
    content::BrowserContext* context) {
  // Manage both the normal context and incognito contexts associated with it.
  return Profile::FromBrowserContext(context)->GetOriginalProfile() ==
      Profile::FromBrowserContext(browser_context());
}

// static
bool ChromeContentRulesRegistry::EvaluateConditionForTab(
    const ContentCondition* condition,
    content::WebContents* tab) {
  for (const std::unique_ptr<const ContentPredicate>& predicate :
       condition->predicates) {
    if (predicate && !predicate->IsIgnored() &&
        !predicate->GetEvaluator()->EvaluatePredicate(predicate.get(), tab)) {
      return false;
    }
  }

  return true;
}

std::set<
    raw_ptr<const ChromeContentRulesRegistry::ContentRule, SetExperimental>>
ChromeContentRulesRegistry::GetMatchingRules(content::WebContents* tab) const {
  const bool is_incognito_tab = tab->GetBrowserContext()->IsOffTheRecord();
  std::set<raw_ptr<const ContentRule, SetExperimental>> matching_rules;
  for (const RulesMap::value_type& rule_id_rule_pair : content_rules_) {
    const ContentRule* rule = rule_id_rule_pair.second.get();
    if (is_incognito_tab &&
        !ShouldEvaluateExtensionRulesForIncognitoRenderer(rule->extension))
      continue;

    for (const std::unique_ptr<const ContentCondition>& condition :
         rule->conditions) {
      if (EvaluateConditionForTab(condition.get(), tab)) {
        matching_rules.insert(rule);
      }
    }
  }
  return matching_rules;
}

std::string ChromeContentRulesRegistry::AddRulesImpl(
    const ExtensionId& extension_id,
    const std::vector<const api::events::Rule*>& api_rules) {
  EvaluationScope evaluation_scope(this);
  const Extension* extension = ExtensionRegistry::Get(browser_context())
      ->enabled_extensions().GetByID(extension_id);
  CHECK(extension);

  std::string error;
  RulesMap new_rules;
  std::map<ContentPredicateEvaluator*,
           std::map<const void*, std::vector<const ContentPredicate*>>>
      new_predicates;

  std::map<std::string, ContentPredicateFactory*> predicate_factories;
  for (const std::unique_ptr<ContentPredicateEvaluator>& evaluator :
       evaluators_) {
    predicate_factories[evaluator->GetPredicateApiAttributeName()] =
        evaluator.get();
  }

  for (auto* api_rule : api_rules) {
    ExtensionIdRuleIdPair rule_id(extension_id, *api_rule->id);
    DCHECK(content_rules_.find(rule_id) == content_rules_.end());

    std::unique_ptr<const ContentRule> rule(
        CreateRule(extension, predicate_factories, *api_rule, &error));
    if (!error.empty()) {
      // Notify evaluators that none of the created predicates will be tracked
      // after all.
      for (const std::unique_ptr<ContentPredicateEvaluator>& evaluator :
           evaluators_) {
        if (!new_predicates[evaluator.get()].empty()) {
          evaluator->TrackPredicates(
              std::map<const void*, std::vector<const ContentPredicate*>>());
        }
      }

      return error;
    }
    DCHECK(rule);

    // Group predicates by evaluator and rule, so we can later notify the
    // evaluators that they have new predicates to manage.
    for (const std::unique_ptr<const ContentCondition>& condition :
         rule->conditions) {
      for (const std::unique_ptr<const ContentPredicate>& predicate :
           condition->predicates) {
        if (predicate.get()) {
          new_predicates[predicate->GetEvaluator()][rule.get()].push_back(
              predicate.get());
        }
      }
    }

    new_rules[rule_id] = std::move(rule);
  }

  // Notify the evaluators about their new predicates.
  for (const std::unique_ptr<ContentPredicateEvaluator>& evaluator :
       evaluators_)
    evaluator->TrackPredicates(new_predicates[evaluator.get()]);

  // Wohoo, everything worked fine.
  content_rules_.insert(std::make_move_iterator(new_rules.begin()),
                        std::make_move_iterator(new_rules.end()));

  // Request evaluation for all WebContents, under the assumption that a
  // non-empty condition has been added.
  for (const auto& web_contents_rules_pair : active_rules_) {
    NotifyPredicateStateUpdated(web_contents_rules_pair.first);
  }

  return std::string();
}

std::string ChromeContentRulesRegistry::RemoveRulesImpl(
    const ExtensionId& extension_id,
    const std::vector<std::string>& rule_identifiers) {
  // Ignore evaluation requests in this function because it reverts actions on
  // any active rules itself. Otherwise, we run the risk of reverting the same
  // rule multiple times.
  EvaluationScope evaluation_scope(this, IGNORE_REQUESTS);

  // Deduplicate the list of rule identifiers to prevent removing the same
  // rule multiple times and invalidating iterators.
  std::set<std::string> deduplicated_rule_identifiers(rule_identifiers.begin(),
                                                      rule_identifiers.end());

  std::vector<RulesMap::iterator> rules_to_erase;
  std::vector<const void*> predicate_groups_to_stop_tracking;
  for (const std::string& id : deduplicated_rule_identifiers) {
    // Skip unknown rules.
    auto content_rules_entry =
        content_rules_.find(std::make_pair(extension_id, id));
    if (content_rules_entry == content_rules_.end()) {
      continue;
    }

    const ContentRule* rule = content_rules_entry->second.get();

    // Remove the ContentRule from active_rules_.
    for (auto& tab_rules_pair : active_rules_) {
      if (tab_rules_pair.second.contains(rule)) {
        ContentAction::ApplyInfo apply_info =
            {rule->extension, browser_context(), tab_rules_pair.first,
             rule->priority};
        for (const auto& action : rule->actions) {
          action->Revert(apply_info);
        }
        tab_rules_pair.second.erase(rule);
      }
    }

    rules_to_erase.push_back(content_rules_entry);
    predicate_groups_to_stop_tracking.push_back(rule);
  }

  // Notify the evaluators to stop tracking the predicates that will be removed.
  for (const std::unique_ptr<ContentPredicateEvaluator>& evaluator :
       evaluators_)
    evaluator->StopTrackingPredicates(predicate_groups_to_stop_tracking);

  // Remove the rules.
  for (auto it : rules_to_erase) {
    content_rules_.erase(it);
  }

  return std::string();
}

std::string ChromeContentRulesRegistry::RemoveAllRulesImpl(
    const ExtensionId& extension_id) {
  // Search all identifiers of rules that belong to extension |extension_id|.
  std::vector<std::string> rule_identifiers;
  for (const RulesMap::value_type& id_rule_pair : content_rules_) {
    const ExtensionIdRuleIdPair& extension_id_rule_id_pair = id_rule_pair.first;
    if (extension_id_rule_id_pair.first == extension_id) {
      rule_identifiers.push_back(extension_id_rule_id_pair.second);
    }
  }

  return RemoveRulesImpl(extension_id, rule_identifiers);
}

void ChromeContentRulesRegistry::EvaluateConditionsForTab(
    content::WebContents* tab) {
  std::set<raw_ptr<const ContentRule, SetExperimental>> matching_rules =
      GetMatchingRules(tab);
  if (matching_rules.empty() && !active_rules_.contains(tab)) {
    return;
  }

  std::set<raw_ptr<const ContentRule, SetExperimental>>& prev_matching_rules =
      active_rules_[tab];
  for (const ContentRule* rule : matching_rules) {
    ContentAction::ApplyInfo apply_info =
        {rule->extension, browser_context(), tab, rule->priority};
    if (!prev_matching_rules.contains(rule)) {
      for (const std::unique_ptr<const ContentAction>& action : rule->actions) {
        action->Apply(apply_info);
      }
    } else {
      for (const std::unique_ptr<const ContentAction>& action : rule->actions) {
        action->Reapply(apply_info);
      }
    }
  }
  for (const ContentRule* rule : prev_matching_rules) {
    if (!matching_rules.contains(rule)) {
      ContentAction::ApplyInfo apply_info =
          {rule->extension, browser_context(), tab, rule->priority};
      for (const std::unique_ptr<const ContentAction>& action : rule->actions) {
        action->Revert(apply_info);
      }
    }
  }

  if (matching_rules.empty()) {
    active_rules_[tab].clear();
  } else {
    swap(matching_rules, prev_matching_rules);
  }
}

bool
ChromeContentRulesRegistry::ShouldEvaluateExtensionRulesForIncognitoRenderer(
    const Extension* extension) const {
  if (!util::IsIncognitoEnabled(extension->id(), browser_context())) {
    return false;
  }

  // Split-mode incognito extensions register their rules with separate
  // RulesRegistries per Original/OffTheRecord browser contexts, whereas
  // spanning-mode extensions share the Original browser context.
  if (util::CanCrossIncognito(extension, browser_context())) {
    // The extension uses spanning mode incognito. No rules should have been
    // registered for the extension in the OffTheRecord registry so
    // execution for that registry should never reach this point.
    CHECK(!browser_context()->IsOffTheRecord());
  } else {
    // The extension uses split mode incognito. Both the Original and
    // OffTheRecord registries may have (separate) rules for this extension.
    // Since we're looking at an incognito renderer, so only the OffTheRecord
    // registry should process its rules.
    if (!browser_context()->IsOffTheRecord()) {
      return false;
    }
  }

  return true;
}

size_t ChromeContentRulesRegistry::GetActiveRulesCountForTesting() {
  size_t count = 0;
  for (const auto& web_contents_rules_pair : active_rules_) {
    count += web_contents_rules_pair.second.size();
  }
  return count;
}

ChromeContentRulesRegistry::~ChromeContentRulesRegistry() = default;

}  // namespace extensions
