// Copyright 2017 The Chromium Authors
// 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 <algorithm>
#include <optional>
#include <string_view>
#include <utility>
#include <vector>

#include "base/containers/span.h"
#include "base/debug/crash_logging.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/self_deleting.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_view_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/trace_event.h"
#include "base/types/expected.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/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_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/common/web_ui_loading_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/url_data_source.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/http/http_byte_range.h"
#include "services/network/public/cpp/parsed_headers.h"
#include "services/network/public/cpp/self_deleting_url_loader_factory.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "ui/base/template_expressions.h"

namespace content {

namespace {

class WebUIURLLoaderFactory;

void ReadData(
    network::mojom::URLResponseHeadPtr headers,
    const ui::TemplateReplacements* replacements,
    bool replace_in_js,
    scoped_refptr<URLDataSourceImpl> data_source,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote,
    std::optional<net::HttpByteRange> requested_range,
    base::ElapsedTimer url_request_elapsed_timer,
    scoped_refptr<base::RefCountedMemory> bytes) {
  TRACE_EVENT0("ui", "WebUIURLLoader::ReadData");
  if (!bytes) {
    webui::CallOnError(std::move(client_remote), net::ERR_FAILED);
    return;
  }

  if (replacements && !replacements->empty()) {
    // We won't know the the final output size ahead of time, so we have to
    // use an intermediate string.
    auto input = base::as_string_view(*bytes);
    std::string temp_str;
    if (replace_in_js) {
      CHECK(
          ui::ReplaceTemplateExpressionsInJS(input, *replacements, &temp_str));
    } else {
      temp_str = ui::ReplaceTemplateExpressions(input, *replacements);
    }
    bytes = base::MakeRefCounted<base::RefCountedString>(std::move(temp_str));
  }

  // Send the bytes to the client. Failed requests do not count towards load
  // time metrics.
  if (!webui::SendData(std::move(headers), std::move(client_remote),
                       std::move(requested_range), bytes)) {
    return;
  }

  UMA_HISTOGRAM_TIMES("WebUI.WebUIURLLoaderFactory.URLRequestLoadTime",
                      url_request_elapsed_timer.Elapsed());
}

void DataAvailable(
    network::mojom::URLResponseHeadPtr headers,
    const ui::TemplateReplacements* replacements,
    bool replace_in_js,
    scoped_refptr<URLDataSourceImpl> source,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote,
    std::optional<net::HttpByteRange> requested_range,
    base::ElapsedTimer url_request_elapsed_timer,
    scoped_refptr<base::RefCountedMemory> bytes) {
  TRACE_EVENT0("ui", "WebUIURLLoader::DataAvailable");
  // 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 SequencedTaskRunner::CurrentDefaultHandle in scope.
  base::ThreadPool::CreateSequencedTaskRunner(
      {base::TaskPriority::USER_BLOCKING, base::MayBlock(),
       base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})
      ->PostTask(FROM_HERE,
                 base::BindOnce(ReadData, std::move(headers), replacements,
                                replace_in_js, source, std::move(client_remote),
                                std::move(requested_range),
                                std::move(url_request_elapsed_timer), bytes));
}

void StartURLLoader(
    const network::ResourceRequest& request,
    FrameTreeNodeId frame_tree_node_id,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client_remote,
    BrowserContext* browser_context) {
  base::ElapsedTimer url_request_elapsed_timer;

  // NOTE: this duplicates code in URLDataManagerBackend::StartRequest.
  if (!URLDataManagerBackend::CheckURLIsValid(request.url)) {
    webui::CallOnError(std::move(client_remote), net::ERR_INVALID_URL);
    return;
  }

  URLDataSourceImpl* source =
      URLDataManagerBackend::GetForBrowserContext(browser_context)
          ->GetDataSourceFromURL(request.url);
  if (!source) {
    webui::CallOnError(std::move(client_remote), net::ERR_INVALID_URL);
    return;
  }

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

  // Load everything by default, but respect the Range header if present.
  base::expected<net::HttpByteRange, webui::GetRequestedRangeError>
      range_or_error = webui::GetRequestedRange(request.headers);
  // Errors (aside from 'no Range header') should be surfaced to the client.
  if (!range_or_error.has_value() &&
      range_or_error.error() != webui::GetRequestedRangeError::kNoRanges) {
    webui::CallOnError(std::move(client_remote),
                       net::ERR_REQUEST_RANGE_NOT_SATISFIABLE);
    return;
  }
  std::optional<net::HttpByteRange> maybe_range =
      range_or_error.has_value() ? std::make_optional(range_or_error.value())
                                 : std::nullopt;

  std::string path = URLDataSource::URLToRequestPath(request.url);
  std::string origin_header =
      request.headers.GetHeader(net::HttpRequestHeaders::kOrigin)
          .value_or(std::string());

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

  auto resource_response = network::mojom::URLResponseHead::New();

  resource_response->headers = headers;
  // Headers from WebUI are trusted, so parsing can happen from a non-sandboxed
  // process.
  resource_response->parsed_headers = network::PopulateParsedHeaders(
      resource_response->headers.get(), request.url);
  resource_response->mime_type = source->source()->GetMimeType(request.url);
  auto now_time = base::Time::Now();
  auto now_ticks = base::TimeTicks::Now();
  resource_response->request_time = now_time;
  resource_response->request_start = now_ticks;
  resource_response->load_timing.request_start_time = now_time;
  resource_response->load_timing.request_start = now_ticks;

  WebContents::Getter wc_getter;

  // Service Workers factories have no associated frame.
  if (frame_tree_node_id.is_null()) {
    wc_getter = base::BindRepeating([]() -> WebContents* { return nullptr; });
  } else {
    wc_getter = base::BindRepeating(WebContents::FromFrameTreeNodeId,
                                    frame_tree_node_id);
  }

  bool replace_in_js =
      source->source()->ShouldReplaceI18nInJS() &&
      source->source()->GetMimeType(request.url) == "application/javascript";

  const ui::TemplateReplacements* replacements = nullptr;
  const std::string mime_type = source->source()->GetMimeType(request.url);
  if (mime_type == "text/html" || mime_type == "text/css" || replace_in_js)
    replacements = source->GetReplacements();

  // To keep the same behavior as the old WebUI code, we call the source to get
  // the value for |replacements| on the IO thread. Since |replacements| is
  // owned by |source| keep a reference to it in the callback.
  URLDataSource::GotDataCallback data_available_callback = base::BindOnce(
      DataAvailable, std::move(resource_response), replacements, replace_in_js,
      base::RetainedRef(source), std::move(client_remote),
      std::move(maybe_range), std::move(url_request_elapsed_timer));

  source->source()->StartDataRequest(request.url, std::move(wc_getter),
                                     std::move(data_available_callback));
}

class WebUIURLLoaderFactory : public network::SelfDeletingURLLoaderFactory {
 public:
  // Returns mojo::PendingRemote to a newly constructed WebUIURLLoaderFactory.
  // The factory is self-owned - it will delete itself once there are no more
  // receivers (including the receiver associated with the returned
  // mojo::PendingRemote and the receivers bound by the Clone method).
  //
  // |allowed_hosts| is an optional set of allowed host names. If empty then
  // all hosts are allowed.
  static mojo::PendingRemote<network::mojom::URLLoaderFactory> CreateForFrame(
      FrameTreeNode* ftn,
      const std::string& scheme,
      base::flat_set<std::string> allowed_hosts) {
    mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote;

    // The WebUIURLLoaderFactory will delete itself when there are no more
    // receivers - see the
    // network::SelfDeletingURLLoaderFactory::OnDisconnect method.
    base::MakeSelfDeleting<WebUIURLLoaderFactory>(
        ftn->current_frame_host()->GetBrowserContext(),
        ftn->frame_tree_node_id(), scheme, std::move(allowed_hosts),
        pending_remote.InitWithNewPipeAndPassReceiver());
    return pending_remote;
  }

