chromium/chrome/browser/ui/webui/ash/lock_screen_reauth/lock_screen_reauth_dialogs.cc

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/webui/ash/lock_screen_reauth/lock_screen_reauth_dialogs.h"

#include <memory>
#include <string>

#include "ash/constants/ash_features.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_writer.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/ash/login/helper.h"
#include "chrome/browser/ash/login/profile_auth_data.h"
#include "chrome/browser/ash/login/ui/oobe_dialog_size_utils.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/webui/ash/lock_screen_reauth/base_lock_dialog.h"
#include "chrome/browser/ui/webui/ash/lock_screen_reauth/lock_screen_captive_portal_dialog.h"
#include "chrome/browser/ui/webui/ash/lock_screen_reauth/lock_screen_network_dialog.h"
#include "chrome/browser/ui/webui/ash/lock_screen_reauth/lock_screen_reauth_handler.h"
#include "chrome/browser/ui/webui/ash/lock_screen_reauth/lock_screen_start_reauth_ui.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/browser_resources.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/network/network_connection_handler.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "components/session_manager/core/session_manager.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_ui_data_source.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "ui/aura/window.h"
#include "ui/strings/grit/ui_strings.h"

namespace ash {

namespace {
LockScreenStartReauthDialog* g_dialog = nullptr;

bool IsDialogLoaded(bool is_loaded,
                    base::OnceClosure& on_loaded_callback,
                    base::OnceClosure callback) {
  if (is_loaded)
    return true;
  DCHECK(!on_loaded_callback);
  on_loaded_callback = std::move(callback);
  return false;
}

void OnDialogLoaded(bool& is_loaded, base::OnceClosure& on_loaded_callback) {
  if (is_loaded)
    return;
  is_loaded = true;
  if (on_loaded_callback) {
    std::move(on_loaded_callback).Run();
  }
}

}  // namespace

// Cleans up the delegate for a WebContentsModalDialogManager on destruction, or
// on WebContents destruction, whichever comes first.
class LockScreenStartReauthDialog::ModalDialogManagerCleanup
    : public content::WebContentsObserver {
 public:
  // This constructor automatically observes |web_contents| for its lifetime.
  explicit ModalDialogManagerCleanup(content::WebContents* web_contents)
      : content::WebContentsObserver(web_contents) {}
  ModalDialogManagerCleanup(const ModalDialogManagerCleanup&) = delete;
  ModalDialogManagerCleanup& operator=(const ModalDialogManagerCleanup&) =
      delete;
  ~ModalDialogManagerCleanup() override { ResetDelegate(); }

  // content::WebContentsObserver:
  void WebContentsDestroyed() override { ResetDelegate(); }

  void ResetDelegate() {
    if (!web_contents())
      return;
    web_modal::WebContentsModalDialogManager::FromWebContents(web_contents())
        ->SetDelegate(nullptr);
  }
};

// static
gfx::Size LockScreenStartReauthDialog::CalculateLockScreenReauthDialogSize(
    bool is_new_layout_enabled) {
  if (!is_new_layout_enabled) {
    return kBaseLockDialogSize;
  }

  // LockscreenReauth Dialog size should match OOBE Dialog size.
  return CalculateOobeDialogSizeForPrimaryDisplay();
}

void LockScreenStartReauthDialog::RequestMediaAccessPermission(
    content::WebContents* web_contents,
    const content::MediaStreamRequest& request,
    content::MediaResponseCallback callback) {
  // This is required for accessing the camera for SAML logins.
  MediaCaptureDevicesDispatcher::GetInstance()->ProcessMediaAccessRequest(
      web_contents, request, std::move(callback), nullptr /* extension */);
}

bool LockScreenStartReauthDialog::CheckMediaAccessPermission(
    content::RenderFrameHost* render_frame_host,
    const url::Origin& security_origin,
    blink::mojom::MediaStreamType type) {
  // This is required for accessing the camera for SAML logins.
  return MediaCaptureDevicesDispatcher::GetInstance()
      ->CheckMediaAccessPermission(render_frame_host, security_origin, type);
}

// static
void LockScreenStartReauthDialog::Show() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK(session_manager::SessionManager::Get()->IsScreenLocked());
  if (g_dialog) {
    g_dialog->Focus();
    return;
  }
  g_dialog = new LockScreenStartReauthDialog();
}

// static
LockScreenStartReauthDialog* LockScreenStartReauthDialog::GetInstance() {
  return g_dialog;
}

