// Copyright 2017 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 "content/browser/renderer_host/web_database_host_impl.h"

#include <string>
#include <utility>

#include "base/bind.h"
#include "base/check_op.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "storage/browser/database/database_util.h"
#include "storage/browser/database/vfs_backend.h"
#include "storage/browser/quota/quota_manager.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/common/database/database_identifier.h"
#include "third_party/blink/public/common/loader/network_utils.h"
#include "third_party/blink/public/mojom/quota/quota_types.mojom.h"
#include "third_party/sqlite/sqlite3.h"
#include "url/origin.h"

using storage::DatabaseUtil;
using storage::VfsBackend;
using storage::QuotaManager;

namespace content {
namespace {
// The number of times to attempt to delete the SQLite database, if there is
// an error.
const int kNumDeleteRetries = 2;
// The delay between each retry to delete the SQLite database.
const int kDelayDeleteRetryMs = 100;

void ValidateOriginOnUIThread(
    int process_id,
    const url::Origin& origin,
    scoped_refptr<base::SequencedTaskRunner> callback_task_runner,
    base::OnceClosure success_callback,
    mojo::ReportBadMessageCallback error_callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // Return early if the process was shutdown before this task was able to run.
  if (!RenderProcessHost::FromID(process_id))
    return;

  if (!ChildProcessSecurityPolicyImpl::GetInstance()->CanAccessDataForOrigin(
          process_id, origin)) {
    callback_task_runner->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(error_callback), "Unauthorized origin."));
    return;
  }

  callback_task_runner->PostTask(FROM_HERE, std::move(success_callback));
}

}  // namespace

WebDatabaseHostImpl::WebDatabaseHostImpl(
    int process_id,
    scoped_refptr<storage::DatabaseTracker> db_tracker)
    : process_id_(process_id),
      observer_added_(false),
      db_tracker_(std::move(db_tracker)) {
  DCHECK(db_tracker_);
}

WebDatabaseHostImpl::~WebDatabaseHostImpl() {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  if (observer_added_) {
    db_tracker_->RemoveObserver(this);

    // If the renderer process died without closing all databases,
    // then we need to manually close those connections
    db_tracker_->CloseDatabases(database_connections_);
    database_connections_.RemoveAllConnections();
  }
}

void WebDatabaseHostImpl::Create(
    int process_id,
    scoped_refptr<storage::DatabaseTracker> db_tracker,
    mojo::PendingReceiver<blink::mojom::WebDatabaseHost> receiver) {
  DCHECK(db_tracker->task_runner()->RunsTasksInCurrentSequence());
  mojo::MakeSelfOwnedReceiver(
      std::make_unique<WebDatabaseHostImpl>(process_id, std::move(db_tracker)),
      std::move(receiver));
}

void WebDatabaseHostImpl::OpenFile(const base::string16& vfs_file_name,
                                   int32_t desired_flags,
                                   OpenFileCallback callback) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  ValidateOrigin(vfs_file_name,
                 base::BindOnce(&WebDatabaseHostImpl::OpenFileValidated,
                                weak_ptr_factory_.GetWeakPtr(), vfs_file_name,
                                desired_flags, std::move(callback)));
}

void WebDatabaseHostImpl::OpenFileValidated(const base::string16& vfs_file_name,
                                            int32_t desired_flags,
                                            OpenFileCallback callback) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  base::File file;
  const base::File* tracked_file = nullptr;
  std::string origin_identifier;
  base::string16 database_name;

  // When in OffTheRecord mode, we want to make sure that all DB files are
  // removed when the OffTheRecord browser context goes away, so we add the
  // SQLITE_OPEN_DELETEONCLOSE flag when opening all files, and keep
  // open handles to them in the database tracker to make sure they're
  // around for as long as needed.
  if (vfs_file_name.empty()) {
    file = VfsBackend::OpenTempFileInDirectory(
        db_tracker_->database_directory(), desired_flags);
  } else if (DatabaseUtil::CrackVfsFileName(vfs_file_name, &origin_identifier,
                                            &database_name, nullptr) &&
             !db_tracker_->IsDatabaseScheduledForDeletion(origin_identifier,
                                                          database_name)) {
    base::FilePath db_file = DatabaseUtil::GetFullFilePathForVfsFile(
        db_tracker_.get(), vfs_file_name);
    if (!db_file.empty()) {
      if (db_tracker_->IsOffTheRecordProfile()) {
        tracked_file = db_tracker_->GetOffTheRecordFile(vfs_file_name);
        if (!tracked_file) {
          file = VfsBackend::OpenFile(
              db_file, desired_flags | SQLITE_OPEN_DELETEONCLOSE);
          if (!(desired_flags & SQLITE_OPEN_DELETEONCLOSE)) {
            tracked_file = db_tracker_->SaveOffTheRecordFile(vfs_file_name,
                                                             std::move(file));
          }
        }
      } else {
        file = VfsBackend::OpenFile(db_file, desired_flags);
      }
    }
  }

  base::File result;
  if (file.IsValid()) {
    result = std::move(file);
  } else if (tracked_file) {
    DCHECK(tracked_file->IsValid());
    result = tracked_file->Duplicate();
  }
  std::move(callback).Run(std::move(result));
}

