summaryrefslogtreecommitdiffstats
path: root/widget/windows/ToastNotification.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /widget/windows/ToastNotification.cpp
parentInitial commit. (diff)
downloadfirefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.tar.xz
firefox-esr-37a0381f8351b370577b65028ba1f6563ae23fdf.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/windows/ToastNotification.cpp')
-rw-r--r--widget/windows/ToastNotification.cpp913
1 files changed, 913 insertions, 0 deletions
diff --git a/widget/windows/ToastNotification.cpp b/widget/windows/ToastNotification.cpp
new file mode 100644
index 0000000000..9b3e2759f9
--- /dev/null
+++ b/widget/windows/ToastNotification.cpp
@@ -0,0 +1,913 @@
+/* -*- 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 "ToastNotification.h"
+
+#include <windows.h>
+#include <appmodel.h>
+#include <ktmw32.h>
+#include <windows.foundation.h>
+#include <wrl/client.h>
+
+#include "ErrorList.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/Buffer.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/DynamicallyLinkedFunctionPtr.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/mscom/COMWrappers.h"
+#include "mozilla/mscom/Utils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Services.h"
+#include "mozilla/WidgetUtils.h"
+#include "mozilla/WindowsVersion.h"
+#include "nsAppRunner.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsIObserverService.h"
+#include "nsIWindowMediator.h"
+#include "nsPIDOMWindow.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "nsWindowsHelpers.h"
+#include "nsXREDirProvider.h"
+#include "prenv.h"
+#include "ToastNotificationHandler.h"
+#include "ToastNotificationHeaderOnlyUtils.h"
+#include "WinTaskbar.h"
+#include "WinUtils.h"
+
+namespace mozilla {
+namespace widget {
+
+using namespace toastnotification;
+
+using namespace ABI::Windows::Foundation;
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+// Needed to disambiguate internal and Windows `ToastNotification` classes.
+using namespace ABI::Windows::UI::Notifications;
+using WinToastNotification = ABI::Windows::UI::Notifications::ToastNotification;
+using IVectorView_ToastNotification =
+ ABI::Windows::Foundation::Collections::IVectorView<WinToastNotification*>;
+using IVectorView_ScheduledToastNotification =
+ ABI::Windows::Foundation::Collections::IVectorView<
+ ScheduledToastNotification*>;
+
+LazyLogModule sWASLog("WindowsAlertsService");
+
+NS_IMPL_ISUPPORTS(ToastNotification, nsIAlertsService, nsIWindowsAlertsService,
+ nsIAlertsDoNotDisturb, nsIObserver)
+
+ToastNotification::ToastNotification() = default;
+
+ToastNotification::~ToastNotification() = default;
+
+nsresult ToastNotification::Init() {
+ if (!IsWin8OrLater()) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ if (!PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR")) {
+ // Windows Toast Notification requires AppId. But allow `xpcshell` to
+ // create the service to test other functionality.
+ if (!EnsureAumidRegistered()) {
+ MOZ_LOG(sWASLog, LogLevel::Warning, ("Failed to register AUMID!"));
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Info, ("Using dummy AUMID in xpcshell test"));
+ mAumid.emplace(u"XpcshellTestToastAumid"_ns);
+ }
+
+ MOZ_LOG(sWASLog, LogLevel::Info,
+ ("Using AUMID: '%s'", NS_ConvertUTF16toUTF8(mAumid.ref()).get()));
+
+ nsCOMPtr<nsIObserverService> obsServ =
+ mozilla::services::GetObserverService();
+ if (obsServ) {
+ Unused << NS_WARN_IF(
+ NS_FAILED(obsServ->AddObserver(this, "last-pb-context-exited", false)));
+ Unused << NS_WARN_IF(
+ NS_FAILED(obsServ->AddObserver(this, "quit-application", false)));
+ }
+
+ return NS_OK;
+}
+
+bool ToastNotification::EnsureAumidRegistered() {
+ // Check if this is an MSIX install, app identity is provided by the package
+ // so no registration is necessary.
+ if (AssignIfMsixAumid(mAumid)) {
+ MOZ_LOG(
+ sWASLog, LogLevel::Info,
+ ("Found MSIX AUMID: '%s'", NS_ConvertUTF16toUTF8(mAumid.ref()).get()));
+ return true;
+ }
+
+ // Fall back to start menu shortcut for Windows 8; toast AUMID registration in
+ // the registry only works in Windows 10+.
+ if (!IsWin10OrLater()) {
+ nsAutoString aumid;
+ if (!WinTaskbar::GetAppUserModelID(aumid)) {
+ return false;
+ }
+
+ mAumid = Some(aumid);
+ return true;
+ }
+
+ nsAutoString installHash;
+ nsresult rv = gDirServiceProvider->GetInstallHash(installHash);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Check if toasts were registered during NSIS/MSI installation.
+ if (AssignIfNsisAumid(installHash, mAumid)) {
+ MOZ_LOG(sWASLog, LogLevel::Info,
+ ("Found AUMID from installer (with install hash '%s'): '%s'",
+ NS_ConvertUTF16toUTF8(installHash).get(),
+ NS_ConvertUTF16toUTF8(mAumid.ref()).get()));
+ return true;
+ }
+
+ // No AUMID registered, fall through to runtime registration for development
+ // and portable builds.
+ if (RegisterRuntimeAumid(installHash, mAumid)) {
+ MOZ_LOG(
+ sWASLog, LogLevel::Info,
+ ("Updated AUMID registration at runtime (for install hash '%s'): '%s'",
+ NS_ConvertUTF16toUTF8(installHash).get(),
+ NS_ConvertUTF16toUTF8(mAumid.ref()).get()));
+ return true;
+ }
+
+ MOZ_LOG(sWASLog, LogLevel::Warning,
+ ("Failed to register AUMID at runtime! (for install hash '%s')",
+ NS_ConvertUTF16toUTF8(installHash).get()));
+ return false;
+}
+
+bool ToastNotification::AssignIfMsixAumid(Maybe<nsAutoString>& aAumid) {
+ // `GetCurrentApplicationUserModelId` added in Windows 8.
+ DynamicallyLinkedFunctionPtr<decltype(&GetCurrentApplicationUserModelId)>
+ pGetCurrentApplicationUserModelId(L"kernel32.dll",
+ "GetCurrentApplicationUserModelId");
+ if (!pGetCurrentApplicationUserModelId) {
+ return false;
+ }
+
+ UINT32 len = 0;
+ // ERROR_INSUFFICIENT_BUFFER signals that we're in an MSIX package, and
+ // therefore should use the package's AUMID.
+ if (pGetCurrentApplicationUserModelId(&len, nullptr) !=
+ ERROR_INSUFFICIENT_BUFFER) {
+ MOZ_LOG(sWASLog, LogLevel::Debug, ("Not an MSIX package"));
+ return false;
+ }
+ mozilla::Buffer<wchar_t> buffer(len);
+ LONG success = pGetCurrentApplicationUserModelId(&len, buffer.Elements());
+ NS_ENSURE_TRUE(success == ERROR_SUCCESS, false);
+
+ aAumid.emplace(buffer.Elements());
+ return true;
+}
+
+bool ToastNotification::AssignIfNsisAumid(nsAutoString& aInstallHash,
+ Maybe<nsAutoString>& aAumid) {
+ nsAutoString nsisAumidName =
+ u""_ns MOZ_TOAST_APP_NAME u"Toast-"_ns + aInstallHash;
+ nsAutoString nsisAumidPath = u"AppUserModelId\\"_ns + nsisAumidName;
+ if (!WinUtils::HasRegistryKey(HKEY_CLASSES_ROOT, nsisAumidPath.get())) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("No CustomActivator value from installer in key 'HKCR\\%s'",
+ NS_ConvertUTF16toUTF8(nsisAumidPath).get()));
+ return false;
+ }
+
+ aAumid.emplace(nsisAumidName);
+ return true;
+}
+
+bool ToastNotification::RegisterRuntimeAumid(nsAutoString& aInstallHash,
+ Maybe<nsAutoString>& aAumid) {
+ // Portable AUMID slightly differs from installed AUMID so we can
+ // differentiate installed to HKCU vs portable installs if necessary.
+ nsAutoString portableAumid =
+ u""_ns MOZ_TOAST_APP_NAME u"PortableToast-"_ns + aInstallHash;
+
+ nsCOMPtr<nsIFile> appdir;
+ nsresult rv = gDirServiceProvider->GetGREDir()->Clone(getter_AddRefs(appdir));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCOMPtr<nsIFile> icon;
+ rv = appdir->Clone(getter_AddRefs(icon));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = icon->Append(u"browser"_ns);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = icon->Append(u"VisualElements"_ns);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = icon->Append(u"VisualElements_70.png"_ns);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoString iconPath;
+ rv = icon->GetPath(iconPath);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCOMPtr<nsIFile> comDll;
+ rv = appdir->Clone(getter_AddRefs(comDll));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = comDll->Append(u"notificationserver.dll"_ns);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoString dllPath;
+ rv = comDll->GetPath(dllPath);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoHandle txn;
+ // Manipulate the registry using a transaction so that any failures are
+ // rolled back.
+ wchar_t transactionName[] = L"" MOZ_TOAST_APP_NAME L" toast registration";
+ txn.own(::CreateTransaction(nullptr, nullptr, TRANSACTION_DO_NOT_PROMOTE, 0,
+ 0, 0, transactionName));
+ NS_ENSURE_TRUE(txn.get() != INVALID_HANDLE_VALUE, false);
+
+ LSTATUS status;
+
+ auto RegisterKey = [&](const nsAString& path, nsAutoRegKey& key) {
+ HKEY rawKey;
+ status = ::RegCreateKeyTransactedW(
+ HKEY_CURRENT_USER, PromiseFlatString(path).get(), 0, nullptr,
+ REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr, &rawKey, nullptr, txn,
+ nullptr);
+ NS_ENSURE_TRUE(status == ERROR_SUCCESS, false);
+
+ key.own(rawKey);
+ return true;
+ };
+ auto RegisterValue = [&](nsAutoRegKey& key, const nsAString& name,
+ unsigned long type, const nsAString& data) {
+ status = ::RegSetValueExW(
+ key, PromiseFlatString(name).get(), 0, type,
+ static_cast<const BYTE*>(PromiseFlatString(data).get()),
+ (data.Length() + 1) * sizeof(wchar_t));
+
+ return status == ERROR_SUCCESS;
+ };
+
+ // clang-format off
+ /* Writes the following keys and values to the registry.
+ * HKEY_CURRENT_USER\Software\Classes\AppID\{GUID} DllSurrogate : REG_SZ = ""
+ * \AppUserModelId\{MOZ_TOAST_APP_NAME}PortableToast-{install hash} CustomActivator : REG_SZ = {GUID}
+ * DisplayName : REG_EXPAND_SZ = {display name}
+ * IconUri : REG_EXPAND_SZ = {icon path}
+ * \CLSID\{GUID} AppID : REG_SZ = {GUID}
+ * \InprocServer32 (Default) : REG_SZ = {notificationserver.dll path}
+ */
+ // clang-format on
+
+ constexpr nsLiteralString classes = u"Software\\Classes\\"_ns;
+
+ nsAutoString aumid = classes + u"AppUserModelId\\"_ns + portableAumid;
+ nsAutoRegKey aumidKey;
+ NS_ENSURE_TRUE(RegisterKey(aumid, aumidKey), false);
+
+ nsAutoString guidStr;
+ {
+ DWORD bufferSizeBytes = NSID_LENGTH * sizeof(wchar_t);
+ Buffer<wchar_t> guidBuffer(bufferSizeBytes);
+ status = ::RegGetValueW(HKEY_CURRENT_USER, aumid.get(), L"CustomActivator",
+ RRF_RT_REG_SZ, 0, guidBuffer.Elements(),
+ &bufferSizeBytes);
+
+ CLSID unused;
+ if (status == ERROR_SUCCESS &&
+ SUCCEEDED(CLSIDFromString(guidBuffer.Elements(), &unused))) {
+ guidStr = guidBuffer.Elements();
+ } else {
+ nsIDToCString uuidString(nsID::GenerateUUID());
+ size_t len = strlen(uuidString.get());
+ MOZ_ASSERT(len == NSID_LENGTH - 1);
+ CopyASCIItoUTF16(nsDependentCSubstring(uuidString.get(), len), guidStr);
+ }
+
+ if (status == ERROR_SUCCESS) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Existing CustomActivator guid found: '%s'",
+ NS_ConvertUTF16toUTF8(guidStr).get()));
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("New CustomActivator guid generated: '%s'",
+ NS_ConvertUTF16toUTF8(guidStr).get()));
+ }
+ }
+ NS_ENSURE_TRUE(
+ RegisterValue(aumidKey, u"CustomActivator"_ns, REG_SZ, guidStr), false);
+ nsAutoString brandName;
+ WidgetUtils::GetBrandShortName(brandName);
+ NS_ENSURE_TRUE(
+ RegisterValue(aumidKey, u"DisplayName"_ns, REG_EXPAND_SZ, brandName),
+ false);
+ NS_ENSURE_TRUE(
+ RegisterValue(aumidKey, u"IconUri"_ns, REG_EXPAND_SZ, iconPath), false);
+
+ nsAutoString appid = classes + u"AppID\\"_ns + guidStr;
+ nsAutoRegKey appidKey;
+ NS_ENSURE_TRUE(RegisterKey(appid, appidKey), false);
+ NS_ENSURE_TRUE(RegisterValue(appidKey, u"DllSurrogate"_ns, REG_SZ, u""_ns),
+ false);
+
+ nsAutoString clsid = classes + u"CLSID\\"_ns + guidStr;
+ nsAutoRegKey clsidKey;
+ NS_ENSURE_TRUE(RegisterKey(clsid, clsidKey), false);
+ NS_ENSURE_TRUE(RegisterValue(clsidKey, u"AppID"_ns, REG_SZ, guidStr), false);
+
+ nsAutoString inproc = clsid + u"\\InprocServer32"_ns;
+ nsAutoRegKey inprocKey;
+ NS_ENSURE_TRUE(RegisterKey(inproc, inprocKey), false);
+ // Set the component's path to this DLL
+ NS_ENSURE_TRUE(RegisterValue(inprocKey, u""_ns, REG_SZ, dllPath), false);
+
+ NS_ENSURE_TRUE(::CommitTransaction(txn), false);
+
+ MOZ_LOG(
+ sWASLog, LogLevel::Debug,
+ ("Updated registration for CustomActivator value in key 'HKCU\\%s': '%s'",
+ NS_ConvertUTF16toUTF8(aumid).get(),
+ NS_ConvertUTF16toUTF8(guidStr).get()));
+ aAumid.emplace(portableAumid);
+ return true;
+}
+
+nsresult ToastNotification::BackgroundDispatch(nsIRunnable* runnable) {
+ return NS_DispatchBackgroundTask(runnable);
+}
+
+NS_IMETHODIMP
+ToastNotification::GetSuppressForScreenSharing(bool* aRetVal) {
+ *aRetVal = mSuppressForScreenSharing;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::SetSuppressForScreenSharing(bool aSuppress) {
+ mSuppressForScreenSharing = aSuppress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ nsDependentCString topic(aTopic);
+
+ for (auto iter = mActiveHandlers.Iter(); !iter.Done(); iter.Next()) {
+ RefPtr<ToastNotificationHandler> handler = iter.UserData();
+
+ auto removeNotification = [&]() {
+ // The handlers' destructors will do the right thing (de-register with
+ // Windows).
+ iter.Remove();
+
+ // Break the cycle between the handler and the MSCOM notification so the
+ // handler's destructor will be called.
+ handler->UnregisterHandler();
+ };
+
+ if (topic == "last-pb-context-exited"_ns) {
+ if (handler->IsPrivate()) {
+ handler->HideAlert();
+ removeNotification();
+ }
+ } else if (topic == "quit-application"_ns) {
+ removeNotification();
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::ShowAlertNotification(
+ const nsAString& aImageUrl, const nsAString& aAlertTitle,
+ const nsAString& aAlertText, bool aAlertTextClickable,
+ const nsAString& aAlertCookie, nsIObserver* aAlertListener,
+ const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang,
+ const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing,
+ bool aRequireInteraction) {
+ nsCOMPtr<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ if (NS_WARN_IF(!alert)) {
+ return NS_ERROR_FAILURE;
+ }
+ // vibrate is unused for now
+ nsTArray<uint32_t> vibrate;
+ nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText,
+ aAlertTextClickable, aAlertCookie, aBidi, aLang,
+ aData, aPrincipal, aInPrivateBrowsing,
+ aRequireInteraction, false, vibrate);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return ShowAlert(alert, aAlertListener);
+}
+
+NS_IMETHODIMP
+ToastNotification::ShowPersistentNotification(const nsAString& aPersistentData,
+ nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+ return ShowAlert(aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP
+ToastNotification::SetManualDoNotDisturb(bool aDoNotDisturb) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ToastNotification::GetManualDoNotDisturb(bool* aRet) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+ToastNotification::ShowAlert(nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+ NS_ENSURE_ARG(aAlert);
+
+ if (mSuppressForScreenSharing) {
+ return NS_OK;
+ }
+
+ nsAutoString cookie;
+ MOZ_TRY(aAlert->GetCookie(cookie));
+
+ nsAutoString name;
+ MOZ_TRY(aAlert->GetName(name));
+
+ nsAutoString title;
+ MOZ_TRY(aAlert->GetTitle(title));
+ if (!EnsureUTF16Validity(title)) {
+ MOZ_LOG(sWASLog, LogLevel::Warning,
+ ("Notification title was invalid UTF16, unpaired surrogates have "
+ "been replaced."));
+ }
+
+ nsAutoString text;
+ MOZ_TRY(aAlert->GetText(text));
+ if (!EnsureUTF16Validity(text)) {
+ MOZ_LOG(sWASLog, LogLevel::Warning,
+ ("Notification text was invalid UTF16, unpaired surrogates have "
+ "been replaced."));
+ }
+
+ bool textClickable;
+ MOZ_TRY(aAlert->GetTextClickable(&textClickable));
+
+ bool isSilent;
+ MOZ_TRY(aAlert->GetSilent(&isSilent));
+
+ nsAutoString hostPort;
+ MOZ_TRY(aAlert->GetSource(hostPort));
+
+ nsAutoString launchUrl;
+ MOZ_TRY(aAlert->GetLaunchURL(launchUrl));
+
+ bool requireInteraction;
+ MOZ_TRY(aAlert->GetRequireInteraction(&requireInteraction));
+
+ bool inPrivateBrowsing;
+ MOZ_TRY(aAlert->GetInPrivateBrowsing(&inPrivateBrowsing));
+
+ nsTArray<RefPtr<nsIAlertAction>> actions;
+ MOZ_TRY(aAlert->GetActions(actions));
+
+ nsCOMPtr<nsIPrincipal> principal;
+ MOZ_TRY(aAlert->GetPrincipal(getter_AddRefs(principal)));
+ bool isSystemPrincipal = principal && principal->IsSystemPrincipal();
+
+ RefPtr<ToastNotificationHandler> oldHandler = mActiveHandlers.Get(name);
+
+ NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED);
+ RefPtr<ToastNotificationHandler> handler = new ToastNotificationHandler(
+ this, mAumid.ref(), aAlertListener, name, cookie, title, text, hostPort,
+ textClickable, requireInteraction, actions, isSystemPrincipal, launchUrl,
+ inPrivateBrowsing, isSilent);
+ mActiveHandlers.InsertOrUpdate(name, RefPtr{handler});
+
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Adding handler '%s': [%p] (now %d handlers)",
+ NS_ConvertUTF16toUTF8(name).get(), handler.get(),
+ mActiveHandlers.Count()));
+
+ nsresult rv = handler->InitAlertAsync(aAlert);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Failed to init alert, removing '%s'",
+ NS_ConvertUTF16toUTF8(name).get()));
+ mActiveHandlers.Remove(name);
+ handler->UnregisterHandler();
+ return rv;
+ }
+
+ // If there was a previous handler with the same name then unregister it.
+ if (oldHandler) {
+ oldHandler->UnregisterHandler();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::GetXmlStringForWindowsAlert(nsIAlertNotification* aAlert,
+ const nsAString& aWindowsTag,
+ nsAString& aString) {
+ NS_ENSURE_ARG(aAlert);
+
+ nsAutoString cookie;
+ MOZ_TRY(aAlert->GetCookie(cookie));
+
+ nsAutoString name;
+ MOZ_TRY(aAlert->GetName(name));
+
+ nsAutoString title;
+ MOZ_TRY(aAlert->GetTitle(title));
+
+ nsAutoString text;
+ MOZ_TRY(aAlert->GetText(text));
+
+ bool textClickable;
+ MOZ_TRY(aAlert->GetTextClickable(&textClickable));
+
+ bool isSilent;
+ MOZ_TRY(aAlert->GetSilent(&isSilent));
+
+ nsAutoString hostPort;
+ MOZ_TRY(aAlert->GetSource(hostPort));
+
+ nsAutoString launchUrl;
+ MOZ_TRY(aAlert->GetLaunchURL(launchUrl));
+
+ bool requireInteraction;
+ MOZ_TRY(aAlert->GetRequireInteraction(&requireInteraction));
+
+ bool inPrivateBrowsing;
+ MOZ_TRY(aAlert->GetInPrivateBrowsing(&inPrivateBrowsing));
+
+ nsTArray<RefPtr<nsIAlertAction>> actions;
+ MOZ_TRY(aAlert->GetActions(actions));
+
+ nsCOMPtr<nsIPrincipal> principal;
+ MOZ_TRY(aAlert->GetPrincipal(getter_AddRefs(principal)));
+ bool isSystemPrincipal = principal && principal->IsSystemPrincipal();
+
+ NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED);
+ RefPtr<ToastNotificationHandler> handler = new ToastNotificationHandler(
+ this, mAumid.ref(), nullptr /* aAlertListener */, name, cookie, title,
+ text, hostPort, textClickable, requireInteraction, actions,
+ isSystemPrincipal, launchUrl, inPrivateBrowsing, isSilent);
+
+ // Usually, this will be empty during testing, making test output
+ // deterministic.
+ MOZ_TRY(handler->SetWindowsTag(aWindowsTag));
+
+ nsAutoString imageURL;
+ MOZ_TRY(aAlert->GetImageURL(imageURL));
+
+ return handler->CreateToastXmlString(imageURL, aString);
+}
+
+// Verifies that the tag recieved associates to a notification created during
+// this application's session, or handles fallback behavior.
+RefPtr<ToastHandledPromise> ToastNotification::VerifyTagPresentOrFallback(
+ const nsAString& aWindowsTag) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Iterating %d handlers", mActiveHandlers.Count()));
+
+ for (auto iter = mActiveHandlers.Iter(); !iter.Done(); iter.Next()) {
+ RefPtr<ToastNotificationHandler> handler = iter.UserData();
+ nsAutoString tag;
+ nsresult rv = handler->GetWindowsTag(tag);
+
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Comparing external windowsTag '%s' to handled windowsTag '%s'",
+ NS_ConvertUTF16toUTF8(aWindowsTag).get(),
+ NS_ConvertUTF16toUTF8(tag).get()));
+ if (aWindowsTag.Equals(tag)) {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("External windowsTag '%s' is handled by handler [%p]",
+ NS_ConvertUTF16toUTF8(aWindowsTag).get(), handler.get()));
+ ToastHandledResolve handled{u""_ns, u""_ns};
+ return ToastHandledPromise::CreateAndResolve(handled, __func__);
+ }
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Failed to get windowsTag for handler [%p]", handler.get()));
+ }
+ }
+
+ // Fallback handling
+
+ RefPtr<ToastHandledPromise::Private> fallbackPromise =
+ new ToastHandledPromise::Private(__func__);
+
+ // TODO: Bug 1806005 - At time of writing this function is called in a call
+ // stack containing `WndProc` callback on an STA thread. As a result attempts
+ // to create a `ToastNotificationManager` instance results an an
+ // `RPC_E_CANTCALLOUT_ININPUTSYNCCALL` error. We can simplify the the XPCOM
+ // interface and synchronize the COM interactions if notification fallback
+ // handling were no longer handled in a `WndProc` context.
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ "VerifyTagPresentOrFallback fallback background task",
+ [fallbackPromise, aWindowsTag = nsString(aWindowsTag),
+ aAumid = nsString(mAumid.ref())]() {
+ MOZ_ASSERT(mscom::IsCOMInitializedOnCurrentThread());
+
+ bool foundTag;
+ nsAutoString launchUrl;
+ nsAutoString privilegedName;
+
+ nsresult rv = ToastNotificationHandler::
+ FindLaunchURLAndPrivilegedNameForWindowsTag(
+ aWindowsTag, aAumid, foundTag, launchUrl, privilegedName);
+
+ if (NS_FAILED(rv) || !foundTag) {
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("Failed to get launch URL and privileged name for "
+ "notification tag '%s'",
+ NS_ConvertUTF16toUTF8(aWindowsTag).get()));
+
+ fallbackPromise->Reject(false, __func__);
+ return;
+ }
+
+ MOZ_LOG(sWASLog, LogLevel::Debug,
+ ("Found launch URL '%s' and privileged name '%s' for "
+ "windowsTag '%s'",
+ NS_ConvertUTF16toUTF8(launchUrl).get(),
+ NS_ConvertUTF16toUTF8(privilegedName).get(),
+ NS_ConvertUTF16toUTF8(aWindowsTag).get()));
+
+ ToastHandledResolve handled{launchUrl, privilegedName};
+ fallbackPromise->Resolve(handled, __func__);
+ }));
+
+ return fallbackPromise;
+}
+
+// Send our window's PID to the notification server so that it can grant us
+// `SetForegroundWindow` permissions. PID 0 is sent to signal no window PID.
+// Absense of PID which may occur when we are yet unable to retrieve the
+// window during startup, which is not a problem in practice as new windows
+// receive focus by default.
+void ToastNotification::SignalComNotificationHandled(
+ const nsAString& aWindowsTag) {
+ DWORD pid = 0;
+
+ 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) {
+ HWND hwnd = (HWND)widget->GetNativeData(NS_NATIVE_WINDOW);
+ GetWindowThreadProcessId(hwnd, &pid);
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug, ("Failed to get widget"));
+ }
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug, ("Failed to get navWin"));
+ }
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Debug, ("Failed to get WinMediator"));
+ }
+
+ // Run pipe communication off the main thread to prevent UI jank from
+ // blocking. Nothing relies on the COM server's response or that it has
+ // responded at time of commit.
+ NS_DispatchBackgroundTask(
+ NS_NewRunnableFunction(
+ "SignalComNotificationHandled background task",
+ [pid, aWindowsTag = nsString{aWindowsTag}]() mutable {
+ std::wstring pipeName = GetNotificationPipeName(aWindowsTag.get());
+
+ nsAutoHandle pipe;
+ pipe.own(CreateFileW(pipeName.c_str(), GENERIC_READ | GENERIC_WRITE,
+ 0, nullptr, OPEN_EXISTING,
+ FILE_FLAG_OVERLAPPED, nullptr));
+ if (pipe.get() == INVALID_HANDLE_VALUE) {
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("Unable to open notification server pipe."));
+ return;
+ }
+
+ DWORD pipeFlags = PIPE_READMODE_MESSAGE;
+ if (!SetNamedPipeHandleState(pipe.get(), &pipeFlags, nullptr,
+ nullptr)) {
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("Error setting pipe handle state, error %lu",
+ GetLastError()));
+ return;
+ }
+
+ // Pass our window's PID to the COM server receive
+ // `SetForegroundWindow` permissions, and wait for a message
+ // acknowledging the permission has been granted.
+ ToastNotificationPidMessage in{};
+ in.pid = pid;
+ ToastNotificationPermissionMessage out{};
+ auto transact = [&](OVERLAPPED& overlapped) {
+ return TransactNamedPipe(pipe.get(), &in, sizeof(in), &out,
+ sizeof(out), nullptr, &overlapped);
+ };
+ bool result =
+ SyncDoOverlappedIOWithTimeout(pipe, sizeof(out), transact);
+
+ if (result && out.setForegroundPermissionGranted && pid != 0) {
+ MOZ_LOG(
+ sWASLog, LogLevel::Info,
+ ("SetForegroundWindow permission granted to our window."));
+ } else {
+ MOZ_LOG(sWASLog, LogLevel::Error,
+ ("SetForegroundWindow permission not granted to our "
+ "window."));
+ }
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+}
+
+NS_IMETHODIMP
+ToastNotification::HandleWindowsTag(const nsAString& aWindowsTag,
+ JSContext* aCx, dom::Promise** aPromise) {
+ NS_ENSURE_TRUE(mAumid.isSome(), NS_ERROR_UNEXPECTED);
+ NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD);
+
+ ErrorResult rv;
+ RefPtr<dom::Promise> promise =
+ dom::Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+ ENSURE_SUCCESS(rv, rv.StealNSResult());
+
+ this->VerifyTagPresentOrFallback(aWindowsTag)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [aWindowsTag = nsString(aWindowsTag),
+ promise](const ToastHandledResolve& aResolved) {
+ // We no longer need to query toast information from OS and can
+ // allow the COM server to proceed (toast information is lost once
+ // the COM server's `Activate` callback returns).
+ SignalComNotificationHandled(aWindowsTag);
+
+ dom::AutoJSAPI js;
+ if (NS_WARN_IF(!js.Init(promise->GetGlobalObject()))) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Resolve the DOM Promise with a JS object. Set `launchUrl` and/or
+ // `privilegedName` properties if fallback handling is necessary.
+
+ JSContext* cx = js.cx();
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+
+ auto setProperty = [&](const char* name, const nsString& value) {
+ JS::Rooted<JSString*> title(cx,
+ JS_NewUCStringCopyZ(cx, value.get()));
+ JS::Rooted<JS::Value> attVal(cx, JS::StringValue(title));
+ Unused << NS_WARN_IF(!JS_SetProperty(cx, obj, name, attVal));
+ };
+
+ if (!aResolved.launchUrl.IsEmpty()) {
+ setProperty("launchUrl", aResolved.launchUrl);
+ }
+ if (!aResolved.privilegedName.IsEmpty()) {
+ setProperty("privilegedName", aResolved.privilegedName);
+ }
+
+ promise->MaybeResolve(obj);
+ },
+ [aWindowsTag = nsString(aWindowsTag), promise]() {
+ // We no longer need to query toast information from OS and can
+ // allow the COM server to proceed (toast information is lost once
+ // the COM server's `Activate` callback returns).
+ SignalComNotificationHandled(aWindowsTag);
+
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ToastNotification::CloseAlert(const nsAString& aAlertName,
+ bool aContextClosed) {
+ RefPtr<ToastNotificationHandler> handler;
+ if (NS_WARN_IF(!mActiveHandlers.Get(aAlertName, getter_AddRefs(handler)))) {
+ return NS_OK;
+ }
+
+ if (!aContextClosed || handler->IsPrivate()) {
+ // Hide the alert when not implicitly closed by tab/window closing or when
+ // notification originated from a private tab.
+ handler->HideAlert();
+ }
+
+ mActiveHandlers.Remove(aAlertName);
+ handler->UnregisterHandler();
+
+ return NS_OK;
+}
+
+bool ToastNotification::IsActiveHandler(const nsAString& aAlertName,
+ ToastNotificationHandler* aHandler) {
+ RefPtr<ToastNotificationHandler> handler;
+ if (NS_WARN_IF(!mActiveHandlers.Get(aAlertName, getter_AddRefs(handler)))) {
+ return false;
+ }
+ return handler == aHandler;
+}
+
+void ToastNotification::RemoveHandler(const nsAString& aAlertName,
+ ToastNotificationHandler* aHandler) {
+ // The alert may have been replaced; only remove it from the active
+ // handler's map if it's the same.
+ if (IsActiveHandler(aAlertName, aHandler)) {
+ // Terrible things happen if the destructor of a handler is called inside
+ // the hashtable .Remove() method. Wait until we have returned from there.
+ RefPtr<ToastNotificationHandler> kungFuDeathGrip(aHandler);
+ mActiveHandlers.Remove(aAlertName);
+ aHandler->UnregisterHandler();
+ }
+}
+
+NS_IMETHODIMP
+ToastNotification::RemoveAllNotificationsForInstall() {
+ HRESULT hr = S_OK;
+
+ ComPtr<IToastNotificationManagerStatics> manager;
+ hr = GetActivationFactory(
+ HStringReference(
+ RuntimeClass_Windows_UI_Notifications_ToastNotificationManager)
+ .Get(),
+ &manager);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
+
+ HString aumid;
+ MOZ_ASSERT(mAumid.isSome());
+ hr = aumid.Set(mAumid.ref().get());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
+
+ // Hide toasts in action center.
+ [&]() {
+ ComPtr<IToastNotificationManagerStatics2> manager2;
+ hr = manager.As(&manager2);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ ComPtr<IToastNotificationHistory> history;
+ hr = manager2->get_History(&history);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ hr = history->ClearWithId(aumid.Get());
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+ }();
+
+ // Hide scheduled toasts.
+ [&]() {
+ ComPtr<IToastNotifier> notifier;
+ hr = manager->CreateToastNotifierWithId(aumid.Get(), &notifier);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ ComPtr<IVectorView_ScheduledToastNotification> scheduledToasts;
+ hr = notifier->GetScheduledToastNotifications(&scheduledToasts);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ unsigned int schedSize;
+ hr = scheduledToasts->get_Size(&schedSize);
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(hr));
+
+ for (unsigned int i = 0; i < schedSize; i++) {
+ ComPtr<IScheduledToastNotification> schedToast;
+ hr = scheduledToasts->GetAt(i, &schedToast);
+ if (NS_WARN_IF(FAILED(hr))) {
+ continue;
+ }
+
+ hr = notifier->RemoveFromSchedule(schedToast.Get());
+ Unused << NS_WARN_IF(FAILED(hr));
+ }
+ }();
+
+ return NS_OK;
+}
+
+} // namespace widget
+} // namespace mozilla