blob: 51974828115be8ab07e5930ca13b61e7186f176d [file] [log] [blame]
// Copyright 2017 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 "chrome/browser/notifications/notification_template_builder.h"
#include <algorithm>
#include "base/files/file_path.h"
#include "base/i18n/time_formatting.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/notifications/notification_image_retainer.h"
#include "chrome/grit/chromium_strings.h"
#include "components/url_formatter/elide_url.h"
#include "third_party/libxml/chromium/libxml_utils.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/notification.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace {
// Constants used for the XML element names and their attributes.
const char kActionElement[] = "action";
const char kActionsElement[] = "actions";
const char kActivationType[] = "activationType";
const char kArguments[] = "arguments";
const char kAttribution[] = "attribution";
const char kAudioElement[] = "audio";
const char kBindingElement[] = "binding";
const char kBindingElementTemplateAttribute[] = "template";
const char kContent[] = "content";
const char kContextMenu[] = "contextMenu";
const char kForeground[] = "foreground";
const char kHero[] = "hero";
const char kHintCrop[] = "hint-crop";
const char kHintCropNone[] = "none";
const char kImageElement[] = "image";
const char kImageUri[] = "imageUri";
const char kInputElement[] = "input";
const char kInputId[] = "id";
const char kInputType[] = "type";
const char kStatus[] = "status";
const char kNotificationSettings[] = "notificationSettings";
const char kPlaceholderContent[] = "placeHolderContent";
const char kPlacement[] = "placement";
const char kPlacementAppLogoOverride[] = "appLogoOverride";
const char kProgress[] = "progress";
const char kReminder[] = "reminder";
const char kScenario[] = "scenario";
const char kSilent[] = "silent";
const char kSrc[] = "src";
const char kText[] = "text";
const char kTextElement[] = "text";
const char kToastElementDisplayTimestamp[] = "displayTimestamp";
const char kTrue[] = "true";
const char kUserResponse[] = "userResponse";
const char kValue[] = "value";
const char kVisualElement[] = "visual";
// Name of the template used for default Chrome notifications.
const char kDefaultTemplate[] = "ToastGeneric";
// The XML version header that has to be stripped from the output.
const char kXmlVersionHeader[] = "<?xml version=\"1.0\"?>\n";
} // namespace
const char kNotificationButtonIndex[] = "buttonIndex";
const char kNotificationToastElement[] = "toast";
const char kNotificationLaunchAttribute[] = "launch";
// static
const char* NotificationTemplateBuilder::context_menu_label_override_ = nullptr;
// static
std::unique_ptr<NotificationTemplateBuilder> NotificationTemplateBuilder::Build(
NotificationImageRetainer* notification_image_retainer,
const std::string& launch_attribute,
const std::string& profile_id,
const message_center::Notification& notification) {
std::unique_ptr<NotificationTemplateBuilder> builder = base::WrapUnique(
new NotificationTemplateBuilder(notification_image_retainer, profile_id));
builder->StartToastElement(launch_attribute, notification);
builder->StartVisualElement();
builder->StartBindingElement(kDefaultTemplate);
// Content for the toast template.
builder->WriteTextElement(base::UTF16ToUTF8(notification.title()),
TextType::NORMAL);
// Message has historically not been shown for list-style notifications.
if (notification.type() == message_center::NOTIFICATION_TYPE_MULTIPLE &&
!notification.items().empty()) {
builder->WriteItems(notification.items());
} else {
builder->WriteTextElement(base::UTF16ToUTF8(notification.message()),
TextType::NORMAL);
}
std::string attribution;
if (notification.UseOriginAsContextMessage())
attribution = builder->FormatOrigin(notification.origin_url());
else if (!notification.context_message().empty())
attribution = base::UTF16ToUTF8(notification.context_message());
if (!attribution.empty())
builder->WriteTextElement(attribution, TextType::ATTRIBUTION);
if (!notification.icon().IsEmpty())
builder->WriteIconElement(notification);
if (!notification.image().IsEmpty())
builder->WriteLargeImageElement(notification);
if (notification.type() == message_center::NOTIFICATION_TYPE_PROGRESS)
builder->WriteProgressElement(notification);
builder->EndBindingElement();
builder->EndVisualElement();
builder->StartActionsElement();
if (!notification.buttons().empty())
builder->AddActions(notification);
builder->AddContextMenu();
builder->EndActionsElement();
if (notification.silent())
builder->WriteAudioSilentElement();
builder->EndToastElement();
return builder;
}
NotificationTemplateBuilder::NotificationTemplateBuilder(
NotificationImageRetainer* notification_image_retainer,
const std::string& profile_id)
: xml_writer_(std::make_unique<XmlWriter>()),
image_retainer_(notification_image_retainer),
profile_id_(profile_id) {
xml_writer_->StartWriting();
}
NotificationTemplateBuilder::~NotificationTemplateBuilder() = default;
base::string16 NotificationTemplateBuilder::GetNotificationTemplate() const {
std::string template_xml = xml_writer_->GetWrittenString();
DCHECK(base::StartsWith(template_xml, kXmlVersionHeader,
base::CompareCase::SENSITIVE));
// The |kXmlVersionHeader| is automatically appended by libxml, but the toast
// system in the Windows Action Center expects it to be absent.
return base::UTF8ToUTF16(template_xml.substr(sizeof(kXmlVersionHeader) - 1));
}
std::string NotificationTemplateBuilder::FormatOrigin(
const GURL& origin) const {
base::string16 origin_string = url_formatter::FormatOriginForSecurityDisplay(
url::Origin::Create(origin),
url_formatter::SchemeDisplay::OMIT_HTTP_AND_HTTPS);
DCHECK(origin_string.size());
return base::UTF16ToUTF8(origin_string);
}
void NotificationTemplateBuilder::StartToastElement(
const std::string& launch_attribute,
const message_center::Notification& notification) {
xml_writer_->StartElement(kNotificationToastElement);
xml_writer_->AddAttribute(kNotificationLaunchAttribute, launch_attribute);
// Note: If the notification doesn't include a button, then Windows will
// ignore the Reminder flag.
if (notification.never_timeout())
xml_writer_->AddAttribute(kScenario, kReminder);
if (notification.timestamp().is_null())
return;
base::Time::Exploded exploded;
notification.timestamp().UTCExplode(&exploded);
xml_writer_->AddAttribute(
kToastElementDisplayTimestamp,
base::StringPrintf("%04d-%02d-%02dT%02d:%02d:%02dZ", exploded.year,
exploded.month, exploded.day_of_month, exploded.hour,
exploded.minute, exploded.second));
}
void NotificationTemplateBuilder::EndToastElement() {
xml_writer_->EndElement();
xml_writer_->StopWriting();
}
void NotificationTemplateBuilder::StartVisualElement() {
xml_writer_->StartElement(kVisualElement);
}
void NotificationTemplateBuilder::EndVisualElement() {
xml_writer_->EndElement();
}
void NotificationTemplateBuilder::StartBindingElement(
const std::string& template_name) {
xml_writer_->StartElement(kBindingElement);
xml_writer_->AddAttribute(kBindingElementTemplateAttribute, template_name);
}
void NotificationTemplateBuilder::EndBindingElement() {
xml_writer_->EndElement();
}
void NotificationTemplateBuilder::WriteTextElement(const std::string& content,
TextType text_type) {
xml_writer_->StartElement(kTextElement);
if (text_type == TextType::ATTRIBUTION)
xml_writer_->AddAttribute(kPlacement, kAttribution);
xml_writer_->AppendElementContent(content);
xml_writer_->EndElement();
}
void NotificationTemplateBuilder::WriteItems(
const std::vector<message_center::NotificationItem>& items) {
// A toast can have a maximum of three text items, of which one is reserved
// for the title. The remaining two can each handle up to four lines of text,
// but the toast can only show four lines total, so there's no point in having
// more than one text item. Therefore, we show them all in one and hope there
// is no truncation at the bottom. There will never be room for items 5 and up
// so we don't make an attempt to show them.
constexpr size_t kMaxEntries = 4;
size_t entries = std::min(kMaxEntries, items.size());
std::string item_list;
for (size_t i = 0; i < entries; ++i) {
const auto& item = items[i];
item_list += base::UTF16ToUTF8(item.title) + " - " +
base::UTF16ToUTF8(item.message) + "\n";
}
WriteTextElement(item_list, TextType::NORMAL);
}
void NotificationTemplateBuilder::WriteIconElement(
const message_center::Notification& notification) {
WriteImageElement(notification.icon(), notification.origin_url(),
kPlacementAppLogoOverride, kHintCropNone);
}
void NotificationTemplateBuilder::WriteLargeImageElement(
const message_center::Notification& notification) {
WriteImageElement(notification.image(), notification.origin_url(), kHero,
std::string());
}
void NotificationTemplateBuilder::WriteImageElement(
const gfx::Image& image,
const GURL& origin,
const std::string& placement,
const std::string& hint_crop) {
base::FilePath path =
image_retainer_->RegisterTemporaryImage(image, profile_id_, origin);
if (!path.empty()) {
xml_writer_->StartElement(kImageElement);
xml_writer_->AddAttribute(kPlacement, placement);
xml_writer_->AddAttribute(kSrc, base::UTF16ToUTF8(path.value()));
if (!hint_crop.empty())
xml_writer_->AddAttribute(kHintCrop, hint_crop);
xml_writer_->EndElement();
}
}
void NotificationTemplateBuilder::WriteProgressElement(
const message_center::Notification& notification) {
// Two other attributes are supported by Microsoft:
// title: A string shown on the left side of the toast, just above the bar.
// valueStringOverride: A string that replaces the percentage on the right.
xml_writer_->StartElement(kProgress);
// Status is mandatory, without it the progress bar is not shown.
xml_writer_->AddAttribute(kStatus, std::string());
xml_writer_->AddAttribute(
kValue, base::StringPrintf("%3.2f", 1.0 * notification.progress() / 100));
xml_writer_->EndElement();
}
void NotificationTemplateBuilder::AddActions(
const message_center::Notification& notification) {
const std::vector<message_center::ButtonInfo>& buttons =
notification.buttons();
bool inline_reply = false;
std::string placeholder;
for (const auto& button : buttons) {
if (button.type != message_center::ButtonType::TEXT)
continue;
inline_reply = true;
placeholder = base::UTF16ToUTF8(button.placeholder);
break;
}
if (inline_reply) {
xml_writer_->StartElement(kInputElement);
xml_writer_->AddAttribute(kInputId, kUserResponse);
xml_writer_->AddAttribute(kInputType, kText);
xml_writer_->AddAttribute(kPlaceholderContent, placeholder);
xml_writer_->EndElement();
}
for (size_t i = 0; i < buttons.size(); ++i)
WriteActionElement(buttons[i], i, notification.origin_url());
}
void NotificationTemplateBuilder::AddContextMenu() {
std::string notification_settings_msg = l10n_util::GetStringUTF8(
IDS_WIN_NOTIFICATION_SETTINGS_CONTEXT_MENU_ITEM_NAME);
if (context_menu_label_override_)
notification_settings_msg = context_menu_label_override_;
WriteContextMenuElement(notification_settings_msg, kNotificationSettings);
}
void NotificationTemplateBuilder::StartActionsElement() {
xml_writer_->StartElement(kActionsElement);
}
void NotificationTemplateBuilder::EndActionsElement() {
xml_writer_->EndElement();
}
void NotificationTemplateBuilder::WriteAudioSilentElement() {
xml_writer_->StartElement(kAudioElement);
xml_writer_->AddAttribute(kSilent, kTrue);
xml_writer_->EndElement();
}
void NotificationTemplateBuilder::WriteActionElement(
const message_center::ButtonInfo& button,
int index,
const GURL& origin) {
xml_writer_->StartElement(kActionElement);
xml_writer_->AddAttribute(kActivationType, kForeground);
xml_writer_->AddAttribute(kContent, base::UTF16ToUTF8(button.title));
std::string param =
std::string(kNotificationButtonIndex) + "=" + base::IntToString(index);
xml_writer_->AddAttribute(kArguments, param);
if (!button.icon.IsEmpty()) {
base::FilePath path = image_retainer_->RegisterTemporaryImage(
button.icon, profile_id_, origin);
if (!path.empty())
xml_writer_->AddAttribute(kImageUri, path.AsUTF8Unsafe());
}
xml_writer_->EndElement();
}
void NotificationTemplateBuilder::WriteContextMenuElement(
const std::string& content,
const std::string& arguments) {
xml_writer_->StartElement(kActionElement);
xml_writer_->AddAttribute(kContent, content);
xml_writer_->AddAttribute(kPlacement, kContextMenu);
xml_writer_->AddAttribute(kActivationType, kForeground);
xml_writer_->AddAttribute(kArguments, arguments);
xml_writer_->EndElement();
}
void NotificationTemplateBuilder::OverrideContextMenuLabelForTesting(
const char* label) {
context_menu_label_override_ = label;
}