chromium/ios/chrome/browser/web/model/web_state_delegate_browser_agent.mm

// Copyright 2021 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/web/model/web_state_delegate_browser_agent.h"

#import "base/strings/sys_string_conversions.h"
#import "components/content_settings/core/browser/host_content_settings_map.h"
#import "components/content_settings/core/common/content_settings.h"
#import "ios/chrome/browser/content_settings/model/host_content_settings_map_factory.h"
#import "ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider.h"
#import "ios/chrome/browser/dialogs/ui_bundled/nsurl_protection_space_util.h"
#import "ios/chrome/browser/overlays/model/public/overlay_callback_manager.h"
#import "ios/chrome/browser/overlays/model/public/overlay_modality.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request_queue.h"
#import "ios/chrome/browser/overlays/model/public/overlay_response.h"
#import "ios/chrome/browser/overlays/model/public/web_content_area/http_auth_overlay.h"
#import "ios/chrome/browser/overlays/model/public/web_content_area/insecure_form_overlay.h"
#import "ios/chrome/browser/permissions/model/permissions_tab_helper.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/snapshots/model/snapshot_tab_helper.h"
#import "ios/chrome/browser/supervised_user/model/supervised_user_capabilities.h"
#import "ios/chrome/browser/tab_insertion/model/tab_insertion_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/chrome/browser/web/model/blocked_popup_tab_helper.h"
#import "ios/chrome/browser/web/model/repost_form_tab_helper.h"
#import "ios/chrome/browser/web/model/web_state_container_view_provider.h"
#import "ios/components/security_interstitials/ios_blocking_page_tab_helper.h"
#import "ios/web/public/permissions/permissions.h"
#import "ios/web/public/ui/context_menu_params.h"

BROWSER_USER_DATA_KEY_IMPL(WebStateDelegateBrowserAgent)

namespace {
// Callback for HTTP authentication dialogs. This callback is a standalone
// function rather than an instance method. This is to ensure that the callback
// can be executed regardless of whether the browser agent has been destroyed.
void OnHTTPAuthOverlayFinished(web::WebStateDelegate::AuthCallback callback,
                               OverlayResponse* response) {
  if (response) {
    HTTPAuthOverlayResponseInfo* auth_info =
        response->GetInfo<HTTPAuthOverlayResponseInfo>();
    if (auth_info) {
      std::move(callback).Run(base::SysUTF8ToNSString(auth_info->username()),
                              base::SysUTF8ToNSString(auth_info->password()));
      return;
    }
  }
  std::move(callback).Run(nil, nil);
}

void OnInsecureFormWarningResponse(base::OnceCallback<void(bool)> callback,
                                   OverlayResponse* response) {
  if (response) {
    InsecureFormDialogResponse* info =
        response->GetInfo<InsecureFormDialogResponse>();
    if (info) {
      std::move(callback).Run(info->allow_send());
      return;
    }
  }
  std::move(callback).Run(false);
}

// Returns true if a supervised user attempts to access the microphone or camera
// content setting when a parent has explicitly set site settings controls to
// block permissions.
bool IsMicOrCameraAccessSubjectToParentalControls(
    ChromeBrowserState* browser_state,
    NSArray<NSNumber*>* permissions) {
  if (!browser_state ||
      !supervised_user::IsSubjectToParentalControls(browser_state)) {
    return false;
  }

  HostContentSettingsMap* host_content_settings_map =
      ios::HostContentSettingsMapFactory::GetForBrowserState(browser_state);
  CHECK(host_content_settings_map);

  ContentSetting default_mic_setting =
      host_content_settings_map->GetDefaultContentSetting(
          ContentSettingsType::MEDIASTREAM_MIC, /*provider_id=*/nullptr);
  ContentSetting default_camera_setting =
      host_content_settings_map->GetDefaultContentSetting(
          ContentSettingsType::MEDIASTREAM_CAMERA, /*provider_id=*/nullptr);

  return ([permissions containsObject:@(web::PermissionMicrophone)] &&
          default_mic_setting == ContentSetting::CONTENT_SETTING_BLOCK) ||
         ([permissions containsObject:@(web::PermissionCamera)] &&
          default_camera_setting == ContentSetting::CONTENT_SETTING_BLOCK);
}

}  // namespace

