// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/pdf/browser/plugin_response_writer.h"

#include <memory>
#include <string>
#include <utility>

#include "base/byte_size.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "components/pdf/browser/pdf_stream_delegate.h"
#include "mojo/public/c/system/types.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/data_pipe_producer.h"
#include "mojo/public/cpp/system/string_data_source.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "pdf/pdf_features.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"

namespace pdf {

namespace {

// Generates a response ready to be used for creating the PDF loader. The
// returned value is a raw string in which the escape characters are not
// processed.
// Note: This function is security sensitive since it defines the boundary of
// HTML and the embedded PDF. Must limit information shared with the PDF plugin
// process through this response.
std::string GenerateResponse(const PdfStreamDelegate::StreamInfo& stream_info) {
  // TODO(crbug.com/40189769): This script in this response is never executed
  // when JavaScript is blocked throughout the browser (set in
  // chrome://settings/content/javascript). A permanent solution would likely
  // have to hook into postMessage() natively.
  static constexpr char kResponseTemplate[] = R"(<!DOCTYPE html>
<style>
body,
embed,
html {
  height: 100%;
  margin: 0;
  width: 100%;
}

embed {
  left: 0;
  position: fixed;
  top: 0;
}

/* Hide scrollbars when in Presentation mode. */
.fullscreen {
  overflow: hidden;
}
</style>
<div id="sizer"></div>
<embed type="application/x-google-chrome-pdf" src="$1" original-url="$2"
    background-color="$4" javascript="$5"$6$7$8>
<script type="module">
$3
</script>
)";

  // TODO(crbug.com/40792950): We should load the injected scripts as network
  // resources instead. Until then, feel free to raise this limit as necessary.
  if (stream_info.injected_script)
    DCHECK_LE(stream_info.injected_script->size(), 16'384u);

  return base::ReplaceStringPlaceholders(
      kResponseTemplate,
      {stream_info.stream_url.spec(),
       base::EscapeForHTML(stream_info.original_url.spec()),
       stream_info.injected_script ? *stream_info.injected_script : "",
       base::NumberToString(stream_info.background_color),
       stream_info.allow_javascript ? "allow" : "block",
       stream_info.full_frame ? " full-frame" : "",
       stream_info.use_skia ? " use-skia" : "",
       stream_info.allow_xfa_forms ? " allow-xfa-forms" : ""},
      /*offsets=*/nullptr);
}

}  // namespace

PluginResponseWriter::PluginResponseWriter(
    const PdfStreamDelegate::StreamInfo& stream_info,
    mojo::PendingRemote<network::mojom::URLLoaderClient> client)
    : body_(GenerateResponse(stream_info)),
      client_(std::move(client)),
      coep_header_(stream_info.coep_header) {}

PluginResponseWriter::~PluginResponseWriter() = default;

void PluginResponseWriter::Start(base::OnceClosure done_callback) {
  auto response = network::mojom::URLResponseHead::New();
  response->headers =
      base::MakeRefCounted<net::HttpResponseHeaders>("HTTP/1.1 200 OK");
  // Allow the PDF plugin to be embedded in cross-origin sites if the original
  // PDF has a COEP: require-corp or COEP: credentialless header.
  if (chrome_pdf::features::IsOopifPdfEnabled() &&
      (coep_header_ == "require-corp" || coep_header_ == "credentialless")) {
    response->headers->AddHeader("Cross-Origin-Embedder-Policy", coep_header_);
    response->headers->AddHeader("Cross-Origin-Resource-Policy",
                                 "cross-origin");
  }
  response->mime_type = "text/html";

  mojo::ScopedDataPipeProducerHandle producer;
  mojo::ScopedDataPipeConsumerHandle consumer;
  if (mojo::CreateDataPipe(nullptr, producer, consumer) != MOJO_RESULT_OK) {
    client_->OnComplete(
        network::URLLoaderCompletionStatus(net::ERR_INSUFFICIENT_RESOURCES));
    std::move(done_callback).Run();
    return;
  }

  client_->OnReceiveResponse(std::move(response), std::move(consumer),
                             std::nullopt);

  producer_ = std::make_unique<mojo::DataPipeProducer>(std::move(producer));

  // Caller is required to keep `this` alive until `done_callback` is called, so
  // `base::Unretained(this)` should be safe.
  producer_->Write(
      std::make_unique<mojo::StringDataSource>(
          body_, mojo::StringDataSource::AsyncWritingMode::
                     STRING_STAYS_VALID_UNTIL_COMPLETION),
      base::BindOnce(&PluginResponseWriter::OnWrite, base::Unretained(this),
                     std::move(done_callback)));
}

void PluginResponseWriter::OnWrite(base::OnceClosure done_callback,
                                   MojoResult result) {
  producer_.reset();

  if (result == MOJO_RESULT_OK) {
    network::URLLoaderCompletionStatus status(net::OK);
    status.encoded_data_length = base::ByteSize(body_.size());
    status.encoded_body_length = base::ByteSize(body_.size());
    status.decoded_body_length = base::ByteSize(body_.size());
    client_->OnComplete(status);
  } else {
    client_->OnComplete(network::URLLoaderCompletionStatus(net::ERR_FAILED));
  }

  std::move(done_callback).Run();
}

}  // namespace pdf
