// 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/public/browser/web_ui_url_loader_factory.h"

#include <map>

#include "base/bind.h"
#include "base/debug/crash_logging.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_piece.h"
#include "base/task/post_task.h"
#include "content/browser/bad_message.h"
#include "content/browser/blob_storage/blob_internals_url_loader.h"
#include "content/browser/blob_storage/chrome_blob_storage_context.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/resource_context_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/webui/network_error_url_loader.h"
#include "content/browser/webui/url_data_manager_backend.h"
#include "content/browser/webui/url_data_source_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/bindings/binding_set.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "third_party/zlib/google/compression_utils.h"
#include "ui/base/template_expressions.h"

namespace content {

namespace {

class WebUIURLLoaderFactory;
base::LazyInstance<std::map<GlobalFrameRoutingId,
                            std::unique_ptr<WebUIURLLoaderFactory>>>::Leaky
    g_web_ui_url_loader_factories = LAZY_INSTANCE_INITIALIZER;

void CallOnError(network::mojom::URLLoaderClientPtrInfo client_info,
                 int error_code) {
  network::mojom::URLLoaderClientPtr client;
  client.Bind(std::move(client_info));

  network::URLLoaderCompletionStatus status;
  status.error_code = error_code;
  client->OnComplete(status);
}

void ReadData(scoped_refptr<network::ResourceResponse> headers,
              const ui::TemplateReplacements* replacements,
              bool gzipped,
              scoped_refptr<URLDataSourceImpl> data_source,
              network::mojom::URLLoaderClientPtrInfo client_info,
              scoped_refptr<base::RefCountedMemory> bytes) {
  if (!bytes) {
    CallOnError(std::move(client_info), net::ERR_FAILED);
    return;
  }

  network::mojom::URLLoaderClientPtr client;
  client.Bind(std::move(client_info));
  client->OnReceiveResponse(headers->head);

  base::StringPiece input(reinterpret_cast<const char*>(bytes->front()),
                          bytes->size());

  // Treats empty gzipped data as unzipped.
  if (!bytes->size())
    gzipped = false;

  if (replacements) {
    std::string temp_string;
    // We won't know the the final output size ahead of time, so we have to
    // use an intermediate string.
    base::StringPiece source;
    std::string temp_str;
    if (gzipped) {
      temp_str.resize(compression::GetUncompressedSize(input));
      source.set(temp_str.c_str(), temp_str.size());
      CHECK(compression::GzipUncompress(input, source));
      gzipped = false;
    } else {
      source = input;
    }
    temp_str = ui::ReplaceTemplateExpressions(source, *replacements);
    bytes = base::RefCountedString::TakeString(&temp_str);
    input.set(reinterpret_cast<const char*>(bytes->front()), bytes->size());
  }

  uint32_t output_size =
      gzipped ? compression::GetUncompressedSize(input) : bytes->size();

  mojo::DataPipe data_pipe(output_size);

  void* buffer = nullptr;
  uint32_t num_bytes = output_size;
  MojoResult result = data_pipe.producer_handle->BeginWriteData(
      &buffer, &num_bytes, MOJO_WRITE_DATA_FLAG_NONE);
  CHECK_EQ(result, MOJO_RESULT_OK);
  CHECK_GE(num_bytes, output_size);

  if (gzipped) {
    base::StringPiece output(static_cast<char*>(buffer), output_size);
    CHECK(compression::GzipUncompress(input, output));
  } else {
    memcpy(buffer, bytes->front(), output_size);
  }
  result = data_pipe.producer_handle->EndWriteData(output_size);
  CHECK_EQ(result, MOJO_RESULT_OK);

  client->OnStartLoadingResponseBody(std::move(data_pipe.consumer_handle));
  network::URLLoaderCompletionStatus status(net::OK);
  status.encoded_data_length = output_size;
  status.encoded_body_length = output_size;
  status.decoded_body_length = output_size;
  client->OnComplete(status);
}

void DataAvailable(scoped_refptr<network::ResourceResponse> headers,
                   const ui::TemplateReplacements* replacements,
                   bool gzipped,
                   scoped_refptr<URLDataSourceImpl> source,
                   network::mojom::URLLoaderClientPtrInfo client_info,
                   scoped_refptr<base::RefCountedMemory> bytes) {
  // Since the bytes are from the memory mapped resource file, copying the
  // data can lead to disk access. Needs to be posted to a SequencedTaskRunner
  // as Mojo requires a SequencedTaskRunnerHandle in scope.
  base::CreateSequencedTaskRunnerWithTraits(
      {base::TaskPriority::USER_BLOCKING, base::MayBlock(),
       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN})
      ->PostTask(FROM_HERE,
                 base::BindOnce(ReadData, headers, replacements, gzipped,
                                source, std::move(client_info), bytes));
}

void StartURLLoader(const network::ResourceRequest& request,
                    int frame_tree_node_id,
                    network::mojom::URLLoaderClientPtrInfo client_info,
                    ResourceContext* resource_context) {
  // NOTE: this duplicates code in URLDataManagerBackend::StartRequest.
  if (!URLDataManagerBackend::CheckURLIsValid(request.url)) {
    CallOnError(std::move(client_info), net::ERR_INVALID_URL);
    return;
  }

  URLDataSourceImpl* source =
      GetURLDataManagerForResourceContext(resource_context)
          ->GetDataSourceFromURL(request.url);
  if (!source) {
    CallOnError(std::move(client_info), net::ERR_INVALID_URL);
    return;
  }

  if (!source->source()->ShouldServiceRequest(request.url, resource_context,
                                              -1)) {
    CallOnError(std::move(client_info), net::ERR_INVALID_URL);
    return;
  }

  std::string path;
  URLDataManagerBackend::URLToRequestPath(request.url, &path);

  std::string origin_header;
  request.headers.GetHeader(net::HttpRequestHeaders::kOrigin, &origin_header);

  scoped_refptr<net::HttpResponseHeaders> headers =
      URLDataManagerBackend::GetHeaders(source, path, origin_header);

  scoped_refptr<network::ResourceResponse> resource_response(
      new network::ResourceResponse);
  resource_response->head.headers = headers;
  resource_response->head.mime_type = source->source()->GetMimeType(path);
  // TODO: fill all the time related field i.e. request_time response_time
  // request_start response_start

  ResourceRequestInfo::WebContentsGetter wc_getter =
      base::Bind(WebContents::FromFrameTreeNodeId, frame_tree_node_id);

  bool gzipped = source->source()->IsGzipped(path);
  const ui::TemplateReplacements* replacements = nullptr;
  if (source->source()->GetMimeType(path) == "text/html")
    replacements = source->GetReplacements();
  // To keep the same behavior as the old WebUI code, we call the source to get
  // the value for |gzipped| and |replacements| on the IO thread. Since
  // |replacements| is owned by |source| keep a reference to it in the callback.
  auto data_available_callback =
      base::Bind(DataAvailable, resource_response, replacements, gzipped,
                 base::RetainedRef(source), base::Passed(&client_info));

  // TODO(jam): once we only have this code path for WebUI, and not the
  // URLLRequestJob one, then we should switch data sources to run on the UI
  // thread by default.
  scoped_refptr<base::SingleThreadTaskRunner> target_runner =
      source->source()->TaskRunnerForRequestPath(path);
  if (!target_runner) {
    source->source()->StartDataRequest(path, std::move(wc_getter),
                                       std::move(data_available_callback));
    return;
  }

  // The DataSource wants StartDataRequest to be called on a specific
  // thread, usually the UI thread, for this path.
  target_runner->PostTask(
      FROM_HERE,
      base::BindOnce(&URLDataSource::StartDataRequest,
                     base::Unretained(source->source()), path,
                     std::move(wc_getter), std::move(data_available_callback)));
}

class WebUIURLLoaderFactory : public network::mojom::URLLoaderFactory,
                              public WebContentsObserver {
 public:
  // |allowed_hosts| is an optional set of allowed host names. If empty then
  // all hosts are allowed.
  WebUIURLLoaderFactory(RenderFrameHost* rfh,
                        const std::string& scheme,
                        base::flat_set<std::string> allowed_hosts)
      : WebContentsObserver(WebContents::FromRenderFrameHost(rfh)),
        render_frame_host_(rfh),
        scheme_(scheme),
        allowed_hosts_(std::move(allowed_hosts)) {}

  ~WebUIURLLoaderFactory() override {}

  network::mojom::URLLoaderFactoryPtr CreateBinding() {
    network::mojom::URLLoaderFactoryPtr factory;
    loader_factory_bindings_.AddBinding(this, mojo::MakeRequest(&factory));
    return factory;
  }

  // network::mojom::URLLoaderFactory implementation:
  void CreateLoaderAndStart(network::mojom::URLLoaderRequest loader,
                            int32_t routing_id,
                            int32_t request_id,
                            uint32_t options,
                            const network::ResourceRequest& request,
                            network::mojom::URLLoaderClientPtr client,
                            const net::MutableNetworkTrafficAnnotationTag&
                                traffic_annotation) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);

