// Copyright 2014 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/cache_storage/cache_storage_manager.h"

#include <stdint.h>

#include <map>
#include <numeric>
#include <set>
#include <utility>

#include "base/barrier_closure.h"
#include "base/bind.h"
#include "base/containers/id_map.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/memory/ptr_util.h"
#include "base/sequenced_task_runner.h"
#include "base/sha1.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "content/browser/cache_storage/cache_storage.h"
#include "content/browser/cache_storage/cache_storage.pb.h"
#include "content/browser/cache_storage/cache_storage_quota_client.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "net/base/url_util.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/common/database/database_identifier.h"
#include "third_party/blink/public/mojom/quota/quota_types.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace content {

namespace {

bool DeleteDir(const base::FilePath& path) {
  return base::DeleteFile(path, true /* recursive */);
}

void DeleteOriginDidDeleteDir(storage::QuotaClient::DeletionCallback callback,
                              bool rv) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(callback),
                     rv ? blink::mojom::QuotaStatusCode::kOk
                        : blink::mojom::QuotaStatusCode::kErrorAbort));
}

// Calculate the sum of all cache sizes in this store, but only if all sizes are
// known. If one or more sizes are not known then return kSizeUnknown.
int64_t GetCacheStorageSize(const proto::CacheStorageIndex& index) {
  int64_t storage_size = 0;
  for (int i = 0, max = index.cache_size(); i < max; ++i) {
    const proto::CacheStorageIndex::Cache& cache = index.cache(i);
    if (!cache.has_size() || cache.size() == CacheStorage::kSizeUnknown)
      return CacheStorage::kSizeUnknown;
    storage_size += cache.size();
  }
  return storage_size;
}

// Open the various cache directories' index files and extract their origins,
// sizes (if current), and last modified times.
void ListOriginsAndLastModifiedOnTaskRunner(
    std::vector<CacheStorageUsageInfo>* usages,
    base::FilePath root_path,
    CacheStorageOwner owner) {
  base::FileEnumerator file_enum(root_path, false /* recursive */,
                                 base::FileEnumerator::DIRECTORIES);

  base::FilePath path;
  while (!(path = file_enum.Next()).empty()) {
    base::FilePath index_path = path.AppendASCII(CacheStorage::kIndexFileName);
    base::File::Info file_info;
    base::Time index_last_modified;
    if (GetFileInfo(index_path, &file_info))
      index_last_modified = file_info.last_modified;
    std::string protobuf;
    base::ReadFileToString(path.AppendASCII(CacheStorage::kIndexFileName),
                           &protobuf);
    proto::CacheStorageIndex index;
    if (index.ParseFromString(protobuf)) {
      if (index.has_origin()) {
        if (path ==
            CacheStorageManager::ConstructOriginPath(
                root_path, url::Origin::Create(GURL(index.origin())), owner)) {
          if (base::GetFileInfo(path, &file_info)) {
            int64_t storage_size = CacheStorage::kSizeUnknown;
            if (file_info.last_modified < index_last_modified)
              storage_size = GetCacheStorageSize(index);
            usages->push_back(CacheStorageUsageInfo(
                GURL(index.origin()), storage_size, file_info.last_modified));
          }
        }
      }
    }
  }
}

std::set<url::Origin> ListOriginsOnTaskRunner(base::FilePath root_path,
                                              CacheStorageOwner owner) {
  std::vector<CacheStorageUsageInfo> usages;
  ListOriginsAndLastModifiedOnTaskRunner(&usages, root_path, owner);

  std::set<url::Origin> out_origins;
  for (const CacheStorageUsageInfo& usage : usages)
    out_origins.insert(url::Origin::Create(usage.origin));

  return out_origins;
}

void GetOriginsForHostDidListOrigins(
    const std::string& host,
    storage::QuotaClient::GetOriginsCallback callback,
    const std::set<url::Origin>& origins) {
  std::set<url::Origin> out_origins;
  for (const url::Origin& origin : origins) {
    if (host == net::GetHostOrSpecFromURL(origin.GetURL()))
      out_origins.insert(origin);
  }
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), out_origins));
}

void AllOriginSizesReported(
    std::unique_ptr<std::vector<CacheStorageUsageInfo>> usages,
    CacheStorageContext::GetUsageInfoCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), *usages));
}

void OneOriginSizeReported(base::OnceClosure callback,
                           CacheStorageUsageInfo* usage,
                           int64_t size) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  DCHECK_NE(size, CacheStorage::kSizeUnknown);
  usage->total_size_bytes = size;
  base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, std::move(callback));
}

}  // namespace

