// Copyright 2016 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/devtools/protocol/storage_handler.h"

#include <memory>
#include <unordered_set>
#include <utility>
#include <vector>

#include "base/strings/string_split.h"
#include "content/browser/cache_storage/cache_storage_context_impl.h"
#include "content/browser/indexed_db/indexed_db_context_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "storage/browser/quota/quota_client.h"
#include "storage/browser/quota/quota_manager.h"
#include "third_party/blink/public/mojom/quota/quota_types.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace content {
namespace protocol {

namespace {
Storage::StorageType GetTypeName(storage::QuotaClient::ID id) {
  switch (id) {
    case storage::QuotaClient::kFileSystem:
      return Storage::StorageTypeEnum::File_systems;
    case storage::QuotaClient::kDatabase:
      return Storage::StorageTypeEnum::Websql;
    case storage::QuotaClient::kAppcache:
      return Storage::StorageTypeEnum::Appcache;
    case storage::QuotaClient::kIndexedDatabase:
      return Storage::StorageTypeEnum::Indexeddb;
    case storage::QuotaClient::kServiceWorkerCache:
      return Storage::StorageTypeEnum::Cache_storage;
    case storage::QuotaClient::kServiceWorker:
      return Storage::StorageTypeEnum::Service_workers;
    default:
      return Storage::StorageTypeEnum::Other;
  }
}

void ReportUsageAndQuotaDataOnUIThread(
    std::unique_ptr<StorageHandler::GetUsageAndQuotaCallback> callback,
    blink::mojom::QuotaStatusCode code,
    int64_t usage,
    int64_t quota,
    base::flat_map<storage::QuotaClient::ID, int64_t> usage_breakdown) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (code != blink::mojom::QuotaStatusCode::kOk) {
    return callback->sendFailure(
        Response::Error("Quota information is not available"));
  }

  std::unique_ptr<Array<Storage::UsageForType>> usageList =
      Array<Storage::UsageForType>::create();
  for (const auto& specific_usage : usage_breakdown) {
    std::unique_ptr<Storage::UsageForType> entry =
        Storage::UsageForType::Create()
            .SetStorageType(GetTypeName(specific_usage.first))
            .SetUsage(specific_usage.second)
            .Build();
    usageList->addItem(std::move(entry));
  }
  callback->sendSuccess(usage, quota, std::move(usageList));
}

void GotUsageAndQuotaDataCallback(
    std::unique_ptr<StorageHandler::GetUsageAndQuotaCallback> callback,
    blink::mojom::QuotaStatusCode code,
    int64_t usage,
    int64_t quota,
    base::flat_map<storage::QuotaClient::ID, int64_t> usage_breakdown) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::BindOnce(ReportUsageAndQuotaDataOnUIThread, std::move(callback),
                     code, usage, quota, std::move(usage_breakdown)));
}

void GetUsageAndQuotaOnIOThread(
    storage::QuotaManager* manager,
    const GURL& url,
    std::unique_ptr<StorageHandler::GetUsageAndQuotaCallback> callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  manager->GetUsageAndQuotaWithBreakdown(
      url, blink::mojom::StorageType::kTemporary,
      base::BindOnce(&GotUsageAndQuotaDataCallback, std::move(callback)));
}
}  // namespace