    if (request.url.scheme() != scheme_) {
      DVLOG(1) << "Bad scheme: " << request.url.scheme();
      ReceivedBadMessage(render_frame_host_->GetProcess(),
                         bad_message::WEBUI_BAD_SCHEME_ACCESS);
      client->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
      return;
    }

    if (!allowed_hosts_.empty() &&
        (!request.url.has_host() ||
         allowed_hosts_.find(request.url.host()) == allowed_hosts_.end())) {
      // Temporary reporting the bad WebUI host for for http://crbug.com/837328.
      static auto* crash_key = base::debug::AllocateCrashKeyString(
          "webui_url", base::debug::CrashKeySize::Size64);
      base::debug::SetCrashKeyString(crash_key, request.url.spec());

      DVLOG(1) << "Bad host: \"" << request.url.host() << '"';
      ReceivedBadMessage(render_frame_host_->GetProcess(),
                         bad_message::WEBUI_BAD_HOST_ACCESS);
      client->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
      return;
    }

    if (request.url.host_piece() == kChromeUIBlobInternalsHost) {
      BrowserThread::PostTask(
          BrowserThread::IO, FROM_HERE,
          base::BindOnce(&StartBlobInternalsURLLoader, request,
                         client.PassInterface(),
                         base::Unretained(ChromeBlobStorageContext::GetFor(
                             GetStoragePartition()->browser_context()))));
      return;
    }