void LockScreenStartReauthDialog::OnProfileInitialized(Profile* profile) {
  if (!profile) {
    // TODO(b/333278160): Create some generic way to show an error on
    // the lock screen.
    LOG(ERROR) << "Failed to load lockscreen profile";
    return;
  }

  profile_ = profile->GetPrimaryOTRProfile(/*create_if_needed=*/true);
  ShowSystemDialogForBrowserContext(profile_);
  const NetworkStateInformer::State state = network_state_informer_->state();
  // Show network or captive portal screen if needed.
  // TODO(crbug.com/1237407): Handle other states in NetworkStateInformer
  // properly.
  if (state == NetworkStateInformer::OFFLINE) {
    ShowLockScreenNetworkDialog();
  } else if (state == NetworkStateInformer::CAPTIVE_PORTAL) {
    ShowLockScreenCaptivePortalDialog();
  }
}

// static
void LockScreenStartReauthDialog::Dismiss() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (g_dialog)
    g_dialog->Close();
}

// static
bool LockScreenStartReauthDialog::IsShown() {
  return g_dialog;
}

int LockScreenStartReauthDialog::GetDialogWidth() const {
  gfx::Size ret;
  GetDialogSize(&ret);
  return ret.width();
}

content::WebContents* LockScreenStartReauthDialog::GetWebContents() {
  auto* web_ui = webui();
  if (!web_ui)
    return nullptr;
  return web_ui->GetWebContents();
}

void LockScreenStartReauthDialog::DeleteLockScreenNetworkDialog() {
  if (!lock_screen_network_dialog_)
    return;
  lock_screen_network_dialog_.reset();
  if (is_network_dialog_visible_) {
    is_network_dialog_visible_ = false;
    // We end up here when user manually closes the network dialog - in this
    // case we should also close reauth dialog as it doesn't make sense to show
    // it without a network connection
    Dismiss();
  }
}

void LockScreenStartReauthDialog::OnDialogShown(content::WebUI* webui) {
  BaseLockDialog::OnDialogShown(webui);

  web_modal::WebContentsModalDialogManager::CreateForWebContents(
      webui->GetWebContents());
  web_modal::WebContentsModalDialogManager::FromWebContents(
      webui->GetWebContents())
      ->SetDelegate(this);
  modal_dialog_manager_cleanup_ =
      std::make_unique<ModalDialogManagerCleanup>(webui->GetWebContents());
}

void LockScreenStartReauthDialog::OnDialogClosed(
    const std::string& json_retval) {
  // `BaseLockDialog::OnDialogClosed` will delete this so we need to
  // move callback to a local variable here for it to be executed.
  base::ScopedClosureRunner closure(
      std::move(on_dialog_closed_callback_for_testing_));
  BaseLockDialog::OnDialogClosed(json_retval);
}

void LockScreenStartReauthDialog::DismissLockScreenNetworkDialog() {
  if (is_network_dialog_visible_ && lock_screen_network_dialog_) {
    is_network_dialog_visible_ = false;
    lock_screen_network_dialog_->Dismiss();
  }
}

void LockScreenStartReauthDialog::DismissLockScreenCaptivePortalDialog() {
  if (captive_portal_dialog_)
    captive_portal_dialog_->Dismiss();
}

void LockScreenStartReauthDialog::ShowLockScreenNetworkDialog() {
  if (lock_screen_network_dialog_)
    return;
  DCHECK(profile_);
  is_network_dialog_visible_ = true;
  lock_screen_network_dialog_ =
      std::make_unique<LockScreenNetworkDialog>(base::BindOnce(
          &LockScreenStartReauthDialog::DeleteLockScreenNetworkDialog,
          base::Unretained(this)));
  lock_screen_network_dialog_->Show(profile_);
}

void LockScreenStartReauthDialog::ShowLockScreenCaptivePortalDialog() {
  if (!captive_portal_dialog_) {
    captive_portal_dialog_ = std::make_unique<LockScreenCaptivePortalDialog>();
    OnCaptivePortalDialogReadyForTesting();
  }
  captive_portal_dialog_->Show(*profile_);
}

bool LockScreenStartReauthDialog::IsNetworkDialogLoadedForTesting(
    base::OnceClosure callback) {
  return IsDialogLoaded(is_network_dialog_loaded_for_testing_,
                        on_network_dialog_loaded_callback_for_testing_,
                        std::move(callback));
}

