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

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

#include <utility>

#include "base/callback.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "content/browser/bad_message.h"
#include "content/browser/notifications/notification_event_dispatcher_impl.h"
#include "content/browser/notifications/notification_id_generator.h"
#include "content/browser/notifications/platform_notification_context_impl.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/common/platform_notification_messages.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/notification_database_data.h"
#include "content/public/browser/platform_notification_service.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "third_party/WebKit/public/platform/modules/notifications/WebNotificationConstants.h"

namespace content {

namespace {

const int kMinimumVibrationDurationMs = 1;      // 1 millisecond
const int kMaximumVibrationDurationMs = 10000;  // 10 seconds

PlatformNotificationData SanitizeNotificationData(
    const PlatformNotificationData& notification_data) {
  PlatformNotificationData sanitized_data = notification_data;

  // Make sure that the vibration values are within reasonable bounds.
  for (int& pattern : sanitized_data.vibration_pattern) {
    pattern = std::min(kMaximumVibrationDurationMs,
                       std::max(kMinimumVibrationDurationMs, pattern));
  }

  // Ensure there aren't more actions than supported.
  if (sanitized_data.actions.size() > blink::kWebNotificationMaxActions)
    sanitized_data.actions.resize(blink::kWebNotificationMaxActions);

  return sanitized_data;
}

// Returns true when |resources| looks ok, false otherwise.
bool ValidateNotificationResources(const NotificationResources& resources) {
  if (!resources.image.drawsNothing() &&
      !base::FeatureList::IsEnabled(features::kNotificationContentImage)) {
    return false;
  }
  if (resources.image.width() > blink::kWebNotificationMaxImageWidthPx ||
      resources.image.height() > blink::kWebNotificationMaxImageHeightPx) {
    return false;
  }
  if (resources.notification_icon.width() >
          blink::kWebNotificationMaxIconSizePx ||
      resources.notification_icon.height() >
          blink::kWebNotificationMaxIconSizePx) {
    return false;
  }
  if (resources.badge.width() > blink::kWebNotificationMaxBadgeSizePx ||
      resources.badge.height() > blink::kWebNotificationMaxBadgeSizePx) {
    return false;
  }
  for (const auto& action_icon : resources.action_icons) {
    if (action_icon.width() > blink::kWebNotificationMaxActionIconSizePx ||
        action_icon.height() > blink::kWebNotificationMaxActionIconSizePx) {
      return false;
    }
  }
  return true;
}

}  // namespace

NotificationMessageFilter::NotificationMessageFilter(
    int process_id,
    PlatformNotificationContextImpl* notification_context,
    ResourceContext* resource_context,
    const scoped_refptr<ServiceWorkerContextWrapper>& service_worker_context,
    BrowserContext* browser_context)
    : BrowserMessageFilter(PlatformNotificationMsgStart),
      process_id_(process_id),
      non_persistent__notification_shown_(false),
      notification_context_(notification_context),
      resource_context_(resource_context),
      service_worker_context_(service_worker_context),
      browser_context_(browser_context),
      weak_factory_io_(this) {}

NotificationMessageFilter::~NotificationMessageFilter() = default;

void NotificationMessageFilter::DidCloseNotification(
    const std::string& notification_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  close_closures_.erase(notification_id);
}

void NotificationMessageFilter::OnDestruct() const {
  if (non_persistent__notification_shown_) {
    NotificationEventDispatcherImpl* event_dispatcher =
        content::NotificationEventDispatcherImpl::GetInstance();
    DCHECK(event_dispatcher);
    event_dispatcher->RendererGone(process_id_);
  }
  BrowserThread::DeleteOnIOThread::Destruct(this);
}

bool NotificationMessageFilter::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(NotificationMessageFilter, message)
    IPC_MESSAGE_HANDLER(PlatformNotificationHostMsg_Show,
                        OnShowPlatformNotification)
    IPC_MESSAGE_HANDLER(PlatformNotificationHostMsg_ShowPersistent,
                        OnShowPersistentNotification)
    IPC_MESSAGE_HANDLER(PlatformNotificationHostMsg_GetNotifications,
                        OnGetNotifications)
    IPC_MESSAGE_HANDLER(PlatformNotificationHostMsg_Close,
                        OnClosePlatformNotification)
    IPC_MESSAGE_HANDLER(PlatformNotificationHostMsg_ClosePersistent,
                        OnClosePersistentNotification)
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()

  return handled;
}

