// Copyright 2015 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/appcache/appcache_internals_ui.h"

#include <stddef.h>
#include <vector>

#include "base/bind.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/threading/platform_thread.h"
#include "base/values.h"
#include "content/browser/appcache/appcache.h"
#include "content/browser/appcache/appcache_response.h"
#include "content/browser/storage_partition_impl.h"
#include "content/grit/content_resources.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/common/url_constants.h"
#include "net/base/escape.h"
#include "net/http/http_response_headers.h"
#include "net/url_request/view_cache_helper.h"
#include "third_party/blink/public/mojom/appcache/appcache.mojom.h"
#include "third_party/blink/public/mojom/appcache/appcache_info.mojom.h"

namespace content {

namespace {
const char kRequestGetAllAppCacheInfo[] = "getAllAppCache";
const char kRequestDeleteAppCache[] = "deleteAppCache";
const char kRequestGetAppCacheDetails[] = "getAppCacheDetails";
const char kRequestGetFileDetails[] = "getFileDetails";

const char kFunctionOnAllAppCacheInfoReady[] =
    "appcache.onAllAppCacheInfoReady";
const char kFunctionOnAppCacheInfoDeleted[] = "appcache.onAppCacheInfoDeleted";
const char kFunctionOnAppCacheDetailsReady[] =
    "appcache.onAppCacheDetailsReady";
const char kFunctionOnFileDetailsReady[] = "appcache.onFileDetailsReady";
const char kFunctionOnFileDetailsFailed[] = "appcache.onFileDetailsFailed";

int64_t ToInt64(const std::string& str) {
  int64_t i = 0;
  base::StringToInt64(str.c_str(), &i);
  return i;
}

bool SortByResourceUrl(const blink::mojom::AppCacheResourceInfo& lhs,
                       const blink::mojom::AppCacheResourceInfo& rhs) {
  return lhs.url.spec() < rhs.url.spec();
}

std::unique_ptr<base::DictionaryValue> GetDictionaryValueForResponseEnquiry(
    const content::AppCacheInternalsUI::Proxy::ResponseEnquiry&
        response_enquiry) {
  std::unique_ptr<base::DictionaryValue> dict_value(
      new base::DictionaryValue());
  dict_value->SetString("manifestURL", response_enquiry.manifest_url);
  dict_value->SetString("groupId",
                        base::Int64ToString(response_enquiry.group_id));
  dict_value->SetString("responseId",
                        base::Int64ToString(response_enquiry.response_id));
  return dict_value;
}

std::unique_ptr<base::DictionaryValue> GetDictionaryValueForAppCacheInfo(
    const blink::mojom::AppCacheInfo& appcache_info) {
  std::unique_ptr<base::DictionaryValue> dict_value(
      new base::DictionaryValue());
  dict_value->SetString("manifestURL", appcache_info.manifest_url.spec());
  dict_value->SetDouble("creationTime", appcache_info.creation_time.ToJsTime());
  dict_value->SetDouble("lastUpdateTime",
                        appcache_info.last_update_time.ToJsTime());
  dict_value->SetDouble("lastAccessTime",
                        appcache_info.last_access_time.ToJsTime());
  dict_value->SetString(
      "size",
      base::UTF16ToUTF8(base::FormatBytesUnlocalized(appcache_info.size)));
  dict_value->SetString("groupId", base::Int64ToString(appcache_info.group_id));

  return dict_value;
}

std::unique_ptr<base::ListValue> GetListValueForAppCacheInfoVector(
    const std::vector<blink::mojom::AppCacheInfo> appcache_info_vector) {
  std::unique_ptr<base::ListValue> list(new base::ListValue());
  for (const blink::mojom::AppCacheInfo& info : appcache_info_vector)
    list->Append(GetDictionaryValueForAppCacheInfo(info));
  return list;
}

std::unique_ptr<base::ListValue> GetListValueFromAppCacheInfoCollection(
    AppCacheInfoCollection* appcache_collection) {
  std::unique_ptr<base::ListValue> list(new base::ListValue());
  for (const auto& key_value : appcache_collection->infos_by_origin) {
    base::DictionaryValue* dict = new base::DictionaryValue;
    // Use GURL::spec() to keep consistency with previous version
    dict->SetString("originURL", key_value.first.GetURL().spec());
    dict->Set("manifests", GetListValueForAppCacheInfoVector(key_value.second));
    list->Append(std::unique_ptr<base::Value>(dict));
  }
  return list;
}

std::unique_ptr<base::DictionaryValue>
GetDictionaryValueForAppCacheResourceInfo(
    const blink::mojom::AppCacheResourceInfo& resource_info) {
  std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue);
  dict->SetString("url", resource_info.url.spec());
  dict->SetString(
      "size",
      base::UTF16ToUTF8(base::FormatBytesUnlocalized(resource_info.size)));
  dict->SetString("responseId", base::Int64ToString(resource_info.response_id));
  dict->SetBoolean("isExplicit", resource_info.is_explicit);
  dict->SetBoolean("isManifest", resource_info.is_manifest);
  dict->SetBoolean("isMaster", resource_info.is_master);
  dict->SetBoolean("isFallback", resource_info.is_fallback);
  dict->SetBoolean("isIntercept", resource_info.is_intercept);
  dict->SetBoolean("isForeign", resource_info.is_foreign);

