chromium/components/password_manager/ios/password_form_helper.mm

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "components/password_manager/ios/password_form_helper.h"

#import <stddef.h>

#import "base/debug/crash_logging.h"
#import "base/debug/dump_without_crashing.h"
#import "base/functional/bind.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros.h"
#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/values.h"
#import "components/autofill/core/common/field_data_manager.h"
#import "components/autofill/core/common/form_data.h"
#import "components/autofill/core/common/password_form_fill_data.h"
#import "components/autofill/core/common/unique_ids.h"
#import "components/autofill/ios/browser/autofill_util.h"
#import "components/autofill/ios/common/field_data_manager_factory_ios.h"
#import "components/autofill/ios/form_util/form_util_java_script_feature.h"
#import "components/password_manager/ios/account_select_fill_data.h"
#import "components/password_manager/ios/ios_password_manager_driver.h"
#import "components/password_manager/ios/ios_password_manager_driver_factory.h"
#import "components/password_manager/ios/password_manager_ios_util.h"
#import "components/password_manager/ios/password_manager_java_script_feature.h"
#import "components/password_manager/ios/password_manager_tab_helper.h"
#import "components/ukm/ios/ukm_url_recorder.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/web_state.h"
#import "services/metrics/public/cpp/ukm_builders.h"

using autofill::FieldPropertiesFlags;
using autofill::FormData;
using autofill::FormRendererId;
using autofill::FieldRendererId;
using autofill::PasswordFormFillData;
using base::SysNSStringToUTF16;
using base::UTF16ToUTF8;
using password_manager::FillData;
using password_manager::JsonStringToFormData;

namespace password_manager {

// The frame id associated with the frame which sent to form message.
const char kFrameIdKey[] = "frame_id";

}  // namespace password_manager

@interface PasswordFormHelper ()

// Parses the |jsonString| which contatins the password forms found on a web
// page to populate the |forms| vector.
- (void)getPasswordForms:(std::vector<FormData>*)forms
                fromJSON:(NSString*)jsonString
                 pageURL:(const GURL&)pageURL
                   frame:(web::WebFrame*)frame;

// Records both UMA & UKM metrics.
- (void)recordFormFillingSuccessMetrics:(bool)success;

@end

@implementation PasswordFormHelper {
  // The WebState this instance is observing. Will be null after
  // -webStateDestroyed: has been called.
  raw_ptr<web::WebState> _webState;

  // Bridge to observe WebState from Objective-C.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;

  // Bridge to observe form activity in |_webState|.
  std::unique_ptr<autofill::FormActivityObserverBridge>
      _formActivityObserverBridge;
}

#pragma mark - Properties

- (const GURL&)lastCommittedURL {
  return _webState ? _webState->GetLastCommittedURL() : GURL::EmptyGURL();
}

#pragma mark - Initialization

- (instancetype)initWithWebState:(web::WebState*)webState {
  self = [super init];
  if (self) {
    DCHECK(webState);
    _webState = webState;
    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    _webState->AddObserver(_webStateObserverBridge.get());
    _formActivityObserverBridge =
        std::make_unique<autofill::FormActivityObserverBridge>(_webState, self);

    password_manager::PasswordManagerTabHelper::GetOrCreateForWebState(webState)
        ->SetFormHelper(self);
  }
  return self;
}

#pragma mark - Dealloc

- (void)dealloc {
  if (_webState) {
    _webState->RemoveObserver(_webStateObserverBridge.get());
  }
}

#pragma mark - CRWWebStateObserver

- (void)webStateDestroyed:(web::WebState*)webState {
  DCHECK_EQ(_webState, webState);
  if (_webState) {
    _webState->RemoveObserver(_webStateObserverBridge.get());
    _webState = nullptr;
  }
  _webStateObserverBridge.reset();
  _formActivityObserverBridge.reset();
}

#pragma mark - FormActivityObserver