  static mojo::PendingRemote<network::mojom::URLLoaderFactory> CreateForWorker(
      BrowserContext* browser_context,
      const std::string& scheme,
      base::flat_set<std::string> allowed_hosts) {
    mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote;
    base::MakeSelfDeleting<WebUIURLLoaderFactory>(
        browser_context, FrameTreeNodeId(), scheme, std::move(allowed_hosts),
        pending_remote.InitWithNewPipeAndPassReceiver());
    return pending_remote;
  }

  WebUIURLLoaderFactory(
      BrowserContext* browser_context,
      FrameTreeNodeId frame_tree_node_id,
      const std::string& scheme,
      base::flat_set<std::string> allowed_hosts,
      mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_receiver,
      base::SelfDeletingPassKey key)
      : network::SelfDeletingURLLoaderFactory(std::move(factory_receiver), key),
        browser_context_(browser_context->GetWeakPtr()),
        frame_tree_node_id_(frame_tree_node_id),
        scheme_(scheme),
        allowed_hosts_(std::move(allowed_hosts)) {}

  WebUIURLLoaderFactory(const WebUIURLLoaderFactory&) = delete;
  WebUIURLLoaderFactory& operator=(const WebUIURLLoaderFactory&) = delete;

 private:
  ~WebUIURLLoaderFactory() override = default;

