// Copyright 2011 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/activity_log/activity_log.h"

#include <stddef.h>

#include <memory>
#include <set>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/json/json_string_value_serializer.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/singleton.h"
#include "base/one_shot_event.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_checker.h"
#include "base/values.h"
#include "chrome/browser/extensions/activity_log/activity_action_constants.h"
#include "chrome/browser/extensions/activity_log/counting_policy.h"
#include "chrome/browser/extensions/activity_log/fullstream_ui_policy.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/preloading/prefetch/no_state_prefetch/no_state_prefetch_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/activity_log_policy_util.h"
#include "chrome/common/pref_names.h"
#include "components/no_state_prefetch/browser/no_state_prefetch_manager.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/api_activity_monitor.h"
#include "extensions/browser/extension_registry_factory.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_system_provider.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/renderer_startup_helper.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/features/feature_provider.h"
#include "extensions/common/hashed_extension_id.h"
#include "extensions/common/mojom/renderer.mojom.h"
#include "third_party/re2/src/re2/re2.h"
#include "url/gurl.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace constants = activity_log_constants;

namespace extensions {

namespace {

using constants::kArgUrlPlaceholder;
using content::BrowserThread;

// If DOM API methods start with this string, we flag them as being of type
// DomActionType::XHR.
const char kDomXhrPrefix[] = "XMLHttpRequest.";

// Specifies a possible action to take to get an extracted URL in the ApiInfo
// structure below.
enum class Transformation {
  kNone,
  kDictLookup,
  kLookupTabId,
  kDictLookupTabId,
};

// Information about specific Chrome and DOM APIs, such as which contain
// arguments that should be extracted into the arg_url field of an Action.
struct ApiInfo {
  // The lookup key consists of the action_type and api_name in the Action
  // object.
  Action::ActionType action_type;
  const char* api_name;

  // If non-negative, an index into args might contain a URL to be extracted
  // into arg_url.
  int arg_url_index;