// static
scoped_refptr<CacheStorageManager> CacheStorageManager::Create(
    const base::FilePath& path,
    scoped_refptr<base::SequencedTaskRunner> cache_task_runner,
    scoped_refptr<storage::QuotaManagerProxy> quota_manager_proxy) {
  base::FilePath root_path = path;
  if (!path.empty()) {
    root_path = path.Append(ServiceWorkerContextCore::kServiceWorkerDirectory)
                    .AppendASCII("CacheStorage");
  }

  return base::WrapRefCounted(new CacheStorageManager(
      root_path, std::move(cache_task_runner), std::move(quota_manager_proxy)));
}

// static
scoped_refptr<CacheStorageManager> CacheStorageManager::Create(
    CacheStorageManager* old_manager) {
  scoped_refptr<CacheStorageManager> manager(new CacheStorageManager(
      old_manager->root_path(), old_manager->cache_task_runner(),
      old_manager->quota_manager_proxy_.get()));
  // These values may be NULL, in which case this will be called again later by
  // the dispatcher host per usual.
  manager->SetBlobParametersForCache(old_manager->url_request_context_getter(),
                                     old_manager->blob_storage_context());
  return manager;
}

CacheStorageManager::~CacheStorageManager() = default;

void CacheStorageManager::OpenCache(
    const url::Origin& origin,
    CacheStorageOwner owner,
    const std::string& cache_name,
    CacheStorage::CacheAndErrorCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  CacheStorage* cache_storage = FindOrCreateCacheStorage(origin, owner);

  cache_storage->OpenCache(cache_name, std::move(callback));
}

void CacheStorageManager::HasCache(
    const url::Origin& origin,
    CacheStorageOwner owner,
    const std::string& cache_name,
    CacheStorage::BoolAndErrorCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  CacheStorage* cache_storage = FindOrCreateCacheStorage(origin, owner);
  cache_storage->HasCache(cache_name, std::move(callback));
}

void CacheStorageManager::DeleteCache(const url::Origin& origin,
                                      CacheStorageOwner owner,
                                      const std::string& cache_name,
                                      CacheStorage::ErrorCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  CacheStorage* cache_storage = FindOrCreateCacheStorage(origin, owner);
  cache_storage->DoomCache(cache_name, std::move(callback));
}

void CacheStorageManager::EnumerateCaches(
    const url::Origin& origin,
    CacheStorageOwner owner,
    CacheStorage::IndexCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  CacheStorage* cache_storage = FindOrCreateCacheStorage(origin, owner);

  cache_storage->EnumerateCaches(std::move(callback));
}

void CacheStorageManager::MatchCache(
    const url::Origin& origin,
    CacheStorageOwner owner,
    const std::string& cache_name,
    std::unique_ptr<ServiceWorkerFetchRequest> request,
    blink::mojom::QueryParamsPtr match_params,
    CacheStorageCache::ResponseCallback callback) {
  CacheStorage* cache_storage = FindOrCreateCacheStorage(origin, owner);

  cache_storage->MatchCache(cache_name, std::move(request),
                            std::move(match_params), std::move(callback));
}

void CacheStorageManager::MatchAllCaches(
    const url::Origin& origin,
    CacheStorageOwner owner,
    std::unique_ptr<ServiceWorkerFetchRequest> request,
    blink::mojom::QueryParamsPtr match_params,
    CacheStorageCache::ResponseCallback callback) {
  CacheStorage* cache_storage = FindOrCreateCacheStorage(origin, owner);

  cache_storage->MatchAllCaches(std::move(request), std::move(match_params),
                                std::move(callback));
}

void CacheStorageManager::WriteToCache(
    const url::Origin& origin,
    CacheStorageOwner owner,
    const std::string& cache_name,
    std::unique_ptr<ServiceWorkerFetchRequest> request,
    std::unique_ptr<ServiceWorkerResponse> response,
    CacheStorage::ErrorCallback callback) {
  // Cache API should write through the dispatcher.
  DCHECK_NE(owner, CacheStorageOwner::kCacheAPI);

  CacheStorage* cache_storage = FindOrCreateCacheStorage(origin, owner);

  cache_storage->WriteToCache(cache_name, std::move(request),
                              std::move(response), std::move(callback));
}

void CacheStorageManager::SetBlobParametersForCache(
    scoped_refptr<net::URLRequestContextGetter> request_context_getter,
    base::WeakPtr<storage::BlobStorageContext> blob_storage_context) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  DCHECK(cache_storage_map_.empty());
  DCHECK(!request_context_getter_ ||
         request_context_getter_.get() == request_context_getter.get());
  DCHECK(!blob_context_ || blob_context_.get() == blob_storage_context.get());
  request_context_getter_ = std::move(request_context_getter);
  blob_context_ = blob_storage_context;
}

