chromium/chrome/browser/chromeos/extensions/echo_private/echo_private_api.cc

// Copyright 2012 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/chromeos/extensions/echo_private/echo_private_api.h"

#include <string>
#include <utility>

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/i18n/time_formatting.h"
#include "base/location.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/echo/echo_util.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/extensions/api/echo_private.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/view_type_utils.h"
#include "extensions/common/extension.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "third_party/icu/source/i18n/unicode/timezone.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/echo_private_ash.h"
#endif

#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/ui/lacros/window_utility.h"
#include "chromeos/crosapi/mojom/echo_private.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#endif

namespace echo_api = extensions::api::echo_private;

namespace chromeos {

namespace echo_offer {

void RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(prefs::kEchoCheckedOffers);
}

// Removes empty dictionaries from |dict|, potentially nested.
// Does not modify empty lists.
void RemoveEmptyValueDicts(base::Value::Dict& dict) {
  auto it = dict.begin();
  while (it != dict.end()) {
    base::Value& value = it->second;
    if (value.is_dict()) {
      base::Value::Dict& sub_dict = value.GetDict();
      RemoveEmptyValueDicts(sub_dict);
      if (sub_dict.empty()) {
        it = dict.erase(it);
        continue;
      }
    }
    it++;
  }
}

}  // namespace echo_offer

}  // namespace chromeos

EchoPrivateGetRegistrationCodeFunction::
    EchoPrivateGetRegistrationCodeFunction() {}

EchoPrivateGetRegistrationCodeFunction::
    ~EchoPrivateGetRegistrationCodeFunction() {}

ExtensionFunction::ResponseAction
EchoPrivateGetRegistrationCodeFunction::Run() {
  std::optional<echo_api::GetRegistrationCode::Params> params =
      echo_api::GetRegistrationCode::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  // Possible ECHO code type and corresponding key name in StatisticsProvider.
  const std::string kCouponType = "COUPON_CODE";
  const std::string kGroupType = "GROUP_CODE";
  std::optional<crosapi::mojom::RegistrationCodeType> type;
  if (params->type == kCouponType) {
    type = crosapi::mojom::RegistrationCodeType::kCoupon;
  } else if (params->type == kGroupType) {
    type = crosapi::mojom::RegistrationCodeType::kGroup;
  }

  if (!type) {
    return RespondNow(ArgumentList(
        echo_api::GetRegistrationCode::Results::Create(std::string())));
  }

  auto callback = base::BindOnce(
      &EchoPrivateGetRegistrationCodeFunction::RespondWithResult, this);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->echo_private_ash()
      ->GetRegistrationCode(type.value(), std::move(callback));
#else
  auto* lacros_service = chromeos::LacrosService::Get();
  if (lacros_service->IsAvailable<crosapi::mojom::EchoPrivate>() &&
      static_cast<uint32_t>(
          lacros_service->GetInterfaceVersion<crosapi::mojom::EchoPrivate>()) >=
          crosapi::mojom::EchoPrivate::kGetRegistrationCodeMinVersion) {
    lacros_service->GetRemote<crosapi::mojom::EchoPrivate>()
        ->GetRegistrationCode(type.value(), std::move(callback));
  } else {
    return RespondNow(Error("EchoPrivate unavailable."));
  }
#endif
  return RespondLater();
}

void EchoPrivateGetRegistrationCodeFunction::RespondWithResult(
    const std::string& result) {
  Respond(WithArguments(result));
}

EchoPrivateSetOfferInfoFunction::EchoPrivateSetOfferInfoFunction() {}

EchoPrivateSetOfferInfoFunction::~EchoPrivateSetOfferInfoFunction() {}

ExtensionFunction::ResponseAction EchoPrivateSetOfferInfoFunction::Run() {
  std::optional<echo_api::SetOfferInfo::Params> params =
      echo_api::SetOfferInfo::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  const std::string& service_id = params->id;
  base::Value::Dict dict = params->offer_info.additional_properties.Clone();
  chromeos::echo_offer::RemoveEmptyValueDicts(dict);

  PrefService* local_state = g_browser_process->local_state();
  ScopedDictPrefUpdate offer_update(local_state, prefs::kEchoCheckedOffers);
  offer_update->Set("echo." + service_id, std::move(dict));
  return RespondNow(NoArguments());
}

EchoPrivateGetOfferInfoFunction::EchoPrivateGetOfferInfoFunction() {}

EchoPrivateGetOfferInfoFunction::~EchoPrivateGetOfferInfoFunction() {}

