// Copyright 2022 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/ash/network/network_portal_signin_controller.h"

#include "ash/public/cpp/new_window_delegate.h"
#include "base/check.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ash/profiles/signin_profile_handler.h"
#include "chrome/browser/chromeos/network/network_portal_signin_window.h"
#include "chrome/browser/prefs/incognito_mode_prefs.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/dialogs/browser_dialogs.h"
#include "chrome/browser/ui/navigator/browser_navigator.h"
#include "chrome/browser/ui/navigator/browser_navigator_params.h"
#include "chrome/browser/ui/scoped_tabbed_browser_displayer.h"
#include "chrome/browser/ui/webui/ash/floating_workspace/floating_workspace_dialog.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/ash/components/network/network_event_log.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/proxy/proxy_config_service_impl.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/constants/pref_names.h"
#include "components/captive_portal/core/captive_portal_detector.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/proxy_config/proxy_prefs.h"
#include "components/strings/grit/components_strings.h"
#include "components/user_manager/user_manager.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/views/widget/widget.h"
#include "ui/web_dialogs/web_dialog_delegate.h"

namespace ash {

namespace {

static NetworkPortalSigninController* g_instance = nullptr;

bool ProxyActive(PrefService& local_state, Profile* profile) {
  std::unique_ptr<ProxyConfigDictionary> proxy_config =
      ProxyConfigServiceImpl::GetActiveProxyConfigDictionary(
          profile->GetPrefs(), &local_state);
  if (!proxy_config) {
    return false;
  }
  ProxyPrefs::ProxyMode mode;
  proxy_config->GetMode(&mode);
  if (mode == ProxyPrefs::MODE_DIRECT) {
    return false;
  }
  NET_LOG(DEBUG) << "GetSigninMode: Proxy config mode: " << mode;
  return true;
}

class SigninWebDialogDelegate : public ui::WebDialogDelegate {
 public:
  SigninWebDialogDelegate(GURL url, bool disable_https_upgrades)
      : disable_https_upgrades_(disable_https_upgrades) {
    set_can_close(true);
    set_can_resize(false);
    set_dialog_content_url(url);
    set_dialog_modal_type(ui::mojom::ModalType::kSystem);
    set_dialog_title(l10n_util::GetStringUTF16(
        IDS_CAPTIVE_PORTAL_AUTHORIZATION_DIALOG_NAME));
    set_show_dialog_title(true);

    const float kScale = 0.8;
    set_dialog_size(gfx::ScaleToRoundedSize(
        display::Screen::Get()->GetPrimaryDisplay().size(), kScale));
  }

  ~SigninWebDialogDelegate() override = default;

  bool ShouldDisableHttpsUpgrades() const override {
    return disable_https_upgrades_;
  }

  void OnLoadingStateChanged(content::WebContents* source) override {
    NetworkHandler::Get()->network_state_handler()->RequestPortalDetection();
  }