  // A transformation to apply to the data found at index arg_url_index in the
  // argument list.
  //
  // If Transformation::kNone, the data is expected to be a string which is
  // treated as a URL.
  //
  // If Transformation::kLookupTabId, the data is either an integer which is
  // treated as a tab ID and translated (in the context of a provided Profile),
  // or a list of tab IDs which are translated.
  //
  // If Transformation::kDictLookup, the data is expected to be a dictionary,
  // and arg_url_dict_path is a path (list of keys delimited by ".") where a URL
  // string is to be found.
  //
  // If Transformation::kDictLookupTabId, the data is expected to be a
  // dictionary, and arg_url_dict_path is a path where a tab ID is to be found
  // and translated.
  Transformation arg_url_transform;
  const char* arg_url_dict_path;
};

static const ApiInfo kApiInfoTable[] = {
    // Tabs APIs that require tab ID translation
    {Action::ACTION_API_CALL, "tabs.connect", 0, Transformation::kLookupTabId,
     nullptr},
    {Action::ACTION_API_CALL, "tabs.detectLanguage", 0,
     Transformation::kLookupTabId, nullptr},
    {Action::ACTION_API_CALL, "tabs.duplicate", 0, Transformation::kLookupTabId,
     nullptr},
    {Action::ACTION_API_CALL, "tabs.executeScript", 0,
     Transformation::kLookupTabId, nullptr},
    {Action::ACTION_API_CALL, "tabs.get", 0, Transformation::kLookupTabId,
     nullptr},
    {Action::ACTION_API_CALL, "tabs.insertCSS", 0, Transformation::kLookupTabId,
     nullptr},
    {Action::ACTION_API_CALL, "tabs.move", 0, Transformation::kLookupTabId,
     nullptr},
    {Action::ACTION_API_CALL, "tabs.reload", 0, Transformation::kLookupTabId,
     nullptr},
    {Action::ACTION_API_CALL, "tabs.remove", 0, Transformation::kLookupTabId,
     nullptr},
    {Action::ACTION_API_CALL, "tabs.sendMessage", 0,
     Transformation::kLookupTabId, nullptr},
    {Action::ACTION_API_CALL, "tabs.update", 0, Transformation::kLookupTabId,
     nullptr},
    {Action::ACTION_API_EVENT, "tabs.onUpdated", 0,
     Transformation::kLookupTabId, nullptr},
    {Action::ACTION_API_EVENT, "tabs.onMoved", 0, Transformation::kLookupTabId,
     nullptr},
    {Action::ACTION_API_EVENT, "tabs.onDetached", 0,
     Transformation::kLookupTabId, nullptr},
    {Action::ACTION_API_EVENT, "tabs.onAttached", 0,
     Transformation::kLookupTabId, nullptr},
    {Action::ACTION_API_EVENT, "tabs.onRemoved", 0,
     Transformation::kLookupTabId, nullptr},
    {Action::ACTION_API_EVENT, "tabs.onReplaced", 0,
     Transformation::kLookupTabId, nullptr},

    // Scripting APIs that require tab ID translation
    {Action::ACTION_API_CALL, "scripting.executeScript", 0,
     Transformation::kDictLookupTabId, "target.tabId"},

    // Other APIs that accept URLs as strings
    {Action::ACTION_API_CALL, "bookmarks.create", 0,
     Transformation::kDictLookup, "url"},
    {Action::ACTION_API_CALL, "bookmarks.update", 1,
     Transformation::kDictLookup, "url"},
    {Action::ACTION_API_CALL, "cookies.get", 0, Transformation::kDictLookup,
     "url"},
    {Action::ACTION_API_CALL, "cookies.getAll", 0, Transformation::kDictLookup,
     "url"},
    {Action::ACTION_API_CALL, "cookies.remove", 0, Transformation::kDictLookup,
     "url"},
    {Action::ACTION_API_CALL, "cookies.set", 0, Transformation::kDictLookup,
     "url"},
    {Action::ACTION_API_CALL, "downloads.download", 0,
     Transformation::kDictLookup, "url"},
    {Action::ACTION_API_CALL, "history.addUrl", 0, Transformation::kDictLookup,
     "url"},
    {Action::ACTION_API_CALL, "history.deleteUrl", 0,
     Transformation::kDictLookup, "url"},
    {Action::ACTION_API_CALL, "history.getVisits", 0,
     Transformation::kDictLookup, "url"},
    {Action::ACTION_API_CALL, "webstore.install", 0, Transformation::kNone,
     nullptr},
    {Action::ACTION_API_CALL, "windows.create", 0, Transformation::kDictLookup,
     "url"},
    {Action::ACTION_DOM_ACCESS, "Document.location", 0, Transformation::kNone,
     nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLAnchorElement.href", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLButtonElement.formAction", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLEmbedElement.src", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLFormElement.action", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLFrameElement.src", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLHtmlElement.manifest", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLIFrameElement.src", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLImageElement.longDesc", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLImageElement.src", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLImageElement.lowsrc", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLInputElement.formAction", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLInputElement.src", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLLinkElement.href", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLMediaElement.src", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLMediaElement.currentSrc", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLModElement.cite", 0, Transformation::kNone,
     nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLObjectElement.data", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLQuoteElement.cite", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLScriptElement.src", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLSourceElement.src", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLTrackElement.src", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "HTMLVideoElement.poster", 0,
     Transformation::kNone, nullptr},
    {Action::ACTION_DOM_ACCESS, "Location.assign", 0, Transformation::kNone,
     nullptr},
    {Action::ACTION_DOM_ACCESS, "Location.replace", 0, Transformation::kNone,
     nullptr},
    {Action::ACTION_DOM_ACCESS, "Window.location", 0, Transformation::kNone,
     nullptr},
    {Action::ACTION_DOM_ACCESS, "XMLHttpRequest.open", 1, Transformation::kNone,
     nullptr}};

// A singleton class which provides lookups into the kApiInfoTable data
// structure.  It inserts all data into a map on first lookup.
class ApiInfoDatabase {
 public:
  ApiInfoDatabase(const ApiInfoDatabase&) = delete;
  ApiInfoDatabase& operator=(const ApiInfoDatabase&) = delete;

  static ApiInfoDatabase* GetInstance() {
    return base::Singleton<ApiInfoDatabase>::get();
  }

  // Retrieves an ApiInfo record for the given Action type.  Returns either a
  // pointer to the record, or NULL if no such record was found.
  const ApiInfo* Lookup(Action::ActionType action_type,
                        const std::string& api_name) const {
    auto i = api_database_.find(api_name);
    if (i == api_database_.end())
      return nullptr;
    if (i->second->action_type != action_type)
      return nullptr;
    return i->second;
  }

 private:
  ApiInfoDatabase() {
    for (const auto& info : kApiInfoTable) {
      api_database_[info.api_name] = &info;
    }
  }
  virtual ~ApiInfoDatabase() = default;

