| // 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 <stddef.h> |
| |
| #include <array> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/command_line.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_navigator_params.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/autofill/content/browser/content_autofill_driver.h" |
| #include "components/autofill/content/browser/test_autofill_manager_injector.h" |
| #include "components/autofill/core/browser/autofill_test_utils.h" |
| #include "components/autofill/core/browser/browser_autofill_manager.h" |
| #include "components/autofill/core/browser/browser_autofill_manager_test_api.h" |
| #include "components/autofill/core/browser/data_model/credit_card.h" |
| #include "components/autofill/core/browser/test_autofill_manager_waiter.h" |
| #include "components/autofill/core/common/autofill_constants.h" |
| #include "components/autofill/core/common/autofill_features.h" |
| #include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_mock_cert_verifier.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/switches.h" |
| |
| using base::ASCIIToUTF16; |
| using testing::AllOf; |
| using testing::AnyOf; |
| using testing::AssertionFailure; |
| using testing::AssertionResult; |
| using testing::Each; |
| using testing::ElementsAre; |
| using testing::Eq; |
| using testing::Field; |
| using testing::Key; |
| using testing::Le; |
| using testing::Ne; |
| using testing::Optional; |
| using testing::Pair; |
| using testing::Pointee; |
| using testing::Property; |
| using testing::ResultOf; |
| |
| namespace autofill { |
| |
| namespace { |
| |
| constexpr char kNameFull[] = "Barack Obama"; |
| constexpr char kNumber[] = "4444333322221111"; |
| constexpr char kExpMonth[] = "12"; |
| constexpr char kExpYear[] = "2035"; |
| constexpr char kExp[] = "12/2035"; |
| constexpr char kCvc[] = "123"; |
| |
| // Adds waiting capabilities to BrowserAutofillManager. |
| class TestAutofillManager : public BrowserAutofillManager { |
| public: |
| explicit TestAutofillManager(ContentAutofillDriver* driver) |
| : BrowserAutofillManager(driver, "en-US") { |
| test_api(*this).set_limit_before_refill(base::Hours(1)); |
| } |
| |
| static TestAutofillManager& GetForRenderFrameHost( |
| content::RenderFrameHost* rfh) { |
| return static_cast<TestAutofillManager&>( |
| ContentAutofillDriver::GetForRenderFrameHost(rfh) |
| ->GetAutofillManager()); |
| } |
| |
| const FormStructure* WaitForMatchingForm( |
| base::RepeatingCallback<bool(const FormStructure&)> pred) { |
| return ::autofill::WaitForMatchingForm(this, std::move(pred)); |
| } |
| |
| [[nodiscard]] AssertionResult WaitForAutofill(size_t num_awaited_calls) { |
| return did_autofill_.Wait(num_awaited_calls); |
| } |
| |
| [[nodiscard]] AssertionResult WaitForSubmission(size_t num_awaited_calls) { |
| return form_submitted_.Wait(num_awaited_calls); |
| } |
| |
| void OnFormSubmittedImpl(const FormData& form, |
| bool known_success, |
| mojom::SubmissionSource source) override { |
| BrowserAutofillManager::OnFormSubmittedImpl(form, known_success, source); |
| // The submitted form does not end up in the form cache, so we need to catch |
| // it here. |
| submitted_form_ = form; |
| } |
| |
| std::optional<FormData> submitted_form() const { return submitted_form_; } |
| |
| private: |
| TestAutofillManagerWaiter did_autofill_{ |
| *this, |
| {AutofillManagerEvent::kDidFillAutofillFormData}}; |
| TestAutofillManagerWaiter form_submitted_{ |
| *this, |
| {AutofillManagerEvent::kFormSubmitted}}; |
| std::optional<FormData> submitted_form_; |
| }; |
| |
| // Fakes an Autofill on of a given form. |
| void FillCard(content::RenderFrameHost* rfh, |
| const FormData& form, |
| const FormFieldData& triggered_field) { |
| CreditCard card; |
| test::SetCreditCardInfo(&card, kNameFull, kNumber, kExpMonth, kExpYear, "", |
| base::ASCIIToUTF16(std::string_view(kCvc))); |
| auto& manager = TestAutofillManager::GetForRenderFrameHost(rfh); |
| manager.FillOrPreviewCreditCardForm( |
| mojom::ActionPersistence::kFill, form, triggered_field, card, |
| base::ASCIIToUTF16(std::string_view(kCvc)), |
| AutofillTriggerDetails(AutofillTriggerSource::kPopup)); |
| } |
| |
| // Returns the values of all fields in the frames of `web_contents`. |
| // The values are sorted by DOM order in the respective frame. |
| std::map<LocalFrameToken, std::vector<std::string>> AllFieldValues( |
| content::WebContents* web_contents) { |
| constexpr const char kExtractValue[] = R"( |
| [...document.querySelectorAll('input, textarea, select')] |
| .map(field => field.value) |
| )"; |
| std::map<LocalFrameToken, std::vector<std::string>> values; |
| web_contents->GetPrimaryMainFrame()->ForEachRenderFrameHost( |
| [&](content::RenderFrameHost* rfh) { |
| content::EvalJsResult r = content::EvalJs(rfh, kExtractValue); |
| if (r.error.empty()) { |
| LocalFrameToken frame(rfh->GetFrameToken().value()); |
| for (const base::Value& value : r.value.GetList()) |
| values[frame].push_back(value.GetString()); |
| } |
| }); |
| return values; |
| } |
| |
| // Returns the values of all fields in the frames of `web_contents`. |
| // The values are sorted according to the ordering of the `form.fields`. |
| // Returns `{}` if there's a mismatch between the DOM fields and `form.fields`. |
| std::vector<std::string> AllFieldValues(content::WebContents* web_contents, |
| const FormData& form) { |
| std::map<LocalFrameToken, std::vector<std::string>> frame_to_values = |
| AllFieldValues(web_contents); |
| std::map<LocalFrameToken, std::vector<std::string>::const_iterator> |
| frame_to_iters; |
| for (const auto& [frame, frame_values] : frame_to_values) |
| frame_to_iters[frame] = frame_values.begin(); |
| |
| std::vector<std::string> values; |
| for (const FormFieldData& field : form.fields) { |
| LocalFrameToken frame = field.host_frame(); |
| if (frame_to_iters[frame] == frame_to_values[frame].end()) |
| return {}; |
| values.push_back(*frame_to_iters[frame]++); |
| } |
| |
| for (const auto& [frame, frame_values] : frame_to_values) { |
| if (frame_to_iters[frame] != frame_values.end()) |
| return {}; |
| } |
| return values; |
| } |
| |
| // Matches a `FormStructure` if its field type frequencies are within the limits |
| // accepted by Autofill. |
| auto IsWithinAutofillLimits() { |
| auto frequencies = [](const FormStructure& form) { |
| std::map<FieldType, size_t> counts; |
| for (const auto& field : form) |
| ++counts[field->Type().GetStorableType()]; |
| return counts; |
| }; |
| return ResultOf(frequencies, |
| Each(AnyOf(Key(NO_SERVER_DATA), Key(UNKNOWN_TYPE), |
| Pair(Eq(CREDIT_CARD_NUMBER), |
| Le(kCreditCardTypeValueFormFillingLimit)), |
| Pair(Ne(CREDIT_CARD_NUMBER), |
| Le(kTypeValueFormFillingLimit))))); |
| } |
| |
| auto HasValue(std::string_view value) { |
| return Property(&FormFieldData::value, base::ASCIIToUTF16(value)); |
| } |
| |
| } // namespace |
| |
| // Test fixture for all tests of AutofillAcrossIframes. A particular goal is to |
| // test that AutofillDriverRouter and FormForest handle the race conditions that |
| // arise during page load correctly; see |
| // go/autofill-iframes-race-condition-explainer for some explanation. |
| class AutofillAcrossIframesTest : public InProcessBrowserTest { |
| public: |
| void SetUpOnMainThread() override { |
| InProcessBrowserTest::SetUpOnMainThread(); |
| // Prevent the Keychain from coming up on Mac. |
| test::DisableSystemServices(browser()->profile()->GetPrefs()); |
| |
| // Set up the HTTPS (!) server (embedded_test_server() is an HTTP server). |
| // Every hostname is handled by that server. |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); |
| embedded_https_test_server().SetSSLConfig(net::EmbeddedTestServer::CERT_OK); |
| embedded_https_test_server().RegisterRequestHandler(base::BindRepeating( |
| [](const std::map<std::string, std::string>* pages, |
| const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| auto it = pages->find(request.GetURL().path()); |
| if (it == pages->end()) |
| return nullptr; |
| auto response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_OK); |
| response->set_content_type("text/html;charset=utf-8"); |
| response->set_content(it->second); |
| return response; |
| }, |
| base::Unretained(&pages_))); |
| ASSERT_TRUE(embedded_https_test_server().InitializeAndListen()); |
| embedded_https_test_server().StartAcceptingConnections(); |
| } |
| |
| void TearDownOnMainThread() override { |
| base::RunLoop().RunUntilIdle(); |
| // Make sure to close any showing popups prior to tearing down the UI. |
| main_autofill_manager().client().HideAutofillSuggestions( |
| SuggestionHidingReason::kTabGone); |
| test::ReenableSystemServices(); |
| InProcessBrowserTest::TearDownOnMainThread(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| cert_verifier_.SetUpCommandLine(command_line); |
| // Slower test bots (ChromeOS, debug, etc.) are flaky due to slower loading |
| // interacting with deferred commits. |
| command_line->AppendSwitch(blink::switches::kAllowPreCommitInput); |
| } |
| |
| // Registers the response `content_html` for a given `relative_path`, with |
| // all placeholders $1, $2, ... in `content_html` replaced with the |
| // corresponding hostname from `kHostnames`. |
| // This response is served by for *every* hostname. |
| void SetUrlContent(std::string relative_path, std::string_view content_html) { |
| ASSERT_EQ(relative_path[0], '/'); |
| std::vector<std::string> replacements; |
| replacements.reserve(std::size(kHostnames)); |
| for (const char* hostname : kHostnames) { |
| replacements.push_back(std::string(base::TrimString( |
| embedded_https_test_server().GetURL(hostname, "/").spec(), "/", |
| base::TRIM_TRAILING))); |
| } |
| pages_[std::move(relative_path)] = |
| base::ReplaceStringPlaceholders(content_html, replacements, nullptr); |
| } |
| |
| // Navigates on https://`kMainHostname`:some_port/`relative_url` and returns a |
| // form, if one exists, that has `num_fields` fields. |
| // |
| // If `click_to_extract`, it additionally clicks into the first field of each |
| // frame (if such a field exists). See GetOrWaitForFormWithFocusableFields() |
| // for details why. |
| // |
| // Each test shall prepare the intended response using SetUrlContent() in |
| // advance. |
| const FormStructure* NavigateToUrl(std::string_view relative_url, |
| size_t num_fields) { |
| NavigateParams params( |
| browser(), |
| embedded_https_test_server().GetURL(kMainHostname, relative_url), |
| ui::PAGE_TRANSITION_LINK); |
| params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| ui_test_utils::NavigateToURL(¶ms); |
| return GetOrWaitForFormWithFocusableFields( |
| /*num_fields=*/num_fields); |
| } |
| |
| // Returns a form with `num_fields` fields. If no such form exists and no such |
| // form appears within a timeout, returns nullptr. |
| // |
| // Sometimes fields are unfocusable (FormFieldData::is_focusable is false) |
| // when they are extracted on page load. This issue appears to be unrelated to |
| // AutofillAcrossIframes; it's probably just a race condition between Blink |
| // and Autofill's form extraction. Focusing a field re-extracts the field's |
| // form, and then fields seem to be focusable. That is, clicking into some |
| // field of each form in each frame would likely work around the focusability |
| // issue for the purposes of this bug. However, since clicking into each may |
| // also have other side effects (parsing more forms again) and is not common |
| // user behaviour, we do not simulate such clicks. Instead, we simply override |
| // FormFieldData::is_focusable for all forms. This is admissible for our |
| // testing purposes because all test forms only have (what should be) |
| // focusable fields. |
| // TODO(crbug.com/40248042): Remove this hack when the focusability issue is |
| // fixed. |
| const FormStructure* GetOrWaitForFormWithFocusableFields(size_t num_fields) { |
| const FormStructure* form = |
| main_autofill_manager().WaitForMatchingForm(base::BindRepeating( |
| [](size_t num_fields, const FormStructure& form) { |
| return num_fields == form.field_count(); |
| }, |
| num_fields)); |
| for (const auto& field : *form) |
| const_cast<AutofillField&>(*field).set_is_focusable(true); |
| return form; |
| } |
| |
| // Fills the form in the DOM that corresponds to `form_structure` and returns |
| // the filled values. The order of the values is aligned with the order of the |
| // `form_structure` fields. |
| std::vector<std::string> FillForm(const FormStructure& form_structure, |
| const AutofillField& trigger_field) { |
| const FormData& form = form_structure.ToFormData(); |
| FillCard(main_frame(), form, trigger_field); |
| return AllFieldValues(web_contents(), form); |
| } |
| |
| content::WebContents* web_contents() { |
| return browser()->tab_strip_model()->GetActiveWebContents(); |
| } |
| |
| content::RenderFrameHost* main_frame() { |
| return web_contents()->GetPrimaryMainFrame(); |
| } |
| |
| TestAutofillManager& main_autofill_manager() { |
| return TestAutofillManager::GetForRenderFrameHost(main_frame()); |
| } |
| |
| private: |
| static constexpr std::array kHostnames = {"a.com", "b.com", "c.com", |
| "d.com", "e.com", "f.com"}; |
| static constexpr const char* kMainHostname = kHostnames[0]; |
| |
| test::AutofillBrowserTestEnvironment autofill_test_environment_; |
| base::test::ScopedFeatureList feature_list_{ |
| features::kAutofillSharedAutofill}; |
| content::ContentMockCertVerifier cert_verifier_; |
| // Maps relative paths to HTML content. |
| std::map<std::string, std::string> pages_; |
| TestAutofillManagerInjector<TestAutofillManager> autofill_manager_injector_; |
| }; |
| |
| // Test fixture for basic filling, in particular for testing the security policy |
| // (same-origin policy and shared-autofill). |
| class AutofillAcrossIframesTest_Simple : public AutofillAcrossIframesTest { |
| public: |
| // Creates a simple form |
| // <form> |
| // <iframe><input autocomplete=cc-name></iframe> |
| // <iframe><input autocomplete=cc-number></iframe> |
| // <iframe><input autocomplete=cc-exp></iframe> |
| // <iframe><input autocomplete=cc-csc></iframe> |
| // </form> |
| // where the hostnames and attributes, such as "allow=shared-autofill" or |
| // "sandbox", can be configured. |
| [[nodiscard]] const FormStructure* LoadForm( |
| std::array<const char*, 4> hostnames = {"$1", "$1", "$1", "$1"}, |
| std::array<const char*, 4> attributes = {"", "", "", ""}) { |
| SetUrlContent("/name.html", R"(<input autocomplete=cc-name>)"); |
| SetUrlContent("/num.html", R"(<input autocomplete=cc-number>)"); |
| SetUrlContent("/exp.html", R"(<input autocomplete=cc-exp>)"); |
| SetUrlContent("/cvc.html", R"(<input autocomplete=cc-csc>)"); |
| SetUrlContent( |
| "/", base::StringPrintf( |
| R"(<iframe %s src="%s/name.html"></iframe> |
| <iframe %s src="%s/num.html"></iframe> |
| <iframe %s src="%s/exp.html"></iframe> |
| <iframe %s src="%s/cvc.html"></iframe>)", |
| attributes[0], hostnames[0], attributes[1], hostnames[1], |
| attributes[2], hostnames[2], attributes[3], hostnames[3])); |
| return NavigateToUrl("/", /*num_fields=*/4); |
| } |
| }; |
| |
| // Tests that autofilling on a main-origin field fills all same-origin fields. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_Simple, SameOrigin_FillAll) { |
| const FormStructure* form = LoadForm({"$1", "$1", "$1", "$1"}); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(0)), |
| ElementsAre(kNameFull, kNumber, kExp, kCvc)); |
| } |
| |
| // Tests that autofilling on a main-origin field fills only fills on the main |
| // origin. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_Simple, CrossOrigin_FillName) { |
| const FormStructure* form = LoadForm({"$1", "$2", "$3", "$4"}); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(0)), |
| ElementsAre(kNameFull, "", "", "")); |
| } |
| |
| // Tests that autofilling on a cross-origin field fills only fills on that |
| // origin and on the main origin (if it's a non-sensitive field). |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_Simple, |
| CrossOrigin_FillNameAndNumber) { |
| const FormStructure* form = LoadForm({"$1", "$2", "$3", "$4"}); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(1)), |
| ElementsAre(kNameFull, kNumber, "", "")); |
| } |
| |
| // Tests that autofilling on a cross-origin field fills only fills on that |
| // origin and on the main origin only non-sensitive fields. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_Simple, |
| CrossOrigin_FillNameOnMainOrigin) { |
| const FormStructure* form = LoadForm({"$1", "$2", "$1", "$1"}); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(1)), |
| ElementsAre(kNameFull, kNumber, kExp, "")); |
| } |
| |
| // Tests that sandboxed frames are treated like other cross-origin frames. |
| // |
| // This test seemed flaky in one patchset due to a DCHECK in |
| // content_settings::PatternPair GetPatternsFromScopingType(), but the issue |
| // didn't occur afterwards. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_Simple, |
| Sandboxed_FillOnlyNumber) { |
| // Our test fixture needs allow-scripts to extract the field values. |
| static constexpr char sandbox[] = "sandbox=allow-scripts"; |
| const FormStructure* form = |
| LoadForm({"$1", "$1", "$1", "$1"}, {sandbox, sandbox, sandbox, sandbox}); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(1)), |
| ElementsAre("", kNumber, "", "")); |
| } |
| |
| // Test fixture for "shared-autofill". The parameter indicates whether or not |
| // shared-autofill has the "relaxed" semantics. |
| class AutofillAcrossIframesTest_SharedAutofill |
| : public AutofillAcrossIframesTest_Simple { |
| private: |
| base::test::ScopedFeatureList feature_list_{ |
| features::kAutofillSharedAutofill}; |
| }; |
| |
| // Tests that autofilling on a main-origin field also fills cross-origin fields |
| // whose frames have shared-autofill enabled. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_SharedAutofill, |
| FillWhenTriggeredOnMainOrigin) { |
| const FormStructure* form = |
| LoadForm({"$1", "$2", "$3", "$4"}, {"", "", "", "allow=shared-autofill"}); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(0)), |
| ElementsAre(kNameFull, "", "", kCvc)); |
| } |
| |
| // Tests that autofilling on a cross-origin field does not fill cross-origin |
| // fields, even if shared-autofill in their document. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_SharedAutofill, |
| FillWhenTriggeredOnNonMainOriginIffRelaxed) { |
| const FormStructure* form = |
| LoadForm({"$1", "$2", "$3", "$4"}, {"", "", "", "allow=shared-autofill"}); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(1)), |
| ElementsAre(kNameFull, kNumber, "", "")); |
| } |
| |
| // Test fixture where a form changes dynamically when it is filled. |
| class AutofillAcrossIframesTest_Dynamic : public AutofillAcrossIframesTest { |
| public: |
| // Adds the CVC iframe (including a field) dynamically when the rest of the |
| // form is filled. |
| const FormStructure* LoadFormWithAppearingFrame() { |
| SetUrlContent("/name.html", R"( |
| <input autocomplete=cc-name> |
| <script> |
| document.body.firstElementChild.onchange = function() { |
| const doc = window.parent.document; |
| const iframe = doc.createElement('iframe'); |
| iframe.src = '$1/cvc.html'; |
| doc.body.appendChild(iframe); |
| }; |
| </script> )"); |
| SetUrlContent("/num.html", R"(<input autocomplete=cc-number>)"); |
| SetUrlContent("/exp.html", R"(<input autocomplete=cc-exp>)"); |
| SetUrlContent("/cvc.html", R"(<input autocomplete=cc-csc>)"); |
| SetUrlContent("/", base::StringPrintf( |
| R"(<iframe src="$1/name.html"></iframe> |
| <iframe src="$1/num.html"></iframe> |
| <iframe src="$1/exp.html"></iframe>)")); |
| return NavigateToUrl("/", /*num_fields=*/3); |
| } |
| |
| // Adds the CVC field dynamically when the rest of the form is filled. |
| const FormStructure* LoadFormWithAppearingField() { |
| SetUrlContent("/name.html", R"( |
| <input autocomplete=cc-name> |
| <script> |
| document.body.firstElementChild.onchange = function() { |
| const doc = window.parent.frames[3].document; |
| const input = doc.createElement('input'); |
| input.autocomplete = 'cc-csc'; |
| doc.body.appendChild(input); |
| }; |
| </script> )"); |
| SetUrlContent("/num.html", R"(<input autocomplete=cc-number>)"); |
| SetUrlContent("/exp.html", R"(<input autocomplete=cc-exp>)"); |
| SetUrlContent("/cvc.html", ""); |
| SetUrlContent("/", base::StringPrintf( |
| R"(<iframe src="$1/name.html"></iframe> |
| <iframe src="$1/num.html"></iframe> |
| <iframe src="$1/exp.html"></iframe> |
| <iframe src="$1/cvc.html"></iframe>)")); |
| return NavigateToUrl("/", /*num_fields=*/3); |
| } |
| |
| // Fills `form_structure` and returns the filled values. The order of the |
| // values is aligned with the order of the `form_structure` fields. |
| std::vector<std::string> FillForm(const FormStructure& form_structure, |
| const AutofillField& trigger_field) { |
| FormData form = form_structure.ToFormData(); |
| EXPECT_EQ(3u, form.fields.size()); // The CVC field doesn't exist yet. |
| TestAutofillManager& manager = main_autofill_manager(); |
| FillCard(main_frame(), form, trigger_field); |
| // Now, after FillCard(), the form gets filled in the renderer (which |
| // triggers three OnDidFillAutofillFormData() events) and then changes. |
| // The change triggers an OnFormsSeen() event, followed by a form |
| // re-extraction and re-fill. The only newly filled field in the refill is |
| // the CVC field, which triggers another OnDidFillAutofillFormData() event. |
| EXPECT_TRUE(manager.WaitForAutofill(3 + 1)); |
| form = |
| manager.form_structures().find(form.global_id())->second->ToFormData(); |
| EXPECT_EQ(4u, form.fields.size()); // The CVC field has now been seen. |
| return AllFieldValues(web_contents(), form); |
| } |
| }; |
| |
| // Tests that a newly emerging frame with a field triggers a refill. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_Dynamic, |
| RefillDynamicFormWithNewFrame) { |
| const FormStructure* form = LoadFormWithAppearingFrame(); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(1)), |
| ElementsAre(kNameFull, kNumber, kExp, kCvc)); |
| } |
| |
| // Tests that a newly emerging field inside a frame triggers a refill. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_Dynamic, |
| RefillDynamicFormWithNewField) { |
| const FormStructure* form = LoadFormWithAppearingField(); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(1)), |
| ElementsAre(kNameFull, kNumber, kExp, kCvc)); |
| } |
| |
| // Test fixture that removes a frame right before the fill. This shall not |
| // confuse the form filling, in particular, it shall not crash. |
| class AutofillAcrossIframesTest_DeletedFrame |
| : public AutofillAcrossIframesTest_Simple { |
| public: |
| std::vector<std::string> FillForm(const FormStructure& form_structure, |
| const AutofillField& trigger_field) { |
| FormData form = form_structure.ToFormData(); |
| EXPECT_EQ(4u, form.fields.size()); |
| EXPECT_EQ(5u, num_frames()); |
| std::ignore = content::EvalJs( |
| main_frame(), |
| R"( document.getElementsByTagName('iframe')[1].remove(); )"); |
| EXPECT_EQ(4u, num_frames()); |
| FillCard(main_frame(), form, trigger_field); |
| form.fields.erase(form.fields.begin() + 1); |
| return AllFieldValues(web_contents(), form); |
| } |
| |
| private: |
| // Returns the number of frames in the frame tree, including the main frame. |
| size_t num_frames() { |
| size_t num = 0; |
| main_frame()->ForEachRenderFrameHost( |
| [&num](content::RenderFrameHost* rfh) { ++num; }); |
| return num; |
| } |
| }; |
| |
| // Tests that we don't crash if the filling data is referring to a non-existent |
| // frame. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_DeletedFrame, |
| DeletingFrameDuringFillDoesNotCrash) { |
| const FormStructure* form = LoadForm(); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(1)), |
| ElementsAre(kNameFull, kExp, kCvc)); |
| } |
| |
| // Test fixture for huge forms. |
| class AutofillAcrossIframesTest_NestedAndLargeForm |
| : public AutofillAcrossIframesTest { |
| public: |
| // Manually specify large frame sizes. This simplifies clicking into them. |
| std::string MakeCss(size_t height) { |
| return base::StringPrintf( |
| R"(<style> |
| * { margin: 0; padding: 0; } |
| iframe { height: %zupx; width: %zupx; border: 0; } |
| input { display: block; height: 20px; width: 100px; } |
| </style>)", |
| height * 100, 100 + height * 10); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList scoped_features_{ |
| features::kAutofillEnableExpirationDateImprovements}; |
| }; |
| |
| // Tests that a large and deeply nested form is extracted and filled correctly. |
| // The test makes heavy use of abbreviations to make it easier to spot the |
| // pattern in the form. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_NestedAndLargeForm, |
| FillAllFieldsOnTriggeredOrigin) { |
| // The `n` in `n.html` is the height of the frame sub-tree, i.e., a frame that |
| // loads `1.html` is a leaf frame, `2.html` has child frames but no |
| // grandchildren, and so on. |
| // The origins are picked arbitrarily. |
| SetUrlContent("/", MakeCss(3) + |
| R"(<iframe src="$4/3.html"></iframe> |
| <iframe src="$3/3.html"></iframe> |
| <iframe src="$2/3.html"></iframe> |
| <iframe src="$1/3.html"></iframe>)"); |
| SetUrlContent("/3.html", MakeCss(2) + |
| R"(<form> |
| <input autocomplete=cc-name> |
| <input> |
| <iframe src="$2/2.html"></iframe> |
| <input> |
| <input autocomplete=cc-csc> |
| </form>)"); |
| SetUrlContent("/2.html", MakeCss(1) + |
| R"(<form> |
| <input autocomplete=cc-number> |
| <input> |
| <iframe src="$5/1.html"></iframe> |
| <input> |
| <input autocomplete=cc-exp> |
| </form>)"); |
| SetUrlContent("/1.html", MakeCss(0) + |
| R"(<form> |
| <input autocomplete=cc-name> |
| <input autocomplete=cc-number> |
| <input autocomplete=cc-exp> |
| <input autocomplete=cc-csc> |
| </form>)"); |
| const FormStructure* form = NavigateToUrl("/", /*num_fields=*/48); |
| ASSERT_TRUE(form); |
| ASSERT_THAT(*form, IsWithinAutofillLimits()); |
| { |
| // Test that the extracted form reflects the structure of the above <iframe> |
| // and <form> elements. |
| auto name = HtmlFieldType::kCreditCardNameFull; |
| auto num = HtmlFieldType::kCreditCardNumber; |
| auto exp = HtmlFieldType::kCreditCardExpDate4DigitYear; |
| auto cvc = HtmlFieldType::kCreditCardVerificationCode; |
| auto unspecified = HtmlFieldType::kUnspecified; |
| auto m = [](std::string_view host, HtmlFieldType type) { |
| return Pointee(AllOf(Property(&AutofillField::html_type, Eq(type)), |
| Property(&AutofillField::origin, |
| Property(&url::Origin::host, Eq(host))))); |
| }; |
| // The indentation reflects the nesting of frames. |
| // clang-format off |
| EXPECT_THAT(form->fields(), |
| ElementsAre( |
| // $4/3.html |
| m("d.com", name), |
| m("d.com", unspecified), |
| m("b.com", num), |
| m("b.com", unspecified), |
| m("e.com", name), |
| m("e.com", num), |
| m("e.com", exp), |
| m("e.com", cvc), |
| m("b.com", unspecified), |
| m("b.com", exp), |
| m("d.com", unspecified), |
| m("d.com", cvc), |
| // $3/3.html |
| m("c.com", name), |
| m("c.com", unspecified), |
| m("b.com", num), |
| m("b.com", unspecified), |
| m("e.com", name), |
| m("e.com", num), |
| m("e.com", exp), |
| m("e.com", cvc), |
| m("b.com", unspecified), |
| m("b.com", exp), |
| m("c.com", unspecified), |
| m("c.com", cvc), |
| // $2/3.html |
| m("b.com", name), |
| m("b.com", unspecified), |
| m("b.com", num), |
| m("b.com", unspecified), |
| m("e.com", name), |
| m("e.com", num), |
| m("e.com", exp), |
| m("e.com", cvc), |
| m("b.com", unspecified), |
| m("b.com", exp), |
| m("b.com", unspecified), |
| m("b.com", cvc), |
| // $1/3.html |
| m("a.com", name), |
| m("a.com", unspecified), |
| m("b.com", num), |
| m("b.com", unspecified), |
| m("e.com", name), |
| m("e.com", num), |
| m("e.com", exp), |
| m("e.com", cvc), |
| m("b.com", unspecified), |
| m("b.com", exp), |
| m("a.com", unspecified), |
| m("a.com", cvc) |
| )); |
| // clang-format on |
| } |
| const FormData& form_data = form->ToFormData(); |
| ASSERT_EQ("e.com", form_data.fields[4].origin().host()); |
| ASSERT_EQ("cc-name", form_data.fields[4].autocomplete_attribute()); |
| FillCard(main_frame(), form_data, form_data.fields[4]); |
| EXPECT_TRUE(main_autofill_manager().WaitForAutofill(5)); |
| { |
| // `rat` represents a value that is not filled only due to rationalization. |
| constexpr const char* rat = ""; |
| constexpr const char* name = kNameFull; |
| constexpr const char* num = kNumber; |
| constexpr const char* exp = kExp; |
| constexpr const char* cvc = kCvc; |
| std::vector<std::string> values = AllFieldValues(web_contents(), form_data); |
| EXPECT_THAT( |
| values, |
| ElementsAre("", "", "", "", name, num, exp, cvc, "", "", "", "", // |
| "", "", "", "", name, num, exp, cvc, "", "", "", "", // |
| "", "", "", "", name, num, exp, cvc, "", "", "", "", // |
| name, "", "", "", name, num, exp, cvc, "", "", "", rat)); |
| } |
| } |
| |
| // Tests that a deeply nested form where some iframes don't even contain any |
| // fields (but their subframes do) is extracted and filled correctly. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_NestedAndLargeForm, |
| FlattenFormEvenAcrossFramesWithoutFields) { |
| SetUrlContent("/", MakeCss(3) + |
| R"(<iframe src="$4/3.html"></iframe> |
| <iframe src="$3/3.html"></iframe> |
| <iframe src="$2/3.html"></iframe> |
| <iframe src="$1/3.html"></iframe>)"); |
| SetUrlContent( |
| "/3.html", |
| MakeCss(2) + R"(<form><iframe src="$2/2.html"></iframe></form>)"); |
| SetUrlContent("/2.html", MakeCss(1) + R"(<iframe src="$1/1.html"></iframe>)"); |
| SetUrlContent("/1.html", MakeCss(0) + |
| R"(<form><input autocomplete=cc-name></form> |
| <form><input autocomplete=cc-number></form> |
| <form><input autocomplete=cc-exp></form> |
| <form><input autocomplete=cc-csc></form>)"); |
| const FormStructure* form = NavigateToUrl("/", /*num_fields=*/16); |
| ASSERT_TRUE(form); |
| ASSERT_THAT(*form, IsWithinAutofillLimits()); |
| { |
| // Test that the extracted form reflects the structure of the above <iframe> |
| // and <form> elements. |
| auto name = HtmlFieldType::kCreditCardNameFull; |
| auto num = HtmlFieldType::kCreditCardNumber; |
| auto exp = HtmlFieldType::kCreditCardExpDate4DigitYear; |
| auto cvc = HtmlFieldType::kCreditCardVerificationCode; |
| auto m = [](HtmlFieldType type) { |
| return Pointee( |
| AllOf(Property(&AutofillField::html_type, Eq(type)), |
| Property(&AutofillField::origin, |
| Property(&url::Origin::host, Eq("a.com"))))); |
| }; |
| EXPECT_THAT(form->fields(), ElementsAre(m(name), m(num), m(exp), m(cvc), // |
| m(name), m(num), m(exp), m(cvc), // |
| m(name), m(num), m(exp), m(cvc), // |
| m(name), m(num), m(exp), m(cvc))); |
| } |
| const FormData& form_data = form->ToFormData(); |
| FillCard(main_frame(), form_data, form_data.fields[0]); |
| EXPECT_TRUE(main_autofill_manager().WaitForAutofill(4)); |
| { |
| const auto* name = kNameFull; |
| const auto* num = kNumber; |
| const auto* exp = kExp; |
| const auto* cvc = kCvc; |
| std::vector<std::string> values = AllFieldValues(web_contents(), form_data); |
| EXPECT_THAT(values, ElementsAre(name, num, exp, cvc, name, num, exp, cvc, |
| name, num, exp, cvc, name, num, exp, cvc)); |
| } |
| } |
| |
| class AutofillAcrossIframesTest_SubmissionBase |
| : public AutofillAcrossIframesTest { |
| public: |
| [[nodiscard]] AssertionResult SubmitInArbitraryIframe() { |
| bool submitted = false; |
| AssertionResult result = AssertionFailure() << "No frame found"; |
| main_frame()->ForEachRenderFrameHost([&](content::RenderFrameHost* rfh) { |
| if (!rfh->IsInPrimaryMainFrame() && !submitted) { |
| result = SubmitInFrame(rfh); |
| submitted = true; |
| } |
| }); |
| return result ? main_autofill_manager().WaitForSubmission(1) : result; |
| } |
| |
| [[nodiscard]] AssertionResult SubmitInMainFrame() { |
| AssertionResult result = SubmitInFrame(main_frame()); |
| return result ? main_autofill_manager().WaitForSubmission(1) : result; |
| } |
| |
| [[nodiscard]] AssertionResult SubmitInFrame(content::RenderFrameHost* rfh) { |
| return content::ExecJs(rfh, R"(document.forms[0].submit();)"); |
| } |
| }; |
| |
| // Test fixture for detecting form submission. The parameter indicates whether |
| // the submission occurs in the main frame or an iframe. |
| class AutofillAcrossIframesTest_Submission |
| : public AutofillAcrossIframesTest_SubmissionBase, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| bool submission_happens_in_main_frame() const { return GetParam(); } |
| |
| void TearDownOnMainThread() override { |
| // RunUntilIdle() is necessary because otherwise, under the hood |
| // PasswordFormManager::OnFetchComplete() callback is run after this test is |
| // destroyed meaning that OsCryptImpl will be used instead of OsCryptMocker, |
| // causing this test to fail. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| // Creates a simple cross-frame form with <form> elements so we can submit the |
| // form in the iframe and the main frame. |
| // |
| // Just to mix things up a little compared to the other tests, here the |
| // "name" field is in the main frame, not just on the main frame origin. |
| [[nodiscard]] const FormStructure* LoadForm( |
| std::array<const char*, 3> hostnames = {"$1", "$1", "$1"}) { |
| auto frame_html = [](const char* autocomplete) { |
| return base::StringPrintf(R"(<form action="$1/submit.html" method="GET"> |
| <input name="%s" autocomplete="%s"> |
| </form>)", |
| autocomplete, autocomplete); |
| }; |
| SetUrlContent("/num.html", frame_html("cc-number")); |
| SetUrlContent("/exp.html", frame_html("cc-exp")); |
| SetUrlContent("/cvc.html", frame_html("cc-csc")); |
| SetUrlContent("/submit.html", "<h1>Submitted</h1>"); |
| SetUrlContent("/", base::StringPrintf( |
| R"(<form method=GET action=submit.html> |
| <input name=cc-name autocomplete=cc-name> |
| <iframe src="%s/num.html"></iframe> |
| <iframe src="%s/exp.html"></iframe> |
| <iframe src="%s/cvc.html"></iframe> |
| </form>)", |
| hostnames[0], hostnames[1], hostnames[2])); |
| return NavigateToUrl("/", /*num_fields=*/4); |
| } |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(AutofillAcrossIframesTest, |
| AutofillAcrossIframesTest_Submission, |
| ::testing::Bool()); |
| |
| // Tests that submission of a cross-frame form is detected in the main frame. |
| IN_PROC_BROWSER_TEST_P(AutofillAcrossIframesTest_Submission, |
| SubmissionGetsDetected) { |
| const FormStructure* form = LoadForm({"$2", "$2", "$2"}); |
| ASSERT_TRUE(form); |
| ASSERT_THAT(FillForm(*form, *form->field(1)), |
| ElementsAre(kNameFull, kNumber, kExp, kCvc)); |
| ASSERT_TRUE(submission_happens_in_main_frame() ? SubmitInMainFrame() |
| : SubmitInArbitraryIframe()); |
| EXPECT_THAT(main_autofill_manager().submitted_form(), |
| Optional(Field(&FormData::fields, |
| ElementsAre(HasValue(kNameFull), HasValue(kNumber), |
| HasValue(kExp), HasValue(kCvc))))); |
| } |
| |
| // Test fixture for a case where on load each iframe contains a full credit card |
| // form (cc-name, cc-number, cc-exp, cc-csc), but then after load the fields are |
| // removed such that the remaining form contains a credit card form in which |
| // each field type exists only once. |
| // This is an integration test for b:245749889. |
| class AutofillAcrossIframesTest_FullIframes |
| : public AutofillAcrossIframesTest_SubmissionBase { |
| public: |
| AutofillAcrossIframesTest_FullIframes() { |
| feature_list_.InitAndEnableFeature( |
| features::kAutofillDetectRemovedFormControls); |
| } |
| |
| [[nodiscard]] const FormStructure* LoadForm() { |
| SetUrlContent("/iframe.html", R"( |
| <div> |
| <form> |
| <input autocomplete=cc-name> |
| <input autocomplete=cc-number> |
| <input autocomplete=cc-exp> |
| <input autocomplete=cc-csc> |
| </form> |
| <div> |
| <script> |
| function deleteAllInputsButIndex(idx) { |
| const fields = [...document.getElementsByTagName('INPUT')]; |
| for (let i = 0; i < fields.length; ++i) { |
| if (i != idx) { |
| fields[i].parentNode.removeChild(fields[i]); |
| } |
| } |
| } |
| function deleteForm() { |
| document.getElementsByTagName('FORM')[0].remove(); |
| } |
| function deleteParentOfForm() { |
| document.getElementsByTagName('DIV')[0].remove(); |
| } |
| </script>)"); |
| SetUrlContent("/submit.html", "<h1>Submitted</h1>"); |
| SetUrlContent("/", R"( |
| <script> |
| function removeFields() { |
| for (let i = 0; i < 4; ++i) { |
| document.getElementsByTagName("IFRAME")[i] |
| .contentWindow |
| .deleteAllInputsButIndex(i); |
| } |
| } |
| </script> |
| <form method=GET action=submit.html> |
| <iframe src="iframe.html"></iframe> |
| <iframe src="iframe.html"></iframe> |
| <iframe src="iframe.html"></iframe> |
| <iframe src="iframe.html"></iframe> |
| </form>)"); |
| return NavigateToUrl("/", /*num_fields=*/4 * 4); |
| } |
| |
| [[nodiscard]] const FormStructure* FormAfterRemovalOfExtraFields() { |
| // A core part of this test is in the following lines: We check that after |
| // removing fields, the BrowserAutofillAgent learns about that. |
| if (!content::ExecJs(web_contents(), "removeFields();")) { |
| ADD_FAILURE() << "Failed to call removeFields();"; |
| return nullptr; |
| } |
| return GetOrWaitForFormWithFocusableFields( |
| /*num_fields=*/4); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Tests that autofilling on a main-origin field fills all same-origin fields. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_FullIframes, FillAll) { |
| ASSERT_TRUE(LoadForm()); |
| const FormStructure* form = FormAfterRemovalOfExtraFields(); |
| ASSERT_TRUE(form); |
| EXPECT_THAT(FillForm(*form, *form->field(0)), |
| ElementsAre(kNameFull, kNumber, kExp, kCvc)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_FullIframes, Submit) { |
| ASSERT_TRUE(LoadForm()); |
| const FormStructure* form = FormAfterRemovalOfExtraFields(); |
| ASSERT_TRUE(form); |
| ASSERT_THAT(FillForm(*form, *form->field(0)), |
| ElementsAre(kNameFull, kNumber, kExp, kCvc)); |
| ASSERT_TRUE(SubmitInMainFrame()); |
| EXPECT_THAT(main_autofill_manager().submitted_form(), |
| Optional(Field(&FormData::fields, |
| ElementsAre(HasValue(kNameFull), HasValue(kNumber), |
| HasValue(kExp), HasValue(kCvc))))); |
| } |
| |
| // Tests that the Autofill Manager notices if an entire <form> is removed. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_FullIframes, |
| DetectFormRemoval) { |
| // This loads 4 iframes, each containing a <form> element with 4 fields. |
| ASSERT_TRUE(LoadForm()); |
| |
| // This removes the entire <form> element for the first iframe. |
| ASSERT_TRUE(content::ExecJs(web_contents(), R"( |
| document.getElementsByTagName("IFRAME")[0] |
| .contentWindow |
| .deleteForm(); |
| )")); |
| |
| // As a consequence only 3 forms of 4 fields remain. |
| EXPECT_TRUE(GetOrWaitForFormWithFocusableFields( |
| /*num_fields=*/3 * 4)); |
| } |
| |
| // Tests that the Autofill Manager notices if the parent containing a <form> is |
| // removed. |
| IN_PROC_BROWSER_TEST_F(AutofillAcrossIframesTest_FullIframes, |
| DetectParentOfFormRemoval) { |
| // This loads 4 iframes, each containing a <form> element with 4 fields. |
| ASSERT_TRUE(LoadForm()); |
| |
| // This removes the entire <form> element for the first iframe. |
| ASSERT_TRUE(content::ExecJs(web_contents(), R"( |
| document.getElementsByTagName("IFRAME")[0] |
| .contentWindow |
| .deleteParentOfForm(); |
| )")); |
| |
| // As a consequence only 3 forms of 4 fields remain. |
| EXPECT_TRUE(GetOrWaitForFormWithFocusableFields( |
| /*num_fields=*/3 * 4)); |
| } |
| |
| } // namespace autofill |