void NotificationMessageFilter::OverrideThreadForMessage(
    const IPC::Message& message,
    content::BrowserThread::ID* thread) {
  if (message.type() == PlatformNotificationHostMsg_Show::ID ||
      message.type() == PlatformNotificationHostMsg_Close::ID)
    *thread = BrowserThread::UI;
}

void NotificationMessageFilter::OnShowPlatformNotification(
    int non_persistent_notification_id,
    const GURL& origin,
    const PlatformNotificationData& notification_data,
    const NotificationResources& notification_resources) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!RenderProcessHost::FromID(process_id_))
    return;

  if (!ValidateNotificationResources(notification_resources)) {
    bad_message::ReceivedBadMessage(this, bad_message::NMF_INVALID_ARGUMENT);
    return;
  }

  PlatformNotificationService* service =
      GetContentClient()->browser()->GetPlatformNotificationService();
  DCHECK(service);

  if (!VerifyNotificationPermissionGranted(service, origin))
    return;

  std::string notification_id =
      GetNotificationIdGenerator()->GenerateForNonPersistentNotification(
          origin, notification_data.tag, non_persistent_notification_id,
          process_id_);
  NotificationEventDispatcherImpl* event_dispatcher =
      content::NotificationEventDispatcherImpl::GetInstance();
  non_persistent__notification_shown_ = true;
  event_dispatcher->RegisterNonPersistentNotification(
      notification_id, process_id_, non_persistent_notification_id);

  base::Closure close_closure;
  service->DisplayNotification(browser_context_, notification_id, origin,
                               SanitizeNotificationData(notification_data),
                               notification_resources, &close_closure);

  if (!close_closure.is_null())
    close_closures_[notification_id] = close_closure;
}

void NotificationMessageFilter::OnShowPersistentNotification(
    int request_id,
    int64_t service_worker_registration_id,
    const GURL& origin,
    const PlatformNotificationData& notification_data,
    const NotificationResources& notification_resources) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  if (GetPermissionForOriginOnIO(origin) !=
      blink::mojom::PermissionStatus::GRANTED) {
    // We can't assume that the renderer is compromised at this point because
    // it's possible for the user to revoke an origin's permission between the
    // time where a website requests the notification to be shown and the call
    // arriving in the message filter.
    return;
  }

  if (!ValidateNotificationResources(notification_resources)) {
    bad_message::ReceivedBadMessage(this, bad_message::NMF_INVALID_ARGUMENT);
    return;
  }

  NotificationDatabaseData database_data;
  database_data.origin = origin;
  database_data.service_worker_registration_id = service_worker_registration_id;

  PlatformNotificationData sanitized_notification_data =
      SanitizeNotificationData(notification_data);
  database_data.notification_data = sanitized_notification_data;

  notification_context_->WriteNotificationData(
      origin, database_data,
      base::Bind(&NotificationMessageFilter::DidWritePersistentNotificationData,
                 weak_factory_io_.GetWeakPtr(), request_id,
                 service_worker_registration_id, origin,
                 sanitized_notification_data, notification_resources));
}

void NotificationMessageFilter::DidWritePersistentNotificationData(
    int request_id,
    int64_t service_worker_registration_id,
    const GURL& origin,
    const PlatformNotificationData& notification_data,
    const NotificationResources& notification_resources,
    bool success,
    const std::string& notification_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  if (!success) {
    Send(new PlatformNotificationMsg_DidShowPersistent(request_id, false));
    return;
  }

  // Get the service worker scope.
  service_worker_context_->FindReadyRegistrationForId(
      service_worker_registration_id, origin,
      base::Bind(&NotificationMessageFilter::DidFindServiceWorkerRegistration,
                 weak_factory_io_.GetWeakPtr(), request_id, origin,
                 notification_data, notification_resources, notification_id));
}

void NotificationMessageFilter::DidFindServiceWorkerRegistration(
    int request_id,
    const GURL& origin,
    const PlatformNotificationData& notification_data,
    const NotificationResources& notification_resources,
    const std::string& notification_id,
    content::ServiceWorkerStatusCode service_worker_status,
    scoped_refptr<content::ServiceWorkerRegistration> registration) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  if (service_worker_status != SERVICE_WORKER_OK) {
    Send(new PlatformNotificationMsg_DidShowPersistent(request_id, false));
    LOG(ERROR) << "Registration not found for " << origin.spec();
    // TODO(peter): Add UMA to track how often this occurs.
    return;
  }

  PlatformNotificationService* service =
      GetContentClient()->browser()->GetPlatformNotificationService();
  DCHECK(service);

  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::Bind(&PlatformNotificationService::DisplayPersistentNotification,
                 base::Unretained(service),  // The service is a singleton.
                 browser_context_, notification_id, registration->pattern(),
                 origin, notification_data, notification_resources));

  Send(new PlatformNotificationMsg_DidShowPersistent(request_id, true));
}

