| // 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."; |
| |
| 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::GetPermissionStatus( |
| GetPermissionStatusCallback callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!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_->GetPlatformNotificationService()) |
| return; |
| |
| if (CheckPermissionStatus() != blink::mojom::PermissionStatus::GRANTED) |
| return; |
| |
| if (!IsValidForNonPersistentNotification()) |
| 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_->GetPlatformNotificationService()) |
| return; |
| |
| if (CheckPermissionStatus() != blink::mojom::PermissionStatus::GRANTED) |
| return; |
| |
| if (!IsValidForNonPersistentNotification()) |
| 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); |
| 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_->GetPlatformNotificationService()) { |
| std::move(callback).Run(PersistentNotificationError::INTERNAL_ERROR); |
| 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::INTERNAL_ERROR); |
| } |
| |
| void BlinkNotificationServiceImpl::ClosePersistentNotification( |
| const std::string& notification_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!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_->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 |