WebStateDelegateBrowserAgent::WebStateDelegateBrowserAgent(
    Browser* browser,
    TabInsertionBrowserAgent* tab_insertion_agent)
    : web_state_list_(browser->GetWebStateList()),
      tab_insertion_agent_(tab_insertion_agent) {
  DCHECK(tab_insertion_agent_);
  browser_ = browser;
  browser_observation_.Observe(browser);
  web_state_list_observation_.Observe(web_state_list_.get());

  // All the BrowserAgent are attached to the Browser during the creation,
  // the WebStateList must be empty at this point.
  DCHECK(web_state_list_->empty())
      << "WebStateDelegateBrowserAgent created for a Browser with a non-empty "
         "WebStateList.";
}

WebStateDelegateBrowserAgent::~WebStateDelegateBrowserAgent() {}

void WebStateDelegateBrowserAgent::SetUIProviders(
    ContextMenuConfigurationProvider* context_menu_provider,
    id<CRWResponderInputView> input_view_provider,
    id<WebStateContainerViewProvider> container_view_provider) {
  context_menu_provider_ = context_menu_provider;
  input_view_provider_ = input_view_provider;
  container_view_provider_ = container_view_provider;
}

void WebStateDelegateBrowserAgent::ClearUIProviders() {
  context_menu_provider_ = nil;
  input_view_provider_ = nil;
  container_view_provider_ = nil;
}

#pragma mark - WebStateListObserver

void WebStateDelegateBrowserAgent::WebStateListDidChange(
    WebStateList* web_state_list,
    const WebStateListChange& change,
    const WebStateListStatus& status) {
  switch (change.type()) {
    case WebStateListChange::Type::kStatusOnly:
      // Do nothing when a WebState is selected and its status is updated.
      break;
    case WebStateListChange::Type::kDetach: {
      const WebStateListChangeDetach& detach_change =
          change.As<WebStateListChangeDetach>();
      ClearWebStateDelegate(detach_change.detached_web_state());
      break;
    }
    case WebStateListChange::Type::kMove:
      // Do nothing when a WebState is moved.
      break;
    case WebStateListChange::Type::kReplace: {
      const WebStateListChangeReplace& replace_change =
          change.As<WebStateListChangeReplace>();
      ClearWebStateDelegate(replace_change.replaced_web_state());
      SetWebStateDelegate(replace_change.inserted_web_state());
      break;
    }
    case WebStateListChange::Type::kInsert: {
      const WebStateListChangeInsert& insert_change =
          change.As<WebStateListChangeInsert>();
      SetWebStateDelegate(insert_change.inserted_web_state());
      break;
    }
    case WebStateListChange::Type::kGroupCreate:
      // Do nothing when a group is created.
      break;
    case WebStateListChange::Type::kGroupVisualDataUpdate:
      // Do nothing when a tab group's visual data are updated.
      break;
    case WebStateListChange::Type::kGroupMove:
      // Do nothing when a tab group is moved.
      break;
    case WebStateListChange::Type::kGroupDelete:
      // Do nothing when a group is deleted.
      break;
  }
}

#pragma mark - BrowserObserver

void WebStateDelegateBrowserAgent::BrowserDestroyed(Browser* browser) {
  DCHECK(browser_observation_.IsObservingSource(browser));

  WebStateList* web_state_list = browser->GetWebStateList();
  DCHECK(web_state_list_observation_.IsObservingSource(web_state_list));
  DCHECK_EQ(web_state_list_, web_state_list);

  // Remove all web state delegates.
  for (int index = 0; index < web_state_list_->count(); ++index)
    web_state_list_->GetWebStateAt(index)->SetDelegate(nullptr);

  web_state_observations_.RemoveAllObservations();
  web_state_list_observation_.Reset();
  browser_observation_.Reset();
}

#pragma mark - WebStateObserver

void WebStateDelegateBrowserAgent::WebStateRealized(web::WebState* web_state) {
  SetWebStateDelegate(web_state);
  web_state_observations_.RemoveObservation(web_state);
}