  // The map is keyed by API name only, since API names aren't be repeated
  // across multiple action types in kApiInfoTable.  However, the action type
  // should still be checked before returning a positive match.
  std::map<std::string, raw_ptr<const ApiInfo, CtnExperimental>> api_database_;

  friend struct base::DefaultSingletonTraits<ApiInfoDatabase>;
};

// Gets the URL for a given tab ID.  Helper method for ExtractUrls.  Returns
// true if able to perform the lookup.  The URL is stored to *url, and
// *is_incognito is set to indicate whether the URL is for an incognito tab.
bool GetUrlForTabId(int tab_id,
                    Profile* profile,
                    GURL* url,
                    bool* is_incognito) {
  content::WebContents* contents = nullptr;
  WindowController* window = nullptr;
  bool found =
      ExtensionTabUtil::GetTabById(tab_id, profile,
                                   true,  // Search incognito tabs, too.
                                   &window, &contents, nullptr);

  if (found && window) {
    *url = contents->GetURL();
    *is_incognito = window->profile()->IsOffTheRecord();
    return true;
  } else {
    return false;
  }
}

// Resolves an argument URL relative to a base page URL.  If the page URL is
// not valid, then only absolute argument URLs are supported.
bool ResolveUrl(const GURL& base, const std::string& arg, GURL* arg_out) {
  if (base.is_valid())
    *arg_out = base.Resolve(arg);
  else
    *arg_out = GURL(arg);

  return arg_out->is_valid();
}

// Performs processing of the Action object to extract URLs from the argument
// list and translate tab IDs to URLs, according to the API call metadata in
// kApiInfoTable.  Mutates the Action object in place.  There is a small chance
// that the tab id->URL translation could be wrong, if the tab has already been
// navigated by the time of invocation.
//
// Any extracted URL is stored into the arg_url field of the action, and the
// URL in the argument list is replaced with the marker value "<arg_url>".  For
// APIs that take a list of tab IDs, extracts the first valid URL into arg_url
// and overwrites the other tab IDs in the argument list with the translated
// URL.
void ExtractUrls(scoped_refptr<Action> action, Profile* profile) {
  const ApiInfo* api_info = ApiInfoDatabase::GetInstance()->Lookup(
      action->action_type(), action->api_name());
  if (api_info == nullptr)
    return;

  int url_index = api_info->arg_url_index;

  if (!action->args() || url_index < 0 ||
      static_cast<size_t>(url_index) >= action->args()->size())
    return;

  // Do not overwrite an existing arg_url value in the Action, so that callers
  // have the option of doing custom arg_url extraction.
  if (action->arg_url().is_valid())
    return;

  base::ListValue& args_list = action->mutable_args();

  GURL arg_url;
  bool arg_incognito = action->page_incognito();

  switch (api_info->arg_url_transform) {
    case Transformation::kNone: {
      // No translation needed; just extract the URL directly from a raw string
      // or from a dictionary.  Succeeds if we can find a string in the
      // argument list and that the string resolves to a valid URL.
      if (args_list[url_index].is_string() &&
          ResolveUrl(action->page_url(), args_list[url_index].GetString(),
                     &arg_url)) {
        args_list[url_index] = base::Value(kArgUrlPlaceholder);
      }
      break;
    }

    case Transformation::kDictLookup: {
      CHECK(api_info->arg_url_dict_path);
      // Look up the URL from a dictionary at the specified location.  Succeeds
      // if we can find a dictionary in the argument list, the dictionary
      // contains the specified key, and the corresponding value resolves to a
      // valid URL.
      if (args_list[url_index].is_dict()) {
        const std::string* url_string =
            args_list[url_index].GetDict().FindStringByDottedPath(
                api_info->arg_url_dict_path);
        if (url_string &&
            ResolveUrl(action->page_url(), *url_string, &arg_url)) {
          args_list[url_index].GetDict().SetByDottedPath(
              api_info->arg_url_dict_path, kArgUrlPlaceholder);
        }
      }
      break;
    }

    case Transformation::kLookupTabId: {
      // Translation of tab IDs to URLs has been requested.  There are two
      // cases to consider: either a single integer or a list of integers (when
      // multiple tabs are manipulated).
      int tab_id;

      if (args_list[url_index].is_int()) {
        tab_id = args_list[url_index].GetInt();
        // Single tab ID to translate.
        GetUrlForTabId(tab_id, profile, &arg_url, &arg_incognito);
        if (arg_url.is_valid())
          args_list[url_index] = base::Value(kArgUrlPlaceholder);
      } else if (args_list[url_index].is_list()) {
        base::ListValue& tab_list = args_list[url_index].GetList();
        // A list of possible IDs to translate.  Work through in reverse order
        // so the last one translated is left in arg_url.
        int extracted_index = -1;  // Which list item is copied to arg_url?
        for (int i = tab_list.size() - 1; i >= 0; --i) {
          if (!tab_list[i].is_int())
            continue;
          tab_id = tab_list[i].GetInt();
          if (!GetUrlForTabId(tab_id, profile, &arg_url, &arg_incognito))
            continue;
          if (!arg_incognito)
            tab_list[i] = base::Value(arg_url.spec());
          extracted_index = i;
        }
        if (extracted_index >= 0)
          tab_list[extracted_index] = base::Value(kArgUrlPlaceholder);
      }
      break;
    }

    case Transformation::kDictLookupTabId: {
      CHECK(api_info->arg_url_dict_path);
      // Look up a tab ID from a dictionary at the specified location.
      if (args_list[url_index].is_dict()) {
        std::optional<int> tab_id =
            args_list[url_index].GetDict().FindIntByDottedPath(
                api_info->arg_url_dict_path);
        if (tab_id &&
            GetUrlForTabId(*tab_id, profile, &arg_url, &arg_incognito)) {
          args_list[url_index].GetDict().SetByDottedPath(
              api_info->arg_url_dict_path, kArgUrlPlaceholder);
        }
      }
      break;
    }

    default:
      NOTREACHED();
  }

  if (arg_url.is_valid()) {
    action->set_arg_incognito(arg_incognito);
    action->set_arg_url(arg_url);
  }
}

// Returns the ActivityLog associated with the given |browser_context| after
// checking that |browser_context| is valid.
ActivityLog* SafeGetActivityLog(content::BrowserContext* browser_context) {
  // There's a chance that the |browser_context| was deleted some time during
  // the thread hops.
  // TODO(devlin): We should probably be doing this more extensively throughout
  // extensions code.
  if (ExtensionsBrowserClient::Get()->IsShuttingDown() ||
      !ExtensionsBrowserClient::Get()->IsValidContext(browser_context)) {
    return nullptr;
  }
  return ActivityLog::GetInstance(browser_context);
}

bool IsExtensionAllowlisted(const std::string& extension_id) {
  // TODO(devlin): Pass in a HashedExtensionId to avoid this conversion.
  return FeatureProvider::GetPermissionFeatures()
      ->GetFeature("activityLogPrivate")
      ->IsIdInAllowlist(HashedExtensionId(extension_id));
}

// Calls into the ActivityLog to log an api event or function call.
void LogApiActivity(content::BrowserContext* browser_context,
                    const std::string& extension_id,
                    const std::string& activity_name,
                    const base::ListValue& args,
                    Action::ActionType type) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (IsExtensionAllowlisted(extension_id))
    return;

