blob: 6b4d5ffe9b747504bb6081c2e6c5603187a81a69 [file] [log] [blame]
// 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 "third_party/blink/renderer/core/speculation_rules/speculation_rule_set.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "services/network/public/mojom/referrer_policy.mojom-shared.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/speculation_rules/document_rule_predicate.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/json/json_parser.h"
#include "third_party/blink/renderer/platform/json/json_values.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_policy.h"
#include "third_party/blink/renderer/platform/wtf/text/string_view.h"
namespace blink {
namespace {
// https://html.spec.whatwg.org/C/#valid-browsing-context-name
bool IsValidContextName(const String& name_or_keyword) {
// "A valid browsing context name is any string with at least one character
// that does not start with a U+005F LOW LINE character. (Names starting with
// an underscore are reserved for special keywords.)"
if (name_or_keyword.empty())
return false;
if (name_or_keyword.StartsWith("_"))
return false;
return true;
}
// https://html.spec.whatwg.org/C/#valid-browsing-context-name-or-keyword
bool IsValidBrowsingContextNameOrKeyword(const String& name_or_keyword) {
// "A valid browsing context name or keyword is any string that is either a
// valid browsing context name or that is an ASCII case-insensitive match for
// one of: _blank, _self, _parent, or _top."
String canonicalized_name_or_keyword = name_or_keyword.LowerASCII();
if (IsValidContextName(name_or_keyword) ||
EqualIgnoringASCIICase(name_or_keyword, "_blank") ||
EqualIgnoringASCIICase(name_or_keyword, "_self") ||
EqualIgnoringASCIICase(name_or_keyword, "_parent") ||
EqualIgnoringASCIICase(name_or_keyword, "_top")) {
return true;
}
return false;
}
SpeculationRule* ParseSpeculationRule(JSONObject* input,
const KURL& base_url,
ExecutionContext* context) {
// https://wicg.github.io/nav-speculation/speculation-rules.html#parse-a-speculation-rule
// If input has any key other than "source", "urls", "requires", "target_hint"
// and "relative_to", then return null.
const char* const kKnownKeys[] = {"source", "urls", "requires",
"target_hint", "where", "relative_to"};
const auto kConditionalKnownKeys = [context]() {
Vector<const char*, 4> conditional_known_keys;
if (RuntimeEnabledFeatures::SpeculationRulesReferrerPolicyKeyEnabled(
context)) {
conditional_known_keys.push_back("referrer_policy");
}
if (RuntimeEnabledFeatures::SpeculationRulesEagernessEnabled(context)) {
conditional_known_keys.push_back("eagerness");
}
return conditional_known_keys;
}();
for (wtf_size_t i = 0; i < input->size(); ++i) {
const String& input_key = input->at(i).first;
if (!base::Contains(kKnownKeys, input_key) &&
!base::Contains(kConditionalKnownKeys, input_key)) {
return nullptr;
}
}
bool document_rules_enabled =
RuntimeEnabledFeatures::SpeculationRulesDocumentRulesEnabled(context);
const bool relative_to_enabled =
RuntimeEnabledFeatures::SpeculationRulesRelativeToDocumentEnabled(
context);
// If input["source"] does not exist or is neither the string "list" nor the
// string "document", then return null.
String source;
if (!input->GetString("source", &source) ||
!(source == "list" || (document_rules_enabled && source == "document")))
return nullptr;
Vector<KURL> urls;
if (source == "list") {
// If input["where"] exists, then return null.
if (input->Get("where"))
return nullptr;
// For now, use the given base URL to construct the list rules.
KURL base_url_to_parse = base_url;
// If input["relative_to"] exists:
if (JSONValue* relative_to = input->Get("relative_to")) {
const char* const kKnownRelativeToValues[] = {"ruleset", "document"};
String value;
// If relativeTo is neither the string "ruleset" nor the string
// "document", then return null.
if (!relative_to_enabled || !relative_to->AsString(&value) ||
!base::Contains(kKnownRelativeToValues, value)) {
return nullptr;
}
// If relativeTo is "document", then set baseURL to the document's
// document base URL.
if (value == "document") {
base_url_to_parse = context->BaseURL();
}
}
// Let urls be an empty list.
// If input["urls"] does not exist, is not a list, or has any element which
// is not a string, then return null.
JSONArray* input_urls = input->GetArray("urls");
if (!input_urls)
return nullptr;
// For each urlString of input["urls"]...
urls.ReserveInitialCapacity(input_urls->size());
for (wtf_size_t i = 0; i < input_urls->size(); ++i) {
String url_string;
if (!input_urls->at(i)->AsString(&url_string))
return nullptr;
// Let parsedURL be the result of parsing urlString with baseURL.
// If parsedURL is failure, then continue.
KURL parsed_url(base_url_to_parse, url_string);
if (!parsed_url.IsValid() || !parsed_url.ProtocolIsInHTTPFamily())
continue;
urls.push_back(std::move(parsed_url));
}
}
DocumentRulePredicate* document_rule_predicate = nullptr;
if (source == "document") {
DCHECK(document_rules_enabled);
// If input["urls"] exists, then return null.
if (input->Get("urls"))
return nullptr;
// If input["where"] does not exist, then set predicate to a document rule
// conjunction whose clauses is an empty list.
if (!input->Get("where")) {
document_rule_predicate = DocumentRulePredicate::MakeDefaultPredicate();
}
// Otherwise, set predicate to the result of parsing a document rule
// predicate given input["where"] and baseURL.
else {
// "relative_to" outside the "href_matches" clause is not allowed for
// document rules.
if (input->Get("relative_to"))
return nullptr;
document_rule_predicate =
DocumentRulePredicate::Parse(input->GetJSONObject("where"), base_url,
context, IGNORE_EXCEPTION_FOR_TESTING);
}
if (!document_rule_predicate)
return nullptr;
}
// Let requirements be an empty ordered set.
// If input["requires"] exists, but is not a list, then return null.
JSONValue* requirements = input->Get("requires");
if (requirements && requirements->GetType() != JSONValue::kTypeArray)
return nullptr;
// For each requirement of input["requires"]...
SpeculationRule::RequiresAnonymousClientIPWhenCrossOrigin
requires_anonymous_client_ip(false);
if (JSONArray* requirements_array = JSONArray::Cast(requirements)) {
for (wtf_size_t i = 0; i < requirements_array->size(); ++i) {
String requirement;
if (!requirements_array->at(i)->AsString(&requirement))
return nullptr;
if (requirement == "anonymous-client-ip-when-cross-origin") {
requires_anonymous_client_ip =
SpeculationRule::RequiresAnonymousClientIPWhenCrossOrigin(true);
} else {
return nullptr;
}
}
}
// Let targetHint be null.
absl::optional<mojom::blink::SpeculationTargetHint> target_hint;
// If input["target_hint"] exists:
JSONValue* target_hint_value = input->Get("target_hint");
if (target_hint_value) {
// If input["target_hint"] is not a valid browsing context name or keyword,
// then return null.
// Set targetHint to input["target_hint"].
String target_hint_str;
if (!target_hint_value->AsString(&target_hint_str))
return nullptr;
if (!IsValidBrowsingContextNameOrKeyword(target_hint_str))
return nullptr;
// Currently only "_blank" and "_self" are supported.
// TODO(https://crbug.com/1354049): Support more browsing context names and
// keywords.
if (EqualIgnoringASCIICase(target_hint_str, "_blank")) {
target_hint = mojom::blink::SpeculationTargetHint::kBlank;
} else if (EqualIgnoringASCIICase(target_hint_str, "_self")) {
target_hint = mojom::blink::SpeculationTargetHint::kSelf;
} else {
target_hint = mojom::blink::SpeculationTargetHint::kNoHint;
}
}
absl::optional<network::mojom::ReferrerPolicy> referrer_policy;
JSONValue* referrer_policy_value = input->Get("referrer_policy");
if (referrer_policy_value) {
// Feature gated due to known keys check above.
DCHECK(RuntimeEnabledFeatures::SpeculationRulesReferrerPolicyKeyEnabled(
context));
String referrer_policy_str;
if (!referrer_policy_value->AsString(&referrer_policy_str))
return nullptr;
if (!referrer_policy_str.empty()) {
network::mojom::ReferrerPolicy referrer_policy_out =
network::mojom::ReferrerPolicy::kDefault;
if (!SecurityPolicy::ReferrerPolicyFromString(
referrer_policy_str, kDoNotSupportReferrerPolicyLegacyKeywords,
&referrer_policy_out)) {
return nullptr;
}
DCHECK_NE(referrer_policy_out, network::mojom::ReferrerPolicy::kDefault);
referrer_policy = referrer_policy_out;
}
}
absl::optional<mojom::blink::SpeculationEagerness> eagerness;
if (JSONValue* eagerness_value = input->Get("eagerness")) {
// Feature gated due to known keys check above.
DCHECK(RuntimeEnabledFeatures::SpeculationRulesEagernessEnabled(context));
String eagerness_str;
if (!eagerness_value->AsString(&eagerness_str)) {
return nullptr;
}
if (eagerness_str == "eager") {
eagerness = mojom::blink::SpeculationEagerness::kEager;
} else if (eagerness_str == "moderate") {
eagerness = mojom::blink::SpeculationEagerness::kModerate;
} else if (eagerness_str == "conservative") {
eagerness = mojom::blink::SpeculationEagerness::kConservative;
} else {
return nullptr;
}
}
return MakeGarbageCollected<SpeculationRule>(
std::move(urls), document_rule_predicate, requires_anonymous_client_ip,
target_hint, referrer_policy, eagerness);
}
} // namespace
// ---- SpeculationRuleSet::Source implementation ----
SpeculationRuleSet::Source::Source(const String& source_text,
Document& document)
: source_text_(source_text),
base_url_(absl::nullopt),
document_(document) {}
SpeculationRuleSet::Source::Source(const String& source_text,
const KURL& base_url)
: source_text_(source_text), base_url_(base_url), document_(nullptr) {}
const String& SpeculationRuleSet::Source::GetSourceText() const {
return source_text_;
}
KURL SpeculationRuleSet::Source::GetBaseURL() const {
if (base_url_) {
DCHECK(!document_);
return base_url_.value();
}
DCHECK(document_);
return document_->BaseURL();
}
void SpeculationRuleSet::Source::Trace(Visitor* visitor) const {
visitor->Trace(document_);
}
// ---- SpeculationRuleSet implementation ----
namespace {
// If enabled, allows non-standard JSON comments in speculation rules.
// TODO(crbug.com/1264024): Remove this feature if no issues arose with
// deprecating it.
BASE_FEATURE(kSpeculationRulesJSONComments,
"SpeculationRulesJSONComments",
base::FEATURE_DISABLED_BY_DEFAULT);
} // namespace
// static
SpeculationRuleSet* SpeculationRuleSet::Parse(Source* source,
ExecutionContext* context,
String* out_error) {
// https://wicg.github.io/nav-speculation/speculation-rules.html#parse-speculation-rules
const String& source_text = source->GetSourceText();
const KURL& base_url = source->GetBaseURL();
// Let parsed be the result of parsing a JSON string to an Infra value given
// input.
// TODO(crbug.com/1264024): Deprecate JSON comments here, if possible.
JSONParseError parse_error;
auto parsed = JSONObject::From(
base::FeatureList::IsEnabled(kSpeculationRulesJSONComments)
? ParseJSONWithCommentsDeprecated(source_text, &parse_error)
: ParseJSON(source_text, &parse_error));
// If parsed is not a map, then return null.
if (!parsed) {
if (out_error) {
*out_error = parse_error.type != JSONParseErrorType::kNoError
? parse_error.message
: "Parsed JSON must be an object.";
}
return nullptr;
}
// Let result be an empty speculation rule set.
SpeculationRuleSet* result = MakeGarbageCollected<SpeculationRuleSet>();
result->source_ = source;
const auto parse_for_action =
[&](const char* key, HeapVector<Member<SpeculationRule>>& destination,
bool allow_target_hint) {
JSONArray* array = parsed->GetArray(key);
if (!array)
return;
for (wtf_size_t i = 0; i < array->size(); ++i) {
// If prefetch/prerenderRule is not a map, then continue.
JSONObject* input_rule = JSONObject::Cast(array->at(i));
if (!input_rule)
continue;
// Let rule be the result of parsing a speculation rule given
// prefetch/prerenderRule and baseURL.
SpeculationRule* rule =
ParseSpeculationRule(input_rule, base_url, context);
// If rule is null, then continue.
if (!rule)
continue;
// If rule's target browsing context name hint is not null, then
// continue.
if (!allow_target_hint &&
rule->target_browsing_context_name_hint().has_value()) {
continue;
}
if (rule->predicate())
result->has_document_rule_ = true;
// Append rule to result's prefetch/prerender rules.
destination.push_back(rule);
}
};
// If parsed["prefetch"] exists and is a list, then for each...
parse_for_action("prefetch", result->prefetch_rules_, false);
// If parsed["prefetch_with_subresources"] exists and is a list, then for
// each...
parse_for_action("prefetch_with_subresources",
result->prefetch_with_subresources_rules_, false);
// If parsed["prerender"] exists and is a list, then for each...
parse_for_action("prerender", result->prerender_rules_, true);
return result;
}
void SpeculationRuleSet::Trace(Visitor* visitor) const {
visitor->Trace(prefetch_rules_);
visitor->Trace(prefetch_with_subresources_rules_);
visitor->Trace(prerender_rules_);
visitor->Trace(source_);
}
} // namespace blink