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

#include "content/browser/notifications/blink_notification_service_impl.h"

#include <string>
#include <utility>

#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "content/browser/notifications/notification_event_dispatcher_impl.h"
#include "content/browser/notifications/platform_notification_context_impl.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_database_data.h"
#include "content/public/browser/permission_controller.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/platform_notification_service.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/weak_document_ptr.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "third_party/blink/public/common/notifications/notification_constants.h"
#include "third_party/blink/public/common/notifications/notification_resources.h"
#include "third_party/blink/public/common/notifications/platform_notification_data.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"

namespace content {

namespace {

const char kBadMessageInvalidNotificationTriggerTimestamp[] =
    "Received an invalid notification trigger timestamp.";
const char kBadMessageInvalidNotificationActionButtons[] =
    "Received a notification with a number of action images that does not "
    "match the number of actions.";
const char kBadMessageNonPersistentNotificationFromServiceWorker[] =
    "Received a non-persistent notification from a service worker.";
const char kBadMessageEmptyNotificationToken[] =
    "Received an empty notification token.";

bool FilterByTag(const std::string& filter_tag,
                 const NotificationDatabaseData& database_data) {
  // An empty filter tag matches all.
  if (filter_tag.empty())
    return true;
  // Otherwise we need an exact match.
  return filter_tag == database_data.notification_data.tag;
}

bool FilterByTriggered(bool include_triggered,
                       const NotificationDatabaseData& database_data) {
  // Including triggered matches all.
  if (include_triggered)
    return true;
  // Notifications without a trigger always match.
  if (!database_data.notification_data.show_trigger_timestamp)
    return true;
  // Otherwise it has to be triggered already.
  return database_data.has_triggered;
}

// Checks if this notification has a valid trigger.
bool CheckNotificationTriggerRange(
    const blink::PlatformNotificationData& data) {
  if (!data.show_trigger_timestamp)
    return true;

  base::TimeDelta show_trigger_delay =
      data.show_trigger_timestamp.value() - base::Time::Now();

  return show_trigger_delay <= blink::kMaxNotificationShowTriggerDelay;
}

}  // namespace

using blink::mojom::PersistentNotificationError;

BlinkNotificationServiceImpl::BlinkNotificationServiceImpl(
    PlatformNotificationContextImpl* notification_context,
    BrowserContext* browser_context,
    scoped_refptr<ServiceWorkerContextWrapper> service_worker_context,
    RenderProcessHost* render_process_host,
    const blink::StorageKey& storage_key,
    const GURL& document_url,
    const WeakDocumentPtr& weak_document_ptr,
    RenderProcessHost::NotificationServiceCreatorType creator_type,
    mojo::PendingReceiver<blink::mojom::NotificationService> receiver)
    : notification_context_(notification_context),
      browser_context_(browser_context),
      service_worker_context_(std::move(service_worker_context)),
      render_process_host_id_(render_process_host->GetDeprecatedID()),
      storage_key_(storage_key),
      storage_key_if_3psp_enabled(
          storage_key.CopyWithForceEnabledThirdPartyStoragePartitioning()),
      document_url_(document_url),
      weak_document_ptr_(weak_document_ptr),
      creator_type_(creator_type),
      receiver_(this, std::move(receiver)) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(notification_context_);
  DCHECK(browser_context_);

  receiver_.set_disconnect_handler(base::BindOnce(
      &BlinkNotificationServiceImpl::OnConnectionError,
      base::Unretained(this) /* the channel is owned by |this| */));
}

BlinkNotificationServiceImpl::~BlinkNotificationServiceImpl() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

void BlinkNotificationServiceImpl::OnContextShutdown() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  browser_context_ = nullptr;
}

void BlinkNotificationServiceImpl::GetPermissionStatus(
    GetPermissionStatusCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!browser_context_ ||
      !browser_context_->GetPlatformNotificationService()) {
    std::move(callback).Run(blink::mojom::PermissionStatus::DENIED);
    return;
  }

  std::move(callback).Run(CheckPermissionStatus());
}

void BlinkNotificationServiceImpl::OnConnectionError() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  notification_context_->RemoveService(this);
  // |this| has now been deleted.
}

// Since a non-persistent notification cannot be created by service workers, we
// report the bad message here and raise a connection error.
bool BlinkNotificationServiceImpl::IsValidForNonPersistentNotification() {
  switch (creator_type_) {
    case RenderProcessHost::NotificationServiceCreatorType::kDocument:
    case RenderProcessHost::NotificationServiceCreatorType::kDedicatedWorker:
    case RenderProcessHost::NotificationServiceCreatorType::kSharedWorker:
      return true;
    case RenderProcessHost::NotificationServiceCreatorType::kServiceWorker:
      receiver_.ReportBadMessage(
          kBadMessageNonPersistentNotificationFromServiceWorker);
      OnConnectionError();
      return false;
  }
}

