Commit c44dadf2 authored by Javier Ernesto Flores Robles's avatar Javier Ernesto Flores Robles Committed by Commit Bot

[iOS][MF] Support iFrames in Manual Fallback

Enables the injection handler used in Manual Fallback to work on frames.
Adds testing utilities to support tapping an element in a window
frame.
Adds the frame messaging flag to the manual fallback test bot.

Bug: 845472
Change-Id: Id054361af1f4be5450f13cd0afabe46e24ea11ff
Reviewed-on: https://chromium-review.googlesource.com/c/1292409
Commit-Queue: Javier Ernesto Flores Robles <javierrobles@chromium.org>
Reviewed-by: default avatarOlivier Robin <olivierrobin@chromium.org>
Reviewed-by: default avatarEugene But <eugenebut@chromium.org>
Reviewed-by: default avatarBen Pastene <bpastene@chromium.org>
Reviewed-by: default avatarMoe Ahmadi <mahmadi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#604364}
parent 8ba674d9
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
{ {
"app": "ios_chrome_manual_fill_egtests", "app": "ios_chrome_manual_fill_egtests",
"test args": [ "test args": [
"--enable-features=AutofillManualFallback" "--enable-features=AutofillManualFallback,WebFrameMessaging"
], ],
"xctest": true "xctest": true
}, },
......
...@@ -123,6 +123,7 @@ source_set("eg_tests") { ...@@ -123,6 +123,7 @@ source_set("eg_tests") {
"//base", "//base",
"//base/test:test_support", "//base/test:test_support",
"//components/autofill/core/common", "//components/autofill/core/common",
"//components/autofill/ios/browser",
"//components/keyed_service/core", "//components/keyed_service/core",
"//components/password_manager/core/browser", "//components/password_manager/core/browser",
"//ios/chrome/browser/passwords", "//ios/chrome/browser/passwords",
...@@ -133,6 +134,6 @@ source_set("eg_tests") { ...@@ -133,6 +134,6 @@ source_set("eg_tests") {
"//ios/third_party/earl_grey:earl_grey+link", "//ios/third_party/earl_grey:earl_grey+link",
"//ios/web:earl_grey_test_support", "//ios/web:earl_grey_test_support",
"//ios/web/public/test/http_server", "//ios/web/public/test/http_server",
"//third_party/ocmock:ocmock", "//third_party/ocmock",
] ]
} }
...@@ -4,7 +4,16 @@ ...@@ -4,7 +4,16 @@
#import "ios/chrome/browser/ui/autofill/manual_fill/manual_fill_injection_handler.h" #import "ios/chrome/browser/ui/autofill/manual_fill/manual_fill_injection_handler.h"
#include <memory>
#include <string>
#include <vector>
#include "base/json/string_escape.h"
#include "base/mac/foundation_util.h" #include "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/values.h"
#include "components/autofill/ios/browser/autofill_switches.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/browser/js_suggestion_manager.h" #import "components/autofill/ios/browser/js_suggestion_manager.h"
#import "components/autofill/ios/form_util/form_activity_observer_bridge.h" #import "components/autofill/ios/form_util/form_activity_observer_bridge.h"
#include "components/autofill/ios/form_util/form_activity_params.h" #include "components/autofill/ios/form_util/form_activity_params.h"
...@@ -12,6 +21,8 @@ ...@@ -12,6 +21,8 @@
#import "ios/chrome/browser/ui/autofill/manual_fill/form_observer_helper.h" #import "ios/chrome/browser/ui/autofill/manual_fill/form_observer_helper.h"
#import "ios/chrome/browser/web_state_list/web_state_list.h" #import "ios/chrome/browser/web_state_list/web_state_list.h"
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h" #import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#include "ios/web/public/web_state/web_frame.h"
#include "ios/web/public/web_state/web_frame_util.h"
#include "ios/web/public/web_state/web_frames_manager.h" #include "ios/web/public/web_state/web_frames_manager.h"
#include "ios/web/public/web_state/web_state.h" #include "ios/web/public/web_state/web_state.h"
#include "url/gurl.h" #include "url/gurl.h"
...@@ -20,24 +31,38 @@ ...@@ -20,24 +31,38 @@
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
namespace {
// The timeout for any JavaScript call in this file.
const int64_t kJavaScriptExecutionTimeoutInSeconds = 1;
}
@interface ManualFillInjectionHandler ()<FormActivityObserver> @interface ManualFillInjectionHandler ()<FormActivityObserver>
// The object in charge of listening to form events and reporting back. // The object in charge of listening to form events and reporting back.
@property(nonatomic, strong) FormObserverHelper* formHelper; @property(nonatomic, strong) FormObserverHelper* formHelper;
// Convenience getter for the current injection reciever. // Convenience getter for the current injection reciever.
@property(nonatomic, readonly) CRWJSInjectionReceiver* injectionReceiver; @property(nonatomic, readonly) CRWJSInjectionReceiver* injectionReceiver;
// Convenience getter for the current suggestion manager. // Convenience getter for the current suggestion manager.
@property(nonatomic, readonly) JsSuggestionManager* suggestionManager; @property(nonatomic, readonly) JsSuggestionManager* suggestionManager;
// The WebStateList with the relevant active web state for the injection. // The WebStateList with the relevant active web state for the injection.
@property(nonatomic, assign) WebStateList* webStateList; @property(nonatomic, assign) WebStateList* webStateList;
// YES if the last focused element is secure within its web frame. To be secure // YES if the last focused element is secure within its web frame. To be secure
// means it has a password type, the web is https and the URL can trusted. // means it has a password type, the web is https and the URL can trusted.
@property(nonatomic, assign) BOOL lastActiveElementIsSecure; @property(nonatomic, assign) BOOL lastFocusedElementIsSecure;
// The last seen frame ID with focus activity.
@property(nonatomic, assign) std::string lastFocusedElementFrameIdentifier;
// The last seen focused element identifier.
@property(nonatomic, assign) std::string lastFocusedElementIdentifier;
@end @end
@implementation ManualFillInjectionHandler @implementation ManualFillInjectionHandler
@synthesize formHelper = _formHelper;
@synthesize lastActiveElementIsSecure = _lastActiveElementIsSecure;
@synthesize webStateList = _webStateList;
- (instancetype)initWithWebStateList:(WebStateList*)webStateList { - (instancetype)initWithWebStateList:(WebStateList*)webStateList {
self = [super init]; self = [super init];
...@@ -52,7 +77,7 @@ ...@@ -52,7 +77,7 @@
#pragma mark - ManualFillViewDelegate #pragma mark - ManualFillViewDelegate
- (void)userDidPickContent:(NSString*)content isSecure:(BOOL)isSecure { - (void)userDidPickContent:(NSString*)content isSecure:(BOOL)isSecure {
if (isSecure && !self.lastActiveElementIsSecure) { if (isSecure && !self.lastFocusedElementIsSecure) {
return; return;
} }
[self fillLastSelectedFieldWithString:content]; [self fillLastSelectedFieldWithString:content];
...@@ -66,13 +91,18 @@ ...@@ -66,13 +91,18 @@
if (params.type != "focus") { if (params.type != "focus") {
return; return;
} }
web::URLVerificationTrustLevel trustLevel; BOOL isContextSecure = autofill::IsContextSecureForWebState(webState);
const GURL pageURL(webState->GetCurrentURL(&trustLevel)); BOOL isPasswordField = params.field_type == "password";
self.lastActiveElementIsSecure = YES; self.lastFocusedElementIsSecure = isContextSecure && isPasswordField;
if (trustLevel != web::URLVerificationTrustLevel::kAbsolute || self.lastFocusedElementIdentifier = params.field_identifier;
!pageURL.SchemeIs(url::kHttpsScheme) || !webState->ContentIsHTML() ||
params.field_type != "password") { if (autofill::switches::IsAutofillIFrameMessagingEnabled()) {
self.lastActiveElementIsSecure = NO; DCHECK(frame);
self.lastFocusedElementFrameIdentifier = frame->GetFrameId();
const GURL frameSecureOrigin = frame->GetSecurityOrigin();
if (!frameSecureOrigin.SchemeIsCryptographic()) {
self.lastFocusedElementIsSecure = NO;
}
} }
} }
...@@ -100,7 +130,32 @@ ...@@ -100,7 +130,32 @@
// Injects the passed string to the active field and jumps to the next field. // Injects the passed string to the active field and jumps to the next field.
- (void)fillLastSelectedFieldWithString:(NSString*)string { - (void)fillLastSelectedFieldWithString:(NSString*)string {
// TODO:(https://crbug.com/878388) validation / escaping of string. if (autofill::switches::IsAutofillIFrameMessagingEnabled()) {
web::WebState* activeWebState = self.webStateList->GetActiveWebState();
if (!activeWebState) {
return;
}
web::WebFrame* activeWebFrame = web::GetWebFrameWithId(
activeWebState, self.lastFocusedElementFrameIdentifier);
if (!activeWebFrame || !activeWebFrame->CanCallJavaScriptFunction()) {
return;
}
base::DictionaryValue data = base::DictionaryValue();
data.SetString("identifier", self.lastFocusedElementIdentifier);
data.SetString("value", base::SysNSStringToUTF16(string));
std::vector<base::Value> parameters;
parameters.push_back(std::move(data));
activeWebFrame->CallJavaScriptFunction(
"autofill.fillActiveFormField", parameters,
base::BindOnce(^(const base::Value*) {
[self jumpToNextField];
}),
base::TimeDelta::FromSeconds(kJavaScriptExecutionTimeoutInSeconds));
return;
}
// Frame messaging is disabled, use the old injection reciever.
NSString* javaScriptQuery = NSString* javaScriptQuery =
[NSString stringWithFormat: [NSString stringWithFormat:
@"__gCrWeb.fill.setInputElementValue(\"%@\", " @"__gCrWeb.fill.setInputElementValue(\"%@\", "
......
...@@ -147,6 +147,10 @@ NSString* const OtherPasswordsAccessibilityIdentifier = ...@@ -147,6 +147,10 @@ NSString* const OtherPasswordsAccessibilityIdentifier =
net::registry_controlled_domains::GetDomainAndRegistry( net::registry_controlled_domains::GetDomainAndRegistry(
visibleURL.host(), visibleURL.host(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
// Sometimes the site_name can be empty. i.e. if the host is an IP address.
if (site_name.empty()) {
site_name = visibleURL.host();
}
NSString* siteName = base::SysUTF8ToNSString(site_name); NSString* siteName = base::SysUTF8ToNSString(site_name);
NSPredicate* predicate = NSPredicate* predicate =
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#import "base/test/ios/wait_util.h" #import "base/test/ios/wait_util.h"
#include "components/autofill/core/common/autofill_features.h" #include "components/autofill/core/common/autofill_features.h"
#include "components/autofill/core/common/password_form.h" #include "components/autofill/core/common/password_form.h"
#include "components/autofill/ios/browser/autofill_switches.h"
#include "components/keyed_service/core/service_access_type.h" #include "components/keyed_service/core/service_access_type.h"
#include "components/password_manager/core/browser/password_store.h" #include "components/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/browser/password_store_consumer.h" #include "components/password_manager/core/browser/password_store_consumer.h"
...@@ -25,8 +26,10 @@ ...@@ -25,8 +26,10 @@
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h" #import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h" #import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/chrome_test_case.h" #import "ios/chrome/test/earl_grey/chrome_test_case.h"
#include "ios/web/public/features.h"
#import "ios/web/public/test/earl_grey/web_view_matchers.h" #import "ios/web/public/test/earl_grey/web_view_matchers.h"
#include "ios/web/public/test/element_selector.h" #include "ios/web/public/test/element_selector.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/gurl.h" #include "url/gurl.h"
...@@ -43,6 +46,7 @@ const char kExampleUsername[] = "concrete username"; ...@@ -43,6 +46,7 @@ const char kExampleUsername[] = "concrete username";
const char kExamplePassword[] = "concrete password"; const char kExamplePassword[] = "concrete password";
const char kFormHTMLFile[] = "/username_password_field_form.html"; const char kFormHTMLFile[] = "/username_password_field_form.html";
const char kIFrameHTMLFile[] = "/iframe_form.html";
// Returns a matcher for the password icon in the keyboard accessory bar. // Returns a matcher for the password icon in the keyboard accessory bar.
id<GREYMatcher> PasswordIconMatcher() { id<GREYMatcher> PasswordIconMatcher() {
...@@ -190,7 +194,17 @@ void SaveExamplePasswordForm() { ...@@ -190,7 +194,17 @@ void SaveExamplePasswordForm() {
autofill::PasswordForm example; autofill::PasswordForm example;
example.username_value = base::ASCIIToUTF16(kExampleUsername); example.username_value = base::ASCIIToUTF16(kExampleUsername);
example.password_value = base::ASCIIToUTF16(kExamplePassword); example.password_value = base::ASCIIToUTF16(kExamplePassword);
example.origin = GURL("https://example.com"); example.origin = GURL("https://example.com/");
example.signon_realm = example.origin.spec();
SaveToPasswordStore(example);
}
// Saves an example form in the store.
void SaveLocalPasswordForm() {
autofill::PasswordForm example;
example.username_value = base::ASCIIToUTF16(kExampleUsername);
example.password_value = base::ASCIIToUTF16(kExamplePassword);
example.origin = GURL("http://127.0.0.1:55264");
example.signon_realm = example.origin.spec(); example.signon_realm = example.origin.spec();
SaveToPasswordStore(example); SaveToPasswordStore(example);
} }
...@@ -204,6 +218,21 @@ void ClearPasswordStore() { ...@@ -204,6 +218,21 @@ void ClearPasswordStore() {
@"PasswordStore was not cleared."); @"PasswordStore was not cleared.");
} }
// Polls the JavaScript query |java_script_condition| until the returned
// |boolValue| is YES with a kWaitForActionTimeout timeout.
BOOL WaitForJavaScriptCondition(NSString* java_script_condition) {
auto verify_block = ^BOOL {
id value = chrome_test_util::ExecuteJavaScript(java_script_condition, nil);
return [value isEqual:@YES];
};
NSTimeInterval timeout = base::test::ios::kWaitForActionTimeout;
NSString* condition_name = [NSString
stringWithFormat:@"Wait for JS condition: %@", java_script_condition];
GREYCondition* condition =
[GREYCondition conditionWithName:condition_name block:verify_block];
return [condition waitWithTimeout:timeout];
}
} // namespace } // namespace
// Integration Tests for Mannual Fallback Passwords View Controller. // Integration Tests for Mannual Fallback Passwords View Controller.
...@@ -475,7 +504,7 @@ void ClearPasswordStore() { ...@@ -475,7 +504,7 @@ void ClearPasswordStore() {
assertWithMatcher:grey_notVisible()]; assertWithMatcher:grey_notVisible()];
} }
// Test that after switching fields the content size of the table view didn't // Tests that after switching fields the content size of the table view didn't
// grow. // grow.
- (void)testPasswordControllerKeepsRightSize { - (void)testPasswordControllerKeepsRightSize {
// Bring up the keyboard. // Bring up the keyboard.
...@@ -503,7 +532,7 @@ void ClearPasswordStore() { ...@@ -503,7 +532,7 @@ void ClearPasswordStore() {
assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_sufficientlyVisible()];
} }
// Test that the Password View Controller stays on rotation. // Tests that the Password View Controller stays on rotation.
- (void)testPasswordControllerSupportsRotation { - (void)testPasswordControllerSupportsRotation {
// Bring up the keyboard. // Bring up the keyboard.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()] [[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
...@@ -525,4 +554,47 @@ void ClearPasswordStore() { ...@@ -525,4 +554,47 @@ void ClearPasswordStore() {
assertWithMatcher:grey_sufficientlyVisible()]; assertWithMatcher:grey_sufficientlyVisible()];
} }
// Tests that content is injected in iframe messaging.
- (void)testPasswordControllerSupportsIFrameMessaging {
// Iframe messaging is not supported on iOS < 11.3.
if (!base::ios::IsRunningOnOrLater(11, 3, 0)) {
EARL_GREY_TEST_SKIPPED(@"Skipped for iOS < 11.3");
}
GREYAssert(base::FeatureList::IsEnabled(web::features::kWebFrameMessaging),
@"Frame Messaging must be enabled for this Test Case");
const GURL URL = self.testServer->GetURL(kIFrameHTMLFile);
[ChromeEarlGrey loadURL:URL];
[ChromeEarlGrey waitForWebViewContainingText:"iFrame"];
SaveLocalPasswordForm();
// Bring up the keyboard.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
performAction:chrome_test_util::TapWebElementInFrame(kFormElementUsername,
0)];
// Wait for the accessory icon to appear.
[GREYKeyboard waitForKeyboardToAppear];
// Tap on the passwords icon.
[[EarlGrey selectElementWithMatcher:PasswordIconMatcher()]
performAction:grey_tap()];
// Verify the password controller table view is visible.
[[EarlGrey selectElementWithMatcher:PasswordTableViewMatcher()]
assertWithMatcher:grey_sufficientlyVisible()];
// Select a username.
[[EarlGrey selectElementWithMatcher:UsernameButtonMatcher()]
performAction:grey_tap()];
// Verify Web Content.
NSString* javaScriptCondition = [NSString
stringWithFormat:
@"window.frames[0].document.getElementById('%s').value === '%s'",
kFormElementUsername, kExampleUsername];
XCTAssertTrue(WaitForJavaScriptCondition(javaScriptCondition));
}
@end @end
...@@ -34,6 +34,13 @@ id<GREYAction> TurnSyncSwitchOn(BOOL on); ...@@ -34,6 +34,13 @@ id<GREYAction> TurnSyncSwitchOn(BOOL on);
// state. // state.
id<GREYAction> TapWebElement(const std::string& element_id); id<GREYAction> TapWebElement(const std::string& element_id);
// Action to tap a web element in iframe with the given |element_id| on the
// current web state. iframe is an immediate child of the main frame with the
// given index. The action fails if target iframe has a different origin from
// the main frame.
id<GREYAction> TapWebElementInFrame(const std::string& element_id,
const int frame_index);
} // namespace chrome_test_util } // namespace chrome_test_util
#endif // IOS_CHROME_TEST_EARL_GREY_CHROME_ACTIONS_H_ #endif // IOS_CHROME_TEST_EARL_GREY_CHROME_ACTIONS_H_
...@@ -74,4 +74,12 @@ id<GREYAction> TapWebElement(const std::string& element_id) { ...@@ -74,4 +74,12 @@ id<GREYAction> TapWebElement(const std::string& element_id) {
web::test::ElementSelector::ElementSelectorId(element_id)); web::test::ElementSelector::ElementSelectorId(element_id));
} }
id<GREYAction> TapWebElementInFrame(const std::string& element_id,
const int frame_index) {
return web::WebViewTapElement(
chrome_test_util::GetCurrentWebState(),
web::test::ElementSelector::ElementSelectorIdInFrame(element_id,
frame_index));
}
} // namespace chrome_test_util } // namespace chrome_test_util
...@@ -77,6 +77,7 @@ bundle_data("http_server_bundle_data") { ...@@ -77,6 +77,7 @@ bundle_data("http_server_bundle_data") {
"data/http_server_files/history.js", "data/http_server_files/history.js",
"data/http_server_files/history_go.html", "data/http_server_files/history_go.html",
"data/http_server_files/history_go.js", "data/http_server_files/history_go.js",
"data/http_server_files/iframe_form.html",
"data/http_server_files/iframe_host.html", "data/http_server_files/iframe_host.html",
"data/http_server_files/links.html", "data/http_server_files/links.html",
"data/http_server_files/memory_usage.html", "data/http_server_files/memory_usage.html",
......
<!DOCTYPE html>
<!-- Copyright 2018 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<p>iFrame
<iframe src="username_password_field_form.html"></iframe>
...@@ -18,6 +18,14 @@ class ElementSelector { ...@@ -18,6 +18,14 @@ class ElementSelector {
// Returns an ElementSelector to retrieve an element by ID. // Returns an ElementSelector to retrieve an element by ID.
static const ElementSelector ElementSelectorId(const std::string element_id); static const ElementSelector ElementSelectorId(const std::string element_id);
// Returns an ElementSelector to retrieve an element in iframe by ID. iframe
// is an immediate child of the main frame with the given index. The script of
// this selector will throw an exception if target iframe has a different
// origin from the main frame.
static const ElementSelector ElementSelectorIdInFrame(
const std::string element_id,
const int frame_index);
// Returns an ElementSelector to retrieve an element by a CSS selector. // Returns an ElementSelector to retrieve an element by a CSS selector.
static const ElementSelector ElementSelectorCss( static const ElementSelector ElementSelectorCss(
const std::string css_selector); const std::string css_selector);
......
...@@ -21,6 +21,17 @@ const ElementSelector ElementSelector::ElementSelectorId( ...@@ -21,6 +21,17 @@ const ElementSelector ElementSelector::ElementSelectorId(
base::StringPrintf("with ID %s", element_id.c_str())); base::StringPrintf("with ID %s", element_id.c_str()));
} }
// Static.
const ElementSelector ElementSelector::ElementSelectorIdInFrame(
const std::string element_id,
const int frame_index) {
return ElementSelector(
base::StringPrintf("window.frames[%d].document.getElementById('%s')",
frame_index, element_id.c_str()),
base::StringPrintf("in iframe with index %d, with ID %s", frame_index,
element_id.c_str()));
}
// Static. // Static.
const ElementSelector ElementSelector::ElementSelectorCss( const ElementSelector ElementSelector::ElementSelectorCss(
const std::string css_selector) { const std::string css_selector) {
......
...@@ -691,7 +691,7 @@ ...@@ -691,7 +691,7 @@
"label": "//ios/chrome/test/earl_grey:ios_chrome_manual_fill_egtests", "label": "//ios/chrome/test/earl_grey:ios_chrome_manual_fill_egtests",
"type": "raw", "type": "raw",
"args": [ "args": [
"--enable-features=AutofillManualFallback", "--enable-features=AutofillManualFallback,WebFrameMessaging",
], ],
}, },
"ios_chrome_reading_list_egtests": { "ios_chrome_reading_list_egtests": {
......
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