  return dict;
}

std::unique_ptr<base::ListValue> GetListValueForAppCacheResourceInfoVector(
    std::vector<blink::mojom::AppCacheResourceInfo>* resource_info_vector) {
  std::unique_ptr<base::ListValue> list(new base::ListValue);
  for (const blink::mojom::AppCacheResourceInfo& res_info :
       *resource_info_vector)
    list->Append(GetDictionaryValueForAppCacheResourceInfo(res_info));
  return list;
}

}  // namespace

AppCacheInternalsUI::Proxy::Proxy(
    base::WeakPtr<AppCacheInternalsUI> appcache_internals_ui,
    const base::FilePath& partition_path)
    : appcache_internals_ui_(appcache_internals_ui),
      partition_path_(partition_path) {}

void AppCacheInternalsUI::Proxy::Initialize(
    const scoped_refptr<ChromeAppCacheService>& chrome_appcache_service) {
  if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) {
    base::PostTaskWithTraits(
        FROM_HERE, {BrowserThread::IO},
        base::BindOnce(&Proxy::Initialize, this, chrome_appcache_service));
    return;
  }
  appcache_service_ = chrome_appcache_service->AsWeakPtr();
  shutdown_called_ = false;
  preparing_response_ = false;
}

AppCacheInternalsUI::Proxy::~Proxy() {
  DCHECK(shutdown_called_);
}

void AppCacheInternalsUI::Proxy::Shutdown() {
  if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) {
    base::PostTaskWithTraits(FROM_HERE, {BrowserThread::IO},
                             base::BindOnce(&Proxy::Shutdown, this));
    return;
  }
  shutdown_called_ = true;
  if (appcache_service_) {
    appcache_service_->storage()->CancelDelegateCallbacks(this);
    appcache_service_.reset();
    response_enquiries_.clear();
  }
}

void AppCacheInternalsUI::Proxy::RequestAllAppCacheInfo() {
  if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) {
    base::PostTaskWithTraits(
        FROM_HERE, {BrowserThread::IO},
        base::BindOnce(&Proxy::RequestAllAppCacheInfo, this));
    return;
  }
  if (appcache_service_) {
    scoped_refptr<AppCacheInfoCollection> collection(
        new AppCacheInfoCollection());
    appcache_service_->GetAllAppCacheInfo(
        collection.get(),
        base::BindOnce(&Proxy::OnAllAppCacheInfoReady, this, collection));
  }
}

void AppCacheInternalsUI::Proxy::OnAllAppCacheInfoReady(
    scoped_refptr<AppCacheInfoCollection> collection,
    int net_result_code) {
  base::PostTaskWithTraits(
      FROM_HERE, {BrowserThread::UI},
      base::BindOnce(&AppCacheInternalsUI::OnAllAppCacheInfoReady,
                     appcache_internals_ui_, collection, partition_path_));
}

void AppCacheInternalsUI::Proxy::DeleteAppCache(
    const std::string& manifest_url) {
  if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) {
    base::PostTaskWithTraits(
        FROM_HERE, {BrowserThread::IO},
        base::BindOnce(&Proxy::DeleteAppCache, this, manifest_url));
    return;
  }
  if (appcache_service_) {
    appcache_service_->DeleteAppCacheGroup(
        GURL(manifest_url),
        base::BindOnce(&Proxy::OnAppCacheInfoDeleted, this, manifest_url));
  }
}

