chromium/ios/chrome/browser/lens_overlay/coordinator/lens_result_page_mediator.mm

// Copyright 2024 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/chrome/browser/lens_overlay/coordinator/lens_result_page_mediator.h"

#import <memory>

#import "base/functional/bind.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/lens_overlay/coordinator/lens_result_page_web_state_delegate.h"
#import "ios/chrome/browser/lens_overlay/ui/lens_result_page_consumer.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/tabs/model/tab_helper_util.h"
#import "ios/web/public/navigation/web_state_policy_decider.h"
#import "ios/web/public/navigation/web_state_policy_decider_bridge.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_delegate.h"
#import "ios/web/public/web_state_delegate_bridge.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "net/base/apple/url_conversions.h"
#import "net/base/url_util.h"
#import "url/gurl.h"

namespace {

/// Returns whether the navigation is allowed inside of the result page.
BOOL IsValidURLToOpenInResultsPage(const GURL& URL) {
  std::string_view host = URL.host_piece();
  return base::EqualsCaseInsensitiveASCII(host, "google.com") ||
         base::EqualsCaseInsensitiveASCII(host, "www.google.com");
}

}  // namespace

@interface LensResultPageMediator () <CRWWebStateDelegate,
                                      CRWWebStateObserver,
                                      CRWWebStatePolicyDecider>

@end

@implementation LensResultPageMediator {
  /// WebState for lens results.
  std::unique_ptr<web::WebState> _webState;
  /// WebState delegate from the browser.
  web::WebStateDelegate* _browserWebStateDelegate;
  /// Web state policy decider.
  std::unique_ptr<web::WebStatePolicyDeciderBridge> _policyDeciderBridge;
  /// Whether the browser is off the record.
  BOOL _isIncognito;
  /// Web state delegate.
  std::unique_ptr<web::WebStateDelegateBridge> _webStateDelegateBridge;
  /// Bridges C++ WebStateObserver methods to this mediator.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
}

- (instancetype)
     initWithWebStateParams:(const web::WebState::CreateParams&)params
    browserWebStateDelegate:(web::WebStateDelegate*)browserWebStateDelegate
                isIncognito:(BOOL)isIncognito {
  self = [super init];
  if (self) {
    _browserWebStateDelegate = browserWebStateDelegate;
    _webStateDelegateBridge =
        std::make_unique<web::WebStateDelegateBridge>(self);
    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    [self attachWebState:web::WebState::Create(params)];
    _isIncognito = isIncognito;
  }
  return self;
}

- (void)setConsumer:(id<LensResultPageConsumer>)consumer {
  _consumer = consumer;
  CHECK(_webState, kLensOverlayNotFatalUntil);
  _webState->SetWebUsageEnabled(true);
  [self.consumer setWebView:_webState->GetView()];
  [self updateBackgroundColor];
}

- (void)setWebStateDelegate:
    (id<LensResultPageWebStateDelegate>)webStateDelegate {
  _webStateDelegate = webStateDelegate;
  if (_webState) {
    [self.webStateDelegate
        lensResultPageDidChangeActiveWebState:_webState.get()];
  }
}

- (void)disconnect {
  _policyDeciderBridge.reset();
  _webState->RemoveObserver(_webStateObserverBridge.get());
  _webState.reset();
  _webStateObserverBridge.reset();
  _webStateDelegateBridge.reset();
}

#pragma mark - LensOverlayResultConsumer

- (void)loadResultsURL:(GURL)URL {
  CHECK(_webState, kLensOverlayNotFatalUntil);

  _webState->OpenURL(web::WebState::OpenURLParams(
      URL, web::Referrer(), WindowOpenDisposition::CURRENT_TAB,
      ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false));
}

#pragma mark - CRWWebStatePolicyDecider

- (void)shouldAllowRequest:(NSURLRequest*)request
               requestInfo:(web::WebStatePolicyDecider::RequestInfo)requestInfo
           decisionHandler:(PolicyDecisionHandler)decisionHandler {
  GURL URL = net::GURLWithNSURL(request.URL);
  if (requestInfo.target_frame_is_main && !IsValidURLToOpenInResultsPage(URL)) {
    decisionHandler(web::WebStatePolicyDecider::PolicyDecision::Cancel());
    OpenNewTabCommand* command =
        [[OpenNewTabCommand alloc] initWithURL:URL
                                      referrer:web::Referrer()
                                   inIncognito:_isIncognito
                                  inBackground:NO
                                      appendTo:OpenPosition::kCurrentTab];
    [self.applicationHandler openURLInNewTab:command];
  } else {
    decisionHandler(web::WebStatePolicyDecider::PolicyDecision::Allow());
  }
}

#pragma mark - CRWWebStateObserver

- (void)webState:(web::WebState*)webState
    didFinishNavigation:(web::NavigationContext*)navigationContext {
  [self updateBackgroundColor];
}