  ActivityLog* activity_log = SafeGetActivityLog(browser_context);
  if (!activity_log ||
      !activity_log->ShouldLog(extension_id, type, activity_name)) {
    return;
  }

  auto action = base::MakeRefCounted<Action>(extension_id, base::Time::Now(),
                                             type, activity_name);
  action->set_args(args.Clone());
  activity_log->LogAction(action);
}

// Handler for API events.
void LogApiEvent(content::BrowserContext* browser_context,
                 const std::string& extension_id,
                 const std::string& event_name,
                 const base::ListValue& args) {
  LogApiActivity(browser_context, extension_id, event_name, args,
                 Action::ACTION_API_EVENT);
}

// Handler for API function calls.
void LogApiFunction(content::BrowserContext* browser_context,
                    const std::string& extension_id,
                    const std::string& event_name,
                    const base::ListValue& args) {
  LogApiActivity(browser_context, extension_id, event_name, args,
                 Action::ACTION_API_CALL);
}

// Handler for webRequest use.
void LogWebRequestActivity(content::BrowserContext* browser_context,
                           const std::string& extension_id,
                           const GURL& url,
                           bool is_incognito,
                           const std::string& api_call,
                           base::DictValue details) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (IsExtensionAllowlisted(extension_id))
    return;

  ActivityLog* activity_log = SafeGetActivityLog(browser_context);
  if (!activity_log ||
      !activity_log->ShouldLog(extension_id, Action::ACTION_WEB_REQUEST,
                               api_call)) {
    return;
  }