    if (request.url.host_piece() == kChromeUINetworkErrorHost ||
        request.url.host_piece() == kChromeUIDinoHost) {
      StartNetworkErrorsURLLoader(request, std::move(client));
      return;
    }

    // We pass the FrameTreeNode ID to get to the WebContents because requests
    // from frames can happen while the RFH is changed for a cross-process
    // navigation. The URLDataSources just need the WebContents; the specific
    // frame doesn't matter.
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::BindOnce(
            &StartURLLoader, request, render_frame_host_->GetFrameTreeNodeId(),
            client.PassInterface(),
            GetStoragePartition()->browser_context()->GetResourceContext()));
  }

  void Clone(network::mojom::URLLoaderFactoryRequest request) override {
    loader_factory_bindings_.AddBinding(this, std::move(request));
  }

  // WebContentsObserver implementation:
  void RenderFrameDeleted(RenderFrameHost* render_frame_host) override {
    if (render_frame_host != render_frame_host_)
      return;
    g_web_ui_url_loader_factories.Get().erase(
        GlobalFrameRoutingId(render_frame_host_->GetRoutingID(),
                             render_frame_host_->GetProcess()->GetID()));
  }

  const std::string& scheme() const { return scheme_; }

 private:
  StoragePartitionImpl* GetStoragePartition() {
    return static_cast<StoragePartitionImpl*>(
        render_frame_host_->GetProcess()->GetStoragePartition());
  }

  RenderFrameHost* render_frame_host_;
  std::string scheme_;
  const base::flat_set<std::string> allowed_hosts_;  // if empty all allowed.
  mojo::BindingSet<network::mojom::URLLoaderFactory> loader_factory_bindings_;

  DISALLOW_COPY_AND_ASSIGN(WebUIURLLoaderFactory);
};

}  // namespace

std::unique_ptr<network::mojom::URLLoaderFactory> CreateWebUIURLLoader(
    RenderFrameHost* render_frame_host,
    const std::string& scheme,
    base::flat_set<std::string> allowed_hosts) {
  return std::make_unique<WebUIURLLoaderFactory>(render_frame_host, scheme,
                                                 std::move(allowed_hosts));
}

network::mojom::URLLoaderFactoryPtr CreateWebUIURLLoaderBinding(
    RenderFrameHost* render_frame_host,
    const std::string& scheme) {
  GlobalFrameRoutingId routing_id(render_frame_host->GetRoutingID(),
                                  render_frame_host->GetProcess()->GetID());
  if (g_web_ui_url_loader_factories.Get().find(routing_id) ==
          g_web_ui_url_loader_factories.Get().end() ||
      g_web_ui_url_loader_factories.Get()[routing_id]->scheme() != scheme) {
    g_web_ui_url_loader_factories.Get()[routing_id] =
        std::make_unique<WebUIURLLoaderFactory>(render_frame_host, scheme,
                                                base::flat_set<std::string>());
  }
  return g_web_ui_url_loader_factories.Get()[routing_id]->CreateBinding();
}

}  // namespace content
