blob: 3486cc6c688e000ed733103d1423e7318ebdbadf [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 "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_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 kButtonIndex[] = "buttonIndex=";
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 kNotificationSettings[] = "notificationSettings";
const char kPlaceholderContent[] = "placeHolderContent";
const char kPlacement[] = "placement";
const char kPlacementAppLogoOverride[] = "appLogoOverride";
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 kToastElement[] = "toast";
const char kToastElementDisplayTimestamp[] = "displayTimestamp";
const char kToastElementLaunchAttribute[] = "launch";
const char kTrue[] = "true";
const char kUserResponse[] = "userResponse";
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
// static
const char* NotificationTemplateBuilder::context_menu_label_override_ = nullptr;
// static
std::unique_ptr<NotificationTemplateBuilder> NotificationTemplateBuilder::Build(
NotificationImageRetainer* notification_image_retainer,
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(notification.id(), notification);
builder->StartVisualElement();
builder->StartBindingElement(kDefaultTemplate);
// Content for the toast template.
builder->WriteTextElement(base::UTF16ToUTF8(notification.title()),
TextType::NORMAL);
builder->WriteTextElement(base::UTF16ToUTF8(notification.message()),
TextType::NORMAL);
builder->WriteTextElement(builder->FormatOrigin(notification.origin_url()),
TextType::ATTRIBUTION);
if (!notification.icon().IsEmpty())
builder->WriteIconElement(notification);
if (!notification.image().IsEmpty())
builder->WriteLargeImageElement(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& notification_id,
const message_center::Notification& notification) {
xml_writer_->StartElement(kToastElement);
xml_writer_->AddAttribute(kToastElementLaunchAttribute, notification_id);
// 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::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::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(kButtonIndex) + 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;
}