bool LockScreenStartReauthDialog::IsCaptivePortalDialogLoadedForTesting(
    base::OnceClosure callback) {
  return IsDialogLoaded(is_captive_portal_dialog_loaded_for_testing_,
                        on_captive_portal_dialog_loaded_callback_for_testing_,
                        std::move(callback));
}

void LockScreenStartReauthDialog::OnNetworkDialogReadyForTesting() {
  OnDialogLoaded(is_network_dialog_loaded_for_testing_,
                 on_network_dialog_loaded_callback_for_testing_);
}

void LockScreenStartReauthDialog::OnCaptivePortalDialogReadyForTesting() {
  OnDialogLoaded(is_captive_portal_dialog_loaded_for_testing_,
                 on_captive_portal_dialog_loaded_callback_for_testing_);
}

LockScreenStartReauthDialog::LockScreenStartReauthDialog()
    : BaseLockDialog(GURL(chrome::kChromeUILockScreenStartReauthURL),
                     CalculateLockScreenReauthDialogSize(
                         features::IsNewLockScreenReauthLayoutEnabled())),
      network_state_informer_(base::MakeRefCounted<NetworkStateInformer>()) {
  network_state_informer_->Init();
  scoped_observation_.Observe(network_state_informer_.get());

  HttpAuthDialog::AddObserver(this);

  enable_ash_httpauth_ = HttpAuthDialog::Enable();

  g_browser_process->profile_manager()->CreateProfileAsync(
      ProfileHelper::GetLockScreenProfileDir(),
      base::BindOnce(&LockScreenStartReauthDialog::OnProfileInitialized,
                     weak_factory_.GetWeakPtr()));
}

LockScreenStartReauthDialog::~LockScreenStartReauthDialog() {
  DCHECK_EQ(this, g_dialog);
  HttpAuthDialog::RemoveObserver(this);
  scoped_observation_.Reset();
  DeleteLockScreenNetworkDialog();
  g_dialog = nullptr;
}

void LockScreenStartReauthDialog::OnWebviewLoadAborted() {
  UpdateState(NetworkError::ERROR_REASON_FRAME_ERROR);
}

void LockScreenStartReauthDialog::UpdateState(
    NetworkError::ErrorReason reason) {
  if (is_proxy_auth_in_progress_)
    return;

  const NetworkStateInformer::State state = network_state_informer_->state();

  // If frame didn't load but we believe that we are online then we want to show
  // the network screen (mimicking behaviour of `ErrorScreen` on signin screen).
  if (reason == NetworkError::ERROR_REASON_FRAME_ERROR &&
      state == NetworkStateInformer::ONLINE) {
    ShowLockScreenNetworkDialog();
    return;
  }

  if (state == NetworkStateInformer::OFFLINE) {
    ShowLockScreenNetworkDialog();
  } else if (state == NetworkStateInformer::CAPTIVE_PORTAL) {
    ShowLockScreenCaptivePortalDialog();
  } else if (state == NetworkStateInformer::PROXY_AUTH_REQUIRED) {
    if (is_network_dialog_visible_) {
      should_reload_gaia_ = true;
    }
  } else {
    DismissLockScreenCaptivePortalDialog();
    DismissLockScreenNetworkDialog();
  }
  if (should_reload_gaia_) {
    DismissLockScreenNetworkDialog();
    LockScreenReauthHandler* reauth_handler =
        static_cast<LockScreenStartReauthUI*>(webui()->GetController())
            ->GetMainHandler();
    if (reauth_handler->IsAuthenticatorLoaded({})) {
      reauth_handler->ReloadGaia();
      should_reload_gaia_ = false;
    }
  }
}

bool LockScreenStartReauthDialog::IsLoadedForTesting(
    base::OnceClosure callback) {
  if (is_dialog_loaded_for_testing_)
    return true;
  DCHECK(!on_dialog_loaded_callback_for_testing_);
  on_dialog_loaded_callback_for_testing_ = std::move(callback);
  return false;
}

bool LockScreenStartReauthDialog::IsClosedForTesting(
    base::OnceClosure callback) {
  if (!is_dialog_loaded_for_testing_)
    return true;
  DCHECK(!on_dialog_closed_callback_for_testing_);
  on_dialog_closed_callback_for_testing_ = std::move(callback);
  return false;
}

