/* -*- 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 gXULAlerts; } // anonymous namespace NS_IMPL_CYCLE_COLLECTION(nsXULAlertObserver, mAlertWindow) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsXULAlertObserver) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(nsXULAlertObserver) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsXULAlertObserver) NS_IMETHODIMP nsXULAlertObserver::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (!strcmp("alertfinished", aTopic)) { mozIDOMWindowProxy* currentAlert = mXULAlerts->mNamedWindows.GetWeak(mAlertName); // The window in mNamedWindows might be a replacement, thus it should only // be removed if it is the same window that is associated with this listener. if (currentAlert == mAlertWindow) { mXULAlerts->mNamedWindows.Remove(mAlertName); if (mIsPersistent) { mXULAlerts->PersistentAlertFinished(); } } } nsresult rv = NS_OK; if (mObserver) { rv = mObserver->Observe(aSubject, aTopic, aData); } return rv; } // We don't cycle collect nsXULAlerts since gXULAlerts will keep the instance // alive till shutdown anyway. NS_IMPL_ISUPPORTS(nsXULAlerts, nsIAlertsService, nsIAlertsDoNotDisturb, nsIAlertsIconURI) /* static */ already_AddRefed nsXULAlerts::GetInstance() { // Gecko on Android does not fully support XUL windows. #ifndef MOZ_WIDGET_ANDROID if (!gXULAlerts) { gXULAlerts = new nsXULAlerts(); ClearOnShutdown(&gXULAlerts); } #endif // MOZ_WIDGET_ANDROID RefPtr instance = gXULAlerts.get(); return instance.forget(); } void nsXULAlerts::PersistentAlertFinished() { MOZ_ASSERT(mPersistentAlertCount); mPersistentAlertCount--; // Show next pending persistent alert if any. if (!mPendingPersistentAlerts.IsEmpty()) { ShowAlertWithIconURI(mPendingPersistentAlerts[0].mAlert, mPendingPersistentAlerts[0].mListener, nullptr); mPendingPersistentAlerts.RemoveElementAt(0); } } NS_IMETHODIMP nsXULAlerts::ShowAlertNotification(const nsAString& aImageUrl, const nsAString& aAlertTitle, const nsAString& aAlertText, bool aAlertTextClickable, const nsAString& aAlertCookie, nsIObserver* aAlertListener, const nsAString& aAlertName, const nsAString& aBidi, const nsAString& aLang, const nsAString& aData, nsIPrincipal* aPrincipal, bool aInPrivateBrowsing, bool aRequireInteraction) { nsCOMPtr alert = do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID); NS_ENSURE_TRUE(alert, NS_ERROR_FAILURE); 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 pendingAlert = mPendingPersistentAlerts[i].mAlert; rv = pendingAlert->GetName(pendingAlertName); NS_ENSURE_SUCCESS(rv, rv); if (pendingAlertName.Equals(name)) { nsAutoString cookie; rv = pendingAlert->GetCookie(cookie); NS_ENSURE_SUCCESS(rv, rv); if (mPendingPersistentAlerts[i].mListener) { rv = mPendingPersistentAlerts[i].mListener->Observe(nullptr, "alertfinished", cookie.get()); NS_ENSURE_SUCCESS(rv, rv); } mPendingPersistentAlerts[i].Init(aAlert, aAlertListener); return NS_OK; } } } bool requireInteraction; rv = aAlert->GetRequireInteraction(&requireInteraction); NS_ENSURE_SUCCESS(rv, rv); if (requireInteraction && !mNamedWindows.Contains(name) && static_cast(mPersistentAlertCount) >= Preferences::GetInt("dom.webnotifications.requireinteraction.count", 0)) { PendingAlert* pa = mPendingPersistentAlerts.AppendElement(); pa->Init(aAlert, aAlertListener); return NS_OK; } else { return ShowAlertWithIconURI(aAlert, aAlertListener, nullptr); } } NS_IMETHODIMP nsXULAlerts::ShowAlertWithIconURI(nsIAlertNotification* aAlert, nsIObserver* aAlertListener, nsIURI* aIconURI) { bool inPrivateBrowsing; nsresult rv = aAlert->GetInPrivateBrowsing(&inPrivateBrowsing); NS_ENSURE_SUCCESS(rv, rv); nsAutoString cookie; rv = aAlert->GetCookie(cookie); NS_ENSURE_SUCCESS(rv, rv); if (mDoNotDisturb) { if (!inPrivateBrowsing) { RefPtr 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 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 wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID)); nsCOMPtr argsArray = nsArray::Create(); // create scriptable versions of our strings that we can store in our nsIMutableArray.... nsCOMPtr scriptableImageUrl (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); NS_ENSURE_TRUE(scriptableImageUrl, NS_ERROR_FAILURE); scriptableImageUrl->SetData(imageUrl); rv = argsArray->AppendElement(scriptableImageUrl, /*weak =*/ false); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr scriptableAlertTitle (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); NS_ENSURE_TRUE(scriptableAlertTitle, NS_ERROR_FAILURE); scriptableAlertTitle->SetData(title); rv = argsArray->AppendElement(scriptableAlertTitle, /*weak =*/ false); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr scriptableAlertText (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); NS_ENSURE_TRUE(scriptableAlertText, NS_ERROR_FAILURE); scriptableAlertText->SetData(text); rv = argsArray->AppendElement(scriptableAlertText, /*weak =*/ false); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr scriptableIsClickable (do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID)); NS_ENSURE_TRUE(scriptableIsClickable, NS_ERROR_FAILURE); scriptableIsClickable->SetData(textClickable); rv = argsArray->AppendElement(scriptableIsClickable, /*weak =*/ false); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr scriptableAlertCookie (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); NS_ENSURE_TRUE(scriptableAlertCookie, NS_ERROR_FAILURE); scriptableAlertCookie->SetData(cookie); rv = argsArray->AppendElement(scriptableAlertCookie, /*weak =*/ false); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr scriptableOrigin (do_CreateInstance(NS_SUPPORTS_PRINT32_CONTRACTID)); NS_ENSURE_TRUE(scriptableOrigin, NS_ERROR_FAILURE); int32_t origin = LookAndFeel::GetInt(LookAndFeel::eIntID_AlertNotificationOrigin); scriptableOrigin->SetData(origin); rv = argsArray->AppendElement(scriptableOrigin, /*weak =*/ false); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr scriptableBidi (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); NS_ENSURE_TRUE(scriptableBidi, NS_ERROR_FAILURE); scriptableBidi->SetData(bidi); rv = argsArray->AppendElement(scriptableBidi, /*weak =*/ false); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr scriptableLang (do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); NS_ENSURE_TRUE(scriptableLang, NS_ERROR_FAILURE); scriptableLang->SetData(lang); rv = argsArray->AppendElement(scriptableLang, /*weak =*/ false); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr scriptableRequireInteraction (do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID)); NS_ENSURE_TRUE(scriptableRequireInteraction, NS_ERROR_FAILURE); scriptableRequireInteraction->SetData(requireInteraction); rv = argsArray->AppendElement(scriptableRequireInteraction, /*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 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 ifptr = do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); RefPtr alertObserver = new nsXULAlertObserver(this, name, aAlertListener, requireInteraction); nsCOMPtr iSupports(do_QueryInterface(alertObserver)); ifptr->SetData(iSupports); ifptr->SetDataIID(&NS_GET_IID(nsIObserver)); rv = argsArray->AppendElement(ifptr, /*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 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 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 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 domWindow = nsPIDOMWindowOuter::From(alert)) { domWindow->DispatchCustomEvent(NS_LITERAL_STRING("XULAlertClose")); } return NS_OK; }