void AppCacheInternalsUI::Proxy::OnAppCacheInfoDeleted(
    const std::string& manifest_url,
    int net_result_code) {
  base::PostTaskWithTraits(
      FROM_HERE, {BrowserThread::UI},
      base::BindOnce(&AppCacheInternalsUI::OnAppCacheInfoDeleted,
                     appcache_internals_ui_, partition_path_, manifest_url,
                     net_result_code == net::OK));
}

void AppCacheInternalsUI::Proxy::RequestAppCacheDetails(
    const std::string& manifest_url) {
  if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) {
    base::PostTaskWithTraits(
        FROM_HERE, {BrowserThread::IO},
        base::BindOnce(&Proxy::RequestAppCacheDetails, this, manifest_url));
    return;
  }

  if (appcache_service_)
    appcache_service_->storage()->LoadOrCreateGroup(GURL(manifest_url), this);
}

void AppCacheInternalsUI::Proxy::OnGroupLoaded(AppCacheGroup* appcache_group,
                                               const GURL& manifest_gurl) {
  std::unique_ptr<std::vector<blink::mojom::AppCacheResourceInfo>>
      resource_info_vector;
  if (appcache_group && appcache_group->newest_complete_cache()) {
    resource_info_vector.reset(
        new std::vector<blink::mojom::AppCacheResourceInfo>);
    appcache_group->newest_complete_cache()->ToResourceInfoVector(
        resource_info_vector.get());
    std::sort(resource_info_vector->begin(), resource_info_vector->end(),
              SortByResourceUrl);
  }
  base::PostTaskWithTraits(
      FROM_HERE, {BrowserThread::UI},
      base::BindOnce(&AppCacheInternalsUI::OnAppCacheDetailsReady,
                     appcache_internals_ui_, partition_path_,
                     manifest_gurl.spec(), std::move(resource_info_vector)));
}

void AppCacheInternalsUI::Proxy::RequestFileDetails(
    const ResponseEnquiry& response_enquiry) {
  if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) {
    base::PostTaskWithTraits(
        FROM_HERE, {BrowserThread::IO},
        base::BindOnce(&Proxy::RequestFileDetails, this, response_enquiry));
    return;
  }
  DCHECK(!shutdown_called_);
  response_enquiries_.push_back(response_enquiry);
  HandleFileDetailsRequest();
}

void AppCacheInternalsUI::Proxy::HandleFileDetailsRequest() {
  if (preparing_response_ || response_enquiries_.empty() || !appcache_service_)
    return;
  preparing_response_ = true;
  appcache_service_->storage()->LoadResponseInfo(
      GURL(response_enquiries_.front().manifest_url),
      response_enquiries_.front().response_id, this);
}

void AppCacheInternalsUI::Proxy::OnResponseInfoLoaded(
    AppCacheResponseInfo* response,
    int64_t response_id) {
  if (shutdown_called_)
    return;
  if (!appcache_service_)
    return;
  ResponseEnquiry response_enquiry = response_enquiries_.front();
  response_enquiries_.pop_front();
  if (response) {
    scoped_refptr<AppCacheResponseInfo> response_info = response;
    const int64_t kLimit = 100 * 1000;
    int64_t amount_to_read =
        std::min(kLimit, response_info->response_data_size());
    scoped_refptr<net::IOBuffer> response_data =
        base::MakeRefCounted<net::IOBuffer>(
            base::checked_cast<size_t>(amount_to_read));
    std::unique_ptr<AppCacheResponseReader> reader =
        appcache_service_->storage()->CreateResponseReader(
            GURL(response_enquiry.manifest_url), response_enquiry.response_id);

    reader->ReadData(response_data.get(), amount_to_read,
                     base::BindOnce(&Proxy::OnResponseDataReadComplete, this,
                                    response_enquiry, response_info,
                                    std::move(reader), response_data));
  } else {
    OnResponseDataReadComplete(response_enquiry, nullptr, nullptr, nullptr, -1);
  }
}