void BlinkNotificationServiceImpl::DisplayNonPersistentNotification(
    const std::string& token,
    const blink::PlatformNotificationData& platform_notification_data,
    const blink::NotificationResources& notification_resources,
    mojo::PendingRemote<blink::mojom::NonPersistentNotificationListener>
        event_listener_remote) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!ValidateNotificationDataAndResources(platform_notification_data,
                                            notification_resources))
    return;

  if (!browser_context_ ||
      !browser_context_->GetPlatformNotificationService()) {
    return;
  }

  if (CheckPermissionStatus() != blink::mojom::PermissionStatus::GRANTED)
    return;

  if (!IsValidForNonPersistentNotification())
    return;

  if (token.empty()) {
    receiver_.ReportBadMessage(kBadMessageEmptyNotificationToken);
    OnConnectionError();
    return;
  }

  base::UmaHistogramBoolean(
      "Notifications.NonPersistentNotificationThirdPartyCount",
      storage_key_if_3psp_enabled.IsThirdPartyContext());

  std::string notification_id =
      notification_context_->notification_id_generator()
          ->GenerateForNonPersistentNotification(storage_key_.origin(), token);

  NotificationEventDispatcherImpl* event_dispatcher =
      NotificationEventDispatcherImpl::GetInstance();
  event_dispatcher->RegisterNonPersistentNotificationListener(
      notification_id, std::move(event_listener_remote), weak_document_ptr_,
      creator_type_);

  browser_context_->GetPlatformNotificationService()->DisplayNotification(
      notification_id, storage_key_.origin().GetURL(), document_url_,
      platform_notification_data, notification_resources);
}

void BlinkNotificationServiceImpl::CloseNonPersistentNotification(
    const std::string& token) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!browser_context_ ||
      !browser_context_->GetPlatformNotificationService()) {
    return;
  }

  if (CheckPermissionStatus() != blink::mojom::PermissionStatus::GRANTED)
    return;

  if (!IsValidForNonPersistentNotification())
    return;

  if (token.empty()) {
    receiver_.ReportBadMessage(kBadMessageEmptyNotificationToken);
    OnConnectionError();
    return;
  }

  std::string notification_id =
      notification_context_->notification_id_generator()
          ->GenerateForNonPersistentNotification(storage_key_.origin(), token);

  browser_context_->GetPlatformNotificationService()->CloseNotification(
      notification_id);

  // TODO(crbug.com/40398221): Pass a callback here to focus the tab
  // which created the notification, unless the event is canceled.
  NotificationEventDispatcherImpl::GetInstance()
      ->DispatchNonPersistentCloseEvent(notification_id, base::DoNothing());
}

blink::mojom::PermissionStatus
BlinkNotificationServiceImpl::CheckPermissionStatus() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!browser_context_) {
    return blink::mojom::PermissionStatus::DENIED;
  }

  if (storage_key_.origin().opaque()) {
    return blink::mojom::PermissionStatus::DENIED;
  }

  const auto permission_descriptor = content::PermissionDescriptorUtil::
      CreatePermissionDescriptorForPermissionType(
          blink::PermissionType::NOTIFICATIONS);

  // TODO(crbug.com/40637582): It is odd that a service instance can be created
  // for cross-origin subframes, yet the instance is completely oblivious of
  // whether it is serving a top-level browsing context or an embedded one.
  if (creator_type_ ==
      RenderProcessHost::NotificationServiceCreatorType::kDocument) {
    RenderFrameHost* rfh = weak_document_ptr_.AsRenderFrameHostIfValid();
    if (!rfh) {
      return blink::mojom::PermissionStatus::DENIED;
    }
    return browser_context_->GetPermissionController()
        ->GetPermissionStatusForCurrentDocument(permission_descriptor, rfh);
  } else {
    RenderProcessHost* rph = RenderProcessHost::FromID(render_process_host_id_);
    if (!rph) {
      return blink::mojom::PermissionStatus::DENIED;
    }
    return browser_context_->GetPermissionController()
        ->GetPermissionStatusForWorker(permission_descriptor, rph,
                                       storage_key_.origin());
  }
}

bool BlinkNotificationServiceImpl::ValidateNotificationDataAndResources(
    const blink::PlatformNotificationData& platform_notification_data,
    const blink::NotificationResources& notification_resources) {
  if (platform_notification_data.actions.size() !=
      notification_resources.action_icons.size()) {
    receiver_.ReportBadMessage(kBadMessageInvalidNotificationActionButtons);
    OnConnectionError();
    return false;
  }

  if (!CheckNotificationTriggerRange(platform_notification_data)) {
    receiver_.ReportBadMessage(kBadMessageInvalidNotificationTriggerTimestamp);
    OnConnectionError();
    return false;
  }
  return true;
}