void NotificationMessageFilter::OnGetNotifications(
    int request_id,
    int64_t service_worker_registration_id,
    const GURL& origin,
    const std::string& filter_tag) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  if (GetPermissionForOriginOnIO(origin) !=
      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 an empty vector
    // indicating that no (accessible) notifications exist at this time.
    Send(new PlatformNotificationMsg_DidGetNotifications(
        request_id, std::vector<PersistentNotificationInfo>()));
    return;
  }

  notification_context_->ReadAllNotificationDataForServiceWorkerRegistration(
      origin, service_worker_registration_id,
      base::Bind(&NotificationMessageFilter::DidGetNotifications,
                 weak_factory_io_.GetWeakPtr(), request_id, filter_tag));
}

void NotificationMessageFilter::DidGetNotifications(
    int request_id,
    const std::string& filter_tag,
    bool success,
    const std::vector<NotificationDatabaseData>& notifications) {
  std::vector<PersistentNotificationInfo> persistent_notifications;
  for (const NotificationDatabaseData& database_data : notifications) {
    if (!filter_tag.empty()) {
      const std::string& tag = database_data.notification_data.tag;
      if (tag != filter_tag)
        continue;
    }

    persistent_notifications.push_back(std::make_pair(
        database_data.notification_id, database_data.notification_data));
  }

  Send(new PlatformNotificationMsg_DidGetNotifications(
      request_id, persistent_notifications));
}

void NotificationMessageFilter::OnClosePlatformNotification(
    const GURL& origin,
    const std::string& tag,
    int non_persistent_notification_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (!RenderProcessHost::FromID(process_id_))
    return;

  std::string notification_id =
      GetNotificationIdGenerator()->GenerateForNonPersistentNotification(
          origin, tag, non_persistent_notification_id, process_id_);

  if (!close_closures_.count(notification_id))
    return;

  close_closures_[notification_id].Run();
  close_closures_.erase(notification_id);
}

void NotificationMessageFilter::OnClosePersistentNotification(
    const GURL& origin,
    const std::string& tag,
    const std::string& notification_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  if (GetPermissionForOriginOnIO(origin) !=
      blink::mojom::PermissionStatus::GRANTED) {
    return;
  }

  PlatformNotificationService* service =
      GetContentClient()->browser()->GetPlatformNotificationService();
  DCHECK(service);

  // There's no point in waiting until the database data has been removed before
  // closing the notification presented to the user. Post that task immediately.
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::Bind(&PlatformNotificationService::ClosePersistentNotification,
                 base::Unretained(service),  // The service is a singleton.
                 browser_context_, notification_id));

  notification_context_->DeleteNotificationData(
      notification_id, origin,
      base::Bind(
          &NotificationMessageFilter::DidDeletePersistentNotificationData,
          weak_factory_io_.GetWeakPtr()));
}

void NotificationMessageFilter::DidDeletePersistentNotificationData(
    bool success) {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);
  // TODO(peter): Consider feeding back to the renderer that the notification
  // has been closed.
}

blink::mojom::PermissionStatus
NotificationMessageFilter::GetPermissionForOriginOnIO(
    const GURL& origin) const {
  DCHECK_CURRENTLY_ON(BrowserThread::IO);

  PlatformNotificationService* service =
      GetContentClient()->browser()->GetPlatformNotificationService();
  if (!service)
    return blink::mojom::PermissionStatus::DENIED;

  return service->CheckPermissionOnIOThread(resource_context_, origin,
                                            process_id_);
}

bool NotificationMessageFilter::VerifyNotificationPermissionGranted(
    PlatformNotificationService* service,
    const GURL& origin) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  blink::mojom::PermissionStatus permission_status =
      service->CheckPermissionOnUIThread(browser_context_, origin, process_id_);

  // We can't assume that the renderer is compromised at this point because
  // it's possible for the user to revoke an origin's permission between the
  // time where a website requests the notification to be shown and the call
  // arriving in the message filter.

  return permission_status == blink::mojom::PermissionStatus::GRANTED;
}

NotificationIdGenerator* NotificationMessageFilter::GetNotificationIdGenerator()
    const {
  return notification_context_->notification_id_generator();
}

}  // namespace content
