| // Copyright 2020 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/notifications/mac/notification_utils.h" |
| |
| #include <optional> |
| |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/i18n/number_formatting.h" |
| #include "base/path_service.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/notifications/notification_display_service_impl.h" |
| #include "chrome/browser/notifications/notification_platform_bridge.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/themes/theme_service.h" |
| #include "chrome/browser/themes/theme_service_factory.h" |
| #include "chrome/browser/ui/chrome_pages.h" |
| #include "chrome/browser/web_applications/web_app_utils.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/notifications/notification_constants.h" |
| #include "chrome/common/notifications/notification_operation.h" |
| #include "components/url_formatter/elide_url.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "third_party/blink/public/common/notifications/notification_constants.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace { |
| |
| void DisplayWebAppSettings(const webapps::AppId& web_app_id, Profile* profile) { |
| if (!profile) { |
| LOG(WARNING) << "Profile not loaded correctly"; |
| return; |
| } |
| chrome::ShowWebAppSettings( |
| profile, web_app_id, |
| web_app::AppSettingsPageEntryPoint::kNotificationSettingsButton); |
| } |
| |
| // Loads the profile and process the Notification response |
| void DoProcessMacNotificationResponse( |
| mac_notifications::mojom::NotificationActionInfoPtr info, |
| std::optional<webapps::AppId> web_app_id) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| ProfileManager* profile_manager = g_browser_process->profile_manager(); |
| DCHECK(profile_manager); |
| |
| std::optional<int> action_index; |
| if (info->button_index != kNotificationInvalidButtonIndex) |
| action_index = info->button_index; |
| |
| auto operation = static_cast<NotificationOperation>(info->operation); |
| ProfileManager::ProfileLoadedCallback callback = |
| (operation == NotificationOperation::kSettings && web_app_id.has_value()) |
| ? base::BindOnce(&DisplayWebAppSettings, *web_app_id) |
| : base::BindOnce( |
| &NotificationDisplayServiceImpl::ProfileLoadedCallback, |
| operation, |
| static_cast<NotificationHandler::Type>(info->meta->type), |
| std::move(info->meta->origin_url), |
| std::move(info->meta->id->id), std::move(action_index), |
| std::move(info->reply), /*by_user=*/true, base::DoNothing()); |
| profile_manager->LoadProfile( |
| NotificationPlatformBridge::GetProfileBaseNameFromProfileId( |
| info->meta->id->profile->id), |
| info->meta->id->profile->incognito, std::move(callback)); |
| } |
| |
| // Get the user data directory. |
| std::string GetUserDataDir() { |
| return base::PathService::CheckedGet(chrome::DIR_USER_DATA).value(); |
| } |
| |
| } // namespace |
| |
| std::u16string CreateMacNotificationTitle( |
| const message_center::Notification& notification) { |
| std::u16string title; |
| // Show progress percentage if available. We don't support indeterminate |
| // states on macOS native notifications. |
| if (notification.type() == message_center::NOTIFICATION_TYPE_PROGRESS && |
| notification.progress() >= 0 && notification.progress() <= 100) { |
| title += base::FormatPercent(notification.progress()); |
| title += u" - "; |
| } |
| title += notification.title(); |
| return title; |
| } |
| |
| std::u16string CreateMacNotificationContext( |
| bool isPersistent, |
| const message_center::Notification& notification, |
| bool requiresAttribution) { |
| if (!requiresAttribution) |
| return notification.context_message(); |
| |
| // Mac OS notifications don't provide a good way to elide the domain (or tell |
| // you the maximum width of the subtitle field). We have experimentally |
| // determined the maximum number of characters that fit using the widest |
| // possible character (m). If the domain fits in those character we show it |
| // completely. Otherwise we use eTLD + 1. |
| |
| // These numbers have been obtained through experimentation on various |
| // Mac OS platforms. |
| |
| constexpr size_t kMaxDomainLengthAlert = 19; |
| constexpr size_t kMaxDomainLengthBanner = 28; |
| |
| size_t maxCharacters = |
| isPersistent ? kMaxDomainLengthAlert : kMaxDomainLengthBanner; |
| |
| std::u16string origin = url_formatter::FormatOriginForSecurityDisplay( |
| url::Origin::Create(notification.origin_url()), |
| url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS); |
| |
| if (origin.size() <= maxCharacters) |
| return origin; |
| |
| // Too long, use etld+1 |
| std::u16string etldplusone = |
| base::UTF8ToUTF16(net::registry_controlled_domains::GetDomainAndRegistry( |
| notification.origin_url(), |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)); |
| |
| // localhost, raw IPs etc. are not handled by GetDomainAndRegistry. |
| if (etldplusone.empty()) |
| return origin; |
| |
| return etldplusone; |
| } |
| |
| bool VerifyMacNotificationData( |
| const mac_notifications::mojom::NotificationActionInfoPtr& info) { |
| if (!info || !info->meta || !info->meta->id || !info->meta->id->profile) { |
| LOG(ERROR) << "Missing required data"; |
| return false; |
| } |
| |
| if (info->meta->user_data_dir != GetUserDataDir()) { |
| return false; |
| } |
| |
| if (info->button_index < kNotificationInvalidButtonIndex || |
| info->button_index >= static_cast<int>(blink::kNotificationMaxActions)) { |
| LOG(ERROR) << "Invalid number of buttons supplied " << info->button_index; |
| return false; |
| } |
| |
| if (info->meta->id->id.empty()) { |
| LOG(ERROR) << "Notification Id is empty"; |
| return false; |
| } |
| |
| if (info->meta->id->profile->id.empty()) { |
| LOG(ERROR) << "ProfileId not provided"; |
| return false; |
| } |
| |
| if (info->meta->type > static_cast<int>(NotificationHandler::Type::MAX)) { |
| LOG(ERROR) << info->meta->type |
| << " Does not correspond to a valid operation."; |
| return false; |
| } |
| |
| // Origin is not actually required but if it's there it should be a valid one. |
| if (!info->meta->origin_url.is_empty() && !info->meta->origin_url.is_valid()) |
| return false; |
| |
| return true; |
| } |
| |
| void ProcessMacNotificationResponse( |
| mac_notifications::NotificationStyle notification_style, |
| mac_notifications::mojom::NotificationActionInfoPtr info, |
| std::optional<webapps::AppId> web_app_id) { |
| bool is_valid = VerifyMacNotificationData(info); |
| if (!is_valid) |
| return; |
| |
| std::optional<int> actionIndex; |
| if (info->button_index != kNotificationInvalidButtonIndex) |
| actionIndex = info->button_index; |
| |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(DoProcessMacNotificationResponse, |
| std::move(info), web_app_id)); |
| } |
| |
| bool IsAlertNotificationMac(const message_center::Notification& notification) { |
| // Check if the |notification| should be shown as alert. |
| return notification.never_timeout() || |
| notification.type() == message_center::NOTIFICATION_TYPE_PROGRESS; |
| } |
| |
| mac_notifications::mojom::NotificationPtr CreateMacNotification( |
| NotificationHandler::Type notification_type, |
| Profile* profile, |
| const message_center::Notification& notification) { |
| auto profile_identifier = mac_notifications::mojom::ProfileIdentifier::New( |
| NotificationPlatformBridge::GetProfileId(profile), |
| profile->IsOffTheRecord()); |
| auto notification_identifier = |
| mac_notifications::mojom::NotificationIdentifier::New( |
| notification.id(), std::move(profile_identifier)); |
| |
| auto meta = mac_notifications::mojom::NotificationMetadata::New( |
| std::move(notification_identifier), static_cast<int>(notification_type), |
| notification.origin_url(), GetUserDataDir()); |
| |
| std::vector<mac_notifications::mojom::NotificationActionButtonPtr> buttons; |
| for (const message_center::ButtonInfo& button : notification.buttons()) { |
| buttons.push_back(mac_notifications::mojom::NotificationActionButton::New( |
| button.title, button.placeholder)); |
| } |
| |
| bool is_alert = IsAlertNotificationMac(notification); |
| bool requires_attribution = |
| notification.context_message().empty() && |
| notification_type != NotificationHandler::Type::EXTENSION; |
| |
| std::u16string body = notification.items().empty() |
| ? notification.message() |
| : (notification.items().at(0).title() + u" - " + |
| notification.items().at(0).message()); |
| |
| return mac_notifications::mojom::Notification::New( |
| std::move(meta), CreateMacNotificationTitle(notification), |
| CreateMacNotificationContext(is_alert, notification, |
| requires_attribution), |
| std::move(body), notification.renotify(), |
| notification.should_show_settings_button(), std::move(buttons), |
| notification.icon().Rasterize( |
| ThemeServiceFactory::GetForProfile(profile)->GetColorProvider())); |
| } |