void LockScreenStartReauthDialog::OnReadyForTesting() {
  if (is_dialog_loaded_for_testing_)
    return;
  is_dialog_loaded_for_testing_ = true;
  if (on_dialog_loaded_callback_for_testing_) {
    std::move(on_dialog_loaded_callback_for_testing_).Run();
  }
}

web_modal::WebContentsModalDialogHost*
LockScreenStartReauthDialog::GetWebContentsModalDialogHost() {
  return this;
}

gfx::Size LockScreenStartReauthDialog::GetMaximumDialogSize() {
  gfx::Size size;
  GetDialogSize(&size);
  return size;
}

gfx::NativeView LockScreenStartReauthDialog::GetHostView() const {
  return dialog_window();
}

gfx::Point LockScreenStartReauthDialog::GetDialogPosition(
    const gfx::Size& size) {
  gfx::Size host_size = GetHostView()->bounds().size();

  // Show all sub-dialogs at center-top.
  return gfx::Point(std::max(0, (host_size.width() - size.width()) / 2), 0);
}

void LockScreenStartReauthDialog::AddObserver(
    web_modal::ModalDialogHostObserver* observer) {
  modal_dialog_host_observer_list_.AddObserver(observer);
}

void LockScreenStartReauthDialog::RemoveObserver(
    web_modal::ModalDialogHostObserver* observer) {
  modal_dialog_host_observer_list_.RemoveObserver(observer);
}

void LockScreenStartReauthDialog::TransferHttpAuthCaches() {
  content::StoragePartition* webview_storage_partition =
      login::SigninPartitionManager::Factory::GetForBrowserContext(profile_)
          ->GetCurrentStoragePartition();
  if (webview_storage_partition) {
    // Transfer auth cache to system network context. This allows to preserve
    // proxy credentials between different unlock attempts.
    webview_storage_partition->GetNetworkContext()
        ->SaveHttpAuthCacheProxyEntries(base::BindOnce(
            &TransferHttpAuthCacheToSystemNetworkContext, base::DoNothing()));

    const user_manager::User* user =
        user_manager::UserManager::Get()->GetPrimaryUser();
    Profile* profile = ProfileHelper::Get()->GetProfileByUser(user);
    // Transfer auth cache to the active user's profile so that there is no need
    // to enter them again after unlocking the device.
    ProfileAuthData::TransferHttpAuthCacheProxyEntries(
        base::DoNothing(), webview_storage_partition,
        profile->GetDefaultStoragePartition());
  }
}

void LockScreenStartReauthDialog::HttpAuthDialogShown(
    content::WebContents* web_contents) {
  if (!Matches(web_contents)) {
    return;
  }
  is_proxy_auth_in_progress_ = true;
}

void LockScreenStartReauthDialog::HttpAuthDialogCancelled(
    content::WebContents* web_contents) {
  if (!Matches(web_contents)) {
    return;
  }
  ReenableNetworkUpdates();
  should_reload_gaia_ = true;
  // If proxy authentication is canceled we disconnect from current network
  // and it triggers offline state which leads to us showing network screen
  // through `LockScreenStartReauthDialog::UpdateState`.
  const std::string network_path =
      NetworkHandler::Get()->network_state_handler()->DefaultNetwork()->path();
  NetworkHandler::Get()->network_connection_handler()->DisconnectNetwork(
      network_path, base::DoNothing(), network_handler::ErrorCallback());
}

void LockScreenStartReauthDialog::HttpAuthDialogSupplied(
    content::WebContents* web_contents) {
  if (!Matches(web_contents)) {
    return;
  }
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&LockScreenStartReauthDialog::ReenableNetworkUpdates,
                     weak_factory_.GetWeakPtr()),
      kProxyAuthTimeout);

  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&LockScreenStartReauthDialog::TransferHttpAuthCaches,
                     weak_factory_.GetWeakPtr()),
      kAuthCacheTransferDelayMs);
  Focus();
}

bool LockScreenStartReauthDialog::Matches(content::WebContents* web_contents) {
  // Check that notification source is related to this dialog's web contents.
  // Otherwise we might falsely react to notifications from chrome tabs which
  // are open in the user's active session. We use NavigationController objects
  // for comparison because `LoginHandler` uses them as the source of
  // proxy-related notifications.
  return base::Contains(webui()->GetWebContents()->GetInnerWebContents(),
                        web_contents);
}

void LockScreenStartReauthDialog::ReenableNetworkUpdates() {
  is_proxy_auth_in_progress_ = false;
}

}  // namespace ash