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