| // 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/win/notification_template_builder.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <vector> |
| |
| #include "base/files/file_path.h" |
| #include "base/i18n/time_formatting.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.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/win/notification_image_retainer.h" |
| #include "chrome/browser/notifications/win/notification_launch_id.h" |
| #include "chrome/grit/chromium_strings.h" |
| #include "chrome/grit/generated_resources.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/public/cpp/notification.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace { |
| |
| // The different types of text nodes to output. |
| enum class TextType { NORMAL, ATTRIBUTION }; |
| |
| // Label to override context menu items in tests. |
| const char* context_menu_label_override = nullptr; |
| |
| // 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 kBackground[] = "background"; |
| 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 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"; |
| |
| // Formats the |origin| for display in the notification template. |
| std::string FormatOrigin(const GURL& origin) { |
| 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); |
| } |
| |
| // Writes the <toast> element with a given |launch_attribute|. |
| // Also closes the |xml_writer_| for writing as the toast is now complete. |
| void StartToastElement(XmlWriter* xml_writer, |
| const NotificationLaunchId& launch_id, |
| const message_center::Notification& notification) { |
| xml_writer->StartElement(kNotificationToastElement); |
| xml_writer->AddAttribute(kNotificationLaunchAttribute, launch_id.Serialize()); |
| |
| // 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 EndToastElement(XmlWriter* xml_writer) { |
| xml_writer->EndElement(); |
| } |
| |
| // Writes the <visual> element. |
| void StartVisualElement(XmlWriter* xml_writer) { |
| xml_writer->StartElement(kVisualElement); |
| } |
| |
| void EndVisualElement(XmlWriter* xml_writer) { |
| xml_writer->EndElement(); |
| } |
| |
| // Writes the <binding> element with the given |template_name|. |
| void StartBindingElement(XmlWriter* xml_writer, |
| const std::string& template_name) { |
| xml_writer->StartElement(kBindingElement); |
| xml_writer->AddAttribute(kBindingElementTemplateAttribute, template_name); |
| } |
| |
| void EndBindingElement(XmlWriter* xml_writer) { |
| xml_writer->EndElement(); |
| } |
| |
| // Writes the <text> element with the given |content|. If |text_type| is |
| // ATTRIBUTION then |content| is treated as the source that the notification is |
| // attributed to. |
| void WriteTextElement(XmlWriter* xml_writer, |
| 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(); |
| } |
| |
| // Writes the <text> element containing the list entries. |
| void WriteItems(XmlWriter* xml_writer, |
| 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(xml_writer, item_list, TextType::NORMAL); |
| } |
| |
| // A helper for constructing image xml. |
| void WriteImageElement(XmlWriter* xml_writer, |
| NotificationImageRetainer* image_retainer, |
| const gfx::Image& image, |
| const std::string& placement, |
| const std::string& hint_crop) { |
| base::FilePath path = image_retainer->RegisterTemporaryImage(image); |
| 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(); |
| } |
| } |
| |
| // Writes the <image> element for the notification icon. |
| void WriteIconElement(XmlWriter* xml_writer, |
| NotificationImageRetainer* image_retainer, |
| const message_center::Notification& notification) { |
| WriteImageElement(xml_writer, image_retainer, notification.icon(), |
| kPlacementAppLogoOverride, kHintCropNone); |
| } |
| |
| // Writes the <image> element for showing a large image within the notification |
| // body. |
| void WriteLargeImageElement(XmlWriter* xml_writer, |
| NotificationImageRetainer* image_retainer, |
| const message_center::Notification& notification) { |
| WriteImageElement(xml_writer, image_retainer, notification.image(), kHero, |
| std::string()); |
| } |
| |
| // Adds a progress bar to the notification XML. |
| void WriteProgressElement(XmlWriter* xml_writer, |
| 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(); |
| } |
| |
| // Writes the <actions> element. |
| void StartActionsElement(XmlWriter* xml_writer) { |
| xml_writer->StartElement(kActionsElement); |
| } |
| |
| void EndActionsElement(XmlWriter* xml_writer) { |
| xml_writer->EndElement(); |
| } |
| |
| // A helper for constructing action xml. |
| void WriteActionElement(XmlWriter* xml_writer, |
| NotificationImageRetainer* image_retainer, |
| const message_center::ButtonInfo& button, |
| int index, |
| NotificationLaunchId copied_launch_id) { |
| xml_writer->StartElement(kActionElement); |
| xml_writer->AddAttribute(kActivationType, kForeground); |
| xml_writer->AddAttribute(kContent, base::UTF16ToUTF8(button.title)); |
| copied_launch_id.set_button_index(index); |
| xml_writer->AddAttribute(kArguments, copied_launch_id.Serialize()); |
| |
| if (!button.icon.IsEmpty()) { |
| base::FilePath path = image_retainer->RegisterTemporaryImage(button.icon); |
| if (!path.empty()) |
| xml_writer->AddAttribute(kImageUri, path.AsUTF8Unsafe()); |
| } |
| |
| xml_writer->EndElement(); |
| } |
| |
| // Fills in the details for the actions (the buttons the notification contains). |
| void AddActions(XmlWriter* xml_writer, |
| NotificationImageRetainer* image_retainer, |
| const message_center::Notification& notification, |
| const NotificationLaunchId& launch_id) { |
| const std::vector<message_center::ButtonInfo>& buttons = |
| notification.buttons(); |
| bool inline_reply = false; |
| std::string placeholder; |
| for (const auto& button : buttons) { |
| if (!button.placeholder) |
| 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(xml_writer, image_retainer, buttons[i], i, launch_id); |
| } |
| |
| // Writes the <audio silent="true"> element. |
| void WriteAudioSilentElement(XmlWriter* xml_writer) { |
| xml_writer->StartElement(kAudioElement); |
| xml_writer->AddAttribute(kSilent, kTrue); |
| xml_writer->EndElement(); |
| } |
| |
| // A helper for constructing context menu xml. |
| void WriteContextMenuElement(XmlWriter* xml_writer, |
| 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(); |
| } |
| |
| // Adds context menu actions to the notification. |
| void AddContextMenu(XmlWriter* xml_writer, |
| NotificationLaunchId copied_launch_id, |
| const std::string& settings_msg) { |
| copied_launch_id.set_is_for_context_menu(); |
| WriteContextMenuElement(xml_writer, settings_msg, |
| copied_launch_id.Serialize()); |
| } |
| |
| // Ensures that every reminder has at least one button, as the Action Center |
| // does not respect the Reminder setting on notifications with no buttons, so we |
| // must add a Dismiss button to the notification for those cases. For more |
| // details, see issue https://crbug.com/781792. |
| void EnsureReminderHasButton(XmlWriter* xml_writer, |
| const message_center::Notification& notification, |
| NotificationLaunchId copied_launch_id) { |
| if (!notification.never_timeout() || !notification.buttons().empty()) |
| return; |
| |
| xml_writer->StartElement(kActionElement); |
| xml_writer->AddAttribute(kActivationType, kBackground); |
| // TODO(finnur): Add our own string here (we're past string-freeze so we're |
| // re-using the already translated "Close" from elsewhere). |
| xml_writer->AddAttribute( |
| kContent, l10n_util::GetStringUTF8(IDS_MD_HISTORY_CLOSE_MENU_PROMO)); |
| copied_launch_id.set_is_for_dismiss_button(); |
| xml_writer->AddAttribute(kArguments, copied_launch_id.Serialize()); |
| xml_writer->EndElement(); |
| } |
| |
| } // namespace |
| |
| const char kNotificationToastElement[] = "toast"; |
| const char kNotificationLaunchAttribute[] = "launch"; |
| |
| // libXml was preferred (over WinXml, which the samples in the link below tend |
| // to use) for building the XML template because it is used frequently in |
| // Chrome, is nicer to use and has already been vetted. |
| // https://docs.microsoft.com/en-us/windows/uwp/controls-and-patterns/tiles-and-notifications-adaptive-interactive-toasts |
| base::string16 BuildNotificationTemplate( |
| NotificationImageRetainer* image_retainer, |
| const NotificationLaunchId& launch_id, |
| const message_center::Notification& notification) { |
| DCHECK(image_retainer); |
| |
| XmlWriter xml_writer; |
| xml_writer.StartWriting(); |
| |
| StartToastElement(&xml_writer, launch_id, notification); |
| StartVisualElement(&xml_writer); |
| |
| StartBindingElement(&xml_writer, kDefaultTemplate); |
| |
| // Content for the toast template. |
| WriteTextElement(&xml_writer, 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()) { |
| WriteItems(&xml_writer, notification.items()); |
| } else { |
| WriteTextElement(&xml_writer, base::UTF16ToUTF8(notification.message()), |
| TextType::NORMAL); |
| } |
| |
| std::string attribution; |
| if (notification.UseOriginAsContextMessage()) |
| attribution = FormatOrigin(notification.origin_url()); |
| else if (!notification.context_message().empty()) |
| attribution = base::UTF16ToUTF8(notification.context_message()); |
| |
| if (!attribution.empty()) |
| WriteTextElement(&xml_writer, attribution, TextType::ATTRIBUTION); |
| |
| if (!notification.icon().IsEmpty()) |
| WriteIconElement(&xml_writer, image_retainer, notification); |
| |
| if (!notification.image().IsEmpty()) |
| WriteLargeImageElement(&xml_writer, image_retainer, notification); |
| |
| if (notification.type() == message_center::NOTIFICATION_TYPE_PROGRESS) |
| WriteProgressElement(&xml_writer, notification); |
| |
| EndBindingElement(&xml_writer); |
| EndVisualElement(&xml_writer); |
| |
| StartActionsElement(&xml_writer); |
| if (!notification.buttons().empty()) |
| AddActions(&xml_writer, image_retainer, notification, launch_id); |
| EnsureReminderHasButton(&xml_writer, notification, launch_id); |
| if (context_menu_label_override) { |
| AddContextMenu(&xml_writer, launch_id, context_menu_label_override); |
| } else { |
| AddContextMenu(&xml_writer, launch_id, |
| l10n_util::GetStringUTF8( |
| IDS_WIN_NOTIFICATION_SETTINGS_CONTEXT_MENU_ITEM_NAME)); |
| } |
| EndActionsElement(&xml_writer); |
| |
| if (notification.silent()) |
| WriteAudioSilentElement(&xml_writer); |
| |
| EndToastElement(&xml_writer); |
| |
| xml_writer.StopWriting(); |
| |
| 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( |
| base::StringPiece(template_xml).substr(sizeof(kXmlVersionHeader) - 1)); |
| } |
| |
| void SetContextMenuLabelForTesting(const char* label) { |
| context_menu_label_override = label; |
| } |