
org.sonar.plugins.html.checks.accessibility.ValidAutocompleteCheck Maven / Gradle / Ivy
The newest version!
/*
* SonarQube HTML
* Copyright (C) 2010-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.plugins.html.checks.accessibility;
import org.sonar.check.Rule;
import org.sonar.plugins.html.api.accessibility.ControlGroup;
import org.sonar.plugins.html.checks.AbstractPageCheck;
import org.sonar.plugins.html.node.TagNode;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
@Rule(key = "S6840")
public class ValidAutocompleteCheck extends AbstractPageCheck {
/**
* A very naive and straightforward implementation of a validator capable of testing a candidate against a series
* of predicates until either one is matched or none is. Should totally be replaced by a validation library would
* we eventually decide to use one.
*/
static class Validator {
protected List> predicates;
public Validator(List> predicates) {
this.predicates = predicates;
}
boolean isValid(TagNode tagNode) {
var autocomplete = tagNode.getAttribute("autocomplete");
if (autocomplete == null) {
return true;
}
var tokens = isADirective(autocomplete) ? new String[]{autocomplete} : autocomplete.split("\\s+");
var numberOfPredicates = predicates.size();
if (tokens.length != numberOfPredicates) {
return false;
}
var isValid = true;
for (int i = 0; i < numberOfPredicates; i++) {
var predicate = predicates.get(i);
var token = tokens[i];
var candidate = new Candidate(token, tagNode);
isValid = isValid && predicate.test(candidate);
}
return isValid;
}
}
/**
* A class representing an autocomplete token as defined by the HTML specification - i.e. a value and a control
* group this value belongs to.
* See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
*/
static class Token {
private final String value;
private final Predicate controlGroupPredicate;
Token(
String value,
Predicate controlGroup
) {
this.value = value;
this.controlGroupPredicate = controlGroup;
}
}
/**
* A class representing an autocomplete value in the context of a tag - e.g. "cc-exp" in the context of an "input" -
* that can be validated as a whole.
*/
static class Candidate {
private final String value;
private final TagNode tagNode;
Candidate(
String value,
TagNode tagNode
) {
this.value = value;
this.tagNode = tagNode;
}
public boolean satisfies(Token token) {
/*
* Directives are considered as wildcards - i.e. they always match the passed token
*/
return (isADirective(this.value) || this.value.equalsIgnoreCase(token.value)) && token.controlGroupPredicate.test(this.tagNode);
}
}
static List validators = List.of(
new Validator(List.of(
candidate -> candidate.value.isBlank()
)),
new Validator(List.of(
candidate -> isADirective(candidate.value)
)),
new Validator(List.of(
ValidAutocompleteCheck::isAOnOffToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isASection,
ValidAutocompleteCheck::isAnAutofillFieldNameToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isASection,
ValidAutocompleteCheck::isAnAutofillFieldNameToken,
ValidAutocompleteCheck::isAWebAuthnToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isASection,
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAnAutofillFieldNameToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isASection,
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAnAutofillFieldNameToken,
ValidAutocompleteCheck::isAWebAuthnToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isASection,
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAMediumValueToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isASection,
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAMediumValueToken,
ValidAutocompleteCheck::isAWebAuthnToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isASection,
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAMediumTypeToken,
ValidAutocompleteCheck::isAMediumValueToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isASection,
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAMediumTypeToken,
ValidAutocompleteCheck::isAMediumValueToken,
ValidAutocompleteCheck::isAWebAuthnToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAnAutofillFieldNameToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAnAutofillFieldNameToken,
ValidAutocompleteCheck::isAWebAuthnToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAMediumTypeToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAMediumTypeToken,
ValidAutocompleteCheck::isAWebAuthnToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAMediumTypeToken,
ValidAutocompleteCheck::isAMediumValueToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAnAddressType,
ValidAutocompleteCheck::isAMediumTypeToken,
ValidAutocompleteCheck::isAMediumValueToken,
ValidAutocompleteCheck::isAWebAuthnToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAnAutofillFieldNameToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAnAutofillFieldNameToken,
ValidAutocompleteCheck::isAWebAuthnToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAMediumValueToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAMediumValueToken,
ValidAutocompleteCheck::isAWebAuthnToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAMediumTypeToken,
ValidAutocompleteCheck::isAMediumValueToken
)),
new Validator(List.of(
ValidAutocompleteCheck::isAMediumTypeToken,
ValidAutocompleteCheck::isAMediumValueToken,
ValidAutocompleteCheck::isAWebAuthnToken
))
);
static boolean isADirective(String value) {
return value.startsWith("<%=") || value.startsWith("");
}
static boolean isAOnOffToken(Candidate candidate) {
return Stream.of(
new Token("on", ControlGroup::belongsToAutofillExpectationMantleControlGroup),
new Token("off", ControlGroup::belongsToAutofillExpectationMantleControlGroup)
).anyMatch(candidate::satisfies);
}
static boolean isAMediumTypeToken(Candidate candidate) {
return Stream.of(
"home",
"work",
"mobile",
"fax",
"pager"
).anyMatch(item -> item.equalsIgnoreCase(candidate.value));
}
static boolean isAMediumValueToken(Candidate candidate) {
return Stream.of(
new Token("tel", ControlGroup::belongsToTelControlGroup),
new Token("tel-country-code", ControlGroup::belongsToTextControlGroup),
new Token("tel-national", ControlGroup::belongsToTextControlGroup),
new Token("tel-area-code", ControlGroup::belongsToTextControlGroup),
new Token("tel-local", ControlGroup::belongsToTextControlGroup),
new Token("tel-local-prefix", ControlGroup::belongsToTextControlGroup),
new Token("tel-local-suffix", ControlGroup::belongsToTextControlGroup),
new Token("tel-extension", ControlGroup::belongsToTextControlGroup),
new Token("email", ControlGroup::belongsToUsernameControlGroup),
new Token("impp", ControlGroup::belongsToUrlControlGroup)
).anyMatch(candidate::satisfies);
}
static boolean isASection(Candidate candidate) {
return candidate.value.toLowerCase().startsWith("section-");
}
static boolean isAnAddressType(Candidate candidate) {
return Stream.of(
"billing",
"shipping"
).anyMatch(item -> item.equalsIgnoreCase(candidate.value));
}
static boolean isAnAutofillFieldNameToken(Candidate candidate) {
return Stream.of(
new Token("name", ControlGroup::belongsToTextControlGroup),
new Token("honorific-prefix", ControlGroup::belongsToTextControlGroup),
new Token("given-name", ControlGroup::belongsToTextControlGroup),
new Token("additional-name", ControlGroup::belongsToTextControlGroup),
new Token("family-name", ControlGroup::belongsToTextControlGroup),
new Token("honorific-suffix", ControlGroup::belongsToTextControlGroup),
new Token("nickname", ControlGroup::belongsToTextControlGroup),
new Token("username", ControlGroup::belongsToUsernameControlGroup),
new Token("new-password", ControlGroup::belongsToPasswordControlGroup),
new Token("current-password", ControlGroup::belongsToPasswordControlGroup),
new Token("one-time-code", ControlGroup::belongsToPasswordControlGroup),
new Token("organization-title", ControlGroup::belongsToTextControlGroup),
new Token("organization", ControlGroup::belongsToTextControlGroup),
new Token("street-address", ControlGroup::belongsToMultilineControlGroup),
new Token("address-line1", ControlGroup::belongsToTextControlGroup),
new Token("address-line2", ControlGroup::belongsToTextControlGroup),
new Token("address-line3", ControlGroup::belongsToTextControlGroup),
new Token("address-level4", ControlGroup::belongsToTextControlGroup),
new Token("address-level3", ControlGroup::belongsToTextControlGroup),
new Token("address-level2", ControlGroup::belongsToTextControlGroup),
new Token("address-level1", ControlGroup::belongsToTextControlGroup),
new Token("country", ControlGroup::belongsToTextControlGroup),
new Token("country-name", ControlGroup::belongsToTextControlGroup),
new Token("postal-code", ControlGroup::belongsToTextControlGroup),
new Token("cc-name", ControlGroup::belongsToTextControlGroup),
new Token("cc-given-name", ControlGroup::belongsToTextControlGroup),
new Token("cc-additional-name", ControlGroup::belongsToTextControlGroup),
new Token("cc-family-name", ControlGroup::belongsToTextControlGroup),
new Token("cc-number", ControlGroup::belongsToTextControlGroup),
new Token("cc-exp", ControlGroup::belongsToMonthControlGroup),
new Token("cc-exp-month", ControlGroup::belongsToNumericControlGroup),
new Token("cc-exp-year", ControlGroup::belongsToMultilineControlGroup),
new Token("cc-csc", ControlGroup::belongsToTextControlGroup),
new Token("cc-type", ControlGroup::belongsToTextControlGroup),
new Token("transaction-currency", ControlGroup::belongsToTextControlGroup),
new Token("transaction-amount", ControlGroup::belongsToMultilineControlGroup),
new Token("language", ControlGroup::belongsToTextControlGroup),
new Token("bday", ControlGroup::belongsToDateControlGroup),
new Token("bday-day", ControlGroup::belongsToMultilineControlGroup),
new Token("bday-month", ControlGroup::belongsToMultilineControlGroup),
new Token("bday-year", ControlGroup::belongsToMultilineControlGroup),
new Token("sex", ControlGroup::belongsToTextControlGroup),
new Token("url", ControlGroup::belongsToUrlControlGroup),
new Token("photo", ControlGroup::belongsToUrlControlGroup)
).anyMatch(candidate::satisfies);
}
static boolean isAWebAuthnToken(Candidate candidate) {
return Stream.of(
new Token("webauthn", node -> node.getNodeName().equalsIgnoreCase("input") || node.getNodeName().equalsIgnoreCase("textarea"))
).anyMatch(candidate::satisfies);
}
@Override
public void startElement(TagNode node) {
var isValid = validators.stream().anyMatch(validator -> validator.isValid(node));
if (!isValid) {
createViolation(node, "DOM elements should use the \"autocomplete\" attribute correctly.");
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy