// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/offline_pages/core/offline_page_metadata_store.h"

#include <utility>

#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/strings/cstring_view.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/offline_page_item.h"
#include "components/offline_pages/core/offline_store_types.h"
#include "components/offline_pages/core/offline_store_utils.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"

namespace offline_pages {

const int OfflinePageMetadataStore::kFirstPostLegacyVersion;
const int OfflinePageMetadataStore::kCurrentVersion;
const int OfflinePageMetadataStore::kCompatibleVersion;

namespace {

// This is a macro instead of a const so that
// it can be used inline in other SQL statements below.
#define OFFLINE_PAGES_TABLE_NAME "offlinepages_v1"

bool CreateOfflinePagesTable(sql::Database* db) {
  static const char kCreateLatestOfflinePagesTableSql[] =
      "CREATE TABLE IF NOT EXISTS " OFFLINE_PAGES_TABLE_NAME
      "(offline_id INTEGER PRIMARY KEY NOT NULL,"
      " creation_time INTEGER NOT NULL,"
      " file_size INTEGER NOT NULL,"
      " last_access_time INTEGER NOT NULL,"
      " access_count INTEGER NOT NULL,"
      " system_download_id INTEGER NOT NULL DEFAULT 0,"
      " file_missing_time INTEGER NOT NULL DEFAULT 0,"
      // upgrade_attempt is deprecated, and should be removed next time the
      // schema needs to be updated.
      " upgrade_attempt INTEGER NOT NULL DEFAULT 0,"
      " client_namespace VARCHAR NOT NULL,"
      " client_id VARCHAR NOT NULL,"
      " online_url VARCHAR NOT NULL,"
      " file_path VARCHAR NOT NULL,"
      " title VARCHAR NOT NULL DEFAULT '',"
      " original_url VARCHAR NOT NULL DEFAULT '',"
      " request_origin VARCHAR NOT NULL DEFAULT '',"
      " digest VARCHAR NOT NULL DEFAULT '',"
      " snippet VARCHAR NOT NULL DEFAULT '',"
      " attribution VARCHAR NOT NULL DEFAULT ''"
      ")";
  return db->Execute(kCreateLatestOfflinePagesTableSql);
}

bool UpgradeWithQuery(sql::Database* db, const base::cstring_view upgrade_sql) {
  if (!db->Execute("ALTER TABLE " OFFLINE_PAGES_TABLE_NAME
                   " RENAME TO temp_" OFFLINE_PAGES_TABLE_NAME)) {
    return false;
  }
  static const char kCreateOfflinePagesTableVersion1Sql[] =
      "CREATE TABLE IF NOT EXISTS " OFFLINE_PAGES_TABLE_NAME
      "(offline_id INTEGER PRIMARY KEY NOT NULL,"
      " creation_time INTEGER NOT NULL,"
      " file_size INTEGER NOT NULL,"
      " last_access_time INTEGER NOT NULL,"
      " access_count INTEGER NOT NULL,"
      " system_download_id INTEGER NOT NULL DEFAULT 0,"
      " file_missing_time INTEGER NOT NULL DEFAULT 0,"
      " upgrade_attempt INTEGER NOT NULL DEFAULT 0,"
      " client_namespace VARCHAR NOT NULL,"
      " client_id VARCHAR NOT NULL,"
      " online_url VARCHAR NOT NULL,"
      " file_path VARCHAR NOT NULL,"
      " title VARCHAR NOT NULL DEFAULT '',"
      " original_url VARCHAR NOT NULL DEFAULT '',"
      " request_origin VARCHAR NOT NULL DEFAULT '',"
      " digest VARCHAR NOT NULL DEFAULT ''"
      ")";
  if (!db->Execute(kCreateOfflinePagesTableVersion1Sql)) {
    return false;
  }
  if (!db->Execute(upgrade_sql)) {
    return false;
  }
  if (!db->Execute("DROP TABLE IF EXISTS temp_" OFFLINE_PAGES_TABLE_NAME)) {
    return false;
  }
  return true;
}

bool UpgradeFrom52(sql::Database* db) {
  static const char kSql[] =
      "INSERT INTO " OFFLINE_PAGES_TABLE_NAME
      " (offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, "
      "online_url, file_path) "
      "SELECT "
      "offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, "
      "online_url, file_path "
      "FROM temp_" OFFLINE_PAGES_TABLE_NAME;
  return UpgradeWithQuery(db, kSql);
}

bool UpgradeFrom53(sql::Database* db) {
  static const char kSql[] =
      "INSERT INTO " OFFLINE_PAGES_TABLE_NAME
      " (offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path) "
      "SELECT "
      "offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path "
      "FROM temp_" OFFLINE_PAGES_TABLE_NAME;
  return UpgradeWithQuery(db, kSql);
}

bool UpgradeFrom54(sql::Database* db) {
  static const char kSql[] =
      "INSERT INTO " OFFLINE_PAGES_TABLE_NAME
      " (offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title) "
      "SELECT "
      "offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title "
      "FROM temp_" OFFLINE_PAGES_TABLE_NAME;
  return UpgradeWithQuery(db, kSql);
}

bool UpgradeFrom55(sql::Database* db) {
  static const char kSql[] =
      "INSERT INTO " OFFLINE_PAGES_TABLE_NAME
      " (offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title) "
      "SELECT "
      "offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title "
      "FROM temp_" OFFLINE_PAGES_TABLE_NAME;
  return UpgradeWithQuery(db, kSql);
}

bool UpgradeFrom56(sql::Database* db) {
  static const char kSql[] =
      "INSERT INTO " OFFLINE_PAGES_TABLE_NAME
      " (offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title, original_url) "
      "SELECT "
      "offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title, original_url "
      "FROM temp_" OFFLINE_PAGES_TABLE_NAME;
  return UpgradeWithQuery(db, kSql);
}

bool UpgradeFrom57(sql::Database* db) {
  static const char kSql[] =
      "INSERT INTO " OFFLINE_PAGES_TABLE_NAME
      " (offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title, original_url) "
      "SELECT "
      "offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title, original_url "
      "FROM temp_" OFFLINE_PAGES_TABLE_NAME;
  return UpgradeWithQuery(db, kSql);
}

bool UpgradeFrom61(sql::Database* db) {
  static const char kSql[] =
      "INSERT INTO " OFFLINE_PAGES_TABLE_NAME
      " (offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title, original_url, request_origin) "
      "SELECT "
      "offline_id, creation_time, file_size, last_access_time, "
      "access_count, client_namespace, client_id, online_url, "
      "file_path, title, original_url, request_origin "
      "FROM temp_" OFFLINE_PAGES_TABLE_NAME;
  return UpgradeWithQuery(db, kSql);
}

bool CreatePageThumbnailsTable(sql::Database* db) {
  // TODO: The next schema change that modifies existing columns on this table
  // should also add "DEFAULT x''" to the definition of the "thumbnail" column.
  static const char kSql[] =
      "CREATE TABLE IF NOT EXISTS page_thumbnails"
      " (offline_id INTEGER PRIMARY KEY NOT NULL,"
      " expiration INTEGER NOT NULL,"
      " thumbnail BLOB NOT NULL,"
      " favicon BLOB NOT NULL DEFAULT x''"
      ")";
  return db->Execute(kSql);
}

bool CreateLatestSchema(sql::Database* db) {
  sql::Transaction transaction(db);
  if (!transaction.Begin()) {
    return false;
  }

  // First time database initialization.
  if (!CreateOfflinePagesTable(db)) {
    return false;
  }
  if (!CreatePageThumbnailsTable(db)) {
    return false;
  }

  sql::MetaTable meta_table;
  if (!meta_table.Init(db, OfflinePageMetadataStore::kCurrentVersion,
                       OfflinePageMetadataStore::kCompatibleVersion)) {
    return false;
  }

  return transaction.Commit();
}

// Upgrades the database from before the database version was stored in the
// MetaTable. This function should never need to be modified.
bool UpgradeFromLegacyVersion(sql::Database* db) {
  sql::Transaction transaction(db);
  if (!transaction.Begin()) {
    return false;
  }

  // Legacy upgrade section. Details are described in the header file.
  if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "expiration_time") &&
      !db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "title")) {
    if (!UpgradeFrom52(db)) {
      return false;
    }
  } else if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "title")) {
    if (!UpgradeFrom53(db)) {
      return false;
    }
  } else if (db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "offline_url")) {
    if (!UpgradeFrom54(db)) {
      return false;
    }
  } else if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "original_url")) {
    if (!UpgradeFrom55(db)) {
      return false;
    }
  } else if (db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "expiration_time")) {
    if (!UpgradeFrom56(db)) {
      return false;
    }
  } else if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "request_origin")) {
    if (!UpgradeFrom57(db)) {
      return false;
    }
  } else if (!db->DoesColumnExist(OFFLINE_PAGES_TABLE_NAME, "digest")) {
    if (!UpgradeFrom61(db)) {
      return false;
    }
  }

  sql::MetaTable meta_table;
  if (!meta_table.Init(db, OfflinePageMetadataStore::kFirstPostLegacyVersion,
                       OfflinePageMetadataStore::kCompatibleVersion)) {
    return false;
  }

  return transaction.Commit();
}