 private:
  const bool disable_https_upgrades_;
};

}  // namespace

NetworkPortalSigninController::NetworkPortalSigninController(
    PrefService& local_state)
    : local_state_(local_state) {}

NetworkPortalSigninController::~NetworkPortalSigninController() = default;

// static
void NetworkPortalSigninController::Init(PrefService& local_state) {
  CHECK(!g_instance);
  g_instance = new NetworkPortalSigninController(local_state);
}

// static
void NetworkPortalSigninController::Shutdown() {
  CHECK(g_instance);
  delete g_instance;
  g_instance = nullptr;
}

// static
NetworkPortalSigninController* NetworkPortalSigninController::Get() {
  CHECK(g_instance);
  return g_instance;
}

void NetworkPortalSigninController::ShowSignin(SigninSource source) {
  GURL url;
  const NetworkState* default_network =
      NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
  if (!default_network) {
    // If no network is connected, do not attempt to show the signin page.
    NET_LOG(EVENT) << "Show signin mode from: " << source << ": No network.";
    return;
  }
  auto portal_state = default_network->portal_state();
  if (portal_state != NetworkState::PortalState::kPortal &&
      portal_state != NetworkState::PortalState::kPortalSuspected) {
    // If no portal signin is required, do not attempt to show the signin page.
    NET_LOG(EVENT) << "Show signin mode from: " << source << ": Network '"
                   << NetworkId(default_network)
                   << "' is in a non portal state: " << portal_state;
    return;
  }

  url = default_network->probe_url();
  if (url.is_empty()) {
    url = GURL(captive_portal::CaptivePortalDetector::GetDefaultUrl());
  }

  SigninMode mode = GetSigninMode(portal_state);
  NET_LOG(EVENT) << "Show signin mode: " << mode << " from: " << source;
  base::UmaHistogramEnumeration("Network.NetworkPortalSigninMode", mode);
  base::UmaHistogramEnumeration("Network.NetworkPortalSigninSource", source);

  signin_network_guid_ = default_network->guid();
  signin_start_time_ = base::TimeTicks::Now();
  if (!network_state_handler_observation_.IsObserving()) {
    network_state_handler_observation_.Observe(
        NetworkHandler::Get()->network_state_handler());
  }

  switch (mode) {
    case SigninMode::kSigninDialog:
    case SigninMode::kFloatingWorkspaceDialog: {
      // OOBE/Login and the Floating Workspace Dialog require the portal signin
      // UI to be shown in a dialog.
      ShowSigninDialog(url);
      break;
    }
    case SigninMode::kNormalTab:
      ShowActiveProfileTab(url);
      break;
    case SigninMode::kSigninDefault: {
      // An OTR profile will be used with extensions enabled and all proxies
      // disabled by the proxy service.
      ShowSigninWindow(url);
      break;
    }
    case SigninMode::kIncognitoDisabledByPolicy:
      ShowTab(ProfileManager::GetActiveUserProfile(), url);
      break;
    case SigninMode::kIncognitoDisabledByParentalControls: {
      // Supervised users require SupervisedUserNavigationThrottle, now
      // available on OTR profiles.
      ShowSigninWindow(url);
      break;
    }
  }
}

NetworkPortalSigninController::SigninMode
NetworkPortalSigninController::GetSigninMode(
    NetworkState::PortalState portal_state) const {
  if (!user_manager::UserManager::IsInitialized() ||
      !user_manager::UserManager::Get()->IsUserLoggedIn()) {
    NET_LOG(DEBUG) << "GetSigninMode: Not logged in";
    return SigninMode::kSigninDialog;
  }

  if (user_manager::UserManager::Get()->IsLoggedInAsAnyKioskApp()) {
    NET_LOG(DEBUG) << "GetSigninMode: Kiosk app";
    return SigninMode::kSigninDialog;
  }

  if (user_manager::UserManager::Get()->IsLoggedInAsChildUser()) {
    NET_LOG(DEBUG) << "GetSigninMode: Child User";
    return SigninMode::kIncognitoDisabledByParentalControls;
  }

  Profile* profile = ProfileManager::GetActiveUserProfile();
  if (!profile) {
    NET_LOG(DEBUG) << "GetSigninMode: No profile";
    return SigninMode::kSigninDialog;
  }

  // This pref defaults to true, but if a policy is active the policy value
  // defaults to false ("any captive portal authentication pages are shown in a
  // regular tab [if a proxy is active]").
  // Note: Generally we always want to show the portal signin UI in an OTR
  // tab to avoid providing cookies, see b/245578628 for details.
  const bool ignore_proxy = profile->GetPrefs()->GetBoolean(
      chromeos::prefs::kCaptivePortalAuthenticationIgnoresProxy);
  if (!ignore_proxy && ProxyActive(local_state_.get(), profile)) {
    return SigninMode::kNormalTab;
  }

  policy::IncognitoModeAvailability availability;
  IncognitoModePrefs::IntToAvailability(
      profile->GetPrefs()->GetInteger(
          policy::policy_prefs::kIncognitoModeAvailability),
      &availability);
  if (availability == policy::IncognitoModeAvailability::kDisabled) {
    return SigninMode::kIncognitoDisabledByPolicy;
  }

  // In case of being called from the FloatingWorkspaceDialog in session we
  // want to show the captive portal on top of the dialog, because by
  // default it will be shown in a tab behind the modal dialog.
  if (ash::FloatingWorkspaceDialog::IsShown()) {
    return SigninMode::kFloatingWorkspaceDialog;
  }

  return SigninMode::kSigninDefault;
}

void NetworkPortalSigninController::CloseSignin() {
  if (dialog_widget_) {
    dialog_widget_->Close();
  }
}

bool NetworkPortalSigninController::DialogIsShown() {
  return !!dialog_widget_;
}

void NetworkPortalSigninController::OnWidgetDestroying(views::Widget* widget) {
  if (widget != dialog_widget_) {
    return;
  }
  dialog_widget_observation_.Reset();
  dialog_widget_ = nullptr;
  SigninProfileHandler::Get()->ClearSigninProfile(base::NullCallback());
}

void NetworkPortalSigninController::PortalStateChanged(
    const NetworkState* default_network,
    NetworkState::PortalState portal_state) {
  bool is_signin_network =
      default_network && default_network->guid() == signin_network_guid_;
  if (is_signin_network && !default_network->IsOnline()) {
    // Signin network is still not online, nothing to do.
    return;
  }

  if (!signin_network_guid_.empty()) {
    // If the signin network is online, record the time since the signin UI was
    // shown. Otherwise record 0 to indicate that signin did not occur.
    base::TimeDelta elapsed;
    if (is_signin_network) {
      elapsed = base::TimeTicks::Now() - signin_start_time_;
    }
    base::UmaHistogramMediumTimes("Network.NetworkPortalSigninTime", elapsed);
    signin_network_guid_ = "";
    network_state_handler_observation_.Reset();
  }

  // If signin is using a dialog in the OOBE/login screen, close it if the
  // default network changed or became online.
  if (dialog_widget_) {
    dialog_widget_->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
  }

  // If signin is using a browser window, the user may still be using the window
  // so we don't try to close it.
}

void NetworkPortalSigninController::OnShuttingDown() {
  network_state_handler_observation_.Reset();
}

void NetworkPortalSigninController::ShowSigninDialog(const GURL& url) {
  if (dialog_widget_) {
    dialog_widget_->Show();
    return;
  }

  // Disable ABH/HTTPS-First Mode when logged out because captive portals
  // require unencrypted HTTP redirects to serve login pages. Forcing HTTPS
  // would block these redirects with SSL error. See crbug.com/493517524
  // for details.
  const bool disable_https_upgrades = ash::LoginState::IsInitialized() &&
                                      !ash::LoginState::Get()->IsUserLoggedIn();

  auto web_dialog_delegate =
      std::make_unique<SigninWebDialogDelegate>(url, disable_https_upgrades);

  dialog_widget_ = views::Widget::GetWidgetForNativeWindow(
      // ui::WebDialogDelegate is self-deleting, so pass ownership of it (as a
      // raw pointer) in here.
      chrome::ShowWebDialog(nullptr, ProfileHelper::GetSigninProfile(),
                            web_dialog_delegate.release()));
  dialog_widget_observation_.Observe(dialog_widget_.get());
}

void NetworkPortalSigninController::ShowSigninWindow(const GURL& url) {
  // Calls NetworkPortalSigninWindow::Show in the appropriate browser.
  ash::NewWindowDelegate::GetInstance()->OpenCaptivePortalSignin(url);
}

void NetworkPortalSigninController::ShowTab(Profile* profile, const GURL& url) {
  chrome::ScopedTabbedBrowserDisplayer displayer(profile);
  if (!displayer.browser_window_interface()) {
    return;
  }

  NavigateParams params(displayer.browser_window_interface(), url,
                        ui::PAGE_TRANSITION_LINK);
  // `captive_portal_window_type = kTab` is used on desktop Chrome to identify
  // captive portal signin tabs. This disables HTTPS-Upgrades for the captive
  // portal navigation.
  params.captive_portal_window_type =
      captive_portal::CaptivePortalWindowType::kTab;
  params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
  ::Navigate(&params);
}

void NetworkPortalSigninController::ShowActiveProfileTab(const GURL& url) {
  // Opens a new tab the appropriate browser.
  ash::NewWindowDelegate::GetInstance()->OpenUrl(
      url, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
}

std::ostream& operator<<(
    std::ostream& stream,
    const NetworkPortalSigninController::SigninMode& signin_mode) {
  switch (signin_mode) {
    case NetworkPortalSigninController::SigninMode::kSigninDialog:
      stream << "Signin Dialog";
      break;
    case NetworkPortalSigninController::SigninMode::kNormalTab:
      stream << "Normal Tab (proxies enabled)";
      break;
    case NetworkPortalSigninController::SigninMode::kSigninDefault:
      stream << "Signin Window";
      break;
    case NetworkPortalSigninController::SigninMode::kIncognitoDisabledByPolicy:
      stream << "Signin Window (Incognito mode disabled by policy)";
      break;
    case NetworkPortalSigninController::SigninMode::
        kIncognitoDisabledByParentalControls:
      stream << "Signin Window (Incognito mode disabled by parental controls)";
      break;
    case NetworkPortalSigninController::SigninMode::kFloatingWorkspaceDialog:
      stream << "Floating Workspace Dialog";
      break;
  }
  return stream;
}

std::ostream& operator<<(
    std::ostream& stream,
    const NetworkPortalSigninController::SigninSource& signin_source) {
  switch (signin_source) {
    case NetworkPortalSigninController::SigninSource::kNotification:
      stream << "Notification";
      break;
    case NetworkPortalSigninController::SigninSource::kSettings:
      stream << "Settings";
      break;
    case NetworkPortalSigninController::SigninSource::kQuickSettings:
      stream << "Quick Settings";
      break;
    case NetworkPortalSigninController::SigninSource::kErrorPage:
      stream << "Error page";
      break;
  }
  return stream;
}

}  // namespace ash
