0

Fix autofill for labels pointing to shadow hosts

This patch exposes ListedElements within forms which cross shadow
boundaries via WebFormElement to allow the autofill code to track them
and consider autofilling them.

It is gated by a feature flag and is planned to be enabled via finch.

Bug: 649162
Change-Id: If500e1581fedb7b458e994ef44933dae72ff8253
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2694018
Commit-Queue: Joey Arhar <jarhar@chromium.org>
Reviewed-by: Christoph Schwering <schwering@google.com>
Reviewed-by: Mason Freed <masonf@chromium.org>
Reviewed-by: Chris Harrelson <chrishtr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#884636}
This commit is contained in:
Joey Arhar
2021-05-19 19:50:58 +00:00
committed by Chromium LUCI CQ
parent 50ee59282d
commit 211cd26846
15 changed files with 699 additions and 74 deletions

@ -78,6 +78,7 @@
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/switches.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/keycodes/dom_us_layout_data.h"
@ -268,7 +269,9 @@ content::RenderFrameHost* RenderFrameHostForName(
class AutofillInteractiveTestBase : public AutofillUiTest {
protected:
AutofillInteractiveTestBase()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
feature_list_.InitAndEnableFeature(blink::features::kAutofillShadowDOM);
}
public:
AutofillInteractiveTestBase(const AutofillInteractiveTestBase&) = delete;
@ -703,7 +706,10 @@ class AutofillInteractiveTestBase : public AutofillUiTest {
void TriggerFormFill(const std::string& field_name) {
FocusFieldByName(field_name);
TriggerFormFillAlreadyFocused();
}
void TriggerFormFillAlreadyFocused() {
// Start filling the first name field with "M" and wait for the popup to be
// shown.
SendKeyToPageAndWait(ui::DomKey::FromCharacter('M'), ui::DomCode::US_M,
@ -770,6 +776,8 @@ class AutofillInteractiveTestBase : public AutofillUiTest {
// The response to return for queries to |kTestUrlPath|
std::string test_url_content_;
base::test::ScopedFeatureList feature_list_;
};
const char AutofillInteractiveTestBase::kTestUrlPath[] =
@ -3509,6 +3517,61 @@ IN_PROC_BROWSER_TEST_F(AutofillDynamicFormInteractiveTest,
ExpectFieldValue("phone", "15125551234");
}
IN_PROC_BROWSER_TEST_F(AutofillInteractiveTest, ShadowDOM) {
CreateTestProfile();
GURL url =
embedded_test_server()->GetURL("a.com", "/autofill/shadowdom.html");
ASSERT_NO_FATAL_FAILURE(ui_test_utils::NavigateToURL(browser(), url));
bool result = false;
ASSERT_TRUE(
content::ExecuteScriptAndExtractBool(GetWebContents(),
R"( function onFocusHandler(e) {
e.target.removeEventListener(e.type, arguments.callee);
domAutomationController.send(true);
}
if (document.readyState === 'complete') {
var target = getNameElement();
target.addEventListener('focus', onFocusHandler);
target.focus();
} else {
domAutomationController.send(false);
})",
&result));
ASSERT_TRUE(result);
TriggerFormFillAlreadyFocused();
std::string name;
ASSERT_TRUE(content::ExecuteScriptAndExtractString(
GetWebContents(), "window.domAutomationController.send(getName())",
&name));
EXPECT_EQ("Milton C. Waddams", name) << " for field name";
std::string address;
ASSERT_TRUE(content::ExecuteScriptAndExtractString(
GetWebContents(), "window.domAutomationController.send(getAddress())",
&address));
EXPECT_EQ("4120 Freidrich Lane", address) << " for field address";
std::string city;
ASSERT_TRUE(content::ExecuteScriptAndExtractString(
GetWebContents(), "window.domAutomationController.send(getCity())",
&city));
EXPECT_EQ("Austin", city) << " for field city";
std::string state;
ASSERT_TRUE(content::ExecuteScriptAndExtractString(
GetWebContents(), "window.domAutomationController.send(getState())",
&state));
EXPECT_EQ("TX", state) << " for field state";
std::string zip;
ASSERT_TRUE(content::ExecuteScriptAndExtractString(
GetWebContents(), "window.domAutomationController.send(getZip())", &zip));
EXPECT_EQ("78744", zip) << " for field zip";
}
INSTANTIATE_TEST_SUITE_P(All,
AutofillDynamicFormReplacementInteractiveTest,
testing::Bool());

@ -0,0 +1,81 @@
<!DOCTYPE html>
<p>go to chrome://settings/addresses to set up autofill</p>
<p>autofill is not supported in file:// urls</p>
<form>
<div>
<label for=input1>name</label>
<span id=input1>
<template shadowroot=open>
<span>
<template shadowroot=open>
<input>
</template>
</span>
</template>
</span>
</div>
<div>
<label for=input2>address</label>
<span id=input2>
<template shadowroot=open>
<input>
</template>
</span>
</div>
<div>
<label for=input3>city</label>
<span id=input3>
<template shadowroot=open>
<input id=shadowinput name=shadowname>
</template>
</span>
</div>
<div>
<label for=input4>state</label>
<span id=input4>
<template shadowroot=open>
<select>
<option value=WA>WA</option>
<option value=CA>CA</option>
<option value=TX>TX</option>
</select>
</template>
</span>
</div>
<div>
<label for=input5>zip</label>
<span id=input5>
<template shadowroot=open>
<input id=shadowinput>
</template>
</span>
</div>
</form>
<script>
function getNameElement() {
return input1.shadowRoot.querySelector('span').shadowRoot.querySelector('input');
}
function getName() {
return getNameElement().value;
}
function getAddress() {
return input2.shadowRoot.querySelector('input').value;
}
function getCity() {
return input3.shadowRoot.querySelector('input').value;
}
function getState() {
return input4.shadowRoot.querySelector('select').value;
}
function getZip() {
return input5.shadowRoot.querySelector('input').value;
}
</script>

@ -80,6 +80,10 @@ const int kMaxLengthForSingleButtonTitle = 30;
// Maximal length of all button titles.
const int kMaxLengthForAllButtonTitles = 200;
// Number of shadow roots to traverse upwards when looking for relevant forms
// and labels of an input element inside a shadow root.
const int kMaxShadowLevelsUp = 2;
// Text features to detect form submission buttons. Features are selected based
// on analysis of real forms and their buttons.
// TODO(crbug.com/910546): Consider to add more features (e.g. non-English
@ -1292,28 +1296,77 @@ void PreviewFormField(const FormFieldData& data,
// pointer.
struct CompareByRendererId {
using is_transparent = void;
constexpr bool operator()(const FormFieldData* f,
const FormFieldData* g) const {
DCHECK(f && g);
return f->unique_renderer_id < g->unique_renderer_id;
bool operator()(const std::pair<FormFieldData*, ShadowFieldData>& f,
const std::pair<FormFieldData*, ShadowFieldData>& g) const {
DCHECK(f.first && g.first);
return f.first->unique_renderer_id < g.first->unique_renderer_id;
}
constexpr bool operator()(const FieldRendererId f,
const FormFieldData* g) const {
DCHECK(g);
return f < g->unique_renderer_id;
bool operator()(const FieldRendererId f,
const std::pair<FormFieldData*, ShadowFieldData>& g) const {
DCHECK(g.first);
return f < g.first->unique_renderer_id;
}
constexpr bool operator()(const FormFieldData* f, FieldRendererId g) const {
DCHECK(f);
return f->unique_renderer_id < g;
bool operator()(const std::pair<FormFieldData*, ShadowFieldData>& f,
FieldRendererId g) const {
DCHECK(f.first);
return f.first->unique_renderer_id < g;
}
};
// Searches field_set for a matching named element in the case that
// Label::CorrespondingControl from blink didn't return a matching form control
// element. Returns nullptr if no match was found.
FormFieldData* SearchForFormControlByName(
const std::u16string& target_name,
const base::flat_set<std::pair<FormFieldData*, ShadowFieldData>,
CompareByRendererId>& field_set) {
// Sometimes site authors will incorrectly specify the corresponding
// field element's name rather than its id, so we compensate here.
if (target_name.empty())
return nullptr;
// Look through the list for elements with this name. There can actually
// be more than one. In this case, the label may not be particularly
// useful, so just discard it.
FormFieldData* field_data = nullptr;
for (const auto& iter : field_set) {
if (iter.first->name == target_name) {
if (field_data) {
field_data = nullptr;
break;
}
field_data = iter.first;
}
}
if (field_data)
return field_data;
// If there is identifying information that will help us find the target
// form control in the form control's shadow host(s), look there too.
for (const auto& iter : field_set) {
for (const std::u16string& shadow_host_name :
iter.second.shadow_host_name_attributes) {
if (shadow_host_name == target_name)
return iter.first;
}
for (const std::u16string& shadow_host_id :
iter.second.shadow_host_id_attributes) {
if (shadow_host_id == target_name)
return iter.first;
}
}
return nullptr;
}
// Updates the FormFieldData::label of each field in `field_set` according to
// the <label> descendant of |form_or_fieldset|, if there is any. The extracted
// label is label.firstChild().nodeValue() of the label element.
void MatchLabelsAndFields(
const WebElement& form_or_fieldset,
const base::flat_set<FormFieldData*, CompareByRendererId>& field_set) {
const base::flat_set<std::pair<FormFieldData*, ShadowFieldData>,
CompareByRendererId>& field_set) {
static base::NoDestructor<WebString> kLabel("label");
static base::NoDestructor<WebString> kFor("for");
static base::NoDestructor<WebString> kHidden("hidden");
@ -1329,23 +1382,8 @@ void MatchLabelsAndFields(
FormFieldData* field_data = nullptr;
if (control.IsNull()) {
// Sometimes site authors will incorrectly specify the corresponding
// field element's name rather than its id, so we compensate here.
std::u16string element_name = label.GetAttribute(*kFor).Utf16();
if (element_name.empty())
continue;
// Look through the field set with this name. There can actually
// be more than one. In this case, the label may not be particularly
// useful, so just discard it.
for (FormFieldData* field : field_set) {
if (field->name == element_name) {
if (field_data) {
field_data = nullptr;
break;
}
field_data = field;
}
}
field_data = SearchForFormControlByName(label.GetAttribute(*kFor).Utf16(),
field_set);
} else if (control.IsFormControlElement()) {
WebFormControlElement form_control = control.To<WebFormControlElement>();
if (form_control.FormControlTypeForAutofill() == *kHidden)
@ -1355,7 +1393,7 @@ void MatchLabelsAndFields(
FieldRendererId(form_control.UniqueRendererFormControlId()));
if (iter == field_set.end())
continue;
field_data = *iter;
field_data = iter->first;
}
if (!field_data)
@ -1397,6 +1435,8 @@ bool FormOrFieldsetsToFormData(
// requirements and thus will be in the resulting |form|.
std::vector<bool> fields_extracted(control_elements.size(), false);
std::vector<ShadowFieldData> shadow_field_data;
// Extracts the fields from |control_elements| with |extract_mask| to
// |form_fields|. |fields_extracted| should have as many elements as
// |control_elements|, initialized to false. Returns true if the number of
@ -1407,9 +1447,12 @@ bool FormOrFieldsetsToFormData(
if (!IsAutofillableElement(control_element))
continue;
form->fields.push_back(FormFieldData());
form->fields.emplace_back(FormFieldData());
ShadowFieldData shadow_field;
WebFormControlElementToFormField(control_element, field_data_manager,
extract_mask, &form->fields.back());
extract_mask, &form->fields.back(),
&shadow_field);
shadow_field_data.emplace_back(shadow_field);
fields_extracted[i] = true;
// To reduce computational costs, we impose a maximum number of allowable
@ -1421,11 +1464,15 @@ bool FormOrFieldsetsToFormData(
}
{
std::vector<FormFieldData*> items;
for (FormFieldData& field : form->fields)
items.push_back(&field);
base::flat_set<FormFieldData*, CompareByRendererId> field_set(
std::move(items));
std::vector<std::pair<FormFieldData*, ShadowFieldData>> items;
DCHECK_EQ(form->fields.size(), shadow_field_data.size());
for (size_t i = 0; i < form->fields.size(); i++) {
items.emplace_back(
std::make_pair(&form->fields[i], std::move(shadow_field_data[i])));
}
base::flat_set<std::pair<FormFieldData*, ShadowFieldData>,
CompareByRendererId>
field_set(std::move(items));
if (form_element) {
MatchLabelsAndFields(*form_element, field_set);
@ -1538,6 +1585,39 @@ base::flat_map<FieldRendererId, size_t> BuildRendererIdToIndex(
return base::flat_map<FieldRendererId, size_t>(std::move(items));
}
std::string GetAutocompleteAttribute(const WebElement& element) {
static base::NoDestructor<WebString> kAutocomplete("autocomplete");
std::string autocomplete_attribute =
element.GetAttribute(*kAutocomplete).Utf8();
if (autocomplete_attribute.size() > kMaxDataLength) {
// Discard overly long attribute values to avoid DOS-ing the browser
// process. However, send over a default string to indicate that the
// attribute was present.
return "x-max-data-length-exceeded";
}
return autocomplete_attribute;
}
void FindFormElementUpShadowRoots(const WebElement& element,
WebFormElement* found_form_element) {
// If we are in shadowdom, then look to see if the host(s) are inside a form
// element we can use.
int levels_up = kMaxShadowLevelsUp;
for (WebElement host = element.OwnerShadowHost(); !host.IsNull() && levels_up;
host = host.OwnerShadowHost(), --levels_up) {
for (WebNode parent = host; !parent.IsNull();
parent = parent.ParentNode()) {
if (parent.IsElementNode()) {
WebElement parentElement = parent.To<WebElement>();
if (parentElement.HasHTMLTagName("form")) {
*found_form_element = parentElement.To<WebFormElement>();
return;
}
}
}
}
}
} // namespace
void GetDataListSuggestions(const WebInputElement& element,
@ -1740,11 +1820,11 @@ void WebFormControlElementToFormField(
const WebFormControlElement& element,
const FieldDataManager* field_data_manager,
ExtractMask extract_mask,
FormFieldData* field) {
FormFieldData* field,
ShadowFieldData* shadow_data) {
DCHECK(field);
DCHECK(!element.IsNull());
DCHECK(element.GetDocument().GetFrame());
static base::NoDestructor<WebString> kAutocomplete("autocomplete");
static base::NoDestructor<WebString> kName("name");
static base::NoDestructor<WebString> kRole("role");
static base::NoDestructor<WebString> kPlaceholder("placeholder");
@ -1763,13 +1843,7 @@ void WebFormControlElementToFormField(
FieldRendererId(element.UniqueRendererFormControlId());
field->form_control_ax_id = element.GetAxId();
field->form_control_type = element.FormControlTypeForAutofill().Utf8();
field->autocomplete_attribute = element.GetAttribute(*kAutocomplete).Utf8();
if (field->autocomplete_attribute.size() > kMaxDataLength) {
// Discard overly long attribute values to avoid DOS-ing the browser
// process. However, send over a default string to indicate that the
// attribute was present.
field->autocomplete_attribute = "x-max-data-length-exceeded";
}
field->autocomplete_attribute = GetAutocompleteAttribute(element);
if (base::LowerCaseEqualsASCII(element.GetAttribute(*kRole).Utf16(),
"presentation")) {
field->role = FormFieldData::RoleAttribute::kPresentation;
@ -1788,6 +1862,40 @@ void WebFormControlElementToFormField(
field->aria_label = GetAriaLabel(element.GetDocument(), element);
field->aria_description = GetAriaDescription(element.GetDocument(), element);
// Traverse up through shadow hosts to see if we can gather missing fields.
WebFormElement form_element_up_shadow_hosts;
FindFormElementUpShadowRoots(element, &form_element_up_shadow_hosts);
int levels_up = kMaxShadowLevelsUp;
for (WebElement host = element.OwnerShadowHost();
!host.IsNull() && levels_up &&
(!form_element_up_shadow_hosts.IsNull() &&
form_element_up_shadow_hosts.OwnerShadowHost() != host);
host = host.OwnerShadowHost(), --levels_up) {
std::u16string shadow_host_id = host.GetIdAttribute().Utf16();
if (shadow_data && !shadow_host_id.empty())
shadow_data->shadow_host_id_attributes.push_back(shadow_host_id);
std::u16string shadow_host_name = host.GetAttribute(*kName).Utf16();
if (shadow_data && !shadow_host_name.empty())
shadow_data->shadow_host_name_attributes.push_back(shadow_host_name);
if (field->id_attribute.empty())
field->id_attribute = host.GetIdAttribute().Utf16();
if (field->name_attribute.empty())
field->name_attribute = host.GetAttribute(*kName).Utf16();
if (field->name.empty()) {
field->name = field->name_attribute.empty() ? field->id_attribute
: field->name_attribute;
}
if (field->autocomplete_attribute.empty())
field->autocomplete_attribute = GetAutocompleteAttribute(host);
if (field->css_classes.empty() && host.HasAttribute(*kClass))
field->css_classes = host.GetAttribute(*kClass).Utf16();
if (field->aria_label.empty())
field->aria_label = GetAriaLabel(host.GetDocument(), host);
if (field->aria_description.empty())
field->aria_description = GetAriaDescription(host.GetDocument(), host);
}
if (!IsAutofillableElement(element))
return;
@ -1998,7 +2106,11 @@ bool FindFormAndFieldForFormControlElement(
extract_mask =
static_cast<ExtractMask>(EXTRACT_VALUE | EXTRACT_OPTIONS | extract_mask);
const WebFormElement form_element = element.Form();
WebFormElement form_element = element.Form();
if (form_element.IsNull())
FindFormElementUpShadowRoots(element, &form_element);
if (form_element.IsNull()) {
// No associated form, try the synthetic form for unowned form elements.
WebDocument document = element.GetDocument();
@ -2027,6 +2139,9 @@ std::vector<WebFormControlElement> FillForm(
const FormData& form,
const WebFormControlElement& element) {
WebFormElement form_element = element.Form();
if (form_element.IsNull())
FindFormElementUpShadowRoots(element, &form_element);
if (form_element.IsNull()) {
return ForEachMatchingUnownedFormField(element, form,
FILTER_ALL_NON_EDITABLE_ELEMENTS,
@ -2046,6 +2161,9 @@ std::vector<WebFormControlElement> PreviewForm(
const FormData& form,
const WebFormControlElement& element) {
WebFormElement form_element = element.Form();
if (form_element.IsNull())
FindFormElementUpShadowRoots(element, &form_element);
if (form_element.IsNull()) {
return ForEachMatchingUnownedFormField(element, form,
FILTER_ALL_NON_EDITABLE_ELEMENTS,
@ -2316,7 +2434,7 @@ std::u16string CoalesceTextByIdList(const WebDocument& document,
} // namespace
std::u16string GetAriaLabel(const blink::WebDocument& document,
const WebFormControlElement& element) {
const WebElement& element) {
static const base::NoDestructor<WebString> kAriaLabelledBy("aria-labelledby");
if (element.HasAttribute(*kAriaLabelledBy)) {
std::u16string text =
@ -2333,11 +2451,16 @@ std::u16string GetAriaLabel(const blink::WebDocument& document,
}
std::u16string GetAriaDescription(const blink::WebDocument& document,
const WebFormControlElement& element) {
const WebElement& element) {
static const base::NoDestructor<WebString> kAriaDescribedBy(
"aria-describedby");
return CoalesceTextByIdList(document,
element.GetAttribute(*kAriaDescribedBy));
}
ShadowFieldData::ShadowFieldData() = default;
ShadowFieldData::ShadowFieldData(const ShadowFieldData& other) = default;
ShadowFieldData::~ShadowFieldData() = default;
} // namespace form_util
} // namespace autofill

@ -70,6 +70,18 @@ enum ExtractMask {
// kMaxDataLength.
};
struct ShadowFieldData {
ShadowFieldData();
ShadowFieldData(const ShadowFieldData& other);
~ShadowFieldData();
// If the form control is inside shadow DOM, then these lists will contain
// id and name attributes of the parent shadow host elements. There may be
// more than one if the form control is in nested shadow DOM.
std::vector<std::u16string> shadow_host_id_attributes;
std::vector<std::u16string> shadow_host_name_attributes;
};
// Gets up to kMaxListSize data list values (with corresponding label) for the
// given element, each value and label have as far as kMaxDataLength.
void GetDataListSuggestions(const blink::WebInputElement& element,
@ -167,7 +179,8 @@ void WebFormControlElementToFormField(
const blink::WebFormControlElement& element,
const FieldDataManager* field_data_manager,
ExtractMask extract_mask,
FormFieldData* field);
FormFieldData* field,
ShadowFieldData* shadow_data = nullptr);
// Fills |form| with the FormData object corresponding to the |form_element|.
// If |field| is non-NULL, also fills |field| with the FormField object
@ -346,12 +359,12 @@ FindFormControlElementsByUniqueRendererId(
// attribute of |element| or the value of the aria-label attribute of
// |element|, with priority given to the aria-labelledby attribute.
std::u16string GetAriaLabel(const blink::WebDocument& document,
const blink::WebFormControlElement& element);
const blink::WebElement& element);
// Returns the ARIA label text of the elements denoted by the aria-describedby
// attribute of |element|.
std::u16string GetAriaDescription(const blink::WebDocument& document,
const blink::WebFormControlElement& element);
const blink::WebElement& element);
} // namespace form_util
} // namespace autofill

@ -347,6 +347,7 @@ void SetRuntimeFeaturesFromChromiumFeatures() {
runtimeFeatureNameToChromiumFeatureMapping[] = {
{"AllowContentInitiatedDataUrlNavigations",
features::kAllowContentInitiatedDataUrlNavigations},
{"AutofillShadowDOM", blink::features::kAutofillShadowDOM},
{"AndroidDownloadableFontsMatching",
features::kAndroidDownloadableFontsMatching},
{"BlockCredentialedSubresources",

@ -973,5 +973,10 @@ const base::Feature kMinimizeAudioProcessingForUnusedOutput{
"MinimizeAudioProcessingForUnusedOutput",
base::FEATURE_DISABLED_BY_DEFAULT};
// Makes autofill look across shadow boundaries when collecting form controls to
// fill.
const base::Feature kAutofillShadowDOM{"AutofillShadowDOM",
base::FEATURE_DISABLED_BY_DEFAULT};
} // namespace features
} // namespace blink

@ -407,6 +407,10 @@ BLINK_COMMON_EXPORT extern const base::Feature kFledgeInterestGroupAPI;
BLINK_COMMON_EXPORT extern const base::Feature
kMinimizeAudioProcessingForUnusedOutput;
// Makes autofill look across shadow boundaries when collecting form controls to
// fill.
BLINK_COMMON_EXPORT extern const base::Feature kAutofillShadowDOM;
} // namespace features
} // namespace blink

@ -85,6 +85,9 @@ class BLINK_EXPORT WebElement : public WebNode {
// Returns true if this is an autonomous custom element.
bool IsAutonomousCustomElement() const;
// Returns the owning shadow host for this element, if there is one.
WebElement OwnerShadowHost() const;
// Returns an author ShadowRoot attached to this element, regardless
// of open or closed. This returns null WebNode if this
// element has no ShadowRoot or has a UA ShadowRoot.

@ -148,6 +148,13 @@ WebNode WebElement::ShadowRoot() const {
return WebNode(root);
}
WebElement WebElement::OwnerShadowHost() const {
if (auto* host = ConstUnwrap<Element>()->OwnerShadowHost()) {
return WebElement(host);
}
return WebElement();
}
WebNode WebElement::OpenOrClosedShadowRoot() {
if (IsNull())
return WebNode();

@ -66,7 +66,8 @@ WebVector<WebFormControlElement> WebFormElement::GetFormControlElements()
const {
const HTMLFormElement* form = ConstUnwrap<HTMLFormElement>();
Vector<WebFormControlElement> form_control_elements;
for (const auto& element : form->ListedElements()) {
for (const auto& element :
form->ListedElements(/*include_shadow_trees=*/true)) {
if (auto* form_control =
blink::DynamicTo<HTMLFormControlElement>(element.Get())) {
form_control_elements.push_back(form_control);

@ -73,9 +73,26 @@
namespace blink {
namespace {
bool HasFormInBetween(const Node* root, const Node* descendant) {
DCHECK(descendant->IsDescendantOf(root));
DCHECK(!IsA<HTMLFormElement>(descendant));
for (ContainerNode* parent = descendant->parentNode(); parent != root;
parent = parent->parentNode()) {
if (DynamicTo<HTMLFormElement>(parent)) {
return true;
}
}
return false;
}
} // namespace
HTMLFormElement::HTMLFormElement(Document& document)
: HTMLElement(html_names::kFormTag, document),
listed_elements_are_dirty_(false),
listed_elements_including_shadow_trees_are_dirty_(false),
image_elements_are_dirty_(false),
has_elements_associated_by_parser_(false),
has_elements_associated_by_form_attribute_(false),
@ -93,6 +110,7 @@ void HTMLFormElement::Trace(Visitor* visitor) const {
visitor->Trace(past_names_map_);
visitor->Trace(radio_button_group_scope_);
visitor->Trace(listed_elements_);
visitor->Trace(listed_elements_including_shadow_trees_);
visitor->Trace(image_elements_);
HTMLElement::Trace(visitor);
}
@ -636,6 +654,8 @@ void HTMLFormElement::ParseAttribute(
void HTMLFormElement::Associate(ListedElement& e) {
listed_elements_are_dirty_ = true;
listed_elements_.clear();
listed_elements_including_shadow_trees_are_dirty_ = true;
listed_elements_including_shadow_trees_.clear();
if (e.ToHTMLElement().FastHasAttribute(html_names::kFormAttr))
has_elements_associated_by_form_attribute_ = true;
}
@ -643,6 +663,8 @@ void HTMLFormElement::Associate(ListedElement& e) {
void HTMLFormElement::Disassociate(ListedElement& e) {
listed_elements_are_dirty_ = true;
listed_elements_.clear();
listed_elements_including_shadow_trees_are_dirty_ = true;
listed_elements_including_shadow_trees_.clear();
RemoveFromPastNamesMap(e.ToHTMLElement());
}
@ -679,31 +701,66 @@ HTMLFormControlsCollection* HTMLFormElement::elements() {
}
void HTMLFormElement::CollectListedElements(
Node& root,
ListedElement::List& elements) const {
const Node& root,
ListedElement::List& elements,
ListedElement::List* elements_including_shadow_trees,
bool in_shadow_tree) const {
DCHECK(!in_shadow_tree || elements_including_shadow_trees);
elements.clear();
for (HTMLElement& element : Traversal<HTMLElement>::StartsAfter(root)) {
ListedElement* listed_element = ListedElement::From(element);
if (listed_element && listed_element->Form() == this)
elements.push_back(listed_element);
if (ListedElement* listed_element = ListedElement::From(element)) {
// If there is a <form> in between |root| and |listed_element|, then we
// shouldn't include it in |elements_including_shadow_trees| in order to
// prevent multiple forms from "owning" the same |listed_element| as shown
// by their |elements_including_shadow_trees|. |elements| doesn't have
// this problem because it can check |listed_element->Form()|.
if (in_shadow_tree && !HasFormInBetween(&root, &element)) {
elements_including_shadow_trees->push_back(listed_element);
} else if (listed_element->Form() == this) {
elements.push_back(listed_element);
if (elements_including_shadow_trees)
elements_including_shadow_trees->push_back(listed_element);
}
}
if (elements_including_shadow_trees && element.AuthorShadowRoot() &&
!HasFormInBetween(&root, &element)) {
const Node& shadow = *element.AuthorShadowRoot();
CollectListedElements(shadow, elements, elements_including_shadow_trees,
/*in_shadow_tree=*/true);
}
}
}
// This function should be const conceptually. However we update some fields
// because of lazy evaluation.
const ListedElement::List& HTMLFormElement::ListedElements() const {
if (!listed_elements_are_dirty_)
return listed_elements_;
HTMLFormElement* mutable_this = const_cast<HTMLFormElement*>(this);
Node* scope = mutable_this;
if (has_elements_associated_by_parser_)
scope = &NodeTraversal::HighestAncestorOrSelf(*mutable_this);
if (isConnected() && has_elements_associated_by_form_attribute_)
scope = &GetTreeScope().RootNode();
DCHECK(scope);
CollectListedElements(*scope, mutable_this->listed_elements_);
mutable_this->listed_elements_are_dirty_ = false;
return listed_elements_;
const ListedElement::List& HTMLFormElement::ListedElements(
bool include_shadow_trees) const {
if (!RuntimeEnabledFeatures::AutofillShadowDOMEnabled())
include_shadow_trees = false;
bool collect_shadow_inputs =
include_shadow_trees && listed_elements_including_shadow_trees_are_dirty_;
if (listed_elements_are_dirty_ || collect_shadow_inputs) {
HTMLFormElement* mutable_this = const_cast<HTMLFormElement*>(this);
Node* scope = mutable_this;
if (has_elements_associated_by_parser_)
scope = &NodeTraversal::HighestAncestorOrSelf(*mutable_this);
if (isConnected() && has_elements_associated_by_form_attribute_)
scope = &GetTreeScope().RootNode();
DCHECK(scope);
mutable_this->listed_elements_.clear();
mutable_this->listed_elements_including_shadow_trees_.clear();
CollectListedElements(
*scope, mutable_this->listed_elements_,
collect_shadow_inputs
? &mutable_this->listed_elements_including_shadow_trees_
: nullptr);
mutable_this->listed_elements_are_dirty_ = false;
mutable_this->listed_elements_including_shadow_trees_are_dirty_ =
!collect_shadow_inputs;
}
return include_shadow_trees ? listed_elements_including_shadow_trees_
: listed_elements_;
}
void HTMLFormElement::CollectImageElements(
@ -949,4 +1006,8 @@ void HTMLFormElement::InvalidateDefaultButtonStyle() const {
}
}
void HTMLFormElement::InvalidateListedElementsIncludingShadowTrees() {
listed_elements_including_shadow_trees_are_dirty_ = true;
}
} // namespace blink

@ -103,7 +103,8 @@ class CORE_EXPORT HTMLFormElement final : public HTMLElement {
return radio_button_group_scope_;
}
const ListedElement::List& ListedElements() const;
const ListedElement::List& ListedElements(
bool include_shadow_trees = false) const;
const HeapVector<Member<HTMLImageElement>>& ImageElements();
#if defined(USE_BLINK_V8_BINDING_NEW_IDL_UNION)
@ -120,6 +121,8 @@ class CORE_EXPORT HTMLFormElement final : public HTMLElement {
unsigned UniqueRendererFormId() const { return unique_renderer_form_id_; }
void InvalidateListedElementsIncludingShadowTrees();
private:
InsertionNotificationRequest InsertedInto(ContainerNode&) override;
void RemovedFrom(ContainerNode&) override;
@ -139,7 +142,11 @@ class CORE_EXPORT HTMLFormElement final : public HTMLElement {
void ScheduleFormSubmission(const Event*,
HTMLFormControlElement* submit_button);
void CollectListedElements(Node& root, ListedElement::List&) const;
void CollectListedElements(
const Node& root,
ListedElement::List& elements,
ListedElement::List* elements_including_shadow_trees = nullptr,
bool in_shadow_tree = false) const;
void CollectImageElements(Node& root, HeapVector<Member<HTMLImageElement>>&);
// Returns true if the submission should proceed.
@ -163,6 +170,9 @@ class CORE_EXPORT HTMLFormElement final : public HTMLElement {
// Do not access listed_elements_ directly. Use ListedElements() instead.
ListedElement::List listed_elements_;
// Do not access listed_elements_including_shadow_trees_ directly. Use
// ListedElements(true) instead.
ListedElement::List listed_elements_including_shadow_trees_;
// Do not access image_elements_ directly. Use ImageElements() instead.
HeapVector<Member<HTMLImageElement>> image_elements_;
@ -173,6 +183,7 @@ class CORE_EXPORT HTMLFormElement final : public HTMLElement {
bool is_constructing_entry_list_ = false;
bool listed_elements_are_dirty_ : 1;
bool listed_elements_including_shadow_trees_are_dirty_ : 1;
bool image_elements_are_dirty_ : 1;
bool has_elements_associated_by_parser_ : 1;
bool has_elements_associated_by_form_attribute_ : 1;

@ -5,6 +5,9 @@
#include "third_party/blink/renderer/core/html/forms/html_form_element.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
namespace blink {
@ -31,4 +34,224 @@ TEST_F(HTMLFormElementTest, UniqueRendererFormId) {
EXPECT_EQ(first_id + 2, form3->UniqueRendererFormId());
}
// This tree is created manually because the HTML parser removes nested forms.
// The created tree looks like this:
// <body>
// <form id=form1>
// <form id=form2>
// <input>
TEST_F(HTMLFormElementTest, ListedElementsNestedForms) {
HTMLBodyElement* body = GetDocument().FirstBodyElement();
HTMLFormElement* form1 = MakeGarbageCollected<HTMLFormElement>(GetDocument());
body->AppendChild(form1);
HTMLFormElement* form2 = MakeGarbageCollected<HTMLFormElement>(GetDocument());
form1->AppendChild(form2);
HTMLInputElement* input = MakeGarbageCollected<HTMLInputElement>(
GetDocument(), CreateElementFlags::ByCreateElement());
form2->AppendChild(input);
ListedElement::List form1elements = form1->ListedElements();
ListedElement::List form2elements = form2->ListedElements();
EXPECT_EQ(form1elements.size(), 0u);
ASSERT_EQ(form2elements.size(), 1u);
EXPECT_EQ(form2elements.at(0)->ToHTMLElement(), input);
}
TEST_F(HTMLFormElementTest, ListedElementsDetachedForm) {
HTMLBodyElement* body = GetDocument().FirstBodyElement();
HTMLFormElement* form = MakeGarbageCollected<HTMLFormElement>(GetDocument());
body->AppendChild(form);
HTMLInputElement* input = MakeGarbageCollected<HTMLInputElement>(
GetDocument(), CreateElementFlags::ByCreateElement());
form->AppendChild(input);
ListedElement::List listed_elements = form->ListedElements();
ASSERT_EQ(listed_elements.size(), 1u);
EXPECT_EQ(listed_elements.at(0)->ToHTMLElement(), input);
form->remove();
listed_elements = form->ListedElements();
ASSERT_EQ(listed_elements.size(), 1u);
EXPECT_EQ(listed_elements.at(0)->ToHTMLElement(), input);
}
// This tree is created manually because the HTML parser removes nested forms.
// The created tree looks like this:
// <body>
// <form id=form1>
// <div id=form1div>
// <template shadowroot=open>
// <form id=form2>
// <form id=form3>
// <div id=form3div>
// <template shadowroot=open>
//
// An <input> element is appended at the bottom and moved up one node at a time
// in this tree, and each step of the way, ListedElements is checked on all
// forms.
TEST_F(HTMLFormElementTest, ListedElementsIncludeShadowTrees) {
HTMLBodyElement* body = GetDocument().FirstBodyElement();
HTMLFormElement* form1 = MakeGarbageCollected<HTMLFormElement>(GetDocument());
body->AppendChild(form1);
HTMLDivElement* form1div =
MakeGarbageCollected<HTMLDivElement>(GetDocument());
form1->AppendChild(form1div);
ShadowRoot& form1root =
form1div->AttachShadowRootInternal(ShadowRootType::kOpen);
HTMLFormElement* form2 = MakeGarbageCollected<HTMLFormElement>(GetDocument());
form1root.AppendChild(form2);
HTMLFormElement* form3 = MakeGarbageCollected<HTMLFormElement>(GetDocument());
form2->AppendChild(form3);
HTMLDivElement* form3div =
MakeGarbageCollected<HTMLDivElement>(GetDocument());
form3->AppendChild(form3div);
ShadowRoot& form3root =
form3div->AttachShadowRootInternal(ShadowRootType::kOpen);
HTMLInputElement* input = MakeGarbageCollected<HTMLInputElement>(
GetDocument(), CreateElementFlags::ByCreateElement());
form3root.AppendChild(input);
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{input});
input->remove();
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
form3div->AppendChild(input);
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{input});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{input});
form3->AppendChild(input);
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{input});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{input});
input->remove();
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
form2->AppendChild(input);
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{input});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{input});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
input->remove();
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
form1root.AppendChild(input);
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{input});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
input->remove();
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
form1div->AppendChild(input);
EXPECT_EQ(form1->ListedElements(), ListedElement::List{input});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{input});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
form1->AppendChild(input);
EXPECT_EQ(form1->ListedElements(), ListedElement::List{input});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{input});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
input->remove();
EXPECT_EQ(form1->ListedElements(), ListedElement::List{});
EXPECT_EQ(form1->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form2->ListedElements(), ListedElement::List{});
EXPECT_EQ(form2->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
EXPECT_EQ(form3->ListedElements(), ListedElement::List{});
EXPECT_EQ(form3->ListedElements(/*include_shadow_trees=*/true),
ListedElement::List{});
}
} // namespace blink

@ -51,6 +51,27 @@
namespace blink {
namespace {
void InvalidateShadowIncludingAncestorForms(ContainerNode& insertion_point) {
if (!RuntimeEnabledFeatures::AutofillShadowDOMEnabled())
return;
// Let any forms in the shadow including ancestors know that this
// ListedElement has changed. Don't include any forms inside the same
// TreeScope know because that relationship isn't tracked by listed elements
// including shadow trees.
for (ContainerNode* parent = insertion_point.OwnerShadowHost(); parent;
parent = parent->ParentOrShadowHostNode()) {
if (HTMLFormElement* form = DynamicTo<HTMLFormElement>(parent)) {
form->InvalidateListedElementsIncludingShadowTrees();
return;
}
}
}
} // namespace
class FormAttributeTargetObserver : public IdTargetObserver {
public:
FormAttributeTargetObserver(const AtomicString& id, ListedElement*);
@ -123,6 +144,8 @@ void ListedElement::InsertedInto(ContainerNode& insertion_point) {
// Trigger for elements outside of forms.
if (!form_ && insertion_point.isConnected())
element.GetDocument().DidAssociateFormControl(&element);
InvalidateShadowIncludingAncestorForms(insertion_point);
}
void ListedElement::RemovedFrom(ContainerNode& insertion_point) {
@ -156,6 +179,8 @@ void ListedElement::RemovedFrom(ContainerNode& insertion_point) {
.GetFormController()
.InvalidateStatefulFormControlList();
}
InvalidateShadowIncludingAncestorForms(insertion_point);
}
HTMLFormElement* ListedElement::FindAssociatedForm(

@ -203,6 +203,10 @@
name: "AudioVideoTracks",
status: "experimental",
},
{
name: "AutofillShadowDOM",
status: "experimental",
},
{
name: "AutoLazyLoadOnReloads",
depends_on: ["LazyFrameLoading"],