blob: 1b545c007c5b0fb240bb32e74567d67465cc2bce [file] [log] [blame]
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "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/json/json_string_value_serializer.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/post_task.h"
#include "base/threading/thread_checker.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chrome_notification_types.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/api/activity_log_private/activity_log_private_api.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/prerender/prerender_manager.h"
#include "chrome/browser/prerender/prerender_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.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/notification_service.h"
#include "content/public/browser/notification_source.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.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/common/extension.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/one_shot_event.h"
#include "third_party/re2/src/re2/re2.h"
#include "url/gurl.h"
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 Transformation {
NONE,
DICT_LOOKUP,
LOOKUP_TAB_ID,
};
// 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 NONE, the data is expected to be a string which is treated as a URL.
//
// If LOOKUP_TAB_ID, 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 DICT_LOOKUP, 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, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.detectLanguage", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.duplicate", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.executeScript", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.get", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.insertCSS", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.move", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.reload", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.remove", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.sendMessage", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_CALL, "tabs.update", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_EVENT, "tabs.onUpdated", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_EVENT, "tabs.onMoved", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_EVENT, "tabs.onDetached", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_EVENT, "tabs.onAttached", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_EVENT, "tabs.onRemoved", 0, LOOKUP_TAB_ID, NULL},
{Action::ACTION_API_EVENT, "tabs.onReplaced", 0, LOOKUP_TAB_ID, NULL},
// Other APIs that accept URLs as strings
{Action::ACTION_API_CALL, "bookmarks.create", 0, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "bookmarks.update", 1, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "cookies.get", 0, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "cookies.getAll", 0, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "cookies.remove", 0, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "cookies.set", 0, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "downloads.download", 0, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "history.addUrl", 0, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "history.deleteUrl", 0, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "history.getVisits", 0, DICT_LOOKUP, "url"},
{Action::ACTION_API_CALL, "webstore.install", 0, NONE, NULL},
{Action::ACTION_API_CALL, "windows.create", 0, DICT_LOOKUP, "url"},
{Action::ACTION_DOM_ACCESS, "Document.location", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLAnchorElement.href", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLButtonElement.formAction", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLEmbedElement.src", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLFormElement.action", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLFrameElement.src", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLHtmlElement.manifest", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLIFrameElement.src", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLImageElement.longDesc", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLImageElement.src", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLImageElement.lowsrc", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLInputElement.formAction", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLInputElement.src", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLLinkElement.href", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLMediaElement.src", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLMediaElement.currentSrc", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLModElement.cite", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLObjectElement.data", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLQuoteElement.cite", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLScriptElement.src", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLSourceElement.src", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLTrackElement.src", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "HTMLVideoElement.poster", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "Location.assign", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "Location.replace", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "Window.location", 0, NONE, NULL},
{Action::ACTION_DOM_ACCESS, "XMLHttpRequest.open", 1, NONE, NULL}};
// A singleton class which provides lookups into the kApiInfoTable data
// structure. It inserts all data into a map on first lookup.
class ApiInfoDatabase {
public:
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 NULL;
if (i->second->action_type != action_type)
return NULL;
return i->second;
}
private:
ApiInfoDatabase() {
for (size_t i = 0; i < arraysize(kApiInfoTable); i++) {
const ApiInfo* info = &kApiInfoTable[i];
api_database_[info->api_name] = info;
}
}
virtual ~ApiInfoDatabase() {}
// 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, const ApiInfo*> api_database_;
friend struct base::DefaultSingletonTraits<ApiInfoDatabase>;
DISALLOW_COPY_AND_ASSIGN(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 = NULL;
Browser* browser = NULL;
bool found = ExtensionTabUtil::GetTabById(
tab_id,
profile,
true, // Search incognito tabs, too.
&browser,
NULL,
&contents,
NULL);
if (found) {
*url = contents->GetURL();
*is_incognito = browser->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 == NULL)
return;
int url_index = api_info->arg_url_index;
if (!action->args() || url_index < 0 ||
static_cast<size_t>(url_index) >= action->args()->GetSize())
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;
GURL arg_url;
bool arg_incognito = action->page_incognito();
switch (api_info->arg_url_transform) {
case NONE: {
// 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.
std::string url_string;
if (action->args()->GetString(url_index, &url_string) &&
ResolveUrl(action->page_url(), url_string, &arg_url)) {
action->mutable_args()->Set(
url_index, std::make_unique<base::Value>(kArgUrlPlaceholder));
}
break;
}
case DICT_LOOKUP: {
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.
base::DictionaryValue* dict = NULL;
std::string url_string;
if (action->mutable_args()->GetDictionary(url_index, &dict) &&
dict->GetString(api_info->arg_url_dict_path, &url_string) &&
ResolveUrl(action->page_url(), url_string, &arg_url)) {
dict->SetString(api_info->arg_url_dict_path, kArgUrlPlaceholder);
}
break;
}
case LOOKUP_TAB_ID: {
// 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;
base::ListValue* tab_list = NULL;
if (action->args()->GetInteger(url_index, &tab_id)) {
// Single tab ID to translate.
GetUrlForTabId(tab_id, profile, &arg_url, &arg_incognito);
if (arg_url.is_valid()) {
action->mutable_args()->Set(
url_index, std::make_unique<base::Value>(kArgUrlPlaceholder));
}
} else if (action->mutable_args()->GetList(url_index, &tab_list)) {
// 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->GetSize() - 1; i >= 0; --i) {
if (tab_list->GetInteger(i, &tab_id) &&
GetUrlForTabId(tab_id, profile, &arg_url, &arg_incognito)) {
if (!arg_incognito)
tab_list->Set(i, std::make_unique<base::Value>(arg_url.spec()));
extracted_index = i;
}
}
if (extracted_index >= 0) {
tab_list->Set(extracted_index,
std::make_unique<base::Value>(kArgUrlPlaceholder));
}
}
break;
}
default:
NOTREACHED();
}
if (arg_url.is_valid()) {
action->set_arg_incognito(arg_incognito);
action->set_arg_url(arg_url);
}
}
// A global, thread-safe record of activity log state.
class ActivityLogState {
public:
ActivityLogState() {}
~ActivityLogState() {}
void AddActiveContext(content::BrowserContext* context) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::AutoLock lock(lock_);
contexts_.insert(context);
}
void RemoveActiveContext(content::BrowserContext* context) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::AutoLock lock(lock_);
contexts_.erase(context);
}
bool IsActiveContext(content::BrowserContext* context) {
base::AutoLock lock(lock_);
return contexts_.count(context) > 0;
}
void AddWhitelistedId(const std::string& id) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
base::AutoLock lock(lock_);
whitelisted_ids_.insert(id);
}
// We don't remove the id entry from g_activity_log_state because it may be
// loaded in multiple profiles, and being whitelisted for the ActivityLog
// is a global permission.
bool IsWhitelistedId(const std::string& id) {
base::AutoLock lock(lock_);
return whitelisted_ids_.count(id) > 0;
}
private:
std::set<const content::BrowserContext*> contexts_;
std::set<std::string> whitelisted_ids_;
base::Lock lock_;
DISALLOW_COPY_AND_ASSIGN(ActivityLogState);
};
base::LazyInstance<ActivityLogState>::DestructorAtExit g_activity_log_state =
LAZY_INSTANCE_INITIALIZER;
// Returns the ActivityLog associated with the given |browser_context| after
// checking that |browser_context| is valid.
ActivityLog* SafeGetActivityLog(content::BrowserContext* browser_context) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// 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 (g_browser_process->IsShuttingDown() ||
!g_browser_process->profile_manager()->IsValidProfile(browser_context)) {
return nullptr;
}
return ActivityLog::GetInstance(browser_context);
}
// Calls into the ActivityLog to log an api event or function call.
// Must be called on the UI thread.
void LogApiActivityOnUI(content::BrowserContext* browser_context,
const std::string& extension_id,
const std::string& activity_name,
std::unique_ptr<base::ListValue> args,
Action::ActionType type) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
ActivityLog* activity_log = SafeGetActivityLog(browser_context);
if (!activity_log || !activity_log->ShouldLog(extension_id))
return;
scoped_refptr<Action> action =
new Action(extension_id, base::Time::Now(), type, activity_name);
action->set_args(std::move(args));
activity_log->LogAction(action);
}
// Generic thread-safe handler for API calls and events.
void LogApiActivity(content::BrowserContext* browser_context,
const std::string& extension_id,
const std::string& activity_name,
const base::ListValue& args,
Action::ActionType type) {
ActivityLogState& state = g_activity_log_state.Get();
if (!state.IsActiveContext(browser_context) ||
state.IsWhitelistedId(extension_id))
return;
if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
base::PostTaskWithTraits(
FROM_HERE, {BrowserThread::UI},
base::BindOnce(&LogApiActivityOnUI, browser_context, extension_id,
activity_name, args.CreateDeepCopy(), type));
return;
}
LogApiActivityOnUI(browser_context, extension_id, activity_name,
args.CreateDeepCopy(), type);
}
// Handler for API events. Thread-safe.
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. Thread-safe.
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);
}
// Calls into the ActivityLog to log a webRequest usage.
// Must be called on the UI thread.
void LogWebRequestActivityOnUI(content::BrowserContext* browser_context,
const std::string& extension_id,
const GURL& url,
bool is_incognito,
const std::string& api_call,
std::unique_ptr<base::DictionaryValue> details) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
ActivityLog* activity_log = SafeGetActivityLog(browser_context);
if (!activity_log || !activity_log->ShouldLog(extension_id))
return;
scoped_refptr<Action> action = new 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);
}
// Handler for webRequest use. Thread-safe.
void LogWebRequestActivity(content::BrowserContext* browser_context,
const std::string& extension_id,
const GURL& url,
bool is_incognito,
const std::string& api_call,
std::unique_ptr<base::DictionaryValue> details) {
ActivityLogState& state = g_activity_log_state.Get();
if (!state.IsActiveContext(browser_context) ||
state.IsWhitelistedId(extension_id))
return;
if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
base::PostTaskWithTraits(
FROM_HERE, {BrowserThread::UI},
base::BindOnce(&LogWebRequestActivityOnUI, browser_context,
extension_id, url, is_incognito, api_call,
std::move(details)));
return;
}
LogWebRequestActivityOnUI(browser_context, extension_id, url, is_incognito,
api_call, std::move(details));
}
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_(NULL),
database_policy_type_(ActivityLogPolicy::POLICY_INVALID),
profile_(Profile::FromBrowserContext(context)),
extension_system_(ExtensionSystem::Get(context)),
db_enabled_(false),
testing_mode_(false),
extension_registry_observer_(this),
active_consumers_(0),
cached_consumer_count_(0),
is_active_(false),
weak_factory_(this) {
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_ = new base::ObserverListThreadSafe<Observer>;
extension_registry_observer_.Add(ExtensionRegistry::Get(profile_));
CheckActive(true); // use cached
extension_system_->ready().Post(
FROM_HERE,
base::Bind(&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();
if (is_active_)
g_activity_log_state.Get().RemoveActiveContext(profile_);
}
// 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::OnExtensionLoaded(content::BrowserContext* browser_context,
const Extension* extension) {
if (!ActivityLogAPI::IsExtensionWhitelisted(extension->id()))
return;
g_activity_log_state.Get().AddWhitelistedId(extension->id());
++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 (!ActivityLogAPI::IsExtensionWhitelisted(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 (ActivityLogAPI::IsExtensionWhitelisted(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, false);
}
// 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::DictionaryValue* other = action->mutable_other();
int dom_verb = -1;
if (other->GetInteger(constants::kActionDomVerb, &dom_verb) &&
dom_verb == DomActionType::METHOD) {
other->SetInteger(constants::kActionDomVerb, DomActionType::XHR);
}
}
if (IsDatabaseEnabled() && database_policy_)
database_policy_->ProcessAction(action);
if (IsWatchdogAppActive())
observers_->Notify(FROM_HERE, &Observer::OnExtensionActivity, action);
if (testing_mode_)
VLOG(1) << action->PrintForDebug();
}
bool ActivityLog::ShouldLog(const std::string& extension_id) const {
return is_active_ && !ActivityLogAPI::IsExtensionWhitelisted(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 (auto it = extension_ids.begin(); it != extension_ids.end(); ++it) {
const Extension* extension =
registry->GetExtensionById(it->first, ExtensionRegistry::ENABLED);
if (!extension || ActivityLogAPI::IsExtensionWhitelisted(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 (!it->second.empty()) {
scoped_refptr<Action> action;
action = new 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::PrerenderManager* prerender_manager =
prerender::PrerenderManagerFactory::GetForBrowserContext(profile_);
if (prerender_manager &&
prerender_manager->IsWebContentsPrerendering(web_contents, NULL))
action->mutable_other()->SetBoolean(constants::kActionPrerender, true);
for (auto it2 = it->second.begin(); it2 != it->second.end(); ++it2) {
action->mutable_args()->AppendString(*it2);
}
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::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 (auto it = restrict_urls.begin(); it != restrict_urls.end(); ++it) {
urls.push_back(*it);
}
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) {
bool has_consumer =
active_consumers_ || (use_cached && cached_consumer_count_);
bool needs_db =
has_consumer || base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableExtensionActivityLogging);
bool should_be_active = needs_db || has_consumer;
if (should_be_active == is_active_)
return;
ActivityLogState& state = g_activity_log_state.Get();
content::BrowserContext* off_the_record =
profile_->HasOffTheRecordProfile() ? profile_->GetOffTheRecordProfile()
: nullptr;
bool has_db = db_enabled_ && database_policy_;
bool old_is_active = is_active_;
if (should_be_active) {
if (needs_db && !has_db) {
db_enabled_ = true;
ChooseDatabasePolicy();
}
state.AddActiveContext(profile_);
if (off_the_record)
state.AddActiveContext(off_the_record);
registrar_.Add(this, chrome::NOTIFICATION_PROFILE_CREATED,
content::NotificationService::AllSources());
registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED,
content::NotificationService::AllSources());
is_active_ = true;
} else {
if (has_db && !needs_db)
db_enabled_ = false;
state.RemoveActiveContext(profile_);
if (off_the_record)
state.RemoveActiveContext(off_the_record);
registrar_.RemoveAll();
is_active_ = false;
}
DCHECK_NE(is_active_, old_is_active);
for (content::RenderProcessHost::iterator iter(
content::RenderProcessHost::AllHostsIterator());
!iter.IsAtEnd(); iter.Advance()) {
content::RenderProcessHost* host = iter.GetCurrentValue();
if (profile_->IsSameProfile(
Profile::FromBrowserContext(host->GetBrowserContext()))) {
host->Send(new ExtensionMsg_SetActivityLoggingEnabled(is_active_));
}
}
}
void ActivityLog::Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK(is_active_);
switch (type) {
case chrome::NOTIFICATION_PROFILE_CREATED: {
Profile* profile = content::Source<Profile>(source).ptr();
if (profile_->IsSameProfile(profile))
g_activity_log_state.Get().AddActiveContext(profile);
break;
}
case chrome::NOTIFICATION_PROFILE_DESTROYED: {
Profile* profile = content::Source<Profile>(source).ptr();
if (profile_->IsSameProfile(profile))
g_activity_log_state.Get().RemoveActiveContext(profile);
break;
}
default:
NOTREACHED();
}
}
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