[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": {