diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /widget/windows/ToastNotificationHandler.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/windows/ToastNotificationHandler.cpp')
-rw-r--r-- | widget/windows/ToastNotificationHandler.cpp | 1115 |
1 files changed, 1115 insertions, 0 deletions
diff --git a/widget/windows/ToastNotificationHandler.cpp b/widget/windows/ToastNotificationHandler.cpp new file mode 100644 index 0000000000..ed46cc9ff7 --- /dev/null +++ b/widget/windows/ToastNotificationHandler.cpp @@ -0,0 +1,1115 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ToastNotificationHandler.h" + +#include <windows.foundation.h> + +#include "gfxUtils.h" +#include "imgIContainer.h" +#include "imgIRequest.h" +#include "mozilla/gfx/2D.h" +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +#endif +#include "mozilla/HashFunctions.h" +#include "mozilla/Result.h" +#include "mozilla/Logging.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/WindowsVersion.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsAppRunner.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIDUtils.h" +#include "nsIStringBundle.h" +#include "nsIToolkitProfile.h" +#include "nsIToolkitProfileService.h" +#include "nsIURI.h" +#include "nsIWidget.h" +#include "nsIWindowMediator.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsProxyRelease.h" +#include "nsXREDirProvider.h" +#include "ToastNotificationHeaderOnlyUtils.h" +#include "WidgetUtils.h" +#include "WinUtils.h" + +#include "ToastNotification.h" + +namespace mozilla { +namespace widget { + +extern LazyLogModule sWASLog; + +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::UI::Notifications; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace toastnotification; + +// Needed to disambiguate internal and Windows `ToastNotification` classes. +using WinToastNotification = ABI::Windows::UI::Notifications::ToastNotification; +using ToastActivationHandler = + ITypedEventHandler<WinToastNotification*, IInspectable*>; +using ToastDismissedHandler = + ITypedEventHandler<WinToastNotification*, ToastDismissedEventArgs*>; +using ToastFailedHandler = + ITypedEventHandler<WinToastNotification*, ToastFailedEventArgs*>; +using IVectorView_ToastNotification = + Collections::IVectorView<WinToastNotification*>; + +NS_IMPL_ISUPPORTS(ToastNotificationHandler, nsIAlertNotificationImageListener) + +static bool SetNodeValueString(const nsString& aString, IXmlNode* node, + IXmlDocument* xml) { + ComPtr<IXmlText> inputText; + HRESULT hr; + hr = xml->CreateTextNode(HStringReference(aString.get()).Get(), &inputText); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr<IXmlNode> inputTextNode; + hr = inputText.As(&inputTextNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr<IXmlNode> appendedChild; + hr = node->AppendChild(inputTextNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +static bool SetAttribute(ComPtr<IXmlElement>& element, + const HStringReference& name, const nsAString& value) { + HString valueStr; + valueStr.Set(PromiseFlatString(value).get()); + + HRESULT hr = element->SetAttribute(name.Get(), valueStr.Get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +static bool AddActionNode(ComPtr<IXmlDocument>& toastXml, + ComPtr<IXmlNode>& actionsNode, + const nsAString& actionTitle, + const nsAString& launchArg, + const nsAString& actionArgs, + const nsAString& actionPlacement = u""_ns, + const nsAString& activationType = u""_ns) { + ComPtr<IXmlElement> action; + HRESULT hr = + toastXml->CreateElement(HStringReference(L"action").Get(), &action); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + bool success = + SetAttribute(action, HStringReference(L"content"), actionTitle); + NS_ENSURE_TRUE(success, false); + + // Action arguments overwrite the toast's launch arguments, so we need to + // prepend the launch arguments necessary for the Notification Server to + // reconstruct the toast's origin. + // + // Web Notification actions are arbitrary strings; to prevent breaking launch + // argument parsing the action argument must be last. All delimiters after + // `action` are part of the action arugment. + nsAutoString args = launchArg + u"\n"_ns + + nsDependentString(kLaunchArgAction) + u"\n"_ns + + actionArgs; + success = SetAttribute(action, HStringReference(L"arguments"), args); + NS_ENSURE_TRUE(success, false); + + if (!actionPlacement.IsEmpty()) { + success = + SetAttribute(action, HStringReference(L"placement"), actionPlacement); + NS_ENSURE_TRUE(success, false); + } + + if (!activationType.IsEmpty()) { + success = SetAttribute(action, HStringReference(L"activationType"), + activationType); + NS_ENSURE_TRUE(success, false); + + // No special argument handling: when `activationType="system"`, `action` is + // a Windows-specific keyword, generally "dismiss" or "snooze". + success = SetAttribute(action, HStringReference(L"arguments"), actionArgs); + NS_ENSURE_TRUE(success, false); + } + + // Add <action> to <actions> + ComPtr<IXmlNode> actionNode; + hr = action.As(&actionNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr<IXmlNode> appendedChild; + hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +nsresult ToastNotificationHandler::GetWindowsTag(nsAString& aWindowsTag) { + aWindowsTag.Assign(mWindowsTag); + return NS_OK; +} + +nsresult ToastNotificationHandler::SetWindowsTag(const nsAString& aWindowsTag) { + mWindowsTag.Assign(aWindowsTag); + return NS_OK; +} + +// clang - format off +/* Populate the launch argument so the COM server can reconstruct the toast + * origin. + * + * program + * {MOZ_APP_NAME} + * profile + * {path to profile} + */ +// clang-format on +Result<nsString, nsresult> ToastNotificationHandler::GetLaunchArgument() { + nsString launchArg; + + // When the preference is false, the COM notification server will be invoked, + // discover that there is no `program`, and exit (successfully), after which + // Windows will invoke the in-product Windows 8-style callbacks. When true, + // the COM notification server will launch Firefox with sufficient arguments + // for Firefox to handle the notification. + if (!Preferences::GetBool( + "alerts.useSystemBackend.windows.notificationserver.enabled", + false)) { + // Include dummy key/value so that newline appended arguments aren't off by + // one line. + launchArg += u"invalid key\ninvalid value"_ns; + return launchArg; + } + + // `program` argument. + launchArg += nsDependentString(kLaunchArgProgram) + u"\n"_ns MOZ_APP_NAME; + + // `profile` argument. + nsCOMPtr<nsIFile> profDir; + bool wantCurrentProfile = true; +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // Notifications popped from a background task want to invoke Firefox with a + // different profile -- the default browsing profile. We'd prefer to not + // specify a profile, so that the Firefox invoked by the notification server + // chooses its default profile, but this might pop the profile chooser in + // some configurations. + wantCurrentProfile = false; + + nsCOMPtr<nsIToolkitProfileService> profileSvc = + do_GetService(NS_PROFILESERVICE_CONTRACTID); + if (profileSvc) { + nsCOMPtr<nsIToolkitProfile> defaultProfile; + nsresult rv = + profileSvc->GetDefaultProfile(getter_AddRefs(defaultProfile)); + if (NS_SUCCEEDED(rv) && defaultProfile) { + // Not all installations have a default profile. But if one is set, + // then it should have a profile directory. + MOZ_TRY(defaultProfile->GetRootDir(getter_AddRefs(profDir))); + } + } + } +#endif + if (wantCurrentProfile) { + MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profDir))); + } + + if (profDir) { + nsAutoString profilePath; + MOZ_TRY(profDir->GetPath(profilePath)); + launchArg += u"\n"_ns + nsDependentString(kLaunchArgProfile) + u"\n"_ns + + profilePath; + } + + if (!mLaunchUrl.IsEmpty()) { + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgUrl) + u"\n"_ns + mLaunchUrl; + } else if (!mHostPort.IsEmpty()) { + // Fall back to the origin domain if no explicit launch url is provided. + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgUrl) + u"\n"_ns + mHostPort; + } + + if (mIsSystemPrincipal && !mName.IsEmpty()) { + // Privileged alerts include any provided name for metrics. + launchArg += u"\n"_ns + nsDependentString(kLaunchArgPrivilegedName) + + u"\n"_ns + mName; + } + + // `windowsTag` argument. + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgTag) + u"\n"_ns + mWindowsTag; + + // `logging` argument. + if (Preferences::GetBool( + "alerts.useSystemBackend.windows.notificationserver.verbose", + false)) { + // Signal notification to log verbose messages. + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgLogging) + u"\nverbose"_ns; + } + + return launchArg; +} + +static ComPtr<IToastNotificationManagerStatics> +GetToastNotificationManagerStatics() { + ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics; + HRESULT hr = GetActivationFactory( + HStringReference( + RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) + .Get(), + &toastNotificationManagerStatics); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + return toastNotificationManagerStatics; +} + +ToastNotificationHandler::~ToastNotificationHandler() { + if (mImageRequest) { + mImageRequest->Cancel(NS_BINDING_ABORTED); + mImageRequest = nullptr; + } + + if (mHasImage && mImageFile) { + DebugOnly<nsresult> rv = mImageFile->Remove(false); + NS_ASSERTION(NS_SUCCEEDED(rv), "Cannot remove temporary image file"); + } + + UnregisterHandler(); +} + +void ToastNotificationHandler::UnregisterHandler() { + if (mNotification) { + mNotification->remove_Dismissed(mDismissedToken); + mNotification->remove_Activated(mActivatedToken); + mNotification->remove_Failed(mFailedToken); + } + + mNotification = nullptr; + mNotifier = nullptr; + + SendFinished(); +} + +nsresult ToastNotificationHandler::InitAlertAsync( + nsIAlertNotification* aAlert) { + MOZ_TRY(InitWindowsTag()); + + return aAlert->LoadImage(/* aTimeout = */ 0, this, /* aUserData = */ nullptr, + getter_AddRefs(mImageRequest)); +} + +// Uniquely identify this toast to Windows. Existing names and cookies are not +// suitable: we want something generated and unique. This is needed to check if +// toast is still present in the Windows Action Center when we receive a dismiss +// timeout. +// +// Local testing reveals that the space of tags is not global but instead is per +// AUMID. Since an installation uses a unique AUMID incorporating the install +// directory hash, it should not witness another installation's tag. +nsresult ToastNotificationHandler::InitWindowsTag() { + mWindowsTag.Truncate(); + + nsAutoString tag; + + // Multiple profiles might overwrite each other's toast messages when a + // common name is used for a given host port. We prevent this by including + // the profile directory as part of the toast hash. + nsCOMPtr<nsIFile> profDir; + MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profDir))); + MOZ_TRY(profDir->GetPath(tag)); + + if (!mHostPort.IsEmpty()) { + // Notification originated from a web notification. + // `mName` will be in the form `{mHostPort}#tag:{tag}` if the notification + // was created with a tag and `{mHostPort}#notag:{uuid}` otherwise. + tag += mName; + } else { + // Notification originated from the browser chrome. + if (!mName.IsEmpty()) { + tag += u"chrome#tag:"_ns; + // Browser chrome notifications don't follow any convention for naming. + tag += mName; + } else { + // No associated name, append a UUID to prevent reuse of the same tag. + nsIDToCString uuidString(nsID::GenerateUUID()); + size_t len = strlen(uuidString.get()); + MOZ_ASSERT(len == NSID_LENGTH - 1); + nsAutoString uuid; + CopyASCIItoUTF16(nsDependentCSubstring(uuidString.get(), len), uuid); + + tag += u"chrome#notag:"_ns; + tag += uuid; + } + } + + // Windows notification tags are limited to 16 characters, or 64 characters + // after the Creators Update; therefore we hash the tag to fit the minimum + // range. + HashNumber hash = HashString(tag); + mWindowsTag.AppendPrintf("%010u", hash); + + return NS_OK; +} + +ComPtr<IXmlDocument> ToastNotificationHandler::CreateToastXmlDocument() { + ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics = + GetToastNotificationManagerStatics(); + NS_ENSURE_TRUE(toastNotificationManagerStatics, nullptr); + + ToastTemplateType toastTemplate; + if (mHostPort.IsEmpty()) { + toastTemplate = + mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText03 + : ToastTemplateType::ToastTemplateType_ToastText03; + } else { + toastTemplate = + mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText04 + : ToastTemplateType::ToastTemplateType_ToastText04; + } + + ComPtr<IXmlDocument> toastXml; + toastNotificationManagerStatics->GetTemplateContent(toastTemplate, &toastXml); + + if (!toastXml) { + return nullptr; + } + + nsresult ns; + HRESULT hr; + bool success; + + if (mHasImage) { + ComPtr<IXmlNodeList> toastImageElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"image").Get(), + &toastImageElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> imageNode; + hr = toastImageElements->Item(0, &imageNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlElement> image; + hr = imageNode.As(&image); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetAttribute(image, HStringReference(L"src"), mImageUri); + NS_ENSURE_TRUE(success, nullptr); + } + + ComPtr<IXmlNodeList> toastTextElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"text").Get(), + &toastTextElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> titleTextNodeRoot; + hr = toastTextElements->Item(0, &titleTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> msgTextNodeRoot; + hr = toastTextElements->Item(1, &msgTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetNodeValueString(mTitle, titleTextNodeRoot.Get(), toastXml.Get()); + NS_ENSURE_TRUE(success, nullptr); + + success = SetNodeValueString(mMsg, msgTextNodeRoot.Get(), toastXml.Get()); + NS_ENSURE_TRUE(success, nullptr); + + ComPtr<IXmlNodeList> toastElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"toast").Get(), + &toastElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> toastNodeRoot; + hr = toastElements->Item(0, &toastNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlElement> toastElement; + hr = toastNodeRoot.As(&toastElement); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + if (mRequireInteraction) { + success = SetAttribute(toastElement, HStringReference(L"scenario"), + u"reminder"_ns); + NS_ENSURE_TRUE(success, nullptr); + } + + auto maybeLaunchArg = GetLaunchArgument(); + NS_ENSURE_TRUE(maybeLaunchArg.isOk(), nullptr); + nsString launchArg = maybeLaunchArg.unwrap(); + + success = SetAttribute(toastElement, HStringReference(L"launch"), launchArg); + NS_ENSURE_TRUE(success, nullptr); + + MOZ_LOG(sWASLog, LogLevel::Debug, + ("launchArg: '%s'", NS_ConvertUTF16toUTF8(launchArg).get())); + + // On modern Windows (10+), use newer toast layout, which makes images larger, + // for system (chrome-privileged) toasts. + if (IsWin10OrLater() && mIsSystemPrincipal) { + ComPtr<IXmlNodeList> bindingElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"binding").Get(), + &bindingElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> bindingNodeRoot; + hr = bindingElements->Item(0, &bindingNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlElement> bindingElement; + hr = bindingNodeRoot.As(&bindingElement); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetAttribute(bindingElement, HStringReference(L"template"), + u"ToastGeneric"_ns); + NS_ENSURE_TRUE(success, nullptr); + } + + ComPtr<IXmlElement> actions; + hr = toastXml->CreateElement(HStringReference(L"actions").Get(), &actions); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IXmlNode> actionsNode; + hr = actions.As(&actionsNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + nsCOMPtr<nsIStringBundleService> sbs = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + NS_ENSURE_TRUE(sbs, nullptr); + + nsCOMPtr<nsIStringBundle> bundle; + sbs->CreateBundle("chrome://alerts/locale/alert.properties", + getter_AddRefs(bundle)); + NS_ENSURE_TRUE(bundle, nullptr); + + if (!mHostPort.IsEmpty()) { + AutoTArray<nsString, 1> formatStrings = {mHostPort}; + + ComPtr<IXmlNode> urlTextNodeRoot; + hr = toastTextElements->Item(2, &urlTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + nsAutoString urlReference; + bundle->FormatStringFromName("source.label", formatStrings, urlReference); + + success = + SetNodeValueString(urlReference, urlTextNodeRoot.Get(), toastXml.Get()); + NS_ENSURE_TRUE(success, nullptr); + + if (IsWin10AnniversaryUpdateOrLater()) { + ComPtr<IXmlElement> placementText; + hr = urlTextNodeRoot.As(&placementText); + if (SUCCEEDED(hr)) { + // placement is supported on Windows 10 Anniversary Update or later + SetAttribute(placementText, HStringReference(L"placement"), + u"attribution"_ns); + } + } + + nsAutoString disableButtonTitle; + ns = bundle->FormatStringFromName("webActions.disableForOrigin.label", + formatStrings, disableButtonTitle); + NS_ENSURE_SUCCESS(ns, nullptr); + + AddActionNode(toastXml, actionsNode, disableButtonTitle, launchArg, + u"snooze"_ns, u"contextmenu"_ns); + } + + bool wantSettings = true; +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // Notifications popped from a background task want to invoke Firefox with a + // different profile -- the default browsing profile. Don't link to Firefox + // settings in some different profile: the relevant Firefox settings won't + // take effect. + wantSettings = false; + } +#endif + if (MOZ_LIKELY(wantSettings)) { + nsAutoString settingsButtonTitle; + bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle); + success = AddActionNode(toastXml, actionsNode, settingsButtonTitle, + launchArg, u"settings"_ns, u"contextmenu"_ns); + NS_ENSURE_TRUE(success, nullptr); + } + + for (const auto& action : mActions) { + // Bug 1778596: include per-action icon from image URL. + nsString title; + ns = action->GetTitle(title); + NS_ENSURE_SUCCESS(ns, nullptr); + + nsString actionString; + ns = action->GetAction(actionString); + NS_ENSURE_SUCCESS(ns, nullptr); + + // Privileged/chrome alerts can have actions that are activated by Windows. + // Recognize these actions and enable these activations. + bool activationType(false); + ns = action->GetWindowsSystemActivationType(&activationType); + NS_ENSURE_SUCCESS(ns, nullptr); + + nsString activationTypeString( + (mIsSystemPrincipal && activationType) ? u"system"_ns : u""_ns); + success = AddActionNode(toastXml, actionsNode, title, launchArg, + actionString, u""_ns, activationTypeString); + NS_ENSURE_TRUE(success, nullptr); + } + + ComPtr<IXmlNode> appendedChild; + hr = toastNodeRoot->AppendChild(actionsNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + if (mIsSilent) { + ComPtr<IXmlNode> audioNode; + // Create <audio silent="true"/> for silent notifications. + ComPtr<IXmlElement> audio; + hr = toastXml->CreateElement(HStringReference(L"audio").Get(), &audio); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + SetAttribute(audio, HStringReference(L"silent"), u"true"_ns); + + hr = audio.As(&audioNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + hr = toastNodeRoot->AppendChild(audioNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + } + + return toastXml; +} + +nsresult ToastNotificationHandler::CreateToastXmlString( + const nsAString& aImageURL, nsAString& aString) { + HRESULT hr; + + if (!aImageURL.IsEmpty()) { + // For testing: don't fetch and write image to disk, just include the URL. + mHasImage = true; + mImageUri.Assign(aImageURL); + } + + ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument(); + if (!toastXml) { + return NS_ERROR_FAILURE; + } + + ComPtr<IXmlNodeSerializer> ser; + hr = toastXml.As(&ser); + NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE); + + HString data; + hr = ser->GetXml(data.GetAddressOf()); + NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE); + + uint32_t len = 0; + const wchar_t* rawData = data.GetRawBuffer(&len); + NS_ENSURE_TRUE(rawData, NS_ERROR_FAILURE); + aString.Assign(rawData, len); + + return NS_OK; +} + +bool ToastNotificationHandler::ShowAlert() { + if (!mBackend->IsActiveHandler(mName, this)) { + return false; + } + + ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument(); + + if (!toastXml) { + return false; + } + + return CreateWindowsNotificationFromXml(toastXml); +} + +bool ToastNotificationHandler::IsPrivate() { return mInPrivateBrowsing; } + +void ToastNotificationHandler::HideAlert() { + if (mNotifier && mNotification) { + mNotifier->Hide(mNotification.Get()); + } +} + +bool ToastNotificationHandler::CreateWindowsNotificationFromXml( + ComPtr<IXmlDocument>& aXml) { + ComPtr<IToastNotificationFactory> factory; + HRESULT hr; + + hr = GetActivationFactory( + HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification) + .Get(), + &factory); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = factory->CreateToastNotification(aXml.Get(), &mNotification); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + RefPtr<ToastNotificationHandler> self = this; + + hr = mNotification->add_Activated( + Callback<ToastActivationHandler>([self](IToastNotification* aNotification, + IInspectable* aInspectable) { + return self->OnActivate(ComPtr<IToastNotification>(aNotification), + ComPtr<IInspectable>(aInspectable)); + }).Get(), + &mActivatedToken); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = mNotification->add_Dismissed( + Callback<ToastDismissedHandler>([self](IToastNotification* aNotification, + IToastDismissedEventArgs* aArgs) { + return self->OnDismiss(ComPtr<IToastNotification>(aNotification), + ComPtr<IToastDismissedEventArgs>(aArgs)); + }).Get(), + &mDismissedToken); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = mNotification->add_Failed( + Callback<ToastFailedHandler>([self](IToastNotification* aNotification, + IToastFailedEventArgs* aArgs) { + return self->OnFail(ComPtr<IToastNotification>(aNotification), + ComPtr<IToastFailedEventArgs>(aArgs)); + }).Get(), + &mFailedToken); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + // `IToastNotification2` not supported on versions older than Windows 10. + if (IsWin10OrLater()) { + ComPtr<IToastNotification2> notification2; + hr = mNotification.As(¬ification2); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + HString hTag; + hr = hTag.Set(mWindowsTag.get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = notification2->put_Tag(hTag.Get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + } + + ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics = + GetToastNotificationManagerStatics(); + NS_ENSURE_TRUE(toastNotificationManagerStatics, false); + + HString aumid; + hr = aumid.Set(mAumid.get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + hr = toastNotificationManagerStatics->CreateToastNotifierWithId(aumid.Get(), + &mNotifier); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + hr = mNotifier->Show(mNotification.Get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + if (mAlertListener) { + mAlertListener->Observe(nullptr, "alertshow", mCookie.get()); + } + + return true; +} + +void ToastNotificationHandler::SendFinished() { + if (!mSentFinished && mAlertListener) { + mAlertListener->Observe(nullptr, "alertfinished", mCookie.get()); + } + + mSentFinished = true; +} + +HRESULT +ToastNotificationHandler::OnActivate( + const ComPtr<IToastNotification>& notification, + const ComPtr<IInspectable>& inspectable) { + MOZ_LOG(sWASLog, LogLevel::Info, ("OnActivate")); + + if (mAlertListener) { + // Extract the `action` value from the argument string. + nsAutoString actionString; + if (inspectable) { + ComPtr<IToastActivatedEventArgs> eventArgs; + HRESULT hr = inspectable.As(&eventArgs); + if (SUCCEEDED(hr)) { + HString arguments; + hr = eventArgs->get_Arguments(arguments.GetAddressOf()); + if (SUCCEEDED(hr)) { + uint32_t len = 0; + const char16_t* buffer = (char16_t*)arguments.GetRawBuffer(&len); + if (buffer) { + MOZ_LOG(sWASLog, LogLevel::Info, + ("OnActivate: arguments: %s", + NS_ConvertUTF16toUTF8(buffer).get())); + + // Toast arguments are a newline separated key/value combination of + // launch arguments and an optional action argument provided as an + // argument to the toast's constructor. After the `action` key is + // found, the remainder of toast argument (including newlines) is + // the `action` value. + Tokenizer16 parse(buffer); + nsDependentSubstring token; + + while (parse.ReadUntil(Tokenizer16::Token::NewLine(), token)) { + if (token == nsDependentString(kLaunchArgAction)) { + Unused << parse.ReadUntil(Tokenizer16::Token::EndOfFile(), + actionString); + } else { + // Next line is a value in a key/value pair, skip. + parse.SkipUntil(Tokenizer16::Token::NewLine()); + } + // Skip newline. + Tokenizer16::Token unused; + Unused << parse.Next(unused); + } + } + } + } + } + + if (actionString.EqualsLiteral("settings")) { + mAlertListener->Observe(nullptr, "alertsettingscallback", mCookie.get()); + } else if (actionString.EqualsLiteral("snooze")) { + mAlertListener->Observe(nullptr, "alertdisablecallback", mCookie.get()); + } else if (mClickable) { + // When clicking toast, focus moves to another process, but we want to set + // focus on Firefox process. + nsCOMPtr<nsIWindowMediator> winMediator( + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID)); + if (winMediator) { + nsCOMPtr<mozIDOMWindowProxy> navWin; + winMediator->GetMostRecentWindow(u"navigator:browser", + getter_AddRefs(navWin)); + if (navWin) { + nsCOMPtr<nsIWidget> widget = + WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin)); + if (widget) { + SetForegroundWindow( + static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW))); + } + } + } + mAlertListener->Observe(nullptr, "alertclickcallback", mCookie.get()); + } + } + mBackend->RemoveHandler(mName, this); + return S_OK; +} + +// Returns `nullptr` if no such toast exists. +/* static */ ComPtr<IToastNotification> +ToastNotificationHandler::FindNotificationByTag(const nsAString& aWindowsTag, + const nsAString& aAumid) { + HRESULT hr = S_OK; + + HString current_id; + current_id.Set(PromiseFlatString(aWindowsTag).get()); + + ComPtr<IToastNotificationManagerStatics> manager = + GetToastNotificationManagerStatics(); + NS_ENSURE_TRUE(manager, nullptr); + + ComPtr<IToastNotificationManagerStatics2> manager2; + hr = manager.As(&manager2); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IToastNotificationHistory> history; + hr = manager2->get_History(&history); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + ComPtr<IToastNotificationHistory2> history2; + hr = history.As(&history2); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr<IVectorView_ToastNotification> toasts; + hr = history2->GetHistoryWithId( + HStringReference(PromiseFlatString(aAumid).get()).Get(), &toasts); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + unsigned int hist_size; + hr = toasts->get_Size(&hist_size); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + for (unsigned int i = 0; i < hist_size; i++) { + ComPtr<IToastNotification> hist_toast; + hr = toasts->GetAt(i, &hist_toast); + if (NS_WARN_IF(FAILED(hr))) { + continue; + } + + ComPtr<IToastNotification2> hist_toast2; + hr = hist_toast.As(&hist_toast2); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + HString history_id; + hr = hist_toast2->get_Tag(history_id.GetAddressOf()); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + // We can not directly compare IToastNotification objects; their IUnknown + // pointers should be equivalent but under inspection were not. Therefore we + // use the notification's tag instead. + if (current_id == history_id) { + return hist_toast; + } + } + + return nullptr; +} + +/* static */ HRESULT ToastNotificationHandler::GetLaunchArgumentValueForKey( + const ComPtr<IToastNotification> toast, const nsAString& key, + nsAString& value) { + ComPtr<IXmlDocument> xml; + HRESULT hr = toast->get_Content(&xml); + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + + ComPtr<IXmlElement> root; + hr = xml->get_DocumentElement(&root); + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + + HString launchHString; + hr = root->GetAttribute(HStringReference(L"launch").Get(), + launchHString.GetAddressOf()); + + NS_ENSURE_TRUE(SUCCEEDED(hr), hr); + + unsigned int len; + const wchar_t* launchPtr = launchHString.GetRawBuffer(&len); + nsAutoString launch(launchPtr, len); + + // Toast arguments are a newline separated key/value combination of launch + // arguments and an optional action argument provided as an argument to the + // toast's constructor. After the `action` key is found, the remainder of + // toast argument (including newlines) is the `action` value. + Tokenizer16 parse((char16_t*)launchPtr); + nsDependentSubstring token; + + while (parse.ReadUntil(Tokenizer16::Token::NewLine(), token)) { + if (token == nsDependentString(kLaunchArgAction)) { + // As soon as we see an action, we're done: we don't want to take a "key" + // from the user-provided action string. + return E_FAIL; + } else if (token.Equals(key)) { + Unused << parse.ReadUntil(Tokenizer16::Token::NewLine(), value); + return S_OK; + } else { + // Next line is a value in a key/value pair, skip. + parse.SkipUntil(Tokenizer16::Token::NewLine()); + // Skip newline. + Tokenizer16::Token unused; + Unused << parse.Next(unused); + } + } + + return E_FAIL; +} + +/* static */ nsresult +ToastNotificationHandler::FindLaunchURLAndPrivilegedNameForWindowsTag( + const nsAString& aWindowsTag, const nsAString& aAumid, bool& aFoundTag, + nsAString& aLaunchUrl, nsAString& aPrivilegedName) { + aFoundTag = false; + aLaunchUrl.Truncate(); + aPrivilegedName.Truncate(); + + ComPtr<IToastNotification> toast = + ToastNotificationHandler::FindNotificationByTag(aWindowsTag, aAumid); + MOZ_LOG(sWASLog, LogLevel::Debug, ("Found toast [%p]", toast.Get())); + NS_ENSURE_TRUE(toast, NS_OK); + + aFoundTag = true; + + HRESULT hr = ToastNotificationHandler::GetLaunchArgumentValueForKey( + toast, nsDependentString(kLaunchArgUrl), aLaunchUrl); + + if (!SUCCEEDED(hr)) { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Did not find %ls [hr=0x%08lX]", kLaunchArgUrl, hr)); + aLaunchUrl.SetIsVoid(true); + } else { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Found %ls [%s]", kLaunchArgUrl, + NS_ConvertUTF16toUTF8(aLaunchUrl).get())); + } + + hr = ToastNotificationHandler::GetLaunchArgumentValueForKey( + toast, nsDependentString(kLaunchArgPrivilegedName), aPrivilegedName); + + if (!SUCCEEDED(hr)) { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Did not find %ls [hr=0x%08lX]", kLaunchArgPrivilegedName, hr)); + aPrivilegedName.SetIsVoid(true); + } else { + MOZ_LOG(sWASLog, LogLevel::Debug, + ("Found %ls [%s]", kLaunchArgPrivilegedName, + NS_ConvertUTF16toUTF8(aPrivilegedName).get())); + } + + return NS_OK; +} + +// A single toast message can receive multiple dismiss events, at most one for +// the popup and at most one for the action center. We can't simply count +// dismiss events as the user may have disabled either popups or action center +// notifications, therefore we have to check if the toast remains in the history +// (action center) to determine if the toast is fully dismissed. +HRESULT +ToastNotificationHandler::OnDismiss( + const ComPtr<IToastNotification>& notification, + const ComPtr<IToastDismissedEventArgs>& aArgs) { + // Multiple dismiss events only occur on Windows 10 and later, prior versions + // of Windows didn't include `IToastNotification2`. + if (IsWin10OrLater()) { + ComPtr<IToastNotification2> notification2; + HRESULT hr = notification.As(¬ification2); + NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL); + + HString tagHString; + hr = notification2->get_Tag(tagHString.GetAddressOf()); + NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL); + + unsigned int len; + const wchar_t* tagPtr = tagHString.GetRawBuffer(&len); + nsAutoString tag(tagPtr, len); + + if (FindNotificationByTag(tag, mAumid)) { + return S_OK; + } + } + + SendFinished(); + mBackend->RemoveHandler(mName, this); + return S_OK; +} + +HRESULT +ToastNotificationHandler::OnFail(const ComPtr<IToastNotification>& notification, + const ComPtr<IToastFailedEventArgs>& aArgs) { + HRESULT err; + aArgs->get_ErrorCode(&err); + MOZ_LOG(sWASLog, LogLevel::Error, + ("Error creating notification, error: %ld", err)); + + SendFinished(); + mBackend->RemoveHandler(mName, this); + return S_OK; +} + +nsresult ToastNotificationHandler::TryShowAlert() { + if (NS_WARN_IF(!ShowAlert())) { + mBackend->RemoveHandler(mName, this); + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +NS_IMETHODIMP +ToastNotificationHandler::OnImageMissing(nsISupports*) { + return TryShowAlert(); +} + +NS_IMETHODIMP +ToastNotificationHandler::OnImageReady(nsISupports*, imgIRequest* aRequest) { + nsresult rv = AsyncSaveImage(aRequest); + if (NS_FAILED(rv)) { + return TryShowAlert(); + } + return rv; +} + +nsresult ToastNotificationHandler::AsyncSaveImage(imgIRequest* aRequest) { + nsresult rv = + NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mImageFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mImageFile->Append(u"notificationimages"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mImageFile->Create(nsIFile::DIRECTORY_TYPE, 0500); + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) { + return rv; + } + + nsID uuid; + rv = nsID::GenerateUUIDInPlace(uuid); + NS_ENSURE_SUCCESS(rv, rv); + + NSID_TrimBracketsASCII uuidStr(uuid); + uuidStr.AppendLiteral(".png"); + mImageFile->AppendNative(uuidStr); + + nsCOMPtr<imgIContainer> imgContainer; + rv = aRequest->GetImage(getter_AddRefs(imgContainer)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMainThreadPtrHandle<ToastNotificationHandler> self( + new nsMainThreadPtrHolder<ToastNotificationHandler>( + "ToastNotificationHandler", this)); + + nsCOMPtr<nsIFile> imageFile(mImageFile); + RefPtr<mozilla::gfx::SourceSurface> surface = imgContainer->GetFrame( + imgIContainer::FRAME_FIRST, + imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + "ToastNotificationHandler::AsyncWriteImage", + [self, imageFile, surface]() -> void { + nsresult rv = NS_ERROR_FAILURE; + if (surface) { + FILE* file = nullptr; + rv = imageFile->OpenANSIFileDesc("wb", &file); + if (NS_SUCCEEDED(rv)) { + rv = gfxUtils::EncodeSourceSurface(surface, ImageType::PNG, u""_ns, + gfxUtils::eBinaryEncode, file); + fclose(file); + } + } + + nsCOMPtr<nsIRunnable> cbRunnable = NS_NewRunnableFunction( + "ToastNotificationHandler::AsyncWriteImageCb", + [self, rv]() -> void { + auto handler = const_cast<ToastNotificationHandler*>(self.get()); + handler->OnWriteImageFinished(rv); + }); + + NS_DispatchToMainThread(cbRunnable); + }); + + return mBackend->BackgroundDispatch(r); +} + +void ToastNotificationHandler::OnWriteImageFinished(nsresult rv) { + if (NS_SUCCEEDED(rv)) { + OnWriteImageSuccess(); + } + TryShowAlert(); +} + +nsresult ToastNotificationHandler::OnWriteImageSuccess() { + nsresult rv; + + nsCOMPtr<nsIURI> fileURI; + rv = NS_NewFileURI(getter_AddRefs(fileURI), mImageFile); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString uriStr; + rv = fileURI->GetSpec(uriStr); + NS_ENSURE_SUCCESS(rv, rv); + + AppendUTF8toUTF16(uriStr, mImageUri); + + mHasImage = true; + + return NS_OK; +} + +} // namespace widget +} // namespace mozilla |