text: // 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.
  
  #include "chrome/browser/extensions/activity_log/fullstream_ui_policy.h"
  
  #include <stddef.h>
  
  #include "base/callback.h"
  #include "base/command_line.h"
  #include "base/files/file_path.h"
  #include "base/json/json_reader.h"
  #include "base/json/json_string_value_serializer.h"
  #include "base/logging.h"
  #include "base/macros.h"
  #include "base/memory/ptr_util.h"
  #include "base/strings/string16.h"
  #include "base/strings/stringprintf.h"
  #include "base/task_runner_util.h"
  #include "chrome/browser/extensions/activity_log/activity_action_constants.h"
  #include "chrome/browser/extensions/activity_log/activity_database.h"
  #include "chrome/browser/extensions/activity_log/activity_log_task_runner.h"
  #include "chrome/browser/profiles/profile.h"
  #include "chrome/common/chrome_constants.h"
  #include "chrome/common/chrome_switches.h"
  #include "extensions/common/dom_action_types.h"
  #include "extensions/common/extension.h"
  #include "sql/statement.h"
  #include "sql/transaction.h"
  #include "url/gurl.h"
  
  using base::FilePath;
  using base::Time;
  using base::Unretained;
  using content::BrowserThread;
  
  namespace constants = activity_log_constants;
  
  namespace extensions {
  
  const char FullStreamUIPolicy::kTableName[] = "activitylog_full";
  const char* const FullStreamUIPolicy::kTableContentFields[] = {
    "extension_id", "time", "action_type", "api_name", "args", "page_url",
    "page_title", "arg_url", "other"
  };
  const char* const FullStreamUIPolicy::kTableFieldTypes[] = {
    "LONGVARCHAR NOT NULL", "INTEGER", "INTEGER", "LONGVARCHAR", "LONGVARCHAR",
    "LONGVARCHAR", "LONGVARCHAR", "LONGVARCHAR", "LONGVARCHAR"
  };
  const int FullStreamUIPolicy::kTableFieldCount =
      arraysize(FullStreamUIPolicy::kTableContentFields);
  
  FullStreamUIPolicy::FullStreamUIPolicy(Profile* profile)
      : ActivityLogDatabasePolicy(
            profile,
            FilePath(chrome::kExtensionActivityLogFilename)) {}
  
  FullStreamUIPolicy::~FullStreamUIPolicy() {}
  
  bool FullStreamUIPolicy::InitDatabase(sql::Connection* db) {
    // Create the unified activity log entry table.
    return ActivityDatabase::InitializeTable(db,
                                             kTableName,
                                             kTableContentFields,
                                             kTableFieldTypes,
                                             arraysize(kTableContentFields));
  }
  
  bool FullStreamUIPolicy::FlushDatabase(sql::Connection* db) {
    if (queued_actions_.empty())
      return true;
  
    sql::Transaction transaction(db);
    if (!transaction.Begin())
      return false;
  
    std::string sql_str =
        "INSERT INTO " + std::string(FullStreamUIPolicy::kTableName) +
        " (extension_id, time, action_type, api_name, args, "
        "page_url, page_title, arg_url, other) VALUES (?,?,?,?,?,?,?,?,?)";
    sql::Statement statement(db->GetCachedStatement(
        sql::StatementID(SQL_FROM_HERE), sql_str.c_str()));
  
    for (size_t i = 0; i != queued_actions_.size(); ++i) {
      const Action& action = *queued_actions_[i];
      statement.Reset(true);
      statement.BindString(0, action.extension_id());
      statement.BindInt64(1, action.time().ToInternalValue());
      statement.BindInt(2, static_cast<int>(action.action_type()));
      statement.BindString(3, action.api_name());
      if (action.args()) {
        statement.BindString(4, Util::Serialize(action.args()));
      }
      std::string page_url_string = action.SerializePageUrl();
      if (!page_url_string.empty()) {
        statement.BindString(5, page_url_string);
      }
      if (!action.page_title().empty()) {
        statement.BindString(6, action.page_title());
      }
      std::string arg_url_string = action.SerializeArgUrl();
      if (!arg_url_string.empty()) {
        statement.BindString(7, arg_url_string);
      }
      if (action.other()) {
        statement.BindString(8, Util::Serialize(action.other()));
      }
  
      if (!statement.Run()) {
        LOG(ERROR) << "Activity log database I/O failed: " << sql_str;
        return false;
      }
    }
  
    if (!transaction.Commit())
      return false;
  
    queued_actions_.clear();
    return true;
  }
  
  std::unique_ptr<Action::ActionVector> FullStreamUIPolicy::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) {
    // 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::Connection* 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=?";
      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 ?";
    }
    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,rowid FROM %s %s %s ORDER BY time DESC LIMIT 300",
        kTableName,
        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(9));
  
      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())));
        }
      }
      actions->push_back(action);
    }
  
    return actions;
  }
  
  void FullStreamUIPolicy::DoRemoveActions(
      const std::vector<int64_t>& action_ids) {
    if (action_ids.empty())
      return;
  
    sql::Connection* 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();
        return;
      }
    }
  
    if (!transaction.Commit()) {
      LOG(ERROR) << "Removing activities from database failed";
    }
  }
  
  void FullStreamUIPolicy::DoRemoveURLs(const std::vector<GURL>& restrict_urls) {
    sql::Connection* 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);
  
    // If no restrictions then then all URLs need to be removed.
    if (restrict_urls.empty()) {
      sql::Statement statement;
      std::string sql_str = base::StringPrintf(
          "UPDATE %s SET page_url=NULL,page_title=NULL,arg_url=NULL",
          kTableName);
      statement.Assign(db->GetCachedStatement(
          sql::StatementID(SQL_FROM_HERE), sql_str.c_str()));
  
      if (!statement.Run()) {
        LOG(ERROR) << "Removing 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) {
      if (!restrict_urls[i].is_valid()) {
        continue;
      }
  
      // Remove any matching page url info.
      sql::Statement statement;
      std::string sql_str = base::StringPrintf(
        "UPDATE %s SET page_url=NULL,page_title=NULL WHERE page_url=?",
        kTableName);
      statement.Assign(db->GetCachedStatement(
          sql::StatementID(SQL_FROM_HERE), sql_str.c_str()));
      statement.BindString(0, restrict_urls[i].spec());
  
      if (!statement.Run()) {
        LOG(ERROR) << "Removing page URL from database failed: "
                   << statement.GetSQLStatement();
        return;
      }
  
      // Remove any matching arg urls.
      sql_str = base::StringPrintf("UPDATE %s SET arg_url=NULL WHERE arg_url=?",
                                   kTableName);
      statement.Assign(db->GetCachedStatement(
          sql::StatementID(SQL_FROM_HERE), sql_str.c_str()));
      statement.BindString(0, restrict_urls[i].spec());
  
      if (!statement.Run()) {
        LOG(ERROR) << "Removing arg URL from database failed: "
                   << statement.GetSQLStatement();
        return;
      }
    }
  }
  
  void FullStreamUIPolicy::DoRemoveExtensionData(
      const std::string& extension_id) {
    if (extension_id.empty())
      return;
  
    sql::Connection* 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=?", kTableName);
    sql::Statement statement;
    statement.Assign(
        db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE), sql_str.c_str()));
    statement.BindString(0, extension_id);
    if (!statement.Run()) {
      LOG(ERROR) << "Removing URLs for extension "
                 << extension_id << "from database failed: "
                 << statement.GetSQLStatement();
    }
  }
  
  void FullStreamUIPolicy::DoDeleteDatabase() {
    sql::Connection* db = GetDatabaseConnection();
    if (!db) {
      LOG(ERROR) << "Unable to connect to database";
      return;
    }
  
    queued_actions_.clear();
  
    // Not wrapped in a transaction because the deletion should happen even if
    // the vacuuming fails.
    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();
    statement.Assign(db->GetCachedStatement(sql::StatementID(SQL_FROM_HERE),
                                            "VACUUM"));
    if (!statement.Run()) {
      LOG(ERROR) << "Vacuuming the database failed: "
                 << statement.GetSQLStatement();
    }
  }
  
  void FullStreamUIPolicy::OnDatabaseFailure() {
    queued_actions_.clear();
  }
  
  void FullStreamUIPolicy::OnDatabaseClose() {
    delete this;
  }
  
  void FullStreamUIPolicy::Close() {
    ScheduleAndForget(activity_database(), &ActivityDatabase::Close);
  }
  
  void FullStreamUIPolicy::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(&FullStreamUIPolicy::DoReadFilteredData,
                       base::Unretained(this), extension_id, type, api_name,
                       page_url, arg_url, days_ago),
        std::move(callback));
  }
  
  void FullStreamUIPolicy::RemoveActions(const std::vector<int64_t>& action_ids) {
    ScheduleAndForget(this, &FullStreamUIPolicy::DoRemoveActions, action_ids);
  }
  
  void FullStreamUIPolicy::RemoveURLs(const std::vector<GURL>& restrict_urls) {
    ScheduleAndForget(this, &FullStreamUIPolicy::DoRemoveURLs, restrict_urls);
  }
  
  void FullStreamUIPolicy::RemoveExtensionData(const std::string& extension_id) {
    ScheduleAndForget(
        this, &FullStreamUIPolicy::DoRemoveExtensionData, extension_id);
  }
  
  void FullStreamUIPolicy::DeleteDatabase() {
    ScheduleAndForget(this, &FullStreamUIPolicy::DoDeleteDatabase);
  }
  
  scoped_refptr<Action> FullStreamUIPolicy::ProcessArguments(
      scoped_refptr<Action> action) const {
    return action;
  }
  
  void FullStreamUIPolicy::ProcessAction(scoped_refptr<Action> action) {
    // TODO(mvrable): Right now this argument stripping updates the Action object
    // in place, which isn't good if there are other users of the object.  When
    // database writing is moved to policy class, the modifications should be
    // made locally.
    action = ProcessArguments(action);
    ScheduleAndForget(this, &FullStreamUIPolicy::QueueAction, action);
  }
  
  void FullStreamUIPolicy::QueueAction(scoped_refptr<Action> action) {
    if (activity_database()->is_db_valid()) {
      queued_actions_.push_back(action);
      activity_database()->AdviseFlush(queued_actions_.size());
    }
  }
  
  }  // namespace extensions
  