From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- toolkit/components/alerts/AlertNotification.cpp | 373 +++++++++++++++++++ toolkit/components/alerts/AlertNotification.h | 86 +++++ .../alerts/AlertNotificationIPCSerializer.h | 128 +++++++ toolkit/components/alerts/alert.css | 37 ++ toolkit/components/alerts/alert.js | 394 ++++++++++++++++++++ toolkit/components/alerts/alert.xhtml | 82 +++++ toolkit/components/alerts/jar.mn | 8 + toolkit/components/alerts/moz.build | 45 +++ toolkit/components/alerts/nsAlertsService.cpp | 330 +++++++++++++++++ toolkit/components/alerts/nsAlertsService.h | 30 ++ toolkit/components/alerts/nsAlertsUtils.cpp | 30 ++ toolkit/components/alerts/nsAlertsUtils.h | 29 ++ toolkit/components/alerts/nsIAlertsService.idl | 337 +++++++++++++++++ .../components/alerts/nsIWindowsAlertsService.idl | 69 ++++ toolkit/components/alerts/nsXULAlerts.cpp | 405 +++++++++++++++++++++ toolkit/components/alerts/nsXULAlerts.h | 84 +++++ toolkit/components/alerts/test/browser.ini | 5 + .../components/alerts/test/browser_bug1682866.js | 56 +++ .../components/alerts/test/file_bug1682866.html | 9 + toolkit/components/alerts/test/image.gif | Bin 0 -> 60901 bytes toolkit/components/alerts/test/image.png | Bin 0 -> 2531 bytes toolkit/components/alerts/test/image_server.sjs | 92 +++++ toolkit/components/alerts/test/mochitest.ini | 20 + toolkit/components/alerts/test/test_alerts.html | 88 +++++ .../alerts/test/test_alerts_noobserve.html | 91 +++++ .../test/test_alerts_requireinteraction.html | 162 +++++++++ toolkit/components/alerts/test/test_image.html | 117 ++++++ .../components/alerts/test/test_invalid_utf16.html | 160 ++++++++ .../alerts/test/test_multiple_alerts.html | 97 +++++ toolkit/components/alerts/test/test_principal.html | 123 +++++++ 30 files changed, 3487 insertions(+) create mode 100644 toolkit/components/alerts/AlertNotification.cpp create mode 100644 toolkit/components/alerts/AlertNotification.h create mode 100644 toolkit/components/alerts/AlertNotificationIPCSerializer.h create mode 100644 toolkit/components/alerts/alert.css create mode 100644 toolkit/components/alerts/alert.js create mode 100644 toolkit/components/alerts/alert.xhtml create mode 100644 toolkit/components/alerts/jar.mn create mode 100644 toolkit/components/alerts/moz.build create mode 100644 toolkit/components/alerts/nsAlertsService.cpp create mode 100644 toolkit/components/alerts/nsAlertsService.h create mode 100644 toolkit/components/alerts/nsAlertsUtils.cpp create mode 100644 toolkit/components/alerts/nsAlertsUtils.h create mode 100644 toolkit/components/alerts/nsIAlertsService.idl create mode 100644 toolkit/components/alerts/nsIWindowsAlertsService.idl create mode 100644 toolkit/components/alerts/nsXULAlerts.cpp create mode 100644 toolkit/components/alerts/nsXULAlerts.h create mode 100644 toolkit/components/alerts/test/browser.ini create mode 100644 toolkit/components/alerts/test/browser_bug1682866.js create mode 100644 toolkit/components/alerts/test/file_bug1682866.html create mode 100644 toolkit/components/alerts/test/image.gif create mode 100644 toolkit/components/alerts/test/image.png create mode 100644 toolkit/components/alerts/test/image_server.sjs create mode 100644 toolkit/components/alerts/test/mochitest.ini create mode 100644 toolkit/components/alerts/test/test_alerts.html create mode 100644 toolkit/components/alerts/test/test_alerts_noobserve.html create mode 100644 toolkit/components/alerts/test/test_alerts_requireinteraction.html create mode 100644 toolkit/components/alerts/test/test_image.html create mode 100644 toolkit/components/alerts/test/test_invalid_utf16.html create mode 100644 toolkit/components/alerts/test/test_multiple_alerts.html create mode 100644 toolkit/components/alerts/test/test_principal.html (limited to 'toolkit/components/alerts') diff --git a/toolkit/components/alerts/AlertNotification.cpp b/toolkit/components/alerts/AlertNotification.cpp new file mode 100644 index 0000000000..b38e31d58a --- /dev/null +++ b/toolkit/components/alerts/AlertNotification.cpp @@ -0,0 +1,373 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/AlertNotification.h" + +#include "imgIContainer.h" +#include "imgINotificationObserver.h" +#include "imgIRequest.h" +#include "imgLoader.h" +#include "nsAlertsUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" + +#include "mozilla/Unused.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(AlertNotification, nsIAlertNotification) + +AlertNotification::AlertNotification() + : mTextClickable(false), mInPrivateBrowsing(false) {} + +AlertNotification::~AlertNotification() = default; + +NS_IMETHODIMP +AlertNotification::Init(const nsAString& aName, const nsAString& aImageURL, + const nsAString& aTitle, const nsAString& aText, + bool aTextClickable, const nsAString& aCookie, + const nsAString& aDir, const nsAString& aLang, + const nsAString& aData, nsIPrincipal* aPrincipal, + bool aInPrivateBrowsing, bool aRequireInteraction, + bool aSilent, const nsTArray& aVibrate) { + mName = aName; + mImageURL = aImageURL; + mTitle = aTitle; + mText = aText; + mTextClickable = aTextClickable; + mCookie = aCookie; + mDir = aDir; + mLang = aLang; + mData = aData; + mPrincipal = aPrincipal; + mInPrivateBrowsing = aInPrivateBrowsing; + mRequireInteraction = aRequireInteraction; + mSilent = aSilent; + mVibrate = aVibrate.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::SetActions( + const nsTArray>& aActions) { + mActions = aActions.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetName(nsAString& aName) { + aName = mName; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetImageURL(nsAString& aImageURL) { + aImageURL = mImageURL; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetTitle(nsAString& aTitle) { + aTitle = mTitle; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetText(nsAString& aText) { + aText = mText; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetTextClickable(bool* aTextClickable) { + *aTextClickable = mTextClickable; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetCookie(nsAString& aCookie) { + aCookie = mCookie; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetDir(nsAString& aDir) { + aDir = mDir; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetLang(nsAString& aLang) { + aLang = mLang; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetRequireInteraction(bool* aRequireInteraction) { + *aRequireInteraction = mRequireInteraction; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetData(nsAString& aData) { + aData = mData; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetPrincipal(nsIPrincipal** aPrincipal) { + NS_IF_ADDREF(*aPrincipal = mPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetURI(nsIURI** aURI) { + if (!nsAlertsUtils::IsActionablePrincipal(mPrincipal)) { + *aURI = nullptr; + return NS_OK; + } + auto* basePrin = BasePrincipal::Cast(mPrincipal); + return basePrin->GetURI(aURI); +} + +NS_IMETHODIMP +AlertNotification::GetInPrivateBrowsing(bool* aInPrivateBrowsing) { + *aInPrivateBrowsing = mInPrivateBrowsing; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetActionable(bool* aActionable) { + *aActionable = nsAlertsUtils::IsActionablePrincipal(mPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetSilent(bool* aSilent) { + *aSilent = mSilent; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetVibrate(nsTArray& aVibrate) { + aVibrate = mVibrate.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetActions(nsTArray>& aActions) { + aActions = mActions.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetSource(nsAString& aSource) { + nsAlertsUtils::GetSourceHostPort(mPrincipal, aSource); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetLaunchURL(nsAString& aLaunchURL) { + aLaunchURL = mLaunchURL; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::SetLaunchURL(const nsAString& aLaunchURL) { + mLaunchURL = aLaunchURL; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::LoadImage(uint32_t aTimeout, + nsIAlertNotificationImageListener* aListener, + nsISupports* aUserData, nsICancelable** aRequest) { + NS_ENSURE_ARG(aListener); + NS_ENSURE_ARG_POINTER(aRequest); + *aRequest = nullptr; + + // Exit early if this alert doesn't have an image. + if (mImageURL.IsEmpty()) { + return aListener->OnImageMissing(aUserData); + } + nsCOMPtr imageURI; + NS_NewURI(getter_AddRefs(imageURI), mImageURL); + if (!imageURI) { + return aListener->OnImageMissing(aUserData); + } + + RefPtr request = new AlertImageRequest( + imageURI, mPrincipal, mInPrivateBrowsing, aTimeout, aListener, aUserData); + request->Start(); + request.forget(aRequest); + return NS_OK; +} + +NS_IMPL_CYCLE_COLLECTION(AlertImageRequest, mURI, mPrincipal, mListener, + mUserData) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AlertImageRequest) + NS_INTERFACE_MAP_ENTRY(imgINotificationObserver) + NS_INTERFACE_MAP_ENTRY(nsICancelable) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsINamed) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, imgINotificationObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AlertImageRequest) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AlertImageRequest) + +AlertImageRequest::AlertImageRequest( + nsIURI* aURI, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing, + uint32_t aTimeout, nsIAlertNotificationImageListener* aListener, + nsISupports* aUserData) + : mURI(aURI), + mPrincipal(aPrincipal), + mInPrivateBrowsing(aInPrivateBrowsing), + mTimeout(aTimeout), + mListener(aListener), + mUserData(aUserData) {} + +AlertImageRequest::~AlertImageRequest() { + if (mRequest) { + mRequest->CancelAndForgetObserver(NS_BINDING_ABORTED); + } +} + +void AlertImageRequest::Notify(imgIRequest* aRequest, int32_t aType, + const nsIntRect* aData) { + MOZ_ASSERT(aRequest == mRequest); + + uint32_t imgStatus = imgIRequest::STATUS_ERROR; + nsresult rv = aRequest->GetImageStatus(&imgStatus); + if (NS_WARN_IF(NS_FAILED(rv)) || (imgStatus & imgIRequest::STATUS_ERROR)) { + NotifyMissing(); + return; + } + + // If the image is already decoded, `FRAME_COMPLETE` will fire before + // `LOAD_COMPLETE`, so we can notify the listener immediately. Otherwise, + // we'll need to request a decode when `LOAD_COMPLETE` fires, and wait + // for the first frame. + + if (aType == imgINotificationObserver::LOAD_COMPLETE) { + if (!(imgStatus & imgIRequest::STATUS_FRAME_COMPLETE)) { + nsCOMPtr image; + rv = aRequest->GetImage(getter_AddRefs(image)); + if (NS_WARN_IF(NS_FAILED(rv) || !image)) { + NotifyMissing(); + return; + } + + // Ask the image to decode at its intrinsic size. + int32_t width = 0, height = 0; + image->GetWidth(&width); + image->GetHeight(&height); + image->RequestDecodeForSize(gfx::IntSize(width, height), + imgIContainer::FLAG_HIGH_QUALITY_SCALING); + } + return; + } + + if (aType == imgINotificationObserver::FRAME_COMPLETE) { + return NotifyComplete(); + } +} + +NS_IMETHODIMP +AlertImageRequest::Notify(nsITimer* aTimer) { + MOZ_ASSERT(aTimer == mTimer); + return NotifyMissing(); +} + +NS_IMETHODIMP +AlertImageRequest::GetName(nsACString& aName) { + aName.AssignLiteral("AlertImageRequest"); + return NS_OK; +} + +NS_IMETHODIMP +AlertImageRequest::Cancel(nsresult aReason) { + if (mRequest) { + mRequest->Cancel(aReason); + } + // We call `NotifyMissing` here because we won't receive a `LOAD_COMPLETE` + // notification if we cancel the request before it loads (bug 1233086, + // comment 33). Once that's fixed, `nsIAlertNotification::loadImage` could + // return the underlying `imgIRequest` instead of the wrapper. + return NotifyMissing(); +} + +nsresult AlertImageRequest::Start() { + // Keep the request alive until we notify the image listener. + NS_ADDREF_THIS(); + + nsresult rv; + if (mTimeout > 0) { + rv = NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, mTimeout, + nsITimer::TYPE_ONE_SHOT); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NotifyMissing(); + } + } + + // Begin loading the image. + imgLoader* il = imgLoader::NormalLoader(); + if (!il) { + return NotifyMissing(); + } + + // Bug 1237405: `LOAD_ANONYMOUS` disables cookies, but we want to use a + // temporary cookie jar instead. We should also use + // `imgLoader::PrivateBrowsingLoader()` instead of the normal loader. + // Unfortunately, the PB loader checks the load group, and asserts if its + // load context's PB flag isn't set. The fix is to pass the load group to + // `nsIAlertNotification::loadImage`. + int32_t loadFlags = nsIRequest::LOAD_NORMAL; + if (mInPrivateBrowsing) { + loadFlags = nsIRequest::LOAD_ANONYMOUS; + } + + rv = il->LoadImageXPCOM( + mURI, nullptr, nullptr, mPrincipal, nullptr, this, nullptr, loadFlags, + nullptr, nsIContentPolicy::TYPE_INTERNAL_IMAGE, getter_AddRefs(mRequest)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NotifyMissing(); + } + + return NS_OK; +} + +nsresult AlertImageRequest::NotifyMissing() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + if (nsCOMPtr listener = + std::move(mListener)) { + nsresult rv = listener->OnImageMissing(mUserData); + NS_RELEASE_THIS(); + return rv; + } + + return NS_OK; +} + +void AlertImageRequest::NotifyComplete() { + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + if (nsCOMPtr listener = + std::move(mListener)) { + listener->OnImageReady(mUserData, mRequest); + NS_RELEASE_THIS(); + } +} + +} // namespace mozilla diff --git a/toolkit/components/alerts/AlertNotification.h b/toolkit/components/alerts/AlertNotification.h new file mode 100644 index 0000000000..73ca770308 --- /dev/null +++ b/toolkit/components/alerts/AlertNotification.h @@ -0,0 +1,86 @@ +/* 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/. */ + +#ifndef mozilla_AlertNotification_h__ +#define mozilla_AlertNotification_h__ + +#include "imgINotificationObserver.h" +#include "nsIAlertsService.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsICancelable.h" +#include "nsINamed.h" +#include "nsIPrincipal.h" +#include "nsString.h" +#include "nsITimer.h" + +namespace mozilla { + +class AlertImageRequest final : public imgINotificationObserver, + public nsICancelable, + public nsITimerCallback, + public nsINamed { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(AlertImageRequest, + imgINotificationObserver) + NS_DECL_IMGINOTIFICATIONOBSERVER + NS_DECL_NSICANCELABLE + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + AlertImageRequest(nsIURI* aURI, nsIPrincipal* aPrincipal, + bool aInPrivateBrowsing, uint32_t aTimeout, + nsIAlertNotificationImageListener* aListener, + nsISupports* aUserData); + + nsresult Start(); + + private: + virtual ~AlertImageRequest(); + + nsresult NotifyMissing(); + void NotifyComplete(); + + nsCOMPtr mURI; + nsCOMPtr mPrincipal; + bool mInPrivateBrowsing; + uint32_t mTimeout; + nsCOMPtr mListener; + nsCOMPtr mUserData; + nsCOMPtr mTimer; + nsCOMPtr mRequest; +}; + +class AlertNotification final : public nsIAlertNotification { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIALERTNOTIFICATION + AlertNotification(); + + protected: + virtual ~AlertNotification(); + + private: + nsString mName; + nsString mImageURL; + nsString mTitle; + nsString mText; + bool mTextClickable; + nsString mCookie; + nsString mDir; + nsString mLang; + bool mRequireInteraction; + nsString mData; + nsCOMPtr mPrincipal; + bool mInPrivateBrowsing; + bool mSilent; + nsTArray mVibrate; + nsTArray> mActions; + nsString mLaunchURL; +}; + +} // namespace mozilla + +#endif /* mozilla_AlertNotification_h__ */ diff --git a/toolkit/components/alerts/AlertNotificationIPCSerializer.h b/toolkit/components/alerts/AlertNotificationIPCSerializer.h new file mode 100644 index 0000000000..bdec9a8f47 --- /dev/null +++ b/toolkit/components/alerts/AlertNotificationIPCSerializer.h @@ -0,0 +1,128 @@ +/* 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/. */ + +#ifndef mozilla_AlertNotificationIPCSerializer_h__ +#define mozilla_AlertNotificationIPCSerializer_h__ + +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIAlertsService.h" +#include "nsIPrincipal.h" +#include "nsString.h" + +#include "ipc/IPCMessageUtils.h" + +#include "mozilla/dom/PermissionMessageUtils.h" + +namespace mozilla { +namespace ipc { + +template <> +struct IPDLParamTraits { + static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor, + nsIAlertNotification* aParam) { + bool isNull = !aParam; + if (isNull) { + WriteIPDLParam(aWriter, aActor, isNull); + return; + } + + nsString name, imageURL, title, text, cookie, dir, lang, data; + bool textClickable, inPrivateBrowsing, requireInteraction, silent; + nsCOMPtr principal; + nsTArray vibrate; + + if (NS_WARN_IF(NS_FAILED(aParam->GetName(name))) || + NS_WARN_IF(NS_FAILED(aParam->GetImageURL(imageURL))) || + NS_WARN_IF(NS_FAILED(aParam->GetTitle(title))) || + NS_WARN_IF(NS_FAILED(aParam->GetText(text))) || + NS_WARN_IF(NS_FAILED(aParam->GetTextClickable(&textClickable))) || + NS_WARN_IF(NS_FAILED(aParam->GetCookie(cookie))) || + NS_WARN_IF(NS_FAILED(aParam->GetDir(dir))) || + NS_WARN_IF(NS_FAILED(aParam->GetLang(lang))) || + NS_WARN_IF(NS_FAILED(aParam->GetData(data))) || + NS_WARN_IF( + NS_FAILED(aParam->GetPrincipal(getter_AddRefs(principal)))) || + NS_WARN_IF( + NS_FAILED(aParam->GetInPrivateBrowsing(&inPrivateBrowsing))) || + NS_WARN_IF( + NS_FAILED(aParam->GetRequireInteraction(&requireInteraction))) || + NS_WARN_IF(NS_FAILED(aParam->GetSilent(&silent))) || + NS_WARN_IF(NS_FAILED(aParam->GetVibrate(vibrate)))) { + // Write a `null` object if any getter returns an error. Otherwise, the + // receiver will try to deserialize an incomplete object and crash. + WriteIPDLParam(aWriter, aActor, /* isNull */ true); + return; + } + + WriteIPDLParam(aWriter, aActor, isNull); + WriteIPDLParam(aWriter, aActor, name); + WriteIPDLParam(aWriter, aActor, imageURL); + WriteIPDLParam(aWriter, aActor, title); + WriteIPDLParam(aWriter, aActor, text); + WriteIPDLParam(aWriter, aActor, textClickable); + WriteIPDLParam(aWriter, aActor, cookie); + WriteIPDLParam(aWriter, aActor, dir); + WriteIPDLParam(aWriter, aActor, lang); + WriteIPDLParam(aWriter, aActor, data); + WriteIPDLParam(aWriter, aActor, principal); + WriteIPDLParam(aWriter, aActor, inPrivateBrowsing); + WriteIPDLParam(aWriter, aActor, requireInteraction); + WriteIPDLParam(aWriter, aActor, silent); + WriteIPDLParam(aWriter, aActor, vibrate); + } + + static bool Read(IPC::MessageReader* aReader, IProtocol* aActor, + RefPtr* aResult) { + bool isNull; + NS_ENSURE_TRUE(ReadIPDLParam(aReader, aActor, &isNull), false); + if (isNull) { + *aResult = nullptr; + return true; + } + + nsString name, imageURL, title, text, cookie, dir, lang, data; + bool textClickable, inPrivateBrowsing, requireInteraction, silent; + nsCOMPtr principal; + nsTArray vibrate; + + if (!ReadIPDLParam(aReader, aActor, &name) || + !ReadIPDLParam(aReader, aActor, &imageURL) || + !ReadIPDLParam(aReader, aActor, &title) || + !ReadIPDLParam(aReader, aActor, &text) || + !ReadIPDLParam(aReader, aActor, &textClickable) || + !ReadIPDLParam(aReader, aActor, &cookie) || + !ReadIPDLParam(aReader, aActor, &dir) || + !ReadIPDLParam(aReader, aActor, &lang) || + !ReadIPDLParam(aReader, aActor, &data) || + !ReadIPDLParam(aReader, aActor, &principal) || + !ReadIPDLParam(aReader, aActor, &inPrivateBrowsing) || + !ReadIPDLParam(aReader, aActor, &requireInteraction) || + !ReadIPDLParam(aReader, aActor, &silent) || + !ReadIPDLParam(aReader, aActor, &vibrate)) { + return false; + } + + nsCOMPtr alert = + do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); + if (NS_WARN_IF(!alert)) { + *aResult = nullptr; + return true; + } + nsresult rv = alert->Init( + name, imageURL, title, text, textClickable, cookie, dir, lang, data, + principal, inPrivateBrowsing, requireInteraction, silent, vibrate); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aResult = nullptr; + return true; + } + *aResult = ToRefPtr(std::move(alert)); + return true; + } +}; + +} // namespace ipc +} // namespace mozilla + +#endif /* mozilla_AlertNotificationIPCSerializer_h__ */ diff --git a/toolkit/components/alerts/alert.css b/toolkit/components/alerts/alert.css new file mode 100644 index 0000000000..273646f399 --- /dev/null +++ b/toolkit/components/alerts/alert.css @@ -0,0 +1,37 @@ +/* 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/. */ + +#alertBox[animate] { + animation-duration: 20s; + animation-fill-mode: both; + animation-name: alert-animation; +} + +#alertBox[animate]:not([clicked], [closing]):hover { + animation-play-state: paused; +} + +#alertBox:not([hasOrigin]) > box > #alertTextBox > #alertFooter, +#alertBox:not([hasIcon]) > box > #alertIcon, +#alertImage:not([src]) { + display: none; +} + +#alertTitleBox { + justify-content: center; + align-items: center; +} + +.alertText { + white-space: pre-wrap; +} + +@keyframes alert-animation { + from { + visibility: visible; + } + to { + visibility: hidden; + } +} diff --git a/toolkit/components/alerts/alert.js b/toolkit/components/alerts/alert.js new file mode 100644 index 0000000000..8a097f967b --- /dev/null +++ b/toolkit/components/alerts/alert.js @@ -0,0 +1,394 @@ +/* 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/. */ + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +// Copied from nsILookAndFeel.h, see comments on eMetric_AlertNotificationOrigin +const NS_ALERT_HORIZONTAL = 1; +const NS_ALERT_LEFT = 2; +const NS_ALERT_TOP = 4; + +const WINDOW_MARGIN = AppConstants.platform == "win" ? 0 : 10; +const BODY_TEXT_LIMIT = 200; +const WINDOW_SHADOW_SPREAD = AppConstants.platform == "win" ? 10 : 0; + +var gOrigin = 0; // Default value: alert from bottom right. +var gReplacedWindow = null; +var gAlertListener = null; +var gAlertTextClickable = false; +var gAlertCookie = ""; +var gIsActive = false; +var gIsReplaced = false; +var gRequireInteraction = false; + +function prefillAlertInfo() { + // unwrap all the args.... + // arguments[0] --> the image src url + // arguments[1] --> the alert title + // arguments[2] --> the alert text + // arguments[3] --> is the text clickable? + // arguments[4] --> the alert cookie to be passed back to the listener + // arguments[5] --> the alert origin reported by the look and feel + // arguments[6] --> bidi + // arguments[7] --> lang + // arguments[8] --> requires interaction + // arguments[9] --> replaced alert window (nsIDOMWindow) + // arguments[10] --> an optional callback listener (nsIObserver) + // arguments[11] -> the nsIURI.hostPort of the origin, optional + // arguments[12] -> the alert icon URL, optional + + switch (window.arguments.length) { + default: + case 13: { + if (window.arguments[12]) { + let alertBox = document.getElementById("alertBox"); + alertBox.setAttribute("hasIcon", true); + + let icon = document.getElementById("alertIcon"); + icon.src = window.arguments[12]; + } + } + // fall through + case 12: { + if (window.arguments[11]) { + let alertBox = document.getElementById("alertBox"); + alertBox.setAttribute("hasOrigin", true); + + let hostPort = window.arguments[11]; + const ALERT_BUNDLE = Services.strings.createBundle( + "chrome://alerts/locale/alert.properties" + ); + const BRAND_BUNDLE = Services.strings.createBundle( + "chrome://branding/locale/brand.properties" + ); + const BRAND_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName"); + let label = document.getElementById("alertSourceLabel"); + label.setAttribute( + "value", + ALERT_BUNDLE.formatStringFromName("source.label", [hostPort]) + ); + let doNotDisturbMenuItem = document.getElementById( + "doNotDisturbMenuItem" + ); + doNotDisturbMenuItem.setAttribute( + "label", + ALERT_BUNDLE.formatStringFromName("pauseNotifications.label", [ + BRAND_NAME, + ]) + ); + let disableForOrigin = document.getElementById( + "disableForOriginMenuItem" + ); + disableForOrigin.setAttribute( + "label", + ALERT_BUNDLE.formatStringFromName( + "webActions.disableForOrigin.label", + [hostPort] + ) + ); + let openSettings = document.getElementById("openSettingsMenuItem"); + openSettings.setAttribute( + "label", + ALERT_BUNDLE.GetStringFromName("webActions.settings.label") + ); + } + } + // fall through + case 11: + gAlertListener = window.arguments[10]; + // fall through + case 10: + gReplacedWindow = window.arguments[9]; + // fall through + case 9: + gRequireInteraction = window.arguments[8]; + // fall through + case 8: + if (window.arguments[7]) { + document + .getElementById("alertTitleLabel") + .setAttribute("lang", window.arguments[7]); + document + .getElementById("alertTextLabel") + .setAttribute("lang", window.arguments[7]); + } + // fall through + case 7: + if (window.arguments[6]) { + document.getElementById("alertNotification").style.direction = + window.arguments[6]; + } + // fall through + case 6: + gOrigin = window.arguments[5]; + // fall through + case 5: + gAlertCookie = window.arguments[4]; + // fall through + case 4: + gAlertTextClickable = window.arguments[3]; + if (gAlertTextClickable) { + document + .getElementById("alertNotification") + .setAttribute("clickable", true); + document + .getElementById("alertTextLabel") + .setAttribute("clickable", true); + } + // fall through + case 3: + if (window.arguments[2]) { + document.getElementById("alertBox").setAttribute("hasBodyText", true); + let bodyText = window.arguments[2]; + let bodyTextLabel = document.getElementById("alertTextLabel"); + + if (bodyText.length > BODY_TEXT_LIMIT) { + bodyTextLabel.setAttribute("tooltiptext", bodyText); + + let ellipsis = "\u2026"; + try { + ellipsis = Services.prefs.getComplexValue( + "intl.ellipsis", + Ci.nsIPrefLocalizedString + ).data; + } catch (e) {} + + // Copied from nsContextMenu.js' formatSearchContextItem(). + // If the JS character after our truncation point is a trail surrogate, + // include it in the truncated string to avoid splitting a surrogate pair. + let truncLength = BODY_TEXT_LIMIT; + let truncChar = bodyText[BODY_TEXT_LIMIT].charCodeAt(0); + if (truncChar >= 0xdc00 && truncChar <= 0xdfff) { + truncLength++; + } + + bodyText = bodyText.substring(0, truncLength) + ellipsis; + } + bodyTextLabel.textContent = bodyText; + } + // fall through + case 2: + document + .getElementById("alertTitleLabel") + .setAttribute("value", window.arguments[1]); + // fall through + case 1: + if (window.arguments[0]) { + document.getElementById("alertBox").setAttribute("hasImage", true); + document + .getElementById("alertImage") + .setAttribute("src", window.arguments[0]); + } + // fall through + case 0: + break; + } +} + +function onAlertLoad() { + const ALERT_DURATION_IMMEDIATE = 20000; + let alertTextBox = document.getElementById("alertTextBox"); + let alertImageBox = document.getElementById("alertImageBox"); + alertImageBox.style.minHeight = alertTextBox.scrollHeight + "px"; + + window.sizeToContent(); + + if (gReplacedWindow && !gReplacedWindow.closed) { + moveWindowToReplace(gReplacedWindow); + gReplacedWindow.gIsReplaced = true; + gReplacedWindow.close(); + } else { + moveWindowToEnd(); + } + + window.addEventListener("XULAlertClose", function () { + window.close(); + }); + + // If the require interaction flag is set, prevent auto-closing the notification. + if (!gRequireInteraction) { + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + setTimeout(function () { + window.close(); + }, ALERT_DURATION_IMMEDIATE); + } else { + let alertBox = document.getElementById("alertBox"); + alertBox.addEventListener("animationend", function hideAlert(event) { + if ( + event.animationName == "alert-animation" || + event.animationName == "alert-clicked-animation" || + event.animationName == "alert-closing-animation" + ) { + alertBox.removeEventListener("animationend", hideAlert); + window.close(); + } + }); + alertBox.setAttribute("animate", true); + } + } + + let alertSettings = document.getElementById("alertSettings"); + alertSettings.addEventListener("focus", onAlertSettingsFocus); + alertSettings.addEventListener("click", onAlertSettingsClick); + + gIsActive = true; + + let ev = new CustomEvent("AlertActive", { bubbles: true, cancelable: true }); + document.documentElement.dispatchEvent(ev); + + if (gAlertListener) { + gAlertListener.observe(null, "alertshow", gAlertCookie); + } +} + +function moveWindowToReplace(aReplacedAlert) { + let heightDelta = window.outerHeight - aReplacedAlert.outerHeight; + + // Move windows that come after the replaced alert if the height is different. + if (heightDelta != 0) { + for (let alertWindow of Services.wm.getEnumerator("alert:alert")) { + if (!alertWindow.gIsActive) { + continue; + } + // boolean to determine if the alert window is after the replaced alert. + let alertIsAfter = + gOrigin & NS_ALERT_TOP + ? alertWindow.screenY > aReplacedAlert.screenY + : aReplacedAlert.screenY > alertWindow.screenY; + if (alertIsAfter) { + // The new Y position of the window. + let adjustedY = + gOrigin & NS_ALERT_TOP + ? alertWindow.screenY + heightDelta + : alertWindow.screenY - heightDelta; + alertWindow.moveTo(alertWindow.screenX, adjustedY); + } + } + } + + let adjustedY = + gOrigin & NS_ALERT_TOP + ? aReplacedAlert.screenY + : aReplacedAlert.screenY - heightDelta; + window.moveTo(aReplacedAlert.screenX, adjustedY); +} + +function moveWindowToEnd() { + // Determine position + let x = + gOrigin & NS_ALERT_LEFT + ? screen.availLeft + : screen.availLeft + screen.availWidth - window.outerWidth; + let y = + gOrigin & NS_ALERT_TOP + ? screen.availTop + : screen.availTop + screen.availHeight - window.outerHeight; + + // Position the window at the end of all alerts. + for (let alertWindow of Services.wm.getEnumerator("alert:alert")) { + if (alertWindow != window && alertWindow.gIsActive) { + if (gOrigin & NS_ALERT_TOP) { + y = Math.max( + y, + alertWindow.screenY + alertWindow.outerHeight - WINDOW_SHADOW_SPREAD + ); + } else { + y = Math.min( + y, + alertWindow.screenY - window.outerHeight + WINDOW_SHADOW_SPREAD + ); + } + } + } + + // Offset the alert by WINDOW_MARGIN pixels from the edge of the screen + y += gOrigin & NS_ALERT_TOP ? WINDOW_MARGIN : -WINDOW_MARGIN; + x += gOrigin & NS_ALERT_LEFT ? WINDOW_MARGIN : -WINDOW_MARGIN; + + window.moveTo(x, y); +} + +function onAlertBeforeUnload() { + if (!gIsReplaced) { + // Move other alert windows to fill the gap left by closing alert. + let heightDelta = window.outerHeight + WINDOW_MARGIN - WINDOW_SHADOW_SPREAD; + for (let alertWindow of Services.wm.getEnumerator("alert:alert")) { + if (alertWindow != window && alertWindow.gIsActive) { + if (gOrigin & NS_ALERT_TOP) { + if (alertWindow.screenY > window.screenY) { + alertWindow.moveTo( + alertWindow.screenX, + alertWindow.screenY - heightDelta + ); + } + } else if (window.screenY > alertWindow.screenY) { + alertWindow.moveTo( + alertWindow.screenX, + alertWindow.screenY + heightDelta + ); + } + } + } + } + + if (gAlertListener) { + gAlertListener.observe(null, "alertfinished", gAlertCookie); + } +} + +function onAlertClick() { + if (gAlertListener && gAlertTextClickable) { + gAlertListener.observe(null, "alertclickcallback", gAlertCookie); + } + + let alertBox = document.getElementById("alertBox"); + if (alertBox.getAttribute("animate") == "true") { + // Closed when the animation ends. + alertBox.setAttribute("clicked", "true"); + } else { + window.close(); + } +} + +function doNotDisturb() { + const alertService = Cc["@mozilla.org/alerts-service;1"] + .getService(Ci.nsIAlertsService) + .QueryInterface(Ci.nsIAlertsDoNotDisturb); + alertService.manualDoNotDisturb = true; + onAlertClose(); +} + +function disableForOrigin() { + gAlertListener.observe(null, "alertdisablecallback", gAlertCookie); + onAlertClose(); +} + +function onAlertSettingsFocus(event) { + event.target.removeAttribute("focusedViaMouse"); +} + +function onAlertSettingsClick(event) { + // XXXjaws Hack used to remove the focus-ring only + // from mouse interaction, but focus-ring drawing + // should only be enabled when interacting via keyboard. + event.target.setAttribute("focusedViaMouse", true); + event.stopPropagation(); +} + +function openSettings() { + gAlertListener.observe(null, "alertsettingscallback", gAlertCookie); + onAlertClose(); +} + +function onAlertClose() { + let alertBox = document.getElementById("alertBox"); + if (alertBox.getAttribute("animate") == "true") { + // Closed when the animation ends. + alertBox.setAttribute("closing", "true"); + } else { + window.close(); + } +} diff --git a/toolkit/components/alerts/alert.xhtml b/toolkit/components/alerts/alert.xhtml new file mode 100644 index 0000000000..5c2f5e369e --- /dev/null +++ b/toolkit/components/alerts/alert.xhtml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + diff --git a/toolkit/components/alerts/jar.mn b/toolkit/components/alerts/jar.mn new file mode 100644 index 0000000000..533df4c224 --- /dev/null +++ b/toolkit/components/alerts/jar.mn @@ -0,0 +1,8 @@ +# 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/. + +toolkit.jar: + content/global/alerts/alert.css (alert.css) + content/global/alerts/alert.xhtml (alert.xhtml) + content/global/alerts/alert.js (alert.js) diff --git a/toolkit/components/alerts/moz.build b/toolkit/components/alerts/moz.build new file mode 100644 index 0000000000..9a65c4c9fe --- /dev/null +++ b/toolkit/components/alerts/moz.build @@ -0,0 +1,45 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] + +BROWSER_CHROME_MANIFESTS += ["test/browser.ini"] + +XPIDL_SOURCES += [ + "nsIAlertsService.idl", +] + +if CONFIG["OS_ARCH"] == "WINNT": + XPIDL_SOURCES += [ + "nsIWindowsAlertsService.idl", + ] + +XPIDL_MODULE = "alerts" + +EXPORTS += [ + "nsAlertsUtils.h", +] + +EXPORTS.mozilla += [ + "AlertNotification.h", + "AlertNotificationIPCSerializer.h", +] + +UNIFIED_SOURCES += [ + "AlertNotification.cpp", + "nsAlertsService.cpp", + "nsAlertsUtils.cpp", + "nsXULAlerts.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +JAR_MANIFESTS += ["jar.mn"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Notifications and Alerts") diff --git a/toolkit/components/alerts/nsAlertsService.cpp b/toolkit/components/alerts/nsAlertsService.cpp new file mode 100644 index 0000000000..94846bbd9f --- /dev/null +++ b/toolkit/components/alerts/nsAlertsService.cpp @@ -0,0 +1,330 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */ +/* 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 "mozilla/dom/ContentChild.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_alerts.h" +#include "mozilla/Telemetry.h" +#include "nsXULAppAPI.h" + +#include "nsAlertsService.h" + +#include "nsXPCOM.h" +#include "nsPromiseFlatString.h" +#include "nsToolkitCompsCID.h" +#include "nsComponentManagerUtils.h" + +#ifdef MOZ_PLACES +# include "nsIFaviconService.h" +#endif // MOZ_PLACES + +#ifdef XP_WIN +# include +#endif + +using namespace mozilla; + +using mozilla::dom::ContentChild; + +namespace { + +#ifdef MOZ_PLACES + +class IconCallback final : public nsIFaviconDataCallback { + public: + NS_DECL_ISUPPORTS + + IconCallback(nsIAlertsService* aBackend, nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) + : mBackend(aBackend), mAlert(aAlert), mAlertListener(aAlertListener) {} + + NS_IMETHOD + OnComplete(nsIURI* aIconURI, uint32_t aIconSize, const uint8_t* aIconData, + const nsACString& aMimeType, uint16_t aWidth) override { + nsresult rv = NS_ERROR_FAILURE; + if (aIconSize > 0) { + nsCOMPtr alertsIconData(do_QueryInterface(mBackend)); + if (alertsIconData) { + rv = alertsIconData->ShowAlertWithIconData(mAlert, mAlertListener, + aIconSize, aIconData); + } + } else if (aIconURI) { + nsCOMPtr alertsIconURI(do_QueryInterface(mBackend)); + if (alertsIconURI) { + rv = alertsIconURI->ShowAlertWithIconURI(mAlert, mAlertListener, + aIconURI); + } + } + if (NS_FAILED(rv)) { + rv = mBackend->ShowAlert(mAlert, mAlertListener); + } + return rv; + } + + private: + virtual ~IconCallback() = default; + + nsCOMPtr mBackend; + nsCOMPtr mAlert; + nsCOMPtr mAlertListener; +}; + +NS_IMPL_ISUPPORTS(IconCallback, nsIFaviconDataCallback) + +#endif // MOZ_PLACES + +nsresult ShowWithIconBackend(nsIAlertsService* aBackend, + nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) { +#ifdef MOZ_PLACES + nsCOMPtr uri; + nsresult rv = aAlert->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv) || !uri) { + return NS_ERROR_FAILURE; + } + + // Ensure the backend supports favicons. + nsCOMPtr alertsIconData(do_QueryInterface(aBackend)); + nsCOMPtr alertsIconURI; + if (!alertsIconData) { + alertsIconURI = do_QueryInterface(aBackend); + } + if (!alertsIconData && !alertsIconURI) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + nsCOMPtr favicons( + do_GetService("@mozilla.org/browser/favicon-service;1")); + NS_ENSURE_TRUE(favicons, NS_ERROR_FAILURE); + + nsCOMPtr callback = + new IconCallback(aBackend, aAlert, aAlertListener); + if (alertsIconData) { + return favicons->GetFaviconDataForPage(uri, callback, 0); + } + return favicons->GetFaviconURLForPage(uri, callback, 0); +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif // !MOZ_PLACES +} + +nsresult ShowWithBackend(nsIAlertsService* aBackend, + nsIAlertNotification* aAlert, + nsIObserver* aAlertListener, + const nsAString& aPersistentData) { + if (!aPersistentData.IsEmpty()) { + return aBackend->ShowPersistentNotification(aPersistentData, aAlert, + aAlertListener); + } + + if (Preferences::GetBool("alerts.showFavicons")) { + nsresult rv = ShowWithIconBackend(aBackend, aAlert, aAlertListener); + if (NS_SUCCEEDED(rv)) { + return rv; + } + } + + // If favicons are disabled, or the backend doesn't support them, show the + // alert without one. + return aBackend->ShowAlert(aAlert, aAlertListener); +} + +} // anonymous namespace + +NS_IMPL_ISUPPORTS(nsAlertsService, nsIAlertsService, nsIAlertsDoNotDisturb) + +nsAlertsService::nsAlertsService() : mBackend(nullptr) { + mBackend = do_GetService(NS_SYSTEMALERTSERVICE_CONTRACTID); +} + +nsAlertsService::~nsAlertsService() = default; + +bool nsAlertsService::ShouldShowAlert() { + bool result = true; + +#ifdef XP_WIN + if (!xpc::IsInAutomation()) { + QUERY_USER_NOTIFICATION_STATE qstate; + if (SUCCEEDED(SHQueryUserNotificationState(&qstate))) { + if (qstate != QUNS_ACCEPTS_NOTIFICATIONS) { + result = false; + } + } + } +#endif + + nsCOMPtr alertsDND(GetDNDBackend()); + if (alertsDND) { + bool suppressForScreenSharing = false; + nsresult rv = + alertsDND->GetSuppressForScreenSharing(&suppressForScreenSharing); + if (NS_SUCCEEDED(rv)) { + result &= !suppressForScreenSharing; + } + } + + return result; +} + +bool nsAlertsService::ShouldUseSystemBackend() { + if (!mBackend) { + return false; + } + return StaticPrefs::alerts_useSystemBackend(); +} + +NS_IMETHODIMP nsAlertsService::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 alert = + do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); + NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE); + // vibrate is unused + nsTArray vibrate; + nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText, + aAlertTextClickable, aAlertCookie, aBidi, aLang, + aData, aPrincipal, aInPrivateBrowsing, + aRequireInteraction, false, vibrate); + NS_ENSURE_SUCCESS(rv, rv); + return ShowAlert(alert, aAlertListener); +} + +NS_IMETHODIMP nsAlertsService::ShowAlert(nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) { + return ShowPersistentNotification(u""_ns, aAlert, aAlertListener); +} + +NS_IMETHODIMP nsAlertsService::ShowPersistentNotification( + const nsAString& aPersistentData, nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) { + NS_ENSURE_ARG(aAlert); + + nsAutoString cookie; + nsresult rv = aAlert->GetCookie(cookie); + NS_ENSURE_SUCCESS(rv, rv); + + if (XRE_IsContentProcess()) { + ContentChild* cpc = ContentChild::GetSingleton(); + + if (aAlertListener) cpc->AddRemoteAlertObserver(cookie, aAlertListener); + + cpc->SendShowAlert(aAlert); + return NS_OK; + } + + // Check if there is an optional service that handles system-level + // notifications + if (ShouldUseSystemBackend()) { + rv = ShowWithBackend(mBackend, aAlert, aAlertListener, aPersistentData); + if (NS_SUCCEEDED(rv)) { + return rv; + } + // If the system backend failed to show the alert, clear the backend and + // retry with XUL notifications. Future alerts will always use XUL. + mBackend = nullptr; + } + + if (!ShouldShowAlert()) { + // Do not display the alert. Instead call alertfinished and get out. + if (aAlertListener) + aAlertListener->Observe(nullptr, "alertfinished", cookie.get()); + return NS_OK; + } + + // Use XUL notifications as a fallback if above methods have failed. + nsCOMPtr xulBackend(nsXULAlerts::GetInstance()); + NS_ENSURE_TRUE(xulBackend, NS_ERROR_FAILURE); + return ShowWithBackend(xulBackend, aAlert, aAlertListener, aPersistentData); +} + +NS_IMETHODIMP nsAlertsService::CloseAlert(const nsAString& aAlertName, + bool aContextClosed) { + if (XRE_IsContentProcess()) { + ContentChild* cpc = ContentChild::GetSingleton(); + cpc->SendCloseAlert(nsAutoString(aAlertName), aContextClosed); + return NS_OK; + } + + nsresult rv; + // Try the system notification service. + if (ShouldUseSystemBackend()) { + rv = mBackend->CloseAlert(aAlertName, aContextClosed); + if (NS_WARN_IF(NS_FAILED(rv))) { + // If the system backend failed to close the alert, fall back to XUL for + // future alerts. + mBackend = nullptr; + } + } else { + nsCOMPtr xulBackend(nsXULAlerts::GetInstance()); + NS_ENSURE_TRUE(xulBackend, NS_ERROR_FAILURE); + rv = xulBackend->CloseAlert(aAlertName, aContextClosed); + } + return rv; +} + +// nsIAlertsDoNotDisturb +NS_IMETHODIMP nsAlertsService::GetManualDoNotDisturb(bool* aRetVal) { +#ifdef MOZ_WIDGET_ANDROID + return NS_ERROR_NOT_IMPLEMENTED; +#else + nsCOMPtr alertsDND(GetDNDBackend()); + NS_ENSURE_TRUE(alertsDND, NS_ERROR_NOT_IMPLEMENTED); + return alertsDND->GetManualDoNotDisturb(aRetVal); +#endif +} + +NS_IMETHODIMP nsAlertsService::SetManualDoNotDisturb(bool aDoNotDisturb) { +#ifdef MOZ_WIDGET_ANDROID + return NS_ERROR_NOT_IMPLEMENTED; +#else + nsCOMPtr alertsDND(GetDNDBackend()); + NS_ENSURE_TRUE(alertsDND, NS_ERROR_NOT_IMPLEMENTED); + + nsresult rv = alertsDND->SetManualDoNotDisturb(aDoNotDisturb); + if (NS_SUCCEEDED(rv)) { + Telemetry::Accumulate(Telemetry::ALERTS_SERVICE_DND_ENABLED, 1); + } + return rv; +#endif +} + +NS_IMETHODIMP nsAlertsService::GetSuppressForScreenSharing(bool* aRetVal) { +#ifdef MOZ_WIDGET_ANDROID + return NS_ERROR_NOT_IMPLEMENTED; +#else + nsCOMPtr alertsDND(GetDNDBackend()); + NS_ENSURE_TRUE(alertsDND, NS_ERROR_NOT_IMPLEMENTED); + return alertsDND->GetSuppressForScreenSharing(aRetVal); +#endif +} + +NS_IMETHODIMP nsAlertsService::SetSuppressForScreenSharing(bool aSuppress) { +#ifdef MOZ_WIDGET_ANDROID + return NS_ERROR_NOT_IMPLEMENTED; +#else + nsCOMPtr alertsDND(GetDNDBackend()); + NS_ENSURE_TRUE(alertsDND, NS_ERROR_NOT_IMPLEMENTED); + return alertsDND->SetSuppressForScreenSharing(aSuppress); +#endif +} + +already_AddRefed nsAlertsService::GetDNDBackend() { + nsCOMPtr backend; + // Try the system notification service. + if (ShouldUseSystemBackend()) { + backend = mBackend; + } + if (!backend) { + backend = nsXULAlerts::GetInstance(); + } + + nsCOMPtr alertsDND(do_QueryInterface(backend)); + return alertsDND.forget(); +} diff --git a/toolkit/components/alerts/nsAlertsService.h b/toolkit/components/alerts/nsAlertsService.h new file mode 100644 index 0000000000..b228f57383 --- /dev/null +++ b/toolkit/components/alerts/nsAlertsService.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsAlertsService_h__ +#define nsAlertsService_h__ + +#include "nsIAlertsService.h" +#include "nsCOMPtr.h" +#include "nsXULAlerts.h" + +class nsAlertsService : public nsIAlertsService, public nsIAlertsDoNotDisturb { + public: + NS_DECL_NSIALERTSDONOTDISTURB + NS_DECL_NSIALERTSSERVICE + NS_DECL_ISUPPORTS + + nsAlertsService(); + + protected: + virtual ~nsAlertsService(); + + bool ShouldShowAlert(); + bool ShouldUseSystemBackend(); + already_AddRefed GetDNDBackend(); + nsCOMPtr mBackend; +}; + +#endif /* nsAlertsService_h__ */ diff --git a/toolkit/components/alerts/nsAlertsUtils.cpp b/toolkit/components/alerts/nsAlertsUtils.cpp new file mode 100644 index 0000000000..b63b368c75 --- /dev/null +++ b/toolkit/components/alerts/nsAlertsUtils.cpp @@ -0,0 +1,30 @@ +/* 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 "nsAlertsUtils.h" + +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIURI.h" +#include "nsString.h" + +/* static */ +bool nsAlertsUtils::IsActionablePrincipal(nsIPrincipal* aPrincipal) { + return aPrincipal && + !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal) && + !aPrincipal->GetIsNullPrincipal(); +} + +/* static */ +void nsAlertsUtils::GetSourceHostPort(nsIPrincipal* aPrincipal, + nsAString& aHostPort) { + if (!IsActionablePrincipal(aPrincipal)) { + return; + } + nsAutoCString hostPort; + if (NS_WARN_IF(NS_FAILED(aPrincipal->GetHostPort(hostPort)))) { + return; + } + CopyUTF8toUTF16(hostPort, aHostPort); +} diff --git a/toolkit/components/alerts/nsAlertsUtils.h b/toolkit/components/alerts/nsAlertsUtils.h new file mode 100644 index 0000000000..c126769de7 --- /dev/null +++ b/toolkit/components/alerts/nsAlertsUtils.h @@ -0,0 +1,29 @@ +/* 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/. */ + +#ifndef nsAlertsUtils_h +#define nsAlertsUtils_h + +#include "nsIPrincipal.h" +#include "nsString.h" + +class nsAlertsUtils final { + private: + nsAlertsUtils() = delete; + + public: + /** + * Indicates whether an alert from |aPrincipal| should include the source + * string and action buttons. Returns false if |aPrincipal| is |nullptr|, or + * a system, expanded, or null principal. + */ + static bool IsActionablePrincipal(nsIPrincipal* aPrincipal); + + /** + * Sets |aHostPort| to the host and port from |aPrincipal|'s URI, or an + * empty string if |aPrincipal| is not actionable. + */ + static void GetSourceHostPort(nsIPrincipal* aPrincipal, nsAString& aHostPort); +}; +#endif /* nsAlertsUtils_h */ diff --git a/toolkit/components/alerts/nsIAlertsService.idl b/toolkit/components/alerts/nsIAlertsService.idl new file mode 100644 index 0000000000..de1f78bbab --- /dev/null +++ b/toolkit/components/alerts/nsIAlertsService.idl @@ -0,0 +1,337 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" +#include "nsIObserver.idl" + +interface imgIRequest; +interface nsICancelable; +interface nsIPrincipal; +interface nsIURI; + +%{C++ +#define ALERT_NOTIFICATION_CONTRACTID "@mozilla.org/alert-notification;1" +%} + +[scriptable, uuid(a71a637d-de1d-47c6-a8d2-c60b2596f471)] +interface nsIAlertNotificationImageListener : nsISupports +{ + /** + * Called when the image finishes loading. + * + * @param aUserData An opaque parameter passed to |loadImage|. + * @param aRequest The image request. + */ + void onImageReady(in nsISupports aUserData, in imgIRequest aRequest); + + /** + * Called if the alert doesn't have an image, or if the image request times + * out or fails. + * + * @param aUserData An opaque parameter passed to |loadImage|. + */ + void onImageMissing(in nsISupports aUserData); +}; + +[scriptable, uuid(a054c2c9-2787-4686-859c-45609d790056)] +interface nsIAlertAction : nsISupports +{ + /** + * Returns a string identifying a user action to be displayed on the alert. + * + * This string is an opaque identifier that identifies an action in potential + * callbacks; it is not displayed to the user. + */ + readonly attribute AString action; + + /** + * Returns a string containing action text to be shown to the user. + */ + readonly attribute AString title; + + /** + * Returns a string containing the URL of an icon to display with the action. + */ + readonly attribute AString iconURL; + + /** + * On Windows, chrome-privileged notifications -- i.e., those with a + * non-actionable principal -- can have actions that are activated by Windows + * and not processed by Firefox. When `windowsSystemActivationType` is true, + * we request Windows to process `action`. At the time of writing, Windows + * recognizes the following actions: + * + * - `action="dismiss"` dismisses the alert entirely. + * - `action="snooze"` snoozes the alert, generally making it disappear before + * reappearing a Windows-determined amount of time later. + * + * On non-Windows, this field is ignored. + */ + readonly attribute boolean windowsSystemActivationType; +}; + +[scriptable, uuid(cf2e4cb6-4b8f-4eca-aea9-d51a8f9f7a50)] +interface nsIAlertNotification : nsISupports +{ + /** Initializes an alert notification. */ + void init([optional] in AString aName, + [optional] in AString aImageURL, + [optional] in AString aTitle, + [optional] in AString aText, + [optional] in boolean aTextClickable, + [optional] in AString aCookie, + [optional] in AString aDir, + [optional] in AString aLang, + [optional] in AString aData, + [optional] in nsIPrincipal aPrincipal, + [optional] in boolean aInPrivateBrowsing, + [optional] in boolean aRequireInteraction, + [optional] in boolean aSilent, + [optional] in Array aVibrate); + + /** + * The name of the notification. On Windows and Android, the name is hashed + * and used as a notification ID. Notifications will replace previous + * notifications with the same name. + */ + readonly attribute AString name; + + /** + * A URL identifying the image to put in the alert. The OS X backend limits + * the amount of time it will wait for the image to load to six seconds. After + * that time, the alert will show without an image. + */ + readonly attribute AString imageURL; + + /** The title for the alert. */ + readonly attribute AString title; + + /** The contents of the alert. */ + readonly attribute AString text; + + /** + * Controls the click behavior. If true, the alert listener will be notified + * when the user clicks on the alert. + */ + readonly attribute boolean textClickable; + + /** + * An opaque cookie that will be passed to the alert listener for each + * callback. + */ + readonly attribute AString cookie; + + /** + * Bidi override for the title and contents. Valid values are "auto", "ltr", + * or "rtl". Ignored if the backend doesn't support localization. + */ + readonly attribute AString dir; + + /** + * Language of the title and text. Ignored if the backend doesn't support + * localization. + */ + readonly attribute AString lang; + + /** + * A Base64-encoded structured clone buffer containing data associated with + * this alert. Only used for web notifications. Chrome callers should use a + * cookie instead. + */ + readonly attribute AString data; + + /** + * The principal of the page that created the alert. Used for IPC security + * checks, and to determine whether the alert is actionable. + */ + readonly attribute nsIPrincipal principal; + + /** + * The URI of the page that created the alert. |null| if the alert is not + * actionable. + */ + readonly attribute nsIURI URI; + + /** + * Controls the image loading behavior. If true, the image request will be + * loaded anonymously (without cookies or authorization tokens). + */ + readonly attribute boolean inPrivateBrowsing; + + /** + * Indicates that the notification should remain readily available until + * the user activates or dismisses the notification. + */ + readonly attribute boolean requireInteraction; + + /** + * When set, indicates that no sounds or vibrations should be made. + */ + readonly attribute boolean silent; + + /** + * A vibration pattern to run with the display of the notification. A + * vibration pattern can be an array with as few as one member. The values + * are times in milliseconds where the even indices (0, 2, 4, etc.) indicate + * how long to vibrate and the odd indices indicate how long to pause. For + * example, [300, 100, 400] would vibrate 300ms, pause 100ms, then vibrate + * 400ms. + */ + readonly attribute Array vibrate; + + /** + * Actions available for users to choose from for interacting with + * the notification. + * + * Implemented only for the system backend on Windows. + */ + attribute Array actions; + + /** + * Indicates whether this alert should show the source string and action + * buttons. False for system alerts (which can omit the principal), or + * expanded, system, and null principals. + */ + readonly attribute boolean actionable; + + /** + * The host and port of the originating page, or an empty string if the alert + * is not actionable. + */ + readonly attribute AString source; + + /** + * A URL to navigate to if the application is relaunched in to complete + * interaction with this alert. + */ + attribute AString launchURL; + + /** + * Loads the image associated with this alert. + * + * @param aTimeout The number of milliseconds to wait before cancelling the + * image request. If zero, there is no timeout. + * @param aListener An |nsIAlertNotificationImageListener| implementation, + * notified when the image loads. The listener is kept alive + * until the request completes. + * @param aUserData An opaque parameter passed to the listener's methods. + * Not used by the libnotify backend, but the OS X backend + * passes the pending notification. + */ + nsICancelable loadImage(in unsigned long aTimeout, + in nsIAlertNotificationImageListener aListener, + [optional] in nsISupports aUserData); +}; + +[scriptable, uuid(f7a36392-d98b-4141-a7d7-4e46642684e3)] +interface nsIAlertsService : nsISupports +{ + void showPersistentNotification(in AString aPersistentData, + in nsIAlertNotification aAlert, + [optional] in nsIObserver aAlertListener); + + void showAlert(in nsIAlertNotification aAlert, + [optional] in nsIObserver aAlertListener); + /** + * Initializes and shows an |nsIAlertNotification| with the given parameters. + * + * @param aAlertListener Used for callbacks. May be null if the caller + * doesn't care about callbacks. + * @see nsIAlertNotification for descriptions of all other parameters. + * @throws NS_ERROR_NOT_AVAILABLE If the notification cannot be displayed. + * + * The following arguments will be passed to the alertListener's observe() + * method: + * subject - null + * topic - "alertfinished" when the alert goes away + * "alertdisablecallback" when alerts should be disabled for the principal + * "alertsettingscallback" when alert settings should be opened + * "alertclickcallback" when the text is clicked + * "alertshow" when the alert is shown + * data - the value of the cookie parameter passed to showAlertNotification. + * + * @note Depending on current circumstances (if the user's in a fullscreen + * application, for instance), the alert might not be displayed at all. + * In that case, if an alert listener is passed in it will receive the + * "alertfinished" notification immediately. + */ + void showAlertNotification(in AString aImageURL, + in AString aTitle, + in AString aText, + [optional] in boolean aTextClickable, + [optional] in AString aCookie, + [optional] in nsIObserver aAlertListener, + [optional] in AString aName, + [optional] in AString aDir, + [optional] in AString aLang, + [optional] in AString aData, + [optional] in nsIPrincipal aPrincipal, + [optional] in boolean aInPrivateBrowsing, + [optional] in boolean aRequireInteraction); + + /** + * Close alerts created by the service. + * + * @param aName The name of the notification to close. If no name + * is provided then only a notification created with + * no name (if any) will be closed. + * @param aContextClosed The notification was implicitly closed, e.g. by tab + * or window closure. This is necessary to track as some + * platforms intentionally leave the notification visible + * unless explicitly closed, e.g. by notification.close(). + */ + void closeAlert([optional] in AString aName, [optional] in boolean aContextClosed); + +}; + +[scriptable, uuid(c5d63e3a-259d-45a8-b964-8377967cb4d2)] +interface nsIAlertsDoNotDisturb : nsISupports +{ + /** + * Toggles a manual Do Not Disturb mode for the service to reduce the amount + * of disruption that alerts cause the user. + * This may mean only displaying them in a notification tray/center or not + * displaying them at all. If a system backend already supports a similar + * feature controlled by the user, enabling this may not have any impact on + * code to show an alert. e.g. on OS X, the system will take care not + * disrupting a user if we simply create a notification like usual. + */ + attribute bool manualDoNotDisturb; + + /** + * Toggles a mode for the service to suppress all notifications from + * being dispatched when sharing the screen via the getMediaDisplay + * API. + */ + attribute bool suppressForScreenSharing; +}; + +[scriptable, uuid(fc6d7f0a-0cf6-4268-8c71-ab640842b9b1)] +interface nsIAlertsIconData : nsISupports +{ + /** + * Shows an alert with an icon. Web notifications use the favicon of the + * page that created the alert. If the favicon is not in the Places database, + * |aIconSize| will be zero. + */ + void showAlertWithIconData(in nsIAlertNotification aAlert, + [optional] in nsIObserver aAlertListener, + [optional] in uint32_t aIconSize, + [const, array, size_is(aIconSize)] in uint8_t + aIconData); +}; + +[scriptable, uuid(f3c82915-bf60-41ea-91ce-6c46b22e381a)] +interface nsIAlertsIconURI : nsISupports +{ + /** + * Shows an alert with an icon URI. Web notifications use |moz-anno:| + * URIs to reference favicons from Places. If the page doesn't have a + * favicon, |aIconURI| will be |null|. + */ + void showAlertWithIconURI(in nsIAlertNotification aAlert, + [optional] in nsIObserver aAlertListener, + [optional] in nsIURI aIconURI); +}; diff --git a/toolkit/components/alerts/nsIWindowsAlertsService.idl b/toolkit/components/alerts/nsIWindowsAlertsService.idl new file mode 100644 index 0000000000..6f39fd321f --- /dev/null +++ b/toolkit/components/alerts/nsIWindowsAlertsService.idl @@ -0,0 +1,69 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsIAlertsService.idl" +#include "nsISupports.idl" +#include "nsIObserver.idl" + +[scriptable, function, uuid(059f8305-4e2f-4d31-a9cb-5b918ee84773)] +interface nsIUnknownWindowsTagListener : nsISupports +{ + /** + * Handle any launch URL associated to the given Windows-specific tag string. + * Usually, this will navigate to the launch URL in some manner. + * + * @param {AString} aWindowsTag the tag + * @param {AString} aLaunchURL associated launch URL, or null. + * @param {AString} aPrivilegedName associated alert name if this is a chrome + * privileged alert, or null. + */ + void handleUnknownWindowsTag(in AString aWindowsTag, + in AString aLaunchURL, + in AString aPrivilegedName); +}; + +[scriptable, uuid(e01c8066-fb4b-4304-b9c9-ab6ed4a8322c)] +interface nsIWindowsAlertsService : nsIAlertsService +{ + /** + * If callbacks for the given Windows-specific tag string will be handled by + * this Firefox process, set the associated event. + * + * @param {AString} aWindowsTag the tag + * @return {Promise} + * @resolves {Object} + * Resolves with an Object, may contain the following optional + * properties if notification exists but wasn't registered with + * the WindowsAlertService: + * + * `launchUrl` {string} a fallback URL to open. + * + * `privilegedName` {string} a privileged name assigned by the + * browser chrome. + * + * @rejects `nsresult` when there was an error retrieving the notification. + */ + [implicit_jscontext] + Promise handleWindowsTag(in AString aWindowsTag); + + /** + * Get the Windows-specific XML generated for the given alert. + * + * @note This method is intended for testing purposes. + * + * @param {nsIAlertNotification} aAlert the alert + * @param {AString} an optional Windows tag; default is generated + * @return {string} generated XML + */ + AString getXmlStringForWindowsAlert(in nsIAlertNotification aAlert, + [optional] in AString aWindowsTag); + + /** + * Removes all action center and snoozed notifications associated with this + * install. Note that this removes all notifications regardless of which profile + * they originated from. + */ + void removeAllNotificationsForInstall(); +}; diff --git a/toolkit/components/alerts/nsXULAlerts.cpp b/toolkit/components/alerts/nsXULAlerts.cpp new file mode 100644 index 0000000000..b45caef721 --- /dev/null +++ b/toolkit/components/alerts/nsXULAlerts.cpp @@ -0,0 +1,405 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */ +/* 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 "nsXULAlerts.h" + +#include "nsArray.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/EventForwards.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/dom/Notification.h" +#include "mozilla/Unused.h" +#include "nsISupportsPrimitives.h" +#include "nsPIDOMWindow.h" +#include "nsIWindowWatcher.h" + +using namespace mozilla; + +#define ALERT_CHROME_URL "chrome://global/content/alerts/alert.xhtml"_ns + +namespace { +StaticRefPtr gXULAlerts; +} // anonymous namespace + +NS_IMPL_CYCLE_COLLECTION(nsXULAlertObserver, mAlertWindow) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULAlertObserver) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULAlertObserver) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULAlertObserver) + +NS_IMETHODIMP +nsXULAlertObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp("alertfinished", aTopic)) { + mozIDOMWindowProxy* currentAlert = + mXULAlerts->mNamedWindows.GetWeak(mAlertName); + // The window in mNamedWindows might be a replacement, thus it should only + // be removed if it is the same window that is associated with this + // listener. + if (currentAlert == mAlertWindow) { + mXULAlerts->mNamedWindows.Remove(mAlertName); + + if (mIsPersistent) { + mXULAlerts->PersistentAlertFinished(); + } + } + } + + nsresult rv = NS_OK; + if (mObserver) { + rv = mObserver->Observe(aSubject, aTopic, aData); + } + return rv; +} + +// We don't cycle collect nsXULAlerts since gXULAlerts will keep the instance +// alive till shutdown anyway. +NS_IMPL_ISUPPORTS(nsXULAlerts, nsIAlertsService, nsIAlertsDoNotDisturb, + nsIAlertsIconURI) + +/* static */ +already_AddRefed nsXULAlerts::GetInstance() { + // Gecko on Android does not fully support XUL windows. +#ifndef MOZ_WIDGET_ANDROID + if (!gXULAlerts) { + gXULAlerts = new nsXULAlerts(); + ClearOnShutdown(&gXULAlerts); + } +#endif // MOZ_WIDGET_ANDROID + RefPtr instance = gXULAlerts.get(); + return instance.forget(); +} + +void nsXULAlerts::PersistentAlertFinished() { + MOZ_ASSERT(mPersistentAlertCount); + mPersistentAlertCount--; + + // Show next pending persistent alert if any. + if (!mPendingPersistentAlerts.IsEmpty()) { + ShowAlertWithIconURI(mPendingPersistentAlerts[0].mAlert, + mPendingPersistentAlerts[0].mListener, nullptr); + mPendingPersistentAlerts.RemoveElementAt(0); + } +} + +NS_IMETHODIMP +nsXULAlerts::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 alert = + do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); + NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE); + // vibrate is unused for now + nsTArray vibrate; + nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, aAlertText, + aAlertTextClickable, aAlertCookie, aBidi, aLang, + aData, aPrincipal, aInPrivateBrowsing, + aRequireInteraction, false, vibrate); + NS_ENSURE_SUCCESS(rv, rv); + return ShowAlert(alert, aAlertListener); +} + +NS_IMETHODIMP +nsXULAlerts::ShowPersistentNotification(const nsAString& aPersistentData, + nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) { + return ShowAlert(aAlert, aAlertListener); +} + +NS_IMETHODIMP +nsXULAlerts::ShowAlert(nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) { + nsAutoString name; + nsresult rv = aAlert->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + + // If there is a pending alert with the same name in the list of + // pending alerts, replace it. + if (!mPendingPersistentAlerts.IsEmpty()) { + for (uint32_t i = 0; i < mPendingPersistentAlerts.Length(); i++) { + nsAutoString pendingAlertName; + nsCOMPtr pendingAlert = + mPendingPersistentAlerts[i].mAlert; + rv = pendingAlert->GetName(pendingAlertName); + NS_ENSURE_SUCCESS(rv, rv); + + if (pendingAlertName.Equals(name)) { + nsAutoString cookie; + rv = pendingAlert->GetCookie(cookie); + NS_ENSURE_SUCCESS(rv, rv); + + if (mPendingPersistentAlerts[i].mListener) { + rv = mPendingPersistentAlerts[i].mListener->Observe( + nullptr, "alertfinished", cookie.get()); + NS_ENSURE_SUCCESS(rv, rv); + } + + mPendingPersistentAlerts[i].Init(aAlert, aAlertListener); + return NS_OK; + } + } + } + + bool requireInteraction; + rv = aAlert->GetRequireInteraction(&requireInteraction); + NS_ENSURE_SUCCESS(rv, rv); + + if (requireInteraction && !mNamedWindows.Contains(name) && + static_cast(mPersistentAlertCount) >= + Preferences::GetInt("dom.webnotifications.requireinteraction.count", + 0)) { + PendingAlert* pa = mPendingPersistentAlerts.AppendElement(); + pa->Init(aAlert, aAlertListener); + return NS_OK; + } else { + return ShowAlertWithIconURI(aAlert, aAlertListener, nullptr); + } +} + +NS_IMETHODIMP +nsXULAlerts::ShowAlertWithIconURI(nsIAlertNotification* aAlert, + nsIObserver* aAlertListener, + nsIURI* aIconURI) { + bool inPrivateBrowsing; + nsresult rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString cookie; + rv = aAlert->GetCookie(cookie); + NS_ENSURE_SUCCESS(rv, rv); + + if (mDoNotDisturb) { + if (aAlertListener) + aAlertListener->Observe(nullptr, "alertfinished", cookie.get()); + return NS_OK; + } + + nsAutoString name; + rv = aAlert->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString imageUrl; + rv = aAlert->GetImageURL(imageUrl); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString title; + rv = aAlert->GetTitle(title); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString text; + rv = aAlert->GetText(text); + NS_ENSURE_SUCCESS(rv, rv); + + bool textClickable; + rv = aAlert->GetTextClickable(&textClickable); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString bidi; + rv = aAlert->GetDir(bidi); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString lang; + rv = aAlert->GetLang(lang); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString source; + rv = aAlert->GetSource(source); + NS_ENSURE_SUCCESS(rv, rv); + + bool requireInteraction; + rv = aAlert->GetRequireInteraction(&requireInteraction); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + + nsCOMPtr argsArray = nsArray::Create(); + + // create scriptable versions of our strings that we can store in our + // nsIMutableArray.... + nsCOMPtr scriptableImageUrl( + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableImageUrl, NS_ERROR_FAILURE); + + scriptableImageUrl->SetData(imageUrl); + rv = argsArray->AppendElement(scriptableImageUrl); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr scriptableAlertTitle( + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableAlertTitle, NS_ERROR_FAILURE); + + scriptableAlertTitle->SetData(title); + rv = argsArray->AppendElement(scriptableAlertTitle); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr scriptableAlertText( + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableAlertText, NS_ERROR_FAILURE); + + scriptableAlertText->SetData(text); + rv = argsArray->AppendElement(scriptableAlertText); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr scriptableIsClickable( + do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID)); + NS_ENSURE_TRUE(scriptableIsClickable, NS_ERROR_FAILURE); + + scriptableIsClickable->SetData(textClickable); + rv = argsArray->AppendElement(scriptableIsClickable); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr scriptableAlertCookie( + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableAlertCookie, NS_ERROR_FAILURE); + + scriptableAlertCookie->SetData(cookie); + rv = argsArray->AppendElement(scriptableAlertCookie); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr scriptableOrigin( + do_CreateInstance(NS_SUPPORTS_PRINT32_CONTRACTID)); + NS_ENSURE_TRUE(scriptableOrigin, NS_ERROR_FAILURE); + + int32_t origin = + LookAndFeel::GetInt(LookAndFeel::IntID::AlertNotificationOrigin); + scriptableOrigin->SetData(origin); + + rv = argsArray->AppendElement(scriptableOrigin); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr scriptableBidi( + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableBidi, NS_ERROR_FAILURE); + + scriptableBidi->SetData(bidi); + rv = argsArray->AppendElement(scriptableBidi); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr scriptableLang( + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableLang, NS_ERROR_FAILURE); + + scriptableLang->SetData(lang); + rv = argsArray->AppendElement(scriptableLang); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr scriptableRequireInteraction( + do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID)); + NS_ENSURE_TRUE(scriptableRequireInteraction, NS_ERROR_FAILURE); + + scriptableRequireInteraction->SetData(requireInteraction); + rv = argsArray->AppendElement(scriptableRequireInteraction); + NS_ENSURE_SUCCESS(rv, rv); + + // Alerts with the same name should replace the old alert in the same + // position. Provide the new alert window with a pointer to the replaced + // window so that it may take the same position. + nsCOMPtr replacedWindow = + do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv); + NS_ENSURE_TRUE(replacedWindow, NS_ERROR_FAILURE); + mozIDOMWindowProxy* previousAlert = mNamedWindows.GetWeak(name); + replacedWindow->SetData(previousAlert); + replacedWindow->SetDataIID(&NS_GET_IID(mozIDOMWindowProxy)); + rv = argsArray->AppendElement(replacedWindow); + NS_ENSURE_SUCCESS(rv, rv); + + if (requireInteraction) { + mPersistentAlertCount++; + } + + // Add an observer (that wraps aAlertListener) to remove the window from + // mNamedWindows when it is closed. + nsCOMPtr ifptr = + do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr alertObserver = + new nsXULAlertObserver(this, name, aAlertListener, requireInteraction); + nsCOMPtr iSupports(do_QueryInterface(alertObserver)); + ifptr->SetData(iSupports); + ifptr->SetDataIID(&NS_GET_IID(nsIObserver)); + rv = argsArray->AppendElement(ifptr); + NS_ENSURE_SUCCESS(rv, rv); + + // The source contains the host and port of the site that sent the + // notification. It is empty for system alerts. + nsCOMPtr scriptableAlertSource( + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableAlertSource, NS_ERROR_FAILURE); + scriptableAlertSource->SetData(source); + rv = argsArray->AppendElement(scriptableAlertSource); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr scriptableIconURL( + do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableIconURL, NS_ERROR_FAILURE); + if (aIconURI) { + nsAutoCString iconURL; + rv = aIconURI->GetSpec(iconURL); + NS_ENSURE_SUCCESS(rv, rv); + scriptableIconURL->SetData(iconURL); + } + rv = argsArray->AppendElement(scriptableIconURL); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr newWindow; + nsAutoCString features("chrome,dialog=yes,titlebar=no,popup=yes"); + if (inPrivateBrowsing) { + features.AppendLiteral(",private"); + } + rv = wwatch->OpenWindow(nullptr, ALERT_CHROME_URL, "_blank"_ns, features, + argsArray, getter_AddRefs(newWindow)); + NS_ENSURE_SUCCESS(rv, rv); + + mNamedWindows.InsertOrUpdate(name, newWindow); + alertObserver->SetAlertWindow(newWindow); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAlerts::SetManualDoNotDisturb(bool aDoNotDisturb) { + mDoNotDisturb = aDoNotDisturb; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAlerts::GetManualDoNotDisturb(bool* aRetVal) { + *aRetVal = mDoNotDisturb; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAlerts::GetSuppressForScreenSharing(bool* aRetVal) { + NS_ENSURE_ARG(aRetVal); + *aRetVal = mSuppressForScreenSharing; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAlerts::SetSuppressForScreenSharing(bool aSuppress) { + mSuppressForScreenSharing = aSuppress; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAlerts::CloseAlert(const nsAString& aAlertName, bool aContextClosed) { + mozIDOMWindowProxy* alert = mNamedWindows.GetWeak(aAlertName); + if (nsCOMPtr domWindow = + nsPIDOMWindowOuter::From(alert)) { + domWindow->DispatchCustomEvent(u"XULAlertClose"_ns, + ChromeOnlyDispatch::eYes); + } + return NS_OK; +} diff --git a/toolkit/components/alerts/nsXULAlerts.h b/toolkit/components/alerts/nsXULAlerts.h new file mode 100644 index 0000000000..62470ea7de --- /dev/null +++ b/toolkit/components/alerts/nsXULAlerts.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef nsXULAlerts_h__ +#define nsXULAlerts_h__ + +#include "nsCycleCollectionParticipant.h" +#include "nsHashKeys.h" +#include "nsInterfaceHashtable.h" + +#include "mozIDOMWindow.h" +#include "nsIAlertsService.h" +#include "nsIObserver.h" + +struct PendingAlert { + void Init(nsIAlertNotification* aAlert, nsIObserver* aListener) { + mAlert = aAlert; + mListener = aListener; + } + nsCOMPtr mAlert; + nsCOMPtr mListener; +}; + +class nsXULAlerts : public nsIAlertsService, + public nsIAlertsDoNotDisturb, + public nsIAlertsIconURI { + friend class nsXULAlertObserver; + + public: + NS_DECL_NSIALERTSICONURI + NS_DECL_NSIALERTSDONOTDISTURB + NS_DECL_NSIALERTSSERVICE + NS_DECL_ISUPPORTS + + nsXULAlerts() = default; + + static already_AddRefed GetInstance(); + + protected: + virtual ~nsXULAlerts() = default; + void PersistentAlertFinished(); + + nsInterfaceHashtable mNamedWindows; + uint32_t mPersistentAlertCount = 0; + nsTArray mPendingPersistentAlerts; + bool mDoNotDisturb = false; + + private: + bool mSuppressForScreenSharing = false; +}; + +/** + * This class wraps observers for alerts and watches + * for the "alertfinished" event in order to release + * the reference on the nsIDOMWindow of the XUL alert. + */ +class nsXULAlertObserver : public nsIObserver { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_CYCLE_COLLECTION_CLASS(nsXULAlertObserver) + + nsXULAlertObserver(nsXULAlerts* aXULAlerts, const nsAString& aAlertName, + nsIObserver* aObserver, bool aIsPersistent) + : mXULAlerts(aXULAlerts), + mAlertName(aAlertName), + mObserver(aObserver), + mIsPersistent(aIsPersistent) {} + + void SetAlertWindow(mozIDOMWindowProxy* aWindow) { mAlertWindow = aWindow; } + + protected: + virtual ~nsXULAlertObserver() = default; + + RefPtr mXULAlerts; + nsString mAlertName; + nsCOMPtr mAlertWindow; + nsCOMPtr mObserver; + bool mIsPersistent; +}; + +#endif /* nsXULAlerts_h__ */ diff --git a/toolkit/components/alerts/test/browser.ini b/toolkit/components/alerts/test/browser.ini new file mode 100644 index 0000000000..6c4b032b92 --- /dev/null +++ b/toolkit/components/alerts/test/browser.ini @@ -0,0 +1,5 @@ +[browser_bug1682866.js] +skip-if = !fission +support-files = + file_bug1682866.html + diff --git a/toolkit/components/alerts/test/browser_bug1682866.js b/toolkit/components/alerts/test/browser_bug1682866.js new file mode 100644 index 0000000000..7ac776d5b5 --- /dev/null +++ b/toolkit/components/alerts/test/browser_bug1682866.js @@ -0,0 +1,56 @@ +const baseURL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const alertURL = `${baseURL}file_bug1682866.html`; + +add_task(async function testAlertForceClosed() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + alertURL, + true /* waitForLoad */ + ); + + // Open a second which is in the same process as tab + let secondTabIsLoaded = BrowserTestUtils.waitForNewTab( + gBrowser, + alertURL, + true, + false + ); + + let isSuspendedAfterAlert = await SpecialPowers.spawn( + tab.linkedBrowser.browsingContext, + [alertURL], + url => { + content.open(url); + var utils = SpecialPowers.getDOMWindowUtils(content); + return utils.isInputTaskManagerSuspended; + } + ); + + await secondTabIsLoaded; + + let secondTab = gBrowser.tabs[2]; + + is( + isSuspendedAfterAlert, + Services.prefs.getBoolPref("dom.input_events.canSuspendInBCG.enabled"), + "InputTaskManager should be suspended because alert is opened" + ); + + let alertClosed = BrowserTestUtils.waitForEvent( + tab.linkedBrowser, + "DOMModalDialogClosed" + ); + + BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:newtab"); + + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await alertClosed; + + gBrowser.removeTab(tab); + gBrowser.removeTab(secondTab); +}); diff --git a/toolkit/components/alerts/test/file_bug1682866.html b/toolkit/components/alerts/test/file_bug1682866.html new file mode 100644 index 0000000000..328edcbac5 --- /dev/null +++ b/toolkit/components/alerts/test/file_bug1682866.html @@ -0,0 +1,9 @@ + + + + + diff --git a/toolkit/components/alerts/test/image.gif b/toolkit/components/alerts/test/image.gif new file mode 100644 index 0000000000..053b4d9261 Binary files /dev/null and b/toolkit/components/alerts/test/image.gif differ diff --git a/toolkit/components/alerts/test/image.png b/toolkit/components/alerts/test/image.png new file mode 100644 index 0000000000..430c3c5e65 Binary files /dev/null and b/toolkit/components/alerts/test/image.png differ diff --git a/toolkit/components/alerts/test/image_server.sjs b/toolkit/components/alerts/test/image_server.sjs new file mode 100644 index 0000000000..4caa21ce27 --- /dev/null +++ b/toolkit/components/alerts/test/image_server.sjs @@ -0,0 +1,92 @@ +const CC = Components.Constructor; + +let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const LocalFile = CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath"); + +const FileInputStream = CC( + "@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", + "init" +); + +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + let params = parseQueryString(request.queryString); + + response.setStatusLine(request.httpVersion, 200, "OK"); + + // Compare and increment a cookie for this request. This is used to test + // private browsing mode; the cookie should not be set if the image is + // loaded anonymously. + if (params.has("c")) { + let expectedValue = parseInt(params.get("c"), 10); + let actualValue = !request.hasHeader("Cookie") + ? 0 + : parseInt( + request.getHeader("Cookie").replace(/^counter=(\d+)/, "$1"), + 10 + ); + if (actualValue != expectedValue) { + response.setStatusLine(request.httpVersion, 400, "Wrong counter value"); + return; + } + response.setHeader("Set-Cookie", `counter=${expectedValue + 1}`, false); + } + + // Wait to send the image if a timeout is given. + let timeout = parseInt(params.get("t"), 10); + if (timeout > 0) { + response.processAsync(); + setTimeout(() => { + respond(params, request, response); + response.finish(); + }, timeout * 1000); + return; + } + + respond(params, request, response); +} + +function parseQueryString(queryString) { + return queryString.split("&").reduce((params, param) => { + let [key, value] = param.split("=", 2); + params.set(key, value); + return params; + }, new Map()); +} + +function respond(params, request, response) { + if (params.has("s")) { + let statusCode = parseInt(params.get("s"), 10); + response.setStatusLine(request.httpVersion, statusCode, "Custom status"); + return; + } + var filename = params.get("f"); + writeFile(filename, response); +} + +function writeFile(name, response) { + var file = new LocalFile(getState("__LOCATION__")).parent; + file.append(name); + + let mimeType = Cc["@mozilla.org/uriloader/external-helper-app-service;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + + let fileStream = new FileInputStream(file, 1, 0, false); + let binaryStream = new BinaryInputStream(fileStream); + + response.setHeader("Content-Type", mimeType, false); + response.bodyOutputStream.writeFrom(binaryStream, binaryStream.available()); + + binaryStream.close(); + fileStream.close(); +} diff --git a/toolkit/components/alerts/test/mochitest.ini b/toolkit/components/alerts/test/mochitest.ini new file mode 100644 index 0000000000..c5078807f9 --- /dev/null +++ b/toolkit/components/alerts/test/mochitest.ini @@ -0,0 +1,20 @@ +[DEFAULT] +skip-if = os == 'android' # We don't use XUL alerts on Android +support-files = + image.gif + image.png + image_server.sjs + +# Synchronous tests like test_alerts.html must come before +# asynchronous tests like test_alerts_noobserve.html! +[test_alerts.html] +[test_alerts_noobserve.html] +[test_alerts_requireinteraction.html] +skip-if = (verify && (os == 'linux')) +[test_image.html] +skip-if = verify +[test_invalid_utf16.html] +run-if = toolkit == 'windows' # Bug 1836526 +[test_multiple_alerts.html] +[test_principal.html] +skip-if = verify # Bug 1810860 diff --git a/toolkit/components/alerts/test/test_alerts.html b/toolkit/components/alerts/test/test_alerts.html new file mode 100644 index 0000000000..9c9371fc70 --- /dev/null +++ b/toolkit/components/alerts/test/test_alerts.html @@ -0,0 +1,88 @@ + + + + + Test for Alerts Service + + + + + +

+ +
Alerts service, with observer "synchronous" case. +
+
Did a notification appear anywhere? +
If so, the test will finish once the notification disappears. + +
+
+
+ + diff --git a/toolkit/components/alerts/test/test_alerts_noobserve.html b/toolkit/components/alerts/test/test_alerts_noobserve.html new file mode 100644 index 0000000000..6b759827cf --- /dev/null +++ b/toolkit/components/alerts/test/test_alerts_noobserve.html @@ -0,0 +1,91 @@ + + + + + Test for Alerts Service + + + + + +

+ +
Alerts service, without observer "asynchronous" case. +
+
A notification should soon appear somewhere. +
If there has been no crash when the notification (later) disappears, assume all is good. + +
+
+
+ + diff --git a/toolkit/components/alerts/test/test_alerts_requireinteraction.html b/toolkit/components/alerts/test/test_alerts_requireinteraction.html new file mode 100644 index 0000000000..c8dc02f621 --- /dev/null +++ b/toolkit/components/alerts/test/test_alerts_requireinteraction.html @@ -0,0 +1,162 @@ + + + + Test for alerts with requireInteraction + + + + +
+
+
+ + diff --git a/toolkit/components/alerts/test/test_image.html b/toolkit/components/alerts/test/test_image.html new file mode 100644 index 0000000000..3928529c13 --- /dev/null +++ b/toolkit/components/alerts/test/test_image.html @@ -0,0 +1,117 @@ + + + + Test for Bug 1233086 + + + + + +

+ +
+
+
+ + diff --git a/toolkit/components/alerts/test/test_invalid_utf16.html b/toolkit/components/alerts/test/test_invalid_utf16.html new file mode 100644 index 0000000000..a4f862238c --- /dev/null +++ b/toolkit/components/alerts/test/test_invalid_utf16.html @@ -0,0 +1,160 @@ + + + + + + Test for stability when providing invalid UTF-16 strings + + + + + + +

+ +

+
+
+
diff --git a/toolkit/components/alerts/test/test_multiple_alerts.html b/toolkit/components/alerts/test/test_multiple_alerts.html
new file mode 100644
index 0000000000..38989db99f
--- /dev/null
+++ b/toolkit/components/alerts/test/test_multiple_alerts.html
@@ -0,0 +1,97 @@
+
+
+
+  Test for multiple alerts
+  
+  
+
+
+
+
+
+ + diff --git a/toolkit/components/alerts/test/test_principal.html b/toolkit/components/alerts/test/test_principal.html new file mode 100644 index 0000000000..5464d92977 --- /dev/null +++ b/toolkit/components/alerts/test/test_principal.html @@ -0,0 +1,123 @@ + + + + Test for Bug 1202933 + + + + + +

+ +
+
+
+ + -- cgit v1.2.3