blob: 013921d85e846a2eb38582a625b9d79f49abd386 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/autofill/content/renderer/form_tracker.h"
#include <optional>
#include <variant>
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "components/autofill/content/renderer/autofill_agent.h"
#include "components/autofill/content/renderer/form_autofill_util.h"
#include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h"
#include "components/autofill/core/common/unique_ids.h"
#include "content/public/renderer/render_frame.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/web/modules/autofill/web_form_element_observer.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_form_control_element.h"
#include "third_party/blink/public/web/web_form_element.h"
#include "third_party/blink/public/web/web_input_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_local_frame_client.h"
#include "ui/base/page_transition_types.h"
using blink::WebDocumentLoader;
using blink::WebElement;
using blink::WebFormControlElement;
using blink::WebFormElement;
using blink::WebInputElement;
namespace autofill {
namespace {
constexpr char kSubmissionSourceHistogram[] =
"Autofill.SubmissionDetectionSource.FormTracker";
bool ShouldReplaceElementsByRendererIds() {
return base::FeatureList::IsEnabled(
features::kAutofillReplaceCachedWebElementsByRendererIds);
}
} // namespace
using mojom::SubmissionSource;
FormRef::FormRef(blink::WebFormElement form)
: form_renderer_id_(form_util::GetFormRendererId(form)) {
if (!ShouldReplaceElementsByRendererIds()) {
form_ = form;
}
}
blink::WebFormElement FormRef::GetForm() const {
return ShouldReplaceElementsByRendererIds()
? form_util::GetFormByRendererId(form_renderer_id_)
: form_;
}
FormRendererId FormRef::GetId() const {
return ShouldReplaceElementsByRendererIds()
? form_renderer_id_
: form_util::GetFormRendererId(form_);
}
FieldRef::FieldRef(blink::WebFormControlElement form_control)
: field_renderer_id_(form_util::GetFieldRendererId(form_control)) {
CHECK(form_control);
if (!ShouldReplaceElementsByRendererIds()) {
field_ = form_control;
}
}
FieldRef::FieldRef(blink::WebElement content_editable)
: field_renderer_id_(content_editable.GetDomNodeId()) {
CHECK(content_editable);
CHECK(content_editable.IsContentEditable());
if (!ShouldReplaceElementsByRendererIds()) {
field_ = content_editable;
}
}
bool operator<(const FieldRef& lhs, const FieldRef& rhs) {
return lhs.field_renderer_id_ < rhs.field_renderer_id_;
}
blink::WebFormControlElement FieldRef::GetField() const {
return ShouldReplaceElementsByRendererIds()
? form_util::GetFormControlByRendererId(field_renderer_id_)
: field_.DynamicTo<WebFormControlElement>();
}
blink::WebElement FieldRef::GetContentEditable() const {
blink::WebElement content_editable =
ShouldReplaceElementsByRendererIds()
? form_util::GetContentEditableByRendererId(field_renderer_id_)
: field_;
return content_editable && content_editable.IsContentEditable()
? content_editable
: blink::WebElement();
}
FieldRendererId FieldRef::GetId() const {
return ShouldReplaceElementsByRendererIds() ? field_renderer_id_
: field_ ? form_util::GetFieldRendererId(field_)
: FieldRendererId();
}
FormTracker::FormTracker(content::RenderFrame* render_frame,
AutofillAgent& agent)
: content::RenderFrameObserver(render_frame),
blink::WebLocalFrameObserver(render_frame->GetWebFrame()),
agent_(agent) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
}
FormTracker::~FormTracker() {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
ResetLastInteractedElements();
}
void FormTracker::AjaxSucceeded() {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
submission_triggering_events_.xhr_succeeded = true;
FireSubmissionIfFormDisappear(SubmissionSource::XHR_SUCCEEDED);
}
void FormTracker::TextFieldValueChanged(const WebFormControlElement& element) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
DCHECK(element.DynamicTo<WebInputElement>() ||
form_util::IsTextAreaElement(element));
// If the element isn't focused then the changes don't matter. This check is
// required to properly handle IME interactions.
if (!element.Focused()) {
return;
}
if (!unsafe_render_frame()) {
return;
}
// Disregard text changes that aren't caused by user gestures or pastes. Note
// that pastes aren't necessarily user gestures because Blink's conception of
// user gestures is centered around creating new windows/tabs.
if (user_gesture_required_ &&
!unsafe_render_frame()->GetWebFrame()->HasTransientUserActivation() &&
!unsafe_render_frame()->IsPasting()) {
return;
}
// We post a task for doing the Autofill as the caret position is not set
// properly at this point (http://bugs.webkit.org/show_bug.cgi?id=16976) and
// it is needed to trigger autofill.
weak_ptr_factory_.InvalidateWeakPtrs();
unsafe_render_frame()
->GetWebFrame()
->GetTaskRunner(blink::TaskType::kInternalAutofill)
->PostTask(FROM_HERE,
base::BindRepeating(&FormTracker::FormControlDidChangeImpl,
weak_ptr_factory_.GetWeakPtr(),
form_util::GetFieldRendererId(element),
SaveFormReason::kTextFieldChanged));
}
void FormTracker::SelectControlSelectionChanged(
const WebFormControlElement& element) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
if (!unsafe_render_frame()) {
return;
}
// Post a task to avoid processing select control change while it is changing.
weak_ptr_factory_.InvalidateWeakPtrs();
unsafe_render_frame()
->GetWebFrame()
->GetTaskRunner(blink::TaskType::kInternalAutofill)
->PostTask(FROM_HERE,
base::BindRepeating(&FormTracker::FormControlDidChangeImpl,
weak_ptr_factory_.GetWeakPtr(),
form_util::GetFieldRendererId(element),
SaveFormReason::kSelectChanged));
}
void FormTracker::ElementDisappeared(const blink::WebElement& element) {
// Signal is discarded altogether when the feature is disabled.
if (!base::FeatureList::IsEnabled(
features::kAutofillReplaceFormElementObserver)) {
return;
}
if (!element.DynamicTo<WebFormElement>() &&
!element.DynamicTo<WebFormControlElement>()) {
return;
}
// If tracking a form, any disappearance other than that form is not
// interesting.
if (element.DynamicTo<WebFormElement>() &&
last_interacted_.form.GetId() != form_util::GetFormRendererId(element)) {
return;
}
// If tracking a field, any disappearance other than that field is not
// interesting.
if (element.DynamicTo<WebFormControlElement>() &&
last_interacted_.formless_element.GetId() !=
form_util::GetFieldRendererId(element)) {
return;
}
if (submission_triggering_events_.xhr_succeeded) {
FireFormSubmission(mojom::SubmissionSource::XHR_SUCCEEDED,
/*submitted_form_element=*/std::nullopt);
return;
}
if (submission_triggering_events_.finished_same_document_navigation) {
FireFormSubmission(mojom::SubmissionSource::SAME_DOCUMENT_NAVIGATION,
/*submitted_form_element=*/std::nullopt);
return;
}
if (submission_triggering_events_.tracked_element_autofilled) {
FireFormSubmission(mojom::SubmissionSource::DOM_MUTATION_AFTER_AUTOFILL,
/*submitted_form_element=*/std::nullopt);
return;
}
submission_triggering_events_.tracked_element_disappeared = true;
}
void FormTracker::TrackAutofilledElement(const WebFormControlElement& element) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
if (!form_util::GetFormControlByRendererId(
form_util::GetFieldRendererId(element))) {
return;
}
blink::WebFormElement form_element = element.GetOwningFormForAutofill();
if (form_element) {
UpdateLastInteractedElement(form_util::GetFormRendererId(form_element));
} else {
UpdateLastInteractedElement(form_util::GetFieldRendererId(element));
}
submission_triggering_events_.tracked_element_autofilled = true;
TrackElement(mojom::SubmissionSource::DOM_MUTATION_AFTER_AUTOFILL);
}
void FormTracker::TrackAutofilledElement(
const base::flat_map<FieldRendererId, FormRendererId>&
filled_fields_and_forms) {
auto field_is_owned =
[](const std::pair<FieldRendererId, FormRendererId>&
filled_field_and_form) {
return !form_util::GetFormByRendererId(filled_field_and_form.second)
.IsNull();
};
if (auto it = std::ranges::find_if(filled_fields_and_forms, field_is_owned);
it != filled_fields_and_forms.end()) {
const auto& [filled_field_id, filled_form_id] = *it;
if (base::FeatureList::IsEnabled(
features::kAutofillAcceptDomMutationAfterAutofillSubmission)) {
TrackAutofilledElement(
form_util::GetFormControlByRendererId(filled_field_id));
} else {
UpdateLastInteractedElement(filled_form_id);
}
} else {
for (const auto& [filled_field_id, filled_form_id] :
filled_fields_and_forms) {
WebFormControlElement control_element =
form_util::GetFormControlByRendererId(filled_field_id);
CHECK(control_element);
if (base::FeatureList::IsEnabled(
features::kAutofillAcceptDomMutationAfterAutofillSubmission)) {
TrackAutofilledElement(control_element);
} else {
UpdateLastInteractedElement(
form_util::GetFieldRendererId(control_element));
}
}
}
}
void FormTracker::FormControlDidChangeImpl(FieldRendererId element_id,
SaveFormReason change_source) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
CHECK_NE(change_source, SaveFormReason::kWillSendSubmitEvent);
WebFormControlElement element =
form_util::GetFormControlByRendererId(element_id);
// This function may be called asynchronously, so a navigation may have
// happened. Since this event isn't submission-related.
if (!form_util::IsOwnedByFrame(element, unsafe_render_frame())) {
return;
}
blink::WebFormElement form_element = element.GetOwningFormForAutofill();
if (form_element) {
UpdateLastInteractedElement(form_util::GetFormRendererId(form_element));
} else {
UpdateLastInteractedElement(form_util::GetFieldRendererId(element));
}
agent_->OnProvisionallySaveForm(form_element, element, change_source);
}
void FormTracker::DidCommitProvisionalLoad(ui::PageTransition transition) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
ResetLastInteractedElements();
}
void FormTracker::DidFinishSameDocumentNavigation() {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
submission_triggering_events_.finished_same_document_navigation = true;
FireSubmissionIfFormDisappear(SubmissionSource::SAME_DOCUMENT_NAVIGATION);
}
void FormTracker::DidStartNavigation(
const GURL& url,
std::optional<blink::WebNavigationType> navigation_type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
if (!unsafe_render_frame()) {
return;
}
// Ony handle primary main frame.
if (!unsafe_render_frame() ||
!unsafe_render_frame()->GetWebFrame()->IsOutermostMainFrame()) {
return;
}
// We are interested only in content-initiated navigations. Explicit browser
// initiated navigations (e.g. via omnibox) don't have a navigation type
// and are discarded here.
if (navigation_type.has_value() &&
navigation_type.value() != blink::kWebNavigationTypeLinkClicked) {
FireFormSubmission(mojom::SubmissionSource::PROBABLY_FORM_SUBMITTED,
/*submitted_form_element=*/std::nullopt);
}
}
void FormTracker::WillDetach(blink::DetachReason detach_reason) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
if (!unsafe_render_frame()) {
return;
}
if (detach_reason == blink::DetachReason::kFrameDeletion &&
!unsafe_render_frame()->GetWebFrame()->IsOutermostMainFrame()) {
// Exclude cases where the previous RenderFrame gets deleted only to be
// replaced by a new RenderFrame, which happens on navigations. This is so
// that we only trigger inferred form submission if the actual frame
// (<iframe> element etc) gets detached.
FireFormSubmission(SubmissionSource::FRAME_DETACHED,
/*submitted_form_element=*/std::nullopt);
}
// TODO(crbug.com/40281981): Figure out if this is still needed, and
// document the reason, otherwise remove.
ResetLastInteractedElements();
}
void FormTracker::WillSendSubmitEvent(const WebFormElement& form) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
CHECK(form);
// TODO(crbug.com/40281981): Figure out if this is still needed, and document
// the reason, otherwise remove.
UpdateLastInteractedElement(form_util::GetFormRendererId(form));
agent_->OnProvisionallySaveForm(form, blink::WebFormControlElement(),
SaveFormReason::kWillSendSubmitEvent);
}
void FormTracker::WillSubmitForm(const WebFormElement& form) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
// A form submission may target a frame other than the frame that owns |form|.
// The WillSubmitForm() event is only fired on the target frame's FormTracker
// (provided that both have the same origin). In such a case, we ignore the
// form submission event. If we didn't, we would send |form| to an
// AutofillAgent and then to a ContentAutofillDriver etc. which haven't seen
// this form before. See crbug.com/1240247#c13 for details.
if (!form_util::IsOwnedByFrame(form, unsafe_render_frame())) {
return;
}
FireFormSubmission(mojom::SubmissionSource::FORM_SUBMISSION, form);
}
void FormTracker::OnDestruct() {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
ResetLastInteractedElements();
}
void FormTracker::FireFormSubmission(
SubmissionSource source,
std::optional<WebFormElement> submitted_form_element) {
DCHECK_CALLED_ON_VALID_SEQUENCE(form_tracker_sequence_checker_);
if (!IsTracking() && source != mojom::SubmissionSource::FORM_SUBMISSION) {
// If no form is being tracked, there's no need to inform the agent of
// submission since no submitted form will be fetched. The only source
// that's an exception for this is SubmissionSource::FORM_SUBMISSION since
// it provides the submitted form element and therefore no tracking is
// needed.
return;
}
base::UmaHistogramEnumeration(kSubmissionSourceHistogram, source);
agent_->OnFormSubmission(source, submitted_form_element);
switch (source) {
case mojom::SubmissionSource::PROBABLY_FORM_SUBMITTED:
case mojom::SubmissionSource::FORM_SUBMISSION:
if (!base::FeatureList::IsEnabled(features::kAutofillFixFormTracking)) {
ResetLastInteractedElements();
}
break;
case mojom::SubmissionSource::SAME_DOCUMENT_NAVIGATION:
case mojom::SubmissionSource::XHR_SUCCEEDED:
case mojom::SubmissionSource::FRAME_DETACHED:
// TODO(crbug.com/40281981): Figure out if this is still needed, and
// document the reason, otherwise remove.
ResetLastInteractedElements();
break;
case mojom::SubmissionSource::DOM_MUTATION_AFTER_AUTOFILL:
break;
case mojom::SubmissionSource::NONE:
NOTREACHED();
}
}
void FormTracker::FireSubmissionIfFormDisappear(SubmissionSource source) {
if (CanInferFormSubmitted() ||
(submission_triggering_events_.tracked_element_disappeared &&
base::FeatureList::IsEnabled(
features::kAutofillReplaceFormElementObserver))) {
FireFormSubmission(source, /*submitted_form_element=*/std::nullopt);
return;
}
TrackElement(source);
}
bool FormTracker::CanInferFormSubmitted() {
if (last_interacted_.form.GetId()) {
WebFormElement last_interacted_form = last_interacted_.form.GetForm();
// Infer submission if the form was removed or all its elements are hidden.
return !last_interacted_form ||
std::ranges::none_of(
last_interacted_form.GetFormControlElements(), // nocheck
&WebElement::IsFocusable);
}
if (last_interacted_.formless_element.GetId()) {
WebFormControlElement last_interacted_formless_element =
last_interacted_.formless_element.GetField();
// Infer submission if the field was removed or it's hidden.
return !last_interacted_formless_element ||
!last_interacted_formless_element.IsFocusable();
}
return false;
}
void FormTracker::TrackElement(mojom::SubmissionSource source) {
if (base::FeatureList::IsEnabled(
features::kAutofillReplaceFormElementObserver)) {
// Do not use WebFormElementObserver. Instead, rely on the signal
// `FormTracker::ElementDisappeared` coming from blink.
return;
}
// Already has observer for last interacted element.
if (form_element_observer_) {
return;
}
auto callback = base::BindOnce(&FormTracker::ElementWasHiddenOrRemoved,
base::Unretained(this), source);
if (WebFormElement last_interacted_form = last_interacted_.form.GetForm()) {
form_element_observer_ = blink::WebFormElementObserver::Create(
last_interacted_form, std::move(callback));
} else if (WebFormControlElement last_interacted_formless_element =
last_interacted_.formless_element.GetField()) {
form_element_observer_ = blink::WebFormElementObserver::Create(
last_interacted_formless_element, std::move(callback));
}
}
void FormTracker::UpdateLastInteractedElement(
std::variant<FormRendererId, FieldRendererId> element_id) {
ResetLastInteractedElements();
// `document` is the WebDocument of `element_id`'s element. It is not
// necessarily the same as the current frame's document.
//
// `form` is null if `element_id` is a FieldRendererId.
auto [document, form_element] = std::visit(
absl::Overload{
[this](FormRendererId form_id) {
CHECK(form_id);
WebFormElement form = form_util::GetFormByRendererId(form_id);
last_interacted_.form =
FormRef(form_util::GetFormByRendererId(form_id));
return std::pair(form.GetDocument(), form);
},
[this](FieldRendererId field_id) {
CHECK(field_id);
WebFormControlElement form_control =
form_util::GetFormControlByRendererId(field_id);
last_interacted_.formless_element = FieldRef(form_control);
return std::pair(form_control.GetDocument(), WebFormElement());
},
},
element_id);
CHECK(document);
// We use the element's `document`, not the current frame's document, because
// `element_id` may refer to an element that is not in the current frame's
// document.
last_interacted_.saved_state = form_util::ExtractFormData(
document, form_element, agent_->field_data_manager(),
agent_->GetCallTimerState(
CallTimerState::CallSite::kUpdateLastInteractedElement),
agent_->button_titles_cache());
}
void FormTracker::ResetLastInteractedElements() {
last_interacted_ = {};
submission_triggering_events_ = {};
if (form_element_observer_) {
form_element_observer_->Disconnect();
form_element_observer_ = nullptr;
}
}
void FormTracker::SetUserGestureRequired(
UserGestureRequired user_gesture_required) {
user_gesture_required_ = user_gesture_required;
}
bool FormTracker::IsTracking() const {
return last_interacted_.form.GetId() ||
last_interacted_.formless_element.GetId() ||
last_interacted_.saved_state;
}
void FormTracker::ElementWasHiddenOrRemoved(mojom::SubmissionSource source) {
FireFormSubmission(source, /*submitted_form_element=*/std::nullopt);
}
} // namespace autofill