// 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 "services/network/public/cpp/link_header_parser.h"

#include <algorithm>
#include <string>
#include <unordered_map>

#include "base/strings/string_util.h"
#include "components/link_header_util/link_header_util.h"
#include "net/base/mime_util.h"
#include "net/http/http_response_headers.h"

namespace network {

namespace {

bool IsValidMimeType(const std::string& type_string) {
  std::string top_level_type;
  if (!net::ParseMimeTypeWithoutParameter(type_string, &top_level_type,
                                          /*subtype=*/nullptr)) {
    return false;
  }

  return net::IsValidTopLevelMimeType(top_level_type);
}

// Parses `rel` attribute and returns its parsed representation. Returns
// std::nullopt when the value isn't pre-defined.
std::optional<mojom::LinkRelAttribute> ParseRelAttribute(
    const std::optional<std::string>& attr) {
  if (!attr.has_value())
    return std::nullopt;

  std::string value = base::ToLowerASCII(attr.value());
  if (value == "dns-prefetch")
    return mojom::LinkRelAttribute::kDnsPrefetch;
  if (value == "preconnect")
    return mojom::LinkRelAttribute::kPreconnect;
  if (value == "preload")
    return mojom::LinkRelAttribute::kPreload;
  else if (value == "modulepreload")
    return mojom::LinkRelAttribute::kModulePreload;
  return std::nullopt;
}

// Parses `as` attribute and returns its parsed representation. Returns
// std::nullopt when the value isn't pre-defined.
std::optional<mojom::LinkAsAttribute> ParseAsAttribute(
    const std::optional<std::string>& attr) {
  if (!attr.has_value())
    return std::nullopt;

  std::string value = base::ToLowerASCII(attr.value());
  if (value == "font") {
    return mojom::LinkAsAttribute::kFont;
  } else if (value == "image") {
    return mojom::LinkAsAttribute::kImage;
  } else if (value == "script") {
    return mojom::LinkAsAttribute::kScript;
  } else if (value == "style") {
    return mojom::LinkAsAttribute::kStyleSheet;
  } else if (value == "fetch") {
    return mojom::LinkAsAttribute::kFetch;
  }
  return std::nullopt;
}

// Parses `crossorigin` attribute and returns its parsed representation. Returns
// std::nullopt when the value isn't pre-defined.
std::optional<mojom::CrossOriginAttribute> ParseCrossOriginAttribute(
    const std::optional<std::string>& attr) {
  if (!attr.has_value())
    return mojom::CrossOriginAttribute::kAnonymous;

  std::string value = base::ToLowerASCII(attr.value());
  if (value == "anonymous")
    return mojom::CrossOriginAttribute::kAnonymous;
  else if (value == "use-credentials")
    return mojom::CrossOriginAttribute::kUseCredentials;
  return std::nullopt;
}

// Parses `fetchpriority` attribute and returns its parsed representation.
// Returns mojom::FetchPriorityAttribute::kAuto which is the missing and
// invalid value for the attribute.
std::optional<mojom::FetchPriorityAttribute> ParseFetchPriorityAttribute(
    const std::optional<std::string>& attr) {
  if (!attr.has_value()) {
    return mojom::FetchPriorityAttribute::kAuto;
  }

  std::string value = base::ToLowerASCII(attr.value());
  if (value == "low") {
    return mojom::FetchPriorityAttribute::kLow;
  } else if (value == "high") {
    return mojom::FetchPriorityAttribute::kHigh;
  } else if (value == "auto") {
    return mojom::FetchPriorityAttribute::kAuto;
  }
  return mojom::FetchPriorityAttribute::kAuto;
}

// Parses attributes of a Link header and populates parsed representations of
// attributes. Returns true only when all attributes and their values are
// pre-definied.
bool ParseAttributes(
    const std::unordered_map<std::string, std::optional<std::string>>& attrs,
    mojom::LinkHeaderPtr& parsed) {
  bool is_rel_set = false;

  for (const auto& attr : attrs) {
    std::string name = base::ToLowerASCII(attr.first);

    if (name == "rel") {
      // Ignore if `rel` is already set.
      if (is_rel_set)
        continue;
      std::optional<mojom::LinkRelAttribute> rel =
          ParseRelAttribute(attr.second);
      if (!rel.has_value())
        return false;
      parsed->rel = rel.value();
      is_rel_set = true;
    } else if (name == "as") {
      // TODO(crbug.com/40170852): Make sure ignoring second and subsequent ones
      // is a reasonable behavior.
      if (parsed->as != mojom::LinkAsAttribute::kUnspecified)
        continue;
      std::optional<mojom::LinkAsAttribute> as = ParseAsAttribute(attr.second);
      if (!as.has_value())
        return false;
      parsed->as = as.value();
    } else if (name == "crossorigin") {
      // TODO(crbug.com/40170852): Make sure ignoring second and subsequent ones
      // is a reasonable behavior.
      if (parsed->cross_origin != mojom::CrossOriginAttribute::kUnspecified)
        continue;
      std::optional<mojom::CrossOriginAttribute> cross_origin =
          ParseCrossOriginAttribute(attr.second);
      if (!cross_origin.has_value())
        return false;
      parsed->cross_origin = cross_origin.value();
    } else if (name == "type") {
      // TODO(crbug.com/40170852): Make sure ignoring second and subsequent ones
      // is a reasonable behavior.
      if (parsed->mime_type.has_value())
        continue;
      if (!attr.second.has_value() || !IsValidMimeType(attr.second.value()))
        return false;
      parsed->mime_type = attr.second.value();
    } else if (name == "fetchpriority") {
      // TODO(crbug.com/40170852): Make sure ignoring second and subsequent ones
      // is a reasonable behavior.
      if (parsed->fetch_priority != mojom::FetchPriorityAttribute::kAuto) {
        continue;
      }
      std::optional<mojom::FetchPriorityAttribute> fetch_priority =
          ParseFetchPriorityAttribute(attr.second);
      if (!fetch_priority.has_value()) {
        return false;
      }
      parsed->fetch_priority = fetch_priority.value();
    } else if (name == "integrity") {
      // Accept and ignore the `integrity` attribute. Subresource integrity is
      // enforced in the renderer against the response body when the resource is
      // consumed, so the metadata is not needed for the speculative preload
      // issued from a 103 Early Hints response. We must still recognize the
      // attribute here, otherwise the whole Link header would be discarded by
      // the `else` branch below and the resource would never be preloaded.
      continue;
    } else {
      // The current Link header contains an attribute which isn't pre-defined.
      return false;
    }
  }

  // `rel` must be present.
  return is_rel_set;
}

}  // namespace

std::vector<mojom::LinkHeaderPtr> ParseLinkHeaders(
    const net::HttpResponseHeaders& headers,
    const GURL& base_url) {
  std::vector<mojom::LinkHeaderPtr> parsed_headers;
  std::string link_header =
      headers.GetNormalizedHeader("link").value_or(std::string());
  for (const auto& value : link_header_util::SplitLinkHeader(link_header)) {
    std::unordered_map<std::string, std::optional<std::string>> attrs;
    std::optional<std::string> url =
        link_header_util::ParseLinkHeaderValue(value, attrs);
    if (!url) {
      continue;
    }

    auto parsed = mojom::LinkHeader::New();

    parsed->href = base_url.Resolve(*url);
    if (!parsed->href.is_valid())
      continue;

    if (!ParseAttributes(attrs, parsed))
      continue;

    parsed_headers.push_back(std::move(parsed));
  }

  return parsed_headers;
}

}  // namespace network
