// 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 #include #include "base/command_line.h" #include "base/json/json_string_value_serializer.h" #include "base/logging.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/threading/thread_checker.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_service.h" #include "chrome/browser/extensions/extension_system.h" #include "chrome/browser/extensions/extension_system_factory.h" #include "chrome/browser/extensions/extension_tab_util.h" #include "chrome/browser/extensions/install_tracker_factory.h" #include "chrome/browser/prefs/pref_service_syncable.h" #include "chrome/browser/prerender/prerender_manager.h" #include "chrome/browser/prerender/prerender_manager_factory.h" #include "chrome/browser/profiles/incognito_helpers.h" #include "chrome/browser/ui/browser.h" #include "chrome/common/chrome_constants.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/extensions/extension.h" #include "chrome/common/pref_names.h" #include "components/browser_context_keyed_service/browser_context_dependency_manager.h" #include "content/public/browser/web_contents.h" #include "third_party/re2/re2/re2.h" #include "url/gurl.h" namespace constants = activity_log_constants; namespace { // Concatenate arguments. std::string MakeArgList(const base::ListValue* args) { std::string call_signature; base::ListValue::const_iterator it = args->begin(); for (; it != args->end(); ++it) { std::string arg; JSONStringValueSerializer serializer(&arg); if (serializer.SerializeAndOmitBinaryValues(**it)) { if (it != args->begin()) call_signature += ", "; call_signature += arg; } } return call_signature; } // Gets the URL for a given tab ID. Helper method for LookupTabId. 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; } } // Translate tab IDs to URLs in tabs API calls. Mutates the Action object in // place. There is a small chance that the URL translation could be wrong, if // the tab has already been navigated by the time of invocation. // // If a single tab ID is translated to a URL, the URL is stored into arg_url // where it can more easily be searched for in the database. For APIs that // take a list of tab IDs, replace each tab ID with the URL in the argument // list; we can only extract a single URL into arg_url so arbitrarily pull out // the first one. void LookupTabIds(scoped_refptr action, Profile* profile) { const std::string& api_call = action->api_name(); if (api_call == "tabs.get" || // api calls, ID as int api_call == "tabs.connect" || api_call == "tabs.sendMessage" || api_call == "tabs.duplicate" || api_call == "tabs.update" || api_call == "tabs.reload" || api_call == "tabs.detectLanguage" || api_call == "tabs.executeScript" || api_call == "tabs.insertCSS" || api_call == "tabs.move" || // api calls, IDs in array api_call == "tabs.remove" || api_call == "tabs.onUpdated" || // events, ID as int api_call == "tabs.onMoved" || api_call == "tabs.onDetached" || api_call == "tabs.onAttached" || api_call == "tabs.onRemoved" || api_call == "tabs.onReplaced") { int tab_id; base::ListValue* id_list; base::ListValue* args = action->mutable_args(); if (args->GetInteger(0, &tab_id)) { // Single tab ID to translate. GURL url; bool is_incognito; if (GetUrlForTabId(tab_id, profile, &url, &is_incognito)) { action->set_arg_url(url); action->set_arg_incognito(is_incognito); } } else if ((api_call == "tabs.move" || api_call == "tabs.remove") && args->GetList(0, &id_list)) { // Array of tab IDs to translate. for (int i = 0; i < static_cast(id_list->GetSize()); ++i) { if (id_list->GetInteger(i, &tab_id)) { GURL url; bool is_incognito; if (GetUrlForTabId(tab_id, profile, &url, &is_incognito) && !is_incognito) { id_list->Set(i, new base::StringValue(url.spec())); if (i == 0) { action->set_arg_url(url); action->set_arg_incognito(is_incognito); } } } else { LOG(ERROR) << "The tab ID array is malformed at index " << i; } } } } } } // namespace namespace extensions { // ActivityLogFactory ActivityLogFactory* ActivityLogFactory::GetInstance() { return Singleton::get(); } BrowserContextKeyedService* ActivityLogFactory::BuildServiceInstanceFor( content::BrowserContext* profile) const { return new ActivityLog(static_cast(profile)); } content::BrowserContext* ActivityLogFactory::GetBrowserContextToUse( content::BrowserContext* context) const { return chrome::GetBrowserContextRedirectedInIncognito(context); } ActivityLogFactory::ActivityLogFactory() : BrowserContextKeyedServiceFactory( "ActivityLog", BrowserContextDependencyManager::GetInstance()) { DependsOn(ExtensionSystemFactory::GetInstance()); DependsOn(InstallTrackerFactory::GetInstance()); } ActivityLogFactory::~ActivityLogFactory() { } // ActivityLog void ActivityLog::SetDefaultPolicy(ActivityLogPolicy::PolicyType policy_type) { // Can't use IsLogEnabled() here because this is called from inside Init. if (policy_type != policy_type_ && enabled_) { // 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 (policy_) policy_->Close(); switch (policy_type) { case ActivityLogPolicy::POLICY_FULLSTREAM: policy_ = new FullStreamUIPolicy(profile_); break; case ActivityLogPolicy::POLICY_COUNTS: policy_ = new CountingPolicy(profile_); break; default: NOTREACHED(); } policy_type_ = policy_type; } } // Use GetInstance instead of directly creating an ActivityLog. ActivityLog::ActivityLog(Profile* profile) : policy_(NULL), policy_type_(ActivityLogPolicy::POLICY_INVALID), profile_(profile), enabled_(false), policy_chosen_(false), testing_mode_(false), has_threads_(true), tracker_(NULL), watchdog_extension_active_(false) { // This controls whether logging statements are printed, which policy is set, // etc. testing_mode_ = CommandLine::ForCurrentProcess()->HasSwitch( switches::kEnableExtensionActivityLogTesting); // Check if the watchdog extension is previously installed and active. watchdog_extension_active_ = profile_->GetPrefs()->GetBoolean(prefs::kWatchdogExtensionActive); observers_ = new ObserverListThreadSafe; // Check that the right threads exist. If not, we shouldn't try to do things // that require them. if (!BrowserThread::IsMessageLoopValid(BrowserThread::DB) || !BrowserThread::IsMessageLoopValid(BrowserThread::FILE) || !BrowserThread::IsMessageLoopValid(BrowserThread::IO)) { LOG(ERROR) << "Missing threads, disabling Activity Logging!"; has_threads_ = false; } enabled_ = has_threads_ && (CommandLine::ForCurrentProcess()-> HasSwitch(switches::kEnableExtensionActivityLogging) || watchdog_extension_active_); if (enabled_) enabled_on_any_profile_ = true; ExtensionSystem::Get(profile_)->ready().Post( FROM_HERE, base::Bind(&ActivityLog::InitInstallTracker, base::Unretained(this))); ChooseDefaultPolicy(); } void ActivityLog::InitInstallTracker() { tracker_ = InstallTrackerFactory::GetForProfile(profile_); tracker_->AddObserver(this); } void ActivityLog::ChooseDefaultPolicy() { if (policy_chosen_ || !enabled_) return; if (testing_mode_) SetDefaultPolicy(ActivityLogPolicy::POLICY_FULLSTREAM); else SetDefaultPolicy(ActivityLogPolicy::POLICY_COUNTS); } void ActivityLog::Shutdown() { if (tracker_) tracker_->RemoveObserver(this); } ActivityLog::~ActivityLog() { if (policy_) policy_->Close(); } // static bool ActivityLog::enabled_on_any_profile_ = false; // static bool ActivityLog::IsLogEnabledOnAnyProfile() { return enabled_on_any_profile_; } bool ActivityLog::IsLogEnabled() { // Make sure we are not enabled when there are no threads. DCHECK(has_threads_ || !enabled_); return enabled_; } void ActivityLog::OnExtensionLoaded(const Extension* extension) { if (extension->id() != kActivityLogExtensionId) return; if (has_threads_) { enabled_ = true; enabled_on_any_profile_ = true; } if (!watchdog_extension_active_) { watchdog_extension_active_ = true; profile_->GetPrefs()->SetBoolean(prefs::kWatchdogExtensionActive, true); } ChooseDefaultPolicy(); } void ActivityLog::OnExtensionUnloaded(const Extension* extension) { if (extension->id() != kActivityLogExtensionId) return; // Make sure we are not enabled when there are no threads. DCHECK(has_threads_ || !enabled_); if (!CommandLine::ForCurrentProcess()->HasSwitch( switches::kEnableExtensionActivityLogging)) enabled_ = false; if (watchdog_extension_active_) { watchdog_extension_active_ = false; profile_->GetPrefs()->SetBoolean(prefs::kWatchdogExtensionActive, false); } } // static ActivityLog* ActivityLog::GetInstance(Profile* profile) { return ActivityLogFactory::GetForProfile(profile); } void ActivityLog::AddObserver(ActivityLog::Observer* observer) { observers_->AddObserver(observer); } void ActivityLog::RemoveObserver(ActivityLog::Observer* observer) { observers_->RemoveObserver(observer); } void ActivityLog::LogAction(scoped_refptr action) { if (!IsLogEnabled() || ActivityLogAPI::IsExtensionWhitelisted(action->extension_id())) return; // Perform some preprocessing of the Action data: convert tab IDs to URLs and // mask out incognito URLs if appropriate. if ((action->action_type() == Action::ACTION_API_CALL || action->action_type() == Action::ACTION_API_EVENT) && StartsWithASCII(action->api_name(), "tabs.", true)) { LookupTabIds(action, profile_); } // TODO(mvrable): Add any necessary processing of incognito URLs here, for // crbug.com/253368 if (policy_) policy_->ProcessAction(action); observers_->Notify(&Observer::OnExtensionActivity, action); if (testing_mode_) LOG(INFO) << action->PrintForDebug(); } void ActivityLog::GetActions( const std::string& extension_id, const int day, const base::Callback > >)>& callback) { if (policy_) { policy_->ReadData(extension_id, day, callback); } } void ActivityLog::OnScriptsExecuted( const content::WebContents* web_contents, const ExecutingScriptsMap& extension_ids, int32 on_page_id, const GURL& on_url) { if (!IsLogEnabled()) return; Profile* profile = Profile::FromBrowserContext(web_contents->GetBrowserContext()); const ExtensionService* extension_service = ExtensionSystem::Get(profile)->extension_service(); const ExtensionSet* extensions = extension_service->extensions(); const prerender::PrerenderManager* prerender_manager = prerender::PrerenderManagerFactory::GetForProfile( Profile::FromBrowserContext(web_contents->GetBrowserContext())); for (ExecutingScriptsMap::const_iterator it = extension_ids.begin(); it != extension_ids.end(); ++it) { const Extension* extension = extensions->GetByID(it->first); 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 = 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()); if (prerender_manager && prerender_manager->IsWebContentsPrerendering(web_contents, NULL)) action->mutable_other()->SetBoolean(constants::kActionPrerender, true); for (std::set::const_iterator it2 = it->second.begin(); it2 != it->second.end(); ++it2) { action->mutable_args()->AppendString(*it2); } LogAction(action); } } } // static void ActivityLog::RegisterProfilePrefs( user_prefs::PrefRegistrySyncable* registry) { registry->RegisterBooleanPref( prefs::kWatchdogExtensionActive, false, user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF); } } // namespace extensions