ExtensionFunction::ResponseAction EchoPrivateGetOfferInfoFunction::Run() {
  std::optional<echo_api::GetOfferInfo::Params> params =
      echo_api::GetOfferInfo::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  const std::string& service_id = params->id;
  PrefService* local_state = g_browser_process->local_state();
  const base::Value::Dict& offer_infos =
      local_state->GetDict(prefs::kEchoCheckedOffers);

  const base::Value* offer_info = offer_infos.Find("echo." + service_id);
  if (!offer_info || !offer_info->is_dict()) {
    return RespondNow(Error("Not found"));
  }

  echo_api::GetOfferInfo::Results::Result result;
  result.additional_properties.Merge(offer_info->GetDict().Clone());
  return RespondNow(
      ArgumentList(echo_api::GetOfferInfo::Results::Create(result)));
}

EchoPrivateGetOobeTimestampFunction::EchoPrivateGetOobeTimestampFunction() {
}

EchoPrivateGetOobeTimestampFunction::~EchoPrivateGetOobeTimestampFunction() {
}

ExtensionFunction::ResponseAction EchoPrivateGetOobeTimestampFunction::Run() {
  chromeos::echo_util::GetOobeTimestamp(base::BindOnce(
      &EchoPrivateGetOobeTimestampFunction::RespondWithResult, this));
  return RespondLater();
}

void EchoPrivateGetOobeTimestampFunction::RespondWithResult(
    std::optional<base::Time> timestamp) {
  if (!timestamp.has_value()) {
    // Returns an empty string on error.
    Respond(WithArguments(std::string()));
    return;
  }
  std::string result = base::UnlocalizedTimeFormatWithPattern(
      timestamp.value(), "y-M-d", icu::TimeZone::getGMT());
  Respond(WithArguments(std::move(result)));
}

EchoPrivateGetUserConsentFunction::EchoPrivateGetUserConsentFunction() =
    default;

EchoPrivateGetUserConsentFunction::~EchoPrivateGetUserConsentFunction() =
    default;

ExtensionFunction::ResponseAction EchoPrivateGetUserConsentFunction::Run() {
  std::optional<echo_api::GetUserConsent::Params> params =
      echo_api::GetUserConsent::Params::Create(args());

  // Verify that the passed origin URL is valid.
  GURL service_origin = GURL(params->consent_requester.origin);
  if (!service_origin.is_valid()) {
    return RespondNow(Error("Invalid origin."));
  }

  content::WebContents* web_contents = nullptr;
  if (!params->consent_requester.tab_id) {
    web_contents = GetSenderWebContents();

    if (!web_contents || extensions::GetViewType(web_contents) !=
                             extensions::mojom::ViewType::kAppWindow) {
      return RespondNow(
          Error("Not called from an app window - the tabId is required."));
    }
  } else {
    TabStripModel* tab_strip = nullptr;
    int tab_index = -1;
    if (!extensions::ExtensionTabUtil::GetTabById(
            *params->consent_requester.tab_id, browser_context(),
            false /*incognito_enabled*/, nullptr /*browser*/, &tab_strip,
            &web_contents, &tab_index)) {
      return RespondNow(Error("Tab not found."));
    }

    // Bail out if the requested tab is not active - the dialog is modal to the
    // window, so showing it for a request from an inactive tab could be
    // misleading/confusing to the user.
    if (tab_index != tab_strip->active_index()) {
      return RespondNow(Error("Consent requested from an inactive tab."));
    }
  }

  DCHECK(web_contents);
#if BUILDFLAG(IS_CHROMEOS_ASH)
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->echo_private_ash()
      ->CheckRedeemOffersAllowed(
          web_contents->GetTopLevelNativeWindow(),
          params->consent_requester.service_name,
          params->consent_requester.origin,
          base::BindOnce(&EchoPrivateGetUserConsentFunction::Finalize, this));
#else
  auto* lacros_service = chromeos::LacrosService::Get();
  if (lacros_service->IsAvailable<crosapi::mojom::EchoPrivate>()) {
    const std::string window_id = lacros_window_utility::GetRootWindowUniqueId(
        web_contents->GetTopLevelNativeWindow());
    lacros_service->GetRemote<crosapi::mojom::EchoPrivate>()
        ->CheckRedeemOffersAllowed(
            std::move(window_id), params->consent_requester.service_name,
            params->consent_requester.origin,
            base::BindOnce(&EchoPrivateGetUserConsentFunction::Finalize, this));
  } else {
    return RespondNow(Error("EchoPrivate unavailable."));
  }
#endif
  return RespondLater();
}

void EchoPrivateGetUserConsentFunction::Finalize(bool consent) {
  Respond(WithArguments(consent));
}