diff --git a/dom/base/nsGlobalWindow.cpp b/dom/base/nsGlobalWindow.cpp index b7096c8e8da7..43c7e8b2b645 100644 --- a/dom/base/nsGlobalWindow.cpp +++ b/dom/base/nsGlobalWindow.cpp @@ -23,6 +23,7 @@ #include "mozilla/dom/Performance.h" #include "mozilla/dom/StorageEvent.h" #include "mozilla/dom/StorageEventBinding.h" +#include "mozilla/dom/StorageNotifierService.h" #include "mozilla/dom/Timeout.h" #include "mozilla/dom/TimeoutHandler.h" #include "mozilla/dom/TimeoutManager.h" @@ -464,8 +465,9 @@ static NS_DEFINE_CID(kXULControllersCID, NS_XULCONTROLLERS_CID); * An indirect observer object that means we don't have to implement nsIObserver * on nsGlobalWindow, where any script could see it. */ -class nsGlobalWindowObserver final : public nsIObserver, - public nsIInterfaceRequestor +class nsGlobalWindowObserver final : public nsIObserver + , public nsIInterfaceRequestor + , public StorageNotificationObserver { public: explicit nsGlobalWindowObserver(nsGlobalWindow* aWindow) : mWindow(aWindow) {} @@ -485,6 +487,23 @@ public: return NS_NOINTERFACE; } + void + ObserveStorageNotification(StorageEvent* aEvent, + const char16_t* aStorageType, + bool aPrivateBrowsing) override + { + if (mWindow) { + mWindow->ObserveStorageNotification(aEvent, aStorageType, + aPrivateBrowsing); + } + } + + virtual nsIEventTarget* + GetEventTarget() const override + { + return mWindow ? mWindow->EventTargetFor(TaskCategory::Other) : nullptr; + } + private: ~nsGlobalWindowObserver() = default; @@ -1631,14 +1650,16 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalWindow *aOuterWindow) // a strong reference. os->AddObserver(mObserver, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false); - - // Watch for dom-storage2-changed and dom-private-storage2-changed so we - // can fire storage events. Use a strong reference. - os->AddObserver(mObserver, "dom-storage2-changed", false); - os->AddObserver(mObserver, "dom-private-storage2-changed", false); } Preferences::AddStrongObserver(mObserver, "intl.accept_languages"); + + // Watch for storage notifications so we can fire storage events. + RefPtr sns = + StorageNotifierService::GetOrCreate(); + if (sns) { + sns->Register(mObserver); + } } } else { // |this| is an outer window. Outer windows start out frozen and @@ -1934,8 +1955,11 @@ nsGlobalWindow::CleanUp() nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { os->RemoveObserver(mObserver, NS_IOSERVICE_OFFLINE_STATUS_TOPIC); - os->RemoveObserver(mObserver, "dom-storage2-changed"); - os->RemoveObserver(mObserver, "dom-private-storage2-changed"); + } + + RefPtr sns = StorageNotifierService::GetOrCreate(); + if (sns) { + sns->Unregister(mObserver); } #ifdef MOZ_B2G @@ -12269,131 +12293,6 @@ nsGlobalWindow::Observe(nsISupports* aSubject, const char* aTopic, return NS_OK; } - // We need these for our private-browsing check below; so save them off. - // (And we can't do the private-browsing enforcement check as part of our - // outer conditional because otherwise we'll trigger an NS_WARNING if control - // flow reaches the bottom of this method.) - bool isNonPrivateLocalStorageChange = - !nsCRT::strcmp(aTopic, "dom-storage2-changed"); - bool isPrivateLocalStorageChange = - !nsCRT::strcmp(aTopic, "dom-private-storage2-changed"); - if (isNonPrivateLocalStorageChange || isPrivateLocalStorageChange) { - // Enforce that the source storage area's private browsing state matches - // this window's state. These flag checks and their maintenance independent - // from the principal's OriginAttributes matter because chrome docshells - // that are part of private browsing windows can be private browsing without - // having their OriginAttributes set (because they have the system - // principal). - bool isPrivateBrowsing = IsPrivateBrowsing(); - if ((isNonPrivateLocalStorageChange && isPrivateBrowsing) || - (isPrivateLocalStorageChange && !isPrivateBrowsing)) { - return NS_OK; - } - - // We require that aData be either u"SessionStorage" or u"localStorage". - // Assert under debug, but ignore the bogus event under non-debug. - MOZ_ASSERT(aData); - if (!aData) { - return NS_ERROR_INVALID_ARG; - } - - // LocalStorage can only exist on an inner window, and we don't want to - // generate events on frozen or otherwise-navigated-away from windows. - // (Actually, this code used to try and buffer events for frozen windows, - // but it never worked, so we've removed it. See bug 1285898.) - if (!IsInnerWindow() || !AsInner()->IsCurrentInnerWindow() || IsFrozen()) { - return NS_OK; - } - - nsIPrincipal *principal = GetPrincipal(); - if (!principal) { - return NS_OK; - } - - RefPtr event = static_cast(aSubject); - if (!event) { - return NS_ERROR_FAILURE; - } - - bool fireMozStorageChanged = false; - nsAutoString eventType; - eventType.AssignLiteral("storage"); - - if (!NS_strcmp(aData, u"sessionStorage")) { - nsCOMPtr changingStorage = event->GetStorageArea(); - MOZ_ASSERT(changingStorage); - - bool check = false; - - nsCOMPtr storageManager = do_QueryInterface(GetDocShell()); - if (storageManager) { - nsresult rv = storageManager->CheckStorage(principal, changingStorage, - &check); - if (NS_FAILED(rv)) { - return rv; - } - } - - if (!check) { - // This storage event is not coming from our storage or is coming - // from a different docshell, i.e. it is a clone, ignore this event. - return NS_OK; - } - - MOZ_LOG(gDOMLeakPRLog, LogLevel::Debug, - ("nsGlobalWindow %p with sessionStorage %p passing event from %p", - this, mSessionStorage.get(), changingStorage.get())); - - fireMozStorageChanged = mSessionStorage == changingStorage; - if (fireMozStorageChanged) { - eventType.AssignLiteral("MozSessionStorageChanged"); - } - } - - else { - MOZ_ASSERT(!NS_strcmp(aData, u"localStorage")); - nsIPrincipal* storagePrincipal = event->GetPrincipal(); - if (!storagePrincipal) { - return NS_OK; - } - - bool equals = false; - nsresult rv = storagePrincipal->Equals(principal, &equals); - NS_ENSURE_SUCCESS(rv, rv); - - if (!equals) { - return NS_OK; - } - - fireMozStorageChanged = mLocalStorage == event->GetStorageArea(); - - if (fireMozStorageChanged) { - eventType.AssignLiteral("MozLocalStorageChanged"); - } - } - - // Clone the storage event included in the observer notification. We want - // to dispatch clones rather than the original event. - ErrorResult error; - RefPtr clonedEvent = - CloneStorageEvent(eventType, event, error); - if (error.Failed()) { - return error.StealNSResult(); - } - - clonedEvent->SetTrusted(true); - - if (fireMozStorageChanged) { - WidgetEvent* internalEvent = clonedEvent->WidgetEventPtr(); - internalEvent->mFlags.mOnlyChromeDispatch = true; - } - - bool defaultActionEnabled; - DispatchEvent(clonedEvent, &defaultActionEnabled); - - return NS_OK; - } - if (!nsCRT::strcmp(aTopic, "offline-cache-update-added")) { if (mApplicationCache) return NS_OK; @@ -12463,6 +12362,114 @@ nsGlobalWindow::Observe(nsISupports* aSubject, const char* aTopic, return NS_ERROR_FAILURE; } +void +nsGlobalWindow::ObserveStorageNotification(StorageEvent* aEvent, + const char16_t* aStorageType, + bool aPrivateBrowsing) +{ + MOZ_ASSERT(aEvent); + + // Enforce that the source storage area's private browsing state matches + // this window's state. These flag checks and their maintenance independent + // from the principal's OriginAttributes matter because chrome docshells + // that are part of private browsing windows can be private browsing without + // having their OriginAttributes set (because they have the system + // principal). + bool isPrivateBrowsing = IsPrivateBrowsing(); + if (isPrivateBrowsing != aPrivateBrowsing) { + return; + } + + // LocalStorage can only exist on an inner window, and we don't want to + // generate events on frozen or otherwise-navigated-away from windows. + // (Actually, this code used to try and buffer events for frozen windows, + // but it never worked, so we've removed it. See bug 1285898.) + if (!IsInnerWindow() || !AsInner()->IsCurrentInnerWindow() || IsFrozen()) { + return; + } + + nsIPrincipal *principal = GetPrincipal(); + if (!principal) { + return; + } + + bool fireMozStorageChanged = false; + nsAutoString eventType; + eventType.AssignLiteral("storage"); + + if (!NS_strcmp(aStorageType, u"sessionStorage")) { + nsCOMPtr changingStorage = aEvent->GetStorageArea(); + MOZ_ASSERT(changingStorage); + + bool check = false; + + nsCOMPtr storageManager = do_QueryInterface(GetDocShell()); + if (storageManager) { + nsresult rv = storageManager->CheckStorage(principal, changingStorage, + &check); + if (NS_FAILED(rv)) { + return; + } + } + + if (!check) { + // This storage event is not coming from our storage or is coming + // from a different docshell, i.e. it is a clone, ignore this event. + return; + } + + MOZ_LOG(gDOMLeakPRLog, LogLevel::Debug, + ("nsGlobalWindow %p with sessionStorage %p passing event from %p", + this, mSessionStorage.get(), changingStorage.get())); + + fireMozStorageChanged = mSessionStorage == changingStorage; + if (fireMozStorageChanged) { + eventType.AssignLiteral("MozSessionStorageChanged"); + } + } + + else { + MOZ_ASSERT(!NS_strcmp(aStorageType, u"localStorage")); + nsIPrincipal* storagePrincipal = aEvent->GetPrincipal(); + if (!storagePrincipal) { + return; + } + + bool equals = false; + nsresult rv = storagePrincipal->Equals(principal, &equals); + NS_ENSURE_SUCCESS_VOID(rv); + + if (!equals) { + return; + } + + fireMozStorageChanged = mLocalStorage == aEvent->GetStorageArea(); + + if (fireMozStorageChanged) { + eventType.AssignLiteral("MozLocalStorageChanged"); + } + } + + // Clone the storage event included in the observer notification. We want + // to dispatch clones rather than the original event. + IgnoredErrorResult error; + RefPtr clonedEvent = + CloneStorageEvent(eventType, aEvent, error); + if (error.Failed()) { + return; + } + + clonedEvent->SetTrusted(true); + + if (fireMozStorageChanged) { + WidgetEvent* internalEvent = clonedEvent->WidgetEventPtr(); + internalEvent->mFlags.mOnlyChromeDispatch = true; + } + + bool defaultActionEnabled; + DispatchEvent(clonedEvent, &defaultActionEnabled); +} + already_AddRefed nsGlobalWindow::CloneStorageEvent(const nsAString& aType, const RefPtr& aEvent, diff --git a/dom/base/nsGlobalWindow.h b/dom/base/nsGlobalWindow.h index f277923339ed..a14584d3b270 100644 --- a/dom/base/nsGlobalWindow.h +++ b/dom/base/nsGlobalWindow.h @@ -611,6 +611,10 @@ public: nsresult Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData); + void ObserveStorageNotification(mozilla::dom::StorageEvent* aEvent, + const char16_t* aStorageType, + bool aPrivateBrowsing); + // Outer windows only. void UnblockScriptedClosing(); diff --git a/dom/storage/Storage.cpp b/dom/storage/Storage.cpp index f797f9d675bc..cf6fa37613cd 100644 --- a/dom/storage/Storage.cpp +++ b/dom/storage/Storage.cpp @@ -5,6 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "Storage.h" +#include "StorageNotifierService.h" #include "mozilla/dom/StorageBinding.h" #include "nsIPrincipal.h" @@ -121,13 +122,21 @@ Storage::NotifyChange(Storage* aStorage, nsIPrincipal* aPrincipal, event->SetPrincipal(aPrincipal); + // This will send the event to any registered window. + StorageNotifierService::Broadcast(event, aStorageType, aIsPrivate, + aImmediateDispatch); + + // This runnable is mainly used by devtools. Windows receive notification by + // StorageNotifierService. + RefPtr r = new StorageNotifierRunnable(event, aStorageType, aIsPrivate); if (aImmediateDispatch) { Unused << r->Run(); } else { - NS_DispatchToMainThread(r, NS_DISPATCH_NORMAL); + SystemGroup::Dispatch("Storage::NotifyChange", TaskCategory::Other, + r.forget()); } } diff --git a/dom/storage/StorageNotifierService.cpp b/dom/storage/StorageNotifierService.cpp new file mode 100644 index 000000000000..909eb9533b0d --- /dev/null +++ b/dom/storage/StorageNotifierService.cpp @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "StorageNotifierService.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { +namespace dom { + +namespace { + +// This boolean is used to avoid the creation of the service after been +// distroyed on shutdown. +bool gStorageShuttingDown = false; + +StaticRefPtr gStorageNotifierService; + +} // anonymous + +/* static */ StorageNotifierService* +StorageNotifierService::GetOrCreate() +{ + MOZ_ASSERT(NS_IsMainThread()); + if (!gStorageNotifierService && !gStorageShuttingDown) { + gStorageNotifierService = new StorageNotifierService(); + ClearOnShutdown(&gStorageNotifierService); + } + + return gStorageNotifierService; +} + +StorageNotifierService::StorageNotifierService() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gStorageNotifierService); +} + +StorageNotifierService::~StorageNotifierService() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gStorageNotifierService); + gStorageShuttingDown = true; +} + +/* static */ void +StorageNotifierService::Broadcast(StorageEvent* aEvent, + const char16_t* aStorageType, + bool aPrivateBrowsing, + bool aImmediateDispatch) +{ + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr service = gStorageNotifierService; + if (!service) { + return; + } + + RefPtr event = aEvent; + + nsTObserverArray>::ForwardIterator + iter(service->mObservers); + + while (iter.HasMore()) { + RefPtr observer = iter.GetNext(); + + RefPtr r = NS_NewRunnableFunction( + "StorageNotifierService::Broadcast", + [observer, event, aStorageType, aPrivateBrowsing] () { + observer->ObserveStorageNotification(event, aStorageType, aPrivateBrowsing); + }); + + if (aImmediateDispatch) { + r->Run(); + } else { + nsCOMPtr et = observer->GetEventTarget(); + if (et) { + et->Dispatch(r.forget()); + } + } + } +} + +void +StorageNotifierService::Register(StorageNotificationObserver* aObserver) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aObserver); + MOZ_ASSERT(!mObservers.Contains(aObserver)); + + mObservers.AppendElement(aObserver); +} + +void +StorageNotifierService::Unregister(StorageNotificationObserver* aObserver) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aObserver); + + // No assertion about mObservers containing aObserver because window calls + // this method multiple times. + + mObservers.RemoveElement(aObserver); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/storage/StorageNotifierService.h b/dom/storage/StorageNotifierService.h new file mode 100644 index 000000000000..62e01037c5fa --- /dev/null +++ b/dom/storage/StorageNotifierService.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_StorageNotifierService_h +#define mozilla_dom_StorageNotifierService_h + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +class StorageEvent; + +class StorageNotificationObserver +{ +public: + NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING + + virtual void + ObserveStorageNotification(StorageEvent* aEvent, + const char16_t* aStorageType, + bool aPrivateBrowsing) = 0; + + virtual nsIEventTarget* + GetEventTarget() const = 0; +}; + +class StorageNotifierService final +{ +public: + NS_INLINE_DECL_REFCOUNTING(StorageNotifierService) + + static StorageNotifierService* + GetOrCreate(); + + static void + Broadcast(StorageEvent* aEvent, const char16_t* aStorageType, + bool aPrivateBrowsing, bool aImmediateDispatch); + + void + Register(StorageNotificationObserver* aObserver); + + void + Unregister(StorageNotificationObserver* aObserver); + +private: + StorageNotifierService(); + ~StorageNotifierService(); + + nsTObserverArray> mObservers; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_StorageNotifierService_h diff --git a/dom/storage/moz.build b/dom/storage/moz.build index 6c3fbf053ce4..c1a94127aae9 100644 --- a/dom/storage/moz.build +++ b/dom/storage/moz.build @@ -13,6 +13,7 @@ EXPORTS.mozilla.dom += [ 'SessionStorageManager.h', 'Storage.h', 'StorageIPC.h', + 'StorageNotifierService.h', ] UNIFIED_SOURCES += [ @@ -26,6 +27,7 @@ UNIFIED_SOURCES += [ 'StorageDBThread.cpp', 'StorageDBUpdater.cpp', 'StorageIPC.cpp', + 'StorageNotifierService.cpp', 'StorageObserver.cpp', 'StorageUtils.cpp', ] diff --git a/dom/tests/mochitest/storageevent/test_storageNotifications.html b/dom/tests/mochitest/storageevent/test_storageNotifications.html index 2f9ec51a8c09..ce8836186d0d 100644 --- a/dom/tests/mochitest/storageevent/test_storageNotifications.html +++ b/dom/tests/mochitest/storageevent/test_storageNotifications.html @@ -40,19 +40,21 @@ function* Tests() is(sessionStorage.length, 0, "Session storage is empty [1]"); is(localStorage.length, 0, "Local storage is empty [1]"); - var onStorageChanged = { - observe: function(subject, topic, type) { - if (topic == "dom-storage2-changed") { - ok(expectedTypes.length > 0, "Not more then expected events encountered"); - is(type, expectedTypes.shift(), "Expected type of the storage notification"); - tests.next(); - } - } + function onSessionStorageChanged(e) { + ok(expectedTypes.length > 0, "Not more then expected events encountered"); + is("sessionStorage", expectedTypes.shift(), "Expected session type of the storage notification"); + tests.next(); } - // Listen for dom-storage2-changed notification - SpecialPowers.Services.obs.addObserver(onStorageChanged, - "dom-storage2-changed"); + function onLocalStorageChanged(e) { + ok(expectedTypes.length > 0, "Not more then expected events encountered"); + is("localStorage", expectedTypes.shift(), "Expected session type of the storage notification"); + tests.next(); + } + + // Listen for storage notifications + SpecialPowers.addChromeEventListener("MozSessionStorageChanged", onSessionStorageChanged, true); + SpecialPowers.addChromeEventListener("MozLocalStorageChanged", onLocalStorageChanged, true); // add an empty-value key localStorage.setItem("empty", ""); @@ -104,8 +106,9 @@ function* Tests() yield undefined; SimpleTest.executeSoon(function () { - SpecialPowers.Services.obs.removeObserver(onStorageChanged, - "dom-storage2-changed", false); + SpecialPowers.removeChromeEventListener("MozSessionStorageChanged", onSessionStorageChanged, true); + SpecialPowers.removeChromeEventListener("MozLocalStorageChanged", onLocalStorageChanged, true); + is(expectedTypes.length, 0, "received the correct number of events"); sessionStorage.clear();