void WebStateDelegateBrowserAgent::WebStateDestroyed(web::WebState* web_state) {
  web_state_observations_.RemoveObservation(web_state);
}

// WebStateDelegate::
web::WebState* WebStateDelegateBrowserAgent::CreateNewWebState(
    web::WebState* source,
    const GURL& url,
    const GURL& opener_url,
    bool initiated_by_user) {
  // Under some circumstances, this callback may be triggered from WebKit
  // synchronously as part of handling some other WebStateList mutation
  // (typically deleting a WebState and then activating another as a side
  // effect). See crbug.com/988504 for details. In this case, the request to
  // create a new WebState is silently dropped.
  if (web_state_list_->IsMutating())
    return nullptr;

  // Check if requested web state is a popup and block it if necessary.
  if (!initiated_by_user) {
    auto* helper = BlockedPopupTabHelper::GetOrCreateForWebState(source);
    if (helper->ShouldBlockPopup(opener_url)) {
      // It's possible for a page to inject a popup into a window created via
      // window.open before its initial load is committed.  Rather than relying
      // on the last committed or pending NavigationItem's referrer policy, just
      // use ReferrerPolicyDefault.
      // TODO(crbug.com/41317904): Update this to a more appropriate referrer
      // policy once referrer policies are correctly recorded in
      // NavigationItems.
      web::Referrer referrer(opener_url, web::ReferrerPolicyDefault);
      helper->HandlePopup(url, referrer);
      return nullptr;
    }
  }

  // Requested web state should not be blocked from opening.
  SnapshotTabHelper::FromWebState(source)->UpdateSnapshotWithCallback(nil);

  return tab_insertion_agent_->InsertWebStateOpenedByDOM(source);
}

void WebStateDelegateBrowserAgent::CloseWebState(web::WebState* source) {
  int index = web_state_list_->GetIndexOfWebState(source);
  if (index != WebStateList::kInvalidIndex)
    web_state_list_->CloseWebStateAt(index, WebStateList::CLOSE_USER_ACTION);
}

web::WebState* WebStateDelegateBrowserAgent::OpenURLFromWebState(
    web::WebState* source,
    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;

  TabInsertion::Params insertion_params;
  insertion_params.parent = source;

  switch (params.disposition) {
    case WindowOpenDisposition::NEW_FOREGROUND_TAB:
    case WindowOpenDisposition::NEW_BACKGROUND_TAB: {
      insertion_params.in_background =
          params.disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB;
      return tab_insertion_agent_->InsertWebState(load_params,
                                                  insertion_params);
    }
    case WindowOpenDisposition::CURRENT_TAB: {
      source->GetNavigationManager()->LoadURLWithParams(load_params);
      return source;
    }
    case WindowOpenDisposition::NEW_POPUP: {
      insertion_params.opened_by_dom = true;
      return tab_insertion_agent_->InsertWebState(load_params,
                                                  insertion_params);
    }
    default:
      NOTIMPLEMENTED();
      return nullptr;
  };
}

void WebStateDelegateBrowserAgent::ShowRepostFormWarningDialog(
    web::WebState* source,
    web::FormWarningType warning_type,
    base::OnceCallback<void(bool)> callback) {
  CHECK_NE(warning_type, web::FormWarningType::kNone);
  if (!container_view_provider_) {
    // There's no way to show the dialog so treat it as if the user said no.
    std::move(callback).Run(false);
    return;
  }

  switch (warning_type) {
    case web::FormWarningType::kRepost:
      // TODO(crbug.com/40203973) : Clean up this API.
      RepostFormTabHelper::FromWebState(source)->PresentDialog(
          [container_view_provider_ dialogLocation], std::move(callback));
      return;

    case web::FormWarningType::kInsecureForm: {
      // Show the insecure form warning overlay.
      std::unique_ptr<OverlayRequest> request =
          OverlayRequest::CreateWithConfig<InsecureFormOverlayRequestConfig>();
      request->GetCallbackManager()->AddCompletionCallback(
          base::BindOnce(&OnInsecureFormWarningResponse, std::move(callback)));
      OverlayRequestQueue::FromWebState(source,
                                        OverlayModality::kWebContentArea)
          ->AddRequest(std::move(request));
      return;
    }

    case web::FormWarningType::kNone:
      NOTREACHED_IN_MIGRATION();
  }
}

