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/ToastNotification.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/windows/ToastNotification.cpp')
-rw-r--r-- | widget/windows/ToastNotification.cpp | 913 |
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(), ¬ifier); + 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 |