void WebDatabaseHostImpl::DeleteFile(const base::string16& vfs_file_name,
                                     bool sync_dir,
                                     DeleteFileCallback callback) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  ValidateOrigin(
      vfs_file_name,
      base::BindOnce(&WebDatabaseHostImpl::DatabaseDeleteFile,
                     weak_ptr_factory_.GetWeakPtr(), vfs_file_name, sync_dir,
                     std::move(callback), kNumDeleteRetries));
}

void WebDatabaseHostImpl::GetFileAttributes(
    const base::string16& vfs_file_name,
    GetFileAttributesCallback callback) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  ValidateOrigin(
      vfs_file_name,
      base::BindOnce(&WebDatabaseHostImpl::GetFileAttributesValidated,
                     weak_ptr_factory_.GetWeakPtr(), vfs_file_name,
                     std::move(callback)));
}

void WebDatabaseHostImpl::GetFileAttributesValidated(
    const base::string16& vfs_file_name,
    GetFileAttributesCallback callback) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  int32_t attributes = -1;
  base::FilePath db_file =
      DatabaseUtil::GetFullFilePathForVfsFile(db_tracker_.get(), vfs_file_name);
  if (!db_file.empty()) {
    attributes = VfsBackend::GetFileAttributes(db_file);
  }
  std::move(callback).Run(attributes);
}

void WebDatabaseHostImpl::GetFileSize(const base::string16& vfs_file_name,
                                      GetFileSizeCallback callback) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  ValidateOrigin(vfs_file_name,
                 base::BindOnce(&WebDatabaseHostImpl::GetFileSizeValidated,
                                weak_ptr_factory_.GetWeakPtr(), vfs_file_name,
                                std::move(callback)));
}

void WebDatabaseHostImpl::GetFileSizeValidated(
    const base::string16& vfs_file_name,
    GetFileSizeCallback callback) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  int64_t size = 0LL;
  base::FilePath db_file =
      DatabaseUtil::GetFullFilePathForVfsFile(db_tracker_.get(), vfs_file_name);
  if (!db_file.empty()) {
    size = VfsBackend::GetFileSize(db_file);
  }
  std::move(callback).Run(size);
}

void WebDatabaseHostImpl::SetFileSize(const base::string16& vfs_file_name,
                                      int64_t expected_size,
                                      SetFileSizeCallback callback) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  ValidateOrigin(vfs_file_name,
                 base::BindOnce(&WebDatabaseHostImpl::SetFileSizeValidated,
                                weak_ptr_factory_.GetWeakPtr(), vfs_file_name,
                                expected_size, std::move(callback)));
}

void WebDatabaseHostImpl::SetFileSizeValidated(
    const base::string16& vfs_file_name,
    int64_t expected_size,
    SetFileSizeCallback callback) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  bool success = false;
  base::FilePath db_file =
      DatabaseUtil::GetFullFilePathForVfsFile(db_tracker_.get(), vfs_file_name);
  if (!db_file.empty()) {
    success = VfsBackend::SetFileSize(db_file, expected_size);
  }
  std::move(callback).Run(success);
}

void WebDatabaseHostImpl::GetSpaceAvailable(
    const url::Origin& origin,
    GetSpaceAvailableCallback callback) {
  // QuotaManager is only available on the IO thread.
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  ValidateOrigin(
      origin, base::BindOnce(&WebDatabaseHostImpl::GetSpaceAvailableValidated,
                             weak_ptr_factory_.GetWeakPtr(), origin,
                             std::move(callback)));
}

