[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: Olivier Robin <olivierrobin@chromium.org>
Reviewed-by: Eugene But <eugenebut@chromium.org>
Reviewed-by: Ben Pastene <bpastene@chromium.org>
Reviewed-by: Moe Ahmadi <mahmadi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#604364}
diff --git a/ios/build/bots/tests/eg_tests.json b/ios/build/bots/tests/eg_tests.json
index b54249e..38706d8 100644
--- a/ios/build/bots/tests/eg_tests.json
+++ b/ios/build/bots/tests/eg_tests.json
@@ -10,7 +10,7 @@
{
"app": "ios_chrome_manual_fill_egtests",
"test args": [
- "--enable-features=AutofillManualFallback"
+ "--enable-features=AutofillManualFallback,WebFrameMessaging"
],
"xctest": true
},
diff --git a/ios/chrome/browser/ui/autofill/manual_fill/BUILD.gn b/ios/chrome/browser/ui/autofill/manual_fill/BUILD.gn
index 076a1b8..7b99fc3 100644
--- a/ios/chrome/browser/ui/autofill/manual_fill/BUILD.gn
+++ b/ios/chrome/browser/ui/autofill/manual_fill/BUILD.gn
@@ -123,6 +123,7 @@
"//base",
"//base/test:test_support",
"//components/autofill/core/common",
+ "//components/autofill/ios/browser",
"//components/keyed_service/core",
"//components/password_manager/core/browser",
"//ios/chrome/browser/passwords",
@@ -133,6 +134,6 @@
"//ios/third_party/earl_grey:earl_grey+link",
"//ios/web:earl_grey_test_support",
"//ios/web/public/test/http_server",
- "//third_party/ocmock:ocmock",
+ "//third_party/ocmock",
]
}
diff --git a/ios/chrome/browser/ui/autofill/manual_fill/manual_fill_injection_handler.mm b/ios/chrome/browser/ui/autofill/manual_fill/manual_fill_injection_handler.mm
index 0f8f831..1c2d53cf 100644
--- a/ios/chrome/browser/ui/autofill/manual_fill/manual_fill_injection_handler.mm
+++ b/ios/chrome/browser/ui/autofill/manual_fill/manual_fill_injection_handler.mm
@@ -4,7 +4,16 @@
#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/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/form_util/form_activity_observer_bridge.h"
#include "components/autofill/ios/form_util/form_activity_params.h"
@@ -12,6 +21,8 @@
#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/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_state.h"
#include "url/gurl.h"
@@ -20,24 +31,38 @@
#error "This file requires ARC support."
#endif
+namespace {
+// The timeout for any JavaScript call in this file.
+const int64_t kJavaScriptExecutionTimeoutInSeconds = 1;
+}
+
@interface ManualFillInjectionHandler ()<FormActivityObserver>
+
// The object in charge of listening to form events and reporting back.
@property(nonatomic, strong) FormObserverHelper* formHelper;
+
// Convenience getter for the current injection reciever.
@property(nonatomic, readonly) CRWJSInjectionReceiver* injectionReceiver;
+
// Convenience getter for the current suggestion manager.
@property(nonatomic, readonly) JsSuggestionManager* suggestionManager;
+
// The WebStateList with the relevant active web state for the injection.
@property(nonatomic, assign) WebStateList* webStateList;
+
// 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.
-@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
@implementation ManualFillInjectionHandler
-@synthesize formHelper = _formHelper;
-@synthesize lastActiveElementIsSecure = _lastActiveElementIsSecure;
-@synthesize webStateList = _webStateList;
- (instancetype)initWithWebStateList:(WebStateList*)webStateList {
self = [super init];
@@ -52,7 +77,7 @@
#pragma mark - ManualFillViewDelegate
- (void)userDidPickContent:(NSString*)content isSecure:(BOOL)isSecure {
- if (isSecure && !self.lastActiveElementIsSecure) {
+ if (isSecure && !self.lastFocusedElementIsSecure) {
return;
}
[self fillLastSelectedFieldWithString:content];
@@ -66,13 +91,18 @@
if (params.type != "focus") {
return;
}
- web::URLVerificationTrustLevel trustLevel;
- const GURL pageURL(webState->GetCurrentURL(&trustLevel));
- self.lastActiveElementIsSecure = YES;
- if (trustLevel != web::URLVerificationTrustLevel::kAbsolute ||
- !pageURL.SchemeIs(url::kHttpsScheme) || !webState->ContentIsHTML() ||
- params.field_type != "password") {
- self.lastActiveElementIsSecure = NO;
+ BOOL isContextSecure = autofill::IsContextSecureForWebState(webState);
+ BOOL isPasswordField = params.field_type == "password";
+ self.lastFocusedElementIsSecure = isContextSecure && isPasswordField;
+ self.lastFocusedElementIdentifier = params.field_identifier;
+
+ if (autofill::switches::IsAutofillIFrameMessagingEnabled()) {
+ DCHECK(frame);
+ self.lastFocusedElementFrameIdentifier = frame->GetFrameId();
+ const GURL frameSecureOrigin = frame->GetSecurityOrigin();
+ if (!frameSecureOrigin.SchemeIsCryptographic()) {
+ self.lastFocusedElementIsSecure = NO;
+ }
}
}
@@ -100,7 +130,32 @@
// Injects the passed string to the active field and jumps to the next field.
- (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 stringWithFormat:
@"__gCrWeb.fill.setInputElementValue(\"%@\", "
diff --git a/ios/chrome/browser/ui/autofill/manual_fill/password_mediator.mm b/ios/chrome/browser/ui/autofill/manual_fill/password_mediator.mm
index f519683..a03b0e6 100644
--- a/ios/chrome/browser/ui/autofill/manual_fill/password_mediator.mm
+++ b/ios/chrome/browser/ui/autofill/manual_fill/password_mediator.mm
@@ -147,6 +147,10 @@
net::registry_controlled_domains::GetDomainAndRegistry(
visibleURL.host(),
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);
NSPredicate* predicate =
diff --git a/ios/chrome/browser/ui/autofill/manual_fill/password_view_controller_egtest.mm b/ios/chrome/browser/ui/autofill/manual_fill/password_view_controller_egtest.mm
index cd7dd6b..46bbeb3 100644
--- a/ios/chrome/browser/ui/autofill/manual_fill/password_view_controller_egtest.mm
+++ b/ios/chrome/browser/ui/autofill/manual_fill/password_view_controller_egtest.mm
@@ -11,6 +11,7 @@
#import "base/test/ios/wait_util.h"
#include "components/autofill/core/common/autofill_features.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/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/browser/password_store_consumer.h"
@@ -25,8 +26,10 @@
#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_test_case.h"
+#include "ios/web/public/features.h"
#import "ios/web/public/test/earl_grey/web_view_matchers.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 "url/gurl.h"
@@ -43,6 +46,7 @@
const char kExamplePassword[] = "concrete password";
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.
id<GREYMatcher> PasswordIconMatcher() {
@@ -190,7 +194,17 @@
autofill::PasswordForm example;
example.username_value = base::ASCIIToUTF16(kExampleUsername);
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();
SaveToPasswordStore(example);
}
@@ -204,6 +218,21 @@
@"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
// Integration Tests for Mannual Fallback Passwords View Controller.
@@ -475,7 +504,7 @@
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.
- (void)testPasswordControllerKeepsRightSize {
// Bring up the keyboard.
@@ -503,7 +532,7 @@
assertWithMatcher:grey_sufficientlyVisible()];
}
-// Test that the Password View Controller stays on rotation.
+// Tests that the Password View Controller stays on rotation.
- (void)testPasswordControllerSupportsRotation {
// Bring up the keyboard.
[[EarlGrey selectElementWithMatcher:chrome_test_util::WebViewMatcher()]
@@ -525,4 +554,47 @@
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
diff --git a/ios/chrome/test/earl_grey/chrome_actions.h b/ios/chrome/test/earl_grey/chrome_actions.h
index 7d4812b..e340a03e 100644
--- a/ios/chrome/test/earl_grey/chrome_actions.h
+++ b/ios/chrome/test/earl_grey/chrome_actions.h
@@ -34,6 +34,13 @@
// state.
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
#endif // IOS_CHROME_TEST_EARL_GREY_CHROME_ACTIONS_H_
diff --git a/ios/chrome/test/earl_grey/chrome_actions.mm b/ios/chrome/test/earl_grey/chrome_actions.mm
index 56c5bd1..f735858 100644
--- a/ios/chrome/test/earl_grey/chrome_actions.mm
+++ b/ios/chrome/test/earl_grey/chrome_actions.mm
@@ -74,4 +74,12 @@
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
diff --git a/ios/testing/BUILD.gn b/ios/testing/BUILD.gn
index b1add658..cbd92d0 100644
--- a/ios/testing/BUILD.gn
+++ b/ios/testing/BUILD.gn
@@ -77,6 +77,7 @@
"data/http_server_files/history.js",
"data/http_server_files/history_go.html",
"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/links.html",
"data/http_server_files/memory_usage.html",
diff --git a/ios/testing/data/http_server_files/iframe_form.html b/ios/testing/data/http_server_files/iframe_form.html
new file mode 100644
index 0000000..f8f03d89
--- /dev/null
+++ b/ios/testing/data/http_server_files/iframe_form.html
@@ -0,0 +1,6 @@
+<!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>
diff --git a/ios/web/public/test/element_selector.h b/ios/web/public/test/element_selector.h
index 4ebc477..06132020 100644
--- a/ios/web/public/test/element_selector.h
+++ b/ios/web/public/test/element_selector.h
@@ -18,6 +18,14 @@
// Returns an ElementSelector to retrieve an element by 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.
static const ElementSelector ElementSelectorCss(
const std::string css_selector);
diff --git a/ios/web/public/test/element_selector.mm b/ios/web/public/test/element_selector.mm
index bb301ab0..eb5d77e 100644
--- a/ios/web/public/test/element_selector.mm
+++ b/ios/web/public/test/element_selector.mm
@@ -22,6 +22,17 @@
}
// 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.
const ElementSelector ElementSelector::ElementSelectorCss(
const std::string css_selector) {
const std::string script(base::StringPrintf("document.querySelector(\"%s\")",
diff --git a/testing/buildbot/gn_isolate_map.pyl b/testing/buildbot/gn_isolate_map.pyl
index 6ee9c22..e0aaf64 100644
--- a/testing/buildbot/gn_isolate_map.pyl
+++ b/testing/buildbot/gn_isolate_map.pyl
@@ -691,7 +691,7 @@
"label": "//ios/chrome/test/earl_grey:ios_chrome_manual_fill_egtests",
"type": "raw",
"args": [
- "--enable-features=AutofillManualFallback",
+ "--enable-features=AutofillManualFallback,WebFrameMessaging",
],
},
"ios_chrome_reading_list_egtests": {