void AppCacheInternalsUI::Proxy::OnResponseDataReadComplete(
    const ResponseEnquiry& response_enquiry,
    scoped_refptr<AppCacheResponseInfo> response_info,
    std::unique_ptr<AppCacheResponseReader> reader,
    scoped_refptr<net::IOBuffer> response_data,
    int net_result_code) {
  if (shutdown_called_)
    return;
  if (!response_info || net_result_code < 0) {
    base::PostTaskWithTraits(
        FROM_HERE, {BrowserThread::UI},
        base::BindOnce(&AppCacheInternalsUI::OnFileDetailsFailed,
                       appcache_internals_ui_, response_enquiry,
                       net_result_code));
  } else {
    base::PostTaskWithTraits(
        FROM_HERE, {BrowserThread::UI},
        base::BindOnce(&AppCacheInternalsUI::OnFileDetailsReady,
                       appcache_internals_ui_, response_enquiry, response_info,
                       response_data, net_result_code));
  }
  preparing_response_ = false;
  HandleFileDetailsRequest();
}

AppCacheInternalsUI::AppCacheInternalsUI(WebUI* web_ui)
    : WebUIController(web_ui), weak_ptr_factory_(this) {
  web_ui->RegisterMessageCallback(
      kRequestGetAllAppCacheInfo,
      base::BindRepeating(&AppCacheInternalsUI::GetAllAppCache, AsWeakPtr()));

  web_ui->RegisterMessageCallback(
      kRequestDeleteAppCache,
      base::BindRepeating(&AppCacheInternalsUI::DeleteAppCache, AsWeakPtr()));

  web_ui->RegisterMessageCallback(
      kRequestGetAppCacheDetails,
      base::BindRepeating(&AppCacheInternalsUI::GetAppCacheDetails,
                          AsWeakPtr()));

  web_ui->RegisterMessageCallback(
      kRequestGetFileDetails,
      base::BindRepeating(&AppCacheInternalsUI::GetFileDetails, AsWeakPtr()));

  WebUIDataSource* source =
      WebUIDataSource::Create(kChromeUIAppCacheInternalsHost);
  source->OverrideContentSecurityPolicyScriptSrc(
      "script-src chrome://resources 'self' 'unsafe-eval';");

  source->SetJsonPath("strings.js");
  source->AddResourcePath("appcache_internals.js", IDR_APPCACHE_INTERNALS_JS);
  source->AddResourcePath("appcache_internals.css", IDR_APPCACHE_INTERNALS_CSS);
  source->SetDefaultResource(IDR_APPCACHE_INTERNALS_HTML);
  source->UseGzip();

  WebUIDataSource::Add(browser_context(), source);

  BrowserContext::StoragePartitionCallback callback = base::BindRepeating(
      &AppCacheInternalsUI::CreateProxyForPartition, AsWeakPtr());
  BrowserContext::ForEachStoragePartition(browser_context(), callback);
}

AppCacheInternalsUI::~AppCacheInternalsUI() {
  for (auto& proxy : appcache_proxies_)
    proxy->Shutdown();
}

void AppCacheInternalsUI::CreateProxyForPartition(
    StoragePartition* storage_partition) {
  scoped_refptr<Proxy> proxy =
      new Proxy(weak_ptr_factory_.GetWeakPtr(), storage_partition->GetPath());
  proxy->Initialize(static_cast<StoragePartitionImpl*>(storage_partition)
                        ->GetAppCacheService());
  appcache_proxies_.push_back(proxy);
}

void AppCacheInternalsUI::GetAllAppCache(const base::ListValue* args) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  for (scoped_refptr<Proxy>& proxy : appcache_proxies_)
    proxy->RequestAllAppCacheInfo();
}

void AppCacheInternalsUI::DeleteAppCache(const base::ListValue* args) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  std::string manifest_url, partition_path;
  args->GetString(0, &partition_path);
  args->GetString(1, &manifest_url);
  Proxy* proxy =
      GetProxyForPartitionPath(base::FilePath::FromUTF8Unsafe(partition_path));
  if (proxy)
    proxy->DeleteAppCache(manifest_url);
}

void AppCacheInternalsUI::GetAppCacheDetails(const base::ListValue* args) {
  std::string manifest_url, partition_path;
  args->GetString(0, &partition_path);
  args->GetString(1, &manifest_url);
  Proxy* proxy =
      GetProxyForPartitionPath(base::FilePath::FromUTF8Unsafe(partition_path));
  if (proxy)
    proxy->RequestAppCacheDetails(manifest_url);
}

