blob: 68e4c2eeec7e4ca366bca9aa7496811f3e10a725 [file] [log] [blame]
// 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 "third_party/blink/renderer/core/speculation_rules/document_rule_predicate.h"
#include "base/containers/contains.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_urlpatterninit_usvstring.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_url_pattern_init.h"
#include "third_party/blink/renderer/core/css/parser/css_parser.h"
#include "third_party/blink/renderer/core/css/style_rule.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/html/html_anchor_element.h"
#include "third_party/blink/renderer/core/url_pattern/url_pattern.h"
#include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
#include "third_party/blink/renderer/platform/heap/member.h"
#include "third_party/blink/renderer/platform/json/json_values.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
namespace blink {
namespace {
// Represents a document rule conjunction:
// https://wicg.github.io/nav-speculation/speculation-rules.html#document-rule-conjunction.
class Conjunction : public DocumentRulePredicate {
public:
explicit Conjunction(HeapVector<Member<DocumentRulePredicate>> clauses)
: clauses_(std::move(clauses)) {}
~Conjunction() override = default;
bool Matches(const HTMLAnchorElement& el) const override {
return base::ranges::all_of(clauses_, [&](DocumentRulePredicate* clause) {
return clause->Matches(el);
});
}
String ToString() const override {
StringBuilder builder;
builder.Append("And(");
for (wtf_size_t i = 0; i < clauses_.size(); i++) {
builder.Append(clauses_[i]->ToString());
if (i != clauses_.size() - 1)
builder.Append(", ");
}
builder.Append(")");
return builder.ReleaseString();
}
Type GetTypeForTesting() const override { return Type::kAnd; }
HeapVector<Member<DocumentRulePredicate>> GetSubPredicatesForTesting()
const override {
return clauses_;
}
void Trace(Visitor* visitor) const override {
visitor->Trace(clauses_);
DocumentRulePredicate::Trace(visitor);
}
private:
HeapVector<Member<DocumentRulePredicate>> clauses_;
};
// Represents a document rule disjunction:
// https://wicg.github.io/nav-speculation/speculation-rules.html#document-rule-disjunction.
class Disjunction : public DocumentRulePredicate {
public:
explicit Disjunction(HeapVector<Member<DocumentRulePredicate>> clauses)
: clauses_(std::move(clauses)) {}
~Disjunction() override = default;
bool Matches(const HTMLAnchorElement& el) const override {
return base::ranges::any_of(clauses_, [&](DocumentRulePredicate* clause) {
return clause->Matches(el);
});
}
String ToString() const override {
StringBuilder builder;
builder.Append("Or(");
for (wtf_size_t i = 0; i < clauses_.size(); i++) {
builder.Append(clauses_[i]->ToString());
if (i != clauses_.size() - 1)
builder.Append(", ");
}
builder.Append(")");
return builder.ReleaseString();
}
Type GetTypeForTesting() const override { return Type::kOr; }
HeapVector<Member<DocumentRulePredicate>> GetSubPredicatesForTesting()
const override {
return clauses_;
}
void Trace(Visitor* visitor) const override {
visitor->Trace(clauses_);
DocumentRulePredicate::Trace(visitor);
}
private:
HeapVector<Member<DocumentRulePredicate>> clauses_;
};
// Represents a document rule negation:
// https://wicg.github.io/nav-speculation/speculation-rules.html#document-rule-negation.
class Negation : public DocumentRulePredicate {
public:
explicit Negation(DocumentRulePredicate* clause) : clause_(clause) {}
~Negation() override = default;
bool Matches(const HTMLAnchorElement& el) const override {
return !clause_->Matches(el);
}
String ToString() const override {
StringBuilder builder;
builder.Append("Not(");
builder.Append(clause_->ToString());
builder.Append(")");
return builder.ReleaseString();
}
Type GetTypeForTesting() const override { return Type::kNot; }
HeapVector<Member<DocumentRulePredicate>> GetSubPredicatesForTesting()
const override {
HeapVector<Member<DocumentRulePredicate>> result;
result.push_back(clause_);
return result;
}
void Trace(Visitor* visitor) const override {
visitor->Trace(clause_);
DocumentRulePredicate::Trace(visitor);
}
private:
Member<DocumentRulePredicate> clause_;
};
} // namespace
// Represents a document rule URL pattern predicate:
// https://wicg.github.io/nav-speculation/speculation-rules.html#document-rule-url-pattern-predicate
class URLPatternPredicate : public DocumentRulePredicate {
public:
explicit URLPatternPredicate(HeapVector<Member<URLPattern>> patterns)
: patterns_(std::move(patterns)) {}
~URLPatternPredicate() override = default;
bool Matches(const HTMLAnchorElement& el) const override {
// Let href be the result of running el’s href getter steps.
const KURL href = el.HrefURL();
// For each pattern of predicate’s patterns:
for (const auto& pattern : patterns_) {
// Match given pattern and href. If the result is not null, return true.
if (pattern->test(/*script_state=*/nullptr,
MakeGarbageCollected<V8URLPatternInput>(href),
ASSERT_NO_EXCEPTION))
return true;
}
return false;
}
String ToString() const override {
StringBuilder builder;
builder.Append("Href([");
for (wtf_size_t i = 0; i < patterns_.size(); i++) {
builder.Append(patterns_[i]->ToString());
if (i != patterns_.size() - 1)
builder.Append(", ");
}
builder.Append("])");
return builder.ReleaseString();
}
Type GetTypeForTesting() const override { return Type::kURLPatterns; }
HeapVector<Member<URLPattern>> GetURLPatternsForTesting() const override {
return patterns_;
}
void Trace(Visitor* visitor) const override {
visitor->Trace(patterns_);
DocumentRulePredicate::Trace(visitor);
}
private:
HeapVector<Member<URLPattern>> patterns_;
};
// Represents a document rule CSS selector predicate:
// https://wicg.github.io/nav-speculation/speculation-rules.html#document-rule-css-selector-predicate
class CSSSelectorPredicate : public DocumentRulePredicate {
public:
explicit CSSSelectorPredicate(HeapVector<Member<StyleRule>> style_rules)
: style_rules_(std::move(style_rules)) {}
bool Matches(const HTMLAnchorElement& link) const override {
// TODO(crbug.com/1371522): Implement this.
return false;
}
String ToString() const override {
StringBuilder builder;
builder.Append("Selector([");
for (wtf_size_t i = 0; i < style_rules_.size(); i++) {
builder.Append(style_rules_[i]->SelectorsText());
if (i != style_rules_.size() - 1) {
builder.Append(", ");
}
}
builder.Append("])");
return builder.ReleaseString();
}
Type GetTypeForTesting() const override { return Type::kCSSSelectors; }
HeapVector<Member<StyleRule>> GetStyleRulesForTesting() const override {
return style_rules_;
}
void Trace(Visitor* visitor) const override {
visitor->Trace(style_rules_);
DocumentRulePredicate::Trace(visitor);
}
private:
// TODO(crbug.com/1371522): If we do not end up integrating with the
// style engine, change this to use CSSSelectorList instead of StyleRule to
// save space.
HeapVector<Member<StyleRule>> style_rules_;
};
namespace {
URLPattern* ParseRawPattern(JSONValue* raw_pattern,
const KURL& base_url,
ExceptionState& exception_state) {
// If rawPattern is a string, then:
if (String raw_string; raw_pattern->AsString(&raw_string)) {
// Set pattern to the result of constructing a URLPattern using the
// URLPattern(input, baseURL) constructor steps given rawPattern and
// serializedBaseURL.
V8URLPatternInput* url_pattern_input =
MakeGarbageCollected<V8URLPatternInput>(raw_string);
return URLPattern::Create(url_pattern_input, base_url, exception_state);
}
// Otherwise, if rawPattern is a map
if (JSONObject* pattern_object = JSONObject::Cast(raw_pattern)) {
// Let init be «[ "baseURL" → serializedBaseURL ]», representing a
// dictionary of type URLPatternInit.
URLPatternInit* init = URLPatternInit::Create();
init->setBaseURL(base_url);
// For each key -> value of rawPattern:
for (wtf_size_t i = 0; i < pattern_object->size(); i++) {
JSONObject::Entry entry = pattern_object->at(i);
String key = entry.first;
String value;
// If value is not a string
if (!entry.second->AsString(&value))
return nullptr;
// Set init[key] to value.
if (key == "protocol")
init->setProtocol(value);
else if (key == "username")
init->setUsername(value);
else if (key == "password")
init->setPassword(value);
else if (key == "hostname")
init->setHostname(value);
else if (key == "port")
init->setPort(value);
else if (key == "pathname")
init->setPathname(value);
else if (key == "search")
init->setSearch(value);
else if (key == "hash")
init->setHash(value);
else if (key == "baseURL")
init->setBaseURL(value);
else
return nullptr;
}
// Set pattern to the result of constructing a URLPattern using the
// URLPattern(input, baseURL) constructor steps given init.
V8URLPatternInput* url_pattern_input =
MakeGarbageCollected<V8URLPatternInput>(init);
return URLPattern::Create(url_pattern_input, exception_state);
}
return nullptr;
}
String GetPredicateType(JSONObject* input) {
String predicate_type;
constexpr const char* kValidTypes[] = {"and", "or", "not", "href_matches",
"selector_matches"};
for (String type : kValidTypes) {
if (input->Get(type)) {
// If we'd already found one, then this is ambiguous.
if (!predicate_type.IsNull())
return String();
// Otherwise, this is the predicate type.
predicate_type = std::move(type);
}
}
return predicate_type;
}
} // namespace
// static
DocumentRulePredicate* DocumentRulePredicate::Parse(
JSONObject* input,
const KURL& ruleset_base_url,
const ExecutionContext* execution_context,
ExceptionState& exception_state) {
// If input is not a map, then return null.
if (!input)
return nullptr;
// If we can't get a valid predicate type, return null.
String predicate_type = GetPredicateType(input);
if (predicate_type.IsNull())
return nullptr;
// If predicateType is "and" or "or"
if (predicate_type == "and" || predicate_type == "or") {
// "and" and "or" cannot be paired with any other keys.
if (input->size() != 1)
return nullptr;
// Let rawClauses be the input[predicateType].
blink::JSONArray* raw_clauses = input->GetArray(predicate_type);
// If rawClauses is not a list, then return null.
if (!raw_clauses)
return nullptr;
// Let clauses be an empty list.
HeapVector<Member<DocumentRulePredicate>> clauses;
clauses.ReserveInitialCapacity(raw_clauses->size());
// For each rawClause of rawClauses:
for (wtf_size_t i = 0; i < raw_clauses->size(); i++) {
JSONObject* raw_clause = JSONObject::Cast(raw_clauses->at(i));
// Let clause be the result of parsing a document rule predicate given
// rawClause and baseURL.
DocumentRulePredicate* clause = Parse(raw_clause, ruleset_base_url,
execution_context, exception_state);
// If clause is null, then return null.
if (!clause)
return nullptr;
// Append clause to clauses.
clauses.push_back(clause);
}
// If predicateType is "and", then return a document rule conjunction whose
// clauses is clauses.
if (predicate_type == "and")
return MakeGarbageCollected<Conjunction>(std::move(clauses));
// If predicateType is "or", then return a document rule disjunction whose
// clauses is clauses.
if (predicate_type == "or")
return MakeGarbageCollected<Disjunction>(std::move(clauses));
}
// If predicateType is "not"
if (predicate_type == "not") {
// "not" cannot be paired with any other keys.
if (input->size() != 1)
return nullptr;
// Let rawClause be the input[predicateType].
JSONObject* raw_clause = input->GetJSONObject(predicate_type);
// Let clause be the result of parsing a document rule predicate given
// rawClause and baseURL.
DocumentRulePredicate* clause =
Parse(raw_clause, ruleset_base_url, execution_context, exception_state);
// If clause is null, then return null.
if (!clause)
return nullptr;
// Return a document rule negation whose clause is clause.
return MakeGarbageCollected<Negation>(clause);
}
// If predicateType is "href_matches"
if (predicate_type == "href_matches") {
// Explainer:
// https://github.com/WICG/nav-speculation/blob/main/triggers.md#using-the-documents-base-url-for-external-speculation-rule-sets
// For now, use the ruleset's base URL to construct the predicates.
KURL base_url = ruleset_base_url;
const bool relative_to_enabled =
RuntimeEnabledFeatures::SpeculationRulesRelativeToDocumentEnabled(
execution_context);
for (wtf_size_t i = 0; i < input->size(); ++i) {
const String key = input->at(i).first;
if (key == "href_matches") {
// This is always expected.
} else if (key == "relative_to") {
const char* const kKnownRelativeToValues[] = {"ruleset", "document"};
String relative_to;
// If relativeTo is neither the string "ruleset" nor the string
// "document", then return null.
if (!relative_to_enabled ||
!input->GetString("relative_to", &relative_to) ||
!base::Contains(kKnownRelativeToValues, relative_to)) {
return nullptr;
}
// If relativeTo is "document", set baseURL to the document's
// document base URL.
if (relative_to == "document") {
base_url = execution_context->BaseURL();
}
} else {
// Otherwise, this is an unrecognized key. The predicate is invalid.
return nullptr;
}
}
// Let rawPatterns be input["href_matches"].
Vector<JSONValue*> raw_patterns;
JSONArray* href_matches = input->GetArray("href_matches");
if (href_matches) {
for (wtf_size_t i = 0; i < href_matches->size(); i++) {
raw_patterns.push_back(href_matches->at(i));
}
} else {
// If rawPatterns is not a list, then set rawPatterns to « rawPatterns ».
raw_patterns.push_back(input->Get("href_matches"));
}
// Let patterns be an empty list.
HeapVector<Member<URLPattern>> patterns;
// For each rawPattern of rawPatterns:
for (JSONValue* raw_pattern : raw_patterns) {
URLPattern* pattern =
ParseRawPattern(raw_pattern, base_url, exception_state);
// If those steps throw, catch the exception and return null.
if (exception_state.HadException()) {
exception_state.ClearException();
return nullptr;
}
if (!pattern)
return nullptr;
// Append pattern to patterns.
patterns.push_back(pattern);
}
// Return a document rule URL pattern predicate whose patterns is patterns.
return MakeGarbageCollected<URLPatternPredicate>(std::move(patterns));
}
// If predicateType is "selector_matches"
if (predicate_type == "selector_matches" && input->size() == 1) {
const bool selector_matches_enabled = RuntimeEnabledFeatures::
SpeculationRulesDocumentRulesSelectorMatchesEnabled(execution_context);
if (!selector_matches_enabled) {
return nullptr;
}
// Let rawSelectors be input["selector_matches"].
Vector<JSONValue*> raw_selectors;
JSONArray* selector_matches = input->GetArray("selector_matches");
if (selector_matches) {
for (wtf_size_t i = 0; i < selector_matches->size(); i++) {
raw_selectors.push_back(selector_matches->at(i));
}
} else {
// If rawSelectors is not a list, then set rawSelectors to « rawSelectors
// ».
raw_selectors.push_back(input->Get("selector_matches"));
}
// Let selectors be an empty list.
HeapVector<Member<StyleRule>> selectors;
HeapVector<CSSSelector> arena;
CSSPropertyValueSet* empty_properties =
ImmutableCSSPropertyValueSet::Create(nullptr, 0, kUASheetMode);
CSSParserContext* css_parser_context =
MakeGarbageCollected<CSSParserContext>(*execution_context);
for (auto* raw_selector : raw_selectors) {
String raw_selector_string;
// If rawSelector is not a string, then return null.
if (!raw_selector->AsString(&raw_selector_string)) {
return nullptr;
}
// Parse a selector from rawSelector. If the result is failure, then
// return null. Otherwise, let selector be the result.
base::span<CSSSelector> selector_vector = CSSParser::ParseSelector(
css_parser_context, nullptr, nullptr, raw_selector_string, arena);
if (selector_vector.empty()) {
return nullptr;
}
StyleRule* selector =
StyleRule::Create(selector_vector, empty_properties);
// Append selector to selectors.
selectors.push_back(std::move(selector));
}
return MakeGarbageCollected<CSSSelectorPredicate>(std::move(selectors));
}
return nullptr;
}
// static
DocumentRulePredicate* DocumentRulePredicate::MakeDefaultPredicate() {
return MakeGarbageCollected<Conjunction>(
HeapVector<Member<DocumentRulePredicate>>());
}
HeapVector<Member<DocumentRulePredicate>>
DocumentRulePredicate::GetSubPredicatesForTesting() const {
NOTREACHED();
return {};
}
HeapVector<Member<URLPattern>> DocumentRulePredicate::GetURLPatternsForTesting()
const {
NOTREACHED();
return {};
}
HeapVector<Member<StyleRule>> DocumentRulePredicate::GetStyleRulesForTesting()
const {
NOTREACHED();
return {};
}
void DocumentRulePredicate::Trace(Visitor*) const {}
} // namespace blink