  auto action = base::MakeRefCounted<Action>(
      extension_id, base::Time::Now(), Action::ACTION_WEB_REQUEST, api_call);
  action->set_page_url(url);
  action->set_page_incognito(is_incognito);
  action->mutable_other().Set(activity_log_constants::kActionWebRequest,
                              std::move(details));
  activity_log->LogAction(action);
}

void SetActivityHandlers() {
  // Set up event handlers. We don't have to worry about unsetting these,
  // because we check whether or not the activity log is active for the context
  // in the monitor methods.
  activity_monitor::Monitor current_function_monitor =
      activity_monitor::GetApiFunctionMonitor();
  DCHECK(!current_function_monitor ||
         current_function_monitor == &LogApiFunction);
  if (!current_function_monitor)
    activity_monitor::SetApiFunctionMonitor(&LogApiFunction);

  activity_monitor::Monitor current_event_monitor =
      activity_monitor::GetApiEventMonitor();
  DCHECK(!current_event_monitor || current_event_monitor == &LogApiEvent);
  if (!current_event_monitor)
    activity_monitor::SetApiEventMonitor(&LogApiEvent);

  activity_monitor::WebRequestMonitor current_web_request_monitor =
      activity_monitor::GetWebRequestMonitor();
  DCHECK(!current_web_request_monitor ||
         current_web_request_monitor == &LogWebRequestActivity);
  if (!current_web_request_monitor)
    activity_monitor::SetWebRequestMonitor(&LogWebRequestActivity);
}

}  // namespace

// SET THINGS UP. --------------------------------------------------------------

static base::LazyInstance<BrowserContextKeyedAPIFactory<ActivityLog>>::
    DestructorAtExit g_activity_log_factory = LAZY_INSTANCE_INITIALIZER;

BrowserContextKeyedAPIFactory<ActivityLog>* ActivityLog::GetFactoryInstance() {
  return g_activity_log_factory.Pointer();
}

// static
ActivityLog* ActivityLog::GetInstance(content::BrowserContext* context) {
  return ActivityLog::GetFactoryInstance()->Get(
      Profile::FromBrowserContext(context));
}

// Use GetInstance instead of directly creating an ActivityLog.
ActivityLog::ActivityLog(content::BrowserContext* context)
    : database_policy_(nullptr),
      database_policy_type_(ActivityLogPolicy::POLICY_INVALID),
      profile_(Profile::FromBrowserContext(context)),
      extension_system_(ExtensionSystem::Get(context)),
      db_enabled_(false),
      testing_mode_(false),
      active_consumers_(0),
      cached_consumer_count_(0),
      has_listeners_(false),
      is_active_(false) {
  SetActivityHandlers();

  // This controls whether logging statements are printed & which policy is set.
  testing_mode_ = base::CommandLine::ForCurrentProcess()->HasSwitch(
      switches::kEnableExtensionActivityLogTesting);

  // Check if the watchdog extension is previously installed and active.
  cached_consumer_count_ =
      profile_->GetPrefs()->GetInteger(prefs::kWatchdogExtensionActive);

  observers_ = base::MakeRefCounted<base::ObserverListThreadSafe<Observer>>();

  extension_registry_observation_.Observe(ExtensionRegistry::Get(profile_));
  CheckActive(true);  // use cached
  extension_system_->ready().Post(
      FROM_HERE, base::BindOnce(&ActivityLog::OnExtensionSystemReady,
                                weak_factory_.GetWeakPtr()));
}

void ActivityLog::SetDatabasePolicy(
    ActivityLogPolicy::PolicyType policy_type) {
  if (database_policy_type_ == policy_type)
    return;
  if (!IsDatabaseEnabled() && !IsWatchdogAppActive())
    return;

  // Deleting the old policy takes place asynchronously, on the database
  // thread.  Initializing a new policy below similarly happens
  // asynchronously.  Since the two operations are both queued for the
  // database, the queue ordering should ensure that the deletion completes
  // before database initialization occurs.
  //
  // However, changing policies at runtime is still not recommended, and
  // likely only should be done for unit tests.
  if (database_policy_)
    database_policy_->Close();

  switch (policy_type) {
    case ActivityLogPolicy::POLICY_FULLSTREAM:
      database_policy_ = new FullStreamUIPolicy(profile_);
      break;
    case ActivityLogPolicy::POLICY_COUNTS:
      database_policy_ = new CountingPolicy(profile_);
      break;
    default:
      NOTREACHED();
  }
  database_policy_->Init();
  database_policy_type_ = policy_type;
}

