1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/extensions/activity_log/activity_log.h"
10 #include "base/command_line.h"
11 #include "base/json/json_string_value_serializer.h"
12 #include "base/logging.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/threading/thread_checker.h"
16 #include "chrome/browser/extensions/activity_log/activity_action_constants.h"
17 #include "chrome/browser/extensions/activity_log/counting_policy.h"
18 #include "chrome/browser/extensions/activity_log/fullstream_ui_policy.h"
19 #include "chrome/browser/extensions/api/activity_log_private/activity_log_private_api.h"
20 #include "chrome/browser/extensions/extension_service.h"
21 #include "chrome/browser/extensions/extension_system.h"
22 #include "chrome/browser/extensions/extension_system_factory.h"
23 #include "chrome/browser/extensions/extension_tab_util.h"
24 #include "chrome/browser/extensions/install_tracker_factory.h"
25 #include "chrome/browser/prefs/pref_service_syncable.h"
26 #include "chrome/browser/prerender/prerender_manager.h"
27 #include "chrome/browser/prerender/prerender_manager_factory.h"
28 #include "chrome/browser/profiles/incognito_helpers.h"
29 #include "chrome/browser/ui/browser.h"
30 #include "chrome/common/chrome_constants.h"
31 #include "chrome/common/chrome_switches.h"
32 #include "chrome/common/extensions/extension.h"
33 #include "chrome/common/pref_names.h"
34 #include "components/browser_context_keyed_service/browser_context_dependency_manager.h"
35 #include "content/public/browser/web_contents.h"
36 #include "third_party/re2/re2/re2.h"
39 #if !defined(OS_ANDROID)
40 #include "chrome/browser/extensions/activity_log/uma_policy.h"
43 namespace constants = activity_log_constants;
47 using extensions::Action;
48 using constants::kArgUrlPlaceholder;
50 // If DOM API methods start with this string, we flag them as being of type
51 // DomActionType::XHR.
52 const char kDomXhrPrefix[] = "XMLHttpRequest.";
54 // Specifies a possible action to take to get an extracted URL in the ApiInfo
62 // Information about specific Chrome and DOM APIs, such as which contain
63 // arguments that should be extracted into the arg_url field of an Action.
65 // The lookup key consists of the action_type and api_name in the Action
67 Action::ActionType action_type;
70 // If non-negative, an index into args might contain a URL to be extracted
74 // A transformation to apply to the data found at index arg_url_index in the
77 // If NONE, the data is expected to be a string which is treated as a URL.
79 // If LOOKUP_TAB_ID, the data is either an integer which is treated as a tab
80 // ID and translated (in the context of a provided Profile), or a list of tab
81 // IDs which are translated.
83 // If DICT_LOOKUP, the data is expected to be a dictionary, and
84 // arg_url_dict_path is a path (list of keys delimited by ".") where a URL
85 // string is to be found.
86 Transformation arg_url_transform;
87 const char* arg_url_dict_path;
90 static const ApiInfo kApiInfoTable[] = {
91 // Tabs APIs that require tab ID translation
92 {Action::ACTION_API_CALL, "tabs.connect", 0, LOOKUP_TAB_ID, NULL},
93 {Action::ACTION_API_CALL, "tabs.detectLanguage", 0, LOOKUP_TAB_ID, NULL},
94 {Action::ACTION_API_CALL, "tabs.duplicate", 0, LOOKUP_TAB_ID, NULL},
95 {Action::ACTION_API_CALL, "tabs.executeScript", 0, LOOKUP_TAB_ID, NULL},
96 {Action::ACTION_API_CALL, "tabs.get", 0, LOOKUP_TAB_ID, NULL},
97 {Action::ACTION_API_CALL, "tabs.insertCSS", 0, LOOKUP_TAB_ID, NULL},
98 {Action::ACTION_API_CALL, "tabs.move", 0, LOOKUP_TAB_ID, NULL},
99 {Action::ACTION_API_CALL, "tabs.reload", 0, LOOKUP_TAB_ID, NULL},
100 {Action::ACTION_API_CALL, "tabs.remove", 0, LOOKUP_TAB_ID, NULL},
101 {Action::ACTION_API_CALL, "tabs.sendMessage", 0, LOOKUP_TAB_ID, NULL},
102 {Action::ACTION_API_CALL, "tabs.update", 0, LOOKUP_TAB_ID, NULL},
104 {Action::ACTION_API_EVENT, "tabs.onUpdated", 0, LOOKUP_TAB_ID, NULL},
105 {Action::ACTION_API_EVENT, "tabs.onMoved", 0, LOOKUP_TAB_ID, NULL},
106 {Action::ACTION_API_EVENT, "tabs.onDetached", 0, LOOKUP_TAB_ID, NULL},
107 {Action::ACTION_API_EVENT, "tabs.onAttached", 0, LOOKUP_TAB_ID, NULL},
108 {Action::ACTION_API_EVENT, "tabs.onRemoved", 0, LOOKUP_TAB_ID, NULL},
109 {Action::ACTION_API_EVENT, "tabs.onReplaced", 0, LOOKUP_TAB_ID, NULL},
111 // Other APIs that accept URLs as strings
112 {Action::ACTION_API_CALL, "bookmarks.create", 0, DICT_LOOKUP, "url"},
113 {Action::ACTION_API_CALL, "bookmarks.update", 1, DICT_LOOKUP, "url"},
114 {Action::ACTION_API_CALL, "cookies.get", 0, DICT_LOOKUP, "url"},
115 {Action::ACTION_API_CALL, "cookies.getAll", 0, DICT_LOOKUP, "url"},
116 {Action::ACTION_API_CALL, "cookies.remove", 0, DICT_LOOKUP, "url"},
117 {Action::ACTION_API_CALL, "cookies.set", 0, DICT_LOOKUP, "url"},
118 {Action::ACTION_API_CALL, "downloads.download", 0, DICT_LOOKUP, "url"},
119 {Action::ACTION_API_CALL, "history.addUrl", 0, DICT_LOOKUP, "url"},
120 {Action::ACTION_API_CALL, "history.deleteUrl", 0, DICT_LOOKUP, "url"},
121 {Action::ACTION_API_CALL, "history.getVisits", 0, DICT_LOOKUP, "url"},
122 {Action::ACTION_API_CALL, "webstore.install", 0, NONE, NULL},
123 {Action::ACTION_API_CALL, "windows.create", 0, DICT_LOOKUP, "url"},
125 {Action::ACTION_DOM_ACCESS, "Document.location", 0, NONE, NULL},
126 {Action::ACTION_DOM_ACCESS, "Location.assign", 0, NONE, NULL},
127 {Action::ACTION_DOM_ACCESS, "Location.replace", 0, NONE, NULL},
128 {Action::ACTION_DOM_ACCESS, "Window.location", 0, NONE, NULL},
129 {Action::ACTION_DOM_ACCESS, "XMLHttpRequest.open", 1, NONE, NULL},
132 // A singleton class which provides lookups into the kApiInfoTable data
133 // structure. It inserts all data into a map on first lookup.
134 class ApiInfoDatabase {
136 static ApiInfoDatabase* GetInstance() {
137 return Singleton<ApiInfoDatabase>::get();
140 // Retrieves an ApiInfo record for the given Action type. Returns either a
141 // pointer to the record, or NULL if no such record was found.
142 const ApiInfo* Lookup(Action::ActionType action_type,
143 const std::string& api_name) const {
144 std::map<std::string, const ApiInfo*>::const_iterator i =
145 api_database_.find(api_name);
146 if (i == api_database_.end())
148 if (i->second->action_type != action_type)
155 for (size_t i = 0; i < arraysize(kApiInfoTable); i++) {
156 const ApiInfo* info = &kApiInfoTable[i];
157 api_database_[info->api_name] = info;
160 virtual ~ApiInfoDatabase() {}
162 // The map is keyed by API name only, since API names aren't be repeated
163 // across multiple action types in kApiInfoTable. However, the action type
164 // should still be checked before returning a positive match.
165 std::map<std::string, const ApiInfo*> api_database_;
167 friend struct DefaultSingletonTraits<ApiInfoDatabase>;
168 DISALLOW_COPY_AND_ASSIGN(ApiInfoDatabase);
171 // Gets the URL for a given tab ID. Helper method for ExtractUrls. Returns
172 // true if able to perform the lookup. The URL is stored to *url, and
173 // *is_incognito is set to indicate whether the URL is for an incognito tab.
174 bool GetUrlForTabId(int tab_id,
177 bool* is_incognito) {
178 content::WebContents* contents = NULL;
179 Browser* browser = NULL;
180 bool found = ExtensionTabUtil::GetTabById(tab_id,
182 true, // search incognito tabs too
188 *url = contents->GetURL();
189 *is_incognito = browser->profile()->IsOffTheRecord();
196 // Resolves an argument URL relative to a base page URL. If the page URL is
197 // not valid, then only absolute argument URLs are supported.
198 bool ResolveUrl(const GURL& base, const std::string& arg, GURL* arg_out) {
200 *arg_out = base.Resolve(arg);
202 *arg_out = GURL(arg);
204 return arg_out->is_valid();
207 // Performs processing of the Action object to extract URLs from the argument
208 // list and translate tab IDs to URLs, according to the API call metadata in
209 // kApiInfoTable. Mutates the Action object in place. There is a small chance
210 // that the tab id->URL translation could be wrong, if the tab has already been
211 // navigated by the time of invocation.
213 // Any extracted URL is stored into the arg_url field of the action, and the
214 // URL in the argument list is replaced with the marker value "<arg_url>". For
215 // APIs that take a list of tab IDs, extracts the first valid URL into arg_url
216 // and overwrites the other tab IDs in the argument list with the translated
218 void ExtractUrls(scoped_refptr<Action> action, Profile* profile) {
219 const ApiInfo* api_info = ApiInfoDatabase::GetInstance()->Lookup(
220 action->action_type(), action->api_name());
221 if (api_info == NULL)
224 int url_index = api_info->arg_url_index;
226 if (!action->args() || url_index < 0 ||
227 static_cast<size_t>(url_index) >= action->args()->GetSize())
230 // Do not overwrite an existing arg_url value in the Action, so that callers
231 // have the option of doing custom arg_url extraction.
232 if (action->arg_url().is_valid())
236 bool arg_incognito = action->page_incognito();
238 switch (api_info->arg_url_transform) {
240 // No translation needed; just extract the URL directly from a raw string
241 // or from a dictionary. Succeeds if we can find a string in the
242 // argument list and that the string resolves to a valid URL.
243 std::string url_string;
244 if (action->args()->GetString(url_index, &url_string) &&
245 ResolveUrl(action->page_url(), url_string, &arg_url)) {
246 action->mutable_args()->Set(url_index,
247 new StringValue(kArgUrlPlaceholder));
253 CHECK(api_info->arg_url_dict_path);
254 // Look up the URL from a dictionary at the specified location. Succeeds
255 // if we can find a dictionary in the argument list, the dictionary
256 // contains the specified key, and the corresponding value resolves to a
258 DictionaryValue* dict = NULL;
259 std::string url_string;
260 if (action->mutable_args()->GetDictionary(url_index, &dict) &&
261 dict->GetString(api_info->arg_url_dict_path, &url_string) &&
262 ResolveUrl(action->page_url(), url_string, &arg_url)) {
263 dict->SetString(api_info->arg_url_dict_path, kArgUrlPlaceholder);
268 case LOOKUP_TAB_ID: {
269 // Translation of tab IDs to URLs has been requested. There are two
270 // cases to consider: either a single integer or a list of integers (when
271 // multiple tabs are manipulated).
273 base::ListValue* tab_list = NULL;
274 if (action->args()->GetInteger(url_index, &tab_id)) {
275 // Single tab ID to translate.
276 GetUrlForTabId(tab_id, profile, &arg_url, &arg_incognito);
277 if (arg_url.is_valid()) {
278 action->mutable_args()->Set(url_index,
279 new StringValue(kArgUrlPlaceholder));
281 } else if (action->mutable_args()->GetList(url_index, &tab_list)) {
282 // A list of possible IDs to translate. Work through in reverse order
283 // so the last one translated is left in arg_url.
284 int extracted_index = -1; // Which list item is copied to arg_url?
285 for (int i = tab_list->GetSize() - 1; i >= 0; --i) {
286 if (tab_list->GetInteger(i, &tab_id) &&
287 GetUrlForTabId(tab_id, profile, &arg_url, &arg_incognito)) {
289 tab_list->Set(i, new base::StringValue(arg_url.spec()));
293 if (extracted_index >= 0)
294 tab_list->Set(extracted_index, new StringValue(kArgUrlPlaceholder));
303 if (arg_url.is_valid()) {
304 action->set_arg_incognito(arg_incognito);
305 action->set_arg_url(arg_url);
311 namespace extensions {
313 // ActivityLogFactory
315 ActivityLogFactory* ActivityLogFactory::GetInstance() {
316 return Singleton<ActivityLogFactory>::get();
319 BrowserContextKeyedService* ActivityLogFactory::BuildServiceInstanceFor(
320 content::BrowserContext* profile) const {
321 return new ActivityLog(static_cast<Profile*>(profile));
324 content::BrowserContext* ActivityLogFactory::GetBrowserContextToUse(
325 content::BrowserContext* context) const {
326 return chrome::GetBrowserContextRedirectedInIncognito(context);
329 ActivityLogFactory::ActivityLogFactory()
330 : BrowserContextKeyedServiceFactory(
332 BrowserContextDependencyManager::GetInstance()) {
333 DependsOn(ExtensionSystemFactory::GetInstance());
334 DependsOn(InstallTrackerFactory::GetInstance());
338 ActivityLog* ActivityLog::GetInstance(Profile* profile) {
339 return ActivityLogFactory::GetForProfile(profile);
342 ActivityLogFactory::~ActivityLogFactory() {
347 // SET THINGS UP. --------------------------------------------------------------
349 // Use GetInstance instead of directly creating an ActivityLog.
350 ActivityLog::ActivityLog(Profile* profile)
351 : database_policy_(NULL),
352 database_policy_type_(ActivityLogPolicy::POLICY_INVALID),
356 testing_mode_(false),
359 watchdog_app_active_(false) {
360 // This controls whether logging statements are printed & which policy is set.
361 testing_mode_ = CommandLine::ForCurrentProcess()->HasSwitch(
362 switches::kEnableExtensionActivityLogTesting);
364 // Check if the watchdog extension is previously installed and active.
365 watchdog_app_active_ =
366 profile_->GetPrefs()->GetBoolean(prefs::kWatchdogExtensionActive);
368 observers_ = new ObserverListThreadSafe<Observer>;
370 // Check that the right threads exist for logging to the database.
371 // If not, we shouldn't try to do things that require them.
372 if (!BrowserThread::IsMessageLoopValid(BrowserThread::DB) ||
373 !BrowserThread::IsMessageLoopValid(BrowserThread::FILE) ||
374 !BrowserThread::IsMessageLoopValid(BrowserThread::IO)) {
375 has_threads_ = false;
378 db_enabled_ = has_threads_
379 && (CommandLine::ForCurrentProcess()->
380 HasSwitch(switches::kEnableExtensionActivityLogging)
381 || watchdog_app_active_);
383 ExtensionSystem::Get(profile_)->ready().Post(
385 base::Bind(&ActivityLog::InitInstallTracker, base::Unretained(this)));
387 // None of this should run on Android since the AL is behind ENABLE_EXTENSION
388 // checks. However, UmaPolicy can't even compile on Android because it uses
389 // BrowserList and related classes that aren't compiled for Android.
390 #if !defined(OS_ANDROID)
391 if (!profile->IsOffTheRecord())
392 uma_policy_ = new UmaPolicy(profile_);
395 ChooseDatabasePolicy();
398 void ActivityLog::SetDatabasePolicy(
399 ActivityLogPolicy::PolicyType policy_type) {
400 if (database_policy_type_ == policy_type)
402 if (!IsDatabaseEnabled() && !IsWatchdogAppActive())
405 // Deleting the old policy takes place asynchronously, on the database
406 // thread. Initializing a new policy below similarly happens
407 // asynchronously. Since the two operations are both queued for the
408 // database, the queue ordering should ensure that the deletion completes
409 // before database initialization occurs.
411 // However, changing policies at runtime is still not recommended, and
412 // likely only should be done for unit tests.
413 if (database_policy_)
414 database_policy_->Close();
416 switch (policy_type) {
417 case ActivityLogPolicy::POLICY_FULLSTREAM:
418 database_policy_ = new FullStreamUIPolicy(profile_);
420 case ActivityLogPolicy::POLICY_COUNTS:
421 database_policy_ = new CountingPolicy(profile_);
426 database_policy_->Init();
427 database_policy_type_ = policy_type;
430 // SHUT DOWN. ------------------------------------------------------------------
432 void ActivityLog::Shutdown() {
433 if (tracker_) tracker_->RemoveObserver(this);
436 ActivityLog::~ActivityLog() {
438 uma_policy_->Close();
439 if (database_policy_)
440 database_policy_->Close();
443 // MAINTAIN STATUS. ------------------------------------------------------------
445 void ActivityLog::InitInstallTracker() {
446 tracker_ = InstallTrackerFactory::GetForProfile(profile_);
447 tracker_->AddObserver(this);
450 void ActivityLog::ChooseDatabasePolicy() {
451 if (!(IsDatabaseEnabled() || IsWatchdogAppActive()))
454 SetDatabasePolicy(ActivityLogPolicy::POLICY_FULLSTREAM);
456 SetDatabasePolicy(ActivityLogPolicy::POLICY_COUNTS);
459 bool ActivityLog::IsDatabaseEnabled() {
460 // Make sure we are not enabled when there are no threads.
461 DCHECK(has_threads_ || !db_enabled_);
465 bool ActivityLog::IsWatchdogAppActive() {
466 return watchdog_app_active_;
469 void ActivityLog::SetWatchdogAppActive(bool active) {
470 watchdog_app_active_ = active;
473 void ActivityLog::OnExtensionLoaded(const Extension* extension) {
474 if (extension->id() != kActivityLogExtensionId) return;
477 if (!watchdog_app_active_) {
478 watchdog_app_active_ = true;
479 profile_->GetPrefs()->SetBoolean(prefs::kWatchdogExtensionActive, true);
481 ChooseDatabasePolicy();
484 void ActivityLog::OnExtensionUnloaded(const Extension* extension) {
485 if (extension->id() != kActivityLogExtensionId) return;
486 if (!CommandLine::ForCurrentProcess()->HasSwitch(
487 switches::kEnableExtensionActivityLogging)) {
490 if (watchdog_app_active_) {
491 watchdog_app_active_ = false;
492 profile_->GetPrefs()->SetBoolean(prefs::kWatchdogExtensionActive,
497 void ActivityLog::OnExtensionUninstalled(const Extension* extension) {
498 if (!database_policy_)
500 // If the extension has been uninstalled but not disabled, we delete the
502 if (extension->id() == kActivityLogExtensionId) {
503 if (!CommandLine::ForCurrentProcess()->HasSwitch(
504 switches::kEnableExtensionActivityLogging)) {
508 database_policy_->RemoveExtensionData(extension->id());
512 void ActivityLog::AddObserver(ActivityLog::Observer* observer) {
513 observers_->AddObserver(observer);
516 void ActivityLog::RemoveObserver(ActivityLog::Observer* observer) {
517 observers_->RemoveObserver(observer);
521 void ActivityLog::RegisterProfilePrefs(
522 user_prefs::PrefRegistrySyncable* registry) {
523 registry->RegisterBooleanPref(
524 prefs::kWatchdogExtensionActive,
526 user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
529 // LOG ACTIONS. ----------------------------------------------------------------
531 void ActivityLog::LogAction(scoped_refptr<Action> action) {
532 if (ActivityLogAPI::IsExtensionWhitelisted(action->extension_id()))
535 // Perform some preprocessing of the Action data: convert tab IDs to URLs and
536 // mask out incognito URLs if appropriate.
537 ExtractUrls(action, profile_);
539 // Mark DOM XHR requests as such, for easier processing later.
540 if (action->action_type() == Action::ACTION_DOM_ACCESS &&
541 StartsWithASCII(action->api_name(), kDomXhrPrefix, true) &&
543 DictionaryValue* other = action->mutable_other();
545 if (other->GetInteger(constants::kActionDomVerb, &dom_verb) &&
546 dom_verb == DomActionType::METHOD) {
547 other->SetInteger(constants::kActionDomVerb, DomActionType::XHR);
552 uma_policy_->ProcessAction(action);
553 if (IsDatabaseEnabled() && database_policy_)
554 database_policy_->ProcessAction(action);
555 if (IsWatchdogAppActive())
556 observers_->Notify(&Observer::OnExtensionActivity, action);
558 LOG(INFO) << action->PrintForDebug();
561 void ActivityLog::OnScriptsExecuted(
562 const content::WebContents* web_contents,
563 const ExecutingScriptsMap& extension_ids,
565 const GURL& on_url) {
567 Profile::FromBrowserContext(web_contents->GetBrowserContext());
568 const ExtensionService* extension_service =
569 ExtensionSystem::Get(profile)->extension_service();
570 const ExtensionSet* extensions = extension_service->extensions();
571 const prerender::PrerenderManager* prerender_manager =
572 prerender::PrerenderManagerFactory::GetForProfile(
573 Profile::FromBrowserContext(web_contents->GetBrowserContext()));
575 for (ExecutingScriptsMap::const_iterator it = extension_ids.begin();
576 it != extension_ids.end(); ++it) {
577 const Extension* extension = extensions->GetByID(it->first);
578 if (!extension || ActivityLogAPI::IsExtensionWhitelisted(extension->id()))
581 // If OnScriptsExecuted is fired because of tabs.executeScript, the list
582 // of content scripts will be empty. We don't want to log it because
583 // the call to tabs.executeScript will have already been logged anyway.
584 if (!it->second.empty()) {
585 scoped_refptr<Action> action;
586 action = new Action(extension->id(),
588 Action::ACTION_CONTENT_SCRIPT,
589 ""); // no API call here
590 action->set_page_url(on_url);
591 action->set_page_title(base::UTF16ToUTF8(web_contents->GetTitle()));
592 action->set_page_incognito(
593 web_contents->GetBrowserContext()->IsOffTheRecord());
594 if (prerender_manager &&
595 prerender_manager->IsWebContentsPrerendering(web_contents, NULL))
596 action->mutable_other()->SetBoolean(constants::kActionPrerender, true);
597 for (std::set<std::string>::const_iterator it2 = it->second.begin();
598 it2 != it->second.end();
600 action->mutable_args()->AppendString(*it2);
607 // LOOKUP ACTIONS. -------------------------------------------------------------
609 void ActivityLog::GetFilteredActions(
610 const std::string& extension_id,
611 const Action::ActionType type,
612 const std::string& api_name,
613 const std::string& page_url,
614 const std::string& arg_url,
617 <void(scoped_ptr<std::vector<scoped_refptr<Action> > >)>& callback) {
618 if (database_policy_) {
619 database_policy_->ReadFilteredData(
620 extension_id, type, api_name, page_url, arg_url, daysAgo, callback);
624 // DELETE ACTIONS. -------------------------------------------------------------
626 void ActivityLog::RemoveURLs(const std::vector<GURL>& restrict_urls) {
627 if (!database_policy_)
629 database_policy_->RemoveURLs(restrict_urls);
632 void ActivityLog::RemoveURLs(const std::set<GURL>& restrict_urls) {
633 if (!database_policy_)
636 std::vector<GURL> urls;
637 for (std::set<GURL>::const_iterator it = restrict_urls.begin();
638 it != restrict_urls.end(); ++it) {
641 database_policy_->RemoveURLs(urls);
644 void ActivityLog::RemoveURL(const GURL& url) {
647 std::vector<GURL> urls;
652 void ActivityLog::DeleteDatabase() {
653 if (!database_policy_)
655 database_policy_->DeleteDatabase();
658 } // namespace extensions