// 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 "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> {
  // Check that public API is called from the correct sequence.
  SEQUENCE_CHECKER(_sequenceChecker);
}

@property(nonatomic, assign, readonly) web::WebStateImpl* webStateImpl;

// 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 self.webStateImpl->IsCustomOpenPanelSupported() &&
             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];
}

#pragma mark - Property

- (web::WebStateImpl*)webStateImpl {
  return [self.delegate webStateImplForWebViewHandler:self];
}

#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 {
  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(self.webStateImpl->IsCustomOpenPanelSupported())
      << "-[CRWWKUIHandler "
         "webView:runOpenPanelWithParameters:initiatedByFrame:"
         "completionHandler:] was called while "
         "self.webStateImpl->IsCustomOpenPanelSupported() returned false.";
  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 (!self.webStateImpl->IsVisible()) {
    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