ActivityLog::~ActivityLog() {
  if (database_policy_)
    database_policy_->Close();
}

// MAINTAIN STATUS. ------------------------------------------------------------

void ActivityLog::ChooseDatabasePolicy() {
  if (!(IsDatabaseEnabled() || IsWatchdogAppActive()))
    return;
  if (testing_mode_)
    SetDatabasePolicy(ActivityLogPolicy::POLICY_FULLSTREAM);
  else
    SetDatabasePolicy(ActivityLogPolicy::POLICY_COUNTS);
}

bool ActivityLog::IsDatabaseEnabled() {
  return db_enabled_;
}

bool ActivityLog::IsWatchdogAppActive() {
  return active_consumers_ > 0;
}

void ActivityLog::UpdateCachedConsumerCount() {
  cached_consumer_count_ = active_consumers_;
  profile_->GetPrefs()->SetInteger(prefs::kWatchdogExtensionActive,
                                   cached_consumer_count_);
}

void ActivityLog::SetWatchdogAppActiveForTesting(bool active) {
  active_consumers_ = active ? 1 : 0;
  CheckActive(false);  // don't use cached
}

void ActivityLog::SetHasListeners(bool has_listeners) {
  has_listeners_ = has_listeners;
  CheckActive(false);  // don't use cached
}

void ActivityLog::OnExtensionLoaded(content::BrowserContext* browser_context,
                                    const Extension* extension) {
  if (!IsExtensionAllowlisted(extension->id()))
    return;

  ++active_consumers_;

  if (!extension_system_->ready().is_signaled())
    return;

  CheckActive(false);  // don't use cached
  UpdateCachedConsumerCount();
}

void ActivityLog::OnExtensionUnloaded(content::BrowserContext* browser_context,
                                      const Extension* extension,
                                      UnloadedExtensionReason reason) {
  if (!IsExtensionAllowlisted(extension->id()))
    return;
  --active_consumers_;

  if (!extension_system_->ready().is_signaled())
    return;

  CheckActive(false);  // don't use cached
  UpdateCachedConsumerCount();
}

// OnExtensionUnloaded will also be called right before this.
void ActivityLog::OnExtensionUninstalled(
    content::BrowserContext* browser_context,
    const Extension* extension,
    extensions::UninstallReason reason) {
  if (IsExtensionAllowlisted(extension->id()) &&
      !base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kEnableExtensionActivityLogging) &&
      active_consumers_ == 0) {
    DeleteDatabase();
  } else if (database_policy_) {
    database_policy_->RemoveExtensionData(extension->id());
  }
}

void ActivityLog::AddObserver(ActivityLog::Observer* observer) {
  observers_->AddObserver(observer);
}

void ActivityLog::RemoveObserver(ActivityLog::Observer* observer) {
  observers_->RemoveObserver(observer);
}

void ActivityLog::SetTelemetryLoggingEnabled(bool enabled,
                                             TelemetryCallback callback) {
  if (enabled) {
    CHECK(!callback.is_null());
  }

  bool was_active = IsTelemetryLoggingActive();

  if (enabled &&
      base::FeatureList::IsEnabled(
          extensions_features::kEnterpriseExtensionDOMActivityTelemetry)) {
    telemetry_callback_ = std::move(callback);
  } else {
    telemetry_callback_.Reset();
  }

  if (was_active != IsTelemetryLoggingActive()) {
    NotifyRenderersOfTelemetryLogging();
  }
}

bool ActivityLog::IsTelemetryLoggingActive() const {
  return !telemetry_callback_.is_null();
}

// static
void ActivityLog::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterIntegerPref(prefs::kWatchdogExtensionActive, 0);
}

// LOG ACTIONS. ----------------------------------------------------------------