- (void)webState:(web::WebState*)webState
    didSubmitDocumentWithFormNamed:(const std::string&)formName
                          withData:(const std::string&)formData
                    hasUserGesture:(BOOL)hasUserGesture
                           inFrame:(web::WebFrame*)frame {
  DCHECK_EQ(_webState, webState);
  GURL pageURL = webState->GetLastCommittedURL();
  if (pageURL.DeprecatedGetOriginAsURL() != frame->GetSecurityOrigin()) {
    // Passwords is only supported on main frame and iframes with the same
    // origin.
    return;
  }
  if (!self.delegate || formData.empty()) {
    return;
  }

  autofill::FieldDataManager* fieldDataManager =
      autofill::FieldDataManagerFactoryIOS::FromWebFrame(frame);

  std::vector<FormData> forms;
  NSString* nsFormData = [NSString stringWithUTF8String:formData.c_str()];
  autofill::ExtractFormsData(nsFormData, false, std::u16string(), pageURL,
                             pageURL.DeprecatedGetOriginAsURL(),
                             *fieldDataManager, frame->GetFrameId(), &forms);
  if (forms.size() != 1) {
    return;
  }

  [self.delegate formHelper:self didSubmitForm:forms[0] inFrame:frame];
}

#pragma mark - Private methods

- (void)getPasswordForms:(std::vector<FormData>*)forms
                fromJSON:(NSString*)JSONString
                 pageURL:(const GURL&)pageURL
                   frame:(web::WebFrame*)frame {
  autofill::FieldDataManager* fieldDataManager =
      autofill::FieldDataManagerFactoryIOS::FromWebFrame(frame);

  std::vector<FormData> formsData;
  if (!autofill::ExtractFormsData(JSONString, false, std::u16string(), pageURL,
                                  frame->GetSecurityOrigin(), *fieldDataManager,
                                  frame->GetFrameId(), &formsData)) {
    return;
  }
  *forms = std::move(formsData);
}

- (void)recordFormFillingSuccessMetrics:(bool)success {
  base::UmaHistogramBoolean("PasswordManager.FillingSuccessIOS", success);
  ukm::SourceId source_id = ukm::GetSourceIdForWebStateDocument(_webState);

  if (source_id == ukm::kInvalidSourceId || !(ukm::UkmRecorder::Get())) {
    return;
  }
  ukm::builders::PasswordManager_PasswordFillingIOS(source_id)
      .SetFillingSuccess(success)
      .Record(ukm::UkmRecorder::Get());
}

#pragma mark - Public methods

- (void)findPasswordFormsInFrame:(web::WebFrame*)frame
               completionHandler:
                   (void (^)(const std::vector<FormData>&))completionHandler {
  if (!_webState) {
    return;
  }

  std::optional<GURL> pageURL = _webState->GetLastCommittedURLIfTrusted();
  if (!pageURL) {
    return;
  }

  __weak PasswordFormHelper* weakSelf = self;
  password_manager::PasswordManagerJavaScriptFeature::GetInstance()
      ->FindPasswordFormsInFrame(
          frame, base::BindOnce(^(NSString* JSONString) {
            std::vector<FormData> forms;
            [weakSelf getPasswordForms:&forms
                              fromJSON:JSONString
                               pageURL:*pageURL
                                 frame:frame];
            completionHandler(forms);
          }));
}

- (void)fillPasswordForm:(FormRendererId)formIdentifier
                      inFrame:(web::WebFrame*)frame
        newPasswordIdentifier:(FieldRendererId)newPasswordIdentifier
    confirmPasswordIdentifier:(FieldRendererId)confirmPasswordIdentifier
            generatedPassword:(NSString*)generatedPassword
            completionHandler:(nullable void (^)(BOOL))completionHandler {
  const scoped_refptr<autofill::FieldDataManager> fieldDataManager =
      autofill::FieldDataManagerFactoryIOS::GetRetainable(frame);

  // Send JSON over to the web view.
  password_manager::PasswordManagerJavaScriptFeature::GetInstance()
      ->FillPasswordForm(
          frame, formIdentifier, newPasswordIdentifier,
          confirmPasswordIdentifier, generatedPassword,
          base::BindOnce(
              ^(BOOL success) {
                if (success) {
                  fieldDataManager->UpdateFieldDataMap(
                      newPasswordIdentifier,
                      SysNSStringToUTF16(generatedPassword),
                      FieldPropertiesFlags::kAutofilledOnUserTrigger);
                  fieldDataManager->UpdateFieldDataMap(
                      confirmPasswordIdentifier,
                      SysNSStringToUTF16(generatedPassword),
                      FieldPropertiesFlags::kAutofilledOnUserTrigger);
                }
                if (completionHandler) {
                  completionHandler(success);
                }
              }));
}