void WebDatabaseHostImpl::GetSpaceAvailableValidated(
    const url::Origin& origin,
    GetSpaceAvailableCallback callback) {
  // QuotaManager is only available on the IO thread.
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  DCHECK(db_tracker_->quota_manager_proxy());
  db_tracker_->quota_manager_proxy()->GetUsageAndQuota(
      db_tracker_->task_runner(), origin, blink::mojom::StorageType::kTemporary,
      base::BindOnce(
          [](GetSpaceAvailableCallback callback,
             blink::mojom::QuotaStatusCode status, int64_t usage,
             int64_t quota) {
            int64_t available = 0;
            if ((status == blink::mojom::QuotaStatusCode::kOk) &&
                (usage < quota)) {
              available = quota - usage;
            }
            std::move(callback).Run(available);
          },
          std::move(callback)));
}

void WebDatabaseHostImpl::DatabaseDeleteFile(
    const base::string16& vfs_file_name,
    bool sync_dir,
    DeleteFileCallback callback,
    int reschedule_count) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  // Return an error if the file name is invalid or if the file could not
  // be deleted after kNumDeleteRetries attempts.
  int error_code = SQLITE_IOERR_DELETE;
  base::FilePath db_file =
      DatabaseUtil::GetFullFilePathForVfsFile(db_tracker_.get(), vfs_file_name);
  if (!db_file.empty()) {
    // In order to delete a journal file in OffTheRecord mode, we only need to
    // close the open handle to it that's stored in the database tracker.
    if (db_tracker_->IsOffTheRecordProfile()) {
      const base::string16 wal_suffix(base::ASCIIToUTF16("-wal"));
      base::string16 sqlite_suffix;

      // WAL files can be deleted without having previously been opened.
      if (!db_tracker_->HasSavedOffTheRecordFileHandle(vfs_file_name) &&
          DatabaseUtil::CrackVfsFileName(vfs_file_name, nullptr, nullptr,
                                         &sqlite_suffix) &&
          sqlite_suffix == wal_suffix) {
        error_code = SQLITE_OK;
      } else {
        db_tracker_->CloseOffTheRecordFileHandle(vfs_file_name);
        error_code = SQLITE_OK;
      }
    } else {
      error_code = VfsBackend::DeleteFile(db_file, sync_dir);
    }

    if ((error_code == SQLITE_IOERR_DELETE) && reschedule_count) {
      // If the file could not be deleted, try again.
      db_tracker_->task_runner()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(&WebDatabaseHostImpl::DatabaseDeleteFile,
                         weak_ptr_factory_.GetWeakPtr(), vfs_file_name,
                         sync_dir, std::move(callback), reschedule_count - 1),
          base::TimeDelta::FromMilliseconds(kDelayDeleteRetryMs));
      return;
    }
  }

  std::move(callback).Run(error_code);
}

void WebDatabaseHostImpl::Opened(const url::Origin& origin,
                                 const base::string16& database_name,
                                 const base::string16& database_description,
                                 int64_t estimated_size) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  if (!observer_added_) {
    observer_added_ = true;
    db_tracker_->AddObserver(this);
  }

  ValidateOrigin(origin, base::BindOnce(&WebDatabaseHostImpl::OpenedValidated,
                                        weak_ptr_factory_.GetWeakPtr(), origin,
                                        database_name, database_description,
                                        estimated_size));
}

void WebDatabaseHostImpl::OpenedValidated(
    const url::Origin& origin,
    const base::string16& database_name,
    const base::string16& database_description,
    int64_t estimated_size) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  UMA_HISTOGRAM_BOOLEAN("websql.OpenDatabase",
                        blink::network_utils::IsOriginSecure(origin.GetURL()));

  int64_t database_size = 0;
  std::string origin_identifier(storage::GetIdentifierFromOrigin(origin));
  db_tracker_->DatabaseOpened(origin_identifier, database_name,
                              database_description, estimated_size,
                              &database_size);

  database_connections_.AddConnection(origin_identifier, database_name);

  GetWebDatabase().UpdateSize(origin, database_name, database_size);
}

