// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web_view/internal/cwv_web_view_internal.h"
#include <memory>
#include <unordered_map>
#include <utility>
#import <WebKit/WebKit.h>
#include "base/apple/foundation_util.h"
#include "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#include "base/json/json_writer.h"
#import "base/notreached.h"
#include "base/strings/sys_string_conversions.h"
#import "components/autofill/ios/browser/autofill_agent.h"
#include "components/password_manager/core/browser/password_manager.h"
#import "components/password_manager/ios/password_controller_driver_helper.h"
#import "components/password_manager/ios/shared_password_controller.h"
#import "components/safe_browsing/ios/browser/safe_browsing_url_allow_list.h"
#include "components/url_formatter/elide_url.h"
#include "google_apis/google_api_keys.h"
#import "ios/components/security_interstitials/lookalikes/lookalike_url_container.h"
#import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_allow_list.h"
#import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.h"
#import "ios/components/security_interstitials/safe_browsing/safe_browsing_client.h"
#import "ios/components/security_interstitials/safe_browsing/safe_browsing_query_manager.h"
#import "ios/components/security_interstitials/safe_browsing/safe_browsing_tab_helper.h"
#import "ios/components/security_interstitials/safe_browsing/safe_browsing_unsafe_resource_container.h"
#import "ios/web/navigation/nscoder_util.h"
#include "ios/web/public/favicon/favicon_url.h"
#include "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#include "ios/web/public/navigation/referrer.h"
#include "ios/web/public/navigation/reload_type.h"
#import "ios/web/public/session/crw_session_storage.h"
#import "ios/web/public/session/proto/metadata.pb.h"
#import "ios/web/public/session/proto/storage.pb.h"
#import "ios/web/public/ui/context_menu_params.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_client.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_view_only/wk_web_view_configuration_util.h"
#include "ios/web_view/internal/app/application_context.h"
#import "ios/web_view/internal/autofill/cwv_autofill_controller_internal.h"
#import "ios/web_view/internal/autofill/web_view_autofill_client_ios.h"
#import "ios/web_view/internal/cwv_back_forward_list_internal.h"
#import "ios/web_view/internal/cwv_favicon_internal.h"
#import "ios/web_view/internal/cwv_find_in_page_controller_internal.h"
#import "ios/web_view/internal/cwv_html_element_internal.h"
#import "ios/web_view/internal/cwv_navigation_action_internal.h"
#import "ios/web_view/internal/cwv_ssl_status_internal.h"
#import "ios/web_view/internal/cwv_web_view_configuration_internal.h"
#import "ios/web_view/internal/language/web_view_url_language_histogram_factory.h"
#import "ios/web_view/internal/passwords/web_view_password_manager_client.h"
#import "ios/web_view/internal/safe_browsing/web_view_safe_browsing_client_factory.h"
#import "ios/web_view/internal/translate/cwv_translation_controller_internal.h"
#import "ios/web_view/internal/translate/web_view_translate_client.h"
#include "ios/web_view/internal/web_view_browser_state.h"
#include "ios/web_view/internal/web_view_global_state_util.h"
#import "ios/web_view/internal/web_view_java_script_dialog_presenter.h"
#import "ios/web_view/internal/web_view_message_handler_java_script_feature.h"
#import "ios/web_view/internal/web_view_web_state_policy_decider.h"
#import "ios/web_view/public/cwv_navigation_delegate.h"
#import "ios/web_view/public/cwv_preview_element_info.h"
#import "ios/web_view/public/cwv_ui_delegate.h"
#import "ios/web_view/public/cwv_web_view_configuration.h"
#import "net/base/apple/url_conversions.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
namespace {
NSString* gCustomUserAgent = nil;
NSString* gUserAgentProduct = nil;
BOOL gChromeContextMenuEnabled = NO;
BOOL gUseOptimizedSessionStorage = NO;
BOOL gWebInspectorEnabled = NO;
BOOL gSkipAccountStorageCheckEnabled = NO;
// A key used in NSCoder to store the session storage object.
// TODO(crbug.com/40945317): remove once the feature has been launched and
// all session migrated to the new format.
NSString* const kSessionStorageKey = @"sessionStorage";
// Keys used to store CWVWebViewProtobufStorage and its properties.
NSString* const kProtobufStorageKey = @"protobufStorage";
NSString* const kStorageKey = @"storage";
NSString* const kSessionKey = @"session";
// Converts base::Value expected to be a dictionary or list to NSDictionary or
// NSArray, respectively.
id NSObjectFromCollectionValue(const base::Value* value) {
DCHECK(value->is_dict() || value->is_list())
<< "Incorrect value type: " << value->type();
std::string json;
const bool success = base::JSONWriter::Write(*value, &json);
DCHECK(success) << "Failed to convert base::Value to JSON";
NSData* json_data = [NSData dataWithBytes:json.c_str() length:json.length()];
id ns_object = [NSJSONSerialization JSONObjectWithData:json_data
options:kNilOptions
error:nil];
DCHECK(ns_object) << "Failed to convert JSON to Collection";
return ns_object;
}
// Converts base::Value to an appropriate Obj-C object.
// |value| must not be null.
id NSObjectFromValue(const base::Value* value) {
switch (value->type()) {
case base::Value::Type::NONE:
return nil;
case base::Value::Type::BOOLEAN:
return @(value->GetBool());
case base::Value::Type::INTEGER:
return @(value->GetInt());
case base::Value::Type::DOUBLE:
return @(value->GetDouble());
case base::Value::Type::STRING:
return base::SysUTF8ToNSString(value->GetString());
case base::Value::Type::BINARY:
// Unsupported.
return nil;
case base::Value::Type::DICT:
case base::Value::Type::LIST:
return NSObjectFromCollectionValue(value);
}
return nil;
}
// Converts base::Value::Dict to NSDictionary.
NSDictionary* NSDictionaryFromDictValue(const base::Value::Dict& value) {
std::string json;
const bool success = base::JSONWriter::Write(value, &json);
DCHECK(success) << "Failed to convert base::Value to JSON";
NSData* json_data = [NSData dataWithBytes:json.c_str() length:json.length()];
NSDictionary* ns_dictionary = base::apple::ObjCCastStrict<NSDictionary>(
[NSJSONSerialization JSONObjectWithData:json_data
options:kNilOptions
error:nil]);
DCHECK(ns_dictionary) << "Failed to convert JSON to NSDictionary";
return ns_dictionary;
}
// A WebStateUserData to hold a reference to a corresponding CWVWebView.
class WebViewHolder : public web::WebStateUserData<WebViewHolder> {
public:
explicit WebViewHolder(web::WebState* web_state) {}
CWVWebView* web_view() const { return web_view_; }
void set_web_view(CWVWebView* web_view) { web_view_ = web_view; }
private:
friend class web::WebStateUserData<WebViewHolder>;
__weak CWVWebView* web_view_ = nil;
WEB_STATE_USER_DATA_KEY_DECL();
};
WEB_STATE_USER_DATA_KEY_IMPL(WebViewHolder)
} // namespace
// Used to serialize the protobuf message and the WebStateID.
@interface CWVWebViewProtobufStorage : NSObject <NSCoding>
- (instancetype)initWithProto:(web::proto::WebStateStorage)storage
webStateID:(web::WebStateID)webStateID
NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder*)coder;
- (instancetype)init NS_UNAVAILABLE;
// Creates a new WebState with `browserState`.
- (std::unique_ptr<web::WebState>)createWebState:
(web::BrowserState*)browserState;
// The protobuf representation.
@property(nonatomic, readonly) const web::proto::WebStateStorage& storage;
// The web state identifier.
@property(nonatomic, readonly) web::WebStateID webStateID;
@end
@implementation CWVWebViewProtobufStorage {
web::proto::WebStateStorage _storage;
}
- (instancetype)initWithProto:(web::proto::WebStateStorage)storage
webStateID:(web::WebStateID)webStateID {
if ((self = [super init])) {
_storage = std::move(storage);
_webStateID = webStateID;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder*)coder {
if (![coder containsValueForKey:kStorageKey]) {
return nil;
}
if (![coder containsValueForKey:kSessionKey]) {
return nil;
}
const std::string buffer =
web::nscoder_util::DecodeString(coder, kStorageKey);
web::proto::WebStateStorage storage;
if (!storage.ParseFromString(buffer)) {
return nil;
}
web::WebStateID webStateID = web::WebStateID::FromSerializedValue(
[coder decodeInt32ForKey:kSessionKey]);
if (!webStateID.valid()) {
return nil;
}
return [self initWithProto:std::move(storage) webStateID:webStateID];
}
- (void)encodeWithCoder:(NSCoder*)coder {
std::string buffer;
_storage.SerializeToString(&buffer);
web::nscoder_util::EncodeString(coder, kStorageKey, buffer);
[coder encodeInt32:_webStateID.identifier() forKey:kSessionKey];
}
- (std::unique_ptr<web::WebState>)createWebState:
(web::BrowserState*)browserState {
return web::WebState::CreateWithStorage(
browserState, self.webStateID, _storage.metadata(),
base::ReturnValueOnce(std::move(_storage)),
base::ReturnValueOnce<NSData*>(nil));
}
- (const web::proto::WebStateStorage&)storage {
return _storage;
}
@end
// Helper used to manage the serialization of CWVWebView's WebState
// with either the legacy or the optimised session serialization code.
@interface CWVWebViewSerializationHelper : NSObject
// Designated initializer.
- (instancetype)initWithConfiguration:(CWVWebViewConfiguration*)configuration
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
// Creates a new WebState from `coder`.
- (std::unique_ptr<web::WebState>)createWebStateWithCoder:(NSCoder*)coder;
// Serializes `webState` to `coder`. If `webState` is null, then the cached
// state is serialized instead (captured by `-updateStateFromWebState:`).
- (void)encodeWebState:(web::WebState*)webState toCoder:(NSCoder*)coder;
// Updates the cached serialization state from `webState`.
- (void)updateStateFromWebState:(web::WebState*)webState;
// Clears the cached serialized state for `webState` if it is possible to
// reconstruct it at a later point (avoid keeping a copy of the state in
// memory when not necessary after serialization).
- (void)clearStateForWebStateIfPossible:(web::WebState*)webState;
@end
@implementation CWVWebViewSerializationHelper {
web::BrowserState* _browserState;
// Cached protobuf message. Only used if the optimised serialisation is used.
CWVWebViewProtobufStorage* _cachedProtobufStorage;
// Cached session storage. Only used if the legacy serialisation code is used.
// TODO(crbug.com/40945317): Remove when the feature has launched.
CRWSessionStorage* _cachedSessionStorage;
}
- (instancetype)initWithConfiguration:(CWVWebViewConfiguration*)configuration {
if ((self = [super init])) {
DCHECK(configuration.browserState);
_browserState = configuration.browserState;
}
return self;
}
- (std::unique_ptr<web::WebState>)createWebStateWithCoder:(NSCoder*)coder {
// To support partial rollout and roll back of the feature, try to load
// the data from `coder` in either the legacy or optimised format. This
// also allow migrating the storage in-place.
_cachedProtobufStorage =
base::apple::ObjCCastStrict<CWVWebViewProtobufStorage>(
[coder decodeObjectForKey:kProtobufStorageKey]);
_cachedSessionStorage = base::apple::ObjCCastStrict<CRWSessionStorage>(
[coder decodeObjectForKey:kSessionStorageKey]);
// If data can't be loaded in either format, return a brand new WebState.
// Since sending a message to nil is well-defined in Objective-C, this
// also cover the case when `coder` is nil.
if (!_cachedProtobufStorage && !_cachedSessionStorage) {
const web::WebState::CreateParams createParams(_browserState);
return web::WebState::Create(createParams);
}
// Support for legacy session serialisation code path.
// TODO(crbug.com/40945317): Remove when the feature has launched.
if (!gUseOptimizedSessionStorage) {
if (!_cachedSessionStorage) {
_cachedSessionStorage = [[CRWSessionStorage alloc]
initWithProto:_cachedProtobufStorage.storage
uniqueIdentifier:_cachedProtobufStorage.webStateID
stableIdentifier:[[NSUUID UUID] UUIDString]];
_cachedProtobufStorage = nil;
}
DCHECK(_cachedSessionStorage);
const web::WebState::CreateParams createParams(_browserState);
return web::WebState::CreateWithStorageSession(
createParams, _cachedSessionStorage,
base::ReturnValueOnce<NSData*>(nil));
}
if (!_cachedProtobufStorage) {
web::proto::WebStateStorage storage;
[_cachedSessionStorage serializeToProto:storage];
_cachedProtobufStorage = [[CWVWebViewProtobufStorage alloc]
initWithProto:std::move(storage)
webStateID:_cachedSessionStorage.uniqueIdentifier];
_cachedSessionStorage = nil;
}
DCHECK(_cachedProtobufStorage);
return [_cachedProtobufStorage createWebState:_browserState];
}
- (void)encodeWebState:(web::WebState*)webState toCoder:(NSCoder*)coder {
// TODO(crbug.com/40945317): Remove when the feature has launched.
if (!gUseOptimizedSessionStorage) {
if (webState) {
[self updateStateFromWebState:webState];
}
[coder encodeObject:_cachedSessionStorage forKey:kSessionStorageKey];
[self clearStateForWebStateIfPossible:webState];
return;
}
if (webState && webState->IsRealized()) {
[self updateStateFromWebState:webState];
}
[coder encodeObject:_cachedProtobufStorage forKey:kProtobufStorageKey];
[self clearStateForWebStateIfPossible:webState];
}
- (void)updateStateFromWebState:(web::WebState*)webState {
// TODO(crbug.com/40945317): Remove when the feature has launched.
if (!gUseOptimizedSessionStorage) {
_cachedSessionStorage = webState->BuildSessionStorage();
return;
}
DCHECK(webState->IsRealized());
web::proto::WebStateStorage storage;
webState->SerializeToProto(storage);
_cachedProtobufStorage = [[CWVWebViewProtobufStorage alloc]
initWithProto:storage
webStateID:webState->GetUniqueIdentifier()];
}
- (void)clearStateForWebStateIfPossible:(web::WebState*)webState {
// TODO(crbug.com/40945317): Remove when the feature has launched.
if (!gUseOptimizedSessionStorage) {
if (webState) {
_cachedSessionStorage = nil;
}
}
if (webState && webState->IsRealized()) {
_cachedProtobufStorage = nil;
}
}
@end
@interface CWVWebView () {
CWVWebViewConfiguration* _configuration;
std::unique_ptr<web::WebState> _webState;
std::unique_ptr<web::WebStateDelegateBridge> _webStateDelegate;
std::unique_ptr<web::WebStateObserverBridge> _webStateObserver;
std::unique_ptr<ios_web_view::WebViewWebStatePolicyDecider>
_webStatePolicyDecider;
double _estimatedProgress;
// Handles presentation of JavaScript dialogs.
std::unique_ptr<ios_web_view::WebViewJavaScriptDialogPresenter>
_javaScriptDialogPresenter;
// Handle the serialization and deserialization.
CWVWebViewSerializationHelper* _serializationHelper;
}
// Redefine these properties as readwrite to define setters, which send KVO
// notifications.
@property(nonatomic, readwrite) double estimatedProgress;
@property(nonatomic, readwrite) BOOL canGoBack;
@property(nonatomic, readwrite) BOOL canGoForward;
@property(nonatomic, readwrite) NSURL* lastCommittedURL;
@property(nonatomic, readwrite) BOOL loading;
@property(nonatomic, readwrite, copy) NSString* title;
@property(nonatomic, readwrite) NSURL* visibleURL;
@property(nonatomic, readwrite) NSString* visibleLocationString;
@property(nonatomic, readwrite) CWVSSLStatus* visibleSSLStatus;
// Updates the availability of the back/forward navigation properties exposed
// through |canGoBack| and |canGoForward|, and also updates |backForwardList|.
- (void)updateNavigationAvailability;
// Updates the URLs exposed through |lastCommittedURL| and |visibleURL|.
- (void)updateCurrentURLs;
// Updates |title| property.
- (void)updateTitle;
// Returns a new CWVAutofillController created from |_webState|.
- (CWVAutofillController*)newAutofillController;
// Returns a new CWVTranslationController created from |_webState|.
- (CWVTranslationController*)newTranslationController;
// Updates |_webState| visiblity.
- (void)updateWebStateVisibility;
@end
@implementation CWVWebView
@synthesize autofillController = _autofillController;
@synthesize backForwardList = _backForwardList;
@synthesize canGoBack = _canGoBack;
@synthesize canGoForward = _canGoForward;
@synthesize configuration = _configuration;
@synthesize estimatedProgress = _estimatedProgress;
@synthesize findInPageController = _findInPageController;
@synthesize lastCommittedURL = _lastCommittedURL;
@synthesize loading = _loading;
@synthesize navigationDelegate = _navigationDelegate;
@synthesize title = _title;
@synthesize translationController = _translationController;
@synthesize UIDelegate = _UIDelegate;
@synthesize visibleURL = _visibleURL;
@synthesize visibleSSLStatus = _visibleSSLStatus;
+ (void)initialize {
if (self != [CWVWebView class]) {
return;
}
ios_web_view::InitializeGlobalState();
}
+ (BOOL)chromeContextMenuEnabled {
return gChromeContextMenuEnabled;
}
+ (void)setChromeContextMenuEnabled:(BOOL)newValue {
gChromeContextMenuEnabled = newValue;
}
+ (BOOL)useOptimizedSessionStorage {
return gUseOptimizedSessionStorage;
}
+ (void)setUseOptimizedSessionStorage:(BOOL)newValue {
gUseOptimizedSessionStorage = newValue;
}
+ (BOOL)webInspectorEnabled {
return gWebInspectorEnabled;
}
+ (void)setWebInspectorEnabled:(BOOL)newValue {
gWebInspectorEnabled = newValue;
}
+ (BOOL)skipAccountStorageCheckEnabled {
return gSkipAccountStorageCheckEnabled;
}
+ (void)setSkipAccountStorageCheckEnabled:(BOOL)newValue {
gSkipAccountStorageCheckEnabled = newValue;
}
+ (NSString*)customUserAgent {
return gCustomUserAgent;
}
+ (void)setCustomUserAgent:(NSString*)customUserAgent {
gCustomUserAgent = [customUserAgent copy];
}
+ (NSString*)userAgentProduct {
return gUserAgentProduct;
}
+ (void)setUserAgentProduct:(NSString*)product {
gUserAgentProduct = [product copy];
}
+ (void)setGoogleAPIKey:(NSString*)googleAPIKey
clientID:(NSString*)clientID
clientSecret:(NSString*)clientSecret {
google_apis::SetAPIKey(base::SysNSStringToUTF8(googleAPIKey));
std::string clientIDString = base::SysNSStringToUTF8(clientID);
std::string clientSecretString = base::SysNSStringToUTF8(clientSecret);
for (size_t i = 0; i < google_apis::CLIENT_NUM_ITEMS; ++i) {
google_apis::OAuth2Client client =
static_cast<google_apis::OAuth2Client>(i);
google_apis::SetOAuth2ClientID(client, clientIDString);
google_apis::SetOAuth2ClientSecret(client, clientSecretString);
}
}
+ (CWVWebView*)webViewForWebState:(web::WebState*)webState {
WebViewHolder* holder = WebViewHolder::FromWebState(webState);
CWVWebView* webView = holder->web_view();
DCHECK(webView);
return webView;
}
- (instancetype)initWithFrame:(CGRect)frame
configuration:(CWVWebViewConfiguration*)configuration {
return [self initWithFrame:frame
configuration:configuration
WKConfiguration:nil
createdWKWebView:nil];
}
- (instancetype)initWithFrame:(CGRect)frame
configuration:(CWVWebViewConfiguration*)configuration
WKConfiguration:(WKWebViewConfiguration*)wkConfiguration
createdWKWebView:(WKWebView**)createdWebView {
self = [super initWithFrame:frame];
if (self) {
_configuration = configuration;
[_configuration registerWebView:self];
_serializationHelper = [[CWVWebViewSerializationHelper alloc]
initWithConfiguration:configuration];
[self resetWebStateWithCoder:nil
WKConfiguration:wkConfiguration
createdWebView:createdWebView];
}
return self;
}
- (UIScrollView*)scrollView {
return [_webState->GetWebViewProxy().scrollViewProxy asUIScrollView];
}
- (BOOL)allowsBackForwardNavigationGestures {
return _webState->GetWebViewProxy().allowsBackForwardNavigationGestures;
}
- (void)setAllowsBackForwardNavigationGestures:
(BOOL)allowsBackForwardNavigationGestures {
_webState->GetWebViewProxy().allowsBackForwardNavigationGestures =
allowsBackForwardNavigationGestures;
}
- (void)dealloc {
if (_webState) {
if (_webStateObserver) {
_webState->RemoveObserver(_webStateObserver.get());
_webStateObserver.reset();
}
WebViewHolder::RemoveFromWebState(_webState.get());
}
}
- (void)goBack {
if (_webState->GetNavigationManager())
_webState->GetNavigationManager()->GoBack();
}
- (void)goForward {
if (_webState->GetNavigationManager())
_webState->GetNavigationManager()->GoForward();
}
- (BOOL)goToBackForwardListItem:(CWVBackForwardListItem*)item {
if (!_backForwardList) {
return NO; // Do nothing if |_backForwardList| is not generated yet.
}
if ([item isEqual:_backForwardList.currentItem]) {
return NO;
}
int index = [_backForwardList internalIndexOfItem:item];
if (index == -1) {
return NO;
}
DCHECK(_webState);
web::NavigationManager* navigationManager = _webState->GetNavigationManager();
navigationManager->GoToIndex(index);
return YES;
}
- (void)reload {
// |check_for_repost| is false because CWVWebView does not support repost form
// dialogs.
_webState->GetNavigationManager()->Reload(web::ReloadType::NORMAL,
false /* check_for_repost */);
}
- (void)stopLoading {
_webState->Stop();
}
- (void)loadRequest:(NSURLRequest*)request {
DCHECK_EQ(nil, request.HTTPBodyStream)
<< "request.HTTPBodyStream is not supported.";
web::NavigationManager::WebLoadParams params(net::GURLWithNSURL(request.URL));
params.transition_type = ui::PAGE_TRANSITION_TYPED;
params.extra_headers = [request.allHTTPHeaderFields copy];
params.post_data = [request.HTTPBody copy];
_webState->GetNavigationManager()->LoadURLWithParams(params);
[self updateCurrentURLs];
}
- (void)evaluateJavaScript:(NSString*)javaScriptString
completionHandler:(void (^)(id result, NSError* error))completion {
web::WebFrame* mainFrame =
_webState->GetPageWorldWebFramesManager()->GetMainWebFrame();
if (!mainFrame) {
if (completion) {
completion(nil, [NSError errorWithDomain:@"org.chromium.chromewebview"
code:0
userInfo:nil]);
}
return;
}
if (!completion) {
mainFrame->ExecuteJavaScript(base::SysNSStringToUTF16(javaScriptString));
return;
}
mainFrame->ExecuteJavaScript(
base::SysNSStringToUTF16(javaScriptString),
base::BindOnce(^(const base::Value* result, NSError* error) {
id jsResult = nil;
if (!error && result) {
jsResult = NSObjectFromValue(result);
}
completion(jsResult, error);
}));
}
- (void)evaluateJavaScript:(NSString*)javaScriptString
completion:(void (^)(id, NSError*))completion {
[self evaluateJavaScript:javaScriptString completionHandler:completion];
}
- (void)setUIDelegate:(id<CWVUIDelegate>)UIDelegate {
_UIDelegate = UIDelegate;
_javaScriptDialogPresenter->SetUIDelegate(_UIDelegate);
}
- (void)setNavigationDelegate:(id<CWVNavigationDelegate>)navigationDelegate {
_navigationDelegate = navigationDelegate;
[self attachSecurityInterstitialHelpersToWebStateIfNecessary];
}
#pragma mark - UIResponder
- (BOOL)becomeFirstResponder {
if (_webState) {
return [_webState->GetWebViewProxy() becomeFirstResponder];
} else {
return [super becomeFirstResponder];
}
}
#pragma mark - UIView
- (void)didMoveToSuperview {
[super didMoveToSuperview];
[self updateWebStateVisibility];
}
#pragma mark - CRWWebStateObserver
- (void)webStateDestroyed:(web::WebState*)webState {
webState->RemoveObserver(_webStateObserver.get());
_webStateObserver.reset();
}
- (void)webState:(web::WebState*)webState
didStartNavigation:(web::NavigationContext*)navigation {
[self updateNavigationAvailability];
if (!navigation->IsSameDocument()) {
SEL oldSelector = @selector(webViewDidStartProvisionalNavigation:);
if ([_navigationDelegate respondsToSelector:oldSelector]) {
[_navigationDelegate webViewDidStartProvisionalNavigation:self];
}
SEL newSelector = @selector(webViewDidStartNavigation:);
if ([_navigationDelegate respondsToSelector:newSelector]) {
[_navigationDelegate webViewDidStartNavigation:self];
}
}
}
- (void)webState:(web::WebState*)webState
didFinishNavigation:(web::NavigationContext*)navigation {
[self updateNavigationAvailability];
[self updateCurrentURLs];
// TODO(crbug.com/41422373): Remove this once crbug.com/898357 is fixed.
[self updateVisibleSSLStatus];
if (navigation->HasCommitted() && !navigation->IsSameDocument() &&
[_navigationDelegate
respondsToSelector:@selector(webViewDidCommitNavigation:)]) {
[_navigationDelegate webViewDidCommitNavigation:self];
}
NSError* error = navigation->GetError();
SEL selector = @selector(webView:didFailNavigationWithError:);
if (error && [_navigationDelegate respondsToSelector:selector]) {
[_navigationDelegate webView:self didFailNavigationWithError:error];
}
}
- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success {
DCHECK_EQ(_webState.get(), webState);
if (!success) {
// Failure callbacks will be handled inside |webState:didFinishNavigation:|.
return;
}
// TODO(crbug.com/40872106): Fragment navigations currently skip calling
// `webViewDidStartNavigation:` and `webViewDidCommitNavigation:`, and instead
// only calls `webViewDidFinishNavigation:` below. Fix this inconsistency.
SEL selector = @selector(webViewDidFinishNavigation:);
if ([_navigationDelegate respondsToSelector:selector]) {
[_navigationDelegate webViewDidFinishNavigation:self];
}
}
- (void)webState:(web::WebState*)webState
didChangeLoadingProgress:(double)progress {
self.estimatedProgress = progress;
}
- (void)webStateDidChangeBackForwardState:(web::WebState*)webState {
[self updateNavigationAvailability];
}
- (void)webStateDidStopLoading:(web::WebState*)webState {
self.loading = _webState->IsLoading();
}
- (void)webStateDidStartLoading:(web::WebState*)webState {
self.loading = _webState->IsLoading();
}
- (void)webStateDidChangeTitle:(web::WebState*)webState {
[self updateTitle];
}
- (void)webStateDidChangeVisibleSecurityState:(web::WebState*)webState {
[self updateVisibleSSLStatus];
}
- (void)renderProcessGoneForWebState:(web::WebState*)webState {
SEL selector = @selector(webViewWebContentProcessDidTerminate:);
if ([_navigationDelegate respondsToSelector:selector]) {
[_navigationDelegate webViewWebContentProcessDidTerminate:self];
}
}
#pragma mark - CRWWebStateDelegate
- (web::WebState*)webState:(web::WebState*)webState
createNewWebStateForURL:(const GURL&)URL
openerURL:(const GURL&)openerURL
initiatedByUser:(BOOL)initiatedByUser {
SEL selector =
@selector(webView:createWebViewWithConfiguration:forNavigationAction:);
if (![_UIDelegate respondsToSelector:selector]) {
return nullptr;
}
NSURLRequest* request =
[[NSURLRequest alloc] initWithURL:net::NSURLWithGURL(URL)];
// The current implemention can't get the real navigation type for the
// navigation action which causes a new web view be created. So uses
// `CWVNavigationTypeNewWindow` before the real navigation type can be gotten
// here.
CWVNavigationAction* navigationAction =
[[CWVNavigationAction alloc] initWithRequest:request
userInitiated:initiatedByUser
navigationType:CWVNavigationTypeNewWindow];
CWVWebView* webView = [_UIDelegate webView:self
createWebViewWithConfiguration:_configuration
forNavigationAction:navigationAction];
if (!webView) {
return nullptr;
}
web::WebState* webViewWebState = webView->_webState.get();
webViewWebState->SetHasOpener(true);
return webViewWebState;
}
- (void)closeWebState:(web::WebState*)webState {
SEL selector = @selector(webViewDidClose:);
if ([_UIDelegate respondsToSelector:selector]) {
[_UIDelegate webViewDidClose:self];
}
}
- (web::WebState*)webState:(web::WebState*)webState
openURLWithParams:(const web::WebState::OpenURLParams&)params {
web::NavigationManager::WebLoadParams load_params(params.url);
load_params.referrer = params.referrer;
load_params.transition_type = params.transition;
load_params.is_renderer_initiated = params.is_renderer_initiated;
load_params.virtual_url = params.virtual_url;
_webState->GetNavigationManager()->LoadURLWithParams(load_params);
[self updateCurrentURLs];
return _webState.get();
}
- (web::JavaScriptDialogPresenter*)javaScriptDialogPresenterForWebState:
(web::WebState*)webState {
return _javaScriptDialogPresenter.get();
}
- (void)webState:(web::WebState*)webState
handlePermissions:(NSArray<NSNumber*>*)permissions
decisionHandler:(web::WebStatePermissionDecisionHandler)decisionHandler {
DCHECK(decisionHandler);
CWVMediaCaptureType mediaCaptureType;
BOOL cameraPermissionRequested =
[permissions containsObject:@(web::PermissionCamera)];
BOOL micPermissionRequested =
[permissions containsObject:@(web::PermissionMicrophone)];
if (cameraPermissionRequested && micPermissionRequested) {
mediaCaptureType = CWVMediaCaptureTypeCameraAndMicrophone;
} else if (cameraPermissionRequested) {
mediaCaptureType = CWVMediaCaptureTypeCamera;
} else if (micPermissionRequested) {
mediaCaptureType = CWVMediaCaptureTypeMicrophone;
} else {
NOTREACHED_IN_MIGRATION() << "Unknown media permissions";
}
SEL selector = @selector(webView:
requestMediaCapturePermissionForType:decisionHandler:);
if ([_UIDelegate respondsToSelector:selector]) {
[_UIDelegate webView:self
requestMediaCapturePermissionForType:mediaCaptureType
decisionHandler:^(CWVPermissionDecision decision) {
switch (decision) {
case CWVPermissionDecisionPrompt:
decisionHandler(
web::
PermissionDecisionShowDefaultPrompt);
break;
case CWVPermissionDecisionGrant:
decisionHandler(
web::PermissionDecisionGrant);
break;
case CWVPermissionDecisionDeny:
decisionHandler(web::PermissionDecisionDeny);
break;
}
}];
} else {
decisionHandler(web::PermissionDecisionShowDefaultPrompt);
}
}
- (void)webState:(web::WebState*)webState
contextMenuConfigurationForParams:(const web::ContextMenuParams&)params
completionHandler:(void (^)(UIContextMenuConfiguration*))
completionHandler {
SEL selector = @selector(webView:
contextMenuConfigurationForElement:completionHandler:);
if ([_UIDelegate respondsToSelector:selector]) {
NSURL* hyperlink = net::NSURLWithGURL(params.link_url);
NSURL* mediaSource = net::NSURLWithGURL(params.src_url);
CWVHTMLElement* HTMLElement =
[[CWVHTMLElement alloc] initWithHyperlink:hyperlink
mediaSource:mediaSource
text:params.text];
[_UIDelegate webView:self
contextMenuConfigurationForElement:HTMLElement
completionHandler:completionHandler];
} else {
completionHandler(nil);
}
}
- (void)webState:(web::WebState*)webState
contextMenuWillCommitWithAnimator:
(id<UIContextMenuInteractionCommitAnimating>)animator {
SEL selector = @selector(webView:contextMenuWillCommitWithAnimator:);
if ([_UIDelegate respondsToSelector:selector]) {
[_UIDelegate webView:self contextMenuWillCommitWithAnimator:animator];
}
}
- (void)webState:(web::WebState*)webState
didUpdateFaviconURLCandidates:
(const std::vector<web::FaviconURL>&)candidates {
if ([_UIDelegate respondsToSelector:@selector(webView:didLoadFavicons:)]) {
[_UIDelegate webView:self
didLoadFavicons:[CWVFavicon faviconsFromFaviconURLs:candidates]];
}
}
- (id<CRWResponderInputView>)webStateInputViewProvider:
(web::WebState*)webState {
if (self.inputAccessoryView != nil) {
return self;
} else {
return nil;
}
}
- (void)addMessageHandler:(void (^)(NSDictionary* payload))handler
forCommand:(NSString*)nsCommand {
DCHECK(handler);
std::string command = base::SysNSStringToUTF8(nsCommand);
WebViewMessageHandlerJavaScriptFeature::GetInstance()->RegisterHandler(
command, base::BindRepeating(^(const base::Value::Dict& payload) {
handler(NSDictionaryFromDictValue(payload));
}));
}
- (void)removeMessageHandlerForCommand:(NSString*)nsCommand {
std::string command = base::SysNSStringToUTF8(nsCommand);
WebViewMessageHandlerJavaScriptFeature::GetInstance()->UnregisterHandler(
command);
}
#pragma mark - Translation
- (CWVTranslationController*)translationController {
if (!_translationController) {
_translationController = [self newTranslationController];
}
return _translationController;
}
- (CWVTranslationController*)newTranslationController {
ios_web_view::WebViewBrowserState* browserState =
ios_web_view::WebViewBrowserState::FromBrowserState(
_webState->GetBrowserState());
auto translateClient = ios_web_view::WebViewTranslateClient::Create(
browserState, _webState.get());
return [[CWVTranslationController alloc]
initWithWebState:_webState.get()
translateClient:std::move(translateClient)];
}
#pragma mark - Autofill
- (CWVAutofillController*)autofillController {
if (!_autofillController) {
_autofillController = [self newAutofillController];
}
return _autofillController;
}
- (CWVAutofillController*)newAutofillController {
auto autofillClient = autofill::WebViewAutofillClientIOS::Create(
_webState.get(), _configuration.browserState);
AutofillAgent* autofillAgent = [[AutofillAgent alloc]
initWithPrefService:_configuration.browserState->GetPrefs()
webState:_webState.get()];
auto passwordManagerClient =
ios_web_view::WebViewPasswordManagerClient::Create(
_webState.get(), _configuration.browserState);
auto passwordManager = std::make_unique<password_manager::PasswordManager>(
passwordManagerClient.get());
PasswordFormHelper* formHelper =
[[PasswordFormHelper alloc] initWithWebState:_webState.get()];
PasswordSuggestionHelper* suggestionHelper =
[[PasswordSuggestionHelper alloc] initWithWebState:_webState.get()];
PasswordControllerDriverHelper* driverHelper =
[[PasswordControllerDriverHelper alloc] initWithWebState:_webState.get()];
SharedPasswordController* passwordController =
[[SharedPasswordController alloc] initWithWebState:_webState.get()
manager:passwordManager.get()
formHelper:formHelper
suggestionHelper:suggestionHelper
driverHelper:driverHelper];
return [[CWVAutofillController alloc]
initWithWebState:_webState.get()
autofillClient:std::move(autofillClient)
autofillAgent:autofillAgent
passwordManager:std::move(passwordManager)
passwordManagerClient:std::move(passwordManagerClient)
passwordController:passwordController
applicationLocale:ios_web_view::ApplicationContext::GetInstance()
->GetApplicationLocale()];
}
#pragma mark - Find In Page
- (CWVFindInPageController*)findInPageController {
if (!_findInPageController) {
_findInPageController =
[[CWVFindInPageController alloc] initWithWebState:_webState.get()];
}
return _findInPageController;
}
#pragma mark - Preserving and Restoring State
- (void)encodeRestorableStateWithCoder:(NSCoder*)coder {
[super encodeRestorableStateWithCoder:coder];
[_serializationHelper encodeWebState:_webState.get() toCoder:coder];
}
- (void)decodeRestorableStateWithCoder:(NSCoder*)coder {
[super decodeRestorableStateWithCoder:coder];
[self resetWebStateWithCoder:coder WKConfiguration:nil createdWebView:nil];
}
#pragma mark - Private methods
- (void)updateWebStateVisibility {
if (_webState == nullptr) {
return;
}
if (self.superview) {
_webState->WasShown();
} else {
_webState->WasHidden();
}
}
// Creates a WebState instance and assigns it to |_webState|.
// The WebState is restored from |coder| if available.
//
// If |wkConfiguration| is provided, the underlying WKWebView is
// initialized with |wkConfiguration|, and assigned to
// |*createdWKWebView| if |createdWKWebView| is not nil.
// |*createdWKWebView| will be provided only if |wkConfiguration| is provided,
// otherwise it will always be reset to nil.
- (void)resetWebStateWithCoder:(NSCoder*)coder
WKConfiguration:(WKWebViewConfiguration*)wkConfiguration
createdWebView:(WKWebView**)createdWebView {
if (_webState) {
if (_webStateObserver) {
_webState->RemoveObserver(_webStateObserver.get());
}
WebViewHolder::RemoveFromWebState(_webState.get());
if (_webState->GetView().superview == self) {
// The web view provided by the old |_webState| has been added as a
// subview. It must be removed and replaced with a new |_webState|'s web
// view, which is added later.
[_webState->GetView() removeFromSuperview];
}
}
BOOL allowsBackForwardNavigationGestures =
_webState &&
_webState->GetWebViewProxy().allowsBackForwardNavigationGestures;
// CWVWebView does not support unrealized WebState, so ignore the
// over-realization check (this simply reset the recent realization
// counter, so do it each time a WebState is created).
web::IgnoreOverRealizationCheck();
_webState = [_serializationHelper createWebStateWithCoder:coder];
DCHECK(_webState);
// WARNING: NOTHING should be here between |web::WebState::Create()| and
// |web::EnsureWebViewCreatedWithConfiguration()|, as this is the requirement
// of |web::EnsureWebViewCreatedWithConfiguration()|
WKWebView* webView = nil;
if (wkConfiguration) {
// When |wkConfiguration| is nil, |self| could be a newly opened web view
// e.g., triggered by JavaScript "window.open()" function. In that case, if
// |self| is not created by the WKWebViewConfiguration provided by WebKit's
// delegate method
// (https://cs.chromium.org/chromium/src/ios/web/web_state/ui/crw_wk_ui_handler.mm?q=crw_wk_ui_handler&sq=package:chromium&dr=C&l=61)
// then calling |web::EnsureWebViewCreatedWithConfiguration()| here would
// result in a crash (https://crbug.com/1054276). Now, we lazily create the
// WKWebView inside |_webState| when |wkConfiguration| is not nil, and the
// correct WKWebViewConfiguration will be passed inside //ios/web.
webView = web::EnsureWebViewCreatedWithConfiguration(_webState.get(),
wkConfiguration);
}
if (createdWebView) {
// If the created webView is needed, returns it by the out variable way.
*createdWebView = webView;
}
[self attachSecurityInterstitialHelpersToWebStateIfNecessary];
WebViewHolder::CreateForWebState(_webState.get());
WebViewHolder::FromWebState(_webState.get())->set_web_view(self);
if (!_webStateObserver) {
_webStateObserver = std::make_unique<web::WebStateObserverBridge>(self);
}
_webState->AddObserver(_webStateObserver.get());
if (_backForwardList) {
_backForwardList.navigationManager = _webState->GetNavigationManager();
}
[self updateWebStateVisibility];
_webStateDelegate = std::make_unique<web::WebStateDelegateBridge>(self);
_webState->SetDelegate(_webStateDelegate.get());
_webStatePolicyDecider =
std::make_unique<ios_web_view::WebViewWebStatePolicyDecider>(
_webState.get(), self);
_javaScriptDialogPresenter =
std::make_unique<ios_web_view::WebViewJavaScriptDialogPresenter>(self,
nullptr);
_webState->GetWebViewProxy().allowsBackForwardNavigationGestures =
allowsBackForwardNavigationGestures;
if (_translationController) {
id<CWVTranslationControllerDelegate> delegate =
_translationController.delegate;
_translationController = [self newTranslationController];
_translationController.delegate = delegate;
}
// Recreate and restore the delegate only if previously lazily loaded.
if (_autofillController) {
id<CWVAutofillControllerDelegate> delegate = _autofillController.delegate;
_autofillController = [self newAutofillController];
_autofillController.delegate = delegate;
}
[self addInternalWebViewAsSubview];
[self updateNavigationAvailability];
[self updateCurrentURLs];
[self updateTitle];
[self updateVisibleSSLStatus];
self.loading = NO;
self.estimatedProgress = 0.0;
// TODO(crbug.com/41407753): The session will not be restored until
// LoadIfNecessary call. Fix the bug and remove extra call.
if (coder) {
_webState->GetNavigationManager()->LoadIfNecessary();
}
}
// Adds the web view provided by |_webState| as a subview unless it has already.
- (void)addInternalWebViewAsSubview {
UIView* subview = _webState->GetView();
if (subview.superview == self) {
return;
}
subview.frame = self.bounds;
subview.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview:subview];
}
- (CWVBackForwardList*)backForwardList {
if (!_backForwardList) {
_backForwardList = [[CWVBackForwardList alloc]
initWithNavigationManager:_webState->GetNavigationManager()];
}
return _backForwardList;
}
- (void)updateNavigationAvailability {
self.canGoBack = _webState && _webState->GetNavigationManager()->CanGoBack();
self.canGoForward =
_webState && _webState->GetNavigationManager()->CanGoForward();
self.backForwardList.navigationManager = _webState->GetNavigationManager();
}
- (void)updateCurrentURLs {
self.lastCommittedURL = net::NSURLWithGURL(_webState->GetLastCommittedURL());
self.visibleURL = net::NSURLWithGURL(_webState->GetVisibleURL());
self.visibleLocationString = base::SysUTF16ToNSString(
url_formatter::FormatUrlForSecurityDisplay(_webState->GetVisibleURL()));
}
- (void)updateTitle {
self.title = base::SysUTF16ToNSString(_webState->GetTitle());
}
- (void)updateVisibleSSLStatus {
web::NavigationItem* visibleItem =
_webState->GetNavigationManager()->GetVisibleItem();
if (visibleItem) {
self.visibleSSLStatus =
[[CWVSSLStatus alloc] initWithInternalStatus:visibleItem->GetSSL()];
} else {
self.visibleSSLStatus = nil;
}
}
- (void)attachSecurityInterstitialHelpersToWebStateIfNecessary {
if (!_webState) {
return;
}
// Lookalike URLs should only be intercepted if handled by the delegate.
if ([_navigationDelegate respondsToSelector:@selector
(webView:handleLookalikeURLWithHandler:)]) {
LookalikeUrlTabHelper::CreateForWebState(_webState.get());
LookalikeUrlTabAllowList::CreateForWebState(_webState.get());
LookalikeUrlContainer::CreateForWebState(_webState.get());
} else {
LookalikeUrlTabHelper::RemoveFromWebState(_webState.get());
LookalikeUrlTabAllowList::RemoveFromWebState(_webState.get());
LookalikeUrlContainer::RemoveFromWebState(_webState.get());
}
// Unsafe URLs should only be intercepted if handled by the delegate.
if ([_navigationDelegate
respondsToSelector:@selector(webView:handleUnsafeURLWithHandler:)]) {
SafeBrowsingClient* client =
ios_web_view::WebViewSafeBrowsingClientFactory::GetForBrowserState(
_webState->GetBrowserState());
SafeBrowsingQueryManager::CreateForWebState(_webState.get(), client);
SafeBrowsingTabHelper::CreateForWebState(_webState.get(), client);
SafeBrowsingUrlAllowList::CreateForWebState(_webState.get());
SafeBrowsingUnsafeResourceContainer::CreateForWebState(_webState.get());
} else {
SafeBrowsingQueryManager::RemoveFromWebState(_webState.get());
SafeBrowsingTabHelper::RemoveFromWebState(_webState.get());
SafeBrowsingUrlAllowList::RemoveFromWebState(_webState.get());
SafeBrowsingUnsafeResourceContainer::RemoveFromWebState(_webState.get());
}
}
#pragma mark - Internal Methods
- (void)shutDown {
if (_webState) {
// CWVBackForwardList is unsafe to use after shutting down.
_backForwardList.navigationManager = nil;
// To handle the case where -[CWVWebView encodeRestorableStateWithCoder:] is
// called after this method, precompute the session storage so it may be
// used during encoding later.
[_serializationHelper updateStateFromWebState:_webState.get()];
if (_webStateObserver) {
_webState->RemoveObserver(_webStateObserver.get());
_webStateObserver.reset();
}
WebViewHolder::RemoveFromWebState(_webState.get());
_webState.reset();
}
}
@end