bool UpgradeFromVersion1ToVersion2(sql::Database* db,
                                   sql::MetaTable* meta_table) {
  return meta_table->SetVersionNumber(2);
  // No actual changes necessary, because upgrade_attempt was deprecated.
}

bool UpgradeFromVersion2ToVersion3(sql::Database* db,
                                   sql::MetaTable* meta_table) {
  sql::Transaction transaction(db);
  if (!transaction.Begin()) {
    return false;
  }

  static const char kCreatePageThumbnailsSql[] =
      "CREATE TABLE IF NOT EXISTS page_thumbnails"
      " (offline_id INTEGER PRIMARY KEY NOT NULL,"
      "expiration INTEGER NOT NULL,"
      "thumbnail BLOB NOT NULL"
      ")";
  if (!db->Execute(kCreatePageThumbnailsSql)) {
    return false;
  }

  return meta_table->SetVersionNumber(3) && transaction.Commit();
}

bool UpgradeFromVersion3ToVersion4(sql::Database* db,
                                   sql::MetaTable* meta_table) {
  sql::Transaction transaction(db);
  if (!transaction.Begin()) {
    return false;
  }

  static constexpr char kAddSnippetColumnSql[] =
      "ALTER TABLE " OFFLINE_PAGES_TABLE_NAME
      " ADD COLUMN snippet VARCHAR NOT NULL DEFAULT ''";

  static constexpr char kAddAttributionColumnSql[] =
      "ALTER TABLE " OFFLINE_PAGES_TABLE_NAME
      " ADD COLUMN attribution VARCHAR NOT NULL DEFAULT ''";

  if (!db->Execute(kAddSnippetColumnSql) ||
      !db->Execute(kAddAttributionColumnSql)) {
    return false;
  }

  static constexpr char kUpgradeThumbnailsTableSql[] =
      "ALTER TABLE page_thumbnails"
      " ADD COLUMN favicon BLOB NOT NULL DEFAULT x''";
  if (!db->Execute(kUpgradeThumbnailsTableSql)) {
    return false;
  }

  return meta_table->SetVersionNumber(4) && transaction.Commit();
}