void BlinkNotificationServiceImpl::DisplayPersistentNotification(
    int64_t service_worker_registration_id,
    const blink::PlatformNotificationData& platform_notification_data,
    const blink::NotificationResources& notification_resources,
    DisplayPersistentNotificationCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // The renderer should have checked and disallowed the request for fenced
  // frames and thrown an error in
  // blink::ServiceWorkerRegistrationNotifications. Report a bad message if the
  // renderer if the renderer side check didn't happen for some reason.
  scoped_refptr<ServiceWorkerRegistration> registration =
      service_worker_context_->GetLiveRegistration(
          service_worker_registration_id);
  if (registration && registration->ancestor_frame_type() ==
                          blink::mojom::AncestorFrameType::kFencedFrame) {
    mojo::ReportBadMessage("Notification is not allowed in a fenced frame");
    return;
  }

  if (!ValidateNotificationDataAndResources(platform_notification_data,
                                            notification_resources))
    return;

  if (!browser_context_ ||
      !browser_context_->GetPlatformNotificationService()) {
    std::move(callback).Run(
        PersistentNotificationError::NOTIFICATION_SERVICE_NOT_FOUND);
    return;
  }

  if (CheckPermissionStatus() != blink::mojom::PermissionStatus::GRANTED) {
    std::move(callback).Run(PersistentNotificationError::PERMISSION_DENIED);
    return;
  }

  base::UmaHistogramBoolean(
      "Notifications.PersistentNotificationThirdPartyCount",
      storage_key_if_3psp_enabled.IsThirdPartyContext());

  int64_t next_persistent_id =
      browser_context_->GetPlatformNotificationService()
          ->ReadNextPersistentNotificationId();

  NotificationDatabaseData database_data;
  database_data.origin = storage_key_.origin().GetURL();
  database_data.service_worker_registration_id = service_worker_registration_id;
  database_data.notification_data = platform_notification_data;
  database_data.notification_resources = notification_resources;

  // TODO(crbug.com/41405589): Validate resources are not too big (either
  // here or in the mojo struct traits).

  notification_context_->WriteNotificationData(
      next_persistent_id, service_worker_registration_id,
      storage_key_.origin().GetURL(), database_data,
      base::BindOnce(&BlinkNotificationServiceImpl::DidWriteNotificationData,
                     weak_factory_for_ui_.GetWeakPtr(), std::move(callback)));
}

void BlinkNotificationServiceImpl::DidWriteNotificationData(
    DisplayPersistentNotificationCallback callback,
    bool success,
    const std::string& notification_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  std::move(callback).Run(success
                              ? PersistentNotificationError::NONE
                              : PersistentNotificationError::DATABASE_ERROR);
}

void BlinkNotificationServiceImpl::ClosePersistentNotification(
    const std::string& notification_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!browser_context_ ||
      !browser_context_->GetPlatformNotificationService()) {
    return;
  }

  if (CheckPermissionStatus() != blink::mojom::PermissionStatus::GRANTED)
    return;

  notification_context_->DeleteNotificationData(
      notification_id, storage_key_.origin().GetURL(),
      /* close_notification= */ true, base::DoNothing());
}

void BlinkNotificationServiceImpl::GetNotifications(
    int64_t service_worker_registration_id,
    const std::string& filter_tag,
    bool include_triggered,
    GetNotificationsCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!browser_context_ ||
      !browser_context_->GetPlatformNotificationService() ||
      CheckPermissionStatus() != blink::mojom::PermissionStatus::GRANTED) {
    // No permission has been granted for the given origin. It is harmless to
    // try to get notifications without permission, so return empty vectors
    // indicating that no (accessible) notifications exist at this time.
    std::move(callback).Run(std::vector<std::string>(),
                            std::vector<blink::PlatformNotificationData>());
    return;
  }

  auto read_notification_data_callback =
      base::BindOnce(&BlinkNotificationServiceImpl::DidGetNotifications,
                     weak_factory_for_ui_.GetWeakPtr(), filter_tag,
                     include_triggered, std::move(callback));

  notification_context_->ReadAllNotificationDataForServiceWorkerRegistration(
      storage_key_.origin().GetURL(), service_worker_registration_id,
      std::move(read_notification_data_callback));
}

void BlinkNotificationServiceImpl::DidGetNotifications(
    const std::string& filter_tag,
    bool include_triggered,
    GetNotificationsCallback callback,
    bool success,
    const std::vector<NotificationDatabaseData>& notifications) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  std::vector<std::string> ids;
  std::vector<blink::PlatformNotificationData> datas;

  for (const NotificationDatabaseData& database_data : notifications) {
    if (FilterByTag(filter_tag, database_data) &&
        FilterByTriggered(include_triggered, database_data)) {
      ids.push_back(database_data.notification_id);
      datas.push_back(database_data.notification_data);
    }
  }

  std::move(callback).Run(std::move(ids), std::move(datas));
}

}  // namespace content
