910e62b5创建于 1月15日历史提交
// Copyright 2022 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/browser/browsing_topics/header_util.h"

#include "base/strings/strcat.h"
#include "components/browsing_topics/common/common_types.h"
#include "components/browsing_topics/common/semantic_tree.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/page.h"
#include "content/public/common/content_client.h"
#include "net/http/http_request_headers.h"
#include "net/http/structured_headers.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"

namespace content {

namespace {

// The max number of digits in a topic. As new taxonomies are introduced and old
// topics are expired, the expectation is this value will gradually grow.
constexpr int kTopicMaxLength = 3;

// The number of characters in a version string, e.g., chrome.1:1:10. This will
// grow as versions require more digits.
constexpr int kVersionMaxLength = 13;

static_assert(browsing_topics::ConfigVersion::kMaxValue < 10,
              "Topics config version should not exceed 1 digit, or "
              "`kVersionMaxLength` should be updated accordingly.");

static_assert(blink::features::kBrowsingTopicsTaxonomyVersionDefault < 10,
              "Topics taxonomy version should not exceed 1 digit, or "
              "`kVersionMaxLength` should be updated accordingly.");

static_assert(browsing_topics::SemanticTree::kNumTopics < 1000,
              "Total number of topics (i.e. max topic ID) should not exceed 3 "
              "digits, or `kTopicMaxLength` should be updated accordingly.");

}  // namespace

std::string DeriveTopicsHeaderValue(
    const std::vector<blink::mojom::EpochTopicPtr>& topics,
    int num_versions_in_epochs) {
  net::structured_headers::List header_list;
  std::optional<std::string> last_version;
  std::vector<net::structured_headers::ParameterizedItem> cur_topics;

  // Build up the header without the padding parameter.
  for (auto& topic : topics) {
    bool new_version =
        (!last_version.has_value() || last_version.value() != topic->version);
    if (new_version) {
      if (cur_topics.size() > 0) {
        CHECK(last_version.has_value());
        header_list.push_back(net::structured_headers::ParameterizedMember(
            cur_topics,
            {{"v",
              net::structured_headers::Item(
                  *last_version, net::structured_headers::Item::kTokenType)}}));
        cur_topics.clear();
      }
      last_version = topic->version;
    }
    cur_topics.push_back(net::structured_headers::ParameterizedItem(
        net::structured_headers::Item(static_cast<int64_t>(topic->topic)), {}));
  }

  if (cur_topics.size() > 0) {
    CHECK(last_version.has_value());
    header_list.push_back(net::structured_headers::ParameterizedMember(
        cur_topics, {{"v", net::structured_headers::Item(
                               *last_version,
                               net::structured_headers::Item::kTokenType)}}));
  }

  // The header is now complete, except for padding. We want the header to be of
  // fixed size for the given number of versions in the list, so we add padding
  // to make that happen.

  // When adding padding, we'll always have at least 1 version.
  if (num_versions_in_epochs == 0) {
    num_versions_in_epochs = 1;
  }

  // The number of topics that should be in the padded response.
  int max_number_of_epochs =
      blink::features::kBrowsingTopicsNumberOfEpochsToExpose.Get();
  CHECK_LE(num_versions_in_epochs, max_number_of_epochs);
  CHECK_GT(max_number_of_epochs, 0);

  // The padded length of the header given the number of versions.
  // Example header: Sec-Browsing-Topics: (100 200);v=chrome.1:1:2,
  // (300);v=chrome.1:1:4, ();p=P00
  int max_length =
      max_number_of_epochs * kTopicMaxLength +  // length of three topics
      max_number_of_epochs -
      num_versions_in_epochs +      // spaces between topics in a list
      num_versions_in_epochs * 5 +  // '();v='
      num_versions_in_epochs *
          kVersionMaxLength +            // max length of the versions
      (num_versions_in_epochs - 1) * 2;  // the ', ' between topic lists

  // Add the bytes for the ", " between the last list and the padding list in
  // the event that there are no topics.
  if (header_list.size() == 0) {
    max_length += 2;
  }

  // How many bytes of padding do we need to add?
  int padding_needed =
      header_list.size() > 0
          ? max_length -
                net::structured_headers::SerializeList(header_list)->length()
          : max_length;

  // The padding should generally be >= 0. It can be negative in certain
  // circumstances and we need to handle that here. It can be negative if a new
  // version is rolled out via finch (e.g., model or taxonomy) that uses an
  // extra digit in its number but the binary hasn't been updated to handle the
  // extra digit yet. It could also happen if there is a race between getting
  // topics and getting the number of distinct topic versions. We clamp to 0 to
  // prevent breakage in these rare circumstances.
  if (padding_needed < 0) {
    padding_needed = 0;
  }

  // Add the padding list at the end.
  header_list.push_back(net::structured_headers::ParameterizedMember(
      std::vector<net::structured_headers::ParameterizedItem>(),
      {{"p", net::structured_headers::Item(
                 base::StrCat({"P", std::string(padding_needed, '0')}),
                 net::structured_headers::Item::kTokenType)}}));

  std::optional<std::string> serialized_header =
      net::structured_headers::SerializeList(header_list);
  CHECK(serialized_header);

  return *serialized_header;
}

void HandleTopicsEligibleResponse(
    const network::mojom::ParsedHeadersPtr& parsed_headers,
    const url::Origin& caller_origin,
    RenderFrameHost& request_initiator_frame,
    browsing_topics::ApiCallerSource caller_source) {
  DCHECK(caller_source == browsing_topics::ApiCallerSource::kFetch ||
         caller_source == browsing_topics::ApiCallerSource::kIframeAttribute ||
         caller_source == browsing_topics::ApiCallerSource::kImgAttribute);

  if (!parsed_headers || !parsed_headers->observe_browsing_topics) {
    return;
  }

  // Check the page's IsPrimary() status again in case it has changed since the
  // request time.
  if (!request_initiator_frame.GetPage().IsPrimary()) {
    return;
  }

  // Store the observation.
  std::vector<blink::mojom::EpochTopicPtr> topics;
  GetContentClient()->browser()->HandleTopicsWebApi(
      caller_origin, request_initiator_frame.GetMainFrame(), caller_source,
      /*get_topics=*/false,
      /*observe=*/true, topics);
}

}  // namespace content