// Observer that listens on the IO thread for cache storage notifications and
// informs the StorageHandler on the UI thread for origins of interest.
// Created on the UI thread but predominantly used and deleted on the IO thread.
// Registered on creation as an observer in CacheStorageContextImpl,
// unregistered on destruction.
class StorageHandler::CacheStorageObserver : CacheStorageContextImpl::Observer {
 public:
  CacheStorageObserver(base::WeakPtr<StorageHandler> owner_storage_handler,
                       CacheStorageContextImpl* cache_storage_context)
      : owner_(owner_storage_handler), context_(cache_storage_context) {
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::BindOnce(&CacheStorageObserver::AddObserverOnIOThread,
                       base::Unretained(this)));
  }

  ~CacheStorageObserver() override {
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    context_->RemoveObserver(this);
  }

  void TrackOriginOnIOThread(const url::Origin& origin) {
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    if (origins_.find(origin) != origins_.end())
      return;
    origins_.insert(origin);
  }

  void UntrackOriginOnIOThread(const url::Origin& origin) {
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    origins_.erase(origin);
  }

  void OnCacheListChanged(const url::Origin& origin) override {
    auto found = origins_.find(origin);
    if (found == origins_.end())
      return;
    BrowserThread::PostTask(
        BrowserThread::UI, FROM_HERE,
        base::BindOnce(&StorageHandler::NotifyCacheStorageListChanged, owner_,
                       origin.Serialize()));
  }

  void OnCacheContentChanged(const url::Origin& origin,
                             const std::string& cache_name) override {
    if (origins_.find(origin) == origins_.end())
      return;
    BrowserThread::PostTask(
        BrowserThread::UI, FROM_HERE,
        base::BindOnce(&StorageHandler::NotifyCacheStorageContentChanged,
                       owner_, origin.Serialize(), cache_name));
  }

 private:
  void AddObserverOnIOThread() {
    DCHECK_CURRENTLY_ON(BrowserThread::IO);
    context_->AddObserver(this);
  }

  // Maintained on the IO thread to avoid thread contention.
  base::flat_set<url::Origin> origins_;

  base::WeakPtr<StorageHandler> owner_;
  scoped_refptr<CacheStorageContextImpl> context_;

  DISALLOW_COPY_AND_ASSIGN(CacheStorageObserver);
};

// Observer that listens on the IDB thread for IndexedDB notifications and
// informs the StorageHandler on the UI thread for origins of interest.
// Created on the UI thread but predominantly used and deleted on the IDB
// thread.
// Registered on creation as an observer in IndexedDBContextImpl, unregistered
// on destruction.
class StorageHandler::IndexedDBObserver : IndexedDBContextImpl::Observer {
 public:
  IndexedDBObserver(base::WeakPtr<StorageHandler> owner_storage_handler,
                    IndexedDBContextImpl* indexed_db_context)
      : owner_(owner_storage_handler), context_(indexed_db_context) {
    context_->TaskRunner()->PostTask(
        FROM_HERE, base::BindOnce(&IndexedDBObserver::AddObserverOnIDBThread,
                                  base::Unretained(this)));
  }

  ~IndexedDBObserver() override {
    DCHECK(context_->TaskRunner()->RunsTasksInCurrentSequence());
    context_->RemoveObserver(this);
  }

  void TrackOriginOnIDBThread(const url::Origin& origin) {
    DCHECK(context_->TaskRunner()->RunsTasksInCurrentSequence());
    if (origins_.find(origin) != origins_.end())
      return;
    origins_.insert(origin);
  }

  void UntrackOriginOnIDBThread(const url::Origin& origin) {
    DCHECK(context_->TaskRunner()->RunsTasksInCurrentSequence());
    origins_.erase(origin);
  }

  void OnIndexedDBListChanged(const url::Origin& origin) override {
    auto found = origins_.find(origin);
    if (found == origins_.end())
      return;
    BrowserThread::PostTask(
        BrowserThread::UI, FROM_HERE,
        base::BindOnce(&StorageHandler::NotifyIndexedDBListChanged, owner_,
                       origin.Serialize()));
  }

  void OnIndexedDBContentChanged(
      const url::Origin& origin,
      const base::string16& database_name,
      const base::string16& object_store_name) override {
    auto found = origins_.find(origin);
    if (found == origins_.end())
      return;
    BrowserThread::PostTask(
        BrowserThread::UI, FROM_HERE,
        base::BindOnce(&StorageHandler::NotifyIndexedDBContentChanged, owner_,
                       origin.Serialize(), database_name, object_store_name));
  }

  base::SequencedTaskRunner* TaskRunner() const {
    return context_->TaskRunner();
  }

 private:
  void AddObserverOnIDBThread() {
    DCHECK(context_->TaskRunner()->RunsTasksInCurrentSequence());
    context_->AddObserver(this);
  }