void CacheStorageManager::AddObserver(
    CacheStorageContextImpl::Observer* observer) {
  DCHECK(!observers_.HasObserver(observer));
  observers_.AddObserver(observer);
}

void CacheStorageManager::RemoveObserver(
    CacheStorageContextImpl::Observer* observer) {
  observers_.RemoveObserver(observer);
}

void CacheStorageManager::NotifyCacheListChanged(const url::Origin& origin) {
  for (auto& observer : observers_)
    observer.OnCacheListChanged(origin);
}

void CacheStorageManager::NotifyCacheContentChanged(const url::Origin& origin,
                                                    const std::string& name) {
  for (auto& observer : observers_)
    observer.OnCacheContentChanged(origin, name);
}

void CacheStorageManager::GetAllOriginsUsage(
    CacheStorageOwner owner,
    CacheStorageContext::GetUsageInfoCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  std::unique_ptr<std::vector<CacheStorageUsageInfo>> usages(
      new std::vector<CacheStorageUsageInfo>());

  if (IsMemoryBacked()) {
    for (const auto& origin_details : cache_storage_map_) {
      if (origin_details.first.second != owner)
        continue;
      usages->push_back(CacheStorageUsageInfo(
          origin_details.first.first.GetURL(), 0 /* size */,
          base::Time() /* last modified */));
    }
    GetAllOriginsUsageGetSizes(std::move(usages), std::move(callback));
    return;
  }

  std::vector<CacheStorageUsageInfo>* usages_ptr = usages.get();
  cache_task_runner_->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(&ListOriginsAndLastModifiedOnTaskRunner, usages_ptr,
                     root_path_, owner),
      base::BindOnce(&CacheStorageManager::GetAllOriginsUsageGetSizes,
                     weak_ptr_factory_.GetWeakPtr(), std::move(usages),
                     std::move(callback)));
}

void CacheStorageManager::GetAllOriginsUsageGetSizes(
    std::unique_ptr<std::vector<CacheStorageUsageInfo>> usages,
    CacheStorageContext::GetUsageInfoCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  DCHECK(usages);

  // The origin GURL and last modified times are set in |usages| but not the
  // size in bytes. Call each CacheStorage's Size() function to fill that out.
  std::vector<CacheStorageUsageInfo>* usages_ptr = usages.get();

  if (usages->empty()) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), *usages));
    return;
  }

  base::RepeatingClosure barrier_closure = base::BarrierClosure(
      usages_ptr->size(),
      base::BindOnce(&AllOriginSizesReported, std::move(usages),
                     std::move(callback)));

  for (CacheStorageUsageInfo& usage : *usages_ptr) {
    if (usage.total_size_bytes != CacheStorage::kSizeUnknown) {
      base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, barrier_closure);
      continue;
    }
    CacheStorage* cache_storage = FindOrCreateCacheStorage(
        url::Origin::Create(usage.origin), CacheStorageOwner::kCacheAPI);
    cache_storage->Size(
        base::BindOnce(&OneOriginSizeReported, barrier_closure, &usage));
  }
}

void CacheStorageManager::GetOriginUsage(
    const url::Origin& origin,
    CacheStorageOwner owner,
    storage::QuotaClient::GetUsageCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  CacheStorage* cache_storage = FindOrCreateCacheStorage(origin, owner);

  cache_storage->Size(std::move(callback));
}

void CacheStorageManager::GetOrigins(
    CacheStorageOwner owner,
    storage::QuotaClient::GetOriginsCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  if (IsMemoryBacked()) {
    std::set<url::Origin> origins;
    for (const auto& key_value : cache_storage_map_)
      if (key_value.first.second == owner)
        origins.insert(key_value.first.first);

    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), origins));
    return;
  }

  PostTaskAndReplyWithResult(
      cache_task_runner_.get(), FROM_HERE,
      base::BindOnce(&ListOriginsOnTaskRunner, root_path_, owner),
      std::move(callback));
}

void CacheStorageManager::GetOriginsForHost(
    const std::string& host,
    CacheStorageOwner owner,
    storage::QuotaClient::GetOriginsCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  if (IsMemoryBacked()) {
    std::set<url::Origin> origins;
    for (const auto& key_value : cache_storage_map_) {
      if (key_value.first.second != owner)
        continue;
      if (host == net::GetHostOrSpecFromURL(key_value.first.first.GetURL()))
        origins.insert(key_value.first.first);
    }
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), origins));
    return;
  }

  PostTaskAndReplyWithResult(
      cache_task_runner_.get(), FROM_HERE,
      base::BindOnce(&ListOriginsOnTaskRunner, root_path_, owner),
      base::BindOnce(&GetOriginsForHostDidListOrigins, host,
                     std::move(callback)));
}