void WebDatabaseHostImpl::Modified(const url::Origin& origin,
                                   const base::string16& database_name) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  ValidateOrigin(origin, base::BindOnce(&WebDatabaseHostImpl::ModifiedValidated,
                                        weak_ptr_factory_.GetWeakPtr(), origin,
                                        database_name));
}

void WebDatabaseHostImpl::ModifiedValidated(
    const url::Origin& origin,
    const base::string16& database_name) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  std::string origin_identifier(storage::GetIdentifierFromOrigin(origin));
  if (!database_connections_.IsDatabaseOpened(origin_identifier,
                                              database_name)) {
    mojo::ReportBadMessage("Database not opened on modify");
    return;
  }

  db_tracker_->DatabaseModified(origin_identifier, database_name);
}

void WebDatabaseHostImpl::Closed(const url::Origin& origin,
                                 const base::string16& database_name) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  ValidateOrigin(origin, base::BindOnce(&WebDatabaseHostImpl::ClosedValidated,
                                        weak_ptr_factory_.GetWeakPtr(), origin,
                                        database_name));
}

void WebDatabaseHostImpl::ClosedValidated(const url::Origin& origin,
                                          const base::string16& database_name) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  std::string origin_identifier(storage::GetIdentifierFromOrigin(origin));
  if (!database_connections_.IsDatabaseOpened(origin_identifier,
                                              database_name)) {
    mojo::ReportBadMessage("Database not opened on close");
    return;
  }

  database_connections_.RemoveConnection(origin_identifier, database_name);
  db_tracker_->DatabaseClosed(origin_identifier, database_name);
}

void WebDatabaseHostImpl::HandleSqliteError(const url::Origin& origin,
                                            const base::string16& database_name,
                                            int32_t error) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  ValidateOrigin(
      origin,
      base::BindOnce(&storage::DatabaseTracker::HandleSqliteError, db_tracker_,
                     storage::GetIdentifierFromOrigin(origin), database_name,
                     error));
}

void WebDatabaseHostImpl::OnDatabaseSizeChanged(
    const std::string& origin_identifier,
    const base::string16& database_name,
    int64_t database_size) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  if (!database_connections_.IsOriginUsed(origin_identifier)) {
    return;
  }

  GetWebDatabase().UpdateSize(
      storage::GetOriginFromIdentifier(origin_identifier), database_name,
      database_size);
}

void WebDatabaseHostImpl::OnDatabaseScheduledForDeletion(
    const std::string& origin_identifier,
    const base::string16& database_name) {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());

  GetWebDatabase().CloseImmediately(
      storage::GetOriginFromIdentifier(origin_identifier), database_name);
}

blink::mojom::WebDatabase& WebDatabaseHostImpl::GetWebDatabase() {
  DCHECK(db_tracker_->task_runner()->RunsTasksInCurrentSequence());
  if (!database_provider_) {
    // The interface binding needs to occur on the UI thread, as we can
    // only call RenderProcessHost::FromID() on the UI thread.
    GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(
            [](int process_id,
               mojo::PendingReceiver<blink::mojom::WebDatabase> receiver) {
              RenderProcessHost* host = RenderProcessHost::FromID(process_id);
              if (host)
                host->BindReceiver(std::move(receiver));
            },
            process_id_, database_provider_.BindNewPipeAndPassReceiver()));
  }
  return *database_provider_.get();
}

void WebDatabaseHostImpl::ValidateOrigin(const url::Origin& origin,
                                         base::OnceClosure callback) {
  if (origin.opaque()) {
    mojo::ReportBadMessage("Invalid origin.");
    return;
  }

  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&ValidateOriginOnUIThread, process_id_, origin,
                     base::RetainedRef(db_tracker_->task_runner()),
                     std::move(callback), mojo::GetBadMessageCallback()));
}

void WebDatabaseHostImpl::ValidateOrigin(const base::string16& vfs_file_name,
                                         base::OnceClosure callback) {
  std::string origin_identifier;
  if (vfs_file_name.empty()) {
    std::move(callback).Run();
    return;
  }

  if (!DatabaseUtil::CrackVfsFileName(vfs_file_name, &origin_identifier,
                                      nullptr, nullptr)) {
    std::move(callback).Run();
    return;
  }

  ValidateOrigin(storage::GetOriginFromIdentifier(origin_identifier),
                 std::move(callback));
}

}  // namespace content
