Commit 6fa61eb9 authored by Maria Kazinova's avatar Maria Kazinova Committed by Commit Bot

[iOS] Submission detection on iframe detach.

Submitted forms in iframes can be removed from DOM by detaching parent
iframes. The moment if detachment is caught by the PasswordController
and the PasswordManager checks if it has any submitted
PasswordFormManagers related to this frame.

Bug: 1042196
Change-Id: Ia02fc946477202768c80b84532a08c20f00538f9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2237933
Commit-Queue: Maria Kazinova <kazinova@google.com>
Reviewed-by: default avatarVadym Doroshenko  <dvadym@chromium.org>
Cr-Commit-Position: refs/heads/master@{#779897}
parent 1658c432
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#include <vector> #include <vector>
#include "base/strings/string16.h" #include "base/strings/string16.h"
#include "build/build_config.h"
#include "components/autofill/core/common/form_field_data.h" #include "components/autofill/core/common/form_field_data.h"
#include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h" #include "components/autofill/core/common/mojom/autofill_types.mojom-shared.h"
#include "components/autofill/core/common/renderer_id.h" #include "components/autofill/core/common/renderer_id.h"
...@@ -114,6 +115,9 @@ struct FormData { ...@@ -114,6 +115,9 @@ struct FormData {
std::vector<FieldRendererId> username_predictions; std::vector<FieldRendererId> username_predictions;
// True if this is a Gaia form which should be skipped on saving. // True if this is a Gaia form which should be skipped on saving.
bool is_gaia_with_skip_save_password_form = false; bool is_gaia_with_skip_save_password_form = false;
#if defined(OS_IOS)
std::string frame_id;
#endif
}; };
// For testing. // For testing.
......
...@@ -138,6 +138,7 @@ bool ExtractFormData(const base::Value& form_value, ...@@ -138,6 +138,7 @@ bool ExtractFormData(const base::Value& form_value,
form_dictionary->GetBoolean("is_form_tag", &form_data->is_form_tag); form_dictionary->GetBoolean("is_form_tag", &form_data->is_form_tag);
form_dictionary->GetBoolean("is_formless_checkout", form_dictionary->GetBoolean("is_formless_checkout",
&form_data->is_formless_checkout); &form_data->is_formless_checkout);
form_dictionary->GetString("frame_id", &form_data->frame_id);
// Field list (mandatory) is extracted. // Field list (mandatory) is extracted.
const base::ListValue* fields_list = nullptr; const base::ListValue* fields_list = nullptr;
......
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
#import "ios/web/public/test/web_test_with_web_state.h" #import "ios/web/public/test/web_test_with_web_state.h"
#include "testing/platform_test.h" #include "testing/platform_test.h"
using web::WebFrame;
class FormTestClient : public web::TestWebClient { class FormTestClient : public web::TestWebClient {
public: public:
NSString* GetDocumentStartScriptForAllFrames( NSString* GetDocumentStartScriptForAllFrames(
...@@ -61,19 +63,22 @@ TEST_F(FormActivityTabHelperTest, TestObserverDocumentSubmitted) { ...@@ -61,19 +63,22 @@ TEST_F(FormActivityTabHelperTest, TestObserverDocumentSubmitted) {
ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);"); ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);");
ASSERT_FALSE(observer_->submit_document_info()); ASSERT_FALSE(observer_->submit_document_info());
const std::string kTestFormName("form-name"); const std::string kTestFormName("form-name");
const std::string kTestFormData(
"[{\"name\":\"form-name\",\"origin\":\"https://chromium.test/" WebFrame* main_frame = web_state()->GetWebFramesManager()->GetMainWebFrame();
"\",\"action\":\"https://chromium.test/\"," std::string mainFrameID = main_frame->GetFrameId();
"\"name_attribute\":\"form-name\",\"id_attribute\":\"\"," const std::string kTestFormData =
"\"unique_renderer_id\":\"0\"}]"); std::string("[{\"name\":\"form-name\",\"origin\":\"https://chromium.test/"
"\",\"action\":\"https://chromium.test/\","
"\"name_attribute\":\"form-name\",\"id_attribute\":\"\","
"\"unique_renderer_id\":\"0\",\"frame_id\":\"") +
mainFrameID + std::string("\"}]");
bool has_user_gesture = false; bool has_user_gesture = false;
bool form_in_main_frame = true; bool form_in_main_frame = true;
EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout( EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForJSCompletionTimeout, ^bool { base::test::ios::kWaitForJSCompletionTimeout, ^bool {
return web_state()->GetWebFramesManager()->GetMainWebFrame() != nullptr; return web_state()->GetWebFramesManager()->GetMainWebFrame() != nullptr;
})); }));
web::WebFrame* main_frame =
web_state()->GetWebFramesManager()->GetMainWebFrame();
ExecuteJavaScript(@"document.getElementById('submit').click();"); ExecuteJavaScript(@"document.getElementById('submit').click();");
ASSERT_TRUE(observer_->submit_document_info()); ASSERT_TRUE(observer_->submit_document_info());
...@@ -96,19 +101,22 @@ TEST_F(FormActivityTabHelperTest, TestFormSubmittedHook) { ...@@ -96,19 +101,22 @@ TEST_F(FormActivityTabHelperTest, TestFormSubmittedHook) {
ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);"); ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);");
ASSERT_FALSE(observer_->submit_document_info()); ASSERT_FALSE(observer_->submit_document_info());
const std::string kTestFormName("form-name"); const std::string kTestFormName("form-name");
const std::string kTestFormData(
"[{\"name\":\"form-name\",\"origin\":\"https://chromium.test/" WebFrame* main_frame = web_state()->GetWebFramesManager()->GetMainWebFrame();
"\",\"action\":\"https://chromium.test/\"," std::string mainFrameID = main_frame->GetFrameId();
"\"name_attribute\":\"form-name\",\"id_attribute\":\"form\"," const std::string kTestFormData =
"\"unique_renderer_id\":\"0\"}]"); std::string("[{\"name\":\"form-name\",\"origin\":\"https://chromium.test/"
"\",\"action\":\"https://chromium.test/\","
"\"name_attribute\":\"form-name\",\"id_attribute\":\"form\","
"\"unique_renderer_id\":\"0\",\"frame_id\":\"") +
mainFrameID + std::string("\"}]");
bool has_user_gesture = false; bool has_user_gesture = false;
bool form_in_main_frame = true; bool form_in_main_frame = true;
EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout( EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForJSCompletionTimeout, ^bool { base::test::ios::kWaitForJSCompletionTimeout, ^bool {
return web_state()->GetWebFramesManager()->GetMainWebFrame() != nullptr; return web_state()->GetWebFramesManager()->GetMainWebFrame() != nullptr;
})); }));
web::WebFrame* main_frame =
web_state()->GetWebFramesManager()->GetMainWebFrame();
ExecuteJavaScript(@"document.getElementById('form').submit();"); ExecuteJavaScript(@"document.getElementById('form').submit();");
ASSERT_TRUE(observer_->submit_document_info()); ASSERT_TRUE(observer_->submit_document_info());
...@@ -133,8 +141,7 @@ TEST_F(FormActivityTabHelperTest, TestObserverFormActivityFrameMessaging) { ...@@ -133,8 +141,7 @@ TEST_F(FormActivityTabHelperTest, TestObserverFormActivityFrameMessaging) {
base::test::ios::kWaitForJSCompletionTimeout, ^bool { base::test::ios::kWaitForJSCompletionTimeout, ^bool {
return web_state()->GetWebFramesManager()->GetMainWebFrame() != nullptr; return web_state()->GetWebFramesManager()->GetMainWebFrame() != nullptr;
})); }));
web::WebFrame* main_frame = WebFrame* main_frame = web_state()->GetWebFramesManager()->GetMainWebFrame();
web_state()->GetWebFramesManager()->GetMainWebFrame();
ASSERT_FALSE(observer_->form_activity_info()); ASSERT_FALSE(observer_->form_activity_info());
// First call will set document.activeElement (which is usually set by user // First call will set document.activeElement (which is usually set by user
// action. Second call will trigger the message. // action. Second call will trigger the message.
......
...@@ -34,6 +34,7 @@ let AutofillFormFieldData; ...@@ -34,6 +34,7 @@ let AutofillFormFieldData;
* origin: string, * origin: string,
* action: string, * action: string,
* fields: Array<AutofillFormFieldData> * fields: Array<AutofillFormFieldData>
* frame_id: string
* }} * }}
*/ */
let AutofillFormData; let AutofillFormData;
...@@ -851,6 +852,8 @@ __gCrWeb.fill.webFormElementToFormData = function( ...@@ -851,6 +852,8 @@ __gCrWeb.fill.webFormElementToFormData = function(
__gCrWeb.fill.setUniqueIDIfNeeded(formElement); __gCrWeb.fill.setUniqueIDIfNeeded(formElement);
form['unique_renderer_id'] = __gCrWeb.fill.getUniqueID(formElement); form['unique_renderer_id'] = __gCrWeb.fill.getUniqueID(formElement);
form['frame_id'] = frame.__gCrWeb.message.getFrameId();
// Note different from form_autofill_util.cc version of this method, which // Note different from form_autofill_util.cc version of this method, which
// computes |form.action| using document.completeURL(form_element.action()) // computes |form.action| using document.completeURL(form_element.action())
// and falls back to formElement.action() if the computed action is invalid, // and falls back to formElement.action() if the computed action is invalid,
......
...@@ -750,6 +750,14 @@ void PasswordManager::OnPasswordFormRemoved(PasswordManagerDriver* driver, ...@@ -750,6 +750,14 @@ void PasswordManager::OnPasswordFormRemoved(PasswordManagerDriver* driver,
} }
} }
} }
void PasswordManager::OnIframeDetach(const std::string& frame_id) {
PasswordFormManager* submitted_manager = GetSubmittedManager();
if (submitted_manager &&
submitted_manager->observed_form().frame_id == frame_id) {
OnLoginSuccessful();
}
}
#endif #endif
bool PasswordManager::IsAutomaticSavePromptAvailable() { bool PasswordManager::IsAutomaticSavePromptAvailable() {
......
...@@ -243,6 +243,10 @@ class PasswordManager : public FormSubmissionObserver { ...@@ -243,6 +243,10 @@ class PasswordManager : public FormSubmissionObserver {
void OnPasswordFormRemoved(PasswordManagerDriver* driver, void OnPasswordFormRemoved(PasswordManagerDriver* driver,
autofill::FormRendererId form_id); autofill::FormRendererId form_id);
// Checks if there is a submitted PasswordFormManager for a form from the
// detached frame.
void OnIframeDetach(const std::string& frame_id);
#endif #endif
private: private:
......
...@@ -112,7 +112,7 @@ const addSubmitButtonTouchEndHandler = function(form) { ...@@ -112,7 +112,7 @@ const addSubmitButtonTouchEndHandler = function(form) {
*/ */
const onSubmitButtonTouchEnd = function(evt) { const onSubmitButtonTouchEnd = function(evt) {
const form = evt.currentTarget.form; const form = evt.currentTarget.form;
const formData = __gCrWeb.passwords.getPasswordFormData(form); const formData = __gCrWeb.passwords.getPasswordFormData(form, window);
if (!formData) { if (!formData) {
return; return;
} }
...@@ -181,7 +181,7 @@ __gCrWeb.passwords['getPasswordFormDataAsString'] = function(identifier) { ...@@ -181,7 +181,7 @@ __gCrWeb.passwords['getPasswordFormDataAsString'] = function(identifier) {
if (!el) { if (!el) {
return '{}'; return '{}';
} }
const formData = __gCrWeb.passwords.getPasswordFormData(el); const formData = __gCrWeb.passwords.getPasswordFormData(el, window);
if (!formData) { if (!formData) {
return '{}'; return '{}';
} }
...@@ -341,7 +341,7 @@ const getPasswordFormDataList = function(formDataList, win) { ...@@ -341,7 +341,7 @@ const getPasswordFormDataList = function(formDataList, win) {
const doc = win.document; const doc = win.document;
const forms = doc.forms; const forms = doc.forms;
for (let i = 0; i < forms.length; i++) { for (let i = 0; i < forms.length; i++) {
const formData = __gCrWeb.passwords.getPasswordFormData(forms[i]); const formData = __gCrWeb.passwords.getPasswordFormData(forms[i], win);
if (formData) { if (formData) {
formDataList.push(formData); formDataList.push(formData);
addSubmitButtonTouchEndHandler(forms[i]); addSubmitButtonTouchEndHandler(forms[i]);
...@@ -384,13 +384,14 @@ function getPasswordFormDataFromUnownedElements_(formDataList, window) { ...@@ -384,13 +384,14 @@ function getPasswordFormDataFromUnownedElements_(formDataList, window) {
/** /**
* Returns a JS object containing the data from |formElement|. * Returns a JS object containing the data from |formElement|.
* @param {HTMLFormElement} formElement An HTML Form element. * @param {HTMLFormElement} formElement An HTML Form element.
* @param {Window} win A window or a frame containing formData.
* @return {Object} Object of data from formElement. * @return {Object} Object of data from formElement.
*/ */
__gCrWeb.passwords.getPasswordFormData = function(formElement) { __gCrWeb.passwords.getPasswordFormData = function(formElement, win) {
const extractMask = __gCrWeb.fill.EXTRACT_MASK_VALUE; const extractMask = __gCrWeb.fill.EXTRACT_MASK_VALUE;
const formData = {}; const formData = {};
const ok = __gCrWeb.fill.webFormElementToFormData( const ok = __gCrWeb.fill.webFormElementToFormData(
window, formElement, null /* formControlElement */, extractMask, formData, win, formElement, null /* formControlElement */, extractMask, formData,
null /* field */); null /* field */);
return ok ? formData : null; return ok ? formData : null;
}; };
......
...@@ -102,6 +102,8 @@ using password_manager::PasswordManager; ...@@ -102,6 +102,8 @@ using password_manager::PasswordManager;
using password_manager::PasswordManagerClient; using password_manager::PasswordManagerClient;
using password_manager::PasswordManagerDriver; using password_manager::PasswordManagerDriver;
using password_manager::SerializePasswordFormFillData; using password_manager::SerializePasswordFormFillData;
using web::WebFrame;
using web::WebState;
namespace { namespace {
// Types of password infobars to display. // Types of password infobars to display.
...@@ -184,7 +186,7 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -184,7 +186,7 @@ NSString* const kSuggestionSuffix = @" ••••••••";
// The WebState this instance is observing. Will be null after // The WebState this instance is observing. Will be null after
// -webStateDestroyed: has been called. // -webStateDestroyed: has been called.
web::WebState* _webState; WebState* _webState;
// Bridge to observe WebState from Objective-C. // Bridge to observe WebState from Objective-C.
std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge; std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
...@@ -206,12 +208,12 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -206,12 +208,12 @@ NSString* const kSuggestionSuffix = @" ••••••••";
NSString* _lastTypedValue; NSString* _lastTypedValue;
} }
- (instancetype)initWithWebState:(web::WebState*)webState { - (instancetype)initWithWebState:(WebState*)webState {
self = [self initWithWebState:webState client:nullptr]; self = [self initWithWebState:webState client:nullptr];
return self; return self;
} }
- (instancetype)initWithWebState:(web::WebState*)webState - (instancetype)initWithWebState:(WebState*)webState
client:(std::unique_ptr<PasswordManagerClient>) client:(std::unique_ptr<PasswordManagerClient>)
passwordManagerClient { passwordManagerClient {
self = [super init]; self = [super init];
...@@ -286,7 +288,7 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -286,7 +288,7 @@ NSString* const kSuggestionSuffix = @" ••••••••";
// If Tab was shown, and there is a pending PasswordForm, display autosign-in // If Tab was shown, and there is a pending PasswordForm, display autosign-in
// notification. // notification.
- (void)webStateWasShown:(web::WebState*)webState { - (void)webStateWasShown:(WebState*)webState {
DCHECK_EQ(_webState, webState); DCHECK_EQ(_webState, webState);
if (_pendingAutoSigninPasswordForm) { if (_pendingAutoSigninPasswordForm) {
[self showAutosigninNotification:std::move(_pendingAutoSigninPasswordForm)]; [self showAutosigninNotification:std::move(_pendingAutoSigninPasswordForm)];
...@@ -295,12 +297,12 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -295,12 +297,12 @@ NSString* const kSuggestionSuffix = @" ••••••••";
} }
// If Tab was hidden, hide auto sign-in notification. // If Tab was hidden, hide auto sign-in notification.
- (void)webStateWasHidden:(web::WebState*)webState { - (void)webStateWasHidden:(WebState*)webState {
DCHECK_EQ(_webState, webState); DCHECK_EQ(_webState, webState);
[self hideAutosigninNotification]; [self hideAutosigninNotification];
} }
- (void)webState:(web::WebState*)webState - (void)webState:(WebState*)webState
didFinishNavigation:(web::NavigationContext*)navigation { didFinishNavigation:(web::NavigationContext*)navigation {
DCHECK_EQ(_webState, webState); DCHECK_EQ(_webState, webState);
if (!navigation->HasCommitted() || navigation->IsSameDocument()) if (!navigation->HasCommitted() || navigation->IsSameDocument())
...@@ -318,7 +320,7 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -318,7 +320,7 @@ NSString* const kSuggestionSuffix = @" ••••••••";
/*form_may_be_submitted=*/navigation->IsRendererInitiated()); /*form_may_be_submitted=*/navigation->IsRendererInitiated());
} }
- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success { - (void)webState:(WebState*)webState didLoadPageWithSuccess:(BOOL)success {
DCHECK_EQ(_webState, webState); DCHECK_EQ(_webState, webState);
// Clear per-page state. // Clear per-page state.
[self.suggestionHelper resetForNewPage]; [self.suggestionHelper resetForNewPage];
...@@ -344,8 +346,8 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -344,8 +346,8 @@ NSString* const kSuggestionSuffix = @" ••••••••";
[self findPasswordFormsAndSendThemToPasswordStore]; [self findPasswordFormsAndSendThemToPasswordStore];
} }
- (void)webState:(web::WebState*)webState - (void)webState:(WebState*)webState
frameDidBecomeAvailable:(web::WebFrame*)web_frame { frameDidBecomeAvailable:(WebFrame*)web_frame {
DCHECK_EQ(_webState, webState); DCHECK_EQ(_webState, webState);
DCHECK(web_frame); DCHECK(web_frame);
UniqueIDTabHelper* uniqueIDTabHelper = UniqueIDTabHelper* uniqueIDTabHelper =
...@@ -355,7 +357,7 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -355,7 +357,7 @@ NSString* const kSuggestionSuffix = @" ••••••••";
[self.formHelper setUpForUniqueIDsWithInitialState:nextAvailableRendererID]; [self.formHelper setUpForUniqueIDsWithInitialState:nextAvailableRendererID];
} }
- (void)webStateDestroyed:(web::WebState*)webState { - (void)webStateDestroyed:(WebState*)webState {
DCHECK_EQ(_webState, webState); DCHECK_EQ(_webState, webState);
if (_webState) { if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get()); _webState->RemoveObserver(_webStateObserverBridge.get());
...@@ -373,6 +375,14 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -373,6 +375,14 @@ NSString* const kSuggestionSuffix = @" ••••••••";
_lastTypedValue = nil; _lastTypedValue = nil;
} }
// Track detaching iframes.
- (void)webState:(WebState*)webState
frameWillBecomeUnavailable:(WebFrame*)web_frame {
if (web_frame->IsMainFrame() || !web_frame->CanCallJavaScriptFunction())
return;
_passwordManager->OnIframeDetach(web_frame->GetFrameId());
}
#pragma mark - FormSuggestionProvider #pragma mark - FormSuggestionProvider
- (id<FormSuggestionProvider>)suggestionProvider { - (id<FormSuggestionProvider>)suggestionProvider {
...@@ -383,7 +393,7 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -383,7 +393,7 @@ NSString* const kSuggestionSuffix = @" ••••••••";
(FormSuggestionProviderQuery*)formQuery (FormSuggestionProviderQuery*)formQuery
isMainFrame:(BOOL)isMainFrame isMainFrame:(BOOL)isMainFrame
hasUserGesture:(BOOL)hasUserGesture hasUserGesture:(BOOL)hasUserGesture
webState:(web::WebState*)webState webState:(WebState*)webState
completionHandler: completionHandler:
(SuggestionsAvailableCompletion)completion { (SuggestionsAvailableCompletion)completion {
if (!GetPageURLAndCheckTrustLevel(webState, nullptr)) if (!GetPageURLAndCheckTrustLevel(webState, nullptr))
...@@ -432,7 +442,7 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -432,7 +442,7 @@ NSString* const kSuggestionSuffix = @" ••••••••";
} }
- (void)retrieveSuggestionsForForm:(FormSuggestionProviderQuery*)formQuery - (void)retrieveSuggestionsForForm:(FormSuggestionProviderQuery*)formQuery
webState:(web::WebState*)webState webState:(WebState*)webState
completionHandler:(SuggestionsReadyCompletion)completion { completionHandler:(SuggestionsReadyCompletion)completion {
if (!GetPageURLAndCheckTrustLevel(webState, nullptr)) if (!GetPageURLAndCheckTrustLevel(webState, nullptr))
return; return;
...@@ -548,7 +558,7 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -548,7 +558,7 @@ NSString* const kSuggestionSuffix = @" ••••••••";
#pragma mark - PasswordManagerClientDelegate #pragma mark - PasswordManagerClientDelegate
- (web::WebState*)webState { - (WebState*)webState {
return _webState; return _webState;
} }
...@@ -1023,9 +1033,9 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -1023,9 +1033,9 @@ NSString* const kSuggestionSuffix = @" ••••••••";
#pragma mark - FormActivityObserver #pragma mark - FormActivityObserver
- (void)webState:(web::WebState*)webState - (void)webState:(WebState*)webState
didRegisterFormActivity:(const autofill::FormActivityParams&)params didRegisterFormActivity:(const autofill::FormActivityParams&)params
inFrame:(web::WebFrame*)frame { inFrame:(WebFrame*)frame {
DCHECK_EQ(_webState, webState); DCHECK_EQ(_webState, webState);
if (!GetPageURLAndCheckTrustLevel(webState, nullptr)) if (!GetPageURLAndCheckTrustLevel(webState, nullptr))
......
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "components/password_manager/ios/js_password_manager.h" #import "components/password_manager/ios/js_password_manager.h"
#include "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/test/web_js_test.h" #import "ios/web/public/test/web_js_test.h"
#import "ios/web/public/test/web_test_with_web_state.h" #import "ios/web/public/test/web_test_with_web_state.h"
#import "ios/web/public/web_state.h"
#include "testing/gtest/include/gtest/gtest.h" #include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h" #include "testing/gtest_mac.h"
...@@ -14,6 +17,8 @@ ...@@ -14,6 +17,8 @@
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
using web::WebFrame;
// Unit tests for // Unit tests for
// components/password_manager/ios/resources/password_controller.js // components/password_manager/ios/resources/password_controller.js
namespace { namespace {
...@@ -158,11 +163,15 @@ TEST_F(PasswordControllerJsTest, ...@@ -158,11 +163,15 @@ TEST_F(PasswordControllerJsTest,
ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);"); ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);");
const std::string base_url = BaseUrl(); const std::string base_url = BaseUrl();
WebFrame* main_frame = web_state()->GetWebFramesManager()->GetMainWebFrame();
std::string mainFrameID = main_frame->GetFrameId();
NSString* result = [NSString NSString* result = [NSString
stringWithFormat: stringWithFormat:
@"[{\"name\":\"login_form\",\"origin\":\"%s\",\"action\":\"https://" @"[{\"name\":\"login_form\",\"origin\":\"%s\",\"action\":\"https://"
@"chromium.test/generic_submit\",\"name_attribute\":\"login_form\"," @"chromium.test/generic_submit\",\"name_attribute\":\"login_form\","
@"\"id_attribute\":\"\",\"unique_renderer_id\":\"0\",\"fields\":[{" @"\"id_attribute\":\"\",\"unique_renderer_id\":\"0\","
@"\"frame_id\":\"%s\","
@"\"fields\":[{"
@"\"identifier\":\"name\"," @"\"identifier\":\"name\","
@"\"name\":\"name\",\"name_attribute\":\"name\",\"id_attribute\":" @"\"name\":\"name\",\"name_attribute\":\"name\",\"id_attribute\":"
@"\"\",\"unique_renderer_id\":\"1\",\"form_control_type\":\"text\"," @"\"\",\"unique_renderer_id\":\"1\",\"form_control_type\":\"text\","
...@@ -177,7 +186,7 @@ TEST_F(PasswordControllerJsTest, ...@@ -177,7 +186,7 @@ TEST_F(PasswordControllerJsTest,
@"\"should_autocomplete\":true,\"is_focusable\":true," @"\"should_autocomplete\":true,\"is_focusable\":true,"
@"\"max_length\":524288,\"is_checkable\":false,\"value\":\"\"," @"\"max_length\":524288,\"is_checkable\":false,\"value\":\"\","
@"\"label\":\"Password:\"}]}]", @"\"label\":\"Password:\"}]}]",
base_url.c_str()]; base_url.c_str(), mainFrameID.c_str()];
EXPECT_NSEQ(result, ExecuteJavaScriptWithFormat( EXPECT_NSEQ(result, ExecuteJavaScriptWithFormat(
@"__gCrWeb.passwords.findPasswordForms()")); @"__gCrWeb.passwords.findPasswordForms()"));
} }
...@@ -201,11 +210,15 @@ TEST_F(PasswordControllerJsTest, ...@@ -201,11 +210,15 @@ TEST_F(PasswordControllerJsTest,
ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);"); ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);");
const std::string base_url = BaseUrl(); const std::string base_url = BaseUrl();
WebFrame* main_frame = web_state()->GetWebFramesManager()->GetMainWebFrame();
std::string mainFrameID = main_frame->GetFrameId();
NSString* result = [NSString NSString* result = [NSString
stringWithFormat: stringWithFormat:
@"[{\"name\":\"login_form1\",\"origin\":\"%s\",\"action\":\"%s" @"[{\"name\":\"login_form1\",\"origin\":\"%s\",\"action\":\"%s"
@"generic_submit\",\"name_attribute\":\"\",\"id_attribute\":" @"generic_submit\",\"name_attribute\":\"\",\"id_attribute\":"
@"\"login_form1\",\"unique_renderer_id\":\"0\",\"fields\":[{" @"\"login_form1\",\"unique_renderer_id\":\"0\","
@"\"frame_id\":\"%s\","
@"\"fields\":[{"
@"\"identifier\":\"name\"," @"\"identifier\":\"name\","
@"\"name\":\"name\",\"name_attribute\":\"name\",\"id_attribute\":" @"\"name\":\"name\",\"name_attribute\":\"name\",\"id_attribute\":"
@"\"\",\"unique_renderer_id\":\"1\",\"form_control_type\":\"text\"," @"\"\",\"unique_renderer_id\":\"1\",\"form_control_type\":\"text\","
...@@ -222,7 +235,9 @@ TEST_F(PasswordControllerJsTest, ...@@ -222,7 +235,9 @@ TEST_F(PasswordControllerJsTest,
@"\"label\":\"Password:\"}]},{\"name\":\"login_form2\",\"origin\":" @"\"label\":\"Password:\"}]},{\"name\":\"login_form2\",\"origin\":"
@"\"https://chromium.test/\",\"action\":\"https://chromium.test/" @"\"https://chromium.test/\",\"action\":\"https://chromium.test/"
@"generic_s2\",\"name_attribute\":\"login_form2\"," @"generic_s2\",\"name_attribute\":\"login_form2\","
@"\"id_attribute\":\"\",\"unique_renderer_id\":\"3\",\"fields\":[{" @"\"id_attribute\":\"\",\"unique_renderer_id\":\"3\","
@"\"frame_id\":\"%s\","
@"\"fields\":[{"
@"\"identifier\":\"name2\"," @"\"identifier\":\"name2\","
@"\"name\":\"name2\",\"name_attribute\":\"name2\",\"id_attribute\":" @"\"name\":\"name2\",\"name_attribute\":\"name2\",\"id_attribute\":"
@"\"\",\"unique_renderer_id\":\"4\",\"form_control_type\":\"text\"," @"\"\",\"unique_renderer_id\":\"4\",\"form_control_type\":\"text\","
...@@ -238,7 +253,8 @@ TEST_F(PasswordControllerJsTest, ...@@ -238,7 +253,8 @@ TEST_F(PasswordControllerJsTest,
@"\"max_length\":524288,\"is_checkable\":false," @"\"max_length\":524288,\"is_checkable\":false,"
@"\"value\":\"\"," @"\"value\":\"\","
@"\"label\":\"Password:\"}]}]", @"\"label\":\"Password:\"}]}]",
base_url.c_str(), base_url.c_str()]; base_url.c_str(), base_url.c_str(), mainFrameID.c_str(),
mainFrameID.c_str()];
EXPECT_NSEQ(result, ExecuteJavaScriptWithFormat( EXPECT_NSEQ(result, ExecuteJavaScriptWithFormat(
@"__gCrWeb.passwords.findPasswordForms()")); @"__gCrWeb.passwords.findPasswordForms()"));
...@@ -259,11 +275,14 @@ TEST_F(PasswordControllerJsTest, GetPasswordFormData) { ...@@ -259,11 +275,14 @@ TEST_F(PasswordControllerJsTest, GetPasswordFormData) {
const std::string base_url = BaseUrl(); const std::string base_url = BaseUrl();
NSString* parameter = @"window.document.getElementsByTagName('form')[0]"; NSString* parameter = @"window.document.getElementsByTagName('form')[0]";
WebFrame* main_frame = web_state()->GetWebFramesManager()->GetMainWebFrame();
std::string mainFrameID = main_frame->GetFrameId();
NSString* result = [NSString NSString* result = [NSString
stringWithFormat: stringWithFormat:
@"{\"name\":\"np\",\"origin\":\"%s\",\"action\":\"%sgeneric_submit\"," @"{\"name\":\"np\",\"origin\":\"%s\",\"action\":\"%sgeneric_submit\","
@"\"name_attribute\":\"np\",\"id_attribute\":\"np1\",\"unique_" @"\"name_attribute\":\"np\",\"id_attribute\":\"np1\",\"unique_"
@"renderer_id\":\"0\"," @"renderer_id\":\"0\","
@"\"frame_id\":\"%s\","
@"\"fields\":[{\"identifier\":\"name\",\"name\":\"name\"," @"\"fields\":[{\"identifier\":\"name\",\"name\":\"name\","
@"\"name_attribute\":\"name\",\"id_attribute\":\"\",\"unique_" @"\"name_attribute\":\"name\",\"id_attribute\":\"\",\"unique_"
@"renderer_id\":\"1\",\"form_" @"renderer_id\":\"1\",\"form_"
...@@ -278,13 +297,12 @@ TEST_F(PasswordControllerJsTest, GetPasswordFormData) { ...@@ -278,13 +297,12 @@ TEST_F(PasswordControllerJsTest, GetPasswordFormData) {
@"\"should_autocomplete\":true,\"is_focusable\":true," @"\"should_autocomplete\":true,\"is_focusable\":true,"
@"\"max_length\":524288," @"\"max_length\":524288,"
@"\"is_checkable\":false,\"value\":\"\",\"label\":\"Password:\"}]}", @"\"is_checkable\":false,\"value\":\"\",\"label\":\"Password:\"}]}",
base_url.c_str(), base_url.c_str()]; base_url.c_str(), base_url.c_str(), mainFrameID.c_str()];
EXPECT_NSEQ( EXPECT_NSEQ(result, ExecuteJavaScriptWithFormat(
result, @"__gCrWeb.stringify(__gCrWeb.passwords."
ExecuteJavaScriptWithFormat( @"getPasswordFormData(%@, window))",
@"__gCrWeb.stringify(__gCrWeb.passwords.getPasswordFormData(%@))", parameter));
parameter));
} }
// Check that if a form action is not set then the action is parsed to the // Check that if a form action is not set then the action is parsed to the
...@@ -301,11 +319,14 @@ TEST_F(PasswordControllerJsTest, FormActionIsNotSet) { ...@@ -301,11 +319,14 @@ TEST_F(PasswordControllerJsTest, FormActionIsNotSet) {
ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);"); ExecuteJavaScript(@"__gCrWeb.fill.setUpForUniqueIDs(0);");
const std::string base_url = BaseUrl(); const std::string base_url = BaseUrl();
WebFrame* main_frame = web_state()->GetWebFramesManager()->GetMainWebFrame();
std::string mainFrameID = main_frame->GetFrameId();
NSString* result = [NSString NSString* result = [NSString
stringWithFormat: stringWithFormat:
@"[{\"name\":\"login_form\",\"origin\":\"%s\",\"action\":\"%s\"," @"[{\"name\":\"login_form\",\"origin\":\"%s\",\"action\":\"%s\","
@"\"name_attribute\":\"login_form\",\"id_attribute\":\"\",\"unique_" @"\"name_attribute\":\"login_form\",\"id_attribute\":\"\",\"unique_"
@"renderer_id\":\"0\"," @"renderer_id\":\"0\","
@"\"frame_id\":\"%s\","
@"\"fields\":[{\"identifier\":\"name\",\"name\":\"name\"," @"\"fields\":[{\"identifier\":\"name\",\"name\":\"name\","
@"\"name_attribute\":\"name\",\"id_attribute\":\"\",\"unique_" @"\"name_attribute\":\"name\",\"id_attribute\":\"\",\"unique_"
@"renderer_id\":\"1\",\"form_" @"renderer_id\":\"1\",\"form_"
...@@ -320,7 +341,7 @@ TEST_F(PasswordControllerJsTest, FormActionIsNotSet) { ...@@ -320,7 +341,7 @@ TEST_F(PasswordControllerJsTest, FormActionIsNotSet) {
@"\"should_autocomplete\":true,\"is_focusable\":true," @"\"should_autocomplete\":true,\"is_focusable\":true,"
@"\"max_length\":524288," @"\"max_length\":524288,"
@"\"is_checkable\":false,\"value\":\"\",\"label\":\"Password:\"}]}]", @"\"is_checkable\":false,\"value\":\"\",\"label\":\"Password:\"}]}]",
base_url.c_str(), base_url.c_str()]; base_url.c_str(), base_url.c_str(), mainFrameID.c_str()];
EXPECT_NSEQ(result, ExecuteJavaScriptWithFormat( EXPECT_NSEQ(result, ExecuteJavaScriptWithFormat(
@"__gCrWeb.passwords.findPasswordForms()")); @"__gCrWeb.passwords.findPasswordForms()"));
} }
...@@ -362,11 +383,14 @@ TEST_F(PasswordControllerJsTest, TouchendAsSubmissionIndicator) { ...@@ -362,11 +383,14 @@ TEST_F(PasswordControllerJsTest, TouchendAsSubmissionIndicator) {
// Check that there was only 1 call for invokeOnHost. // Check that there was only 1 call for invokeOnHost.
EXPECT_NSEQ(@1, ExecuteJavaScriptWithFormat(@"invokeOnHostCalls")); EXPECT_NSEQ(@1, ExecuteJavaScriptWithFormat(@"invokeOnHostCalls"));
WebFrame* main_frame = web_state()->GetWebFramesManager()->GetMainWebFrame();
std::string mainFrameID = main_frame->GetFrameId();
NSString* expected_command = [NSString NSString* expected_command = [NSString
stringWithFormat: stringWithFormat:
@"{\"name\":\"login_form\",\"origin\":\"https://chromium.test/" @"{\"name\":\"login_form\",\"origin\":\"https://chromium.test/"
@"\",\"action\":\"%s\",\"name_attribute\":\"login_form\"," @"\",\"action\":\"%s\",\"name_attribute\":\"login_form\","
@"\"id_attribute\":\"login_form\",\"unique_renderer_id\":\"0\"," @"\"id_attribute\":\"login_form\",\"unique_renderer_id\":\"0\","
@"\"frame_id\":\"%s\","
@"\"fields\":" @"\"fields\":"
@"[{\"identifier\":\"username\"," @"[{\"identifier\":\"username\","
@"\"name\":\"username\",\"name_attribute\":\"username\"," @"\"name\":\"username\",\"name_attribute\":\"username\","
...@@ -385,7 +409,7 @@ TEST_F(PasswordControllerJsTest, TouchendAsSubmissionIndicator) { ...@@ -385,7 +409,7 @@ TEST_F(PasswordControllerJsTest, TouchendAsSubmissionIndicator) {
@"\"is_focusable\":true,\"max_length\":524288,\"is_checkable\":false," @"\"is_focusable\":true,\"max_length\":524288,\"is_checkable\":false,"
@"\"value\":\"password1\",\"label\":\"Password:\"}]," @"\"value\":\"password1\",\"label\":\"Password:\"}],"
@"\"command\":\"passwordForm.submitButtonClick\"}", @"\"command\":\"passwordForm.submitButtonClick\"}",
BaseUrl().c_str()]; BaseUrl().c_str(), mainFrameID.c_str()];
// Check that invokeOnHost was called with the correct argument. // Check that invokeOnHost was called with the correct argument.
EXPECT_NSEQ( EXPECT_NSEQ(
......
...@@ -84,6 +84,7 @@ using base::test::ios::kWaitForJSCompletionTimeout; ...@@ -84,6 +84,7 @@ using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::WaitUntilConditionOrTimeout; using base::test::ios::WaitUntilConditionOrTimeout;
using testing::WithArg; using testing::WithArg;
using testing::_; using testing::_;
using web::WebFrame;
namespace { namespace {
...@@ -1890,7 +1891,7 @@ TEST_F(PasswordControllerTest, FindDynamicallyAddedForm2) { ...@@ -1890,7 +1891,7 @@ TEST_F(PasswordControllerTest, FindDynamicallyAddedForm2) {
ExecuteJavaScript(kAddFormDynamicallyScript); ExecuteJavaScript(kAddFormDynamicallyScript);
std::string mainFrameID = web::GetMainWebFrameId(web_state()); std::string mainFrameID = web::GetMainWebFrameId(web_state());
web::WebFrame* frame = web::GetWebFrameWithId(web_state(), mainFrameID); WebFrame* frame = web::GetWebFrameWithId(web_state(), mainFrameID);
autofill::FormActivityParams params; autofill::FormActivityParams params;
params.type = "form_changed"; params.type = "form_changed";
params.frame_id = mainFrameID; params.frame_id = mainFrameID;
...@@ -1926,7 +1927,7 @@ TEST_F(PasswordControllerTest, DetectSubmissionOnRemovedForm) { ...@@ -1926,7 +1927,7 @@ TEST_F(PasswordControllerTest, DetectSubmissionOnRemovedForm) {
EXPECT_CALL(*weak_client_, PromptUserToSaveOrUpdatePasswordPtr) EXPECT_CALL(*weak_client_, PromptUserToSaveOrUpdatePasswordPtr)
.WillOnce(WithArg<0>(SaveToScopedPtr(&form_manager_to_save))); .WillOnce(WithArg<0>(SaveToScopedPtr(&form_manager_to_save)));
web::WebFrame* frame = web::GetWebFrameWithId(web_state(), mainFrameID); WebFrame* frame = web::GetWebFrameWithId(web_state(), mainFrameID);
autofill::FormActivityParams params; autofill::FormActivityParams params;
params.type = "password_form_removed"; params.type = "password_form_removed";
params.unique_form_id = 0; params.unique_form_id = 0;
...@@ -1960,7 +1961,7 @@ TEST_F(PasswordControllerTest, ...@@ -1960,7 +1961,7 @@ TEST_F(PasswordControllerTest,
EXPECT_CALL(*weak_client_, PromptUserToSaveOrUpdatePasswordPtr).Times(0); EXPECT_CALL(*weak_client_, PromptUserToSaveOrUpdatePasswordPtr).Times(0);
std::string mainFrameID = web::GetMainWebFrameId(web_state()); std::string mainFrameID = web::GetMainWebFrameId(web_state());
web::WebFrame* frame = web::GetWebFrameWithId(web_state(), mainFrameID); WebFrame* frame = web::GetWebFrameWithId(web_state(), mainFrameID);
autofill::FormActivityParams params; autofill::FormActivityParams params;
params.type = "password_form_removed"; params.type = "password_form_removed";
params.unique_form_id = 0; params.unique_form_id = 0;
...@@ -1970,3 +1971,110 @@ TEST_F(PasswordControllerTest, ...@@ -1970,3 +1971,110 @@ TEST_F(PasswordControllerTest,
didRegisterFormActivity:params didRegisterFormActivity:params
inFrame:frame]; inFrame:frame];
} }
// Tests that submission is detected on removal of the form that had user input.
TEST_F(PasswordControllerTest, DetectSubmissionOnIFrameDetach) {
ON_CALL(*store_, GetLogins)
.WillByDefault(WithArg<1>(InvokeEmptyConsumerWithForms()));
EXPECT_TRUE(
LoadHtml("<script>"
" function FillFrame() {"
" var doc = frames['frame1'].document.open();"
" doc.write('<form id=\"form1\">');"
" doc.write('<input id=\"un\" type=\"text\">');"
" doc.write('<input id=\"pw\" type=\"password\">');"
" doc.write('</form>');"
" doc.close();"
// This event listerer is set by Chrome, but it gets disabled
// by document.write(). This is quite uncommon way to add
// content to an iframe, but it is the only way for this test.
// Reattaching it manually for test purposes.
" frames[0].addEventListener('unload', function(event) {"
" __gCrWeb.common.sendWebKitMessage('FrameBecameUnavailable',"
" frames[0].__gCrWeb.message.getFrameId());"
"});"
"}"
"</script>"
"<body onload='FillFrame()'>"
"<iframe id='frame1' name='frame1'></iframe>"
"</body>"));
std::string mainFrameID = web::GetMainWebFrameId(web_state());
std::set<WebFrame*> all_frames =
web_state()->GetWebFramesManager()->GetAllWebFrames();
std::string iFrameID;
for (auto* frame : all_frames) {
if (!frame->IsMainFrame()) {
iFrameID = frame->GetFrameId();
break;
}
}
SimulateUserTyping("form1", FormRendererId(0), "un", FieldRendererId(1),
"user1", iFrameID);
SimulateUserTyping("form1", FormRendererId(0), "pw", FieldRendererId(2),
"password1", iFrameID);
std::unique_ptr<PasswordFormManagerForUI> form_manager_to_save;
EXPECT_CALL(*weak_client_, PromptUserToSaveOrUpdatePasswordPtr)
.WillOnce(WithArg<0>(SaveToScopedPtr(&form_manager_to_save)));
ExecuteJavaScript(@"var frame1 = document.getElementById('frame1');"
"frame1.parentNode.removeChild(frame1);");
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool() {
auto frames = web_state()->GetWebFramesManager()->GetAllWebFrames();
return frames.size() == 1;
}));
ASSERT_TRUE(form_manager_to_save);
EXPECT_EQ("https://chromium.test/",
form_manager_to_save->GetPendingCredentials().signon_realm);
EXPECT_EQ(ASCIIToUTF16("user1"),
form_manager_to_save->GetPendingCredentials().username_value);
EXPECT_EQ(ASCIIToUTF16("password1"),
form_manager_to_save->GetPendingCredentials().password_value);
auto* form_manager =
static_cast<PasswordFormManager*>(form_manager_to_save.get());
EXPECT_TRUE(form_manager->is_submitted());
EXPECT_FALSE(form_manager->IsPasswordUpdate());
}
// Tests that no submission is detected on removal of the form that had no user
// input.
TEST_F(PasswordControllerTest,
DetectNoSubmissionOnIFrameDetachWithoutUserInput) {
ON_CALL(*store_, GetLogins)
.WillByDefault(WithArg<1>(InvokeEmptyConsumerWithForms()));
EXPECT_TRUE(
LoadHtml("<script>"
" function FillFrame() {"
" var doc = frames['frame1'].document.open();"
" doc.write('<form id=\"form1\">');"
" doc.write('<input id=\"un\" type=\"text\">');"
" doc.write('<input id=\"pw\" type=\"password\">');"
" doc.write('</form>');"
" doc.close();"
// This event listerer is set by Chrome, but it gets disabled
// by document.write(). This is quite uncommon way to add
// content to an iframe, but it is the only way for this test.
// Reattaching it manually for test purposes.
" frames[0].addEventListener('unload', function(event) {"
" __gCrWeb.common.sendWebKitMessage('FrameBecameUnavailable',"
" frames[0].__gCrWeb.message.getFrameId());"
"});"
"}"
"</script>"
"<body onload='FillFrame()'>"
"<iframe id='frame1' name='frame1'></iframe>"
"</body>"));
EXPECT_CALL(*weak_client_, PromptUserToSaveOrUpdatePasswordPtr).Times(0);
ExecuteJavaScript(@"var frame1 = document.getElementById('frame1');"
"frame1.parentNode.removeChild(frame1);");
ASSERT_TRUE(WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^bool() {
auto frames = web_state()->GetWebFramesManager()->GetAllWebFrames();
return frames.size() == 1;
}));
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment