// Copyright 2013 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/extensions/api/gcm/gcm_api.h"

#include <stddef.h>

#include <algorithm>
#include <map>
#include <memory>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "chrome/browser/extensions/api/gcm/extension_gcm_app_handler.h"
#include "chrome/browser/gcm/gcm_profile_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/gcm.h"
#include "components/gcm_driver/common/gcm_message.h"
#include "components/gcm_driver/gcm_client.h"
#include "components/gcm_driver/gcm_driver.h"
#include "components/gcm_driver/gcm_profile_service.h"
#include "extensions/browser/event_router.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace {

const size_t kMaximumGcmMessageSize = 4096;  // in bytes.
const char kCollapseKey[] = "collapse_key";
const char kGoogDotRestrictedPrefix[] = "goog.";
const char kGoogleRestrictedPrefix[] = "google";

// Error messages.
const char kInvalidParameter[] =
    "Function was called with invalid parameters.";
const char kGCMDisabled[] = "GCM is currently disabled.";
const char kAsyncOperationPending[] =
    "Asynchronous operation is pending.";
const char kNetworkError[] = "Network error occurred.";
const char kServerError[] = "Server error occurred.";
const char kTtlExceeded[] = "Time-to-live exceeded.";
const char kUnknownError[] = "Unknown error occurred.";

const char* GcmResultToError(gcm::GCMClient::Result result) {
  switch (result) {
    case gcm::GCMClient::SUCCESS:
      return "";
    case gcm::GCMClient::INVALID_PARAMETER:
      return kInvalidParameter;
    case gcm::GCMClient::GCM_DISABLED:
      return kGCMDisabled;
    case gcm::GCMClient::ASYNC_OPERATION_PENDING:
      return kAsyncOperationPending;
    case gcm::GCMClient::NETWORK_ERROR:
      return kNetworkError;
    case gcm::GCMClient::SERVER_ERROR:
      return kServerError;
    case gcm::GCMClient::TTL_EXCEEDED:
      return kTtlExceeded;
    case gcm::GCMClient::UNKNOWN_ERROR:
      return kUnknownError;
    default:
      NOTREACHED() << "Unexpected value of result cannot be converted: "
                   << result;
  }
}

bool IsMessageKeyValid(const std::string& key) {
  std::string lower = base::ToLowerASCII(key);
  return !key.empty() &&
         key.compare(0, std::size(kCollapseKey) - 1, kCollapseKey) != 0 &&
         lower.compare(0, std::size(kGoogleRestrictedPrefix) - 1,
                       kGoogleRestrictedPrefix) != 0 &&
         lower.compare(0, std::size(kGoogDotRestrictedPrefix),
                       kGoogDotRestrictedPrefix) != 0;
}

}  // namespace

namespace extensions {

bool GcmApiFunction::PreRunValidation(std::string* error) {
  return IsGcmApiEnabled(error);
}

bool GcmApiFunction::IsGcmApiEnabled(std::string* error) const {
  Profile* profile = Profile::FromBrowserContext(browser_context());

  // GCM is not supported in incognito mode.
  if (profile->IsOffTheRecord()) {
    *error = "GCM is not supported in incognito mode.";
    return false;
  }

  return true;
}

gcm::GCMDriver* GcmApiFunction::GetGCMDriver() const {
  return gcm::GCMProfileServiceFactory::GetForProfile(
      Profile::FromBrowserContext(browser_context()))->driver();
}

GcmRegisterFunction::GcmRegisterFunction() = default;

GcmRegisterFunction::~GcmRegisterFunction() = default;

ExtensionFunction::ResponseAction GcmRegisterFunction::Run() {
  std::optional<api::gcm::Register::Params> params =
      api::gcm::Register::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

#if BUILDFLAG(IS_ANDROID)
  // This server API was deprecated by Firebase in 2019. Don't bother trying to
  // implement register() on Android - it will just return an error.
  // TODO(crbug.com/421235963): Consider deprecating on other platforms.
  return RespondNow(Error(GcmResultToError(gcm::GCMClient::UNKNOWN_ERROR)));
#else
  GetGCMDriver()->Register(
      extension()->id(), params->sender_ids,
      base::BindOnce(&GcmRegisterFunction::CompleteFunctionWithResult, this));

  // Register() might have returned synchronously.
  return did_respond() ? AlreadyResponded() : RespondLater();
#endif  // BUILDFLAG(IS_ANDROID)
}

void GcmRegisterFunction::CompleteFunctionWithResult(
    const std::string& registration_id,
    gcm::GCMClient::Result gcm_result) {
  base::ListValue result;
  result.Append(registration_id);

  const bool succeeded = gcm::GCMClient::SUCCESS == gcm_result;
  Respond(succeeded
              ? ArgumentList(std::move(result))
              // TODO(lazyboy): We shouldn't be using |result| in case of error.
              : ErrorWithArgumentsDoNotUse(std::move(result),
                                           GcmResultToError(gcm_result)));
}

GcmUnregisterFunction::GcmUnregisterFunction() = default;

GcmUnregisterFunction::~GcmUnregisterFunction() = default;

ExtensionFunction::ResponseAction GcmUnregisterFunction::Run() {
#if BUILDFLAG(IS_ANDROID)
  // This server API was deprecated by Firebase in 2019. Don't bother trying to
  // implement register() on Android - it will just return an error.
  // TODO(crbug.com/421235963): Consider deprecating on other platforms.
  return RespondNow(Error(GcmResultToError(gcm::GCMClient::UNKNOWN_ERROR)));
#else
  GetGCMDriver()->Unregister(
      extension()->id(),
      base::BindOnce(&GcmUnregisterFunction::CompleteFunctionWithResult, this));

  // Unregister might have responded already (synchronously).
  return did_respond() ? AlreadyResponded() : RespondLater();
#endif  // BUILDFLAG(IS_ANDROID)
}

void GcmUnregisterFunction::CompleteFunctionWithResult(
    gcm::GCMClient::Result result) {
  const bool succeeded = gcm::GCMClient::SUCCESS == result;
  Respond(succeeded ? NoArguments() : Error(GcmResultToError(result)));
}

GcmSendFunction::GcmSendFunction() = default;

GcmSendFunction::~GcmSendFunction() = default;

ExtensionFunction::ResponseAction GcmSendFunction::Run() {
  std::optional<api::gcm::Send::Params> params =
      api::gcm::Send::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);
  EXTENSION_FUNCTION_VALIDATE(
      ValidateMessageData(params->message.data.additional_properties));

  gcm::OutgoingMessage outgoing_message;
  outgoing_message.id = params->message.message_id;
  outgoing_message.data = params->message.data.additional_properties;
  if (params->message.time_to_live) {
    outgoing_message.time_to_live = *params->message.time_to_live;
  }

  GetGCMDriver()->Send(
      extension()->id(), params->message.destination_id, outgoing_message,
      base::BindOnce(&GcmSendFunction::CompleteFunctionWithResult, this));

  // Send might have already responded synchronously.
  return did_respond() ? AlreadyResponded() : RespondLater();
}

void GcmSendFunction::CompleteFunctionWithResult(
    const std::string& message_id,
    gcm::GCMClient::Result gcm_result) {
  base::ListValue result;
  result.Append(message_id);

  const bool succeeded = gcm::GCMClient::SUCCESS == gcm_result;
  Respond(succeeded
              ? ArgumentList(std::move(result))
              // TODO(lazyboy): We shouldn't be using |result| in case of error.
              : ErrorWithArgumentsDoNotUse(std::move(result),
                                           GcmResultToError(gcm_result)));
}

bool GcmSendFunction::ValidateMessageData(const gcm::MessageData& data) const {
  size_t total_size = 0u;
  for (const auto& [key, value] : data) {
    total_size += key.size() + value.size();

    if (!IsMessageKeyValid(key) || kMaximumGcmMessageSize < key.size() ||
        kMaximumGcmMessageSize < value.size() ||
        kMaximumGcmMessageSize < total_size)
      return false;
  }

  return total_size != 0;
}

GcmJsEventRouter::GcmJsEventRouter(Profile* profile) : profile_(profile) {
}

GcmJsEventRouter::~GcmJsEventRouter() = default;

void GcmJsEventRouter::OnMessage(const std::string& app_id,
                                 const gcm::IncomingMessage& message) {
  api::gcm::OnMessage::Message message_arg;
  message_arg.data.additional_properties = message.data;
  if (!message.sender_id.empty()) {
    message_arg.from = message.sender_id;
  }
  if (!message.collapse_key.empty()) {
    message_arg.collapse_key = message.collapse_key;
  }

  std::unique_ptr<Event> event(
      new Event(events::GCM_ON_MESSAGE, api::gcm::OnMessage::kEventName,
                api::gcm::OnMessage::Create(message_arg), profile_));
  EventRouter::Get(profile_)
      ->DispatchEventToExtension(app_id, std::move(event));
}

void GcmJsEventRouter::OnMessagesDeleted(const std::string& app_id) {
  std::unique_ptr<Event> event(new Event(
      events::GCM_ON_MESSAGES_DELETED, api::gcm::OnMessagesDeleted::kEventName,
      api::gcm::OnMessagesDeleted::Create(), profile_));
  EventRouter::Get(profile_)
      ->DispatchEventToExtension(app_id, std::move(event));
}

void GcmJsEventRouter::OnSendError(
    const std::string& app_id,
    const gcm::GCMClient::SendErrorDetails& send_error_details) {
  api::gcm::OnSendError::Error error;
  error.message_id = send_error_details.message_id;
  error.error_message = GcmResultToError(send_error_details.result);
  error.details.additional_properties = send_error_details.additional_data;

  std::unique_ptr<Event> event(
      new Event(events::GCM_ON_SEND_ERROR, api::gcm::OnSendError::kEventName,
                api::gcm::OnSendError::Create(error), profile_));
  EventRouter::Get(profile_)
      ->DispatchEventToExtension(app_id, std::move(event));
}

}  // namespace extensions