  // Maintained on the IDB thread to avoid thread contention.
  base::flat_set<url::Origin> origins_;

  base::WeakPtr<StorageHandler> owner_;
  scoped_refptr<IndexedDBContextImpl> context_;

  DISALLOW_COPY_AND_ASSIGN(IndexedDBObserver);
};

StorageHandler::StorageHandler()
    : DevToolsDomainHandler(Storage::Metainfo::domainName),
      storage_partition_(nullptr),
      weak_ptr_factory_(this) {}

StorageHandler::~StorageHandler() {
  DCHECK(!cache_storage_observer_);
  DCHECK(!indexed_db_observer_);
}

void StorageHandler::Wire(UberDispatcher* dispatcher) {
  frontend_ = std::make_unique<Storage::Frontend>(dispatcher->channel());
  Storage::Dispatcher::wire(dispatcher, this);
}

void StorageHandler::SetRenderer(int process_host_id,
                                 RenderFrameHostImpl* frame_host) {
  RenderProcessHost* process = RenderProcessHost::FromID(process_host_id);
  storage_partition_ = process ? process->GetStoragePartition() : nullptr;
}

Response StorageHandler::Disable() {
  if (cache_storage_observer_) {
    BrowserThread::DeleteSoon(BrowserThread::IO, FROM_HERE,
                              cache_storage_observer_.release());
  }
  if (indexed_db_observer_) {
    scoped_refptr<base::SequencedTaskRunner> observer_task_runner =
        indexed_db_observer_->TaskRunner();
    observer_task_runner->DeleteSoon(FROM_HERE,
                                     std::move(indexed_db_observer_));
  }

  return Response::OK();
}

void StorageHandler::ClearDataForOrigin(
    const std::string& origin,
    const std::string& storage_types,
    std::unique_ptr<ClearDataForOriginCallback> callback) {
  if (!storage_partition_)
    return callback->sendFailure(Response::InternalError());

  std::vector<std::string> types = base::SplitString(
      storage_types, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  std::unordered_set<std::string> set(types.begin(), types.end());
  uint32_t remove_mask = 0;
  if (set.count(Storage::StorageTypeEnum::Appcache))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_APPCACHE;
  if (set.count(Storage::StorageTypeEnum::Cookies))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_COOKIES;
  if (set.count(Storage::StorageTypeEnum::File_systems))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_FILE_SYSTEMS;
  if (set.count(Storage::StorageTypeEnum::Indexeddb))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_INDEXEDDB;
  if (set.count(Storage::StorageTypeEnum::Local_storage))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_LOCAL_STORAGE;
  if (set.count(Storage::StorageTypeEnum::Shader_cache))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_SHADER_CACHE;
  if (set.count(Storage::StorageTypeEnum::Websql))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_WEBSQL;
  if (set.count(Storage::StorageTypeEnum::Service_workers))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_SERVICE_WORKERS;
  if (set.count(Storage::StorageTypeEnum::Cache_storage))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_CACHE_STORAGE;
  if (set.count(Storage::StorageTypeEnum::All))
    remove_mask |= StoragePartition::REMOVE_DATA_MASK_ALL;

  if (!remove_mask) {
    return callback->sendFailure(
        Response::InvalidParams("No valid storage type specified"));
  }

  storage_partition_->ClearData(
      remove_mask, StoragePartition::QUOTA_MANAGED_STORAGE_MASK_ALL,
      GURL(origin), StoragePartition::OriginMatcherFunction(), base::Time(),
      base::Time::Max(),
      base::BindOnce(&ClearDataForOriginCallback::sendSuccess,
                     std::move(callback)));
}

void StorageHandler::GetUsageAndQuota(
    const String& origin,
    std::unique_ptr<GetUsageAndQuotaCallback> callback) {
  if (!storage_partition_)
    return callback->sendFailure(Response::InternalError());

  GURL origin_url(origin);
  if (!origin_url.is_valid()) {
    return callback->sendFailure(
        Response::Error(origin + " is not a valid URL"));
  }

  storage::QuotaManager* manager = storage_partition_->GetQuotaManager();
  BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      base::BindOnce(&GetUsageAndQuotaOnIOThread, base::RetainedRef(manager),
                     origin_url, std::move(callback)));
}

