// Copyright (c) 2010 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.
//
// This class isn't pretty. It's just a step better than globals, which is what
// these were previously.

#include "chrome/browser/sync/util/user_settings.h"

#include "build/build_config.h"

#if defined(OS_WIN)
#include <windows.h>
#endif

#include <limits>
#include <string>
#include <vector>

#include "base/file_util.h"
#include "base/string_util.h"
#include "chrome/browser/sync/syncable/directory_manager.h"  // For migration.
#include "chrome/browser/sync/util/crypto_helpers.h"
#include "chrome/browser/sync/util/data_encryption.h"
#include "chrome/common/sqlite_utils.h"

using std::numeric_limits;
using std::string;
using std::vector;

using syncable::DirectoryManager;

namespace browser_sync {

void ExecOrDie(sqlite3* dbhandle, const char *query) {
  SQLStatement statement;
  statement.prepare(dbhandle, query);
  if (SQLITE_DONE != statement.step()) {
    LOG(FATAL) << query << "\n" << sqlite3_errmsg(dbhandle);
  }
}

// Useful for encoding any sequence of bytes into a string that can be used in
// a table name. Kind of like hex encoding, except that A is zero and P is 15.
string APEncode(const string& in) {
  string result;
  result.reserve(in.size() * 2);
  for (string::const_iterator i = in.begin(); i != in.end(); ++i) {
    unsigned int c = static_cast<unsigned char>(*i);
    result.push_back((c & 0x0F) + 'A');
    result.push_back(((c >> 4) & 0x0F) + 'A');
  }
  return result;
}

string APDecode(const string& in) {
  string result;
  result.reserve(in.size() / 2);
  for (string::const_iterator i = in.begin(); i != in.end(); ++i) {
    unsigned int c = *i - 'A';
    if (++i != in.end())
      c = c | (static_cast<unsigned char>(*i - 'A') << 4);
    result.push_back(c);
  }
  return result;
}

static const char PASSWORD_HASH[] = "password_hash2";
static const char SALT[] = "salt2";

static const int kSaltSize = 20;
static const int kCurrentDBVersion = 12;

UserSettings::ScopedDBHandle::ScopedDBHandle(UserSettings* settings)
    : mutex_lock_(settings->dbhandle_mutex_), handle_(&settings->dbhandle_) {
}

UserSettings::UserSettings() : dbhandle_(NULL) {
}

string UserSettings::email() const {
  base::AutoLock lock(mutex_);
  return email_;
}

static void MakeSigninsTable(sqlite3* const dbhandle) {
  // Multiple email addresses can map to the same Google Account. This table
  // keeps a map of sign-in email addresses to primary Google Account email
  // addresses.
  ExecOrDie(dbhandle,
            "CREATE TABLE signins"
            " (signin, primary_email, "
            " PRIMARY KEY(signin, primary_email) ON CONFLICT REPLACE)");
}

void UserSettings::MigrateOldVersionsAsNeeded(sqlite3* const handle,
    int current_version) {
  switch (current_version) {
    // Versions 1-9 are unhandled.  Version numbers greater than
    // kCurrentDBVersion should have already been weeded out by the caller.
    default:
      // When the version is too old, we just try to continue anyway.  There
      // should not be a released product that makes a database too old for us
      // to handle.
      LOG(WARNING) << "UserSettings database version " << current_version <<
          " is too old to handle.";
      return;
    case 10:
      {
        // Scrape the 'shares' table to find the syncable DB.  'shares' had a
        // pair of string columns that mapped the username to the filename of
        // the sync data sqlite3 file.  Version 11 switched to a constant
        // filename, so here we read the string, copy the file to the new name,
        // delete the old one, and then drop the unused shares table.
        SQLStatement share_query;
        share_query.prepare(handle, "SELECT share_name, file_name FROM shares");
        int query_result = share_query.step();
        CHECK(SQLITE_ROW == query_result);
        FilePath::StringType share_name, file_name;
#if defined(OS_POSIX)
        share_name = share_query.column_string(0);
        file_name = share_query.column_string(1);
#else
        share_name = share_query.column_wstring(0);
        file_name = share_query.column_wstring(1);
#endif

        const FilePath& src_syncdata_path = FilePath(file_name);
        FilePath dst_syncdata_path(src_syncdata_path.DirName());
        file_util::AbsolutePath(&dst_syncdata_path);
        dst_syncdata_path = dst_syncdata_path.Append(
            DirectoryManager::GetSyncDataDatabaseFilename());
        if (!file_util::Move(src_syncdata_path, dst_syncdata_path)) {
          LOG(WARNING) << "Unable to upgrade UserSettings from v10";
          return;
        }
      }
      ExecOrDie(handle, "DROP TABLE shares");
      ExecOrDie(handle, "UPDATE db_version SET version = 11");
    // FALL THROUGH
    case 11:
      ExecOrDie(handle, "DROP TABLE signin_types");
      ExecOrDie(handle, "UPDATE db_version SET version = 12");
    // FALL THROUGH
    case kCurrentDBVersion:
      // Nothing to migrate.
      return;
  }
}

static void MakeCookiesTable(sqlite3* const dbhandle) {
  // This table keeps a list of auth tokens for each signed in account. There
  // will be as many rows as there are auth tokens per sign in.
  // The service_token column will store encrypted values.
  ExecOrDie(dbhandle,
            "CREATE TABLE cookies"
            " (email, service_name, service_token, "
            " PRIMARY KEY(email, service_name) ON CONFLICT REPLACE)");
}

static void MakeClientIDTable(sqlite3* const dbhandle) {
  // Stores a single client ID value that can be used as the client id, if
  // there's not another such ID provided on the install.
  ExecOrDie(dbhandle, "CREATE TABLE client_id (id) ");
  {
    SQLStatement statement;
    statement.prepare(dbhandle,
                      "INSERT INTO client_id values ( ? )");
    statement.bind_string(0, Generate128BitRandomHexString());
    if (SQLITE_DONE != statement.step()) {
      LOG(FATAL) << "INSERT INTO client_id\n" << sqlite3_errmsg(dbhandle);
    }
  }
}

bool UserSettings::Init(const FilePath& settings_path) {
  {  // Scope the handle.
    ScopedDBHandle dbhandle(this);
    if (dbhandle_)
      sqlite3_close(dbhandle_);

    if (SQLITE_OK != sqlite_utils::OpenSqliteDb(settings_path, &dbhandle_))
      return false;

    // In the worst case scenario, the user may hibernate his computer during
    // one of our transactions.
    sqlite3_busy_timeout(dbhandle_, numeric_limits<int>::max());
    ExecOrDie(dbhandle.get(), "PRAGMA fullfsync = 1");
    ExecOrDie(dbhandle.get(), "PRAGMA synchronous = 2");

    SQLTransaction transaction(dbhandle.get());
    transaction.BeginExclusive();
    SQLStatement table_query;
    table_query.prepare(dbhandle.get(),
                        "select count(*) from sqlite_master"
                        " where type = 'table' and name = 'db_version'");
    int query_result = table_query.step();
    CHECK(SQLITE_ROW == query_result);
    int table_count = table_query.column_int(0);
    table_query.reset();
    if (table_count > 0) {
      SQLStatement version_query;
      version_query.prepare(dbhandle.get(),
                            "SELECT version FROM db_version");
      query_result = version_query.step();
      CHECK(SQLITE_ROW == query_result);
      const int version = version_query.column_int(0);
      version_query.reset();
      if (version > kCurrentDBVersion) {
        LOG(WARNING) << "UserSettings database is too new.";
        return false;
      }

      MigrateOldVersionsAsNeeded(dbhandle.get(), version);
    } else {
      // Create settings table.
      {
        SQLStatement statement;
        statement.prepare(dbhandle.get(),
                          "CREATE TABLE settings"
                          " (email, key, value, "
                          "  PRIMARY KEY(email, key) ON CONFLICT REPLACE)");
        if (SQLITE_DONE != statement.step()) {
          return false;
        }
      }
      // Create and populate version table.
      {
        SQLStatement statement;
        statement.prepare(dbhandle.get(),
                          "CREATE TABLE db_version ( version )");
        if (SQLITE_DONE != statement.step()) {
          return false;
        }
      }
      {
        SQLStatement statement;
        statement.prepare(dbhandle.get(),
                          "INSERT INTO db_version values ( ? )");
        statement.bind_int(0, kCurrentDBVersion);
        if (SQLITE_DONE != statement.step()) {
          return false;
        }
      }

      MakeSigninsTable(dbhandle.get());
      MakeCookiesTable(dbhandle.get());
      MakeClientIDTable(dbhandle.get());
    }
    transaction.Commit();
  }
#if defined(OS_WIN)
  // Do not index this file. Scanning can occur every time we close the file,
  // which causes long delays in SQLite's file locking.
  const DWORD attrs = GetFileAttributes(settings_path.value().c_str());
  const BOOL attrs_set =
    SetFileAttributes(settings_path.value().c_str(),
                      attrs | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
#endif
  return true;
}

UserSettings::~UserSettings() {
  if (dbhandle_)
    sqlite3_close(dbhandle_);
}

const int32 kInvalidHash = 0xFFFFFFFF;

// We use 10 bits of data from the MD5 digest as the hash.
const int32 kHashMask = 0x3FF;

int32 GetHashFromDigest(const vector<uint8>& digest) {
  int32 hash = 0;
  int32 mask = kHashMask;
  for (vector<uint8>::const_iterator i = digest.begin(); i != digest.end();
       ++i) {
    hash = hash << 8;
    hash = hash | (*i & kHashMask);
    mask = mask >> 8;
    if (0 == mask)
      break;
  }
  return hash;
}

void UserSettings::StoreEmailForSignin(const string& signin,
                                       const string& primary_email) {
  ScopedDBHandle dbhandle(this);
  SQLTransaction transaction(dbhandle.get());
  int sqlite_result = transaction.BeginExclusive();
  CHECK(SQLITE_OK == sqlite_result);
  SQLStatement query;
  query.prepare(dbhandle.get(),
                "SELECT COUNT(*) FROM signins"
                " WHERE signin = ? AND primary_email = ?");
  query.bind_string(0, signin);
  query.bind_string(1, primary_email);
  int query_result = query.step();
  CHECK(SQLITE_ROW == query_result);
  int32 count = query.column_int(0);
  query.reset();
  if (0 == count) {
    // Migrate any settings the user might have from earlier versions.
    {
      SQLStatement statement;
      statement.prepare(dbhandle.get(),
                        "UPDATE settings SET email = ? WHERE email = ?");
      statement.bind_string(0, signin);
      statement.bind_string(1, primary_email);
      if (SQLITE_DONE != statement.step()) {
        LOG(FATAL) << sqlite3_errmsg(dbhandle.get());
      }
    }
    // Store this signin:email mapping.
    {
      SQLStatement statement;
      statement.prepare(dbhandle.get(),
                        "INSERT INTO signins(signin, primary_email)"
                        " values ( ?, ? )");
      statement.bind_string(0, signin);
      statement.bind_string(1, primary_email);
      if (SQLITE_DONE != statement.step()) {
        LOG(FATAL) << sqlite3_errmsg(dbhandle.get());
      }
    }
  }
  transaction.Commit();
}

// string* signin is both the input and the output of this function.
bool UserSettings::GetEmailForSignin(string* signin) {
  ScopedDBHandle dbhandle(this);
  string result;
  SQLStatement query;
  query.prepare(dbhandle.get(),
                "SELECT primary_email FROM signins WHERE signin = ?");
  query.bind_string(0, *signin);
  int query_result = query.step();
  if (SQLITE_ROW == query_result) {
    query.column_string(0, &result);
    if (!result.empty()) {
      swap(result, *signin);
      return true;
    }
  }
  return false;
}

void UserSettings::StoreHashedPassword(const string& email,
                                       const string& password) {
  // Save one-way hashed password:
  char binary_salt[kSaltSize];
  GetRandomBytes(binary_salt, sizeof(binary_salt));

  const string salt = APEncode(string(binary_salt, sizeof(binary_salt)));
  MD5Calculator md5;
  md5.AddData(salt.data(), salt.size());
  md5.AddData(password.data(), password.size());
  ScopedDBHandle dbhandle(this);
  SQLTransaction transaction(dbhandle.get());
  transaction.BeginExclusive();
  {
    SQLStatement statement;
    statement.prepare(dbhandle.get(),
                      "INSERT INTO settings(email, key, value)"
                      " values ( ?, ?, ? )");
    statement.bind_string(0, email);
    statement.bind_string(1, PASSWORD_HASH);
    statement.bind_int(2, GetHashFromDigest(md5.GetDigest()));
    if (SQLITE_DONE != statement.step()) {
      LOG(FATAL) << sqlite3_errmsg(dbhandle.get());
    }
  }
  {
    SQLStatement statement;
    statement.prepare(dbhandle.get(),
                      "INSERT INTO settings(email, key, value)"
                      " values ( ?, ?, ? )");
    statement.bind_string(0, email);
    statement.bind_string(1, SALT);
    statement.bind_string(2, salt);
    if (SQLITE_DONE != statement.step()) {
      LOG(FATAL) << sqlite3_errmsg(dbhandle.get());
    }
  }
  transaction.Commit();
}

bool UserSettings::VerifyAgainstStoredHash(const string& email,
                                           const string& password) {
  ScopedDBHandle dbhandle(this);
  string salt_and_digest;

  SQLStatement query;
  query.prepare(dbhandle.get(),
                "SELECT key, value FROM settings"
                " WHERE email = ? AND (key = ? OR key = ?)");
  query.bind_string(0, email);
  query.bind_string(1, PASSWORD_HASH);
  query.bind_string(2, SALT);
  int query_result = query.step();
  string salt;
  int32 hash = kInvalidHash;
  while (SQLITE_ROW == query_result) {
    string key(query.column_string(0));
    if (key == SALT)
      salt = query.column_string(1);
    else
      hash = query.column_int(1);
    query_result = query.step();
  }
  CHECK(SQLITE_DONE == query_result);
  if (salt.empty() || hash == kInvalidHash)
    return false;
  MD5Calculator md5;
  md5.AddData(salt.data(), salt.size());
  md5.AddData(password.data(), password.size());
  return hash == GetHashFromDigest(md5.GetDigest());
}

void UserSettings::SwitchUser(const string& username) {
  {
    base::AutoLock lock(mutex_);
    email_ = username;
  }
}

string UserSettings::GetClientId() {
  ScopedDBHandle dbhandle(this);
  SQLStatement statement;
  statement.prepare(dbhandle.get(), "SELECT id FROM client_id");
  int query_result = statement.step();
  string client_id;
  if (query_result == SQLITE_ROW)
    client_id = statement.column_string(0);
  return client_id;
}

void UserSettings::ClearAllServiceTokens() {
  ScopedDBHandle dbhandle(this);
  ExecOrDie(dbhandle.get(), "DELETE FROM cookies");
}

bool UserSettings::GetLastUser(string* username) {
  ScopedDBHandle dbhandle(this);
  SQLStatement query;
  query.prepare(dbhandle.get(), "SELECT email FROM cookies");
  if (SQLITE_ROW == query.step()) {
    *username = query.column_string(0);
    return true;
  } else {
    return false;
  }
}

}  // namespace browser_sync