web::JavaScriptDialogPresenter*
WebStateDelegateBrowserAgent::GetJavaScriptDialogPresenter(
    web::WebState* source) {
  return &java_script_dialog_presenter_;
}

void WebStateDelegateBrowserAgent::HandlePermissionsDecisionRequest(
    web::WebState* source,
    NSArray<NSNumber*>* permissions,
    web::WebStatePermissionDecisionHandler handler) {
  ChromeBrowserState* chrome_browser_state =
      ChromeBrowserState::FromBrowserState(source->GetBrowserState());
  // For supervised users, sites can be denied permission to access camera or
  // mic by default. In this case, we do not show the dialog.
  if (IsMicOrCameraAccessSubjectToParentalControls(chrome_browser_state,
                                                   permissions)) {
    handler(web::PermissionDecisionDeny);
    return;
  }

  PermissionsTabHelper::FromWebState(source)
      ->PresentPermissionsDecisionDialogWithCompletionHandler(permissions,
                                                              handler);
}

void WebStateDelegateBrowserAgent::OnAuthRequired(
    web::WebState* source,
    NSURLProtectionSpace* protection_space,
    NSURLCredential* proposed_credential,
    web::WebStateDelegate::AuthCallback callback) {
  std::string message = base::SysNSStringToUTF8(
      nsurlprotectionspace_util::MessageForHTTPAuth(protection_space));
  std::string default_username;
  if (proposed_credential.user)
    default_username = base::SysNSStringToUTF8(proposed_credential.user);
  std::unique_ptr<OverlayRequest> request =
      OverlayRequest::CreateWithConfig<HTTPAuthOverlayRequestConfig>(
          nsurlprotectionspace_util::RequesterOrigin(protection_space), message,
          default_username);
  request->GetCallbackManager()->AddCompletionCallback(
      base::BindOnce(&OnHTTPAuthOverlayFinished, std::move(callback)));
  OverlayRequestQueue::FromWebState(source, OverlayModality::kWebContentArea)
      ->AddRequest(std::move(request));
}

UIView* WebStateDelegateBrowserAgent::GetWebViewContainer(
    web::WebState* source) {
  return [container_view_provider_ containerView];
}

void WebStateDelegateBrowserAgent::ContextMenuConfiguration(
    web::WebState* source,
    const web::ContextMenuParams& params,
    void (^completion_handler)(UIContextMenuConfiguration*)) {
  UIContextMenuConfiguration* configuration =
      [context_menu_provider_ contextMenuConfigurationForWebState:source
                                                           params:params];
  completion_handler(configuration);
}

void WebStateDelegateBrowserAgent::ContextMenuWillCommitWithAnimator(
    web::WebState* source,
    id<UIContextMenuInteractionCommitAnimating> animator) {
  GURL url_to_load = [context_menu_provider_ URLToLoad];
  if (!url_to_load.is_valid())
    return;

  UrlLoadParams params = UrlLoadParams::InCurrentTab(url_to_load);
  UrlLoadingBrowserAgent::FromBrowser(browser_)->Load(params);
}

id<CRWResponderInputView> WebStateDelegateBrowserAgent::GetResponderInputView(
    web::WebState* source) {
  return input_view_provider_;
}

void WebStateDelegateBrowserAgent::SetWebStateDelegate(
    web::WebState* web_state) {
  DCHECK(web_state);
  if (web_state->IsRealized()) {
    web_state->SetDelegate(this);
  } else {
    web_state_observations_.AddObservation(web_state);
  }
}

void WebStateDelegateBrowserAgent::ClearWebStateDelegate(
    web::WebState* web_state) {
  DCHECK(web_state);
  if (web_state->IsRealized()) {
    web_state->SetDelegate(nullptr);
  } else {
    web_state_observations_.RemoveObservation(web_state);
  }
}