void ActivityLog::LogAction(scoped_refptr<Action> action) {
  DCHECK(ShouldLog(action->extension_id(), action->action_type(),
                   action->api_name()));

  // Perform some preprocessing of the Action data: convert tab IDs to URLs and
  // mask out incognito URLs if appropriate.
  ExtractUrls(action, profile_);

  // Mark DOM XHR requests as such, for easier processing later.
  if (action->action_type() == Action::ACTION_DOM_ACCESS &&
      base::StartsWith(action->api_name(), kDomXhrPrefix,
                       base::CompareCase::SENSITIVE) &&
      action->other()) {
    base::DictValue& other = action->mutable_other();
    std::optional<int> dom_verb = other.FindInt(constants::kActionDomVerb);
    if (dom_verb == DomActionType::METHOD)
      other.Set(constants::kActionDomVerb, DomActionType::XHR);
  }
  if (IsDatabaseEnabled() && database_policy_)
    database_policy_->ProcessAction(action);
  if (has_listeners_)
    observers_->Notify(FROM_HERE, &Observer::OnExtensionActivity, action);
  if (!telemetry_callback_.is_null()) {
    telemetry_callback_.Run(action);
  }
  if (testing_mode_)
    VLOG(1) << action->PrintForDebug();
}

bool ActivityLog::ShouldLog(const std::string& extension_id,
                            Action::ActionType type,
                            const std::string& api_name) const {
  // 1. Early exit if NO logging is active at all.
  // This avoids expensive allowlist lookups for most users.
  if (!is_active_ && !IsTelemetryLoggingActive()) {
    return false;
  }

  // 2. Do not log for activities from the browser/WebUI or allowlisted
  // extensions.
  if (extension_id.empty() || IsExtensionAllowlisted(extension_id)) {
    return false;
  }

  // 3. If standard Activity Log is active, log everything.
  if (is_active_) {
    return true;
  }

  // 4. Telemetry-specific filtering.
  // Map browser-side ActionType to common ActivityType.
  activity_log_policy_util::ActivityType activity_type;
  switch (type) {
    case Action::ACTION_DOM_ACCESS:
      activity_type = activity_log_policy_util::ActivityType::kDomAccess;
      break;
    case Action::ACTION_API_CALL:
      activity_type = activity_log_policy_util::ActivityType::kApiCall;
      break;
    case Action::ACTION_API_EVENT:
      activity_type = activity_log_policy_util::ActivityType::kApiEvent;
      break;
    case Action::ACTION_WEB_REQUEST:
      activity_type = activity_log_policy_util::ActivityType::kWebRequest;
      break;
    case Action::ACTION_CONTENT_SCRIPT:
      activity_type = activity_log_policy_util::ActivityType::kContentScript;
      break;
    default:
      return false;
  }
  return activity_log_policy_util::IsActivityIncludedInTelemetry(api_name,
                                                                 activity_type);
}

void ActivityLog::OnScriptsExecuted(content::WebContents* web_contents,
                                    const ExecutingScriptsMap& extension_ids,
                                    const GURL& on_url) {
  if (!is_active_ && !IsTelemetryLoggingActive()) {
    return;
  }
  ExtensionRegistry* registry = ExtensionRegistry::Get(profile_);
  for (const auto& extension_id : extension_ids) {
    const Extension* extension =
        registry->enabled_extensions().GetByID(extension_id.first);
    if (!extension || !ShouldLog(extension->id(), Action::ACTION_CONTENT_SCRIPT,
                                 std::string())) {
      continue;
    }

    // If OnScriptsExecuted is fired because of tabs.executeScript, the list
    // of content scripts will be empty.  We don't want to log it because
    // the call to tabs.executeScript will have already been logged anyway.
    if (!extension_id.second.empty()) {
      auto action = base::MakeRefCounted<Action>(
          extension->id(), base::Time::Now(), Action::ACTION_CONTENT_SCRIPT,
          "");  // no API call here
      action->set_page_url(on_url);
      action->set_page_title(base::UTF16ToUTF8(web_contents->GetTitle()));
      action->set_page_incognito(
          web_contents->GetBrowserContext()->IsOffTheRecord());

      const prerender::NoStatePrefetchManager* no_state_prefetch_manager =
          prerender::NoStatePrefetchManagerFactory::GetForBrowserContext(
              profile_);
      if (no_state_prefetch_manager &&
          no_state_prefetch_manager->IsWebContentsPrefetching(web_contents))
        action->mutable_other().Set(constants::kActionPrerender, true);
      for (const auto& id : extension_id.second) {
        action->mutable_args().Append(id);
      }
      LogAction(action);
    }
  }
}

void ActivityLog::ObserveScripts(ScriptExecutor* executor) {
  executor->set_observer(base::BindRepeating(&ActivityLog::OnScriptsExecuted,
                                             weak_factory_.GetWeakPtr()));
}

// LOOKUP ACTIONS. -------------------------------------------------------------