// Handles the result from filling with fill data. Returns YES if the fill
// operation is considered as a success.
- (BOOL)handleFillResult:(const base::Value*)result
            fromFillData:(password_manager::FillData)fillData
    withFieldDataManager:(autofill::FieldDataManager*)manager
                  driver:(IOSPasswordManagerDriver*)driver {
  if (!result || !result->is_dict()) {
    return NO;
  }

  std::optional<bool> did_attempt_fill =
      result->GetDict().FindBool("didAttemptFill");
  std::optional<bool> did_fill_username =
      result->GetDict().FindBool("didFillUsername");
  std::optional<bool> did_fill_password =
      result->GetDict().FindBool("didFillPassword");

  if (!did_attempt_fill || !did_fill_username || !did_fill_password) {
    return NO;
  }
  const bool success = *did_attempt_fill;

  [self recordFormFillingSuccessMetrics:success];

  // TODO(crbug.com/347882357): Add a metric to record the reason why
  // |didFillUsername| or |didFillPassword| is false.

  // Set the fill property even if |*did_fill_password| or |*did_fill_username|
  // are false to avoid skewing the PasswordManager.FillingAssistance histogram.
  // This is the status quo with how the field data used to be updated, before
  // crrev.com/c/5494354.
  if (fillData.username_element_id && success) {
    manager->UpdateFieldDataMap(fillData.username_element_id,
                                fillData.username_value,
                                FieldPropertiesFlags::kAutofilledOnUserTrigger);

    if (*did_fill_username) {
      driver->GetPasswordManager()->UpdateStateOnUserInput(
          driver, fillData.form_id, fillData.username_element_id,
          fillData.username_value);
    }
  }
  if (fillData.password_element_id && success) {
    manager->UpdateFieldDataMap(fillData.password_element_id,
                                fillData.password_value,
                                FieldPropertiesFlags::kAutofilledOnUserTrigger);
    if (*did_fill_password) {
      driver->GetPasswordManager()->UpdateStateOnUserInput(
          driver, fillData.form_id, fillData.password_element_id,
          fillData.password_value);
    }
  }

  return success;
}

- (void)fillPasswordFormWithFillData:(password_manager::FillData)fillData
                             inFrame:(web::WebFrame*)frame
                    triggeredOnField:(FieldRendererId)fieldRendererID
                   completionHandler:
                       (nullable void (^)(BOOL))completionHandler {
  const scoped_refptr<autofill::FieldDataManager> fieldDataManager =
      autofill::FieldDataManagerFactoryIOS::GetRetainable(frame);
  const scoped_refptr<IOSPasswordManagerDriver> driver =
      IOSPasswordManagerDriverFactory::GetRetainableDriver(_webState, frame);

  // Do not fill the username if filling was triggered on a password field and
  // the username field has user typed input.
  BOOL fillUsername =
      fieldRendererID == fillData.username_element_id ||
      !fieldDataManager->DidUserType(fillData.username_element_id);
  __weak PasswordFormHelper* weakSelf = self;
  password_manager::PasswordManagerJavaScriptFeature::GetInstance()
      ->FillPasswordForm(frame, fillData, fillUsername,
                         UTF16ToUTF8(fillData.username_value),
                         UTF16ToUTF8(fillData.password_value),
                         base::BindOnce(^(const base::Value* result) {
                           const BOOL success =
                               [weakSelf handleFillResult:result
                                             fromFillData:fillData
                                     withFieldDataManager:fieldDataManager.get()
                                                   driver:driver.get()];

                           if (completionHandler) {
                             completionHandler(success);
                           }
                         }));
}

