summaryrefslogtreecommitdiffstats
path: root/toolkit/components/alerts
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/alerts')
-rw-r--r--toolkit/components/alerts/AlertNotification.cpp373
-rw-r--r--toolkit/components/alerts/AlertNotification.h86
-rw-r--r--toolkit/components/alerts/AlertNotificationIPCSerializer.h128
-rw-r--r--toolkit/components/alerts/alert.css37
-rw-r--r--toolkit/components/alerts/alert.js394
-rw-r--r--toolkit/components/alerts/alert.xhtml85
-rw-r--r--toolkit/components/alerts/jar.mn8
-rw-r--r--toolkit/components/alerts/moz.build45
-rw-r--r--toolkit/components/alerts/nsAlertsService.cpp342
-rw-r--r--toolkit/components/alerts/nsAlertsService.h30
-rw-r--r--toolkit/components/alerts/nsAlertsUtils.cpp30
-rw-r--r--toolkit/components/alerts/nsAlertsUtils.h29
-rw-r--r--toolkit/components/alerts/nsIAlertsService.idl351
-rw-r--r--toolkit/components/alerts/nsIWindowsAlertsService.idl76
-rw-r--r--toolkit/components/alerts/nsXULAlerts.cpp403
-rw-r--r--toolkit/components/alerts/nsXULAlerts.h84
-rw-r--r--toolkit/components/alerts/test/browser.toml4
-rw-r--r--toolkit/components/alerts/test/browser_bug1682866.js56
-rw-r--r--toolkit/components/alerts/test/file_bug1682866.html9
-rw-r--r--toolkit/components/alerts/test/image.gifbin0 -> 60901 bytes
-rw-r--r--toolkit/components/alerts/test/image.pngbin0 -> 2531 bytes
-rw-r--r--toolkit/components/alerts/test/image_server.sjs92
-rw-r--r--toolkit/components/alerts/test/mochitest.toml28
-rw-r--r--toolkit/components/alerts/test/test_alerts.html88
-rw-r--r--toolkit/components/alerts/test/test_alerts_noobserve.html93
-rw-r--r--toolkit/components/alerts/test/test_alerts_requireinteraction.html164
-rw-r--r--toolkit/components/alerts/test/test_image.html117
-rw-r--r--toolkit/components/alerts/test/test_invalid_utf16.html160
-rw-r--r--toolkit/components/alerts/test/test_multiple_alerts.html99
-rw-r--r--toolkit/components/alerts/test/test_principal.html123
30 files changed, 3534 insertions, 0 deletions
diff --git a/toolkit/components/alerts/AlertNotification.cpp b/toolkit/components/alerts/AlertNotification.cpp
new file mode 100644
index 0000000000..3cc2ec4380
--- /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<uint32_t>& 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<RefPtr<nsIAlertAction>>& 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<uint32_t>& aVibrate) {
+ aVibrate = mVibrate.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetActions(nsTArray<RefPtr<nsIAlertAction>>& aActions) {
+ aActions = mActions.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetSource(nsAString& aSource) {
+ nsAlertsUtils::GetSourceHostPort(mPrincipal, aSource);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::GetOpaqueRelaunchData(nsAString& aOpaqueRelaunchData) {
+ aOpaqueRelaunchData = mOpaqueRelaunchData;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertNotification::SetOpaqueRelaunchData(const nsAString& aOpaqueRelaunchData) {
+ mOpaqueRelaunchData = aOpaqueRelaunchData;
+ 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<nsIURI> imageURI;
+ NS_NewURI(getter_AddRefs(imageURI), mImageURL);
+ if (!imageURI) {
+ return aListener->OnImageMissing(aUserData);
+ }
+
+ RefPtr<AlertImageRequest> 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<imgIContainer> 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<nsIAlertNotificationImageListener> 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<nsIAlertNotificationImageListener> 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..3ecf3c8eea
--- /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<nsIURI> mURI;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ bool mInPrivateBrowsing;
+ uint32_t mTimeout;
+ nsCOMPtr<nsIAlertNotificationImageListener> mListener;
+ nsCOMPtr<nsISupports> mUserData;
+ nsCOMPtr<nsITimer> mTimer;
+ nsCOMPtr<imgIRequest> mRequest;
+};
+
+class AlertNotification : 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<nsIPrincipal> mPrincipal;
+ bool mInPrivateBrowsing;
+ bool mSilent;
+ nsTArray<uint32_t> mVibrate;
+ nsTArray<RefPtr<nsIAlertAction>> mActions;
+ nsString mOpaqueRelaunchData;
+};
+
+} // 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<nsIAlertNotification*> {
+ 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<nsIPrincipal> principal;
+ nsTArray<uint32_t> 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<nsIAlertNotification>* 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<nsIPrincipal> principal;
+ nsTArray<uint32_t> 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<nsIAlertNotification> 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..8f7c706452
--- /dev/null
+++ b/toolkit/components/alerts/alert.xhtml
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE window>
+
+<window
+ id="alertNotification"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="alert:alert"
+ xmlns:xhtml="http://www.w3.org/1999/xhtml"
+ role="alert"
+ pack="start"
+ onload="onAlertLoad();"
+ onclick="onAlertClick();"
+ onbeforeunload="onAlertBeforeUnload();"
+>
+ <linkset>
+ <xhtml:link
+ rel="stylesheet"
+ href="chrome://global/content/alerts/alert.css"
+ />
+ <xhtml:link rel="stylesheet" href="chrome://global/skin/alert.css" />
+
+ <xhtml:link rel="localization" href="toolkit/global/alert.ftl" />
+ </linkset>
+
+ <script src="chrome://global/content/alerts/alert.js" />
+
+ <vbox id="alertBox">
+ <box id="alertTitleBox">
+ <image id="alertIcon" />
+ <label id="alertTitleLabel" class="alertTitle plain" crop="end" />
+ <vbox>
+ <toolbarbutton
+ class="close-icon"
+ data-l10n-id="alert-close"
+ onclick="event.stopPropagation();"
+ oncommand="onAlertClose();"
+ />
+ </vbox>
+ </box>
+ <box>
+ <hbox
+ id="alertImageBox"
+ class="alertImageBox"
+ align="center"
+ pack="center"
+ >
+ <image id="alertImage" />
+ </hbox>
+
+ <vbox id="alertTextBox" class="alertTextBox">
+ <label id="alertTextLabel" class="alertText plain" />
+ <spacer flex="1" />
+ <box id="alertFooter">
+ <label id="alertSourceLabel" class="alertSource plain" />
+ <button
+ type="menu"
+ id="alertSettings"
+ data-l10n-id="alert-settings-title"
+ >
+ <menupopup position="after_end">
+ <menuitem id="doNotDisturbMenuItem" oncommand="doNotDisturb();" />
+ <menuseparator />
+ <menuitem
+ id="disableForOriginMenuItem"
+ oncommand="disableForOrigin();"
+ />
+ <menuitem id="openSettingsMenuItem" oncommand="openSettings();" />
+ </menupopup>
+ </button>
+ </box>
+ </vbox>
+ </box>
+ </vbox>
+
+ <!-- This method is called inline because we want to make sure we establish the width
+ and height of the alert before we fire the onload handler. -->
+ <script>
+ /* eslint-disable no-undef */
+ prefillAlertInfo();
+ </script>
+</window>
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..b07ce72f0d
--- /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.toml"]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.toml"]
+
+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", "Alerts Service")
diff --git a/toolkit/components/alerts/nsAlertsService.cpp b/toolkit/components/alerts/nsAlertsService.cpp
new file mode 100644
index 0000000000..9a8fb7e7dc
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsService.cpp
@@ -0,0 +1,342 @@
+/* -*- 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 <shellapi.h>
+#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<nsIAlertsIconData> alertsIconData(do_QueryInterface(mBackend));
+ if (alertsIconData) {
+ rv = alertsIconData->ShowAlertWithIconData(mAlert, mAlertListener,
+ aIconSize, aIconData);
+ }
+ } else if (aIconURI) {
+ nsCOMPtr<nsIAlertsIconURI> 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<nsIAlertsService> mBackend;
+ nsCOMPtr<nsIAlertNotification> mAlert;
+ nsCOMPtr<nsIObserver> mAlertListener;
+};
+
+NS_IMPL_ISUPPORTS(IconCallback, nsIFaviconDataCallback)
+
+#endif // MOZ_PLACES
+
+nsresult ShowWithIconBackend(nsIAlertsService* aBackend,
+ nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener) {
+#ifdef MOZ_PLACES
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = aAlert->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv) || !uri) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Ensure the backend supports favicons.
+ nsCOMPtr<nsIAlertsIconData> alertsIconData(do_QueryInterface(aBackend));
+ nsCOMPtr<nsIAlertsIconURI> alertsIconURI;
+ if (!alertsIconData) {
+ alertsIconURI = do_QueryInterface(aBackend);
+ }
+ if (!alertsIconData && !alertsIconURI) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ nsCOMPtr<nsIFaviconService> favicons(
+ do_GetService("@mozilla.org/browser/favicon-service;1"));
+ NS_ENSURE_TRUE(favicons, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIFaviconDataCallback> 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<nsIAlertsDoNotDisturb> 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<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE);
+ // vibrate is unused
+ nsTArray<uint32_t> 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);
+}
+
+static bool ShouldFallBackToXUL() {
+#if defined(XP_WIN) || defined(XP_MACOSX)
+ // We know we always have system backend on Windows and macOS. Let's not
+ // permanently fall back to XUL just because of temporary failure.
+ return false;
+#else
+ // The system may not have the notification library, we should fall back to
+ // XUL.
+ return true;
+#endif
+}
+
+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) || !ShouldFallBackToXUL()) {
+ 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<nsIAlertsService> 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)) && ShouldFallBackToXUL()) {
+ // If the system backend failed to close the alert, fall back to XUL for
+ // future alerts.
+ mBackend = nullptr;
+ }
+ } else {
+ nsCOMPtr<nsIAlertsService> 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<nsIAlertsDoNotDisturb> 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<nsIAlertsDoNotDisturb> 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<nsIAlertsDoNotDisturb> 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<nsIAlertsDoNotDisturb> alertsDND(GetDNDBackend());
+ NS_ENSURE_TRUE(alertsDND, NS_ERROR_NOT_IMPLEMENTED);
+ return alertsDND->SetSuppressForScreenSharing(aSuppress);
+#endif
+}
+
+already_AddRefed<nsIAlertsDoNotDisturb> nsAlertsService::GetDNDBackend() {
+ nsCOMPtr<nsIAlertsService> backend;
+ // Try the system notification service.
+ if (ShouldUseSystemBackend()) {
+ backend = mBackend;
+ }
+ if (!backend) {
+ backend = nsXULAlerts::GetInstance();
+ }
+
+ nsCOMPtr<nsIAlertsDoNotDisturb> 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<nsIAlertsDoNotDisturb> GetDNDBackend();
+ nsCOMPtr<nsIAlertsService> 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..2e75205ef5
--- /dev/null
+++ b/toolkit/components/alerts/nsIAlertsService.idl
@@ -0,0 +1,351 @@
+/* -*- 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;
+
+ /**
+ * On Windows, chrome-privileged notifications -- i.e., those with a
+ * non-actionable principal -- can have action-specific `opaqueRelaunchData`.
+ * This data will be provided to the application at relaunch and can trigger
+ * specific actions.
+ *
+ * On non-Windows, this field is ignored.
+ */
+ readonly attribute AString opaqueRelaunchData;
+};
+
+[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<uint32_t> 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<uint32_t> vibrate;
+
+ /**
+ * Actions available for users to choose from for interacting with
+ * the notification.
+ *
+ * Implemented only for the system backend on Windows.
+ */
+ attribute Array<nsIAlertAction> 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;
+
+ /**
+ * On Windows, chrome-privileged notifications -- i.e., those with a
+ * non-actionable principal -- can have `opaqueRelaunchData`. This data will
+ * be provided to the application at relaunch and can trigger specific
+ * actions.
+ *
+ * On non-Windows, this field is ignored.
+ */
+ attribute AString opaqueRelaunchData;
+
+ /**
+ * 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,
+ in nsIObserver aAlertListener,
+ 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 |cached-favicon:|
+ * 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..7e7a122eeb
--- /dev/null
+++ b/toolkit/components/alerts/nsIWindowsAlertsService.idl
@@ -0,0 +1,76 @@
+/* -*- 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"
+
+[scriptable, uuid(a46c385b-a45c-4b48-ab7c-aaed1252bb83)]
+interface nsIWindowsAlertNotification : nsIAlertNotification
+{
+ /**
+ * Boolean to signal that notification button actions will be handled, and the
+ * resulting action should be communicated back on clicks through the
+ * `aData` field.
+ */
+ attribute boolean handleActions;
+
+ /**
+ * Image placements corresponding to placements in Windows Toast
+ * Notification XML.
+ */
+ cenum ImagePlacement : 8 {
+ eInline,
+ eHero,
+ eIcon,
+ };
+
+ /**
+ * Enum to specify image placement we want in the notification. n.b. in the
+ * future we could extend this to instead allow multiple images in differing
+ * placements in the same notification.
+ */
+ attribute nsIWindowsAlertNotification_ImagePlacement imagePlacement;
+};
+
+[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 WindowsAlertsService:
+ *
+ * `notificationData` {string} relaunch data, generally opaque to
+ * the Windows notification server DLL, for this relaunch.
+ *
+ * @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..0a8c9ea752
--- /dev/null
+++ b/toolkit/components/alerts/nsXULAlerts.cpp
@@ -0,0 +1,403 @@
+/* -*- 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 "nsISupportsPrimitives.h"
+#include "nsPIDOMWindow.h"
+#include "nsIWindowWatcher.h"
+
+using namespace mozilla;
+
+namespace {
+StaticRefPtr<nsXULAlerts> 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> 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<nsXULAlerts> 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<nsIAlertNotification> alert =
+ do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
+ NS_ENSURE_TRUE(alert, 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);
+ 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<nsIAlertNotification> 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<int32_t>(mPersistentAlertCount) >=
+ Preferences::GetInt("dom.webnotifications.requireinteraction.count",
+ 0)) {
+ PendingAlert* pa = mPendingPersistentAlerts.AppendElement();
+ pa->Init(aAlert, aAlertListener);
+ return NS_OK;
+ }
+ 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<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+
+ nsCOMPtr<nsIMutableArray> argsArray = nsArray::Create();
+
+ // create scriptable versions of our strings that we can store in our
+ // nsIMutableArray....
+ nsCOMPtr<nsISupportsString> 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<nsISupportsString> 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<nsISupportsString> 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<nsISupportsPRBool> 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<nsISupportsString> 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<nsISupportsPRInt32> 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<nsISupportsString> 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<nsISupportsString> 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<nsISupportsPRBool> 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<nsISupportsInterfacePointer> 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<nsISupportsInterfacePointer> ifptr =
+ do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<nsXULAlertObserver> alertObserver =
+ new nsXULAlertObserver(this, name, aAlertListener, requireInteraction);
+ nsCOMPtr<nsISupports> 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<nsISupportsString> 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<nsISupportsCString> 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<mozIDOMWindowProxy> newWindow;
+ nsAutoCString features("chrome,dialog=yes,alert=yes,titlebar=no");
+ if (inPrivateBrowsing) {
+ features.AppendLiteral(",private");
+ }
+ rv = wwatch->OpenWindow(
+ nullptr, "chrome://global/content/alerts/alert.xhtml"_ns, "_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<nsPIDOMWindowOuter> 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<nsIAlertNotification> mAlert;
+ nsCOMPtr<nsIObserver> 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<nsXULAlerts> GetInstance();
+
+ protected:
+ virtual ~nsXULAlerts() = default;
+ void PersistentAlertFinished();
+
+ nsInterfaceHashtable<nsStringHashKey, mozIDOMWindowProxy> mNamedWindows;
+ uint32_t mPersistentAlertCount = 0;
+ nsTArray<PendingAlert> 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<nsXULAlerts> mXULAlerts;
+ nsString mAlertName;
+ nsCOMPtr<mozIDOMWindowProxy> mAlertWindow;
+ nsCOMPtr<nsIObserver> mObserver;
+ bool mIsPersistent;
+};
+
+#endif /* nsXULAlerts_h__ */
diff --git a/toolkit/components/alerts/test/browser.toml b/toolkit/components/alerts/test/browser.toml
new file mode 100644
index 0000000000..61724f2c12
--- /dev/null
+++ b/toolkit/components/alerts/test/browser.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+["browser_bug1682866.js"]
+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..24b8b6f7b8
--- /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.startLoadingURIString(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 @@
+<html>
+ <body>
+ <script>
+ window.onload = function() {
+ alert("This is an alert");
+ }
+ </script>
+ </body>
+</html>
diff --git a/toolkit/components/alerts/test/image.gif b/toolkit/components/alerts/test/image.gif
new file mode 100644
index 0000000000..053b4d9261
--- /dev/null
+++ b/toolkit/components/alerts/test/image.gif
Binary files differ
diff --git a/toolkit/components/alerts/test/image.png b/toolkit/components/alerts/test/image.png
new file mode 100644
index 0000000000..430c3c5e65
--- /dev/null
+++ b/toolkit/components/alerts/test/image.png
Binary files 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.toml b/toolkit/components/alerts/test/mochitest.toml
new file mode 100644
index 0000000000..bb6da55f33
--- /dev/null
+++ b/toolkit/components/alerts/test/mochitest.toml
@@ -0,0 +1,28 @@
+[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 = ["os == 'win'"] # 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 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>Test for Alerts Service</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+
+<br>Alerts service, with observer "synchronous" case.
+<br>
+<br>Did a notification appear anywhere?
+<br>If so, the test will finish once the notification disappears.
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var notifier;
+var alertName = "fiorello";
+
+var observer = {
+ alertShow: false,
+ observe(aSubject, aTopic, aData) {
+ is(aData, "foobarcookie", "Checking whether the alert cookie was passed correctly");
+ if (aTopic == "alertclickcallback") {
+ todo(false, "Did someone click the notification while running mochitests? (Please don't.)");
+ } else if (aTopic == "alertshow") {
+ ok(!this.alertShow, "Alert should not be shown more than once");
+ this.alertShow = true;
+
+ // Notifications are native on OS X 10.8 and later, GNOME Shell with
+ // libnotify (bug 1236036) and Windows >= 8. These notifications persist
+ // in the Notification Center, and only fire the `alertfinished` event
+ // when closed. For platforms where native notifications may be used, we
+ // need to close explicitly to avoid a hang. This also works for XUL
+ // notifications when running this test on OS X < 10.8, or a window
+ // manager like Ubuntu Unity with incomplete libnotify support.
+ notifier.closeAlert(alertName);
+ } else {
+ is(aTopic, "alertfinished", "Checking the topic for a finished notification");
+ SimpleTest.finish();
+ }
+ },
+};
+
+function runTest() {
+ const Cc = SpecialPowers.Cc;
+ const Ci = SpecialPowers.Ci;
+
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application");
+ return;
+ }
+
+ ok(true, "Alerts service exists in this application");
+
+ try {
+ notifier = Cc["@mozilla.org/alerts-service;1"].
+ getService(Ci.nsIAlertsService);
+ ok(true, "Alerts service is available");
+ } catch (ex) {
+ todo(false,
+ "Alerts service is not available.", ex);
+ return;
+ }
+
+ try {
+ SimpleTest.waitForExplicitFinish();
+ notifier.showAlertNotification(null, "Notification test",
+ "Surprise! I'm here to test notifications!",
+ false, "foobarcookie", observer, alertName);
+ ok(true, "showAlertNotification() succeeded. Waiting for notification...");
+ } catch (ex) {
+ todo(false, "showAlertNotification() failed.", ex);
+ SimpleTest.finish();
+ }
+}
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
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..b6a7658e17
--- /dev/null
+++ b/toolkit/components/alerts/test/test_alerts_noobserve.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>Test for Alerts Service</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+
+<br>Alerts service, without observer "asynchronous" case.
+<br>
+<br>A notification should soon appear somewhere.
+<br>If there has been no crash when the notification (later) disappears, assume all is good.
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+
+const chromeScript = SpecialPowers.loadChromeScript(_ => {
+ /* eslint-env mozilla/chrome-script */
+ const {setTimeout} = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+ );
+
+ function anyXULAlertsVisible() {
+ var windows = Services.wm.getEnumerator("alert:alert");
+ return windows.hasMoreElements();
+ }
+
+ addMessageListener("anyXULAlertsVisible", anyXULAlertsVisible);
+
+ addMessageListener("waitForAlerts", function waitForAlerts() {
+ if (anyXULAlertsVisible()) {
+ setTimeout(waitForAlerts, 1000);
+ } else {
+ sendAsyncMessage("waitedForAlerts");
+ }
+ });
+});
+
+function waitForAlertsThenFinish() {
+ chromeScript.addMessageListener("waitedForAlerts", function waitedForAlerts() {
+ chromeScript.removeMessageListener("waitedForAlerts", waitedForAlerts);
+ ok(true, "Alert disappeared.");
+ SimpleTest.finish();
+ });
+ chromeScript.sendAsyncMessage("waitForAlerts");
+}
+
+async function runTest() {
+ var alertsVisible = await chromeScript.sendQuery("anyXULAlertsVisible");
+ ok(!alertsVisible, "Alerts should not be present at the start of the test.");
+
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application");
+ } else {
+ ok(true, "Alerts service exists in this application");
+
+ var notifier;
+ try {
+ notifier = Cc["@mozilla.org/alerts-service;1"].
+ getService(Ci.nsIAlertsService);
+ ok(true, "Alerts service is available");
+ } catch (ex) {
+ todo(false, "Alerts service is not available.", ex);
+ }
+
+ if (notifier) {
+ try {
+ notifier.showAlertNotification(null, "Notification test",
+ "This notification has no observer");
+ ok(true, "showAlertNotification() succeeded");
+ } catch (ex) {
+ todo(false, "showAlertNotification() failed.", ex);
+ }
+ }
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+runTest();
+setTimeout(waitForAlertsThenFinish, 1000);
+</script>
+</pre>
+</body>
+</html>
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..ca8fa1c9e3
--- /dev/null
+++ b/toolkit/components/alerts/test/test_alerts_requireinteraction.html
@@ -0,0 +1,164 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for alerts with requireInteraction</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+
+const chromeScript = SpecialPowers.loadChromeScript(_ => {
+ /* eslint-env mozilla/chrome-script */
+ const {clearTimeout, setTimeout} = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+ );
+
+ addMessageListener("waitForXULAlert", function() {
+ var timer = setTimeout(function() {
+ Services.ww.unregisterNotification(windowObserver);
+ sendAsyncMessage("waitForXULAlert", false);
+ }, 2000);
+
+ var windowObserver = function(win, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ win.addEventListener("load", function() {
+ let windowType = win.document.documentElement.getAttribute("windowtype");
+ if (windowType == "alert:alert") {
+ clearTimeout(timer);
+ Services.ww.unregisterNotification(windowObserver);
+
+ sendAsyncMessage("waitForXULAlert", true);
+ }
+ }, {once: true});
+ };
+
+ Services.ww.registerNotification(windowObserver);
+ });
+});
+
+var cookie = 0;
+function promiseCreateXULAlert(alertService, listener, name) {
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("waitForXULAlert", function waitedForAlert(result) {
+ chromeScript.removeMessageListener("waitForXULAlert", waitedForAlert);
+ resolve(result);
+ });
+
+ chromeScript.sendAsyncMessage("waitForXULAlert");
+ alertService.showAlertNotification(null, "title", "body",
+ true, cookie++, listener, name, null, null, null,
+ null, false, true);
+ });
+}
+
+add_task(async function test_require_interaction() {
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application.");
+ return;
+ }
+
+ ok(true, "Alerts service exists in this application.");
+
+ var alertService;
+ try {
+ alertService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+ ok(true, "Alerts service is available.");
+ } catch (ex) {
+ todo(false, "Alerts service is not available.");
+ return;
+ }
+
+ await SpecialPowers.pushPrefEnv({"set": [
+ [ "dom.webnotifications.requireinteraction.enabled", true ],
+ [ "dom.webnotifications.requireinteraction.count", 2 ],
+ ]});
+
+ var expectedSequence = [
+ "first show",
+ "second show",
+ "second finished",
+ "second replacement show",
+ "third finished",
+ "first finished",
+ "third replacement show",
+ "second replacement finished",
+ "third replacement finished",
+ ];
+
+ var actualSequence = [];
+
+ function createAlertListener(name, showCallback, finishCallback) {
+ return (subject, topic, data) => {
+ if (topic == "alertshow") {
+ actualSequence.push(name + " show");
+ if (showCallback) {
+ showCallback();
+ }
+ } else if (topic == "alertfinished") {
+ actualSequence.push(name + " finished");
+ if (finishCallback) {
+ finishCallback();
+ }
+ }
+ };
+ }
+
+ var xulAlertCreated = await promiseCreateXULAlert(alertService,
+ createAlertListener("first"), "first");
+ if (!xulAlertCreated) {
+ ok(true, "Platform does not use XUL alerts.");
+ alertService.closeAlert("first");
+ return;
+ }
+
+ xulAlertCreated = await promiseCreateXULAlert(alertService,
+ createAlertListener("second"), "second");
+ ok(xulAlertCreated, "Create XUL alert");
+
+ // Replace second alert
+ xulAlertCreated = await promiseCreateXULAlert(alertService,
+ createAlertListener("second replacement"), "second");
+ ok(xulAlertCreated, "Create XUL alert");
+
+ var testFinishResolve;
+ var testFinishPromise = new Promise((resolve) => { testFinishResolve = resolve; });
+
+ xulAlertCreated = await promiseCreateXULAlert(alertService,
+ createAlertListener("third"), "third");
+ ok(!xulAlertCreated, "XUL alert should not be visible");
+
+ // Replace the not-yet-visible third alert.
+ xulAlertCreated = await promiseCreateXULAlert(alertService,
+ createAlertListener("third replacement",
+ function showCallback() {
+ alertService.closeAlert("second");
+ alertService.closeAlert("third");
+ },
+ function finishCallback() {
+ // Check actual sequence of alert events compared to expected sequence.
+ for (var i = 0; i < actualSequence.length; i++) {
+ is(actualSequence[i], expectedSequence[i],
+ "Alert callback at index " + i + " should be in expected order.");
+ }
+
+ testFinishResolve();
+ }), "third");
+
+ ok(!xulAlertCreated, "XUL alert should not be visible");
+
+ alertService.closeAlert("first");
+
+ await testFinishPromise;
+});
+
+</script>
+</pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 1233086</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+const Services = SpecialPowers.Services;
+
+const imageServerURL = "http://mochi.test:8888/tests/toolkit/components/alerts/test/image_server.sjs";
+
+function makeAlert(...params) {
+ var alert = Cc["@mozilla.org/alert-notification;1"]
+ .createInstance(Ci.nsIAlertNotification);
+ alert.init(...params);
+ return alert;
+}
+
+function promiseImage(alert, timeout = 0, userData = null) {
+ return new Promise(resolve => {
+ var isDone = false;
+ function done(value) {
+ ok(!isDone, "Should call the image listener once");
+ isDone = true;
+ resolve(value);
+ }
+ alert.loadImage(timeout, SpecialPowers.wrapCallbackObject({
+ onImageReady(aUserData, aRequest) {
+ done([true, aRequest, aUserData]);
+ },
+ onImageMissing(aUserData) {
+ done([false, aUserData]);
+ },
+ }), SpecialPowers.wrap(userData));
+ });
+}
+
+add_task(async function testContext() {
+ var inUserData = Cc["@mozilla.org/supports-PRInt64;1"]
+ .createInstance(Ci.nsISupportsPRInt64);
+ inUserData.data = 123;
+
+ var alert = makeAlert(null, imageServerURL + "?f=image.png");
+ var [ready, , userData] = await promiseImage(alert, 0, inUserData);
+ ok(ready, "Should load requested image");
+ is(userData.QueryInterface(Ci.nsISupportsPRInt64).data, 123,
+ "Should pass user data for loaded image");
+
+ alert = makeAlert(null, imageServerURL + "?s=404");
+ [ready, userData] = await promiseImage(alert, 0, inUserData);
+ ok(!ready, "Should not load missing image");
+ is(userData.QueryInterface(Ci.nsISupportsPRInt64).data, 123,
+ "Should pass user data for missing image");
+});
+
+add_task(async function testTimeout() {
+ var alert = makeAlert(null, imageServerURL + "?f=image.png&t=3");
+ var [ready] = await promiseImage(alert, 1000);
+ ok(!ready, "Should cancel request if timeout fires");
+
+ [ready] = await promiseImage(alert, 45000);
+ ok(ready, "Should load image if request finishes before timeout");
+});
+
+add_task(async function testAnimatedGIF() {
+ var alert = makeAlert(null, imageServerURL + "?f=image.gif");
+ var [ready, request] = await promiseImage(alert);
+ ok(ready, "Should load first animated GIF frame");
+ is(request.mimeType, "image/gif", "Should report correct GIF MIME type");
+ is(request.image.width, 256, "GIF width should be 256px");
+ is(request.image.height, 256, "GIF height should be 256px");
+});
+
+add_task(async function testCancel() {
+ var alert = makeAlert(null, imageServerURL + "?f=image.gif&t=180");
+ await new Promise((resolve, reject) => {
+ var request = alert.loadImage(0, SpecialPowers.wrapCallbackObject({
+ onImageReady() {
+ reject(new Error("Should not load cancelled request"));
+ },
+ onImageMissing() {
+ resolve();
+ },
+ }), null);
+ request.cancel(SpecialPowers.Cr.NS_BINDING_ABORTED);
+ });
+});
+
+add_task(async function testMixedContent() {
+ // Loading principal is HTTPS; image URL is HTTP.
+ var origin = "https://mochi.test:8888";
+ var principal = Services.scriptSecurityManager
+ .createContentPrincipalFromOrigin(origin);
+
+ var alert = makeAlert(null, imageServerURL + "?f=image.png",
+ null, null, false, null, null, null,
+ null, principal);
+ var [ready, request] = await promiseImage(alert);
+ ok(ready, "Should load cross-protocol image");
+ is(request.mimeType, "image/png", "Should report correct MIME type");
+ is(request.image.width, 32, "Width should be 32px");
+ is(request.image.height, 32, "Height should be 32px");
+});
+
+</script>
+</pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <meta charset="utf-8">
+ <title>Test for stability when providing invalid UTF-16 strings</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+ <script>
+ const Cc = SpecialPowers.Cc;
+ const Ci = SpecialPowers.Ci;
+ let notifier = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);;
+ let notification = Cc["@mozilla.org/alert-notification;1"]
+
+ function buildObserver(alertName) {
+ let resolve;
+ let reject;
+ let promise = new Promise((res, rej) => {resolve = res; reject = rej});
+
+ let success = false;
+ function observe(aSubject, aTopic, aData) {
+ if (aTopic == "alertshow") {
+ success = true;
+ notifier.closeAlert(alertName);
+ } else if (aTopic == "alertfinished") {
+ ok(true, "alertfinished");
+ if (success) {
+ resolve();
+ } else {
+ reject();
+ }
+ }
+ }
+
+ return { promise, observe };
+ };
+
+ function buildAlert(options) {
+ let alert = notification.createInstance(
+ Ci.nsIAlertNotification
+ );
+ alert.init(
+ options.name,
+ options.imageURL,
+ options.title,
+ options.text,
+ options.textClickable,
+ options.cookie,
+ options.dir,
+ options.lang,
+ options.data,
+ options.principal,
+ options.inPrivateBrowsing,
+ options.requireInteraction,
+ options.silent,
+ options.vibrate || []
+ );
+ if (options.actions) {
+ alert.actions = options.actions;
+ }
+ return alert;
+ }
+
+ async function runTest(options) {
+ let alert = buildAlert(options)
+ const { promise, observe } = buildObserver(options.name);
+ notifier.showAlert(alert, observe);
+ await promise;
+ }
+
+ let invalidUtf16 = String.fromCharCode(0xdfff);
+
+ add_task(async function test_invalid_utf16_name() {
+ let name = invalidUtf16;
+ let alert = buildAlert({name});
+
+ // Extract the alert name to ensure it was not forced to be valid UTF-16.
+ ok(name == alert.name, "Notification name was not forced to be valid UTF-16");
+
+ const { promise, observe } = buildObserver(name);
+ notifier.showAlert(alert, observe);
+ await promise;
+
+ ok(true, "Notification shown with invalid UTF-16 name");
+ });
+
+ add_task(async function test_invalid_utf16_title() {
+ let name = "invalid title";
+ let title = invalidUtf16;
+ await runTest({name, title});
+
+ ok(true, "Notification shown with invalid UTF-16 title");
+ });
+
+ add_task(async function test_invalid_utf16_body() {
+ let name = "invalid body";
+ let text = invalidUtf16;
+ await runTest({name, text});
+
+ ok(true, "Notification shown with invalid UTF-16 body");
+ });
+
+ add_task(async function test_invalid_utf16_image_url() {
+ let name = "invalid image URL";
+ let imageURL = invalidUtf16;
+ await runTest({name, imageURL});
+
+ ok(true, "Notification shown with invalid UTF-16 image url");
+ });
+
+ add_task(async function test_invalid_utf16_data() {
+ let name = "invalid data";
+ let data = invalidUtf16;
+ await runTest({name, data});
+
+ ok(true, "Notification shown with invalid UTF-16 data");
+ });
+
+ // At time of writing, actions are a Windows only, privileged feature.
+ add_task(async function test_invalid_utf16_action_body() {
+ let name = "invalid action body";
+ let actions = [
+ { action: invalidUtf16 },
+ ];
+ await runTest({name, actions});
+
+ ok(true, "Notification shown with invalid UTF-16 action body");
+ });
+
+ // At time of writing, actions are a Windows only, privileged feature.
+ add_task(async function test_invalid_utf16_action_title() {
+ let name = "invalid action title";
+ let actions = [
+ {title:invalidUtf16},
+ ];
+ await runTest({name, actions});
+
+ ok(true, "Notification shown with invalid UTF-16 action title");
+ });
+
+ // At time of writing, actions are a Windows only, privileged feature.
+ add_task(async function test_invalid_utf16_action_image_url() {
+ let name = "invalid action image URL";
+ let actions = [
+ { iconURL: invalidUtf16 },
+ ];
+ await runTest({name, actions});
+
+ ok(true, "Notification shown with invalid UTF-16 action image URL");
+ });
+ </script>
+</head>
+
+<body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+
+</html>
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..95843b2e89
--- /dev/null
+++ b/toolkit/components/alerts/test/test_multiple_alerts.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for multiple alerts</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+
+const chromeScript = SpecialPowers.loadChromeScript(_ => {
+ /* eslint-env mozilla/chrome-script */
+ const {clearTimeout, setTimeout} = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+ );
+
+ const alertService = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService);
+
+ addMessageListener("waitForPosition", function() {
+ var timer = setTimeout(function() {
+ Services.ww.unregisterNotification(windowObserver);
+ sendAsyncMessage("waitedForPosition", null);
+ }, 2000);
+
+ var windowObserver = function(win, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ // Alerts are implemented using XUL.
+ clearTimeout(timer);
+
+ Services.ww.unregisterNotification(windowObserver);
+
+ win.addEventListener("pageshow", function() {
+ var x = win.screenX;
+ var y = win.screenY;
+
+ win.addEventListener("pagehide", function() {
+ sendAsyncMessage("waitedForPosition", { x, y });
+ }, {once: true});
+
+ alertService.closeAlert();
+ }, {once: true});
+ };
+
+ Services.ww.registerNotification(windowObserver);
+ });
+});
+
+function promiseAlertPosition(alertService) {
+ return new Promise(resolve => {
+ chromeScript.addMessageListener("waitedForPosition", function waitedForPosition(result) {
+ chromeScript.removeMessageListener("waitedForPosition", waitedForPosition);
+ resolve(result);
+ });
+ chromeScript.sendAsyncMessage("waitForPosition");
+
+ alertService.showAlertNotification(null, "title", "body");
+ ok(true, "Alert shown.");
+ });
+}
+
+add_task(async function test_multiple_alerts() {
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application.");
+ return;
+ }
+
+ ok(true, "Alerts service exists in this application.");
+
+ var alertService;
+ try {
+ alertService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
+ ok(true, "Alerts service is available.");
+ } catch (ex) {
+ todo(false, "Alerts service is not available.");
+ return;
+ }
+
+ var firstAlertPosition = await promiseAlertPosition(alertService);
+ if (!firstAlertPosition) {
+ ok(true, "Platform does not use XUL alerts.");
+ return;
+ }
+
+ var secondAlertPosition = await promiseAlertPosition(alertService);
+ is(secondAlertPosition.x, firstAlertPosition.x, "Second alert should be opened in the same position.");
+ is(secondAlertPosition.y, firstAlertPosition.y, "Second alert should be opened in the same position.");
+});
+
+</script>
+</pre>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 1202933</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+
+<body>
+<p id="display"></p>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const Cc = SpecialPowers.Cc;
+const Ci = SpecialPowers.Ci;
+const Services = SpecialPowers.Services;
+
+const notifier = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService);
+
+const chromeScript = SpecialPowers.loadChromeScript(_ => {
+ /* eslint-env mozilla/chrome-script */
+ addMessageListener("anyXULAlertsVisible", function() {
+ var windows = Services.wm.getEnumerator("alert:alert");
+ return windows.hasMoreElements();
+ });
+
+ addMessageListener("getAlertSource", function() {
+ var alertWindows = Services.wm.getEnumerator("alert:alert");
+ if (!alertWindows) {
+ return null;
+ }
+ var alertWindow = alertWindows.getNext();
+ return alertWindow.document.getElementById("alertSourceLabel").getAttribute("value");
+ });
+});
+
+function notify(alertName, principal) {
+ return new Promise((resolve, reject) => {
+ var source;
+ async function observe(subject, topic, data) {
+ if (topic == "alertclickcallback") {
+ reject(new Error("Alerts should not be clicked during test"));
+ } else if (topic == "alertshow") {
+ source = await chromeScript.sendQuery("getAlertSource");
+ notifier.closeAlert(alertName);
+ } else {
+ is(topic, "alertfinished", "Should hide alert");
+ resolve(source);
+ }
+ }
+ notifier.showAlertNotification(null, "Notification test",
+ "Surprise! I'm here to test notifications!",
+ false, alertName, observe, alertName,
+ null, null, null, principal);
+ if (SpecialPowers.Services.appinfo.OS == "Darwin") {
+ notifier.closeAlert(alertName);
+ }
+ });
+}
+
+async function testNoPrincipal() {
+ var source = await notify("noPrincipal", null);
+ ok(!source, "Should omit source without principal");
+}
+
+async function testSystemPrincipal() {
+ var principal = Services.scriptSecurityManager.getSystemPrincipal();
+ var source = await notify("systemPrincipal", principal);
+ ok(!source, "Should omit source for system principal");
+}
+
+async function testNullPrincipal() {
+ var principal = Services.scriptSecurityManager.createNullPrincipal({});
+ var source = await notify("nullPrincipal", principal);
+ ok(!source, "Should omit source for null principal");
+}
+
+async function testNodePrincipal() {
+ var principal = SpecialPowers.wrap(document).nodePrincipal;
+ var source = await notify("nodePrincipal", principal);
+
+ var stringBundle = Services.strings.createBundle(
+ "chrome://alerts/locale/alert.properties"
+ );
+ var localizedSource = stringBundle.formatStringFromName(
+ "source.label", [principal.hostPort]);
+ is(source, localizedSource, "Should include source for node principal");
+}
+
+function runTest() {
+ if (!("@mozilla.org/alerts-service;1" in Cc)) {
+ todo(false, "Alerts service does not exist in this application");
+ return;
+ }
+
+ ok(true, "Alerts service exists in this application");
+
+ add_setup(async () => {
+ // This test verifies behavior specific to XUL alerts.
+ await SpecialPowers.pushPrefEnv({
+ set: [["alerts.useSystemBackend", false]],
+ });
+ });
+
+ // sendSyncMessage returns an array of arrays. See the comments in
+ // test_alerts_noobserve.html and test_SpecialPowersLoadChromeScript.html.
+ add_task(async () => {
+ var alertsVisible = await chromeScript.sendQuery("anyXULAlertsVisible");
+ ok(!alertsVisible, "Alerts should not be present at the start of the test.");
+ });
+
+ add_task(testNoPrincipal);
+ add_task(testSystemPrincipal);
+ add_task(testNullPrincipal);
+ add_task(testNodePrincipal);
+}
+
+runTest();
+</script>
+</pre>
+</body>
+</html>