void AppCacheInternalsUI::GetFileDetails(const base::ListValue* args) {
  std::string manifest_url, partition_path, group_id_str, response_id_str;
  args->GetString(0, &partition_path);
  args->GetString(1, &manifest_url);
  args->GetString(2, &group_id_str);
  args->GetString(3, &response_id_str);
  Proxy* proxy =
      GetProxyForPartitionPath(base::FilePath::FromUTF8Unsafe(partition_path));
  if (proxy)
    proxy->RequestFileDetails(
        {manifest_url, ToInt64(group_id_str), ToInt64(response_id_str)});
}

void AppCacheInternalsUI::OnAllAppCacheInfoReady(
    scoped_refptr<AppCacheInfoCollection> collection,
    const base::FilePath& partition_path) {
  std::string incognito_path_prefix;
  if (browser_context()->IsOffTheRecord())
    incognito_path_prefix = "Incognito ";
  web_ui()->CallJavascriptFunctionUnsafe(
      kFunctionOnAllAppCacheInfoReady,
      base::Value(incognito_path_prefix + partition_path.AsUTF8Unsafe()),
      *GetListValueFromAppCacheInfoCollection(collection.get()));
}

void AppCacheInternalsUI::OnAppCacheInfoDeleted(
    const base::FilePath& partition_path,
    const std::string& manifest_url,
    bool deleted) {
  web_ui()->CallJavascriptFunctionUnsafe(
      kFunctionOnAppCacheInfoDeleted,
      base::Value(partition_path.AsUTF8Unsafe()), base::Value(manifest_url),
      base::Value(deleted));
}

void AppCacheInternalsUI::OnAppCacheDetailsReady(
    const base::FilePath& partition_path,
    const std::string& manifest_url,
    std::unique_ptr<std::vector<blink::mojom::AppCacheResourceInfo>>
        resource_info_vector) {
  if (resource_info_vector) {
    web_ui()->CallJavascriptFunctionUnsafe(
        kFunctionOnAppCacheDetailsReady, base::Value(manifest_url),
        base::Value(partition_path.AsUTF8Unsafe()),
        *GetListValueForAppCacheResourceInfoVector(resource_info_vector.get()));
  } else {
    web_ui()->CallJavascriptFunctionUnsafe(
        kFunctionOnAppCacheDetailsReady, base::Value(manifest_url),
        base::Value(partition_path.AsUTF8Unsafe()));
  }
}

void AppCacheInternalsUI::OnFileDetailsReady(
    const Proxy::ResponseEnquiry& response_enquiry,
    scoped_refptr<AppCacheResponseInfo> response_info,
    scoped_refptr<net::IOBuffer> response_data,
    int data_length) {
  std::string headers;
  headers.append("<hr><pre>");
  headers.append(net::EscapeForHTML(
      response_info->http_response_info().headers->GetStatusLine()));
  headers.push_back('\n');

  size_t iter = 0;
  std::string name, value;
  while (response_info->http_response_info().headers->EnumerateHeaderLines(
      &iter, &name, &value)) {
    headers.append(net::EscapeForHTML(name));
    headers.append(": ");
    headers.append(net::EscapeForHTML(value));
    headers.push_back('\n');
  }
  headers.append("</pre>");

  std::string hex_dump = base::StringPrintf(
      "<hr><pre> Showing %d of %d bytes\n\n", static_cast<int>(data_length),
      static_cast<int>(response_info->response_data_size()));
  net::ViewCacheHelper::HexDump(response_data->data(), data_length, &hex_dump);
  if (data_length < response_info->response_data_size())
    hex_dump.append("\nNote: data is truncated...");
  hex_dump.append("</pre>");
  web_ui()->CallJavascriptFunctionUnsafe(
      kFunctionOnFileDetailsReady,
      *GetDictionaryValueForResponseEnquiry(response_enquiry),
      base::Value(headers), base::Value(hex_dump));
}

void AppCacheInternalsUI::OnFileDetailsFailed(
    const Proxy::ResponseEnquiry& response_enquiry,
    int net_result_code) {
  web_ui()->CallJavascriptFunctionUnsafe(
      kFunctionOnFileDetailsFailed,
      *GetDictionaryValueForResponseEnquiry(response_enquiry),
      base::Value(net_result_code));
}

AppCacheInternalsUI::Proxy* AppCacheInternalsUI::GetProxyForPartitionPath(
    const base::FilePath& partition_path) {
  for (const scoped_refptr<Proxy>& proxy : appcache_proxies_) {
    if (proxy->partition_path_ == partition_path)
      return proxy.get();
  }
  NOTREACHED();
  return nullptr;
}

}  // namespace content