// Finds the password form named |formName| and calls
// |completionHandler| with the populated |FormData| data structure. |found| is
// YES if the current form was found successfully, NO otherwise.
- (void)extractPasswordFormData:(FormRendererId)formIdentifier
                        inFrame:(web::WebFrame*)frame
              completionHandler:(void (^)(BOOL found, const FormData& form))
                                    completionHandler {
  DCHECK(completionHandler);

  if (!_webState) {
    return;
  }

  std::optional<GURL> pageURL = _webState->GetLastCommittedURLIfTrusted();
  if (!pageURL) {
    completionHandler(NO, FormData());
    return;
  }

  const scoped_refptr<autofill::FieldDataManager> fieldDataManager =
      autofill::FieldDataManagerFactoryIOS::GetRetainable(frame);

  std::string frame_id = frame->GetFrameId();
  password_manager::PasswordManagerJavaScriptFeature::GetInstance()
      ->ExtractForm(
          frame, formIdentifier, base::BindOnce(^(NSString* jsonString) {
            FormData formData;
            if (!JsonStringToFormData(jsonString, &formData, *pageURL,
                                      *fieldDataManager, frame_id)) {
              completionHandler(NO, FormData());
              return;
            }

            completionHandler(YES, formData);
          }));
}

- (void)updateFieldDataOnUserInput:(autofill::FieldRendererId)field_id
                           inFrame:(web::WebFrame*)frame
                        inputValue:(NSString*)value {
  autofill::FieldDataManager* fieldDataManager =
      autofill::FieldDataManagerFactoryIOS::FromWebFrame(frame);

  fieldDataManager->UpdateFieldDataMap(
      field_id, base::SysNSStringToUTF16(value),
      autofill::FieldPropertiesFlags::kUserTyped);
}

- (HandleSubmittedFormStatus)handleFormSubmittedMessage:
    (const web::ScriptMessage&)message {
  if (!_webState) {
    return HandleSubmittedFormStatus::kRejectedNoWebState;
  }

  if (!self.delegate) {
    return HandleSubmittedFormStatus::kRejectedNoDelegate;
  }

  std::optional<GURL> pageURL = _webState->GetLastCommittedURLIfTrusted();
  if (!pageURL) {
    return HandleSubmittedFormStatus::kRejectedNoTrustedUrl;
  }

  web::WebFrame* frame = nullptr;
  base::Value* body = message.body();

  if (!body->is_dict()) {
    // Don't handle the message if it isn't of dictionary type. The renderer
    // must provide that type of message so it can be interpreted.
    return HandleSubmittedFormStatus::kRejectedMessageBodyNotADict;
  }

  const auto& dict = body->GetDict();
  const std::string* frame_id = dict.FindString(password_manager::kFrameIdKey);
  if (frame_id) {
    password_manager::PasswordManagerJavaScriptFeature* feature =
        password_manager::PasswordManagerJavaScriptFeature::GetInstance();
    frame = feature->GetWebFramesManager(_webState)->GetFrameWithId(*frame_id);
  }
  if (!frame) {
    return HandleSubmittedFormStatus::kRejectedNoFrameMatchingId;
  }

  autofill::FieldDataManager* fieldDataManager =
      autofill::FieldDataManagerFactoryIOS::FromWebFrame(frame);

  FormData form;
  if (!autofill::ExtractFormData(dict, false, std::u16string(), *pageURL,
                                 pageURL->DeprecatedGetOriginAsURL(),
                                 *fieldDataManager, *frame_id, &form)) {
    return HandleSubmittedFormStatus::kRejectedCantExtractFormData;
  }

  [self.delegate formHelper:self didSubmitForm:form inFrame:frame];

  return HandleSubmittedFormStatus::kHandled;
}

@end