  // network::mojom::URLLoaderFactory implementation:
  void CreateLoaderAndStart(
      mojo::PendingReceiver<network::mojom::URLLoader> loader,
      int32_t request_id,
      uint32_t options,
      const network::ResourceRequest& request,
      mojo::PendingRemote<network::mojom::URLLoaderClient> client,
      const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
      override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    if (browser_context_.WasInvalidated()) {
      DVLOG(1) << "Context has been destroyed";
      webui::CallOnError(std::move(client), net::ERR_FAILED);
      DisconnectReceiversAndDestroy();
      return;
    }

    if (frame_tree_node_id_ &&
        !FrameTreeNode::GloballyFindByID(frame_tree_node_id_)) {
      webui::CallOnError(std::move(client), net::ERR_FAILED);
      return;
    }

    if (request.url.GetScheme() != scheme_) {
      DVLOG(1) << "Bad scheme: " << request.url.GetScheme();
      SCOPED_CRASH_KEY_STRING32("WebUI", "actual_scheme",
                                request.url.GetScheme());
      SCOPED_CRASH_KEY_STRING32("WebUI", "expected_scheme", scheme_);
      SCOPED_CRASH_KEY_STRING64("WebUI", "requested_url", request.url.spec());
      SCOPED_CRASH_KEY_STRING64(
          "WebUI", "initiator_origin",
          request.request_initiator.has_value()
              ? request.request_initiator->GetDebugString(false)
              : "nullopt");
      mojo::ReportBadMessage("Incorrect scheme");
      mojo::Remote<network::mojom::URLLoaderClient>(std::move(client))
          ->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
      return;
    }

    CHECK(allowed_hosts_.empty() ||
          (request.url.has_host() &&
           allowed_hosts_.find(request.url.GetHost()) != allowed_hosts_.end()))
        << "Incorrect host: " << request.url.GetHost();

    if (request.url.scheme() == kChromeUIScheme &&
        request.url.host() == kChromeUIBlobInternalsHost) {
      GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(
              &StartBlobInternalsURLLoader, request, std::move(client),
              base::Unretained(
                  ChromeBlobStorageContext::GetFor(browser_context_.get()))));
      return;
    }

    // This path is entered on user-trigger navigations (e.g. from omnibox or
    // links) to chrome://network-error or chrome://dino. Actual network error
    // does not trigger this path.
    if (request.url.scheme() == kChromeUIScheme &&
        (request.url.host() == kChromeUINetworkErrorHost ||
         request.url.host() == kChromeUIDinoHost)) {
      // Simulate a network error.
      StartNetworkErrorsURLLoader(request, std::move(client));
      // Logs WebUI usage. These WebUIs don't create a WebUI object.
      // TODO(crbug.com/40089364): all WebUIs should have a WebUI object.
      WebContents* web_contents =
          WebContents::FromFrameTreeNodeId(frame_tree_node_id_);
      CHECK(web_contents);
      GetContentClient()->browser()->LogWebUIUsage(request.url);
      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.
    StartURLLoader(request, frame_tree_node_id_, std::move(client),
                   browser_context_.get());
  }

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

  base::WeakPtr<BrowserContext> browser_context_;
  const FrameTreeNodeId frame_tree_node_id_;
  const std::string scheme_;
  const base::flat_set<std::string> allowed_hosts_;  // if empty all allowed.
};

}  // namespace

mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateWebUIURLLoaderFactory(RenderFrameHost* render_frame_host,
                            const std::string& scheme,
                            base::flat_set<std::string> allowed_hosts) {
  return WebUIURLLoaderFactory::CreateForFrame(
      FrameTreeNode::From(render_frame_host), scheme, std::move(allowed_hosts));
}

mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateWebUIURLLoaderFactoryForWorker(
    BrowserContext* browser_context,
    const std::string& scheme,
    base::flat_set<std::string> allowed_hosts) {
  return WebUIURLLoaderFactory::CreateForWorker(browser_context, scheme,
                                                std::move(allowed_hosts));
}

}  // namespace content
