| // Copyright 2013 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. |
| |
| // A policy for storing activity log data to a database that performs |
| // aggregation to reduce the size of the database. The database layout is |
| // nearly the same as FullStreamUIPolicy, which stores a complete log, with a |
| // few changes: |
| // - a "count" column is added to track how many log records were merged |
| // together into this row |
| // - the "time" column measures the most recent time that the current row was |
| // updated |
| // When writing a record, if a row already exists where all other columns |
| // (extension_id, action_type, api_name, args, urls, etc.) all match, and the |
| // previous time falls within today (the current time), then the count field on |
| // the old row is incremented. Otherwise, a new row is written. |
| // |
| // For many text columns, repeated strings are compressed by moving string |
| // storage to a separate table ("string_ids") and storing only an identifier in |
| // the logging table. For example, if the api_name_x column contained the |
| // value 4 and the string_ids table contained a row with primary key 4 and |
| // value 'tabs.query', then the api_name field should be taken to have the |
| // value 'tabs.query'. Each column ending with "_x" is compressed in this way. |
| // All lookups are to the string_ids table, except for the page_url_x and |
| // arg_url_x columns, which are converted via the url_ids table (this |
| // separation of URL values is to help simplify history clearing). |
| // |
| // The activitylog_uncompressed view allows for simpler reading of the activity |
| // log contents with identifiers already translated to string values. |
| |
| #include "chrome/browser/extensions/activity_log/counting_policy.h" |
| |
| #include <stddef.h> |
| |
| #include <map> |
| #include <string> |
| #include <vector> |
| |
| #include "base/callback.h" |
| #include "base/files/file_path.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_string_value_serializer.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task_runner_util.h" |
| #include "chrome/browser/extensions/activity_log/activity_log_task_runner.h" |
| #include "chrome/common/chrome_constants.h" |
| #include "sql/statement.h" |
| #include "sql/transaction.h" |
| |
| namespace { |
| |
| using extensions::Action; |
| |
| // Delay between cleaning passes (to delete old action records) through the |
| // database. |
| constexpr base::TimeDelta kCleaningDelay = base::TimeDelta::FromHours(12); |
| |
| // We should log the arguments to these API calls. Be careful when |
| // constructing this whitelist to not keep arguments that might compromise |
| // privacy by logging too much data to the activity log. |
| // |
| // TODO(mvrable): The contents of this whitelist should be reviewed and |
| // expanded as needed. |
| struct ApiList { |
| Action::ActionType type; |
| const char* name; |
| }; |
| |
| const ApiList kAlwaysLog[] = { |
| {Action::ACTION_API_CALL, "bookmarks.create"}, |
| {Action::ACTION_API_CALL, "bookmarks.update"}, |
| {Action::ACTION_API_CALL, "cookies.get"}, |
| {Action::ACTION_API_CALL, "cookies.getAll"}, |
| {Action::ACTION_API_CALL, "extension.connect"}, |
| {Action::ACTION_API_CALL, "extension.sendMessage"}, |
| {Action::ACTION_API_CALL, "fileSystem.chooseEntry"}, |
| {Action::ACTION_API_CALL, "socket.bind"}, |
| {Action::ACTION_API_CALL, "socket.connect"}, |
| {Action::ACTION_API_CALL, "socket.create"}, |
| {Action::ACTION_API_CALL, "socket.listen"}, |
| {Action::ACTION_API_CALL, "tabs.executeScript"}, |
| {Action::ACTION_API_CALL, "tabs.insertCSS"}, |
| {Action::ACTION_API_CALL, "types.ChromeSetting.clear"}, |
| {Action::ACTION_API_CALL, "types.ChromeSetting.get"}, |
| {Action::ACTION_API_CALL, "types.ChromeSetting.set"}, |
| {Action::ACTION_CONTENT_SCRIPT, ""}, |
| {Action::ACTION_DOM_ACCESS, "Document.createElement"}, |
| {Action::ACTION_DOM_ACCESS, "Document.createElementNS"}, |
| }; |
| |
| // Columns in the main database table. See the file-level comment for a |
| // discussion of how data is stored and the meanings of the _x columns. |
| const char* const kTableContentFields[] = { |
| "count", "extension_id_x", "time", "action_type", "api_name_x", "args_x", |
| "page_url_x", "page_title_x", "arg_url_x", "other_x"}; |
| const char* const kTableFieldTypes[] = { |
| "INTEGER NOT NULL DEFAULT 1", "INTEGER NOT NULL", "INTEGER", "INTEGER", |
| "INTEGER", "INTEGER", "INTEGER", "INTEGER", "INTEGER", |
| "INTEGER"}; |
| |
| // Miscellaneous SQL commands for initializing the database; these should be |
| // idempotent. |
| static const char kPolicyMiscSetup[] = |
| // The activitylog_uncompressed view performs string lookups for simpler |
| // access to the log data. |
| "DROP VIEW IF EXISTS activitylog_uncompressed;\n" |
| "CREATE VIEW activitylog_uncompressed AS\n" |
| "SELECT count,\n" |
| " x1.value AS extension_id,\n" |
| " time,\n" |
| " action_type,\n" |
| " x2.value AS api_name,\n" |
| " x3.value AS args,\n" |
| " x4.value AS page_url,\n" |
| " x5.value AS page_title,\n" |
| " x6.value AS arg_url,\n" |
| " x7.value AS other,\n" |
| " activitylog_compressed.rowid AS activity_id\n" |
| "FROM activitylog_compressed\n" |
| " LEFT JOIN string_ids AS x1 ON (x1.id = extension_id_x)\n" |
| " LEFT JOIN string_ids AS x2 ON (x2.id = api_name_x)\n" |
| " LEFT JOIN string_ids AS x3 ON (x3.id = args_x)\n" |
| " LEFT JOIN url_ids AS x4 ON (x4.id = page_url_x)\n" |
| " LEFT JOIN string_ids AS x5 ON (x5.id = page_title_x)\n" |
| " LEFT JOIN url_ids AS x6 ON (x6.id = arg_url_x)\n" |
| " LEFT JOIN string_ids AS x7 ON (x7.id = other_x);\n" |
| // An index on all fields except count and time: all the fields that aren't |
| // changed when incrementing a count. This should accelerate finding the |
| // rows to update (at worst several rows will need to be checked to find |
| // the one in the right time range). |
| "CREATE INDEX IF NOT EXISTS activitylog_compressed_index\n" |
| "ON activitylog_compressed(extension_id_x, action_type, api_name_x,\n" |
| " args_x, page_url_x, page_title_x, arg_url_x, other_x)"; |
| |
| // SQL statements to clean old, unused entries out of the string and URL id |
| // tables. |
| static const char kStringTableCleanup[] = |
| "DELETE FROM string_ids WHERE id NOT IN\n" |
| "(SELECT extension_id_x FROM activitylog_compressed\n" |
| " WHERE extension_id_x IS NOT NULL\n" |
| " UNION SELECT api_name_x FROM activitylog_compressed\n" |
| " WHERE api_name_x IS NOT NULL\n" |
| " UNION SELECT args_x FROM activitylog_compressed\n" |
| " WHERE args_x IS NOT NULL\n" |
| " UNION SELECT page_title_x FROM activitylog_compressed\n" |
| " WHERE page_title_x IS NOT NULL\n" |
| " UNION SELECT other_x FROM activitylog_compressed\n" |
| " WHERE other_x IS NOT NULL)"; |
| static const char kUrlTableCleanup[] = |
| "DELETE FROM url_ids WHERE id NOT IN\n" |
| "(SELECT page_url_x FROM activitylog_compressed\n" |
| " WHERE page_url_x IS NOT NULL\n" |
| " UNION SELECT arg_url_x FROM activitylog_compressed\n" |
| " WHERE arg_url_x IS NOT NULL)"; |
| |
| } // namespace |
| |
| namespace extensions { |
| |
| const char CountingPolicy::kTableName[] = "activitylog_compressed"; |
| const char CountingPolicy::kReadViewName[] = "activitylog_uncompressed"; |
| |
| CountingPolicy::CountingPolicy(Profile* profile) |
| : ActivityLogDatabasePolicy( |
| profile, |
| base::FilePath(chrome::kExtensionActivityLogFilename)), |
| string_table_("string_ids"), |
| url_table_("url_ids"), |
| retention_time_(base::TimeDelta::FromHours(60)) { |
| for (size_t i = 0; i < arraysize(kAlwaysLog); i++) { |
| api_arg_whitelist_.insert( |
| std::make_pair(kAlwaysLog[i].type, kAlwaysLog[i].name)); |
| } |
| } |
| |
| CountingPolicy::~CountingPolicy() {} |
| |
| bool CountingPolicy::InitDatabase(sql::Database* db) { |
| if (!string_table_.Initialize(db)) |
| return false; |
| if (!url_table_.Initialize(db)) |
| return false; |
| |
| // Create the unified activity log entry table. |
| if (!ActivityDatabase::InitializeTable(db, |
| kTableName, |
| kTableContentFields, |
| kTableFieldTypes, |
| arraysize(kTableContentFields))) |
| return false; |
| |
| // Create a view for easily accessing the uncompressed form of the data, and |
| // any necessary indexes if needed. |
| return db->Execute(kPolicyMiscSetup); |
| } |
| |
| void CountingPolicy::ProcessAction(scoped_refptr<Action> action) { |
| ScheduleAndForget(this, &CountingPolicy::QueueAction, action); |
| } |
| |
| void CountingPolicy::QueueAction(scoped_refptr<Action> action) { |
| if (activity_database()->is_db_valid()) { |
| action = action->Clone(); |
| Util::StripPrivacySensitiveFields(action); |
| Util::StripArguments(api_arg_whitelist_, action); |
| |
| // If the current action falls on a different date than the ones in the |
| // queue, flush the queue out now to prevent any false merging (actions |
| // from different days being merged). |
| base::Time new_date = action->time().LocalMidnight(); |
| if (new_date != queued_actions_date_) |
| activity_database()->AdviseFlush(ActivityDatabase::kFlushImmediately); |
| queued_actions_date_ = new_date; |
| |
| auto queued_entry = queued_actions_.find(action); |
| if (queued_entry == queued_actions_.end()) { |
| queued_actions_[action] = 1; |
| } else { |
| // Update the timestamp in the key to be the latest time seen. Modifying |
| // the time is safe since that field is not involved in key comparisons |
| // in the map. |
| using std::max; |
| queued_entry->first->set_time( |
| max(queued_entry->first->time(), action->time())); |
| queued_entry->second++; |
| } |
| activity_database()->AdviseFlush(queued_actions_.size()); |
| } |
| } |
| |
| bool CountingPolicy::FlushDatabase(sql::Database* db) { |
| // Columns that must match exactly for database rows to be coalesced. |
| static const char* const matched_columns[] = { |
| "extension_id_x", "action_type", "api_name_x", "args_x", "page_url_x", |
| "page_title_x", "arg_url_x", "other_x"}; |
| ActionQueue queue; |
| queue.swap(queued_actions_); |
| |
| // Whether to clean old records out of the activity log database. Do this |
| // much less frequently than database flushes since it is expensive, but |
| // always check on the first database flush (since there might be a large |
| // amount of data to clear). |
| bool clean_database = (last_database_cleaning_time_.is_null() || |
| Now() - last_database_cleaning_time_ > kCleaningDelay); |
| |
| if (queue.empty() && !clean_database) |
| return true; |
| |
| sql::Transaction transaction(db); |
| if (!transaction.Begin()) |
| return false; |
| |
| // Adding an Action to the database is a two step process that depends on |
| // whether the count on an existing row can be incremented or a new row needs |
| // to be inserted. |
| // 1. Run the query in locate_str to search for a row which matches and can |
| // have the count incremented. |
| // 2a. If found, increment the count using update_str and the rowid found in |
| // step 1, or |
| // 2b. If not found, insert a new row using insert_str. |
| std::string locate_str = |
| "SELECT rowid FROM " + std::string(kTableName) + |
| " WHERE time >= ? AND time < ?"; |
| std::string insert_str = |
| "INSERT INTO " + std::string(kTableName) + "(count, time"; |
| std::string update_str = |
| "UPDATE " + std::string(kTableName) + |
| " SET count = count + ?, time = max(?, time)" |
| " WHERE rowid = ?"; |
| |
| for (size_t i = 0; i < arraysize(matched_columns); i++) { |
| locate_str = base::StringPrintf( |
| "%s AND %s IS ?", locate_str.c_str(), matched_columns[i]); |
| insert_str = |
| base::StringPrintf("%s, %s", insert_str.c_str(), matched_columns[i]); |
| } |
| insert_str += ") VALUES (?, ?"; |
| for (size_t i = 0; i < arraysize(matched_columns); i++) { |
| insert_str += ", ?"; |
| } |
| locate_str += " ORDER BY time DESC LIMIT 1"; |
| insert_str += ")"; |
| |
| for (auto i = queue.begin(); i != queue.end(); ++i) { |
| const Action& action = *i->first; |
| int count = i->second; |
| |
| base::Time day_start = action.time().LocalMidnight(); |
| base::Time next_day = Util::AddDays(day_start, 1); |
| |
| // The contents in values must match up with fields in matched_columns. A |
| // value of -1 is used to encode a null database value. |
| int64_t id; |
| std::vector<int64_t> matched_values; |
| |
| if (!string_table_.StringToInt(db, action.extension_id(), &id)) |
| return false; |
| matched_values.push_back(id); |
| |
| matched_values.push_back(static_cast<int>(action.action_type())); |
| |
| if (!string_table_.StringToInt(db, action.api_name(), &id)) |
| return false; |
| matched_values.push_back(id); |
| |
| if (action.args()) { |
| std::string args = Util::Serialize(action.args()); |
| // TODO(mvrable): For now, truncate long argument lists. This is a |
| // workaround for excessively-long values coming from DOM logging. When |
| // the V8ValueConverter is fixed to return more reasonable values, we can |
| // drop the truncation. |
| if (args.length() > 10000) { |
| args = "[\"<too_large>\"]"; |
| } |
| if (!string_table_.StringToInt(db, args, &id)) |
| return false; |
| matched_values.push_back(id); |
| } else { |
| matched_values.push_back(-1); |
| } |
| |
| std::string page_url_string = action.SerializePageUrl(); |
| if (!page_url_string.empty()) { |
| if (!url_table_.StringToInt(db, page_url_string, &id)) |
| return false; |
| matched_values.push_back(id); |
| } else { |
| matched_values.push_back(-1); |
| } |
| |
| // TODO(mvrable): Create a title_table_? |
| if (!action.page_title().empty()) { |
| if (!string_table_.StringToInt(db, action.page_title(), &id)) |
| return false; |
| matched_values.push_back(id); |
| } else { |
| matched_values.push_back(-1); |
| } |
| |
| std::string arg_url_string = action.SerializeArgUrl(); |
| if (!arg_url_string.empty()) { |
| if (!url_table_.StringToInt(db, arg_url_string, &id)) |
| return false; |
| matched_values.push_back(id); |
| } else { |
| matched_values.push_back(-1); |
| } |
| |
| if (action.other()) { |
| if (!string_table_.StringToInt(db, Util::Serialize(action.other()), &id)) |
| return false; |
| matched_values.push_back(id); |
| } else { |
| matched_values.push_back(-1); |
| } |
| |
| // Search for a matching row for this action whose count can be |
| // incremented. |
| sql::Statement locate_statement(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), locate_str.c_str())); |
| locate_statement.BindInt64(0, day_start.ToInternalValue()); |
| locate_statement.BindInt64(1, next_day.ToInternalValue()); |
| for (size_t j = 0; j < matched_values.size(); j++) { |
| // A call to BindNull when matched_values contains -1 is likely not |
| // necessary as parameters default to null before they are explicitly |
| // bound. But to be completely clear, and in case a cached statement |
| // ever comes with some values already bound, we bind all parameters |
| // (even null ones) explicitly. |
| if (matched_values[j] == -1) |
| locate_statement.BindNull(j + 2); |
| else |
| locate_statement.BindInt64(j + 2, matched_values[j]); |
| } |
| |
| if (locate_statement.Step()) { |
| // A matching row was found. Update the count and time. |
| int64_t rowid = locate_statement.ColumnInt64(0); |
| sql::Statement update_statement(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), update_str.c_str())); |
| update_statement.BindInt(0, count); |
| update_statement.BindInt64(1, action.time().ToInternalValue()); |
| update_statement.BindInt64(2, rowid); |
| if (!update_statement.Run()) |
| return false; |
| } else if (locate_statement.Succeeded()) { |
| // No matching row was found, so we need to insert one. |
| sql::Statement insert_statement(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), insert_str.c_str())); |
| insert_statement.BindInt(0, count); |
| insert_statement.BindInt64(1, action.time().ToInternalValue()); |
| for (size_t j = 0; j < matched_values.size(); j++) { |
| if (matched_values[j] == -1) |
| insert_statement.BindNull(j + 2); |
| else |
| insert_statement.BindInt64(j + 2, matched_values[j]); |
| } |
| if (!insert_statement.Run()) |
| return false; |
| } else { |
| // Database error. |
| return false; |
| } |
| } |
| |
| if (clean_database) { |
| base::Time cutoff = (Now() - retention_time()).LocalMidnight(); |
| if (!CleanOlderThan(db, cutoff)) |
| return false; |
| last_database_cleaning_time_ = Now(); |
| } |
| |
| if (!transaction.Commit()) |
| return false; |
| |
| return true; |
| } |
| |
| std::unique_ptr<Action::ActionVector> CountingPolicy::DoReadFilteredData( |
| 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 days_ago) { |
| DCHECK(GetActivityLogTaskRunner()->RunsTasksInCurrentSequence()); |
| // Ensure data is flushed to the database first so that we query over all |
| // data. |
| activity_database()->AdviseFlush(ActivityDatabase::kFlushImmediately); |
| std::unique_ptr<Action::ActionVector> actions(new Action::ActionVector()); |
| |
| sql::Database* db = GetDatabaseConnection(); |
| if (!db) |
| return actions; |
| |
| // Build up the query based on which parameters were specified. |
| std::string where_str = ""; |
| std::string where_next = ""; |
| if (!extension_id.empty()) { |
| where_str += "extension_id=?"; |
| where_next = " AND "; |
| } |
| if (!api_name.empty()) { |
| where_str += where_next + "api_name LIKE ?"; |
| where_next = " AND "; |
| } |
| if (type != Action::ACTION_ANY) { |
| where_str += where_next + "action_type=?"; |
| where_next = " AND "; |
| } |
| if (!page_url.empty()) { |
| where_str += where_next + "page_url LIKE ?"; |
| where_next = " AND "; |
| } |
| if (!arg_url.empty()) { |
| where_str += where_next + "arg_url LIKE ?"; |
| where_next = " AND "; |
| } |
| if (days_ago >= 0) |
| where_str += where_next + "time BETWEEN ? AND ?"; |
| |
| std::string query_str = base::StringPrintf( |
| "SELECT extension_id,time, action_type, api_name, args, page_url," |
| "page_title, arg_url, other, count, activity_id FROM %s %s %s ORDER BY " |
| "count DESC, time DESC LIMIT 300", |
| kReadViewName, |
| where_str.empty() ? "" : "WHERE", |
| where_str.c_str()); |
| sql::Statement query(db->GetUniqueStatement(query_str.c_str())); |
| int i = -1; |
| if (!extension_id.empty()) |
| query.BindString(++i, extension_id); |
| if (!api_name.empty()) |
| query.BindString(++i, api_name + "%"); |
| if (type != Action::ACTION_ANY) |
| query.BindInt(++i, static_cast<int>(type)); |
| if (!page_url.empty()) |
| query.BindString(++i, page_url + "%"); |
| if (!arg_url.empty()) |
| query.BindString(++i, arg_url + "%"); |
| if (days_ago >= 0) { |
| int64_t early_bound; |
| int64_t late_bound; |
| Util::ComputeDatabaseTimeBounds(Now(), days_ago, &early_bound, &late_bound); |
| query.BindInt64(++i, early_bound); |
| query.BindInt64(++i, late_bound); |
| } |
| |
| // Execute the query and get results. |
| while (query.is_valid() && query.Step()) { |
| scoped_refptr<Action> action = |
| new Action(query.ColumnString(0), |
| base::Time::FromInternalValue(query.ColumnInt64(1)), |
| static_cast<Action::ActionType>(query.ColumnInt(2)), |
| query.ColumnString(3), query.ColumnInt64(10)); |
| |
| if (query.ColumnType(4) != sql::COLUMN_TYPE_NULL) { |
| std::unique_ptr<base::Value> parsed_value = |
| base::JSONReader::Read(query.ColumnString(4)); |
| if (parsed_value && parsed_value->is_list()) { |
| action->set_args(base::WrapUnique( |
| static_cast<base::ListValue*>(parsed_value.release()))); |
| } |
| } |
| |
| action->ParsePageUrl(query.ColumnString(5)); |
| action->set_page_title(query.ColumnString(6)); |
| action->ParseArgUrl(query.ColumnString(7)); |
| |
| if (query.ColumnType(8) != sql::COLUMN_TYPE_NULL) { |
| std::unique_ptr<base::Value> parsed_value = |
| base::JSONReader::Read(query.ColumnString(8)); |
| if (parsed_value && parsed_value->is_dict()) { |
| action->set_other(base::WrapUnique( |
| static_cast<base::DictionaryValue*>(parsed_value.release()))); |
| } |
| } |
| action->set_count(query.ColumnInt(9)); |
| actions->push_back(action); |
| } |
| |
| return actions; |
| } |
| |
| void CountingPolicy::DoRemoveActions(const std::vector<int64_t>& action_ids) { |
| if (action_ids.empty()) |
| return; |
| |
| sql::Database* db = GetDatabaseConnection(); |
| if (!db) { |
| LOG(ERROR) << "Unable to connect to database"; |
| return; |
| } |
| |
| // Flush data first so the activity removal affects queued-up data as well. |
| activity_database()->AdviseFlush(ActivityDatabase::kFlushImmediately); |
| |
| sql::Transaction transaction(db); |
| if (!transaction.Begin()) |
| return; |
| |
| std::string statement_str = |
| base::StringPrintf("DELETE FROM %s WHERE rowid = ?", kTableName); |
| sql::Statement statement(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), statement_str.c_str())); |
| for (size_t i = 0; i < action_ids.size(); i++) { |
| statement.Reset(true); |
| statement.BindInt64(0, action_ids[i]); |
| if (!statement.Run()) { |
| LOG(ERROR) << "Removing activities from database failed: " |
| << statement.GetSQLStatement(); |
| break; |
| } |
| } |
| |
| CleanStringTables(db); |
| |
| if (!transaction.Commit()) { |
| LOG(ERROR) << "Removing activities from database failed"; |
| } |
| } |
| |
| void CountingPolicy::DoRemoveURLs(const std::vector<GURL>& restrict_urls) { |
| sql::Database* db = GetDatabaseConnection(); |
| if (!db) { |
| LOG(ERROR) << "Unable to connect to database"; |
| return; |
| } |
| |
| // Flush data first so the URL clearing affects queued-up data as well. |
| activity_database()->AdviseFlush(ActivityDatabase::kFlushImmediately); |
| |
| // If no restrictions then then all URLs need to be removed. |
| if (restrict_urls.empty()) { |
| std::string sql_str = base::StringPrintf( |
| "UPDATE %s SET page_url_x=NULL,page_title_x=NULL,arg_url_x=NULL", |
| kTableName); |
| |
| sql::Statement statement; |
| statement.Assign(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), sql_str.c_str())); |
| |
| if (!statement.Run()) { |
| LOG(ERROR) << "Removing all URLs from database failed: " |
| << statement.GetSQLStatement(); |
| return; |
| } |
| } |
| |
| // If URLs are specified then restrict to only those URLs. |
| for (size_t i = 0; i < restrict_urls.size(); ++i) { |
| int64_t url_id; |
| if (!restrict_urls[i].is_valid() || |
| !url_table_.StringToInt(db, restrict_urls[i].spec(), &url_id)) { |
| continue; |
| } |
| |
| // Remove any that match the page_url. |
| std::string sql_str = base::StringPrintf( |
| "UPDATE %s SET page_url_x=NULL,page_title_x=NULL WHERE page_url_x IS ?", |
| kTableName); |
| |
| sql::Statement statement; |
| statement.Assign(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), sql_str.c_str())); |
| statement.BindInt64(0, url_id); |
| |
| if (!statement.Run()) { |
| LOG(ERROR) << "Removing page URL from database failed: " |
| << statement.GetSQLStatement(); |
| return; |
| } |
| |
| // Remove any that match the arg_url. |
| sql_str = base::StringPrintf( |
| "UPDATE %s SET arg_url_x=NULL WHERE arg_url_x IS ?", kTableName); |
| |
| statement.Assign(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), sql_str.c_str())); |
| statement.BindInt64(0, url_id); |
| |
| if (!statement.Run()) { |
| LOG(ERROR) << "Removing arg URL from database failed: " |
| << statement.GetSQLStatement(); |
| return; |
| } |
| } |
| |
| // Clean up unused strings from the strings and urls table to really delete |
| // the urls and page titles. Should be called even if an error occured when |
| // removing a URL as there may some things to clean up. |
| CleanStringTables(db); |
| } |
| |
| void CountingPolicy::DoRemoveExtensionData(const std::string& extension_id) { |
| if (extension_id.empty()) |
| return; |
| |
| sql::Database* db = GetDatabaseConnection(); |
| if (!db) { |
| LOG(ERROR) << "Unable to connect to database"; |
| return; |
| } |
| |
| // Make sure any queued in memory are sent to the database before cleaning. |
| activity_database()->AdviseFlush(ActivityDatabase::kFlushImmediately); |
| |
| std::string sql_str = base::StringPrintf( |
| "DELETE FROM %s WHERE extension_id_x=?", kTableName); |
| sql::Statement statement( |
| db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE), sql_str.c_str())); |
| int64_t id; |
| if (string_table_.StringToInt(db, extension_id, &id)) { |
| statement.BindInt64(0, id); |
| } else { |
| // If the string isn't listed, that means we never recorded anything about |
| // the extension so there's no deletion to do. |
| statement.Clear(); |
| return; |
| } |
| if (!statement.Run()) { |
| LOG(ERROR) << "Removing URLs for extension " |
| << extension_id << "from database failed: " |
| << statement.GetSQLStatement(); |
| } |
| CleanStringTables(db); |
| } |
| |
| void CountingPolicy::DoDeleteDatabase() { |
| sql::Database* db = GetDatabaseConnection(); |
| if (!db) { |
| LOG(ERROR) << "Unable to connect to database"; |
| return; |
| } |
| |
| queued_actions_.clear(); |
| |
| // Not wrapped in a transaction because a late failure shouldn't undo a |
| // previous deletion. |
| std::string sql_str = base::StringPrintf("DELETE FROM %s", kTableName); |
| sql::Statement statement(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), |
| sql_str.c_str())); |
| if (!statement.Run()) { |
| LOG(ERROR) << "Deleting the database failed: " |
| << statement.GetSQLStatement(); |
| return; |
| } |
| statement.Clear(); |
| string_table_.ClearCache(); |
| statement.Assign(db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE), |
| "DELETE FROM string_ids")); |
| if (!statement.Run()) { |
| LOG(ERROR) << "Deleting the database failed: " |
| << statement.GetSQLStatement(); |
| return; |
| } |
| statement.Clear(); |
| url_table_.ClearCache(); |
| statement.Assign(db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE), |
| "DELETE FROM url_ids")); |
| if (!statement.Run()) { |
| LOG(ERROR) << "Deleting the database failed: " |
| << statement.GetSQLStatement(); |
| return; |
| } |
| statement.Clear(); |
| statement.Assign(db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE), |
| "VACUUM")); |
| if (!statement.Run()) { |
| LOG(ERROR) << "Vacuuming the database failed: " |
| << statement.GetSQLStatement(); |
| } |
| } |
| |
| void CountingPolicy::ReadFilteredData( |
| 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 days_ago, |
| base::OnceCallback<void(std::unique_ptr<Action::ActionVector>)> callback) { |
| base::PostTaskAndReplyWithResult( |
| GetActivityLogTaskRunner().get(), FROM_HERE, |
| base::BindOnce(&CountingPolicy::DoReadFilteredData, |
| base::Unretained(this), extension_id, type, api_name, |
| page_url, arg_url, days_ago), |
| std::move(callback)); |
| } |
| |
| void CountingPolicy::RemoveActions(const std::vector<int64_t>& action_ids) { |
| ScheduleAndForget(this, &CountingPolicy::DoRemoveActions, action_ids); |
| } |
| |
| void CountingPolicy::RemoveURLs(const std::vector<GURL>& restrict_urls) { |
| ScheduleAndForget(this, &CountingPolicy::DoRemoveURLs, restrict_urls); |
| } |
| |
| void CountingPolicy::RemoveExtensionData(const std::string& extension_id) { |
| ScheduleAndForget(this, &CountingPolicy::DoRemoveExtensionData, extension_id); |
| } |
| |
| void CountingPolicy::DeleteDatabase() { |
| ScheduleAndForget(this, &CountingPolicy::DoDeleteDatabase); |
| } |
| |
| void CountingPolicy::OnDatabaseFailure() { |
| queued_actions_.clear(); |
| } |
| |
| void CountingPolicy::OnDatabaseClose() { |
| delete this; |
| } |
| |
| // Cleans old records from the activity log database. |
| bool CountingPolicy::CleanOlderThan(sql::Database* db, |
| const base::Time& cutoff) { |
| std::string clean_statement = |
| "DELETE FROM " + std::string(kTableName) + " WHERE time < ?"; |
| sql::Statement cleaner(db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE), |
| clean_statement.c_str())); |
| cleaner.BindInt64(0, cutoff.ToInternalValue()); |
| if (!cleaner.Run()) |
| return false; |
| return CleanStringTables(db); |
| } |
| |
| // Cleans unused interned strings from the database. This should be run after |
| // deleting rows from the main log table to clean out stale values. |
| bool CountingPolicy::CleanStringTables(sql::Database* db) { |
| sql::Statement cleaner1(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), kStringTableCleanup)); |
| if (!cleaner1.Run()) |
| return false; |
| if (db->GetLastChangeCount() > 0) |
| string_table_.ClearCache(); |
| |
| sql::Statement cleaner2(db->GetCachedStatement( |
| sql::StatementID(SQL_FROM_HERE), kUrlTableCleanup)); |
| if (!cleaner2.Run()) |
| return false; |
| if (db->GetLastChangeCount() > 0) |
| url_table_.ClearCache(); |
| |
| return true; |
| } |
| |
| void CountingPolicy::Close() { |
| ScheduleAndForget(activity_database(), &ActivityDatabase::Close); |
| } |
| |
| } // namespace extensions |