blob: 6fde26c67773dc0b9683260fabfd1e2d3a265ab0 [file] [log] [blame]
// 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.
#import "components/password_manager/ios/password_controller_helper.h"
#include <stddef.h>
#include "base/strings/sys_string_conversions.h"
#include "base/values.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/password_form.h"
#include "components/autofill/ios/browser/autofill_util.h"
#include "components/password_manager/core/browser/form_parsing/ios_form_parser.h"
#include "components/password_manager/ios/js_password_manager.h"
#import "ios/web/public/web_state/web_state.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using autofill::FormData;
using autofill::PasswordForm;
using password_manager::GetPageURLAndCheckTrustLevel;
namespace password_manager {
bool GetPageURLAndCheckTrustLevel(web::WebState* web_state,
GURL* __nullable page_url) {
auto trustLevel = web::URLVerificationTrustLevel::kNone;
GURL dummy;
if (!page_url) {
page_url = &dummy;
}
*page_url = web_state->GetCurrentURL(&trustLevel);
return trustLevel == web::URLVerificationTrustLevel::kAbsolute;
}
} // namespace password_manager
namespace {
// Script command prefix for form changes. Possible command to be sent from
// injected JS is 'passwordForm.submitButtonClick'.
constexpr char kCommandPrefix[] = "passwordForm";
} // namespace
@interface PasswordControllerHelper ()
@property(nonatomic, weak) id<PasswordControllerHelperDelegate> delegate;
// Handler for injected JavaScript callbacks.
- (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand;
// Finds the currently submitted password form named |formName| and calls
// |completionHandler| with the populated data structure. |found| is YES if the
// current form was found successfully, NO otherwise. |completionHandler|
// cannot be nil.
- (void)extractSubmittedPasswordForm:(const std::string&)formName
completionHandler:
(void (^)(BOOL found,
const PasswordForm& form))completionHandler;
@end
@implementation PasswordControllerHelper {
// The WebState this instance is observing. Will be null after
// -webStateDestroyed: has been called.
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
@synthesize delegate = _delegate;
@synthesize jsPasswordManager = _jsPasswordManager;
#pragma mark - Initialization
- (instancetype)initWithWebState:(web::WebState*)webState
delegate:
(id<PasswordControllerHelperDelegate>)delegate {
self = [super init];
if (self) {
DCHECK(webState);
_webState = webState;
_delegate = delegate;
_webStateObserverBridge =
std::make_unique<web::WebStateObserverBridge>(self);
_webState->AddObserver(_webStateObserverBridge.get());
_formActivityObserverBridge =
std::make_unique<autofill::FormActivityObserverBridge>(_webState, self);
_jsPasswordManager = [[JsPasswordManager alloc]
initWithReceiver:_webState->GetJSInjectionReceiver()];
__weak PasswordControllerHelper* weakSelf = self;
auto callback = base::BindRepeating(
^bool(const base::DictionaryValue& JSON, const GURL& originURL,
bool interacting, bool isMainFrame) {
if (!isMainFrame) {
// Passwords is only supported on main frame.
return false;
}
// |originURL| and |interacting| aren't used.
return [weakSelf handleScriptCommand:JSON];
});
_webState->AddScriptCommandCallback(callback, kCommandPrefix);
}
return self;
}
#pragma mark - Dealloc
- (void)dealloc {
if (_webState) {
_webState->RemoveScriptCommandCallback(kCommandPrefix);
_webState->RemoveObserver(_webStateObserverBridge.get());
}
}
#pragma mark - CRWWebStateObserver
- (void)webStateDestroyed:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
if (_webState) {
_webState->RemoveScriptCommandCallback(kCommandPrefix);
_webState->RemoveObserver(_webStateObserverBridge.get());
_webState = nullptr;
}
_webStateObserverBridge.reset();
_formActivityObserverBridge.reset();
}
#pragma mark - FormActivityObserver
- (void)webState:(web::WebState*)webState
submittedDocumentWithFormNamed:(const std::string&)formName
hasUserGesture:(BOOL)hasUserGesture
formInMainFrame:(BOOL)formInMainFrame {
DCHECK_EQ(_webState, webState);
__weak PasswordControllerHelper* weakSelf = self;
// This code is racing against the new page loading and will not get the
// password form data if the page has changed. In most cases this code wins
// the race.
// TODO(crbug.com/418827): Fix this by passing in more data from the JS side.
id completionHandler = ^(BOOL found, const autofill::PasswordForm& form) {
PasswordControllerHelper* strongSelf = weakSelf;
id<PasswordControllerHelperDelegate> strongDelegate = strongSelf.delegate;
if (!strongSelf || !strongSelf->_webState || !strongDelegate) {
return;
}
[strongDelegate helper:strongSelf
didSubmitForm:form
inMainFrame:formInMainFrame];
};
[self extractSubmittedPasswordForm:formName
completionHandler:completionHandler];
}
#pragma mark - Private methods
- (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand {
std::string command;
if (!JSONCommand.GetString("command", &command)) {
return NO;
}
if (command != "passwordForm.submitButtonClick") {
return NO;
}
GURL pageURL;
if (!GetPageURLAndCheckTrustLevel(_webState, &pageURL)) {
return NO;
}
FormData formData;
if (!autofill::ExtractFormData(JSONCommand, false, base::string16(), pageURL,
&formData)) {
return NO;
}
std::unique_ptr<PasswordForm> form =
ParseFormData(formData, password_manager::FormParsingMode::SAVING);
if (!form) {
return NO;
}
if (_webState && self.delegate) {
[self.delegate helper:self didSubmitForm:*form inMainFrame:YES];
return YES;
}
return NO;
}
- (void)extractSubmittedPasswordForm:(const std::string&)formName
completionHandler:
(void (^)(BOOL found,
const PasswordForm& form))completionHandler {
DCHECK(completionHandler);
if (!_webState) {
return;
}
GURL pageURL;
if (!GetPageURLAndCheckTrustLevel(_webState, &pageURL)) {
completionHandler(NO, PasswordForm());
return;
}
id extractSubmittedFormCompletionHandler = ^(NSString* jsonString) {
std::unique_ptr<base::Value> formValue = autofill::ParseJson(jsonString);
if (!formValue) {
completionHandler(NO, PasswordForm());
return;
}
FormData formData;
if (!autofill::ExtractFormData(*formValue, false, base::string16(), pageURL,
&formData)) {
completionHandler(NO, PasswordForm());
return;
}
std::unique_ptr<PasswordForm> form =
ParseFormData(formData, password_manager::FormParsingMode::SAVING);
if (!form) {
completionHandler(NO, PasswordForm());
return;
}
completionHandler(YES, *form);
};
[self.jsPasswordManager extractForm:base::SysUTF8ToNSString(formName)
completionHandler:extractSubmittedFormCompletionHandler];
}
@end