blob: 32bd0dfd13eb3449b281520f9b40a075a3d36260 [file] [log] [blame]
/*
* Copyright (C) 2025 Shopify Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "SpeculationRules.h"
#include "Node.h"
#include "ReferrerPolicy.h"
#include <algorithm>
#include <wtf/HashSet.h>
#include <wtf/JSONValues.h>
#include <wtf/URL.h>
#include <wtf/URLHash.h>
#include <wtf/Vector.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/WTFString.h>
namespace WebCore {
Ref<SpeculationRules> SpeculationRules::create()
{
return adoptRef(*new SpeculationRules);
}
SpeculationRules::DocumentPredicate::DocumentPredicate(PredicateVariant&& value)
: m_value(WTF::move(value))
{
}
const SpeculationRules::DocumentPredicate::PredicateVariant& SpeculationRules::DocumentPredicate::value() const
{
return m_value;
}
static std::optional<Vector<String>> parseStringOrStringList(JSON::Object& object, const String& key)
{
Vector<String> result;
auto value = object.getValue(key);
if (!value)
return Vector<String> { };
if (value->type() == JSON::Value::Type::String) {
String stringValue = value->asString();
if (!stringValue.isNull()) {
result.append(stringValue);
return result;
}
}
if (RefPtr arrayValue = value->asArray()) {
for (auto& item : *arrayValue) {
if (item->type() == JSON::Value::Type::String) {
String element = item->asString();
if (element.isNull())
return std::nullopt;
result.append(element);
} else
return std::nullopt;
}
return result;
}
return Vector<String> { };
}
static std::optional<SpeculationRules::DocumentPredicate> parseDocumentPredicate(JSON::Object&);
// https://html.spec.whatwg.org/C#document-rule-predicate
static std::optional<SpeculationRules::DocumentPredicate> parseDocumentPredicate(JSON::Object& object)
{
auto andValue = object.getValue("and"_s);
if (andValue && andValue->type() == JSON::Value::Type::Array) {
if (auto array = andValue->asArray()) {
SpeculationRules::Conjunction conjunction;
for (auto& item : *array) {
auto clauseObject = item->asObject();
if (!clauseObject)
return std::nullopt;
auto predicate = parseDocumentPredicate(*clauseObject);
if (!predicate)
return std::nullopt;
conjunction.clauses.append(WTF::move(*predicate));
}
return { { Box<SpeculationRules::Conjunction>::create(WTF::move(conjunction)) } };
}
}
auto orValue = object.getValue("or"_s);
if (orValue && orValue->type() == JSON::Value::Type::Array) {
if (auto array = orValue->asArray()) {
SpeculationRules::Disjunction disjunction;
for (auto& item : *array) {
auto clauseObject = item->asObject();
if (!clauseObject)
return std::nullopt;
auto predicate = parseDocumentPredicate(*clauseObject);
if (!predicate)
return std::nullopt;
disjunction.clauses.append(WTF::move(*predicate));
}
return { { Box<SpeculationRules::Disjunction>::create(WTF::move(disjunction)) } };
}
}
auto notValue = object.getValue("not"_s);
if (notValue && notValue->type() == JSON::Value::Type::Object) {
if (auto clauseObject = notValue->asObject()) {
auto predicate = parseDocumentPredicate(*clauseObject);
if (!predicate)
return std::nullopt;
SpeculationRules::Negation negation { Box<SpeculationRules::DocumentPredicate>::create(WTF::move(*predicate)) };
return { { Box<SpeculationRules::Negation>::create(WTF::move(negation)) } };
}
}
SpeculationRules::URLPatternPredicate urlPredicate;
auto urlMatches = parseStringOrStringList(object, "url_matches"_s);
if (urlMatches)
urlPredicate.patterns.appendVector(*urlMatches);
auto hrefMatches = parseStringOrStringList(object, "href_matches"_s);
if (hrefMatches)
urlPredicate.patterns.appendVector(*hrefMatches);
SpeculationRules::CSSSelectorPredicate selectorPredicate;
auto selectorMatches = parseStringOrStringList(object, "selector_matches"_s);
if (selectorMatches)
selectorPredicate.selectors.appendVector(*selectorMatches);
bool hasURLPredicate = !urlPredicate.patterns.isEmpty();
bool hasSelectorPredicate = !selectorPredicate.selectors.isEmpty();
if (hasURLPredicate && hasSelectorPredicate) {
SpeculationRules::Conjunction conjunction;
conjunction.clauses.append(SpeculationRules::DocumentPredicate { WTF::move(urlPredicate) });
conjunction.clauses.append(SpeculationRules::DocumentPredicate { WTF::move(selectorPredicate) });
return { { Box<SpeculationRules::Conjunction>::create(WTF::move(conjunction)) } };
}
if (hasURLPredicate)
return { { WTF::move(urlPredicate) } };
if (hasSelectorPredicate)
return { { WTF::move(selectorPredicate) } };
return std::nullopt;
}
// https://html.spec.whatwg.org/C#parse-a-speculation-rule
static std::optional<SpeculationRules::Rule> parseSingleRule(const JSON::Object& input, const String& rulesetLevelTag, const URL& rulesetBaseURL, const URL& documentBaseURL)
{
const HashSet<String> allowedKeys = {
"source"_s, "urls"_s, "where"_s, "requires"_s, "target_hint"_s,
"referrer_policy"_s, "relative_to"_s, "eagerness"_s,
"expects_no_vary_search"_s, "tag"_s
};
for (const auto& key : input.keys()) {
if (!allowedKeys.contains(key))
return std::nullopt;
}
String source;
auto sourceValue = input.getValue("source"_s);
if (sourceValue && sourceValue->type() == JSON::Value::Type::String)
source = sourceValue->asString();
if (source.isEmpty()) {
bool hasURLs = !!input.getValue("urls"_s);
bool hasWhere = !!input.getValue("where"_s);
if (hasURLs && !hasWhere)
source = "list"_s;
else if (hasWhere && !hasURLs)
source = "document"_s;
else
return std::nullopt;
}
SpeculationRules::Rule rule;
if (source == "list"_s) {
if (input.getValue("where"_s))
return std::nullopt;
auto urlsValue = input.getValue("urls"_s);
if (!urlsValue || urlsValue->type() != JSON::Value::Type::Array)
return std::nullopt;
auto urlsArray = urlsValue->asArray();
if (!urlsArray)
return std::nullopt;
URL currentBaseURL = rulesetBaseURL;
auto relativeToValue = input.getValue("relative_to"_s);
if (relativeToValue && relativeToValue->type() == JSON::Value::Type::String) {
String relativeTo = relativeToValue->asString();
if (relativeTo != "ruleset"_s && relativeTo != "document"_s)
return std::nullopt;
if (relativeTo == "document"_s)
currentBaseURL = documentBaseURL;
}
for (const auto& urlValue : *urlsArray) {
if (urlValue->type() != JSON::Value::Type::String)
return std::nullopt;
String urlString = urlValue->asString();
URL parsedURL(currentBaseURL, urlString);
if (parsedURL.isValid() && (parsedURL.protocolIs("http"_s) || parsedURL.protocolIs("https"_s)))
rule.urls.append(parsedURL);
}
rule.eagerness = SpeculationRules::Eagerness::Immediate;
} else if (source == "document"_s) {
if (input.getValue("urls"_s) || input.getValue("relative_to"_s))
return std::nullopt;
auto whereValue = input.getValue("where"_s);
if (whereValue && whereValue->type() == JSON::Value::Type::Object) {
if (auto whereObject = whereValue->asObject()) {
auto predicate = parseDocumentPredicate(*whereObject);
if (!predicate)
return std::nullopt;
rule.predicate = WTF::move(*predicate);
}
} else {
// No "where" means match all links, which is an empty conjunction.
rule.predicate = { { Box<SpeculationRules::Conjunction>::create() } };
}
rule.eagerness = SpeculationRules::Eagerness::Conservative;
} else
return std::nullopt;
auto requiresValue = input.getValue("requires"_s);
if (requiresValue && requiresValue->type() == JSON::Value::Type::Array) {
if (auto requiresArray = requiresValue->asArray()) {
for (const auto& reqValue : *requiresArray) {
if (reqValue->type() != JSON::Value::Type::String)
return std::nullopt;
String requirement = reqValue->asString();
if (requirement != "anonymous-client-ip-when-cross-origin"_s)
return std::nullopt;
rule.requirements.append(requirement);
}
}
}
auto referrerPolicyValue = input.getValue("referrer_policy"_s);
if (referrerPolicyValue && referrerPolicyValue->type() == JSON::Value::Type::String) {
String referrerPolicyString = referrerPolicyValue->asString();
if (!referrerPolicyString.isEmpty()) {
rule.referrerPolicy = parseReferrerPolicy(referrerPolicyString, ReferrerPolicySource::SpeculationRules);
// 15.1. If input["referrer_policy"] is not a referrer policy... Return null.
if (!rule.referrerPolicy)
return std::nullopt;
}
}
auto eagernessValue = input.getValue("eagerness"_s);
if (eagernessValue && eagernessValue->type() == JSON::Value::Type::String) {
String eagernessString = eagernessValue->asString();
if (eagernessString == "immediate"_s)
rule.eagerness = SpeculationRules::Eagerness::Immediate;
else if (eagernessString == "eager"_s)
rule.eagerness = SpeculationRules::Eagerness::Eager;
else if (eagernessString == "moderate"_s)
rule.eagerness = SpeculationRules::Eagerness::Moderate;
else if (eagernessString == "conservative"_s)
rule.eagerness = SpeculationRules::Eagerness::Conservative;
else
return std::nullopt;
}
auto noVarySearchValue = input.getValue("expects_no_vary_search"_s);
if (noVarySearchValue && noVarySearchValue->type() == JSON::Value::Type::String)
rule.noVarySearchHint = noVarySearchValue->asString();
if (!rulesetLevelTag.isNull())
rule.tags.append(rulesetLevelTag);
// 18. If input["tag"] exists:
auto tagValue = input.getValue("tag"_s);
if (tagValue) {
// 18.1. If input["tag"] is not a speculation rule tag... return null.
if (tagValue->type() != JSON::Value::Type::String)
return std::nullopt;
String ruleTag = tagValue->asString();
if (!ruleTag.containsOnlyASCII() || !ruleTag.containsOnly<isASCIIPrintable>())
return std::nullopt;
StringBuilder ruleTagBuilder;
ruleTagBuilder.appendQuotedJSONString(ruleTag);
String processedRuleTag = ruleTagBuilder.toString();
// 18.2 Append input["tag"] to tags if it's different from ruleset tag.
if (processedRuleTag != rulesetLevelTag)
rule.tags.append(processedRuleTag);
}
// 19. If tags is empty, then append null to tags.
if (rule.tags.isEmpty())
rule.tags.append(nullAtom());
return rule;
}
// https://html.spec.whatwg.org/C#parse-a-speculation-rule-set-string step 8
static std::optional<Vector<SpeculationRules::Rule>> parseRules(const JSON::Object& object, const String& key, const String& rulesetLevelTag, const URL& rulesetBaseURL, const URL& documentBaseURL)
{
auto value = object.getValue(key);
// 8.1 If parsed[type] exists:
if (!value || value->type() != JSON::Value::Type::Array)
return Vector<SpeculationRules::Rule> { };
auto array = value->asArray();
if (!array)
return Vector<SpeculationRules::Rule> { };
Vector<SpeculationRules::Rule> rules;
for (auto& item : *array) {
// 8.1.1 If parsed[type] is a list, then for each rule of parsed[type]:
auto ruleObject = item->asObject();
if (!ruleObject)
return std::nullopt;
// 8.1.1.1 Let rule be the result of parsing a speculation rule given rule, tag, document, and baseURL.
if (auto rule = parseSingleRule(*ruleObject, rulesetLevelTag, rulesetBaseURL, documentBaseURL))
rules.append(WTF::move(*rule));
// 8.1.1.2. If rule is null, then continue.
}
return rules;
}
// https://html.spec.whatwg.org/C#parse-a-speculation-rule-set-string
bool SpeculationRules::parseSpeculationRules(Node& sourceNode, const StringView& text, const URL& rulesetBaseURL, const URL& documentBaseURL)
{
// 1. Let parsed be the result of parsing a JSON string to an Infra value given input.
auto jsonValue = JSON::Value::parseJSON(text);
if (!jsonValue)
return false;
// 2. If parsed is not a map, then throw a TypeError indicating that the top-level value needs to be a JSON object.
auto jsonObject = jsonValue->asObject();
if (!jsonObject)
return false;
String rulesetLevelTag;
auto tagValue = jsonObject->getValue("tag"_s);
// 5. If parsed["tag"] exists:
if (tagValue) {
// 5.1. If parsed["tag"] is not a speculation rule tag, then throw a TypeError indicating that the speculation rule tag is invalid.
if (tagValue->type() != JSON::Value::Type::String)
return false;
String candidateTag = tagValue->asString();
if (!candidateTag.containsOnlyASCII() || !candidateTag.containsOnly<isASCIIPrintable>())
return false;
StringBuilder ruleTagBuilder;
ruleTagBuilder.appendQuotedJSONString(candidateTag);
// 5.2. Set tag to parsed["tag"].
rulesetLevelTag = ruleTagBuilder.toString();
}
auto prefetch = parseRules(*jsonObject, "prefetch"_s, rulesetLevelTag, rulesetBaseURL, documentBaseURL);
if (prefetch) {
auto& nodeRules = m_prefetchRulesByNode.ensure(sourceNode, [] {
return Vector<Rule>();
}).iterator->value;
nodeRules.appendVector(WTF::move(*prefetch));
}
return true;
}
// https://html.spec.whatwg.org/C#unregister-speculation-rules
Vector<URL> SpeculationRules::unregisterSpeculationRules(Node& sourceNode)
{
Vector<URL> removedURLs;
auto it = m_prefetchRulesByNode.find(sourceNode);
if (it != m_prefetchRulesByNode.end()) {
for (const auto& rule : it->value) {
for (const auto& url : rule.urls)
removedURLs.append(url);
}
m_prefetchRulesByNode.remove(it);
}
return removedURLs;
}
} // namespace WebCore