- (void)webStateDidChangeUnderPageBackgroundColor:(web::WebState*)webState {
  [self updateBackgroundColor];
}

#pragma mark - CRWWebStateDelegate

- (void)webState:(web::WebState*)webState
    contextMenuConfigurationForParams:(const web::ContextMenuParams&)params
                    completionHandler:(void (^)(UIContextMenuConfiguration*))
                                          completionHandler {
  completionHandler(nil);
  // TODO(crbug.com/349100642): Add context menu configuration.
}

- (void)webState:(web::WebState*)webState
    contextMenuWillCommitWithAnimator:
        (id<UIContextMenuInteractionCommitAnimating>)animator {
}

- (UIView*)webViewContainerForWebState:(web::WebState*)webState {
  if (CGRectIsEmpty(self.webViewContainer.frame)) {
    return nil;
  }
  return self.webViewContainer;
}

- (void)closeWebState:(web::WebState*)webState {
  // This should not happen in the result page.
  NOTREACHED(kLensOverlayNotFatalUntil);
}

#pragma mark CRWWebStateDelegate with _browserWebStateDelegate

- (web::WebState*)webState:(web::WebState*)webState
    createNewWebStateForURL:(const GURL&)URL
                  openerURL:(const GURL&)openerURL
            initiatedByUser:(BOOL)initiatedByUser {
  return _browserWebStateDelegate->CreateNewWebState(webState, URL, openerURL,
                                                     initiatedByUser);
}

- (web::WebState*)webState:(web::WebState*)webState
         openURLWithParams:(const web::WebState::OpenURLParams&)params {
  return _browserWebStateDelegate->OpenURLFromWebState(webState, params);
}

- (web::JavaScriptDialogPresenter*)javaScriptDialogPresenterForWebState:
    (web::WebState*)webState {
  return _browserWebStateDelegate->GetJavaScriptDialogPresenter(webState);
}

- (void)webState:(web::WebState*)webState
    handlePermissions:(NSArray<NSNumber*>*)permissions
      decisionHandler:(web::WebStatePermissionDecisionHandler)decisionHandler {
  _browserWebStateDelegate->HandlePermissionsDecisionRequest(
      webState, permissions, decisionHandler);
}

- (void)webState:(web::WebState*)webState
    didRequestHTTPAuthForProtectionSpace:(NSURLProtectionSpace*)protectionSpace
                      proposedCredential:(NSURLCredential*)proposedCredential
                       completionHandler:(void (^)(NSString* username,
                                                   NSString* password))handler {
  _browserWebStateDelegate->OnAuthRequired(
      webState, protectionSpace, proposedCredential, base::BindOnce(handler));
}

// This API can be used to show custom input views in the web view.
- (id<CRWResponderInputView>)webStateInputViewProvider:
    (web::WebState*)webState {
  return _browserWebStateDelegate->GetResponderInputView(webState);
}

#pragma mark - LensWebProvider

- (web::WebState*)webState {
  if (!_webState) {
    return nullptr;
  }
  return _webState.get();
}

#pragma mark - Private

/// Detaches and returns the current web state.
- (std::unique_ptr<web::WebState>)detachWebState {
  CHECK(_webState, kLensOverlayNotFatalUntil);
  _policyDeciderBridge.reset();
  _webState->RemoveObserver(_webStateObserverBridge.get());
  _webState->SetDelegate(nullptr);
  return std::move(_webState);
}

/// Attaches `webState` to the mediator.
- (void)attachWebState:(std::unique_ptr<web::WebState>)webState {
  /// Detach the current web state before attaching a new one.
  CHECK(!_webState, kLensOverlayNotFatalUntil);
  CHECK(!_policyDeciderBridge, kLensOverlayNotFatalUntil);
  _webState = std::move(webState);
  _webState->SetDelegate(_webStateDelegateBridge.get());
  _webState->AddObserver(_webStateObserverBridge.get());
  _policyDeciderBridge =
      std::make_unique<web::WebStatePolicyDeciderBridge>(_webState.get(), self);
  AttachTabHelpers(_webState.get(), TabHelperFilter::kBottomSheet);

  if (self.consumer) {
    _webState->SetWebUsageEnabled(true);
    [self.consumer setWebView:_webState->GetView()];
  }
  [self.webStateDelegate lensResultPageDidChangeActiveWebState:_webState.get()];
}

/// Updates the consumer's background color.
- (void)updateBackgroundColor {
  UIColor* backgroundColor = _webState->GetUnderPageBackgroundColor();
  if (backgroundColor) {
    [self.consumer setBackgroundColor:backgroundColor];
  }
}

#pragma mark - CRWWebStateObserver

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

  [self.webStateDelegate lensResultPageWebStateDestroyed];
}

@end