Commit f22818c6 authored by Vadym Doroshenko's avatar Vadym Doroshenko Committed by Commit Bot

Propagate didFinishNavigation to the PasswordManager.

This CL makes that Chrome Password Manager on iOS will detect some form
submissions that are done with JavaScript w/o html form submission.

Bug: 1042196
Change-Id: I53c0b7090d843e963259944f629ba7fee6d570d4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2047026
Commit-Queue: Vadym Doroshenko <dvadym@chromium.org>
Reviewed-by: default avatarVasilii Sukhanov <vasilii@chromium.org>
Cr-Commit-Position: refs/heads/master@{#744717}
parent c07a64a5
...@@ -65,6 +65,7 @@ ...@@ -65,6 +65,7 @@
#import "ios/web/public/deprecated/crw_js_injection_receiver.h" #import "ios/web/public/deprecated/crw_js_injection_receiver.h"
#include "ios/web/public/js_messaging/web_frame.h" #include "ios/web/public/js_messaging/web_frame.h"
#include "ios/web/public/js_messaging/web_frame_util.h" #include "ios/web/public/js_messaging/web_frame_util.h"
#include "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/web_state.h" #import "ios/web/public/web_state.h"
#include "services/network/public/cpp/shared_url_loader_factory.h" #include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/base/l10n/l10n_util_mac.h" #include "ui/base/l10n/l10n_util_mac.h"
...@@ -284,6 +285,24 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -284,6 +285,24 @@ NSString* const kSuggestionSuffix = @" ••••••••";
[self hideAutosigninNotification]; [self hideAutosigninNotification];
} }
- (void)webState:(web::WebState*)webState
didFinishNavigation:(web::NavigationContext*)navigation {
DCHECK_EQ(_webState, webState);
if (!navigation->HasCommitted() || navigation->IsSameDocument())
return;
if (!GetPageURLAndCheckTrustLevel(webState, nullptr))
return;
// On non-iOS platforms navigations initiated by link click are excluded from
// navigations which might be form submssions. On iOS there is no easy way to
// check that the navigation is link initiated, so it is skipped. It should
// not be so important since it is unlikely that the user clicks on a link
// after filling password form w/o submitting it.
self.passwordManager->DidNavigateMainFrame(
/*form_may_be_submitted=*/navigation->IsRendererInitiated());
}
- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success { - (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success {
DCHECK_EQ(_webState, webState); DCHECK_EQ(_webState, webState);
// Clear per-page state. // Clear per-page state.
...@@ -298,10 +317,6 @@ NSString* const kSuggestionSuffix = @" ••••••••"; ...@@ -298,10 +317,6 @@ NSString* const kSuggestionSuffix = @" ••••••••";
if (!web::UrlHasWebScheme(pageURL)) if (!web::UrlHasWebScheme(pageURL))
return; return;
// Notify the password manager that the page loaded so it can clear its own
// per-page state.
self.passwordManager->DidNavigateMainFrame(/*form_may_be_submitted=*/true);
if (!webState->ContentIsHTML()) { if (!webState->ContentIsHTML()) {
// If the current page is not HTML, it does not contain any HTML forms. // If the current page is not HTML, it does not contain any HTML forms.
[self didFinishPasswordFormExtraction:std::vector<autofill::FormData>()]; [self didFinishPasswordFormExtraction:std::vector<autofill::FormData>()];
......
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
#import "ios/web/public/js_messaging/web_frames_manager.h" #import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/navigation/navigation_item.h" #import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h" #import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/fakes/test_web_state.h" #import "ios/web/public/test/fakes/test_web_state.h"
#import "ios/web/public/test/web_js_test.h" #import "ios/web/public/test/web_js_test.h"
#include "testing/gmock/include/gmock/gmock.h" #include "testing/gmock/include/gmock/gmock.h"
...@@ -61,6 +62,7 @@ using autofill::FormData; ...@@ -61,6 +62,7 @@ using autofill::FormData;
using autofill::FormFieldData; using autofill::FormFieldData;
using autofill::PasswordForm; using autofill::PasswordForm;
using autofill::PasswordFormFillData; using autofill::PasswordFormFillData;
using base::SysUTF8ToNSString;
using password_manager::PasswordFormManagerForUI; using password_manager::PasswordFormManagerForUI;
using password_manager::PasswordFormManager; using password_manager::PasswordFormManager;
using password_manager::PasswordStoreConsumer; using password_manager::PasswordStoreConsumer;
...@@ -330,6 +332,43 @@ class PasswordControllerTest : public ChromeWebTest { ...@@ -330,6 +332,43 @@ class PasswordControllerTest : public ChromeWebTest {
completionHandler:[OCMArg any]]; completionHandler:[OCMArg any]];
} }
void SimulateUserTyping(const std::string& form_name,
const std::string& field_identifier,
const std::string& typed_value,
const std::string& main_frame_id) {
__block BOOL completion_handler_called = NO;
[passwordController_
checkIfSuggestionsAvailableForForm:SysUTF8ToNSString(form_name)
fieldIdentifier:SysUTF8ToNSString(field_identifier)
fieldType:@"not_important"
type:@"text"
typedValue:SysUTF8ToNSString(typed_value)
frameID:SysUTF8ToNSString(main_frame_id)
isMainFrame:YES
hasUserGesture:YES
webState:web_state()
completionHandler:^(BOOL success) {
completion_handler_called = YES;
}];
// Wait until the expected handler is called.
EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool() {
return completion_handler_called;
}));
}
void LoadHtmlWithRendererInitiatedNavigation(NSString* html,
GURL gurl = GURL()) {
web::FakeNavigationContext context;
context.SetHasCommitted(true);
context.SetIsSameDocument(false);
context.SetIsRendererInitiated(true);
[passwordController_ webState:web_state() didFinishNavigation:&context];
if (gurl.is_empty())
LoadHtml(html);
else
LoadHtml(html, gurl);
}
// SuggestionController for testing. // SuggestionController for testing.
PasswordsTestSuggestionController* suggestionController_; PasswordsTestSuggestionController* suggestionController_;
...@@ -1525,7 +1564,8 @@ TEST_F(PasswordControllerTest, ShowingSavingPromptOnSuccessfulSubmission) { ...@@ -1525,7 +1564,8 @@ TEST_F(PasswordControllerTest, ShowingSavingPromptOnSuccessfulSubmission) {
@"document.getElementsByName('username')[0].value = 'user1';" @"document.getElementsByName('username')[0].value = 'user1';"
"document.getElementsByName('password')[0].value = 'password1';" "document.getElementsByName('password')[0].value = 'password1';"
"document.getElementById('submit_button').click();"); "document.getElementById('submit_button').click();");
LoadHtml(SysUTF8ToNSString("<html><body>Success</body></html>")); LoadHtmlWithRendererInitiatedNavigation(
SysUTF8ToNSString("<html><body>Success</body></html>"));
EXPECT_EQ("https://chromium.test/", EXPECT_EQ("https://chromium.test/",
form_manager_to_save->GetPendingCredentials().signon_realm); form_manager_to_save->GetPendingCredentials().signon_realm);
EXPECT_EQ(ASCIIToUTF16("user1"), EXPECT_EQ(ASCIIToUTF16("user1"),
...@@ -1558,7 +1598,8 @@ TEST_F(PasswordControllerTest, NotShowingSavingPromptWithoutSubmission) { ...@@ -1558,7 +1598,8 @@ TEST_F(PasswordControllerTest, NotShowingSavingPromptWithoutSubmission) {
ExecuteJavaScript( ExecuteJavaScript(
@"document.getElementsByName('username')[0].value = 'user1';" @"document.getElementsByName('username')[0].value = 'user1';"
"document.getElementsByName('password')[0].value = 'password1';"); "document.getElementsByName('password')[0].value = 'password1';");
LoadHtml(SysUTF8ToNSString("<html><body>New page</body></html>")); LoadHtmlWithRendererInitiatedNavigation(
SysUTF8ToNSString("<html><body>New page</body></html>"));
} }
// Tests that the user is not prompted to save or update password on a // Tests that the user is not prompted to save or update password on a
...@@ -1583,7 +1624,8 @@ TEST_F(PasswordControllerTest, NotShowingSavingPromptWhileSavingIsDisabled) { ...@@ -1583,7 +1624,8 @@ TEST_F(PasswordControllerTest, NotShowingSavingPromptWhileSavingIsDisabled) {
@"document.getElementsByName('username')[0].value = 'user1';" @"document.getElementsByName('username')[0].value = 'user1';"
"document.getElementsByName('password')[0].value = 'password1';" "document.getElementsByName('password')[0].value = 'password1';"
"document.getElementById('submit_button').click();"); "document.getElementById('submit_button').click();");
LoadHtml(SysUTF8ToNSString("<html><body>Success</body></html>")); LoadHtmlWithRendererInitiatedNavigation(
SysUTF8ToNSString("<html><body>Success</body></html>"));
} }
// Tests that the user is prompted to update password on a succesful // Tests that the user is prompted to update password on a succesful
...@@ -1591,9 +1633,7 @@ TEST_F(PasswordControllerTest, NotShowingSavingPromptWhileSavingIsDisabled) { ...@@ -1591,9 +1633,7 @@ TEST_F(PasswordControllerTest, NotShowingSavingPromptWhileSavingIsDisabled) {
// username in the store. // username in the store.
TEST_F(PasswordControllerTest, ShowingUpdatePromptOnSuccessfulSubmission) { TEST_F(PasswordControllerTest, ShowingUpdatePromptOnSuccessfulSubmission) {
PasswordForm form(MakeSimpleForm()); PasswordForm form(MakeSimpleForm());
ON_CALL(*store_, GetLogins(_, _)) ON_CALL(*store_, GetLogins).WillByDefault(WithArg<1>(InvokeConsumer(form)));
.WillByDefault(WithArg<1>(InvokeConsumer(form)));
const char* kHtml = {"<html><body>" const char* kHtml = {"<html><body>"
"<form name='login_form' id='login_form'>" "<form name='login_form' id='login_form'>"
" <input type='text' name='Username'>" " <input type='text' name='Username'>"
...@@ -1611,8 +1651,9 @@ TEST_F(PasswordControllerTest, ShowingUpdatePromptOnSuccessfulSubmission) { ...@@ -1611,8 +1651,9 @@ TEST_F(PasswordControllerTest, ShowingUpdatePromptOnSuccessfulSubmission) {
@"document.getElementsByName('Username')[0].value = 'googleuser';" @"document.getElementsByName('Username')[0].value = 'googleuser';"
"document.getElementsByName('Passwd')[0].value = 'new_password';" "document.getElementsByName('Passwd')[0].value = 'new_password';"
"document.getElementById('submit_button').click();"); "document.getElementById('submit_button').click();");
LoadHtml(SysUTF8ToNSString("<html><body>Success</body></html>"), LoadHtmlWithRendererInitiatedNavigation(
GURL("http://www.google.com/a/Login")); SysUTF8ToNSString("<html><body>Success</body></html>"),
GURL("http://www.google.com/a/Login"));
EXPECT_EQ("http://www.google.com/", EXPECT_EQ("http://www.google.com/",
form_manager_to_save->GetPendingCredentials().signon_realm); form_manager_to_save->GetPendingCredentials().signon_realm);
EXPECT_EQ(ASCIIToUTF16("googleuser"), EXPECT_EQ(ASCIIToUTF16("googleuser"),
...@@ -1625,3 +1666,92 @@ TEST_F(PasswordControllerTest, ShowingUpdatePromptOnSuccessfulSubmission) { ...@@ -1625,3 +1666,92 @@ TEST_F(PasswordControllerTest, ShowingUpdatePromptOnSuccessfulSubmission) {
EXPECT_TRUE(form_manager->is_submitted()); EXPECT_TRUE(form_manager->is_submitted());
EXPECT_TRUE(form_manager->IsPasswordUpdate()); EXPECT_TRUE(form_manager->IsPasswordUpdate());
} }
TEST_F(PasswordControllerTest, SavingOnNavigateMainFrame) {
constexpr char kHtml[] = "<html><body>"
"<form name='login_form' id='login_form'>"
" <input type='text' name='username'>"
" <input type='password' name='pw'>"
"</form>"
"</body></html>";
ON_CALL(*store_, GetLogins)
.WillByDefault(WithArg<1>(InvokeEmptyConsumerWithForms()));
for (bool has_commited : {false, true}) {
for (bool is_same_document : {false, true}) {
for (bool is_renderer_initiated : {false, true}) {
SCOPED_TRACE(testing::Message("has_commited = ")
<< has_commited << " is_same_document=" << is_same_document
<< " is_renderer_initiated=" << is_renderer_initiated);
LoadHtml(SysUTF8ToNSString(kHtml));
std::string main_frame_id = web::GetMainWebFrameId(web_state());
SimulateUserTyping("login_form", "username", "user1", main_frame_id);
SimulateUserTyping("login_form", "pw", "password1", main_frame_id);
bool prompt_should_be_shown =
has_commited && !is_same_document && is_renderer_initiated;
std::unique_ptr<PasswordFormManagerForUI> form_manager;
if (prompt_should_be_shown) {
EXPECT_CALL(*weak_client_, PromptUserToSaveOrUpdatePasswordPtr)
.WillOnce(WithArg<0>(SaveToScopedPtr(&form_manager)));
} else {
EXPECT_CALL(*weak_client_, PromptUserToSaveOrUpdatePasswordPtr)
.Times(0);
}
web::FakeNavigationContext context;
context.SetHasCommitted(has_commited);
context.SetIsSameDocument(is_same_document);
context.SetIsRendererInitiated(is_renderer_initiated);
[passwordController_ webState:web_state() didFinishNavigation:&context];
// Simulate a successful submission by loading the landing page without
// a form.
LoadHtml(
SysUTF8ToNSString("<html><body>Login success page</body></html>"));
if (prompt_should_be_shown) {
ASSERT_TRUE(form_manager);
EXPECT_EQ(ASCIIToUTF16("user1"),
form_manager->GetPendingCredentials().username_value);
EXPECT_EQ(ASCIIToUTF16("password1"),
form_manager->GetPendingCredentials().password_value);
}
testing::Mock::VerifyAndClearExpectations(weak_client_);
}
}
}
}
TEST_F(PasswordControllerTest, NoSavingOnNavigateMainFrameFailedSubmission) {
constexpr char kHtml[] = "<html><body>"
"<form name='login_form' id='login_form'>"
" <input type='text' name='username'>"
" <input type='password' name='pw'>"
"</form>"
"</body></html>";
ON_CALL(*store_, GetLogins)
.WillByDefault(WithArg<1>(InvokeEmptyConsumerWithForms()));
LoadHtml(SysUTF8ToNSString(kHtml));
std::string main_frame_id = web::GetMainWebFrameId(web_state());
SimulateUserTyping("login_form", "username", "user1", main_frame_id);
SimulateUserTyping("login_form", "pw", "password1", main_frame_id);
EXPECT_CALL(*weak_client_, PromptUserToSaveOrUpdatePasswordPtr).Times(0);
web::FakeNavigationContext context;
context.SetHasCommitted(true);
context.SetIsSameDocument(false);
context.SetIsRendererInitiated(true);
[passwordController_ webState:web_state() didFinishNavigation:&context];
// Simulate a failed submission by loading the same form again.
LoadHtml(SysUTF8ToNSString(kHtml));
}
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