Response StorageHandler::TrackCacheStorageForOrigin(const std::string& origin) {
  if (!storage_partition_)
    return Response::InternalError();

  GURL origin_url(origin);
  if (!origin_url.is_valid())
    return Response::InvalidParams(origin + " is not a valid URL");

  BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      base::BindOnce(&CacheStorageObserver::TrackOriginOnIOThread,
                     base::Unretained(GetCacheStorageObserver()),
                     url::Origin::Create(origin_url)));
  return Response::OK();
}

Response StorageHandler::UntrackCacheStorageForOrigin(
    const std::string& origin) {
  if (!storage_partition_)
    return Response::InternalError();

  GURL origin_url(origin);
  if (!origin_url.is_valid())
    return Response::InvalidParams(origin + " is not a valid URL");

  BrowserThread::PostTask(
      BrowserThread::IO, FROM_HERE,
      base::BindOnce(&CacheStorageObserver::UntrackOriginOnIOThread,
                     base::Unretained(GetCacheStorageObserver()),
                     url::Origin::Create(origin_url)));
  return Response::OK();
}

Response StorageHandler::TrackIndexedDBForOrigin(const std::string& origin) {
  if (!storage_partition_)
    return Response::InternalError();

  GURL origin_url(origin);
  if (!origin_url.is_valid())
    return Response::InvalidParams(origin + " is not a valid URL");

  GetIndexedDBObserver()->TaskRunner()->PostTask(
      FROM_HERE, base::BindOnce(&IndexedDBObserver::TrackOriginOnIDBThread,
                                base::Unretained(GetIndexedDBObserver()),
                                url::Origin::Create(origin_url)));
  return Response::OK();
}

Response StorageHandler::UntrackIndexedDBForOrigin(const std::string& origin) {
  if (!storage_partition_)
    return Response::InternalError();

  GURL origin_url(origin);
  if (!origin_url.is_valid())
    return Response::InvalidParams(origin + " is not a valid URL");

  GetIndexedDBObserver()->TaskRunner()->PostTask(
      FROM_HERE, base::BindOnce(&IndexedDBObserver::UntrackOriginOnIDBThread,
                                base::Unretained(GetIndexedDBObserver()),
                                url::Origin::Create(origin_url)));
  return Response::OK();
}

StorageHandler::CacheStorageObserver*
StorageHandler::GetCacheStorageObserver() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!cache_storage_observer_) {
    cache_storage_observer_ = std::make_unique<CacheStorageObserver>(
        weak_ptr_factory_.GetWeakPtr(),
        static_cast<CacheStorageContextImpl*>(
            storage_partition_->GetCacheStorageContext()));
  }
  return cache_storage_observer_.get();
}

StorageHandler::IndexedDBObserver* StorageHandler::GetIndexedDBObserver() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!indexed_db_observer_) {
    indexed_db_observer_ = std::make_unique<IndexedDBObserver>(
        weak_ptr_factory_.GetWeakPtr(),
        static_cast<IndexedDBContextImpl*>(
            storage_partition_->GetIndexedDBContext()));
  }
  return indexed_db_observer_.get();
}

void StorageHandler::NotifyCacheStorageListChanged(const std::string& origin) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  frontend_->CacheStorageListUpdated(origin);
}

void StorageHandler::NotifyCacheStorageContentChanged(const std::string& origin,
                                                      const std::string& name) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  frontend_->CacheStorageContentUpdated(origin, name);
}

void StorageHandler::NotifyIndexedDBListChanged(const std::string& origin) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  frontend_->IndexedDBListUpdated(origin);
}

void StorageHandler::NotifyIndexedDBContentChanged(
    const std::string& origin,
    const base::string16& database_name,
    const base::string16& object_store_name) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  frontend_->IndexedDBContentUpdated(origin, base::UTF16ToUTF8(database_name),
                                     base::UTF16ToUTF8(object_store_name));
}

}  // namespace protocol
}  // namespace content
