counting_policy.cc revision 58537e28ecd584eab876aee8be7156509866d23a
1// Copyright 2013 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.
4
5// A policy for storing activity log data to a database that performs
6// aggregation to reduce the size of the database.  The database layout is
7// nearly the same as FullStreamUIPolicy, which stores a complete log, with a
8// few changes:
9//   - a "count" column is added to track how many log records were merged
10//     together into this row
11//   - the "time" column measures the most recent time that the current row was
12//     updated
13// When writing a record, if a row already exists where all other columns
14// (extension_id, action_type, api_name, args, urls, etc.) all match, and the
15// previous time falls within today (the current time), then the count field on
16// the old row is incremented.  Otherwise, a new row is written.
17//
18// For many text columns, repeated strings are compressed by moving string
19// storage to a separate table ("string_ids") and storing only an identifier in
20// the logging table.  For example, if the api_name_x column contained the
21// value 4 and the string_ids table contained a row with primary key 4 and
22// value 'tabs.query', then the api_name field should be taken to have the
23// value 'tabs.query'.  Each column ending with "_x" is compressed in this way.
24// All lookups are to the string_ids table, except for the page_url_x and
25// arg_url_x columns, which are converted via the url_ids table (this
26// separation of URL values is to help simplify history clearing).
27//
28// The activitylog_uncompressed view allows for simpler reading of the activity
29// log contents with identifiers already translated to string values.
30
31#include "chrome/browser/extensions/activity_log/counting_policy.h"
32
33#include <map>
34#include <string>
35#include <vector>
36
37#include "base/callback.h"
38#include "base/files/file_path.h"
39#include "base/json/json_reader.h"
40#include "base/json/json_string_value_serializer.h"
41#include "base/strings/string_util.h"
42#include "base/strings/stringprintf.h"
43#include "chrome/common/chrome_constants.h"
44
45using content::BrowserThread;
46
47namespace {
48
49using extensions::Action;
50
51// Delay between cleaning passes (to delete old action records) through the
52// database.
53const int kCleaningDelayInHours = 12;
54
55// We should log the arguments to these API calls.  Be careful when
56// constructing this whitelist to not keep arguments that might compromise
57// privacy by logging too much data to the activity log.
58//
59// TODO(mvrable): The contents of this whitelist should be reviewed and
60// expanded as needed.
61struct ApiList {
62  Action::ActionType type;
63  const char* name;
64};
65
66const ApiList kAlwaysLog[] = {
67    {Action::ACTION_API_CALL, "extension.connect"},
68    {Action::ACTION_API_CALL, "extension.sendMessage"},
69    {Action::ACTION_API_CALL, "tabs.executeScript"},
70    {Action::ACTION_API_CALL, "tabs.insertCSS"},
71    {Action::ACTION_CONTENT_SCRIPT, ""},
72    {Action::ACTION_DOM_ACCESS, "XMLHttpRequest.open"},
73};
74
75// Columns in the main database table.  See the file-level comment for a
76// discussion of how data is stored and the meanings of the _x columns.
77const char* kTableContentFields[] = {
78    "count", "extension_id_x", "time", "action_type", "api_name_x", "args_x",
79    "page_url_x", "page_title_x", "arg_url_x", "other_x"};
80const char* kTableFieldTypes[] = {
81    "INTEGER NOT NULL DEFAULT 1", "INTEGER NOT NULL", "INTEGER", "INTEGER",
82    "INTEGER", "INTEGER", "INTEGER", "INTEGER", "INTEGER",
83    "INTEGER"};
84
85// Miscellaneous SQL commands for initializing the database; these should be
86// idempotent.
87static const char kPolicyMiscSetup[] =
88    // The activitylog_uncompressed view performs string lookups for simpler
89    // access to the log data.
90    "DROP VIEW IF EXISTS activitylog_uncompressed;\n"
91    "CREATE VIEW activitylog_uncompressed AS\n"
92    "SELECT count,\n"
93    "    x1.value AS extension_id,\n"
94    "    time,\n"
95    "    action_type,\n"
96    "    x2.value AS api_name,\n"
97    "    x3.value AS args,\n"
98    "    x4.value AS page_url,\n"
99    "    x5.value AS page_title,\n"
100    "    x6.value AS arg_url,\n"
101    "    x7.value AS other\n"
102    "FROM activitylog_compressed\n"
103    "    LEFT JOIN string_ids AS x1 ON (x1.id = extension_id_x)\n"
104    "    LEFT JOIN string_ids AS x2 ON (x2.id = api_name_x)\n"
105    "    LEFT JOIN string_ids AS x3 ON (x3.id = args_x)\n"
106    "    LEFT JOIN url_ids    AS x4 ON (x4.id = page_url_x)\n"
107    "    LEFT JOIN string_ids AS x5 ON (x5.id = page_title_x)\n"
108    "    LEFT JOIN url_ids    AS x6 ON (x6.id = arg_url_x)\n"
109    "    LEFT JOIN string_ids AS x7 ON (x7.id = other_x);\n"
110    // An index on all fields except count and time: all the fields that aren't
111    // changed when incrementing a count.  This should accelerate finding the
112    // rows to update (at worst several rows will need to be checked to find
113    // the one in the right time range).
114    "CREATE INDEX IF NOT EXISTS activitylog_compressed_index\n"
115    "ON activitylog_compressed(extension_id_x, action_type, api_name_x,\n"
116    "    args_x, page_url_x, page_title_x, arg_url_x, other_x)";
117
118// SQL statements to clean old, unused entries out of the string and URL id
119// tables.
120static const char kStringTableCleanup[] =
121    "DELETE FROM string_ids WHERE id NOT IN\n"
122    "(SELECT extension_id_x FROM activitylog_compressed\n"
123    "    WHERE extension_id_x IS NOT NULL\n"
124    " UNION SELECT api_name_x FROM activitylog_compressed\n"
125    "    WHERE api_name_x IS NOT NULL\n"
126    " UNION SELECT args_x FROM activitylog_compressed\n"
127    "    WHERE args_x IS NOT NULL\n"
128    " UNION SELECT page_title_x FROM activitylog_compressed\n"
129    "    WHERE page_title_x IS NOT NULL\n"
130    " UNION SELECT other_x FROM activitylog_compressed\n"
131    "    WHERE other_x IS NOT NULL)";
132static const char kUrlTableCleanup[] =
133    "DELETE FROM url_ids WHERE id NOT IN\n"
134    "(SELECT page_url_x FROM activitylog_compressed\n"
135    "    WHERE page_url_x IS NOT NULL\n"
136    " UNION SELECT arg_url_x FROM activitylog_compressed\n"
137    "    WHERE arg_url_x IS NOT NULL)";
138
139}  // namespace
140
141namespace extensions {
142
143const char* CountingPolicy::kTableName = "activitylog_compressed";
144const char* CountingPolicy::kReadViewName = "activitylog_uncompressed";
145
146CountingPolicy::CountingPolicy(Profile* profile)
147    : ActivityLogDatabasePolicy(
148          profile,
149          base::FilePath(chrome::kExtensionActivityLogFilename)),
150      string_table_("string_ids"),
151      url_table_("url_ids"),
152      retention_time_(base::TimeDelta::FromHours(60)) {
153  for (size_t i = 0; i < arraysize(kAlwaysLog); i++) {
154    api_arg_whitelist_.insert(
155        std::make_pair(kAlwaysLog[i].type, kAlwaysLog[i].name));
156  }
157}
158
159CountingPolicy::~CountingPolicy() {}
160
161bool CountingPolicy::InitDatabase(sql::Connection* db) {
162  if (!Util::DropObsoleteTables(db))
163    return false;
164
165  if (!string_table_.Initialize(db))
166    return false;
167  if (!url_table_.Initialize(db))
168    return false;
169
170  // Create the unified activity log entry table.
171  if (!ActivityDatabase::InitializeTable(db,
172                                         kTableName,
173                                         kTableContentFields,
174                                         kTableFieldTypes,
175                                         arraysize(kTableContentFields)))
176    return false;
177
178  // Create a view for easily accessing the uncompressed form of the data, and
179  // any necessary indexes if needed.
180  return db->Execute(kPolicyMiscSetup);
181}
182
183void CountingPolicy::ProcessAction(scoped_refptr<Action> action) {
184  ScheduleAndForget(this, &CountingPolicy::QueueAction, action);
185}
186
187void CountingPolicy::QueueAction(scoped_refptr<Action> action) {
188  if (activity_database()->is_db_valid()) {
189    action = action->Clone();
190    Util::StripPrivacySensitiveFields(action);
191    Util::StripArguments(api_arg_whitelist_, action);
192
193    // If the current action falls on a different date than the ones in the
194    // queue, flush the queue out now to prevent any false merging (actions
195    // from different days being merged).
196    base::Time new_date = action->time().LocalMidnight();
197    if (new_date != queued_actions_date_)
198      activity_database()->AdviseFlush(ActivityDatabase::kFlushImmediately);
199    queued_actions_date_ = new_date;
200
201    ActionQueue::iterator queued_entry = queued_actions_.find(action);
202    if (queued_entry == queued_actions_.end()) {
203      queued_actions_[action] = 1;
204    } else {
205      // Update the timestamp in the key to be the latest time seen.  Modifying
206      // the time is safe since that field is not involved in key comparisons
207      // in the map.
208      using std::max;
209      queued_entry->first->set_time(
210          max(queued_entry->first->time(), action->time()));
211      queued_entry->second++;
212    }
213    activity_database()->AdviseFlush(queued_actions_.size());
214  }
215}
216
217bool CountingPolicy::FlushDatabase(sql::Connection* db) {
218  // Columns that must match exactly for database rows to be coalesced.
219  static const char* matched_columns[] = {
220      "extension_id_x", "action_type", "api_name_x", "args_x", "page_url_x",
221      "page_title_x", "arg_url_x", "other_x"};
222  ActionQueue queue;
223  queue.swap(queued_actions_);
224
225  // Whether to clean old records out of the activity log database.  Do this
226  // much less frequently than database flushes since it is expensive, but
227  // always check on the first database flush (since there might be a large
228  // amount of data to clear).
229  bool clean_database = (last_database_cleaning_time_.is_null() ||
230                         Now() - last_database_cleaning_time_ >
231                             base::TimeDelta::FromHours(kCleaningDelayInHours));
232
233  if (queue.empty() && !clean_database)
234    return true;
235
236  sql::Transaction transaction(db);
237  if (!transaction.Begin())
238    return false;
239
240  // Adding an Action to the database is a two step process that depends on
241  // whether the count on an existing row can be incremented or a new row needs
242  // to be inserted.
243  //   1. Run the query in locate_str to search for a row which matches and can
244  //      have the count incremented.
245  //  2a. If found, increment the count using update_str and the rowid found in
246  //      step 1, or
247  //  2b. If not found, insert a new row using insert_str.
248  std::string locate_str =
249      "SELECT rowid FROM " + std::string(kTableName) +
250      " WHERE time >= ? AND time < ?";
251  std::string insert_str =
252      "INSERT INTO " + std::string(kTableName) + "(count, time";
253  std::string update_str =
254      "UPDATE " + std::string(kTableName) +
255      " SET count = count + ?, time = max(?, time)"
256      " WHERE rowid = ?";
257
258  for (size_t i = 0; i < arraysize(matched_columns); i++) {
259    locate_str = base::StringPrintf(
260        "%s AND %s IS ?", locate_str.c_str(), matched_columns[i]);
261    insert_str =
262        base::StringPrintf("%s, %s", insert_str.c_str(), matched_columns[i]);
263  }
264  insert_str += ") VALUES (?, ?";
265  for (size_t i = 0; i < arraysize(matched_columns); i++) {
266    insert_str += ", ?";
267  }
268  locate_str += " ORDER BY time DESC LIMIT 1";
269  insert_str += ")";
270
271  for (ActionQueue::iterator i = queue.begin(); i != queue.end(); ++i) {
272    const Action& action = *i->first;
273    int count = i->second;
274
275    base::Time day_start = action.time().LocalMidnight();
276    base::Time next_day = Util::AddDays(day_start, 1);
277
278    // The contents in values must match up with fields in matched_columns.  A
279    // value of -1 is used to encode a null database value.
280    int64 id;
281    std::vector<int64> matched_values;
282
283    if (!string_table_.StringToInt(db, action.extension_id(), &id))
284      return false;
285    matched_values.push_back(id);
286
287    matched_values.push_back(static_cast<int>(action.action_type()));
288
289    if (!string_table_.StringToInt(db, action.api_name(), &id))
290      return false;
291    matched_values.push_back(id);
292
293    if (action.args()) {
294      std::string args = Util::Serialize(action.args());
295      // TODO(mvrable): For now, truncate long argument lists.  This is a
296      // workaround for excessively-long values coming from DOM logging.  When
297      // the V8ValueConverter is fixed to return more reasonable values, we can
298      // drop the truncation.
299      if (args.length() > 10000) {
300        args = "[\"<too_large>\"]";
301      }
302      if (!string_table_.StringToInt(db, args, &id))
303        return false;
304      matched_values.push_back(id);
305    } else {
306      matched_values.push_back(-1);
307    }
308
309    std::string page_url_string = action.SerializePageUrl();
310    if (!page_url_string.empty()) {
311      if (!url_table_.StringToInt(db, page_url_string, &id))
312        return false;
313      matched_values.push_back(id);
314    } else {
315      matched_values.push_back(-1);
316    }
317
318    // TODO(mvrable): Create a title_table_?
319    if (!action.page_title().empty()) {
320      if (!string_table_.StringToInt(db, action.page_title(), &id))
321        return false;
322      matched_values.push_back(id);
323    } else {
324      matched_values.push_back(-1);
325    }
326
327    std::string arg_url_string = action.SerializeArgUrl();
328    if (!arg_url_string.empty()) {
329      if (!url_table_.StringToInt(db, arg_url_string, &id))
330        return false;
331      matched_values.push_back(id);
332    } else {
333      matched_values.push_back(-1);
334    }
335
336    if (action.other()) {
337      if (!string_table_.StringToInt(db, Util::Serialize(action.other()), &id))
338        return false;
339      matched_values.push_back(id);
340    } else {
341      matched_values.push_back(-1);
342    }
343
344    // Search for a matching row for this action whose count can be
345    // incremented.
346    sql::Statement locate_statement(db->GetCachedStatement(
347        sql::StatementID(SQL_FROM_HERE), locate_str.c_str()));
348    locate_statement.BindInt64(0, day_start.ToInternalValue());
349    locate_statement.BindInt64(1, next_day.ToInternalValue());
350    for (size_t j = 0; j < matched_values.size(); j++) {
351      // A call to BindNull when matched_values contains -1 is likely not
352      // necessary as parameters default to null before they are explicitly
353      // bound.  But to be completely clear, and in case a cached statement
354      // ever comes with some values already bound, we bind all parameters
355      // (even null ones) explicitly.
356      if (matched_values[j] == -1)
357        locate_statement.BindNull(j + 2);
358      else
359        locate_statement.BindInt64(j + 2, matched_values[j]);
360    }
361
362    if (locate_statement.Step()) {
363      // A matching row was found.  Update the count and time.
364      int64 rowid = locate_statement.ColumnInt64(0);
365      sql::Statement update_statement(db->GetCachedStatement(
366          sql::StatementID(SQL_FROM_HERE), update_str.c_str()));
367      update_statement.BindInt(0, count);
368      update_statement.BindInt64(1, action.time().ToInternalValue());
369      update_statement.BindInt64(2, rowid);
370      if (!update_statement.Run())
371        return false;
372    } else if (locate_statement.Succeeded()) {
373      // No matching row was found, so we need to insert one.
374      sql::Statement insert_statement(db->GetCachedStatement(
375          sql::StatementID(SQL_FROM_HERE), insert_str.c_str()));
376      insert_statement.BindInt(0, count);
377      insert_statement.BindInt64(1, action.time().ToInternalValue());
378      for (size_t j = 0; j < matched_values.size(); j++) {
379        if (matched_values[j] == -1)
380          insert_statement.BindNull(j + 2);
381        else
382          insert_statement.BindInt64(j + 2, matched_values[j]);
383      }
384      if (!insert_statement.Run())
385        return false;
386    } else {
387      // Database error.
388      return false;
389    }
390  }
391
392  if (clean_database) {
393    base::Time cutoff = (Now() - retention_time()).LocalMidnight();
394    if (!CleanOlderThan(db, cutoff))
395      return false;
396    last_database_cleaning_time_ = Now();
397  }
398
399  if (!transaction.Commit())
400    return false;
401
402  return true;
403}
404
405scoped_ptr<Action::ActionVector> CountingPolicy::DoReadFilteredData(
406    const std::string& extension_id,
407    const Action::ActionType type,
408    const std::string& api_name,
409    const std::string& page_url,
410    const std::string& arg_url,
411    const int days_ago) {
412  // Ensure data is flushed to the database first so that we query over all
413  // data.
414  activity_database()->AdviseFlush(ActivityDatabase::kFlushImmediately);
415  scoped_ptr<Action::ActionVector> actions(new Action::ActionVector());
416
417  sql::Connection* db = GetDatabaseConnection();
418  if (!db)
419    return actions.Pass();
420
421  // Build up the query based on which parameters were specified.
422  std::string where_str = "";
423  std::string where_next = "";
424  if (!extension_id.empty()) {
425    where_str += "extension_id=?";
426    where_next = " AND ";
427  }
428  if (!api_name.empty()) {
429    where_str += where_next + "api_name=?";
430    where_next = " AND ";
431  }
432  if (type != Action::ACTION_ANY) {
433    where_str += where_next + "action_type=?";
434    where_next = " AND ";
435  }
436  if (!page_url.empty()) {
437    where_str += where_next + "page_url LIKE ?";
438    where_next = " AND ";
439  }
440  if (!arg_url.empty()) {
441    where_str += where_next + "arg_url LIKE ?";
442    where_next = " AND ";
443  }
444  if (days_ago >= 0)
445    where_str += where_next + "time BETWEEN ? AND ?";
446
447  std::string query_str = base::StringPrintf(
448      "SELECT extension_id,time, action_type, api_name, args, page_url,"
449      "page_title, arg_url, other, count FROM %s %s %s ORDER BY count DESC,"
450      " time DESC LIMIT 300",
451      kReadViewName,
452      where_str.empty() ? "" : "WHERE",
453      where_str.c_str());
454  sql::Statement query(db->GetUniqueStatement(query_str.c_str()));
455  int i = -1;
456  if (!extension_id.empty())
457    query.BindString(++i, extension_id);
458  if (!api_name.empty())
459    query.BindString(++i, api_name);
460  if (type != Action::ACTION_ANY)
461    query.BindInt(++i, static_cast<int>(type));
462  if (!page_url.empty())
463    query.BindString(++i, page_url + "%");
464  if (!arg_url.empty())
465    query.BindString(++i, arg_url + "%");
466  if (days_ago >= 0) {
467    int64 early_bound;
468    int64 late_bound;
469    Util::ComputeDatabaseTimeBounds(Now(), days_ago, &early_bound, &late_bound);
470    query.BindInt64(++i, early_bound);
471    query.BindInt64(++i, late_bound);
472  }
473
474  // Execute the query and get results.
475  while (query.is_valid() && query.Step()) {
476    scoped_refptr<Action> action =
477        new Action(query.ColumnString(0),
478                   base::Time::FromInternalValue(query.ColumnInt64(1)),
479                   static_cast<Action::ActionType>(query.ColumnInt(2)),
480                   query.ColumnString(3));
481
482    if (query.ColumnType(4) != sql::COLUMN_TYPE_NULL) {
483      scoped_ptr<Value> parsed_value(
484          base::JSONReader::Read(query.ColumnString(4)));
485      if (parsed_value && parsed_value->IsType(Value::TYPE_LIST)) {
486        action->set_args(
487            make_scoped_ptr(static_cast<ListValue*>(parsed_value.release())));
488      }
489    }
490
491    action->ParsePageUrl(query.ColumnString(5));
492    action->set_page_title(query.ColumnString(6));
493    action->ParseArgUrl(query.ColumnString(7));
494
495    if (query.ColumnType(8) != sql::COLUMN_TYPE_NULL) {
496      scoped_ptr<Value> parsed_value(
497          base::JSONReader::Read(query.ColumnString(8)));
498      if (parsed_value && parsed_value->IsType(Value::TYPE_DICTIONARY)) {
499        action->set_other(make_scoped_ptr(
500            static_cast<DictionaryValue*>(parsed_value.release())));
501      }
502    }
503    action->set_count(query.ColumnInt(9));
504    actions->push_back(action);
505  }
506
507  return actions.Pass();
508}
509
510void CountingPolicy::DoRemoveURLs(const std::vector<GURL>& restrict_urls) {
511  sql::Connection* db = GetDatabaseConnection();
512  if (!db) {
513    LOG(ERROR) << "Unable to connect to database";
514    return;
515  }
516
517  // Flush data first so the URL clearing affects queued-up data as well.
518  activity_database()->AdviseFlush(ActivityDatabase::kFlushImmediately);
519
520  // If no restrictions then then all URLs need to be removed.
521  if (restrict_urls.empty()) {
522    std::string sql_str = base::StringPrintf(
523      "UPDATE %s SET page_url_x=NULL,page_title_x=NULL,arg_url_x=NULL",
524      kTableName);
525
526    sql::Statement statement;
527    statement.Assign(db->GetCachedStatement(
528        sql::StatementID(SQL_FROM_HERE), sql_str.c_str()));
529
530    if (!statement.Run()) {
531      LOG(ERROR) << "Removing all URLs from database failed: "
532                 << statement.GetSQLStatement();
533      return;
534    }
535  }
536
537  // If URLs are specified then restrict to only those URLs.
538  for (size_t i = 0; i < restrict_urls.size(); ++i) {
539    int64 url_id;
540    if (!restrict_urls[i].is_valid() ||
541        !url_table_.StringToInt(db, restrict_urls[i].spec(), &url_id)) {
542      continue;
543    }
544
545    // Remove any that match the page_url.
546    std::string sql_str = base::StringPrintf(
547      "UPDATE %s SET page_url_x=NULL,page_title_x=NULL WHERE page_url_x IS ?",
548      kTableName);
549
550    sql::Statement statement;
551    statement.Assign(db->GetCachedStatement(
552        sql::StatementID(SQL_FROM_HERE), sql_str.c_str()));
553    statement.BindInt64(0, url_id);
554
555    if (!statement.Run()) {
556      LOG(ERROR) << "Removing page URL from database failed: "
557                 << statement.GetSQLStatement();
558      return;
559    }
560
561    // Remove any that match the arg_url.
562    sql_str = base::StringPrintf(
563      "UPDATE %s SET arg_url_x=NULL WHERE arg_url_x IS ?", kTableName);
564
565    statement.Assign(db->GetCachedStatement(
566        sql::StatementID(SQL_FROM_HERE), sql_str.c_str()));
567    statement.BindInt64(0, url_id);
568
569    if (!statement.Run()) {
570      LOG(ERROR) << "Removing arg URL from database failed: "
571                 << statement.GetSQLStatement();
572      return;
573    }
574  }
575
576  // Clean up unused strings from the strings and urls table to really delete
577  // the urls and page titles. Should be called even if an error occured when
578  // removing a URL as there may some things to clean up.
579  CleanStringTables(db);
580}
581
582void CountingPolicy::DoDeleteDatabase() {
583  sql::Connection* db = GetDatabaseConnection();
584  if (!db) {
585    LOG(ERROR) << "Unable to connect to database";
586    return;
587  }
588
589  queued_actions_.clear();
590
591  // Not wrapped in a transaction because a late failure shouldn't undo a
592  // previous deletion.
593  std::string sql_str = base::StringPrintf("DELETE FROM %s", kTableName);
594  sql::Statement statement(db->GetCachedStatement(
595      sql::StatementID(SQL_FROM_HERE),
596      sql_str.c_str()));
597  if (!statement.Run()) {
598    LOG(ERROR) << "Deleting the database failed: "
599               << statement.GetSQLStatement();
600    return;
601  }
602  statement.Clear();
603  statement.Assign(db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE),
604                                          "DELETE FROM string_ids"));
605  if (!statement.Run()) {
606    LOG(ERROR) << "Deleting the database failed: "
607               << statement.GetSQLStatement();
608    return;
609  }
610  statement.Clear();
611  statement.Assign(db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE),
612                                          "DELETE FROM url_ids"));
613  if (!statement.Run()) {
614    LOG(ERROR) << "Deleting the database failed: "
615               << statement.GetSQLStatement();
616    return;
617  }
618  statement.Clear();
619  statement.Assign(db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE),
620                                          "VACUUM"));
621  if (!statement.Run()) {
622    LOG(ERROR) << "Vacuuming the database failed: "
623               << statement.GetSQLStatement();
624  }
625}
626
627void CountingPolicy::ReadFilteredData(
628    const std::string& extension_id,
629    const Action::ActionType type,
630    const std::string& api_name,
631    const std::string& page_url,
632    const std::string& arg_url,
633    const int days_ago,
634    const base::Callback
635        <void(scoped_ptr<Action::ActionVector>)>& callback) {
636  BrowserThread::PostTaskAndReplyWithResult(
637      BrowserThread::DB,
638      FROM_HERE,
639      base::Bind(&CountingPolicy::DoReadFilteredData,
640                 base::Unretained(this),
641                 extension_id,
642                 type,
643                 api_name,
644                 page_url,
645                 arg_url,
646                 days_ago),
647      callback);
648}
649
650void CountingPolicy::RemoveURLs(const std::vector<GURL>& restrict_urls) {
651  ScheduleAndForget(this, &CountingPolicy::DoRemoveURLs, restrict_urls);
652}
653
654void CountingPolicy::DeleteDatabase() {
655  ScheduleAndForget(this, &CountingPolicy::DoDeleteDatabase);
656}
657
658void CountingPolicy::OnDatabaseFailure() {
659  queued_actions_.clear();
660}
661
662void CountingPolicy::OnDatabaseClose() {
663  delete this;
664}
665
666// Cleans old records from the activity log database.
667bool CountingPolicy::CleanOlderThan(sql::Connection* db,
668                                    const base::Time& cutoff) {
669  std::string clean_statement =
670      "DELETE FROM " + std::string(kTableName) + " WHERE time < ?";
671  sql::Statement cleaner(db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE),
672                                                clean_statement.c_str()));
673  cleaner.BindInt64(0, cutoff.ToInternalValue());
674  if (!cleaner.Run())
675    return false;
676  return CleanStringTables(db);
677}
678
679// Cleans unused interned strings from the database.  This should be run after
680// deleting rows from the main log table to clean out stale values.
681bool CountingPolicy::CleanStringTables(sql::Connection* db) {
682  sql::Statement cleaner1(db->GetCachedStatement(
683      sql::StatementID(SQL_FROM_HERE), kStringTableCleanup));
684  if (!cleaner1.Run())
685    return false;
686  if (db->GetLastChangeCount() > 0)
687    string_table_.ClearCache();
688
689  sql::Statement cleaner2(db->GetCachedStatement(
690      sql::StatementID(SQL_FROM_HERE), kUrlTableCleanup));
691  if (!cleaner2.Run())
692    return false;
693  if (db->GetLastChangeCount() > 0)
694    url_table_.ClearCache();
695
696  return true;
697}
698
699void CountingPolicy::Close() {
700  // The policy object should have never been created if there's no DB thread.
701  DCHECK(BrowserThread::IsMessageLoopValid(BrowserThread::DB));
702  ScheduleAndForget(activity_database(), &ActivityDatabase::Close);
703}
704
705}  // namespace extensions
706