// 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/device_api/device_service_impl.h"

#include <functional>
#include <optional>
#include <utility>

#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/types/expected_macros.h"
#include "build/build_config.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/device_api/device_attribute_api.h"
#include "chrome/browser/permissions/permission_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_features.h"
#include "chrome/browser/web_applications/policy/web_app_policy_constants.h"
#include "chrome/browser/web_applications/web_app_filter.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chrome/common/pref_names.h"
#include "components/permissions/features.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/permission_controller_delegate.h"
#include "content/public/browser/render_frame_host.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "url/origin.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_pref_names.h"
#include "chrome/browser/ash/app_mode/isolated_web_app/kiosk_iwa_data.h"
#include "chrome/browser/ash/app_mode/isolated_web_app/kiosk_iwa_manager.h"
#include "chrome/browser/ash/app_mode/web_app/kiosk_web_app_data.h"
#include "chrome/browser/ash/app_mode/web_app/kiosk_web_app_manager.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/common/url_constants.h"
#include "chromeos/components/kiosk/kiosk_utils.h"
#include "components/user_manager/user_manager.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

namespace {
#if BUILDFLAG(IS_CHROMEOS)

// Returns an origin of the current kiosk web app.
// Should only be called when the current user is a web kiosk.
url::Origin GetWebKioskOrigin() {
  const AccountId& account_id =
      user_manager::UserManager::Get()->GetPrimaryUser()->GetAccountId();
  CHECK(ash::KioskWebAppManager::IsInitialized());
  const ash::KioskWebAppData* app_data =
      ash::KioskWebAppManager::Get()->GetAppByAccountId(account_id);
  return url::Origin::Create(CHECK_DEREF(app_data).install_url());
}

// Returns an origin of the current kiosk isolated web app.
// Should only be called when the current user is an IWA kiosk.
url::Origin GetIwaKioskOrigin() {
  const AccountId& account_id =
      user_manager::UserManager::Get()->GetPrimaryUser()->GetAccountId();
  const ash::KioskIwaData* iwa_data =
      CHECK_DEREF(ash::KioskIwaManager::Get()).GetApp(account_id);
  return CHECK_DEREF(iwa_data).origin();
}

std::optional<url::Origin> MaybeGetCurrentKioskOrigin() {
  if (chromeos::IsWebKioskSession()) {
    return GetWebKioskOrigin();
  }
  if (chromeos::IsIwaKioskSession()) {
    return GetIwaKioskOrigin();
  }
  return std::nullopt;
}

// Check whether the target origin is the same as the main application running
// in the Kiosk session.
bool IsEqualToKioskOrigin(const url::Origin& origin) {
  std::optional<url::Origin> current_kiosk_origin =
      MaybeGetCurrentKioskOrigin();
  return current_kiosk_origin.has_value() &&
         (current_kiosk_origin.value() == origin);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

Profile* GetProfile(content::RenderFrameHost& host) {
  return Profile::FromBrowserContext(host.GetBrowserContext());
}

std::optional<std::reference_wrapper<const web_app::WebAppRegistrar>>
GetRegistrar(content::RenderFrameHost& host) {
  const web_app::WebAppProvider* web_app_provider =
      web_app::WebAppProvider::GetForWebApps(GetProfile(host));
  if (!web_app_provider) {
    return std::nullopt;
  }
  return web_app_provider->registrar_unsafe();
}

// Check whether an app with the target origin is in the WebAppRegistrar and is
// a dev mode installed IWA.
bool IsDevModeInstalledIwaOrigin(content::RenderFrameHost& host,
                                 const url::Origin& origin) {
  ASSIGN_OR_RETURN(const web_app::WebAppRegistrar& registrar,
                   GetRegistrar(host), [] { return false; });
  return registrar
      .FindBestAppWithUrlInScope(origin.GetURL(),
                                 web_app::WebAppFilter::IsDevModeIsolatedApp(),
                                 {.exclude_scope_extensions = true})
      .has_value();
}

bool IsAffiliatedUser(Profile* profile) {
#if BUILDFLAG(IS_CHROMEOS)
  const user_manager::User* user =
      ash::ProfileHelper::Get()->GetUserByProfile(profile);
  return (user != nullptr) && user->IsAffiliated();
#else
  return false;
#endif  // BUILDFLAG(IS_CHROMEOS)
}

bool IsTrustedContext(content::RenderFrameHost& host,
                      const url::Origin& origin) {
  // Do not create the service for the incognito mode.
  if (GetProfile(host)->IsIncognitoProfile()) {
    return false;
  }

#if BUILDFLAG(IS_CHROMEOS)
  if (IsRunningInAppMode()) {
    if (base::FeatureList::IsEnabled(
            permissions::features::
                kAllowMultipleOriginsForWebKioskPermissions)) {
      return IsEqualToKioskOrigin(origin) ||
             IsWebKioskOriginAllowed(GetProfile(host)->GetPrefs(),
                                     origin.GetURL());
    }

    return IsEqualToKioskOrigin(origin);
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  ASSIGN_OR_RETURN(const web_app::WebAppRegistrar& registrar,
                   GetRegistrar(host), [] { return false; });
  return registrar
      .FindBestAppWithUrlInScope(
          origin.GetURL(),
          web_app::WebAppFilter::PolicyInstalledIsolatedWebApp() |
              web_app::WebAppFilter::IsDevModeIsolatedApp(),
          {.exclude_scope_extensions = true})
      .has_value();
}

bool IsAllowedByPermissionsPolicy(content::RenderFrameHost& host) {
  return host.IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kDeviceAttributes);
}

bool IsAllowedByContentSettings(content::RenderFrameHost& host,
                                const url::Origin& origin) {
  switch (HostContentSettingsMapFactory::GetForProfile(GetProfile(host))
              ->GetContentSetting(origin.GetURL(), origin.GetURL(),
                                  ContentSettingsType::DEVICE_ATTRIBUTES)) {
    case CONTENT_SETTING_ALLOW:
      return true;
    case CONTENT_SETTING_BLOCK:
      return false;
    default:
      NOTREACHED();
  }
}
}  // namespace

DeviceServiceImpl::DeviceServiceImpl(
    content::RenderFrameHost& host,
    mojo::PendingReceiver<blink::mojom::DeviceAPIService> receiver,
    std::unique_ptr<DeviceAttributeApi> device_attribute_api)
    : DocumentService(host, std::move(receiver)),
      device_attribute_api_(std::move(device_attribute_api)) {
  Profile* const profile = GetProfile(host);
  pref_change_registrar_.Init(profile->GetPrefs());
  pref_change_registrar_.Add(
      ::prefs::kWebAppInstallForceList,
      base::BindRepeating(&DeviceServiceImpl::OnDisposingIfNeeded,
                          base::Unretained(this)));
  pref_change_registrar_.Add(
      ::prefs::kIsolatedWebAppInstallForceList,
      base::BindRepeating(&DeviceServiceImpl::OnDisposingIfNeeded,
                          base::Unretained(this)));
#if BUILDFLAG(IS_CHROMEOS)
  pref_change_registrar_.Add(
      ash::prefs::kKioskBrowserPermissionsAllowedForOrigins,
      base::BindRepeating(&DeviceServiceImpl::OnDisposingIfNeeded,
                          base::Unretained(this)));
#endif  // BUILDFLAG(IS_CHROMEOS)
  content_settings_observation_.Observe(
      HostContentSettingsMapFactory::GetForProfile(profile));
  auto& provider = CHECK_DEREF(web_app::WebAppProvider::GetForWebApps(profile));
  install_manager_observation_.Observe(&provider.install_manager());
}

DeviceServiceImpl::~DeviceServiceImpl() = default;

// static
void DeviceServiceImpl::Create(
    content::RenderFrameHost* host,
    mojo::PendingReceiver<blink::mojom::DeviceAPIService> receiver,
    std::unique_ptr<DeviceAttributeApi> device_attribute_api) {
  CHECK(host);
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  if (host->GetParentOrOuterDocumentOrEmbedder()) {
    mojo::ReportBadMessage(
        "Device Attributes are allowed only in top level frames.");
    return;
  }
  if (!IsTrustedContext(*host, host->GetLastCommittedOrigin())) {
    // Not sending bad message here since the API is always exposed to the end
    // user.
    return;
  }
  if (!IsAllowedByPermissionsPolicy(*host)) {
    mojo::ReportBadMessage(
        "Permissions policy blocks access to Device Attributes.");
    return;
  }
  // The object is bound to the lifetime of |host| and the mojo
  // connection. See DocumentService for details.
  new DeviceServiceImpl(*host, std::move(receiver),
                        std::move(device_attribute_api));
}

// static
void DeviceServiceImpl::Create(
    content::RenderFrameHost* host,
    mojo::PendingReceiver<blink::mojom::DeviceAPIService> receiver) {
  Create(host, std::move(receiver), std::make_unique<DeviceAttributeApiImpl>());
}

// static
void DeviceServiceImpl::CreateForTest(
    content::RenderFrameHost* host,
    mojo::PendingReceiver<blink::mojom::DeviceAPIService> receiver,
    std::unique_ptr<DeviceAttributeApi> device_attribute_api) {
  CHECK_IS_TEST();
  Create(host, std::move(receiver), std::move(device_attribute_api));
}

void DeviceServiceImpl::OnWebAppSourceRemoved(const webapps::AppId& app_id) {
  OnDisposingIfNeeded();
}

void DeviceServiceImpl::OnWebAppUninstalled(
    const webapps::AppId& app_id,
    webapps::WebappUninstallSource uninstall_source) {
  OnDisposingIfNeeded();
}

void DeviceServiceImpl::OnWebAppInstallManagerDestroyed() {
  install_manager_observation_.Reset();
}

void DeviceServiceImpl::OnContentSettingChanged(
    const ContentSettingsPattern& primary_pattern,
    const ContentSettingsPattern& secondary_pattern,
    ContentSettingsTypeSet content_type_set) {
  if (!content_type_set.Contains(ContentSettingsType::DEVICE_ATTRIBUTES)) {
    return;
  }
  OnDisposingIfNeeded();
}

void DeviceServiceImpl::OnDisposingIfNeeded() {
  // DeviceServiceImpl is allocated on the heap, thus it is safe to remove it
  // like this.
  if (!IsTrustedContext(render_frame_host(), origin())) {
    ResetAndDeleteThis();
  }
}

void DeviceServiceImpl::GetDirectoryId(GetDirectoryIdCallback callback) {
  GetDeviceAttribute(&DeviceAttributeApi::GetDirectoryId, std::move(callback));
}

void DeviceServiceImpl::GetHostname(GetHostnameCallback callback) {
  GetDeviceAttribute(&DeviceAttributeApi::GetHostname, std::move(callback));
}

void DeviceServiceImpl::GetSerialNumber(GetSerialNumberCallback callback) {
  GetDeviceAttribute(&DeviceAttributeApi::GetSerialNumber, std::move(callback));
}

void DeviceServiceImpl::GetAnnotatedAssetId(
    GetAnnotatedAssetIdCallback callback) {
  GetDeviceAttribute(&DeviceAttributeApi::GetAnnotatedAssetId,
                     std::move(callback));
}

void DeviceServiceImpl::GetAnnotatedLocation(
    GetAnnotatedLocationCallback callback) {
  GetDeviceAttribute(&DeviceAttributeApi::GetAnnotatedLocation,
                     std::move(callback));
}

void DeviceServiceImpl::GetDeviceAttribute(
    void (DeviceAttributeApi::*method)(DeviceAttributeCallback callback),
    DeviceAttributeCallback callback) {
  if (!IsAffiliatedUser(GetProfile(render_frame_host())) &&
      !IsDevModeInstalledIwaOrigin(render_frame_host(), origin())) {
    device_attribute_api_->ReportNotAffiliatedError(std::move(callback));
    return;
  }

  if (!IsAllowedByContentSettings(render_frame_host(), origin())) {
    device_attribute_api_->ReportNotAllowedError(std::move(callback));
    return;
  }

  (device_attribute_api_.get()->*method)(std::move(callback));
}
