blob: 88f79ec85ed96e7df6802192fcc2a40c4f9530b5 [file] [log] [blame]
// Copyright 2019 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/web_state/ui/crw_wk_ui_handler.h"
#import "base/logging.h"
#import "base/metrics/histogram_functions.h"
#import "base/sequence_checker.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "ios/web/common/features.h"
#import "ios/web/navigation/wk_navigation_action_util.h"
#import "ios/web/navigation/wk_navigation_util.h"
#import "ios/web/public/permissions/permissions.h"
#import "ios/web/public/ui/context_menu_params.h"
#import "ios/web/public/web_client.h"
#import "ios/web/util/wk_security_origin_util.h"
#import "ios/web/web_state/ui/crw_media_capture_permission_request.h"
#import "ios/web/web_state/ui/crw_wk_ui_handler_delegate.h"
#import "ios/web/web_state/user_interaction_state.h"
#import "ios/web/web_state/web_state_impl.h"
#import "ios/web/webui/mojo_facade.h"
#import "net/base/apple/url_conversions.h"
#import "url/gurl.h"
#import "url/origin.h"
namespace {
// Values for UMA permission histograms. These values are based on
// WKMediaCaptureType and persisted to logs. Entries should not be renumbered
// and numeric values should never be reused.
enum class PermissionRequest {
RequestCamera = 0,
RequestMicrophone = 1,
RequestCameraAndMicrophone = 2,
kMaxValue = RequestCameraAndMicrophone,
};
// Records permission histogram enum for `media_capture_type` on UMA.
void RecordHistogramForPermissionRequestForWKMediaCaptureType(
WKMediaCaptureType media_capture_type) {
PermissionRequest type;
switch (media_capture_type) {
case WKMediaCaptureTypeCamera:
type = PermissionRequest::RequestCamera;
break;
case WKMediaCaptureTypeMicrophone:
type = PermissionRequest::RequestMicrophone;
break;
case WKMediaCaptureTypeCameraAndMicrophone:
type = PermissionRequest::RequestCameraAndMicrophone;
break;
}
base::UmaHistogramEnumeration("IOS.Permission.Requests", type);
}
} // namespace
@interface CRWWKUIHandler () <CRWMediaCapturePermissionPresenter> {
// Backs up property with the same name.
std::unique_ptr<web::MojoFacade> _mojoFacade;
// Check that public API is called from the correct sequence.
SEQUENCE_CHECKER(_sequenceChecker);
}
@property(nonatomic, assign, readonly) web::WebStateImpl* webStateImpl;
// Facade for Mojo API.
@property(nonatomic, readonly) web::MojoFacade* mojoFacade;
// Task runner that creates this object.
@property(nonatomic, readonly) scoped_refptr<base::SequencedTaskRunner>
mainTaskRunner;
@end
@implementation CRWWKUIHandler
- (instancetype)init {
if ((self = [super init])) {
_mainTaskRunner = base::SequencedTaskRunner::GetCurrentDefault();
CHECK(_mainTaskRunner);
}
return self;
}
#pragma mark - NSObject
// Overriden to return NO for
// -webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:
// if there is no delegate or `delegate->CanRunOpenPanel()` returns false.
- (BOOL)respondsToSelector:(SEL)selector {
SEL runOpenPanelWithParametersSelector = @selector
(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:);
if (selector == runOpenPanelWithParametersSelector) {
if (@available(iOS 18.4, *)) {
return web::GetWebClient()->CanRunOpenPanel(self.webStateImpl);
} else {
NOTREACHED() << "@selector(-webView:runOpenPanelWithParameters:"
"initiatedByFrame:completionHandler:) only exists on "
"18.4+ so it should not be used in former versions.";
}
}
return [super respondsToSelector:selector];
}
#pragma mark - CRWWebViewHandler
- (void)close {
[super close];
_mojoFacade.reset();
}
#pragma mark - Property
- (web::WebStateImpl*)webStateImpl {
return [self.delegate webStateImplForWebViewHandler:self];
}
- (web::MojoFacade*)mojoFacade {
if (!_mojoFacade) {
_mojoFacade = std::make_unique<web::MojoFacade>(self.webStateImpl);
}
return _mojoFacade.get();
}
#pragma mark - WKUIDelegate
- (void)webView:(WKWebView*)webView
requestMediaCapturePermissionForOrigin:(WKSecurityOrigin*)origin
initiatedByFrame:(WKFrameInfo*)frame
type:(WKMediaCaptureType)type
decisionHandler:
(void (^)(WKPermissionDecision decision))
decisionHandler {
RecordHistogramForPermissionRequestForWKMediaCaptureType(type);
CRWMediaCapturePermissionRequest* request =
[[CRWMediaCapturePermissionRequest alloc]
initWithDecisionHandler:decisionHandler
onTaskRunner:self.mainTaskRunner];
request.presenter = self;
GURL securityOrigin = web::GURLOriginWithWKSecurityOrigin(origin);
if (web::GetWebClient()->EnableFullscreenAPI()) {
if (@available(iOS 16, *)) {
if (webView.fullscreenState == WKFullscreenStateInFullscreen ||
webView.fullscreenState == WKFullscreenStateEnteringFullscreen) {
[webView closeAllMediaPresentationsWithCompletionHandler:^{
[request displayPromptForMediaCaptureType:type origin:securityOrigin];
}];
return;
}
}
}
[request displayPromptForMediaCaptureType:type origin:securityOrigin];
}
- (WKWebView*)webView:(WKWebView*)webView
createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration
forNavigationAction:(WKNavigationAction*)action
windowFeatures:(WKWindowFeatures*)windowFeatures {
// Do not create windows for non-empty invalid URLs.
GURL requestURL = net::GURLWithNSURL(action.request.URL);
if (!requestURL.is_empty() && !requestURL.is_valid()) {
DLOG(WARNING) << "Unable to open a window with invalid URL: "
<< requestURL.possibly_invalid_spec();
return nil;
}
NSString* referrer = [action.request
valueForHTTPHeaderField:web::wk_navigation_util::kReferrerHeaderName];
GURL openerURL = referrer.length
? GURL(base::SysNSStringToUTF8(referrer))
: [self.delegate documentURLForWebViewHandler:self];
// There is no reliable way to tell if there was a user gesture, so this code
// checks if user has recently tapped on web view. TODO(crbug.com/40561701):
// Remove the usage of -userIsInteracting when rdar://19989909 is fixed.
bool initiatedByUser = [self.delegate UIHandler:self
isUserInitiatedAction:action];
if (UIAccessibilityIsVoiceOverRunning()) {
// -userIsInteracting returns NO if VoiceOver is On. Inspect action's
// description, which may contain the information about user gesture for
// certain link clicks.
initiatedByUser = initiatedByUser ||
web::GetNavigationActionInitiationTypeWithVoiceOverOn(
action.description) ==
web::NavigationActionInitiationType::kUserInitiated;
}
web::WebState* childWebState = self.webStateImpl->CreateNewWebState(
requestURL, openerURL, initiatedByUser);
if (!childWebState) {
return nil;
}
// WKWebView requires WKUIDelegate to return a child view created with
// exactly the same `configuration` object (exception is raised if config is
// different). `configuration` param and config returned by
// WKWebViewConfigurationProvider are different objects because WKWebView
// makes a shallow copy of the config inside init, so every WKWebView
// owns a separate shallow copy of WKWebViewConfiguration.
WKWebView* newWebView = [self.delegate UIHandler:self
createWebViewWithConfiguration:configuration
forWebState:childWebState];
if (childWebState->GetDelegate()) {
childWebState->GetDelegate()->OnNewWebViewCreated(childWebState);
}
return newWebView;
}
- (void)webViewDidClose:(WKWebView*)webView {
// This is triggered by a JavaScript `close()` method call, only if the tab
// was opened using `window.open`. WebKit is checking that this is the case,
// so we can close the tab unconditionally here.
if (self.webStateImpl) {
__weak __typeof(self) weakSelf = self;
// -webViewDidClose will typically trigger another webState to activate,
// which may in turn also close. To prevent reentrant modificationre in
// WebStateList, trigger a PostTask here.
self.mainTaskRunner->PostTask(FROM_HERE, base::BindOnce(^{
web::WebStateImpl* webStateImpl =
weakSelf.webStateImpl;
if (webStateImpl) {
webStateImpl->CloseWebState();
}
}));
}
}
- (void)webView:(WKWebView*)webView
runJavaScriptAlertPanelWithMessage:(NSString*)message
initiatedByFrame:(WKFrameInfo*)frame
completionHandler:(void (^)())completionHandler {
DCHECK(completionHandler);
GURL requestURL = net::GURLWithNSURL(frame.request.URL);
if (![self shouldPresentJavaScriptDialogForRequestURL:requestURL
isMainFrame:frame.mainFrame]) {
completionHandler();
return;
}
url::Origin origin = web::OriginWithWKSecurityOrigin(frame.securityOrigin);
self.webStateImpl->RunJavaScriptAlertDialog(
origin, message, base::BindOnce(completionHandler));
}
- (void)webView:(WKWebView*)webView
runJavaScriptConfirmPanelWithMessage:(NSString*)message
initiatedByFrame:(WKFrameInfo*)frame
completionHandler:
(void (^)(BOOL result))completionHandler {
DCHECK(completionHandler);
GURL requestURL = net::GURLWithNSURL(frame.request.URL);
if (![self shouldPresentJavaScriptDialogForRequestURL:requestURL
isMainFrame:frame.mainFrame]) {
completionHandler(NO);
return;
}
url::Origin origin = web::OriginWithWKSecurityOrigin(frame.securityOrigin);
self.webStateImpl->RunJavaScriptConfirmDialog(
origin, message, base::BindOnce(completionHandler));
}
- (void)webView:(WKWebView*)webView
runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt
defaultText:(NSString*)defaultText
initiatedByFrame:(WKFrameInfo*)frame
completionHandler:
(void (^)(NSString* result))completionHandler {
GURL origin_url(web::GURLOriginWithWKSecurityOrigin(frame.securityOrigin));
if (web::GetWebClient()->IsAppSpecificURL(origin_url)) {
std::string mojoResponse =
self.mojoFacade->HandleMojoMessage(base::SysNSStringToUTF8(prompt));
completionHandler(base::SysUTF8ToNSString(mojoResponse));
return;
}
DCHECK(completionHandler);
GURL requestURL = net::GURLWithNSURL(frame.request.URL);
if (![self shouldPresentJavaScriptDialogForRequestURL:requestURL
isMainFrame:frame.mainFrame]) {
completionHandler(nil);
return;
}
url::Origin origin = web::OriginWithWKSecurityOrigin(frame.securityOrigin);
self.webStateImpl->RunJavaScriptPromptDialog(
origin, prompt, defaultText, base::BindOnce(completionHandler));
}
- (void)webView:(WKWebView*)webView
contextMenuConfigurationForElement:(WKContextMenuElementInfo*)elementInfo
completionHandler:
(void (^)(UIContextMenuConfiguration* _Nullable))
completionHandler {
web::WebStateDelegate* delegate = self.webStateImpl->GetDelegate();
if (!delegate) {
completionHandler(nil);
return;
}
web::ContextMenuParams params;
params.link_url = net::GURLWithNSURL(elementInfo.linkURL);
delegate->ContextMenuConfiguration(self.webStateImpl, params,
completionHandler);
}
- (void)webView:(WKWebView*)webView
contextMenuForElement:(WKContextMenuElementInfo*)elementInfo
willCommitWithAnimator:
(id<UIContextMenuInteractionCommitAnimating>)animator {
web::WebStateDelegate* delegate = self.webStateImpl->GetDelegate();
if (!delegate) {
return;
}
delegate->ContextMenuWillCommitWithAnimator(self.webStateImpl, animator);
}
- (void)webView:(WKWebView*)webView
runOpenPanelWithParameters:(WKOpenPanelParameters*)parameters
initiatedByFrame:(WKFrameInfo*)frame
completionHandler:(void (^)(NSArray<NSURL*>*))completionHandler
API_AVAILABLE(ios(18.4)) {
CHECK(web::GetWebClient()->CanRunOpenPanel(self.webStateImpl))
<< "-[CRWWKUIHandler "
"webView:runOpenPanelWithParameters:initiatedByFrame:"
"completionHandler:] was called while "
"web::GetWebClient()->CanRunOpenPanel() returned false.";
web::GetWebClient()->RunOpenPanel(self.webStateImpl, parameters, frame,
base::BindOnce(completionHandler));
}
#pragma mark - CRWMediaCapturePermissionPresenter
- (web::WebStateImpl*)presentingWebState {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
return self.webStateImpl;
}
#pragma mark - Helper
// Helper that returns whether or not a dialog should be presented for a
// frame with `requestURL`.
- (BOOL)shouldPresentJavaScriptDialogForRequestURL:(const GURL&)requestURL
isMainFrame:(BOOL)isMainFrame {
DCHECK_CALLED_ON_VALID_SEQUENCE(_sequenceChecker);
// JavaScript dialogs should not be presented if there is no information about
// the requesting page's URL.
if (!requestURL.is_valid()) {
return NO;
}
if (isMainFrame && url::Origin::Create(self.webStateImpl->GetVisibleURL()) !=
url::Origin::Create(requestURL)) {
// Dialog was requested by web page's main frame, but visible URL has
// different origin. This could happen if the user has started a new
// browser initiated navigation. There is no value in showing dialogs
// requested by page, which this WebState is about to leave. But presenting
// the dialog can lead to phishing and other abusive behaviors.
return NO;
}
return YES;
}
@end