bool CreateSchema(sql::Database* db) {
  if (!sql::MetaTable::DoesTableExist(db)) {
    // If this looks like a completely empty DB, simply start from scratch.
    if (!db->DoesTableExist(OFFLINE_PAGES_TABLE_NAME)) {
      return CreateLatestSchema(db);
    }

    // Otherwise we need to run a legacy upgrade.
    if (!UpgradeFromLegacyVersion(db)) {
      return false;
    }
  }

  sql::MetaTable meta_table;
  if (!meta_table.Init(db, OfflinePageMetadataStore::kCurrentVersion,
                       OfflinePageMetadataStore::kCompatibleVersion)) {
    return false;
  }

  for (;;) {
    switch (meta_table.GetVersionNumber()) {
      case 1:
        if (!UpgradeFromVersion1ToVersion2(db, &meta_table)) {
          return false;
        }
        break;
      case 2:
        if (!UpgradeFromVersion2ToVersion3(db, &meta_table)) {
          return false;
        }
        break;
      case 3:
        if (!UpgradeFromVersion3ToVersion4(db, &meta_table)) {
          return false;
        }
        break;
      case OfflinePageMetadataStore::kCurrentVersion:
        return true;
      default:
        return false;
    }
  }
}

StoreState InitializationStatusToStoreState(
    SqlStoreBase::InitializationStatus status) {
  switch (status) {
    case SqlStoreBase::InitializationStatus::kNotInitialized:
      return StoreState::NOT_LOADED;
    case SqlStoreBase::InitializationStatus::kInProgress:
      return StoreState::INITIALIZING;
    case SqlStoreBase::InitializationStatus::kSuccess:
      return StoreState::LOADED;
    case SqlStoreBase::InitializationStatus::kFailure:
      return StoreState::FAILED_LOADING;
  }
}

}  // anonymous namespace