void CacheStorageManager::DeleteOriginData(
    const url::Origin& origin,
    CacheStorageOwner owner,
    storage::QuotaClient::DeletionCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  // Create the CacheStorage for the origin if it hasn't been loaded yet.
  FindOrCreateCacheStorage(origin, owner);

  CacheStorageMap::iterator it = cache_storage_map_.find({origin, owner});
  DCHECK(it != cache_storage_map_.end());

  CacheStorage* cache_storage = it->second.release();
  cache_storage->ResetManager();
  cache_storage_map_.erase({origin, owner});
  cache_storage->GetSizeThenCloseAllCaches(
      base::BindOnce(&CacheStorageManager::DeleteOriginDidClose,
                     weak_ptr_factory_.GetWeakPtr(), origin, owner,
                     std::move(callback), base::WrapUnique(cache_storage)));
}

void CacheStorageManager::DeleteOriginData(const url::Origin& origin,
                                           CacheStorageOwner owner) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  DeleteOriginData(origin, owner, base::DoNothing());
}

void CacheStorageManager::DeleteOriginDidClose(
    const url::Origin& origin,
    CacheStorageOwner owner,
    storage::QuotaClient::DeletionCallback callback,
    std::unique_ptr<CacheStorage> cache_storage,
    int64_t origin_size) {
  // TODO(jkarlin): Deleting the storage leaves any unfinished operations
  // hanging, resulting in unresolved promises. Fix this by returning early from
  // CacheStorage operations posted after GetSizeThenCloseAllCaches is called.
  cache_storage.reset();

  quota_manager_proxy_->NotifyStorageModified(
      CacheStorageQuotaClient::GetIDFromOwner(owner), origin,
      blink::mojom::StorageType::kTemporary, -1 * origin_size);

  if (owner == CacheStorageOwner::kCacheAPI)
    NotifyCacheListChanged(origin);

  if (IsMemoryBacked()) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback),
                                  blink::mojom::QuotaStatusCode::kOk));
    return;
  }

  PostTaskAndReplyWithResult(
      cache_task_runner_.get(), FROM_HERE,
      base::BindOnce(&DeleteDir,
                     ConstructOriginPath(root_path_, origin, owner)),
      base::BindOnce(&DeleteOriginDidDeleteDir, std::move(callback)));
}

CacheStorageManager::CacheStorageManager(
    const base::FilePath& path,
    scoped_refptr<base::SequencedTaskRunner> cache_task_runner,
    scoped_refptr<storage::QuotaManagerProxy> quota_manager_proxy)
    : root_path_(path),
      cache_task_runner_(std::move(cache_task_runner)),
      quota_manager_proxy_(std::move(quota_manager_proxy)),
      weak_ptr_factory_(this) {
  if (quota_manager_proxy_.get()) {
    quota_manager_proxy_->RegisterClient(new CacheStorageQuotaClient(
        weak_ptr_factory_.GetWeakPtr(), CacheStorageOwner::kCacheAPI));
    quota_manager_proxy_->RegisterClient(new CacheStorageQuotaClient(
        weak_ptr_factory_.GetWeakPtr(), CacheStorageOwner::kBackgroundFetch));
  }
}

CacheStorage* CacheStorageManager::FindOrCreateCacheStorage(
    const url::Origin& origin,
    CacheStorageOwner owner) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  DCHECK(request_context_getter_);
  CacheStorageMap::const_iterator it = cache_storage_map_.find({origin, owner});
  if (it == cache_storage_map_.end()) {
    CacheStorage* cache_storage = new CacheStorage(
        ConstructOriginPath(root_path_, origin, owner), IsMemoryBacked(),
        cache_task_runner_.get(), request_context_getter_, quota_manager_proxy_,
        blob_context_, this, origin, owner);
    cache_storage_map_[{origin, owner}] = base::WrapUnique(cache_storage);
    return cache_storage;
  }
  return it->second.get();
}

// static
base::FilePath CacheStorageManager::ConstructOriginPath(
    const base::FilePath& root_path,
    const url::Origin& origin,
    CacheStorageOwner owner) {
  std::string identifier = storage::GetIdentifierFromOrigin(origin);
  if (owner != CacheStorageOwner::kCacheAPI) {
    identifier += "-" + std::to_string(static_cast<int>(owner));
  }
  const std::string origin_hash = base::SHA1HashString(identifier);
  const std::string origin_hash_hex = base::ToLowerASCII(
      base::HexEncode(origin_hash.c_str(), origin_hash.length()));
  return root_path.AppendASCII(origin_hash_hex);
}

}  // namespace content
