summaryrefslogtreecommitdiffstats
path: root/toolkit/components/alerts
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/alerts')
-rw-r--r--toolkit/components/alerts/AlertNotification.cpp361
-rw-r--r--toolkit/components/alerts/AlertNotification.h81
-rw-r--r--toolkit/components/alerts/AlertNotificationIPCSerializer.h122
-rw-r--r--toolkit/components/alerts/jar.mn8
-rw-r--r--toolkit/components/alerts/moz.build38
-rw-r--r--toolkit/components/alerts/nsAlertsService.cpp320
-rw-r--r--toolkit/components/alerts/nsAlertsService.h48
-rw-r--r--toolkit/components/alerts/nsAlertsUtils.cpp43
-rw-r--r--toolkit/components/alerts/nsAlertsUtils.h32
-rw-r--r--toolkit/components/alerts/nsIAlertsService.idl259
-rw-r--r--toolkit/components/alerts/nsXULAlerts.cpp398
-rw-r--r--toolkit/components/alerts/nsXULAlerts.h84
-rw-r--r--toolkit/components/alerts/resources/content/alert.css34
-rw-r--r--toolkit/components/alerts/resources/content/alert.js332
-rw-r--r--toolkit/components/alerts/resources/content/alert.xul67
-rw-r--r--toolkit/components/alerts/test/.eslintrc.js7
-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.sjs82
-rw-r--r--toolkit/components/alerts/test/mochitest.ini16
-rw-r--r--toolkit/components/alerts/test/test_alerts.html89
-rw-r--r--toolkit/components/alerts/test/test_alerts_noobserve.html96
-rw-r--r--toolkit/components/alerts/test/test_alerts_requireinteraction.html168
-rw-r--r--toolkit/components/alerts/test/test_image.html118
-rw-r--r--toolkit/components/alerts/test/test_multiple_alerts.html103
-rw-r--r--toolkit/components/alerts/test/test_principal.html122
26 files changed, 3028 insertions, 0 deletions
diff --git a/toolkit/components/alerts/AlertNotification.cpp b/toolkit/components/alerts/AlertNotification.cpp
new file mode 100644
index 000000000..b828f1100
--- /dev/null
+++ b/toolkit/components/alerts/AlertNotification.cpp
@@ -0,0 +1,361 @@
+/* This Source Code Form is subject to the terms of the Mozilla Pub
+ * License, v. 2.0. If a copy of the MPL was not distributed with t
+ * 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()
+{}
+
+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)
+{
+ mName = aName;
+ mImageURL = aImageURL;
+ mTitle = aTitle;
+ mText = aText;
+ mTextClickable = aTextClickable;
+ mCookie = aCookie;
+ mDir = aDir;
+ mLang = aLang;
+ mData = aData;
+ mPrincipal = aPrincipal;
+ mInPrivateBrowsing = aInPrivateBrowsing;
+ mRequireInteraction = aRequireInteraction;
+ 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;
+ }
+ return mPrincipal->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::GetSource(nsAString& aSource)
+{
+ nsAlertsUtils::GetSourceHostPort(mPrincipal, aSource);
+ 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);
+ nsresult rv = request->Start();
+ request.forget(aRequest);
+ return rv;
+}
+
+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_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);
+ }
+}
+
+NS_IMETHODIMP
+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)) {
+ return NotifyMissing();
+ }
+
+ // 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)) {
+ return NotifyMissing();
+ }
+
+ // 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_NONE);
+ }
+ return NS_OK;
+ }
+
+ if (aType == imgINotificationObserver::FRAME_COMPLETE) {
+ return NotifyComplete();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AlertImageRequest::Notify(nsITimer* aTimer)
+{
+ MOZ_ASSERT(aTimer == mTimer);
+ return NotifyMissing();
+}
+
+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) {
+ mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+ if (NS_WARN_IF(!mTimer)) {
+ return NotifyMissing();
+ }
+ rv = mTimer->InitWithCallback(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 = mInPrivateBrowsing ? nsIRequest::LOAD_ANONYMOUS :
+ nsIRequest::LOAD_NORMAL;
+
+ rv = il->LoadImageXPCOM(mURI, nullptr, nullptr,
+ NS_LITERAL_STRING("default"), 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 = mListener.forget()) {
+ nsresult rv = listener->OnImageMissing(mUserData);
+ NS_RELEASE_THIS();
+ return rv;
+ }
+ return NS_OK;
+}
+
+nsresult
+AlertImageRequest::NotifyComplete()
+{
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ if (nsCOMPtr<nsIAlertNotificationImageListener> listener = mListener.forget()) {
+ nsresult rv = listener->OnImageReady(mUserData, mRequest);
+ NS_RELEASE_THIS();
+ return rv;
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/alerts/AlertNotification.h b/toolkit/components/alerts/AlertNotification.h
new file mode 100644
index 000000000..c0bcc0ba9
--- /dev/null
+++ b/toolkit/components/alerts/AlertNotification.h
@@ -0,0 +1,81 @@
+/* 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 "nsIPrincipal.h"
+#include "nsString.h"
+#include "nsITimer.h"
+
+namespace mozilla {
+
+class AlertImageRequest final : public imgINotificationObserver,
+ public nsICancelable,
+ public nsITimerCallback
+{
+public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(AlertImageRequest,
+ imgINotificationObserver)
+ NS_DECL_IMGINOTIFICATIONOBSERVER
+ NS_DECL_NSICANCELABLE
+ NS_DECL_NSITIMERCALLBACK
+
+ AlertImageRequest(nsIURI* aURI, nsIPrincipal* aPrincipal,
+ bool aInPrivateBrowsing, uint32_t aTimeout,
+ nsIAlertNotificationImageListener* aListener,
+ nsISupports* aUserData);
+
+ nsresult Start();
+
+private:
+ virtual ~AlertImageRequest();
+
+ nsresult NotifyMissing();
+ nsresult 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 final : public nsIAlertNotification
+{
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIALERTNOTIFICATION
+ AlertNotification();
+
+protected:
+ virtual ~AlertNotification();
+
+private:
+ nsString mName;
+ nsString mImageURL;
+ nsString mTitle;
+ nsString mText;
+ bool mTextClickable;
+ nsString mCookie;
+ nsString mDir;
+ nsString mLang;
+ bool mRequireInteraction;
+ nsString mData;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ bool mInPrivateBrowsing;
+};
+
+} // 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 000000000..9544f9633
--- /dev/null
+++ b/toolkit/components/alerts/AlertNotificationIPCSerializer.h
@@ -0,0 +1,122 @@
+/* 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"
+
+typedef nsIAlertNotification* AlertNotificationType;
+
+namespace IPC {
+
+template <>
+struct ParamTraits<AlertNotificationType>
+{
+ typedef AlertNotificationType paramType;
+
+ static void Write(Message* aMsg, const paramType& aParam)
+ {
+ bool isNull = !aParam;
+ if (isNull) {
+ WriteParam(aMsg, isNull);
+ return;
+ }
+
+ nsString name, imageURL, title, text, cookie, dir, lang, data;
+ bool textClickable, inPrivateBrowsing, requireInteraction;
+ nsCOMPtr<nsIPrincipal> principal;
+
+ 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)))) {
+
+ // Write a `null` object if any getter returns an error. Otherwise, the
+ // receiver will try to deserialize an incomplete object and crash.
+ WriteParam(aMsg, /* isNull */ true);
+ return;
+ }
+
+ WriteParam(aMsg, isNull);
+ WriteParam(aMsg, name);
+ WriteParam(aMsg, imageURL);
+ WriteParam(aMsg, title);
+ WriteParam(aMsg, text);
+ WriteParam(aMsg, textClickable);
+ WriteParam(aMsg, cookie);
+ WriteParam(aMsg, dir);
+ WriteParam(aMsg, lang);
+ WriteParam(aMsg, data);
+ WriteParam(aMsg, IPC::Principal(principal));
+ WriteParam(aMsg, inPrivateBrowsing);
+ WriteParam(aMsg, requireInteraction);
+ }
+
+ static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
+ {
+ bool isNull;
+ NS_ENSURE_TRUE(ReadParam(aMsg, aIter, &isNull), false);
+ if (isNull) {
+ *aResult = nullptr;
+ return true;
+ }
+
+ nsString name, imageURL, title, text, cookie, dir, lang, data;
+ bool textClickable, inPrivateBrowsing, requireInteraction;
+ IPC::Principal principal;
+
+ if (!ReadParam(aMsg, aIter, &name) ||
+ !ReadParam(aMsg, aIter, &imageURL) ||
+ !ReadParam(aMsg, aIter, &title) ||
+ !ReadParam(aMsg, aIter, &text) ||
+ !ReadParam(aMsg, aIter, &textClickable) ||
+ !ReadParam(aMsg, aIter, &cookie) ||
+ !ReadParam(aMsg, aIter, &dir) ||
+ !ReadParam(aMsg, aIter, &lang) ||
+ !ReadParam(aMsg, aIter, &data) ||
+ !ReadParam(aMsg, aIter, &principal) ||
+ !ReadParam(aMsg, aIter, &inPrivateBrowsing) ||
+ !ReadParam(aMsg, aIter, &requireInteraction)) {
+
+ 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);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ *aResult = nullptr;
+ return true;
+ }
+ alert.forget(aResult);
+ return true;
+ }
+};
+
+} // namespace IPC
+
+#endif /* mozilla_AlertNotificationIPCSerializer_h__ */
diff --git a/toolkit/components/alerts/jar.mn b/toolkit/components/alerts/jar.mn
new file mode 100644
index 000000000..c45939078
--- /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 (resources/content/alert.css)
+ content/global/alerts/alert.xul (resources/content/alert.xul)
+ content/global/alerts/alert.js (resources/content/alert.js)
diff --git a/toolkit/components/alerts/moz.build b/toolkit/components/alerts/moz.build
new file mode 100644
index 000000000..cdbf92511
--- /dev/null
+++ b/toolkit/components/alerts/moz.build
@@ -0,0 +1,38 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ['test/mochitest.ini']
+
+XPIDL_SOURCES += [
+ 'nsIAlertsService.idl',
+]
+
+XPIDL_MODULE = 'alerts'
+
+EXPORTS += [
+ 'nsAlertsUtils.h',
+]
+
+EXPORTS.mozilla += [
+ 'AlertNotification.h',
+ 'AlertNotificationIPCSerializer.h',
+]
+
+UNIFIED_SOURCES += [
+ 'AlertNotification.cpp',
+ 'nsAlertsService.cpp',
+ 'nsAlertsUtils.cpp',
+ 'nsXULAlerts.cpp',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Notifications and Alerts')
diff --git a/toolkit/components/alerts/nsAlertsService.cpp b/toolkit/components/alerts/nsAlertsService.cpp
new file mode 100644
index 000000000..35418dd17
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsService.cpp
@@ -0,0 +1,320 @@
+/* -*- 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/Telemetry.h"
+#include "nsXULAppAPI.h"
+
+#include "nsAlertsService.h"
+
+#include "nsXPCOM.h"
+#include "nsIServiceManager.h"
+#include "nsIDOMWindow.h"
+#include "nsPromiseFlatString.h"
+#include "nsToolkitCompsCID.h"
+
+#ifdef MOZ_PLACES
+#include "mozIAsyncFavicons.h"
+#include "nsIFaviconService.h"
+#endif // MOZ_PLACES
+
+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) 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() {}
+
+ 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<mozIAsyncFavicons> 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);
+ }
+ return favicons->GetFaviconURLForPage(uri, callback);
+#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()
+{}
+
+bool nsAlertsService::ShouldShowAlert()
+{
+ bool result = true;
+
+#ifdef XP_WIN
+ HMODULE shellDLL = ::LoadLibraryW(L"shell32.dll");
+ if (!shellDLL)
+ return result;
+
+ SHQueryUserNotificationStatePtr pSHQueryUserNotificationState =
+ (SHQueryUserNotificationStatePtr) ::GetProcAddress(shellDLL, "SHQueryUserNotificationState");
+
+ if (pSHQueryUserNotificationState) {
+ MOZ_QUERY_USER_NOTIFICATION_STATE qstate;
+ if (SUCCEEDED(pSHQueryUserNotificationState(&qstate))) {
+ if (qstate != QUNS_ACCEPTS_NOTIFICATIONS) {
+ result = false;
+ }
+ }
+ }
+
+ ::FreeLibrary(shellDLL);
+#endif
+
+ return result;
+}
+
+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);
+ nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle,
+ aAlertText, aAlertTextClickable,
+ aAlertCookie, aBidi, aLang, aData,
+ aPrincipal, aInPrivateBrowsing,
+ aRequireInteraction);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ShowAlert(alert, aAlertListener);
+}
+
+
+NS_IMETHODIMP nsAlertsService::ShowAlert(nsIAlertNotification * aAlert,
+ nsIObserver * aAlertListener)
+{
+ return ShowPersistentNotification(EmptyString(), aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP nsAlertsService::ShowPersistentNotification(const nsAString & aPersistentData,
+ nsIAlertNotification * aAlert,
+ nsIObserver * aAlertListener)
+{
+ NS_ENSURE_ARG(aAlert);
+
+ nsAutoString cookie;
+ nsresult rv = aAlert->GetCookie(cookie);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (XRE_IsContentProcess()) {
+ ContentChild* cpc = ContentChild::GetSingleton();
+
+ if (aAlertListener)
+ cpc->AddRemoteAlertObserver(cookie, aAlertListener);
+
+ cpc->SendShowAlert(aAlert);
+ return NS_OK;
+ }
+
+ // Check if there is an optional service that handles system-level notifications
+ if (mBackend) {
+ rv = ShowWithBackend(mBackend, aAlert, aAlertListener, aPersistentData);
+ if (NS_SUCCEEDED(rv)) {
+ return rv;
+ }
+ // If the system backend failed to show the alert, clear the backend and
+ // retry with XUL notifications. Future alerts will always use XUL.
+ mBackend = nullptr;
+ }
+
+ if (!ShouldShowAlert()) {
+ // Do not display the alert. Instead call alertfinished and get out.
+ if (aAlertListener)
+ aAlertListener->Observe(nullptr, "alertfinished", cookie.get());
+ return NS_OK;
+ }
+
+ // Use XUL notifications as a fallback if above methods have failed.
+ nsCOMPtr<nsIAlertsService> xulBackend(nsXULAlerts::GetInstance());
+ NS_ENSURE_TRUE(xulBackend, NS_ERROR_FAILURE);
+ return ShowWithBackend(xulBackend, aAlert, aAlertListener, aPersistentData);
+}
+
+NS_IMETHODIMP nsAlertsService::CloseAlert(const nsAString& aAlertName,
+ nsIPrincipal* aPrincipal)
+{
+ if (XRE_IsContentProcess()) {
+ ContentChild* cpc = ContentChild::GetSingleton();
+ cpc->SendCloseAlert(nsAutoString(aAlertName), IPC::Principal(aPrincipal));
+ return NS_OK;
+ }
+
+ nsresult rv;
+ // Try the system notification service.
+ if (mBackend) {
+ rv = mBackend->CloseAlert(aAlertName, aPrincipal);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // If the system backend failed to close the alert, fall back to XUL for
+ // future alerts.
+ mBackend = nullptr;
+ }
+ } else {
+ nsCOMPtr<nsIAlertsService> xulBackend(nsXULAlerts::GetInstance());
+ NS_ENSURE_TRUE(xulBackend, NS_ERROR_FAILURE);
+ rv = xulBackend->CloseAlert(aAlertName, aPrincipal);
+ }
+ 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
+}
+
+already_AddRefed<nsIAlertsDoNotDisturb>
+nsAlertsService::GetDNDBackend()
+{
+ // Try the system notification service.
+ nsCOMPtr<nsIAlertsService> 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 000000000..3f23eaabf
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsService.h
@@ -0,0 +1,48 @@
+// /* -*- 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"
+
+#ifdef XP_WIN
+typedef enum tagMOZ_QUERY_USER_NOTIFICATION_STATE {
+ QUNS_NOT_PRESENT = 1,
+ QUNS_BUSY = 2,
+ QUNS_RUNNING_D3D_FULL_SCREEN = 3,
+ QUNS_PRESENTATION_MODE = 4,
+ QUNS_ACCEPTS_NOTIFICATIONS = 5,
+ QUNS_QUIET_TIME = 6,
+ QUNS_IMMERSIVE = 7
+} MOZ_QUERY_USER_NOTIFICATION_STATE;
+
+extern "C" {
+// This function is Windows Vista or later
+typedef HRESULT (__stdcall *SHQueryUserNotificationStatePtr)(MOZ_QUERY_USER_NOTIFICATION_STATE *pquns);
+}
+#endif // defined(XP_WIN)
+
+class nsAlertsService : public nsIAlertsService,
+ public nsIAlertsDoNotDisturb
+{
+public:
+ NS_DECL_NSIALERTSDONOTDISTURB
+ NS_DECL_NSIALERTSSERVICE
+ NS_DECL_ISUPPORTS
+
+ nsAlertsService();
+
+protected:
+ virtual ~nsAlertsService();
+
+ bool ShouldShowAlert();
+ 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 000000000..5f7d92d2a
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsUtils.cpp
@@ -0,0 +1,43 @@
+/* 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 "nsIStringBundle.h"
+#include "nsIURI.h"
+#include "nsXPIDLString.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;
+ }
+ nsCOMPtr<nsIURI> principalURI;
+ if (NS_WARN_IF(NS_FAILED(
+ aPrincipal->GetURI(getter_AddRefs(principalURI))))) {
+ return;
+ }
+ if (!principalURI) {
+ return;
+ }
+ nsAutoCString hostPort;
+ if (NS_WARN_IF(NS_FAILED(principalURI->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 000000000..bc11f6351
--- /dev/null
+++ b/toolkit/components/alerts/nsAlertsUtils.h
@@ -0,0 +1,32 @@
+/* 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 000000000..5629dabc9
--- /dev/null
+++ b/toolkit/components/alerts/nsIAlertsService.idl
@@ -0,0 +1,259 @@
+/* -*- 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(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);
+
+ /**
+ * The name of the notification. On 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;
+
+ /**
+ * 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;
+
+ /**
+ * 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.
+ */
+ void closeAlert([optional] in AString aName,
+ [optional] in nsIPrincipal aPrincipal);
+
+};
+
+[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;
+};
+
+[scriptable, uuid(fc6d7f0a-0cf6-4268-8c71-ab640842b9b1)]
+interface nsIAlertsIconData : nsISupports
+{
+ /**
+ * Shows an alert with an icon. Web notifications use the favicon of the
+ * page that created the alert. If the favicon is not in the Places database,
+ * |aIconSize| will be zero.
+ */
+ void showAlertWithIconData(in nsIAlertNotification aAlert,
+ [optional] in nsIObserver aAlertListener,
+ [optional] in uint32_t aIconSize,
+ [const, array, size_is(aIconSize)] in uint8_t
+ aIconData);
+};
+
+[scriptable, uuid(f3c82915-bf60-41ea-91ce-6c46b22e381a)]
+interface nsIAlertsIconURI : nsISupports
+{
+ /**
+ * Shows an alert with an icon URI. Web notifications use |moz-anno:|
+ * URIs to reference favicons from Places. If the page doesn't have a
+ * favicon, |aIconURI| will be |null|.
+ */
+ void showAlertWithIconURI(in nsIAlertNotification aAlert,
+ [optional] in nsIObserver aAlertListener,
+ [optional] in nsIURI aIconURI);
+};
diff --git a/toolkit/components/alerts/nsXULAlerts.cpp b/toolkit/components/alerts/nsXULAlerts.cpp
new file mode 100644
index 000000000..882617637
--- /dev/null
+++ b/toolkit/components/alerts/nsXULAlerts.cpp
@@ -0,0 +1,398 @@
+/* -*- 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/LookAndFeel.h"
+#include "mozilla/dom/Notification.h"
+#include "mozilla/Unused.h"
+#include "nsIServiceManager.h"
+#include "nsISupportsPrimitives.h"
+#include "nsPIDOMWindow.h"
+#include "nsIWindowWatcher.h"
+
+using namespace mozilla;
+using mozilla::dom::NotificationTelemetryService;
+
+#define ALERT_CHROME_URL "chrome://global/content/alerts/alert.xul"
+
+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);
+ nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle,
+ aAlertText, aAlertTextClickable,
+ aAlertCookie, aBidi, aLang, aData,
+ aPrincipal, aInPrivateBrowsing,
+ aRequireInteraction);
+ 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;
+ } else {
+ return ShowAlertWithIconURI(aAlert, aAlertListener, nullptr);
+ }
+}
+
+NS_IMETHODIMP
+nsXULAlerts::ShowAlertWithIconURI(nsIAlertNotification* aAlert,
+ nsIObserver* aAlertListener,
+ nsIURI* aIconURI)
+{
+ bool inPrivateBrowsing;
+ nsresult rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString cookie;
+ rv = aAlert->GetCookie(cookie);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mDoNotDisturb) {
+ if (!inPrivateBrowsing) {
+ RefPtr<NotificationTelemetryService> telemetry =
+ NotificationTelemetryService::GetInstance();
+ if (telemetry) {
+ // Record the number of unique senders for XUL alerts. The OS X and
+ // libnotify backends will fire `alertshow` even if "do not disturb"
+ // is enabled. In that case, `NotificationObserver` will record the
+ // sender.
+ nsCOMPtr<nsIPrincipal> principal;
+ if (NS_SUCCEEDED(aAlert->GetPrincipal(getter_AddRefs(principal)))) {
+ Unused << NS_WARN_IF(NS_FAILED(telemetry->RecordSender(principal)));
+ }
+ }
+ }
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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::eIntID_AlertNotificationOrigin);
+ scriptableOrigin->SetData(origin);
+
+ rv = argsArray->AppendElement(scriptableOrigin, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ 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, /*weak =*/ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<mozIDOMWindowProxy> newWindow;
+ nsAutoCString features("chrome,dialog=yes,titlebar=no,popup=yes");
+ if (inPrivateBrowsing) {
+ features.AppendLiteral(",private");
+ }
+ rv = wwatch->OpenWindow(nullptr, ALERT_CHROME_URL, "_blank", features.get(),
+ argsArray, getter_AddRefs(newWindow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mNamedWindows.Put(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::CloseAlert(const nsAString& aAlertName,
+ nsIPrincipal* aPrincipal)
+{
+ mozIDOMWindowProxy* alert = mNamedWindows.GetWeak(aAlertName);
+ if (nsCOMPtr<nsPIDOMWindowOuter> domWindow = nsPIDOMWindowOuter::From(alert)) {
+ domWindow->DispatchCustomEvent(NS_LITERAL_STRING("XULAlertClose"));
+ }
+ return NS_OK;
+}
+
diff --git a/toolkit/components/alerts/nsXULAlerts.h b/toolkit/components/alerts/nsXULAlerts.h
new file mode 100644
index 000000000..557716ee6
--- /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 "nsDataHashtable.h"
+#include "nsHashKeys.h"
+#include "nsInterfaceHashtable.h"
+
+#include "mozIDOMWindow.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()
+ {
+ }
+
+ static already_AddRefed<nsXULAlerts> GetInstance();
+
+protected:
+ virtual ~nsXULAlerts() {}
+ void PersistentAlertFinished();
+
+ nsInterfaceHashtable<nsStringHashKey, mozIDOMWindowProxy> mNamedWindows;
+ uint32_t mPersistentAlertCount = 0;
+ nsTArray<PendingAlert> mPendingPersistentAlerts;
+ bool mDoNotDisturb = 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() {}
+
+ RefPtr<nsXULAlerts> mXULAlerts;
+ nsString mAlertName;
+ nsCOMPtr<mozIDOMWindowProxy> mAlertWindow;
+ nsCOMPtr<nsIObserver> mObserver;
+ bool mIsPersistent;
+};
+
+#endif /* nsXULAlerts_h__ */
+
diff --git a/toolkit/components/alerts/resources/content/alert.css b/toolkit/components/alerts/resources/content/alert.css
new file mode 100644
index 000000000..c4d94a543
--- /dev/null
+++ b/toolkit/components/alerts/resources/content/alert.css
@@ -0,0 +1,34 @@
+/* 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]):not([closing]):hover {
+ animation-play-state: paused;
+}
+
+#alertBox:not([hasOrigin]) > box > #alertTextBox > #alertFooter,
+#alertBox:not([hasIcon]) > box > #alertIcon,
+#alertImage:not([src]) {
+ display: none;
+}
+
+#alertTitleBox {
+ -moz-box-pack: center;
+ -moz-box-align: center;
+}
+
+.alertText {
+ white-space: pre-wrap;
+}
+
+@keyframes alert-animation {
+ to {
+ visibility: hidden;
+ }
+}
diff --git a/toolkit/components/alerts/resources/content/alert.js b/toolkit/components/alerts/resources/content/alert.js
new file mode 100644
index 000000000..523ec378e
--- /dev/null
+++ b/toolkit/components/alerts/resources/content/alert.js
@@ -0,0 +1,332 @@
+/* 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/. */
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// 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 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];
+ }
+ }
+ 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],
+ 1));
+ let doNotDisturbMenuItem = document.getElementById("doNotDisturbMenuItem");
+ doNotDisturbMenuItem.setAttribute("label",
+ ALERT_BUNDLE.formatStringFromName("doNotDisturb.label",
+ [BRAND_NAME],
+ 1));
+ let disableForOrigin = document.getElementById("disableForOriginMenuItem");
+ disableForOrigin.setAttribute("label",
+ ALERT_BUNDLE.formatStringFromName("webActions.disableForOrigin.label",
+ [hostPort],
+ 1));
+ let openSettings = document.getElementById("openSettingsMenuItem");
+ openSettings.setAttribute("label",
+ ALERT_BUNDLE.GetStringFromName("webActions.settings.label"));
+ }
+ }
+ case 11:
+ gAlertListener = window.arguments[10];
+ case 10:
+ gReplacedWindow = window.arguments[9];
+ case 9:
+ gRequireInteraction = window.arguments[8];
+ case 8:
+ if (window.arguments[7]) {
+ document.getElementById("alertTitleLabel").setAttribute("lang", window.arguments[7]);
+ document.getElementById("alertTextLabel").setAttribute("lang", window.arguments[7]);
+ }
+ case 7:
+ if (window.arguments[6]) {
+ document.getElementById("alertNotification").style.direction = window.arguments[6];
+ }
+ case 6:
+ gOrigin = window.arguments[5];
+ case 5:
+ gAlertCookie = window.arguments[4];
+ case 4:
+ gAlertTextClickable = window.arguments[3];
+ if (gAlertTextClickable) {
+ document.getElementById("alertNotification").setAttribute("clickable", true);
+ document.getElementById("alertTextLabel").setAttribute("clickable", true);
+ }
+ 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;
+ }
+ case 2:
+ document.getElementById("alertTitleLabel").setAttribute("value", window.arguments[1]);
+ case 1:
+ if (window.arguments[0]) {
+ document.getElementById("alertBox").setAttribute("hasImage", true);
+ document.getElementById("alertImage").setAttribute("src", window.arguments[0]);
+ }
+ 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";
+
+ 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 (Services.prefs.getBoolPref("alerts.disableSlidingEffect")) {
+ 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, false);
+ window.close();
+ }
+ }, false);
+ alertBox.setAttribute("animate", true);
+ }
+ }
+
+ let alertSettings = document.getElementById("alertSettings");
+ alertSettings.addEventListener("focus", onAlertSettingsFocus);
+ alertSettings.addEventListener("click", onAlertSettingsClick);
+
+ 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) {
+ let windows = Services.wm.getEnumerator("alert:alert");
+ while (windows.hasMoreElements()) {
+ let alertWindow = windows.getNext();
+ // 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.
+ let windows = Services.wm.getEnumerator("alert:alert");
+ while (windows.hasMoreElements()) {
+ let alertWindow = windows.getNext();
+ if (alertWindow != window) {
+ 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;
+ let windows = Services.wm.getEnumerator("alert:alert");
+ while (windows.hasMoreElements()) {
+ let alertWindow = windows.getNext();
+ if (alertWindow != window) {
+ 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;
+ Services.telemetry.getHistogramById("WEB_NOTIFICATION_MENU")
+ .add(0);
+ 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/resources/content/alert.xul b/toolkit/components/alerts/resources/content/alert.xul
new file mode 100644
index 000000000..8597d9954
--- /dev/null
+++ b/toolkit/components/alerts/resources/content/alert.xul
@@ -0,0 +1,67 @@
+<?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 [
+<!ENTITY % alertDTD SYSTEM "chrome://alerts/locale/alert.dtd">
+%alertDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/content/alerts/alert.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/alerts/alert.css" type="text/css"?>
+
+<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();">
+
+ <script type="application/javascript" src="chrome://global/content/alerts/alert.js"/>
+
+ <vbox id="alertBox" class="alertBox">
+ <box id="alertTitleBox">
+ <image id="alertIcon"/>
+ <label id="alertTitleLabel" class="alertTitle plain" crop="end"/>
+ <vbox class="alertCloseBox">
+ <toolbarbutton class="alertCloseButton close-icon"
+ tooltiptext="&closeAlert.tooltip;"
+ 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" tooltiptext="&settings.label;">
+ <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 type="application/javascript">prefillAlertInfo();</script>
+</window>
+
diff --git a/toolkit/components/alerts/test/.eslintrc.js b/toolkit/components/alerts/test/.eslintrc.js
new file mode 100644
index 000000000..3c788d6d6
--- /dev/null
+++ b/toolkit/components/alerts/test/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../testing/mochitest/mochitest.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/alerts/test/image.gif b/toolkit/components/alerts/test/image.gif
new file mode 100644
index 000000000..053b4d926
--- /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 000000000..430c3c5e6
--- /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 000000000..622052943
--- /dev/null
+++ b/toolkit/components/alerts/test/image_server.sjs
@@ -0,0 +1,82 @@
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC } = Components;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+
+const LocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile",
+ "initWithPath");
+
+const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
+ "nsIFileInputStream", "init");
+
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream", "setInputStream");
+
+function handleRequest(request, response) {
+ let params = parseQueryString(request.queryString);
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ // Compare and increment a cookie for this request. This is used to test
+ // private browsing mode; the cookie should not be set if the image is
+ // loaded anonymously.
+ if (params.has("c")) {
+ let expectedValue = parseInt(params.get("c"), 10);
+ let actualValue = !request.hasHeader("Cookie") ? 0 :
+ parseInt(request.getHeader("Cookie")
+ .replace(/^counter=(\d+)/, "$1"), 10);
+ if (actualValue != expectedValue) {
+ response.setStatusLine(request.httpVersion, 400, "Wrong counter value");
+ return;
+ }
+ response.setHeader("Set-Cookie", `counter=${expectedValue + 1}`, false);
+ }
+
+ // Wait to send the image if a timeout is given.
+ let timeout = parseInt(params.get("t"), 10);
+ if (timeout > 0) {
+ response.processAsync();
+ setTimeout(() => {
+ respond(params, request, response);
+ response.finish();
+ }, timeout * 1000);
+ return;
+ }
+
+ respond(params, request, response);
+}
+
+function parseQueryString(queryString) {
+ return queryString.split("&").reduce((params, param) => {
+ let [key, value] = param.split("=", 2);
+ params.set(key, value);
+ return params;
+ }, new Map());
+}
+
+function respond(params, request, response) {
+ if (params.has("s")) {
+ let statusCode = parseInt(params.get("s"), 10);
+ response.setStatusLine(request.httpVersion, statusCode, "Custom status");
+ return;
+ }
+ var filename = params.get("f");
+ writeFile(filename, response);
+}
+
+function writeFile(name, response) {
+ var file = new LocalFile(getState("__LOCATION__")).parent;
+ file.append(name);
+
+ let mimeType = Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromFile(file);
+
+ let fileStream = new FileInputStream(file, 1, 0, false);
+ let binaryStream = new BinaryInputStream(fileStream);
+
+ response.setHeader("Content-Type", mimeType, false);
+ response.bodyOutputStream.writeFrom(binaryStream, binaryStream.available());
+
+ binaryStream.close();
+ fileStream.close();
+}
diff --git a/toolkit/components/alerts/test/mochitest.ini b/toolkit/components/alerts/test/mochitest.ini
new file mode 100644
index 000000000..12e2a8704
--- /dev/null
+++ b/toolkit/components/alerts/test/mochitest.ini
@@ -0,0 +1,16 @@
+[DEFAULT]
+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]
+skip-if = toolkit == 'android'
+[test_alerts_noobserve.html]
+[test_alerts_requireinteraction.html]
+[test_image.html]
+[test_multiple_alerts.html]
+[test_principal.html]
+skip-if = toolkit == 'android'
diff --git a/toolkit/components/alerts/test/test_alerts.html b/toolkit/components/alerts/test/test_alerts.html
new file mode 100644
index 000000000..cb087e48a
--- /dev/null
+++ b/toolkit/components/alerts/test/test_alerts.html
@@ -0,0 +1,89 @@
+<!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 type="text/javascript" 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 observer = {
+ alertShow: false,
+ observe: function (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;
+ } 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");
+
+ 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);
+ return;
+ }
+
+ try {
+ var alertName = "fiorello";
+ 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...");
+
+ if ("@mozilla.org/system-alerts-service;1" in Cc) {
+ // Notifications are native on OS X 10.8 and later, as well as GNOME
+ // Shell with libnotify (bug 1236036). 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);
+ }
+ } 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 000000000..0cc452b8a
--- /dev/null
+++ b/toolkit/components/alerts/test/test_alerts_noobserve.html
@@ -0,0 +1,96 @@
+<!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 type="text/javascript" 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(_ => {
+ const { utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Timer.jsm");
+
+ 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");
+}
+
+function runTest() {
+ 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");
+
+// sendSyncMessage returns an array of arrays: the outer array is from the
+// message manager, and the inner array is from the chrome script's listeners.
+// See the comment in test_SpecialPowersLoadChromeScript.html.
+var [[alertsVisible]] = chromeScript.sendSyncMessage("anyXULAlertsVisible");
+ok(!alertsVisible, "Alerts should not be present at the start of the test.");
+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 000000000..26fe87104
--- /dev/null
+++ b/toolkit/components/alerts/test/test_alerts_requireinteraction.html
@@ -0,0 +1,168 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for alerts with requireInteraction</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(_ => {
+ const { utils: Cu } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Timer.jsm");
+
+ addMessageListener("waitForXULAlert", function() {
+ var timer = setTimeout(function() {
+ Services.ww.unregisterNotification(windowObserver);
+ sendAsyncMessage("waitForXULAlert", false);
+ }, 2000);
+
+ var windowObserver = function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ var win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ let windowType = win.document.documentElement.getAttribute("windowtype");
+ if (windowType == "alert:alert") {
+ clearTimeout(timer);
+ Services.ww.unregisterNotification(windowObserver);
+
+ sendAsyncMessage("waitForXULAlert", 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(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;
+ }
+
+ yield 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 = yield promiseCreateXULAlert(alertService,
+ createAlertListener("first"), "first");
+ if (!xulAlertCreated) {
+ ok(true, "Platform does not use XUL alerts.");
+ alertService.closeAlert("first");
+ return;
+ }
+
+ xulAlertCreated = yield promiseCreateXULAlert(alertService,
+ createAlertListener("second"), "second");
+ ok(xulAlertCreated, "Create XUL alert");
+
+ // Replace second alert
+ xulAlertCreated = yield promiseCreateXULAlert(alertService,
+ createAlertListener("second replacement"), "second");
+ ok(xulAlertCreated, "Create XUL alert");
+
+ var testFinishResolve;
+ var testFinishPromise = new Promise((resolve) => { testFinishResolve = resolve; });
+
+ xulAlertCreated = yield promiseCreateXULAlert(alertService,
+ createAlertListener("third"), "third"),
+ ok(!xulAlertCreated, "XUL alert should not be visible");
+
+ // Replace the not-yet-visible third alert.
+ xulAlertCreated = yield 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");
+
+ yield 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 000000000..7bf89fab2
--- /dev/null
+++ b/toolkit/components/alerts/test/test_image.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 1233086</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(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] = yield 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] = yield 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(function* testTimeout() {
+ var alert = makeAlert(null, imageServerURL + "?f=image.png&t=3");
+ var [ready] = yield promiseImage(alert, 1000);
+ ok(!ready, "Should cancel request if timeout fires");
+
+ [ready, request] = yield promiseImage(alert, 45000);
+ ok(ready, "Should load image if request finishes before timeout");
+});
+
+add_task(function* testAnimatedGIF() {
+ var alert = makeAlert(null, imageServerURL + "?f=image.gif");
+ var [ready, request] = yield 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(function* testCancel() {
+ var alert = makeAlert(null, imageServerURL + "?f=image.gif&t=180");
+ yield 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(function* testMixedContent() {
+ // Loading principal is HTTPS; image URL is HTTP.
+ var origin = "https://mochi.test:8888";
+ var principal = Services.scriptSecurityManager
+ .createCodebasePrincipalFromOrigin(origin);
+
+ var alert = makeAlert(null, imageServerURL + "?f=image.png",
+ null, null, false, null, null, null,
+ null, principal);
+ var [ready, request] = yield 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_multiple_alerts.html b/toolkit/components/alerts/test/test_multiple_alerts.html
new file mode 100644
index 000000000..9d939b63a
--- /dev/null
+++ b/toolkit/components/alerts/test/test_multiple_alerts.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for multiple alerts</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(_ => {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource://gre/modules/Timer.jsm");
+
+ const alertService = Components.classes["@mozilla.org/alerts-service;1"]
+ .getService(Components.interfaces.nsIAlertsService);
+
+ addMessageListener("waitForPosition", function() {
+ var timer = setTimeout(function() {
+ Services.ww.unregisterNotification(windowObserver);
+ sendAsyncMessage("waitedForPosition", null);
+ }, 2000);
+
+ var windowObserver = function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened") {
+ return;
+ }
+
+ // Alerts are implemented using XUL.
+ clearTimeout(timer);
+
+ Services.ww.unregisterNotification(windowObserver);
+
+ var win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);
+ win.addEventListener("pageshow", function onPageShow() {
+ win.removeEventListener("pageshow", onPageShow, false);
+
+ var x = win.screenX;
+ var y = win.screenY;
+
+ win.addEventListener("pagehide", function onPageHide() {
+ win.removeEventListener("pagehide", onPageHide, false);
+ sendAsyncMessage("waitedForPosition", { x, y });
+ }, false);
+
+ alertService.closeAlert();
+ }, false);
+ };
+
+ 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(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 = yield promiseAlertPosition(alertService);
+ if (!firstAlertPosition) {
+ ok(true, "Platform does not use XUL alerts.");
+ return;
+ }
+
+ var secondAlertPosition = yield 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 000000000..74a20dbd7
--- /dev/null
+++ b/toolkit/components/alerts/test/test_principal.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 1202933</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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(_ => {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ 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;
+ function observe(subject, topic, data) {
+ if (topic == "alertclickcallback") {
+ reject(new Error("Alerts should not be clicked during test"));
+ } else if (topic == "alertshow") {
+ source = chromeScript.sendSyncMessage("getAlertSource")[0][0];
+ 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);
+ }
+ });
+}
+
+function* testNoPrincipal() {
+ var source = yield notify("noPrincipal", null);
+ ok(!source, "Should omit source without principal");
+}
+
+function* testSystemPrincipal() {
+ var principal = Services.scriptSecurityManager.getSystemPrincipal();
+ var source = yield notify("systemPrincipal", principal);
+ ok(!source, "Should omit source for system principal");
+}
+
+function* testNullPrincipal() {
+ var principal = Services.scriptSecurityManager.createNullPrincipal({});
+ var source = yield notify("nullPrincipal", principal);
+ ok(!source, "Should omit source for null principal");
+}
+
+function* testNodePrincipal() {
+ var principal = SpecialPowers.wrap(document).nodePrincipal;
+ var source = yield notify("nodePrincipal", principal);
+
+ var stringBundle = Services.strings.createBundle(
+ "chrome://alerts/locale/alert.properties"
+ );
+ var localizedSource = stringBundle.formatStringFromName(
+ "source.label", [principal.URI.hostPort], 1);
+ 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;
+ }
+
+ if ("@mozilla.org/system-alerts-service;1" in Cc) {
+ todo(false, "Native alerts service exists in this application");
+ return;
+ }
+
+ ok(true, "Alerts service exists in this application");
+
+ // sendSyncMessage returns an array of arrays. See the comments in
+ // test_alerts_noobserve.html and test_SpecialPowersLoadChromeScript.html.
+ var [[alertsVisible]] = chromeScript.sendSyncMessage("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>