OfflinePageMetadataStore::OfflinePageMetadataStore(
    scoped_refptr<base::SequencedTaskRunner> background_task_runner)
    : SqlStoreBase("OfflinePageMetadata",
                   std::move(background_task_runner),
                   base::FilePath()) {}

OfflinePageMetadataStore::OfflinePageMetadataStore(
    scoped_refptr<base::SequencedTaskRunner> background_task_runner,
    const base::FilePath& path)
    : SqlStoreBase("OfflinePageMetadata",
                   std::move(background_task_runner),
                   path.AppendASCII("OfflinePages.db")) {}

OfflinePageMetadataStore::~OfflinePageMetadataStore() = default;

base::OnceCallback<bool(sql::Database* db)>
OfflinePageMetadataStore::GetSchemaInitializationFunction() {
  return base::BindOnce(&CreateSchema);
}

StoreState OfflinePageMetadataStore::GetStateForTesting() const {
  return InitializationStatusToStoreState(initialization_status_for_testing());
}

void OfflinePageMetadataStore::OnOpenStart(base::TimeTicks last_closing_time) {
  TRACE_EVENT_BEGIN("offline_pages", "Metadata Store",
                    perfetto::NamedTrack::FromPointer(
                        "offline_pages::OfflinePageMetadataStore", this),
                    "is reopen", !last_closing_time.is_null());
  TRACE_EVENT_BEGIN("offline_pages", "Metadata Store: Initializing",
                    perfetto::NamedTrack::FromPointer(
                        "offline_pages::OfflinePageMetadataStore", this));
}

void OfflinePageMetadataStore::OnOpenDone(bool success) {
  TRACE_EVENT_END("offline_pages",
                  /*Metadata Store: Initializing */
                  perfetto::NamedTrack::FromPointer(
                      "offline_pages::OfflinePageMetadataStore", this),
                  "succeeded", success);
}

void OfflinePageMetadataStore::OnTaskBegin(bool is_initialized) {
  TRACE_EVENT_BEGIN("offline_pages", "Metadata Store: task execution",
                    perfetto::NamedTrack::FromPointer(
                        "offline_pages::OfflinePageMetadataStore", this),
                    "is store loaded", is_initialized);
}

void OfflinePageMetadataStore::OnTaskRunComplete() {
  // Note: the time recorded for this trace step will include thread hop wait
  // times to the background thread and back.
  TRACE_EVENT_END(
      "offline_pages",
      /* Metadata Store: task execution*/ perfetto::NamedTrack::FromPointer(
          "offline_pages::OfflinePageMetadataStore", this));
}

void OfflinePageMetadataStore::OnTaskReturnComplete() {
  TRACE_EVENT_END(
      "offline_pages",
      /* Metadata Store: task execution */ perfetto::NamedTrack::FromPointer(
          "offline_pages::OfflinePageMetadataStore", this));
}

void OfflinePageMetadataStore::OnCloseStart(
    InitializationStatus status_before_close) {
  if (status_before_close != InitializationStatus::kSuccess) {
    return;
  }
  TRACE_EVENT_BEGIN("offline_pages", "Metadata Store: Closing",
                    perfetto::NamedTrack::FromPointer(
                        "offline_pages::OfflinePageMetadataStore", this));
}

void OfflinePageMetadataStore::OnCloseComplete() {
  TRACE_EVENT_END(
      "offline_pages",
      /* Metadata Store: Closing */ perfetto::NamedTrack::FromPointer(
          "offline_pages::OfflinePageMetadataStore", this));
  TRACE_EVENT_END("offline_pages",
                  /* Metadata Store */ perfetto::NamedTrack::FromPointer(
                      "offline_pages::OfflinePageMetadataStore", this));
}

}  // namespace offline_pages
