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