void ActivityLog::GetFilteredActions(
    const std::string& extension_id,
    const Action::ActionType type,
    const std::string& api_name,
    const std::string& page_url,
    const std::string& arg_url,
    const int daysAgo,
    base::OnceCallback<
        void(std::unique_ptr<std::vector<scoped_refptr<Action>>>)> callback) {
  if (database_policy_) {
    database_policy_->ReadFilteredData(extension_id, type, api_name, page_url,
                                       arg_url, daysAgo, std::move(callback));
  }
}

// DELETE ACTIONS. -------------------------------------------------------------

void ActivityLog::RemoveActions(const std::vector<int64_t>& action_ids) {
  if (!database_policy_)
    return;
  database_policy_->RemoveActions(action_ids);
}

void ActivityLog::RemoveExtensionData(const std::string& extension_id) {
  if (!database_policy_)
    return;
  database_policy_->RemoveExtensionData(extension_id);
}

void ActivityLog::RemoveURLs(const std::vector<GURL>& restrict_urls) {
  if (!database_policy_)
    return;
  database_policy_->RemoveURLs(restrict_urls);
}

void ActivityLog::RemoveURLs(const std::set<GURL>& restrict_urls) {
  if (!database_policy_)
    return;

  std::vector<GURL> urls;
  for (const auto& url : restrict_urls) {
    urls.push_back(url);
  }
  database_policy_->RemoveURLs(urls);
}

void ActivityLog::RemoveURL(const GURL& url) {
  if (url.is_empty())
    return;
  std::vector<GURL> urls;
  urls.push_back(url);
  RemoveURLs(urls);
}

void ActivityLog::DeleteDatabase() {
  if (!database_policy_)
    return;
  database_policy_->DeleteDatabase();
}

void ActivityLog::CheckActive(bool use_cached) {
  const bool has_switch = base::CommandLine::ForCurrentProcess()->HasSwitch(
      switches::kEnableExtensionActivityLogging);
  const bool has_consumer =
      active_consumers_ || (use_cached && cached_consumer_count_) ||
      // Only check |has_listeners_| if the switch is also present, since
      // we want to ensure the activity log is inactive unless the switch
      // or the app (covered by active_consumers_) is present.
      (has_listeners_ && has_switch);
  const bool should_be_active = has_consumer || has_switch;

  if (should_be_active == is_active_)
    return;

  bool has_db = db_enabled_ && database_policy_;
  db_enabled_ = should_be_active;

  if (should_be_active && !has_db)
    ChooseDatabasePolicy();

  is_active_ = should_be_active;

  for (content::RenderProcessHost::iterator iter(
           content::RenderProcessHost::AllHostsIterator());
       !iter.IsAtEnd(); iter.Advance()) {
    content::RenderProcessHost* host = iter.GetCurrentValue();
    if (host->IsInitializedAndNotDead() &&
        profile_->IsSameOrParent(
            Profile::FromBrowserContext(host->GetBrowserContext()))) {
      mojom::Renderer* renderer =
          RendererStartupHelperFactory::GetForBrowserContext(
              host->GetBrowserContext())
              ->GetRenderer(host);
      if (renderer)
        renderer->SetActivityLoggingEnabled(is_active_);
    }
  }
}

void ActivityLog::OnExtensionSystemReady() {
  if (active_consumers_ != cached_consumer_count_) {
    CheckActive(false);
    UpdateCachedConsumerCount();
  }
}

void ActivityLog::NotifyRenderersOfTelemetryLogging() {
  for (content::RenderProcessHost::iterator iter(
           content::RenderProcessHost::AllHostsIterator());
       !iter.IsAtEnd(); iter.Advance()) {
    content::RenderProcessHost* host = iter.GetCurrentValue();
    if (host->IsInitializedAndNotDead()) {
      Profile* host_profile =
          Profile::FromBrowserContext(host->GetBrowserContext());
      // Don't gather telemetry from incognito profiles.
      if (!host_profile->IsOffTheRecord() &&
          profile_->IsSameOrParent(host_profile)) {
        mojom::Renderer* renderer =
            RendererStartupHelperFactory::GetForBrowserContext(
                host->GetBrowserContext())
                ->GetRenderer(host);
        if (renderer) {
          renderer->SetPolicyActivityLoggingEnabled(IsTelemetryLoggingActive());
        }
      }
    }
  }
}

template <>
void BrowserContextKeyedAPIFactory<ActivityLog>::DeclareFactoryDependencies() {
  DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory());
  DependsOn(ExtensionRegistryFactory::GetInstance());
}

}  // namespace extensions
