diff options
Diffstat (limited to 'toolkit/components/alerts')
26 files changed, 3028 insertions, 0 deletions
diff --git a/toolkit/components/alerts/AlertNotification.cpp b/toolkit/components/alerts/AlertNotification.cpp new file mode 100644 index 000000000..b828f1100 --- /dev/null +++ b/toolkit/components/alerts/AlertNotification.cpp @@ -0,0 +1,361 @@ +/* This Source Code Form is subject to the terms of the Mozilla Pub + * License, v. 2.0. If a copy of the MPL was not distributed with t + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/AlertNotification.h" + +#include "imgIContainer.h" +#include "imgINotificationObserver.h" +#include "imgIRequest.h" +#include "imgLoader.h" +#include "nsAlertsUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" + +#include "mozilla/Unused.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(AlertNotification, nsIAlertNotification) + +AlertNotification::AlertNotification() + : mTextClickable(false) + , mInPrivateBrowsing(false) +{} + +AlertNotification::~AlertNotification() +{} + +NS_IMETHODIMP +AlertNotification::Init(const nsAString& aName, const nsAString& aImageURL, + const nsAString& aTitle, const nsAString& aText, + bool aTextClickable, const nsAString& aCookie, + const nsAString& aDir, const nsAString& aLang, + const nsAString& aData, nsIPrincipal* aPrincipal, + bool aInPrivateBrowsing, bool aRequireInteraction) +{ + mName = aName; + mImageURL = aImageURL; + mTitle = aTitle; + mText = aText; + mTextClickable = aTextClickable; + mCookie = aCookie; + mDir = aDir; + mLang = aLang; + mData = aData; + mPrincipal = aPrincipal; + mInPrivateBrowsing = aInPrivateBrowsing; + mRequireInteraction = aRequireInteraction; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetName(nsAString& aName) +{ + aName = mName; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetImageURL(nsAString& aImageURL) +{ + aImageURL = mImageURL; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetTitle(nsAString& aTitle) +{ + aTitle = mTitle; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetText(nsAString& aText) +{ + aText = mText; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetTextClickable(bool* aTextClickable) +{ + *aTextClickable = mTextClickable; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetCookie(nsAString& aCookie) +{ + aCookie = mCookie; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetDir(nsAString& aDir) +{ + aDir = mDir; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetLang(nsAString& aLang) +{ + aLang = mLang; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetRequireInteraction(bool* aRequireInteraction) +{ + *aRequireInteraction = mRequireInteraction; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetData(nsAString& aData) +{ + aData = mData; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetPrincipal(nsIPrincipal** aPrincipal) +{ + NS_IF_ADDREF(*aPrincipal = mPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetURI(nsIURI** aURI) +{ + if (!nsAlertsUtils::IsActionablePrincipal(mPrincipal)) { + *aURI = nullptr; + return NS_OK; + } + return mPrincipal->GetURI(aURI); +} + +NS_IMETHODIMP +AlertNotification::GetInPrivateBrowsing(bool* aInPrivateBrowsing) +{ + *aInPrivateBrowsing = mInPrivateBrowsing; + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetActionable(bool* aActionable) +{ + *aActionable = nsAlertsUtils::IsActionablePrincipal(mPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::GetSource(nsAString& aSource) +{ + nsAlertsUtils::GetSourceHostPort(mPrincipal, aSource); + return NS_OK; +} + +NS_IMETHODIMP +AlertNotification::LoadImage(uint32_t aTimeout, + nsIAlertNotificationImageListener* aListener, + nsISupports* aUserData, + nsICancelable** aRequest) +{ + NS_ENSURE_ARG(aListener); + NS_ENSURE_ARG_POINTER(aRequest); + *aRequest = nullptr; + + // Exit early if this alert doesn't have an image. + if (mImageURL.IsEmpty()) { + return aListener->OnImageMissing(aUserData); + } + nsCOMPtr<nsIURI> imageURI; + NS_NewURI(getter_AddRefs(imageURI), mImageURL); + if (!imageURI) { + return aListener->OnImageMissing(aUserData); + } + + RefPtr<AlertImageRequest> request = new AlertImageRequest(imageURI, mPrincipal, + mInPrivateBrowsing, + aTimeout, aListener, + aUserData); + nsresult rv = request->Start(); + request.forget(aRequest); + return rv; +} + +NS_IMPL_CYCLE_COLLECTION(AlertImageRequest, mURI, mPrincipal, mListener, + mUserData) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AlertImageRequest) + NS_INTERFACE_MAP_ENTRY(imgINotificationObserver) + NS_INTERFACE_MAP_ENTRY(nsICancelable) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, imgINotificationObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AlertImageRequest) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AlertImageRequest) + +AlertImageRequest::AlertImageRequest(nsIURI* aURI, nsIPrincipal* aPrincipal, + bool aInPrivateBrowsing, uint32_t aTimeout, + nsIAlertNotificationImageListener* aListener, + nsISupports* aUserData) + : mURI(aURI) + , mPrincipal(aPrincipal) + , mInPrivateBrowsing(aInPrivateBrowsing) + , mTimeout(aTimeout) + , mListener(aListener) + , mUserData(aUserData) +{} + +AlertImageRequest::~AlertImageRequest() +{ + if (mRequest) { + mRequest->CancelAndForgetObserver(NS_BINDING_ABORTED); + } +} + +NS_IMETHODIMP +AlertImageRequest::Notify(imgIRequest* aRequest, int32_t aType, + const nsIntRect* aData) +{ + MOZ_ASSERT(aRequest == mRequest); + + uint32_t imgStatus = imgIRequest::STATUS_ERROR; + nsresult rv = aRequest->GetImageStatus(&imgStatus); + if (NS_WARN_IF(NS_FAILED(rv)) || + (imgStatus & imgIRequest::STATUS_ERROR)) { + return NotifyMissing(); + } + + // If the image is already decoded, `FRAME_COMPLETE` will fire before + // `LOAD_COMPLETE`, so we can notify the listener immediately. Otherwise, + // we'll need to request a decode when `LOAD_COMPLETE` fires, and wait + // for the first frame. + + if (aType == imgINotificationObserver::LOAD_COMPLETE) { + if (!(imgStatus & imgIRequest::STATUS_FRAME_COMPLETE)) { + nsCOMPtr<imgIContainer> image; + rv = aRequest->GetImage(getter_AddRefs(image)); + if (NS_WARN_IF(NS_FAILED(rv) || !image)) { + return NotifyMissing(); + } + + // Ask the image to decode at its intrinsic size. + int32_t width = 0, height = 0; + image->GetWidth(&width); + image->GetHeight(&height); + image->RequestDecodeForSize(gfx::IntSize(width, height), imgIContainer::FLAG_NONE); + } + return NS_OK; + } + + if (aType == imgINotificationObserver::FRAME_COMPLETE) { + return NotifyComplete(); + } + + return NS_OK; +} + +NS_IMETHODIMP +AlertImageRequest::Notify(nsITimer* aTimer) +{ + MOZ_ASSERT(aTimer == mTimer); + return NotifyMissing(); +} + +NS_IMETHODIMP +AlertImageRequest::Cancel(nsresult aReason) +{ + if (mRequest) { + mRequest->Cancel(aReason); + } + // We call `NotifyMissing` here because we won't receive a `LOAD_COMPLETE` + // notification if we cancel the request before it loads (bug 1233086, + // comment 33). Once that's fixed, `nsIAlertNotification::loadImage` could + // return the underlying `imgIRequest` instead of the wrapper. + return NotifyMissing(); +} + +nsresult +AlertImageRequest::Start() +{ + // Keep the request alive until we notify the image listener. + NS_ADDREF_THIS(); + + nsresult rv; + if (mTimeout > 0) { + mTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + if (NS_WARN_IF(!mTimer)) { + return NotifyMissing(); + } + rv = mTimer->InitWithCallback(this, mTimeout, + nsITimer::TYPE_ONE_SHOT); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NotifyMissing(); + } + } + + // Begin loading the image. + imgLoader* il = imgLoader::NormalLoader(); + if (!il) { + return NotifyMissing(); + } + + // Bug 1237405: `LOAD_ANONYMOUS` disables cookies, but we want to use a + // temporary cookie jar instead. We should also use + // `imgLoader::PrivateBrowsingLoader()` instead of the normal loader. + // Unfortunately, the PB loader checks the load group, and asserts if its + // load context's PB flag isn't set. The fix is to pass the load group to + // `nsIAlertNotification::loadImage`. + int32_t loadFlags = mInPrivateBrowsing ? nsIRequest::LOAD_ANONYMOUS : + nsIRequest::LOAD_NORMAL; + + rv = il->LoadImageXPCOM(mURI, nullptr, nullptr, + NS_LITERAL_STRING("default"), mPrincipal, nullptr, + this, nullptr, loadFlags, nullptr, + nsIContentPolicy::TYPE_INTERNAL_IMAGE, + getter_AddRefs(mRequest)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NotifyMissing(); + } + + return NS_OK; +} + +nsresult +AlertImageRequest::NotifyMissing() +{ + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + if (nsCOMPtr<nsIAlertNotificationImageListener> listener = mListener.forget()) { + nsresult rv = listener->OnImageMissing(mUserData); + NS_RELEASE_THIS(); + return rv; + } + return NS_OK; +} + +nsresult +AlertImageRequest::NotifyComplete() +{ + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + if (nsCOMPtr<nsIAlertNotificationImageListener> listener = mListener.forget()) { + nsresult rv = listener->OnImageReady(mUserData, mRequest); + NS_RELEASE_THIS(); + return rv; + } + return NS_OK; +} + +} // namespace mozilla diff --git a/toolkit/components/alerts/AlertNotification.h b/toolkit/components/alerts/AlertNotification.h new file mode 100644 index 000000000..c0bcc0ba9 --- /dev/null +++ b/toolkit/components/alerts/AlertNotification.h @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_AlertNotification_h__ +#define mozilla_AlertNotification_h__ + +#include "imgINotificationObserver.h" +#include "nsIAlertsService.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsICancelable.h" +#include "nsIPrincipal.h" +#include "nsString.h" +#include "nsITimer.h" + +namespace mozilla { + +class AlertImageRequest final : public imgINotificationObserver, + public nsICancelable, + public nsITimerCallback +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(AlertImageRequest, + imgINotificationObserver) + NS_DECL_IMGINOTIFICATIONOBSERVER + NS_DECL_NSICANCELABLE + NS_DECL_NSITIMERCALLBACK + + AlertImageRequest(nsIURI* aURI, nsIPrincipal* aPrincipal, + bool aInPrivateBrowsing, uint32_t aTimeout, + nsIAlertNotificationImageListener* aListener, + nsISupports* aUserData); + + nsresult Start(); + +private: + virtual ~AlertImageRequest(); + + nsresult NotifyMissing(); + nsresult NotifyComplete(); + + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIPrincipal> mPrincipal; + bool mInPrivateBrowsing; + uint32_t mTimeout; + nsCOMPtr<nsIAlertNotificationImageListener> mListener; + nsCOMPtr<nsISupports> mUserData; + nsCOMPtr<nsITimer> mTimer; + nsCOMPtr<imgIRequest> mRequest; +}; + +class AlertNotification final : public nsIAlertNotification +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIALERTNOTIFICATION + AlertNotification(); + +protected: + virtual ~AlertNotification(); + +private: + nsString mName; + nsString mImageURL; + nsString mTitle; + nsString mText; + bool mTextClickable; + nsString mCookie; + nsString mDir; + nsString mLang; + bool mRequireInteraction; + nsString mData; + nsCOMPtr<nsIPrincipal> mPrincipal; + bool mInPrivateBrowsing; +}; + +} // namespace mozilla + +#endif /* mozilla_AlertNotification_h__ */ diff --git a/toolkit/components/alerts/AlertNotificationIPCSerializer.h b/toolkit/components/alerts/AlertNotificationIPCSerializer.h new file mode 100644 index 000000000..9544f9633 --- /dev/null +++ b/toolkit/components/alerts/AlertNotificationIPCSerializer.h @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_AlertNotificationIPCSerializer_h__ +#define mozilla_AlertNotificationIPCSerializer_h__ + +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsIAlertsService.h" +#include "nsIPrincipal.h" +#include "nsString.h" + +#include "ipc/IPCMessageUtils.h" + +#include "mozilla/dom/PermissionMessageUtils.h" + +typedef nsIAlertNotification* AlertNotificationType; + +namespace IPC { + +template <> +struct ParamTraits<AlertNotificationType> +{ + typedef AlertNotificationType paramType; + + static void Write(Message* aMsg, const paramType& aParam) + { + bool isNull = !aParam; + if (isNull) { + WriteParam(aMsg, isNull); + return; + } + + nsString name, imageURL, title, text, cookie, dir, lang, data; + bool textClickable, inPrivateBrowsing, requireInteraction; + nsCOMPtr<nsIPrincipal> principal; + + if (NS_WARN_IF(NS_FAILED(aParam->GetName(name))) || + NS_WARN_IF(NS_FAILED(aParam->GetImageURL(imageURL))) || + NS_WARN_IF(NS_FAILED(aParam->GetTitle(title))) || + NS_WARN_IF(NS_FAILED(aParam->GetText(text))) || + NS_WARN_IF(NS_FAILED(aParam->GetTextClickable(&textClickable))) || + NS_WARN_IF(NS_FAILED(aParam->GetCookie(cookie))) || + NS_WARN_IF(NS_FAILED(aParam->GetDir(dir))) || + NS_WARN_IF(NS_FAILED(aParam->GetLang(lang))) || + NS_WARN_IF(NS_FAILED(aParam->GetData(data))) || + NS_WARN_IF(NS_FAILED(aParam->GetPrincipal(getter_AddRefs(principal)))) || + NS_WARN_IF(NS_FAILED(aParam->GetInPrivateBrowsing(&inPrivateBrowsing))) || + NS_WARN_IF(NS_FAILED(aParam->GetRequireInteraction(&requireInteraction)))) { + + // Write a `null` object if any getter returns an error. Otherwise, the + // receiver will try to deserialize an incomplete object and crash. + WriteParam(aMsg, /* isNull */ true); + return; + } + + WriteParam(aMsg, isNull); + WriteParam(aMsg, name); + WriteParam(aMsg, imageURL); + WriteParam(aMsg, title); + WriteParam(aMsg, text); + WriteParam(aMsg, textClickable); + WriteParam(aMsg, cookie); + WriteParam(aMsg, dir); + WriteParam(aMsg, lang); + WriteParam(aMsg, data); + WriteParam(aMsg, IPC::Principal(principal)); + WriteParam(aMsg, inPrivateBrowsing); + WriteParam(aMsg, requireInteraction); + } + + static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult) + { + bool isNull; + NS_ENSURE_TRUE(ReadParam(aMsg, aIter, &isNull), false); + if (isNull) { + *aResult = nullptr; + return true; + } + + nsString name, imageURL, title, text, cookie, dir, lang, data; + bool textClickable, inPrivateBrowsing, requireInteraction; + IPC::Principal principal; + + if (!ReadParam(aMsg, aIter, &name) || + !ReadParam(aMsg, aIter, &imageURL) || + !ReadParam(aMsg, aIter, &title) || + !ReadParam(aMsg, aIter, &text) || + !ReadParam(aMsg, aIter, &textClickable) || + !ReadParam(aMsg, aIter, &cookie) || + !ReadParam(aMsg, aIter, &dir) || + !ReadParam(aMsg, aIter, &lang) || + !ReadParam(aMsg, aIter, &data) || + !ReadParam(aMsg, aIter, &principal) || + !ReadParam(aMsg, aIter, &inPrivateBrowsing) || + !ReadParam(aMsg, aIter, &requireInteraction)) { + + return false; + } + + nsCOMPtr<nsIAlertNotification> alert = + do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); + if (NS_WARN_IF(!alert)) { + *aResult = nullptr; + return true; + } + nsresult rv = alert->Init(name, imageURL, title, text, textClickable, + cookie, dir, lang, data, principal, + inPrivateBrowsing, requireInteraction); + if (NS_WARN_IF(NS_FAILED(rv))) { + *aResult = nullptr; + return true; + } + alert.forget(aResult); + return true; + } +}; + +} // namespace IPC + +#endif /* mozilla_AlertNotificationIPCSerializer_h__ */ diff --git a/toolkit/components/alerts/jar.mn b/toolkit/components/alerts/jar.mn new file mode 100644 index 000000000..c45939078 --- /dev/null +++ b/toolkit/components/alerts/jar.mn @@ -0,0 +1,8 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +toolkit.jar: + content/global/alerts/alert.css (resources/content/alert.css) + content/global/alerts/alert.xul (resources/content/alert.xul) + content/global/alerts/alert.js (resources/content/alert.js) diff --git a/toolkit/components/alerts/moz.build b/toolkit/components/alerts/moz.build new file mode 100644 index 000000000..cdbf92511 --- /dev/null +++ b/toolkit/components/alerts/moz.build @@ -0,0 +1,38 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +MOCHITEST_MANIFESTS += ['test/mochitest.ini'] + +XPIDL_SOURCES += [ + 'nsIAlertsService.idl', +] + +XPIDL_MODULE = 'alerts' + +EXPORTS += [ + 'nsAlertsUtils.h', +] + +EXPORTS.mozilla += [ + 'AlertNotification.h', + 'AlertNotificationIPCSerializer.h', +] + +UNIFIED_SOURCES += [ + 'AlertNotification.cpp', + 'nsAlertsService.cpp', + 'nsAlertsUtils.cpp', + 'nsXULAlerts.cpp', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +JAR_MANIFESTS += ['jar.mn'] + +with Files('**'): + BUG_COMPONENT = ('Toolkit', 'Notifications and Alerts') diff --git a/toolkit/components/alerts/nsAlertsService.cpp b/toolkit/components/alerts/nsAlertsService.cpp new file mode 100644 index 000000000..35418dd17 --- /dev/null +++ b/toolkit/components/alerts/nsAlertsService.cpp @@ -0,0 +1,320 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/Preferences.h" +#include "mozilla/Telemetry.h" +#include "nsXULAppAPI.h" + +#include "nsAlertsService.h" + +#include "nsXPCOM.h" +#include "nsIServiceManager.h" +#include "nsIDOMWindow.h" +#include "nsPromiseFlatString.h" +#include "nsToolkitCompsCID.h" + +#ifdef MOZ_PLACES +#include "mozIAsyncFavicons.h" +#include "nsIFaviconService.h" +#endif // MOZ_PLACES + +using namespace mozilla; + +using mozilla::dom::ContentChild; + +namespace { + +#ifdef MOZ_PLACES + +class IconCallback final : public nsIFaviconDataCallback +{ +public: + NS_DECL_ISUPPORTS + + IconCallback(nsIAlertsService* aBackend, + nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) + : mBackend(aBackend) + , mAlert(aAlert) + , mAlertListener(aAlertListener) + {} + + NS_IMETHOD + OnComplete(nsIURI *aIconURI, uint32_t aIconSize, const uint8_t *aIconData, + const nsACString &aMimeType) override + { + nsresult rv = NS_ERROR_FAILURE; + if (aIconSize > 0) { + nsCOMPtr<nsIAlertsIconData> alertsIconData(do_QueryInterface(mBackend)); + if (alertsIconData) { + rv = alertsIconData->ShowAlertWithIconData(mAlert, mAlertListener, + aIconSize, aIconData); + } + } else if (aIconURI) { + nsCOMPtr<nsIAlertsIconURI> alertsIconURI(do_QueryInterface(mBackend)); + if (alertsIconURI) { + rv = alertsIconURI->ShowAlertWithIconURI(mAlert, mAlertListener, + aIconURI); + } + } + if (NS_FAILED(rv)) { + rv = mBackend->ShowAlert(mAlert, mAlertListener); + } + return rv; + } + +private: + virtual ~IconCallback() {} + + nsCOMPtr<nsIAlertsService> mBackend; + nsCOMPtr<nsIAlertNotification> mAlert; + nsCOMPtr<nsIObserver> mAlertListener; +}; + +NS_IMPL_ISUPPORTS(IconCallback, nsIFaviconDataCallback) + +#endif // MOZ_PLACES + +nsresult +ShowWithIconBackend(nsIAlertsService* aBackend, nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) +{ +#ifdef MOZ_PLACES + nsCOMPtr<nsIURI> uri; + nsresult rv = aAlert->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv) || !uri) { + return NS_ERROR_FAILURE; + } + + // Ensure the backend supports favicons. + nsCOMPtr<nsIAlertsIconData> alertsIconData(do_QueryInterface(aBackend)); + nsCOMPtr<nsIAlertsIconURI> alertsIconURI; + if (!alertsIconData) { + alertsIconURI = do_QueryInterface(aBackend); + } + if (!alertsIconData && !alertsIconURI) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + nsCOMPtr<mozIAsyncFavicons> favicons(do_GetService( + "@mozilla.org/browser/favicon-service;1")); + NS_ENSURE_TRUE(favicons, NS_ERROR_FAILURE); + + nsCOMPtr<nsIFaviconDataCallback> callback = + new IconCallback(aBackend, aAlert, aAlertListener); + if (alertsIconData) { + return favicons->GetFaviconDataForPage(uri, callback); + } + return favicons->GetFaviconURLForPage(uri, callback); +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif // !MOZ_PLACES +} + +nsresult +ShowWithBackend(nsIAlertsService* aBackend, nsIAlertNotification* aAlert, + nsIObserver* aAlertListener, const nsAString& aPersistentData) +{ + if (!aPersistentData.IsEmpty()) { + return aBackend->ShowPersistentNotification( + aPersistentData, aAlert, aAlertListener); + } + + if (Preferences::GetBool("alerts.showFavicons")) { + nsresult rv = ShowWithIconBackend(aBackend, aAlert, aAlertListener); + if (NS_SUCCEEDED(rv)) { + return rv; + } + } + + // If favicons are disabled, or the backend doesn't support them, show the + // alert without one. + return aBackend->ShowAlert(aAlert, aAlertListener); +} + +} // anonymous namespace + +NS_IMPL_ISUPPORTS(nsAlertsService, nsIAlertsService, nsIAlertsDoNotDisturb) + +nsAlertsService::nsAlertsService() : + mBackend(nullptr) +{ + mBackend = do_GetService(NS_SYSTEMALERTSERVICE_CONTRACTID); +} + +nsAlertsService::~nsAlertsService() +{} + +bool nsAlertsService::ShouldShowAlert() +{ + bool result = true; + +#ifdef XP_WIN + HMODULE shellDLL = ::LoadLibraryW(L"shell32.dll"); + if (!shellDLL) + return result; + + SHQueryUserNotificationStatePtr pSHQueryUserNotificationState = + (SHQueryUserNotificationStatePtr) ::GetProcAddress(shellDLL, "SHQueryUserNotificationState"); + + if (pSHQueryUserNotificationState) { + MOZ_QUERY_USER_NOTIFICATION_STATE qstate; + if (SUCCEEDED(pSHQueryUserNotificationState(&qstate))) { + if (qstate != QUNS_ACCEPTS_NOTIFICATIONS) { + result = false; + } + } + } + + ::FreeLibrary(shellDLL); +#endif + + return result; +} + +NS_IMETHODIMP nsAlertsService::ShowAlertNotification(const nsAString & aImageUrl, const nsAString & aAlertTitle, + const nsAString & aAlertText, bool aAlertTextClickable, + const nsAString & aAlertCookie, + nsIObserver * aAlertListener, + const nsAString & aAlertName, + const nsAString & aBidi, + const nsAString & aLang, + const nsAString & aData, + nsIPrincipal * aPrincipal, + bool aInPrivateBrowsing, + bool aRequireInteraction) +{ + nsCOMPtr<nsIAlertNotification> alert = + do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); + NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE); + nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, + aAlertText, aAlertTextClickable, + aAlertCookie, aBidi, aLang, aData, + aPrincipal, aInPrivateBrowsing, + aRequireInteraction); + NS_ENSURE_SUCCESS(rv, rv); + return ShowAlert(alert, aAlertListener); +} + + +NS_IMETHODIMP nsAlertsService::ShowAlert(nsIAlertNotification * aAlert, + nsIObserver * aAlertListener) +{ + return ShowPersistentNotification(EmptyString(), aAlert, aAlertListener); +} + +NS_IMETHODIMP nsAlertsService::ShowPersistentNotification(const nsAString & aPersistentData, + nsIAlertNotification * aAlert, + nsIObserver * aAlertListener) +{ + NS_ENSURE_ARG(aAlert); + + nsAutoString cookie; + nsresult rv = aAlert->GetCookie(cookie); + NS_ENSURE_SUCCESS(rv, rv); + + if (XRE_IsContentProcess()) { + ContentChild* cpc = ContentChild::GetSingleton(); + + if (aAlertListener) + cpc->AddRemoteAlertObserver(cookie, aAlertListener); + + cpc->SendShowAlert(aAlert); + return NS_OK; + } + + // Check if there is an optional service that handles system-level notifications + if (mBackend) { + rv = ShowWithBackend(mBackend, aAlert, aAlertListener, aPersistentData); + if (NS_SUCCEEDED(rv)) { + return rv; + } + // If the system backend failed to show the alert, clear the backend and + // retry with XUL notifications. Future alerts will always use XUL. + mBackend = nullptr; + } + + if (!ShouldShowAlert()) { + // Do not display the alert. Instead call alertfinished and get out. + if (aAlertListener) + aAlertListener->Observe(nullptr, "alertfinished", cookie.get()); + return NS_OK; + } + + // Use XUL notifications as a fallback if above methods have failed. + nsCOMPtr<nsIAlertsService> xulBackend(nsXULAlerts::GetInstance()); + NS_ENSURE_TRUE(xulBackend, NS_ERROR_FAILURE); + return ShowWithBackend(xulBackend, aAlert, aAlertListener, aPersistentData); +} + +NS_IMETHODIMP nsAlertsService::CloseAlert(const nsAString& aAlertName, + nsIPrincipal* aPrincipal) +{ + if (XRE_IsContentProcess()) { + ContentChild* cpc = ContentChild::GetSingleton(); + cpc->SendCloseAlert(nsAutoString(aAlertName), IPC::Principal(aPrincipal)); + return NS_OK; + } + + nsresult rv; + // Try the system notification service. + if (mBackend) { + rv = mBackend->CloseAlert(aAlertName, aPrincipal); + if (NS_WARN_IF(NS_FAILED(rv))) { + // If the system backend failed to close the alert, fall back to XUL for + // future alerts. + mBackend = nullptr; + } + } else { + nsCOMPtr<nsIAlertsService> xulBackend(nsXULAlerts::GetInstance()); + NS_ENSURE_TRUE(xulBackend, NS_ERROR_FAILURE); + rv = xulBackend->CloseAlert(aAlertName, aPrincipal); + } + return rv; +} + + +// nsIAlertsDoNotDisturb +NS_IMETHODIMP nsAlertsService::GetManualDoNotDisturb(bool* aRetVal) +{ +#ifdef MOZ_WIDGET_ANDROID + return NS_ERROR_NOT_IMPLEMENTED; +#else + nsCOMPtr<nsIAlertsDoNotDisturb> alertsDND(GetDNDBackend()); + NS_ENSURE_TRUE(alertsDND, NS_ERROR_NOT_IMPLEMENTED); + return alertsDND->GetManualDoNotDisturb(aRetVal); +#endif +} + +NS_IMETHODIMP nsAlertsService::SetManualDoNotDisturb(bool aDoNotDisturb) +{ +#ifdef MOZ_WIDGET_ANDROID + return NS_ERROR_NOT_IMPLEMENTED; +#else + nsCOMPtr<nsIAlertsDoNotDisturb> alertsDND(GetDNDBackend()); + NS_ENSURE_TRUE(alertsDND, NS_ERROR_NOT_IMPLEMENTED); + + nsresult rv = alertsDND->SetManualDoNotDisturb(aDoNotDisturb); + if (NS_SUCCEEDED(rv)) { + Telemetry::Accumulate(Telemetry::ALERTS_SERVICE_DND_ENABLED, 1); + } + return rv; +#endif +} + +already_AddRefed<nsIAlertsDoNotDisturb> +nsAlertsService::GetDNDBackend() +{ + // Try the system notification service. + nsCOMPtr<nsIAlertsService> backend = mBackend; + if (!backend) { + backend = nsXULAlerts::GetInstance(); + } + + nsCOMPtr<nsIAlertsDoNotDisturb> alertsDND(do_QueryInterface(backend)); + return alertsDND.forget(); +} diff --git a/toolkit/components/alerts/nsAlertsService.h b/toolkit/components/alerts/nsAlertsService.h new file mode 100644 index 000000000..3f23eaabf --- /dev/null +++ b/toolkit/components/alerts/nsAlertsService.h @@ -0,0 +1,48 @@ +// /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAlertsService_h__ +#define nsAlertsService_h__ + +#include "nsIAlertsService.h" +#include "nsCOMPtr.h" +#include "nsXULAlerts.h" + +#ifdef XP_WIN +typedef enum tagMOZ_QUERY_USER_NOTIFICATION_STATE { + QUNS_NOT_PRESENT = 1, + QUNS_BUSY = 2, + QUNS_RUNNING_D3D_FULL_SCREEN = 3, + QUNS_PRESENTATION_MODE = 4, + QUNS_ACCEPTS_NOTIFICATIONS = 5, + QUNS_QUIET_TIME = 6, + QUNS_IMMERSIVE = 7 +} MOZ_QUERY_USER_NOTIFICATION_STATE; + +extern "C" { +// This function is Windows Vista or later +typedef HRESULT (__stdcall *SHQueryUserNotificationStatePtr)(MOZ_QUERY_USER_NOTIFICATION_STATE *pquns); +} +#endif // defined(XP_WIN) + +class nsAlertsService : public nsIAlertsService, + public nsIAlertsDoNotDisturb +{ +public: + NS_DECL_NSIALERTSDONOTDISTURB + NS_DECL_NSIALERTSSERVICE + NS_DECL_ISUPPORTS + + nsAlertsService(); + +protected: + virtual ~nsAlertsService(); + + bool ShouldShowAlert(); + already_AddRefed<nsIAlertsDoNotDisturb> GetDNDBackend(); + nsCOMPtr<nsIAlertsService> mBackend; +}; + +#endif /* nsAlertsService_h__ */ diff --git a/toolkit/components/alerts/nsAlertsUtils.cpp b/toolkit/components/alerts/nsAlertsUtils.cpp new file mode 100644 index 000000000..5f7d92d2a --- /dev/null +++ b/toolkit/components/alerts/nsAlertsUtils.cpp @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsAlertsUtils.h" + +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIStringBundle.h" +#include "nsIURI.h" +#include "nsXPIDLString.h" + +/* static */ +bool +nsAlertsUtils::IsActionablePrincipal(nsIPrincipal* aPrincipal) +{ + return aPrincipal && + !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal) && + !aPrincipal->GetIsNullPrincipal(); +} + +/* static */ +void +nsAlertsUtils::GetSourceHostPort(nsIPrincipal* aPrincipal, + nsAString& aHostPort) +{ + if (!IsActionablePrincipal(aPrincipal)) { + return; + } + nsCOMPtr<nsIURI> principalURI; + if (NS_WARN_IF(NS_FAILED( + aPrincipal->GetURI(getter_AddRefs(principalURI))))) { + return; + } + if (!principalURI) { + return; + } + nsAutoCString hostPort; + if (NS_WARN_IF(NS_FAILED(principalURI->GetHostPort(hostPort)))) { + return; + } + CopyUTF8toUTF16(hostPort, aHostPort); +} diff --git a/toolkit/components/alerts/nsAlertsUtils.h b/toolkit/components/alerts/nsAlertsUtils.h new file mode 100644 index 000000000..bc11f6351 --- /dev/null +++ b/toolkit/components/alerts/nsAlertsUtils.h @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAlertsUtils_h +#define nsAlertsUtils_h + +#include "nsIPrincipal.h" +#include "nsString.h" + +class nsAlertsUtils final +{ +private: + nsAlertsUtils() = delete; + +public: + /** + * Indicates whether an alert from |aPrincipal| should include the source + * string and action buttons. Returns false if |aPrincipal| is |nullptr|, or + * a system, expanded, or null principal. + */ + static bool + IsActionablePrincipal(nsIPrincipal* aPrincipal); + + /** + * Sets |aHostPort| to the host and port from |aPrincipal|'s URI, or an + * empty string if |aPrincipal| is not actionable. + */ + static void + GetSourceHostPort(nsIPrincipal* aPrincipal, nsAString& aHostPort); +}; +#endif /* nsAlertsUtils_h */ diff --git a/toolkit/components/alerts/nsIAlertsService.idl b/toolkit/components/alerts/nsIAlertsService.idl new file mode 100644 index 000000000..5629dabc9 --- /dev/null +++ b/toolkit/components/alerts/nsIAlertsService.idl @@ -0,0 +1,259 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIObserver.idl" + +interface imgIRequest; +interface nsICancelable; +interface nsIPrincipal; +interface nsIURI; + +%{C++ +#define ALERT_NOTIFICATION_CONTRACTID "@mozilla.org/alert-notification;1" +%} + +[scriptable, uuid(a71a637d-de1d-47c6-a8d2-c60b2596f471)] +interface nsIAlertNotificationImageListener : nsISupports +{ + /** + * Called when the image finishes loading. + * + * @param aUserData An opaque parameter passed to |loadImage|. + * @param aRequest The image request. + */ + void onImageReady(in nsISupports aUserData, in imgIRequest aRequest); + + /** + * Called if the alert doesn't have an image, or if the image request times + * out or fails. + * + * @param aUserData An opaque parameter passed to |loadImage|. + */ + void onImageMissing(in nsISupports aUserData); +}; + +[scriptable, uuid(cf2e4cb6-4b8f-4eca-aea9-d51a8f9f7a50)] +interface nsIAlertNotification : nsISupports +{ + /** Initializes an alert notification. */ + void init([optional] in AString aName, + [optional] in AString aImageURL, + [optional] in AString aTitle, + [optional] in AString aText, + [optional] in boolean aTextClickable, + [optional] in AString aCookie, + [optional] in AString aDir, + [optional] in AString aLang, + [optional] in AString aData, + [optional] in nsIPrincipal aPrincipal, + [optional] in boolean aInPrivateBrowsing, + [optional] in boolean aRequireInteraction); + + /** + * The name of the notification. On Android, the name is hashed and used as + * a notification ID. Notifications will replace previous notifications with + * the same name. + */ + readonly attribute AString name; + + /** + * A URL identifying the image to put in the alert. The OS X backend limits + * the amount of time it will wait for the image to load to six seconds. After + * that time, the alert will show without an image. + */ + readonly attribute AString imageURL; + + /** The title for the alert. */ + readonly attribute AString title; + + /** The contents of the alert. */ + readonly attribute AString text; + + /** + * Controls the click behavior. If true, the alert listener will be notified + * when the user clicks on the alert. + */ + readonly attribute boolean textClickable; + + /** + * An opaque cookie that will be passed to the alert listener for each + * callback. + */ + readonly attribute AString cookie; + + /** + * Bidi override for the title and contents. Valid values are "auto", "ltr", + * or "rtl". Ignored if the backend doesn't support localization. + */ + readonly attribute AString dir; + + /** + * Language of the title and text. Ignored if the backend doesn't support + * localization. + */ + readonly attribute AString lang; + + /** + * A Base64-encoded structured clone buffer containing data associated with + * this alert. Only used for web notifications. Chrome callers should use a + * cookie instead. + */ + readonly attribute AString data; + + /** + * The principal of the page that created the alert. Used for IPC security + * checks, and to determine whether the alert is actionable. + */ + readonly attribute nsIPrincipal principal; + + /** + * The URI of the page that created the alert. |null| if the alert is not + * actionable. + */ + readonly attribute nsIURI URI; + + /** + * Controls the image loading behavior. If true, the image request will be + * loaded anonymously (without cookies or authorization tokens). + */ + readonly attribute boolean inPrivateBrowsing; + + /** + * Indicates that the notification should remain readily available until + * the user activates or dismisses the notification. + */ + readonly attribute boolean requireInteraction; + + /** + * Indicates whether this alert should show the source string and action + * buttons. False for system alerts (which can omit the principal), or + * expanded, system, and null principals. + */ + readonly attribute boolean actionable; + + /** + * The host and port of the originating page, or an empty string if the alert + * is not actionable. + */ + readonly attribute AString source; + + /** + * Loads the image associated with this alert. + * + * @param aTimeout The number of milliseconds to wait before cancelling the + * image request. If zero, there is no timeout. + * @param aListener An |nsIAlertNotificationImageListener| implementation, + * notified when the image loads. The listener is kept alive + * until the request completes. + * @param aUserData An opaque parameter passed to the listener's methods. + * Not used by the libnotify backend, but the OS X backend + * passes the pending notification. + */ + nsICancelable loadImage(in unsigned long aTimeout, + in nsIAlertNotificationImageListener aListener, + [optional] in nsISupports aUserData); +}; + +[scriptable, uuid(f7a36392-d98b-4141-a7d7-4e46642684e3)] +interface nsIAlertsService : nsISupports +{ + void showPersistentNotification(in AString aPersistentData, + in nsIAlertNotification aAlert, + [optional] in nsIObserver aAlertListener); + + void showAlert(in nsIAlertNotification aAlert, + [optional] in nsIObserver aAlertListener); + /** + * Initializes and shows an |nsIAlertNotification| with the given parameters. + * + * @param aAlertListener Used for callbacks. May be null if the caller + * doesn't care about callbacks. + * @see nsIAlertNotification for descriptions of all other parameters. + * @throws NS_ERROR_NOT_AVAILABLE If the notification cannot be displayed. + * + * The following arguments will be passed to the alertListener's observe() + * method: + * subject - null + * topic - "alertfinished" when the alert goes away + * "alertdisablecallback" when alerts should be disabled for the principal + * "alertsettingscallback" when alert settings should be opened + * "alertclickcallback" when the text is clicked + * "alertshow" when the alert is shown + * data - the value of the cookie parameter passed to showAlertNotification. + * + * @note Depending on current circumstances (if the user's in a fullscreen + * application, for instance), the alert might not be displayed at all. + * In that case, if an alert listener is passed in it will receive the + * "alertfinished" notification immediately. + */ + void showAlertNotification(in AString aImageURL, + in AString aTitle, + in AString aText, + [optional] in boolean aTextClickable, + [optional] in AString aCookie, + [optional] in nsIObserver aAlertListener, + [optional] in AString aName, + [optional] in AString aDir, + [optional] in AString aLang, + [optional] in AString aData, + [optional] in nsIPrincipal aPrincipal, + [optional] in boolean aInPrivateBrowsing, + [optional] in boolean aRequireInteraction); + + /** + * Close alerts created by the service. + * + * @param aName The name of the notification to close. If no name + * is provided then only a notification created with + * no name (if any) will be closed. + */ + void closeAlert([optional] in AString aName, + [optional] in nsIPrincipal aPrincipal); + +}; + +[scriptable, uuid(c5d63e3a-259d-45a8-b964-8377967cb4d2)] +interface nsIAlertsDoNotDisturb : nsISupports +{ + /** + * Toggles a manual Do Not Disturb mode for the service to reduce the amount + * of disruption that alerts cause the user. + * This may mean only displaying them in a notification tray/center or not + * displaying them at all. If a system backend already supports a similar + * feature controlled by the user, enabling this may not have any impact on + * code to show an alert. e.g. on OS X, the system will take care not + * disrupting a user if we simply create a notification like usual. + */ + attribute bool manualDoNotDisturb; +}; + +[scriptable, uuid(fc6d7f0a-0cf6-4268-8c71-ab640842b9b1)] +interface nsIAlertsIconData : nsISupports +{ + /** + * Shows an alert with an icon. Web notifications use the favicon of the + * page that created the alert. If the favicon is not in the Places database, + * |aIconSize| will be zero. + */ + void showAlertWithIconData(in nsIAlertNotification aAlert, + [optional] in nsIObserver aAlertListener, + [optional] in uint32_t aIconSize, + [const, array, size_is(aIconSize)] in uint8_t + aIconData); +}; + +[scriptable, uuid(f3c82915-bf60-41ea-91ce-6c46b22e381a)] +interface nsIAlertsIconURI : nsISupports +{ + /** + * Shows an alert with an icon URI. Web notifications use |moz-anno:| + * URIs to reference favicons from Places. If the page doesn't have a + * favicon, |aIconURI| will be |null|. + */ + void showAlertWithIconURI(in nsIAlertNotification aAlert, + [optional] in nsIObserver aAlertListener, + [optional] in nsIURI aIconURI); +}; diff --git a/toolkit/components/alerts/nsXULAlerts.cpp b/toolkit/components/alerts/nsXULAlerts.cpp new file mode 100644 index 000000000..882617637 --- /dev/null +++ b/toolkit/components/alerts/nsXULAlerts.cpp @@ -0,0 +1,398 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsXULAlerts.h" + +#include "nsArray.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/dom/Notification.h" +#include "mozilla/Unused.h" +#include "nsIServiceManager.h" +#include "nsISupportsPrimitives.h" +#include "nsPIDOMWindow.h" +#include "nsIWindowWatcher.h" + +using namespace mozilla; +using mozilla::dom::NotificationTelemetryService; + +#define ALERT_CHROME_URL "chrome://global/content/alerts/alert.xul" + +namespace { +StaticRefPtr<nsXULAlerts> gXULAlerts; +} // anonymous namespace + +NS_IMPL_CYCLE_COLLECTION(nsXULAlertObserver, mAlertWindow) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULAlertObserver) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULAlertObserver) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULAlertObserver) + +NS_IMETHODIMP +nsXULAlertObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + if (!strcmp("alertfinished", aTopic)) { + mozIDOMWindowProxy* currentAlert = mXULAlerts->mNamedWindows.GetWeak(mAlertName); + // The window in mNamedWindows might be a replacement, thus it should only + // be removed if it is the same window that is associated with this listener. + if (currentAlert == mAlertWindow) { + mXULAlerts->mNamedWindows.Remove(mAlertName); + + if (mIsPersistent) { + mXULAlerts->PersistentAlertFinished(); + } + } + } + + nsresult rv = NS_OK; + if (mObserver) { + rv = mObserver->Observe(aSubject, aTopic, aData); + } + return rv; +} + +// We don't cycle collect nsXULAlerts since gXULAlerts will keep the instance +// alive till shutdown anyway. +NS_IMPL_ISUPPORTS(nsXULAlerts, nsIAlertsService, nsIAlertsDoNotDisturb, nsIAlertsIconURI) + +/* static */ already_AddRefed<nsXULAlerts> +nsXULAlerts::GetInstance() +{ + // Gecko on Android does not fully support XUL windows. +#ifndef MOZ_WIDGET_ANDROID + if (!gXULAlerts) { + gXULAlerts = new nsXULAlerts(); + ClearOnShutdown(&gXULAlerts); + } +#endif // MOZ_WIDGET_ANDROID + RefPtr<nsXULAlerts> instance = gXULAlerts.get(); + return instance.forget(); +} + +void +nsXULAlerts::PersistentAlertFinished() +{ + MOZ_ASSERT(mPersistentAlertCount); + mPersistentAlertCount--; + + // Show next pending persistent alert if any. + if (!mPendingPersistentAlerts.IsEmpty()) { + ShowAlertWithIconURI(mPendingPersistentAlerts[0].mAlert, + mPendingPersistentAlerts[0].mListener, + nullptr); + mPendingPersistentAlerts.RemoveElementAt(0); + } +} + +NS_IMETHODIMP +nsXULAlerts::ShowAlertNotification(const nsAString& aImageUrl, const nsAString& aAlertTitle, + const nsAString& aAlertText, bool aAlertTextClickable, + const nsAString& aAlertCookie, nsIObserver* aAlertListener, + const nsAString& aAlertName, const nsAString& aBidi, + const nsAString& aLang, const nsAString& aData, + nsIPrincipal* aPrincipal, bool aInPrivateBrowsing, + bool aRequireInteraction) +{ + nsCOMPtr<nsIAlertNotification> alert = + do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); + NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE); + nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle, + aAlertText, aAlertTextClickable, + aAlertCookie, aBidi, aLang, aData, + aPrincipal, aInPrivateBrowsing, + aRequireInteraction); + NS_ENSURE_SUCCESS(rv, rv); + return ShowAlert(alert, aAlertListener); +} + +NS_IMETHODIMP +nsXULAlerts::ShowPersistentNotification(const nsAString& aPersistentData, + nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) +{ + return ShowAlert(aAlert, aAlertListener); +} + +NS_IMETHODIMP +nsXULAlerts::ShowAlert(nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) +{ + nsAutoString name; + nsresult rv = aAlert->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + + // If there is a pending alert with the same name in the list of + // pending alerts, replace it. + if (!mPendingPersistentAlerts.IsEmpty()) { + for (uint32_t i = 0; i < mPendingPersistentAlerts.Length(); i++) { + nsAutoString pendingAlertName; + nsCOMPtr<nsIAlertNotification> pendingAlert = mPendingPersistentAlerts[i].mAlert; + rv = pendingAlert->GetName(pendingAlertName); + NS_ENSURE_SUCCESS(rv, rv); + + if (pendingAlertName.Equals(name)) { + nsAutoString cookie; + rv = pendingAlert->GetCookie(cookie); + NS_ENSURE_SUCCESS(rv, rv); + + if (mPendingPersistentAlerts[i].mListener) { + rv = mPendingPersistentAlerts[i].mListener->Observe(nullptr, "alertfinished", cookie.get()); + NS_ENSURE_SUCCESS(rv, rv); + } + + mPendingPersistentAlerts[i].Init(aAlert, aAlertListener); + return NS_OK; + } + } + } + + bool requireInteraction; + rv = aAlert->GetRequireInteraction(&requireInteraction); + NS_ENSURE_SUCCESS(rv, rv); + + if (requireInteraction && + !mNamedWindows.Contains(name) && + static_cast<int32_t>(mPersistentAlertCount) >= + Preferences::GetInt("dom.webnotifications.requireinteraction.count", 0)) { + PendingAlert* pa = mPendingPersistentAlerts.AppendElement(); + pa->Init(aAlert, aAlertListener); + return NS_OK; + } else { + return ShowAlertWithIconURI(aAlert, aAlertListener, nullptr); + } +} + +NS_IMETHODIMP +nsXULAlerts::ShowAlertWithIconURI(nsIAlertNotification* aAlert, + nsIObserver* aAlertListener, + nsIURI* aIconURI) +{ + bool inPrivateBrowsing; + nsresult rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString cookie; + rv = aAlert->GetCookie(cookie); + NS_ENSURE_SUCCESS(rv, rv); + + if (mDoNotDisturb) { + if (!inPrivateBrowsing) { + RefPtr<NotificationTelemetryService> telemetry = + NotificationTelemetryService::GetInstance(); + if (telemetry) { + // Record the number of unique senders for XUL alerts. The OS X and + // libnotify backends will fire `alertshow` even if "do not disturb" + // is enabled. In that case, `NotificationObserver` will record the + // sender. + nsCOMPtr<nsIPrincipal> principal; + if (NS_SUCCEEDED(aAlert->GetPrincipal(getter_AddRefs(principal)))) { + Unused << NS_WARN_IF(NS_FAILED(telemetry->RecordSender(principal))); + } + } + } + if (aAlertListener) + aAlertListener->Observe(nullptr, "alertfinished", cookie.get()); + return NS_OK; + } + + nsAutoString name; + rv = aAlert->GetName(name); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString imageUrl; + rv = aAlert->GetImageURL(imageUrl); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString title; + rv = aAlert->GetTitle(title); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString text; + rv = aAlert->GetText(text); + NS_ENSURE_SUCCESS(rv, rv); + + bool textClickable; + rv = aAlert->GetTextClickable(&textClickable); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString bidi; + rv = aAlert->GetDir(bidi); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString lang; + rv = aAlert->GetLang(lang); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString source; + rv = aAlert->GetSource(source); + NS_ENSURE_SUCCESS(rv, rv); + + bool requireInteraction; + rv = aAlert->GetRequireInteraction(&requireInteraction); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIWindowWatcher> wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + + nsCOMPtr<nsIMutableArray> argsArray = nsArray::Create(); + + // create scriptable versions of our strings that we can store in our nsIMutableArray.... + nsCOMPtr<nsISupportsString> scriptableImageUrl (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableImageUrl, NS_ERROR_FAILURE); + + scriptableImageUrl->SetData(imageUrl); + rv = argsArray->AppendElement(scriptableImageUrl, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsString> scriptableAlertTitle (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableAlertTitle, NS_ERROR_FAILURE); + + scriptableAlertTitle->SetData(title); + rv = argsArray->AppendElement(scriptableAlertTitle, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsString> scriptableAlertText (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableAlertText, NS_ERROR_FAILURE); + + scriptableAlertText->SetData(text); + rv = argsArray->AppendElement(scriptableAlertText, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsPRBool> scriptableIsClickable (do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID)); + NS_ENSURE_TRUE(scriptableIsClickable, NS_ERROR_FAILURE); + + scriptableIsClickable->SetData(textClickable); + rv = argsArray->AppendElement(scriptableIsClickable, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsString> scriptableAlertCookie (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableAlertCookie, NS_ERROR_FAILURE); + + scriptableAlertCookie->SetData(cookie); + rv = argsArray->AppendElement(scriptableAlertCookie, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsPRInt32> scriptableOrigin (do_CreateInstance(NS_SUPPORTS_PRINT32_CONTRACTID)); + NS_ENSURE_TRUE(scriptableOrigin, NS_ERROR_FAILURE); + + int32_t origin = + LookAndFeel::GetInt(LookAndFeel::eIntID_AlertNotificationOrigin); + scriptableOrigin->SetData(origin); + + rv = argsArray->AppendElement(scriptableOrigin, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsString> scriptableBidi (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableBidi, NS_ERROR_FAILURE); + + scriptableBidi->SetData(bidi); + rv = argsArray->AppendElement(scriptableBidi, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsString> scriptableLang (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableLang, NS_ERROR_FAILURE); + + scriptableLang->SetData(lang); + rv = argsArray->AppendElement(scriptableLang, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsPRBool> scriptableRequireInteraction (do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID)); + NS_ENSURE_TRUE(scriptableRequireInteraction, NS_ERROR_FAILURE); + + scriptableRequireInteraction->SetData(requireInteraction); + rv = argsArray->AppendElement(scriptableRequireInteraction, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + // Alerts with the same name should replace the old alert in the same position. + // Provide the new alert window with a pointer to the replaced window so that + // it may take the same position. + nsCOMPtr<nsISupportsInterfacePointer> replacedWindow = do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv); + NS_ENSURE_TRUE(replacedWindow, NS_ERROR_FAILURE); + mozIDOMWindowProxy* previousAlert = mNamedWindows.GetWeak(name); + replacedWindow->SetData(previousAlert); + replacedWindow->SetDataIID(&NS_GET_IID(mozIDOMWindowProxy)); + rv = argsArray->AppendElement(replacedWindow, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + if (requireInteraction) { + mPersistentAlertCount++; + } + + // Add an observer (that wraps aAlertListener) to remove the window from + // mNamedWindows when it is closed. + nsCOMPtr<nsISupportsInterfacePointer> ifptr = do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr<nsXULAlertObserver> alertObserver = new nsXULAlertObserver(this, name, aAlertListener, requireInteraction); + nsCOMPtr<nsISupports> iSupports(do_QueryInterface(alertObserver)); + ifptr->SetData(iSupports); + ifptr->SetDataIID(&NS_GET_IID(nsIObserver)); + rv = argsArray->AppendElement(ifptr, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + // The source contains the host and port of the site that sent the + // notification. It is empty for system alerts. + nsCOMPtr<nsISupportsString> scriptableAlertSource (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableAlertSource, NS_ERROR_FAILURE); + scriptableAlertSource->SetData(source); + rv = argsArray->AppendElement(scriptableAlertSource, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsCString> scriptableIconURL (do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableIconURL, NS_ERROR_FAILURE); + if (aIconURI) { + nsAutoCString iconURL; + rv = aIconURI->GetSpec(iconURL); + NS_ENSURE_SUCCESS(rv, rv); + scriptableIconURL->SetData(iconURL); + } + rv = argsArray->AppendElement(scriptableIconURL, /*weak =*/ false); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozIDOMWindowProxy> newWindow; + nsAutoCString features("chrome,dialog=yes,titlebar=no,popup=yes"); + if (inPrivateBrowsing) { + features.AppendLiteral(",private"); + } + rv = wwatch->OpenWindow(nullptr, ALERT_CHROME_URL, "_blank", features.get(), + argsArray, getter_AddRefs(newWindow)); + NS_ENSURE_SUCCESS(rv, rv); + + mNamedWindows.Put(name, newWindow); + alertObserver->SetAlertWindow(newWindow); + + return NS_OK; +} + +NS_IMETHODIMP +nsXULAlerts::SetManualDoNotDisturb(bool aDoNotDisturb) +{ + mDoNotDisturb = aDoNotDisturb; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAlerts::GetManualDoNotDisturb(bool* aRetVal) +{ + *aRetVal = mDoNotDisturb; + return NS_OK; +} + +NS_IMETHODIMP +nsXULAlerts::CloseAlert(const nsAString& aAlertName, + nsIPrincipal* aPrincipal) +{ + mozIDOMWindowProxy* alert = mNamedWindows.GetWeak(aAlertName); + if (nsCOMPtr<nsPIDOMWindowOuter> domWindow = nsPIDOMWindowOuter::From(alert)) { + domWindow->DispatchCustomEvent(NS_LITERAL_STRING("XULAlertClose")); + } + return NS_OK; +} + diff --git a/toolkit/components/alerts/nsXULAlerts.h b/toolkit/components/alerts/nsXULAlerts.h new file mode 100644 index 000000000..557716ee6 --- /dev/null +++ b/toolkit/components/alerts/nsXULAlerts.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsXULAlerts_h__ +#define nsXULAlerts_h__ + +#include "nsCycleCollectionParticipant.h" +#include "nsDataHashtable.h" +#include "nsHashKeys.h" +#include "nsInterfaceHashtable.h" + +#include "mozIDOMWindow.h" +#include "nsIObserver.h" + +struct PendingAlert +{ + void Init(nsIAlertNotification* aAlert, nsIObserver* aListener) + { + mAlert = aAlert; + mListener = aListener; + } + nsCOMPtr<nsIAlertNotification> mAlert; + nsCOMPtr<nsIObserver> mListener; +}; + +class nsXULAlerts : public nsIAlertsService, + public nsIAlertsDoNotDisturb, + public nsIAlertsIconURI +{ + friend class nsXULAlertObserver; +public: + NS_DECL_NSIALERTSICONURI + NS_DECL_NSIALERTSDONOTDISTURB + NS_DECL_NSIALERTSSERVICE + NS_DECL_ISUPPORTS + + nsXULAlerts() + { + } + + static already_AddRefed<nsXULAlerts> GetInstance(); + +protected: + virtual ~nsXULAlerts() {} + void PersistentAlertFinished(); + + nsInterfaceHashtable<nsStringHashKey, mozIDOMWindowProxy> mNamedWindows; + uint32_t mPersistentAlertCount = 0; + nsTArray<PendingAlert> mPendingPersistentAlerts; + bool mDoNotDisturb = false; +}; + +/** + * This class wraps observers for alerts and watches + * for the "alertfinished" event in order to release + * the reference on the nsIDOMWindow of the XUL alert. + */ +class nsXULAlertObserver : public nsIObserver { +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_CYCLE_COLLECTION_CLASS(nsXULAlertObserver) + + nsXULAlertObserver(nsXULAlerts* aXULAlerts, const nsAString& aAlertName, + nsIObserver* aObserver, bool aIsPersistent) + : mXULAlerts(aXULAlerts), mAlertName(aAlertName), + mObserver(aObserver), mIsPersistent(aIsPersistent) {} + + void SetAlertWindow(mozIDOMWindowProxy* aWindow) { mAlertWindow = aWindow; } + +protected: + virtual ~nsXULAlertObserver() {} + + RefPtr<nsXULAlerts> mXULAlerts; + nsString mAlertName; + nsCOMPtr<mozIDOMWindowProxy> mAlertWindow; + nsCOMPtr<nsIObserver> mObserver; + bool mIsPersistent; +}; + +#endif /* nsXULAlerts_h__ */ + diff --git a/toolkit/components/alerts/resources/content/alert.css b/toolkit/components/alerts/resources/content/alert.css new file mode 100644 index 000000000..c4d94a543 --- /dev/null +++ b/toolkit/components/alerts/resources/content/alert.css @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#alertBox[animate] { + animation-duration: 20s; + animation-fill-mode: both; + animation-name: alert-animation; +} + +#alertBox[animate]:not([clicked]):not([closing]):hover { + animation-play-state: paused; +} + +#alertBox:not([hasOrigin]) > box > #alertTextBox > #alertFooter, +#alertBox:not([hasIcon]) > box > #alertIcon, +#alertImage:not([src]) { + display: none; +} + +#alertTitleBox { + -moz-box-pack: center; + -moz-box-align: center; +} + +.alertText { + white-space: pre-wrap; +} + +@keyframes alert-animation { + to { + visibility: hidden; + } +} diff --git a/toolkit/components/alerts/resources/content/alert.js b/toolkit/components/alerts/resources/content/alert.js new file mode 100644 index 000000000..523ec378e --- /dev/null +++ b/toolkit/components/alerts/resources/content/alert.js @@ -0,0 +1,332 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +// Copied from nsILookAndFeel.h, see comments on eMetric_AlertNotificationOrigin +const NS_ALERT_HORIZONTAL = 1; +const NS_ALERT_LEFT = 2; +const NS_ALERT_TOP = 4; + +const WINDOW_MARGIN = AppConstants.platform == "win" ? 0 : 10; +const BODY_TEXT_LIMIT = 200; +const WINDOW_SHADOW_SPREAD = AppConstants.platform == "win" ? 10 : 0; + + +var gOrigin = 0; // Default value: alert from bottom right. +var gReplacedWindow = null; +var gAlertListener = null; +var gAlertTextClickable = false; +var gAlertCookie = ""; +var gIsReplaced = false; +var gRequireInteraction = false; + +function prefillAlertInfo() { + // unwrap all the args.... + // arguments[0] --> the image src url + // arguments[1] --> the alert title + // arguments[2] --> the alert text + // arguments[3] --> is the text clickable? + // arguments[4] --> the alert cookie to be passed back to the listener + // arguments[5] --> the alert origin reported by the look and feel + // arguments[6] --> bidi + // arguments[7] --> lang + // arguments[8] --> requires interaction + // arguments[9] --> replaced alert window (nsIDOMWindow) + // arguments[10] --> an optional callback listener (nsIObserver) + // arguments[11] -> the nsIURI.hostPort of the origin, optional + // arguments[12] -> the alert icon URL, optional + + switch (window.arguments.length) { + default: + case 13: { + if (window.arguments[12]) { + let alertBox = document.getElementById("alertBox"); + alertBox.setAttribute("hasIcon", true); + + let icon = document.getElementById("alertIcon"); + icon.src = window.arguments[12]; + } + } + case 12: { + if (window.arguments[11]) { + let alertBox = document.getElementById("alertBox"); + alertBox.setAttribute("hasOrigin", true); + + let hostPort = window.arguments[11]; + const ALERT_BUNDLE = Services.strings.createBundle( + "chrome://alerts/locale/alert.properties"); + const BRAND_BUNDLE = Services.strings.createBundle( + "chrome://branding/locale/brand.properties"); + const BRAND_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName"); + let label = document.getElementById("alertSourceLabel"); + label.setAttribute("value", + ALERT_BUNDLE.formatStringFromName("source.label", + [hostPort], + 1)); + let doNotDisturbMenuItem = document.getElementById("doNotDisturbMenuItem"); + doNotDisturbMenuItem.setAttribute("label", + ALERT_BUNDLE.formatStringFromName("doNotDisturb.label", + [BRAND_NAME], + 1)); + let disableForOrigin = document.getElementById("disableForOriginMenuItem"); + disableForOrigin.setAttribute("label", + ALERT_BUNDLE.formatStringFromName("webActions.disableForOrigin.label", + [hostPort], + 1)); + let openSettings = document.getElementById("openSettingsMenuItem"); + openSettings.setAttribute("label", + ALERT_BUNDLE.GetStringFromName("webActions.settings.label")); + } + } + case 11: + gAlertListener = window.arguments[10]; + case 10: + gReplacedWindow = window.arguments[9]; + case 9: + gRequireInteraction = window.arguments[8]; + case 8: + if (window.arguments[7]) { + document.getElementById("alertTitleLabel").setAttribute("lang", window.arguments[7]); + document.getElementById("alertTextLabel").setAttribute("lang", window.arguments[7]); + } + case 7: + if (window.arguments[6]) { + document.getElementById("alertNotification").style.direction = window.arguments[6]; + } + case 6: + gOrigin = window.arguments[5]; + case 5: + gAlertCookie = window.arguments[4]; + case 4: + gAlertTextClickable = window.arguments[3]; + if (gAlertTextClickable) { + document.getElementById("alertNotification").setAttribute("clickable", true); + document.getElementById("alertTextLabel").setAttribute("clickable", true); + } + case 3: + if (window.arguments[2]) { + document.getElementById("alertBox").setAttribute("hasBodyText", true); + let bodyText = window.arguments[2]; + let bodyTextLabel = document.getElementById("alertTextLabel"); + + if (bodyText.length > BODY_TEXT_LIMIT) { + bodyTextLabel.setAttribute("tooltiptext", bodyText); + + let ellipsis = "\u2026"; + try { + ellipsis = Services.prefs.getComplexValue("intl.ellipsis", + Ci.nsIPrefLocalizedString).data; + } catch (e) { } + + // Copied from nsContextMenu.js' formatSearchContextItem(). + // If the JS character after our truncation point is a trail surrogate, + // include it in the truncated string to avoid splitting a surrogate pair. + let truncLength = BODY_TEXT_LIMIT; + let truncChar = bodyText[BODY_TEXT_LIMIT].charCodeAt(0); + if (truncChar >= 0xDC00 && truncChar <= 0xDFFF) { + truncLength++; + } + + bodyText = bodyText.substring(0, truncLength) + + ellipsis; + } + bodyTextLabel.textContent = bodyText; + } + case 2: + document.getElementById("alertTitleLabel").setAttribute("value", window.arguments[1]); + case 1: + if (window.arguments[0]) { + document.getElementById("alertBox").setAttribute("hasImage", true); + document.getElementById("alertImage").setAttribute("src", window.arguments[0]); + } + case 0: + break; + } +} + +function onAlertLoad() { + const ALERT_DURATION_IMMEDIATE = 20000; + let alertTextBox = document.getElementById("alertTextBox"); + let alertImageBox = document.getElementById("alertImageBox"); + alertImageBox.style.minHeight = alertTextBox.scrollHeight + "px"; + + sizeToContent(); + + if (gReplacedWindow && !gReplacedWindow.closed) { + moveWindowToReplace(gReplacedWindow); + gReplacedWindow.gIsReplaced = true; + gReplacedWindow.close(); + } else { + moveWindowToEnd(); + } + + window.addEventListener("XULAlertClose", function() { window.close(); }); + + // If the require interaction flag is set, prevent auto-closing the notification. + if (!gRequireInteraction) { + if (Services.prefs.getBoolPref("alerts.disableSlidingEffect")) { + setTimeout(function() { window.close(); }, ALERT_DURATION_IMMEDIATE); + } else { + let alertBox = document.getElementById("alertBox"); + alertBox.addEventListener("animationend", function hideAlert(event) { + if (event.animationName == "alert-animation" || + event.animationName == "alert-clicked-animation" || + event.animationName == "alert-closing-animation") { + alertBox.removeEventListener("animationend", hideAlert, false); + window.close(); + } + }, false); + alertBox.setAttribute("animate", true); + } + } + + let alertSettings = document.getElementById("alertSettings"); + alertSettings.addEventListener("focus", onAlertSettingsFocus); + alertSettings.addEventListener("click", onAlertSettingsClick); + + let ev = new CustomEvent("AlertActive", {bubbles: true, cancelable: true}); + document.documentElement.dispatchEvent(ev); + + if (gAlertListener) { + gAlertListener.observe(null, "alertshow", gAlertCookie); + } +} + +function moveWindowToReplace(aReplacedAlert) { + let heightDelta = window.outerHeight - aReplacedAlert.outerHeight; + + // Move windows that come after the replaced alert if the height is different. + if (heightDelta != 0) { + let windows = Services.wm.getEnumerator("alert:alert"); + while (windows.hasMoreElements()) { + let alertWindow = windows.getNext(); + // boolean to determine if the alert window is after the replaced alert. + let alertIsAfter = gOrigin & NS_ALERT_TOP ? + alertWindow.screenY > aReplacedAlert.screenY : + aReplacedAlert.screenY > alertWindow.screenY; + if (alertIsAfter) { + // The new Y position of the window. + let adjustedY = gOrigin & NS_ALERT_TOP ? + alertWindow.screenY + heightDelta : + alertWindow.screenY - heightDelta; + alertWindow.moveTo(alertWindow.screenX, adjustedY); + } + } + } + + let adjustedY = gOrigin & NS_ALERT_TOP ? aReplacedAlert.screenY : + aReplacedAlert.screenY - heightDelta; + window.moveTo(aReplacedAlert.screenX, adjustedY); +} + +function moveWindowToEnd() { + // Determine position + let x = gOrigin & NS_ALERT_LEFT ? screen.availLeft : + screen.availLeft + screen.availWidth - window.outerWidth; + let y = gOrigin & NS_ALERT_TOP ? screen.availTop : + screen.availTop + screen.availHeight - window.outerHeight; + + // Position the window at the end of all alerts. + let windows = Services.wm.getEnumerator("alert:alert"); + while (windows.hasMoreElements()) { + let alertWindow = windows.getNext(); + if (alertWindow != window) { + if (gOrigin & NS_ALERT_TOP) { + y = Math.max(y, alertWindow.screenY + alertWindow.outerHeight - WINDOW_SHADOW_SPREAD); + } else { + y = Math.min(y, alertWindow.screenY - window.outerHeight + WINDOW_SHADOW_SPREAD); + } + } + } + + // Offset the alert by WINDOW_MARGIN pixels from the edge of the screen + y += gOrigin & NS_ALERT_TOP ? WINDOW_MARGIN : -WINDOW_MARGIN; + x += gOrigin & NS_ALERT_LEFT ? WINDOW_MARGIN : -WINDOW_MARGIN; + + window.moveTo(x, y); +} + +function onAlertBeforeUnload() { + if (!gIsReplaced) { + // Move other alert windows to fill the gap left by closing alert. + let heightDelta = window.outerHeight + WINDOW_MARGIN - WINDOW_SHADOW_SPREAD; + let windows = Services.wm.getEnumerator("alert:alert"); + while (windows.hasMoreElements()) { + let alertWindow = windows.getNext(); + if (alertWindow != window) { + if (gOrigin & NS_ALERT_TOP) { + if (alertWindow.screenY > window.screenY) { + alertWindow.moveTo(alertWindow.screenX, alertWindow.screenY - heightDelta); + } + } else if (window.screenY > alertWindow.screenY) { + alertWindow.moveTo(alertWindow.screenX, alertWindow.screenY + heightDelta); + } + } + } + } + + if (gAlertListener) { + gAlertListener.observe(null, "alertfinished", gAlertCookie); + } +} + +function onAlertClick() { + if (gAlertListener && gAlertTextClickable) { + gAlertListener.observe(null, "alertclickcallback", gAlertCookie); + } + + let alertBox = document.getElementById("alertBox"); + if (alertBox.getAttribute("animate") == "true") { + // Closed when the animation ends. + alertBox.setAttribute("clicked", "true"); + } else { + window.close(); + } +} + +function doNotDisturb() { + const alertService = Cc["@mozilla.org/alerts-service;1"] + .getService(Ci.nsIAlertsService) + .QueryInterface(Ci.nsIAlertsDoNotDisturb); + alertService.manualDoNotDisturb = true; + Services.telemetry.getHistogramById("WEB_NOTIFICATION_MENU") + .add(0); + onAlertClose(); +} + +function disableForOrigin() { + gAlertListener.observe(null, "alertdisablecallback", gAlertCookie); + onAlertClose(); +} + +function onAlertSettingsFocus(event) { + event.target.removeAttribute("focusedViaMouse"); +} + +function onAlertSettingsClick(event) { + // XXXjaws Hack used to remove the focus-ring only + // from mouse interaction, but focus-ring drawing + // should only be enabled when interacting via keyboard. + event.target.setAttribute("focusedViaMouse", true); + event.stopPropagation(); +} + +function openSettings() { + gAlertListener.observe(null, "alertsettingscallback", gAlertCookie); + onAlertClose(); +} + +function onAlertClose() { + let alertBox = document.getElementById("alertBox"); + if (alertBox.getAttribute("animate") == "true") { + // Closed when the animation ends. + alertBox.setAttribute("closing", "true"); + } else { + window.close(); + } +} diff --git a/toolkit/components/alerts/resources/content/alert.xul b/toolkit/components/alerts/resources/content/alert.xul new file mode 100644 index 000000000..8597d9954 --- /dev/null +++ b/toolkit/components/alerts/resources/content/alert.xul @@ -0,0 +1,67 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE window [ +<!ENTITY % alertDTD SYSTEM "chrome://alerts/locale/alert.dtd"> +%alertDTD; +]> + +<?xml-stylesheet href="chrome://global/content/alerts/alert.css" type="text/css"?> +<?xml-stylesheet href="chrome://global/skin/alerts/alert.css" type="text/css"?> + +<window id="alertNotification" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + windowtype="alert:alert" + xmlns:xhtml="http://www.w3.org/1999/xhtml" + role="alert" + pack="start" + onload="onAlertLoad();" + onclick="onAlertClick();" + onbeforeunload="onAlertBeforeUnload();"> + + <script type="application/javascript" src="chrome://global/content/alerts/alert.js"/> + + <vbox id="alertBox" class="alertBox"> + <box id="alertTitleBox"> + <image id="alertIcon"/> + <label id="alertTitleLabel" class="alertTitle plain" crop="end"/> + <vbox class="alertCloseBox"> + <toolbarbutton class="alertCloseButton close-icon" + tooltiptext="&closeAlert.tooltip;" + onclick="event.stopPropagation();" + oncommand="onAlertClose();"/> + </vbox> + </box> + <box> + <hbox id="alertImageBox" class="alertImageBox" align="center" pack="center"> + <image id="alertImage"/> + </hbox> + + <vbox id="alertTextBox" class="alertTextBox"> + <label id="alertTextLabel" class="alertText plain"/> + <spacer flex="1"/> + <box id="alertFooter"> + <label id="alertSourceLabel" class="alertSource plain"/> + <button type="menu" id="alertSettings" tooltiptext="&settings.label;"> + <menupopup position="after_end"> + <menuitem id="doNotDisturbMenuItem" + oncommand="doNotDisturb();"/> + <menuseparator/> + <menuitem id="disableForOriginMenuItem" + oncommand="disableForOrigin();"/> + <menuitem id="openSettingsMenuItem" + oncommand="openSettings();"/> + </menupopup> + </button> + </box> + </vbox> + </box> + </vbox> + + <!-- This method is called inline because we want to make sure we establish the width + and height of the alert before we fire the onload handler. --> + <script type="application/javascript">prefillAlertInfo();</script> +</window> + diff --git a/toolkit/components/alerts/test/.eslintrc.js b/toolkit/components/alerts/test/.eslintrc.js new file mode 100644 index 000000000..3c788d6d6 --- /dev/null +++ b/toolkit/components/alerts/test/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/mochitest.eslintrc.js" + ] +}; diff --git a/toolkit/components/alerts/test/image.gif b/toolkit/components/alerts/test/image.gif Binary files differnew file mode 100644 index 000000000..053b4d926 --- /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 000000000..430c3c5e6 --- /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 000000000..622052943 --- /dev/null +++ b/toolkit/components/alerts/test/image_server.sjs @@ -0,0 +1,82 @@ +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC } = Components; + +Cu.import("resource://gre/modules/Timer.jsm"); + +const LocalFile = CC("@mozilla.org/file/local;1", "nsILocalFile", + "initWithPath"); + +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", + "nsIFileInputStream", "init"); + +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", "setInputStream"); + +function handleRequest(request, response) { + let params = parseQueryString(request.queryString); + + response.setStatusLine(request.httpVersion, 200, "OK"); + + // Compare and increment a cookie for this request. This is used to test + // private browsing mode; the cookie should not be set if the image is + // loaded anonymously. + if (params.has("c")) { + let expectedValue = parseInt(params.get("c"), 10); + let actualValue = !request.hasHeader("Cookie") ? 0 : + parseInt(request.getHeader("Cookie") + .replace(/^counter=(\d+)/, "$1"), 10); + if (actualValue != expectedValue) { + response.setStatusLine(request.httpVersion, 400, "Wrong counter value"); + return; + } + response.setHeader("Set-Cookie", `counter=${expectedValue + 1}`, false); + } + + // Wait to send the image if a timeout is given. + let timeout = parseInt(params.get("t"), 10); + if (timeout > 0) { + response.processAsync(); + setTimeout(() => { + respond(params, request, response); + response.finish(); + }, timeout * 1000); + return; + } + + respond(params, request, response); +} + +function parseQueryString(queryString) { + return queryString.split("&").reduce((params, param) => { + let [key, value] = param.split("=", 2); + params.set(key, value); + return params; + }, new Map()); +} + +function respond(params, request, response) { + if (params.has("s")) { + let statusCode = parseInt(params.get("s"), 10); + response.setStatusLine(request.httpVersion, statusCode, "Custom status"); + return; + } + var filename = params.get("f"); + writeFile(filename, response); +} + +function writeFile(name, response) { + var file = new LocalFile(getState("__LOCATION__")).parent; + file.append(name); + + let mimeType = Cc["@mozilla.org/uriloader/external-helper-app-service;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(file); + + let fileStream = new FileInputStream(file, 1, 0, false); + let binaryStream = new BinaryInputStream(fileStream); + + response.setHeader("Content-Type", mimeType, false); + response.bodyOutputStream.writeFrom(binaryStream, binaryStream.available()); + + binaryStream.close(); + fileStream.close(); +} diff --git a/toolkit/components/alerts/test/mochitest.ini b/toolkit/components/alerts/test/mochitest.ini new file mode 100644 index 000000000..12e2a8704 --- /dev/null +++ b/toolkit/components/alerts/test/mochitest.ini @@ -0,0 +1,16 @@ +[DEFAULT] +support-files = + image.gif + image.png + image_server.sjs + +# Synchronous tests like test_alerts.html must come before +# asynchronous tests like test_alerts_noobserve.html! +[test_alerts.html] +skip-if = toolkit == 'android' +[test_alerts_noobserve.html] +[test_alerts_requireinteraction.html] +[test_image.html] +[test_multiple_alerts.html] +[test_principal.html] +skip-if = toolkit == 'android' diff --git a/toolkit/components/alerts/test/test_alerts.html b/toolkit/components/alerts/test/test_alerts.html new file mode 100644 index 000000000..cb087e48a --- /dev/null +++ b/toolkit/components/alerts/test/test_alerts.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test for Alerts Service</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> +<p id="display"></p> + +<br>Alerts service, with observer "synchronous" case. +<br> +<br>Did a notification appear anywhere? +<br>If so, the test will finish once the notification disappears. + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var observer = { + alertShow: false, + observe: function (aSubject, aTopic, aData) { + is(aData, "foobarcookie", "Checking whether the alert cookie was passed correctly"); + if (aTopic == "alertclickcallback") { + todo(false, "Did someone click the notification while running mochitests? (Please don't.)"); + } else if (aTopic == "alertshow") { + ok(!this.alertShow, "Alert should not be shown more than once"); + this.alertShow = true; + } else { + is(aTopic, "alertfinished", "Checking the topic for a finished notification"); + SimpleTest.finish(); + } + } +}; + +function runTest() { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + + if (!("@mozilla.org/alerts-service;1" in Cc)) { + todo(false, "Alerts service does not exist in this application"); + return; + } + + ok(true, "Alerts service exists in this application"); + + var notifier; + try { + notifier = Cc["@mozilla.org/alerts-service;1"]. + getService(Ci.nsIAlertsService); + ok(true, "Alerts service is available"); + } catch (ex) { + todo(false, + "Alerts service is not available.", ex); + return; + } + + try { + var alertName = "fiorello"; + SimpleTest.waitForExplicitFinish(); + notifier.showAlertNotification(null, "Notification test", + "Surprise! I'm here to test notifications!", + false, "foobarcookie", observer, alertName); + ok(true, "showAlertNotification() succeeded. Waiting for notification..."); + + if ("@mozilla.org/system-alerts-service;1" in Cc) { + // Notifications are native on OS X 10.8 and later, as well as GNOME + // Shell with libnotify (bug 1236036). These notifications persist in the + // Notification Center, and only fire the `alertfinished` event when + // closed. For platforms where native notifications may be used, we need + // to close explicitly to avoid a hang. This also works for XUL + // notifications when running this test on OS X < 10.8, or a window + // manager like Ubuntu Unity with incomplete libnotify support. + notifier.closeAlert(alertName); + } + } catch (ex) { + todo(false, "showAlertNotification() failed.", ex); + SimpleTest.finish(); + } +} + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/alerts/test/test_alerts_noobserve.html b/toolkit/components/alerts/test/test_alerts_noobserve.html new file mode 100644 index 000000000..0cc452b8a --- /dev/null +++ b/toolkit/components/alerts/test/test_alerts_noobserve.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<head> + <title>Test for Alerts Service</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> +<p id="display"></p> + +<br>Alerts service, without observer "asynchronous" case. +<br> +<br>A notification should soon appear somewhere. +<br>If there has been no crash when the notification (later) disappears, assume all is good. + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; + +const chromeScript = SpecialPowers.loadChromeScript(_ => { + const { utils: Cu } = Components; + + Cu.import("resource://gre/modules/Services.jsm"); + Cu.import("resource://gre/modules/Timer.jsm"); + + function anyXULAlertsVisible() { + var windows = Services.wm.getEnumerator("alert:alert"); + return windows.hasMoreElements(); + } + + addMessageListener("anyXULAlertsVisible", anyXULAlertsVisible); + + addMessageListener("waitForAlerts", function waitForAlerts() { + if (anyXULAlertsVisible()) { + setTimeout(waitForAlerts, 1000); + } else { + sendAsyncMessage("waitedForAlerts"); + } + }); +}); + +function waitForAlertsThenFinish() { + chromeScript.addMessageListener("waitedForAlerts", function waitedForAlerts() { + chromeScript.removeMessageListener("waitedForAlerts", waitedForAlerts); + ok(true, "Alert disappeared."); + SimpleTest.finish(); + }); + chromeScript.sendAsyncMessage("waitForAlerts"); +} + +function runTest() { + if (!("@mozilla.org/alerts-service;1" in Cc)) { + todo(false, "Alerts service does not exist in this application"); + } else { + ok(true, "Alerts service exists in this application"); + + var notifier; + try { + notifier = Cc["@mozilla.org/alerts-service;1"]. + getService(Ci.nsIAlertsService); + ok(true, "Alerts service is available"); + } catch (ex) { + todo(false, "Alerts service is not available.", ex); + } + + if (notifier) { + try { + notifier.showAlertNotification(null, "Notification test", + "This notification has no observer"); + ok(true, "showAlertNotification() succeeded"); + } catch (ex) { + todo(false, "showAlertNotification() failed.", ex); + } + } + } +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +// sendSyncMessage returns an array of arrays: the outer array is from the +// message manager, and the inner array is from the chrome script's listeners. +// See the comment in test_SpecialPowersLoadChromeScript.html. +var [[alertsVisible]] = chromeScript.sendSyncMessage("anyXULAlertsVisible"); +ok(!alertsVisible, "Alerts should not be present at the start of the test."); +runTest(); +setTimeout(waitForAlertsThenFinish, 1000); +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/alerts/test/test_alerts_requireinteraction.html b/toolkit/components/alerts/test/test_alerts_requireinteraction.html new file mode 100644 index 000000000..26fe87104 --- /dev/null +++ b/toolkit/components/alerts/test/test_alerts_requireinteraction.html @@ -0,0 +1,168 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for alerts with requireInteraction</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; + +const chromeScript = SpecialPowers.loadChromeScript(_ => { + const { utils: Cu } = Components; + + Cu.import("resource://gre/modules/Services.jsm"); + Cu.import("resource://gre/modules/Timer.jsm"); + + addMessageListener("waitForXULAlert", function() { + var timer = setTimeout(function() { + Services.ww.unregisterNotification(windowObserver); + sendAsyncMessage("waitForXULAlert", false); + }, 2000); + + var windowObserver = function(aSubject, aTopic, aData) { + if (aTopic != "domwindowopened") { + return; + } + + var win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + let windowType = win.document.documentElement.getAttribute("windowtype"); + if (windowType == "alert:alert") { + clearTimeout(timer); + Services.ww.unregisterNotification(windowObserver); + + sendAsyncMessage("waitForXULAlert", true); + } + }); + }; + + Services.ww.registerNotification(windowObserver); + }); +}); + +var cookie = 0; +function promiseCreateXULAlert(alertService, listener, name) { + return new Promise(resolve => { + chromeScript.addMessageListener("waitForXULAlert", function waitedForAlert(result) { + chromeScript.removeMessageListener("waitForXULAlert", waitedForAlert); + resolve(result); + }); + + chromeScript.sendAsyncMessage("waitForXULAlert"); + alertService.showAlertNotification(null, "title", "body", + true, cookie++, listener, name, null, null, null, + null, false, true); + }); +} + +add_task(function* test_require_interaction() { + if (!("@mozilla.org/alerts-service;1" in Cc)) { + todo(false, "Alerts service does not exist in this application."); + return; + } + + ok(true, "Alerts service exists in this application."); + + var alertService; + try { + alertService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); + ok(true, "Alerts service is available."); + } catch (ex) { + todo(false, "Alerts service is not available."); + return; + } + + yield SpecialPowers.pushPrefEnv({"set": [ + [ "dom.webnotifications.requireinteraction.enabled", true ], + [ "dom.webnotifications.requireinteraction.count", 2 ] + ]}); + + var expectedSequence = [ + "first show", + "second show", + "second finished", + "second replacement show", + "third finished", + "first finished", + "third replacement show", + "second replacement finished", + "third replacement finished" + ]; + + var actualSequence = []; + + function createAlertListener(name, showCallback, finishCallback) { + return (subject, topic, data) => { + if (topic == "alertshow") { + actualSequence.push(name + " show"); + if (showCallback) { + showCallback(); + } + } else if (topic == "alertfinished") { + actualSequence.push(name + " finished"); + if (finishCallback) { + finishCallback(); + } + } + } + } + + var xulAlertCreated = yield promiseCreateXULAlert(alertService, + createAlertListener("first"), "first"); + if (!xulAlertCreated) { + ok(true, "Platform does not use XUL alerts."); + alertService.closeAlert("first"); + return; + } + + xulAlertCreated = yield promiseCreateXULAlert(alertService, + createAlertListener("second"), "second"); + ok(xulAlertCreated, "Create XUL alert"); + + // Replace second alert + xulAlertCreated = yield promiseCreateXULAlert(alertService, + createAlertListener("second replacement"), "second"); + ok(xulAlertCreated, "Create XUL alert"); + + var testFinishResolve; + var testFinishPromise = new Promise((resolve) => { testFinishResolve = resolve; }); + + xulAlertCreated = yield promiseCreateXULAlert(alertService, + createAlertListener("third"), "third"), + ok(!xulAlertCreated, "XUL alert should not be visible"); + + // Replace the not-yet-visible third alert. + xulAlertCreated = yield promiseCreateXULAlert(alertService, + createAlertListener("third replacement", + function showCallback() { + alertService.closeAlert("second"); + alertService.closeAlert("third"); + }, + function finishCallback() { + // Check actual sequence of alert events compared to expected sequence. + for (var i = 0; i < actualSequence.length; i++) { + is(actualSequence[i], expectedSequence[i], + "Alert callback at index " + i + " should be in expected order."); + } + + testFinishResolve(); + }), "third"); + + ok(!xulAlertCreated, "XUL alert should not be visible"); + + alertService.closeAlert("first"); + + yield testFinishPromise; +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/alerts/test/test_image.html b/toolkit/components/alerts/test/test_image.html new file mode 100644 index 000000000..7bf89fab2 --- /dev/null +++ b/toolkit/components/alerts/test/test_image.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 1233086</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> +<p id="display"></p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; +const Services = SpecialPowers.Services; + +const imageServerURL = "http://mochi.test:8888/tests/toolkit/components/alerts/test/image_server.sjs"; + +function makeAlert(...params) { + var alert = Cc["@mozilla.org/alert-notification;1"] + .createInstance(Ci.nsIAlertNotification); + alert.init(...params); + return alert; +} + +function promiseImage(alert, timeout = 0, userData = null) { + return new Promise(resolve => { + var isDone = false; + function done(value) { + ok(!isDone, "Should call the image listener once"); + isDone = true; + resolve(value); + } + alert.loadImage(timeout, SpecialPowers.wrapCallbackObject({ + onImageReady(aUserData, aRequest) { + done([true, aRequest, aUserData]); + }, + onImageMissing(aUserData) { + done([false, aUserData]); + }, + }), SpecialPowers.wrap(userData)); + }); +} + +add_task(function* testContext() { + var inUserData = Cc["@mozilla.org/supports-PRInt64;1"] + .createInstance(Ci.nsISupportsPRInt64); + inUserData.data = 123; + + var alert = makeAlert(null, imageServerURL + "?f=image.png"); + var [ready, , userData] = yield promiseImage(alert, 0, inUserData); + ok(ready, "Should load requested image"); + is(userData.QueryInterface(Ci.nsISupportsPRInt64).data, 123, + "Should pass user data for loaded image"); + + alert = makeAlert(null, imageServerURL + "?s=404"); + [ready, userData] = yield promiseImage(alert, 0, inUserData); + ok(!ready, "Should not load missing image"); + is(userData.QueryInterface(Ci.nsISupportsPRInt64).data, 123, + "Should pass user data for missing image"); +}); + +add_task(function* testTimeout() { + var alert = makeAlert(null, imageServerURL + "?f=image.png&t=3"); + var [ready] = yield promiseImage(alert, 1000); + ok(!ready, "Should cancel request if timeout fires"); + + [ready, request] = yield promiseImage(alert, 45000); + ok(ready, "Should load image if request finishes before timeout"); +}); + +add_task(function* testAnimatedGIF() { + var alert = makeAlert(null, imageServerURL + "?f=image.gif"); + var [ready, request] = yield promiseImage(alert); + ok(ready, "Should load first animated GIF frame"); + is(request.mimeType, "image/gif", "Should report correct GIF MIME type"); + is(request.image.width, 256, "GIF width should be 256px"); + is(request.image.height, 256, "GIF height should be 256px"); +}); + +add_task(function* testCancel() { + var alert = makeAlert(null, imageServerURL + "?f=image.gif&t=180"); + yield new Promise((resolve, reject) => { + var request = alert.loadImage(0, SpecialPowers.wrapCallbackObject({ + onImageReady() { + reject(new Error("Should not load cancelled request")); + }, + onImageMissing() { + resolve(); + }, + }), null); + request.cancel(SpecialPowers.Cr.NS_BINDING_ABORTED); + }); +}); + +add_task(function* testMixedContent() { + // Loading principal is HTTPS; image URL is HTTP. + var origin = "https://mochi.test:8888"; + var principal = Services.scriptSecurityManager + .createCodebasePrincipalFromOrigin(origin); + + var alert = makeAlert(null, imageServerURL + "?f=image.png", + null, null, false, null, null, null, + null, principal); + var [ready, request] = yield promiseImage(alert); + ok(ready, "Should load cross-protocol image"); + is(request.mimeType, "image/png", "Should report correct MIME type"); + is(request.image.width, 32, "Width should be 32px"); + is(request.image.height, 32, "Height should be 32px"); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/alerts/test/test_multiple_alerts.html b/toolkit/components/alerts/test/test_multiple_alerts.html new file mode 100644 index 000000000..9d939b63a --- /dev/null +++ b/toolkit/components/alerts/test/test_multiple_alerts.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for multiple alerts</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; + +const chromeScript = SpecialPowers.loadChromeScript(_ => { + Components.utils.import("resource://gre/modules/Services.jsm"); + Components.utils.import("resource://gre/modules/Timer.jsm"); + + const alertService = Components.classes["@mozilla.org/alerts-service;1"] + .getService(Components.interfaces.nsIAlertsService); + + addMessageListener("waitForPosition", function() { + var timer = setTimeout(function() { + Services.ww.unregisterNotification(windowObserver); + sendAsyncMessage("waitedForPosition", null); + }, 2000); + + var windowObserver = function(aSubject, aTopic, aData) { + if (aTopic != "domwindowopened") { + return; + } + + // Alerts are implemented using XUL. + clearTimeout(timer); + + Services.ww.unregisterNotification(windowObserver); + + var win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow); + win.addEventListener("pageshow", function onPageShow() { + win.removeEventListener("pageshow", onPageShow, false); + + var x = win.screenX; + var y = win.screenY; + + win.addEventListener("pagehide", function onPageHide() { + win.removeEventListener("pagehide", onPageHide, false); + sendAsyncMessage("waitedForPosition", { x, y }); + }, false); + + alertService.closeAlert(); + }, false); + }; + + Services.ww.registerNotification(windowObserver); + }); +}); + +function promiseAlertPosition(alertService) { + return new Promise(resolve => { + chromeScript.addMessageListener("waitedForPosition", function waitedForPosition(result) { + chromeScript.removeMessageListener("waitedForPosition", waitedForPosition); + resolve(result); + }); + chromeScript.sendAsyncMessage("waitForPosition"); + + alertService.showAlertNotification(null, "title", "body"); + ok(true, "Alert shown."); + }); +} + +add_task(function* test_multiple_alerts() { + if (!("@mozilla.org/alerts-service;1" in Cc)) { + todo(false, "Alerts service does not exist in this application."); + return; + } + + ok(true, "Alerts service exists in this application."); + + var alertService; + try { + alertService = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService); + ok(true, "Alerts service is available."); + } catch (ex) { + todo(false, "Alerts service is not available."); + return; + } + + var firstAlertPosition = yield promiseAlertPosition(alertService); + if (!firstAlertPosition) { + ok(true, "Platform does not use XUL alerts."); + return; + } + + var secondAlertPosition = yield promiseAlertPosition(alertService); + is(secondAlertPosition.x, firstAlertPosition.x, "Second alert should be opened in the same position."); + is(secondAlertPosition.y, firstAlertPosition.y, "Second alert should be opened in the same position."); +}); + +</script> +</pre> +</body> +</html> diff --git a/toolkit/components/alerts/test/test_principal.html b/toolkit/components/alerts/test/test_principal.html new file mode 100644 index 000000000..74a20dbd7 --- /dev/null +++ b/toolkit/components/alerts/test/test_principal.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 1202933</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> +<p id="display"></p> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; +const Services = SpecialPowers.Services; + +const notifier = Cc["@mozilla.org/alerts-service;1"] + .getService(Ci.nsIAlertsService); + +const chromeScript = SpecialPowers.loadChromeScript(_ => { + Components.utils.import("resource://gre/modules/Services.jsm"); + + addMessageListener("anyXULAlertsVisible", function() { + var windows = Services.wm.getEnumerator("alert:alert"); + return windows.hasMoreElements(); + }); + + addMessageListener("getAlertSource", function() { + var alertWindows = Services.wm.getEnumerator("alert:alert"); + if (!alertWindows) { + return null; + } + var alertWindow = alertWindows.getNext(); + return alertWindow.document.getElementById("alertSourceLabel").getAttribute("value"); + }); +}); + +function notify(alertName, principal) { + return new Promise((resolve, reject) => { + var source; + function observe(subject, topic, data) { + if (topic == "alertclickcallback") { + reject(new Error("Alerts should not be clicked during test")); + } else if (topic == "alertshow") { + source = chromeScript.sendSyncMessage("getAlertSource")[0][0]; + notifier.closeAlert(alertName); + } else { + is(topic, "alertfinished", "Should hide alert"); + resolve(source); + } + } + notifier.showAlertNotification(null, "Notification test", + "Surprise! I'm here to test notifications!", + false, alertName, observe, alertName, + null, null, null, principal); + if (SpecialPowers.Services.appinfo.OS == "Darwin") { + notifier.closeAlert(alertName); + } + }); +} + +function* testNoPrincipal() { + var source = yield notify("noPrincipal", null); + ok(!source, "Should omit source without principal"); +} + +function* testSystemPrincipal() { + var principal = Services.scriptSecurityManager.getSystemPrincipal(); + var source = yield notify("systemPrincipal", principal); + ok(!source, "Should omit source for system principal"); +} + +function* testNullPrincipal() { + var principal = Services.scriptSecurityManager.createNullPrincipal({}); + var source = yield notify("nullPrincipal", principal); + ok(!source, "Should omit source for null principal"); +} + +function* testNodePrincipal() { + var principal = SpecialPowers.wrap(document).nodePrincipal; + var source = yield notify("nodePrincipal", principal); + + var stringBundle = Services.strings.createBundle( + "chrome://alerts/locale/alert.properties" + ); + var localizedSource = stringBundle.formatStringFromName( + "source.label", [principal.URI.hostPort], 1); + is(source, localizedSource, "Should include source for node principal"); +} + +function runTest() { + if (!("@mozilla.org/alerts-service;1" in Cc)) { + todo(false, "Alerts service does not exist in this application"); + return; + } + + if ("@mozilla.org/system-alerts-service;1" in Cc) { + todo(false, "Native alerts service exists in this application"); + return; + } + + ok(true, "Alerts service exists in this application"); + + // sendSyncMessage returns an array of arrays. See the comments in + // test_alerts_noobserve.html and test_SpecialPowersLoadChromeScript.html. + var [[alertsVisible]] = chromeScript.sendSyncMessage("anyXULAlertsVisible"); + ok(!alertsVisible, "Alerts should not be present at the start of the test."); + + add_task(testNoPrincipal); + add_task(testSystemPrincipal); + add_task(testNullPrincipal); + add_task(testNodePrincipal); +} + +runTest(); +</script> +</pre> +</body> +</html> |