| // 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/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/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/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, |
| }; |
| |
| // 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. |
| 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}, |
| |
| // 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::Value::List& 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::Value::List& 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; |
| } |
| |
| 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::Value::List& 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)) |
| 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::Value::List& 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::Value::List& 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::Value::Dict 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)) |
| 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); |
| } |
| |
| // 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())); |
| |
| // 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::Value::Dict& 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 (testing_mode_) |
| VLOG(1) << action->PrintForDebug(); |
| } |
| |
| bool ActivityLog::ShouldLog(const std::string& extension_id) const { |
| // Do not log for activities from the browser/WebUI, which is indicated by an |
| // empty extension ID. |
| return is_active_ && !extension_id.empty() && |
| !IsExtensionAllowlisted(extension_id); |
| } |
| |
| void ActivityLog::OnScriptsExecuted(content::WebContents* web_contents, |
| const ExecutingScriptsMap& extension_ids, |
| const GURL& on_url) { |
| if (!is_active_) |
| 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 || IsExtensionAllowlisted(extension->id())) |
| 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(); |
| } |
| } |
| |
| template <> |
| void BrowserContextKeyedAPIFactory<ActivityLog>::DeclareFactoryDependencies() { |
| DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory()); |
| DependsOn(ExtensionRegistryFactory::GetInstance()); |
| } |
| |
| } // namespace extensions |