chromium/chrome/browser/notifications/mac/notification_utils.cc

// 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);
  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()));
}