And apply the auto-fix from the linter. Note that we exclude RefPtr from the check as it plays badly with our CanRunScriptChecker's linter. We also explicitly silent warning for loops on nsTObserverArray, as their iterator is designed to support invalidation. Differential Revision: https://phabricator.services.mozilla.com/D293497
555 lines
15 KiB
C++
555 lines
15 KiB
C++
/* 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 "StorageObserver.h"
|
|
|
|
#include "LocalStorageCache.h"
|
|
#include "StorageCommon.h"
|
|
#include "StorageDBThread.h"
|
|
#include "StorageIPC.h"
|
|
#include "StorageUtils.h"
|
|
#include "mozilla/BasePrincipal.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/Services.h"
|
|
#include "mozilla/SpinEventLoopUntil.h"
|
|
#include "mozilla/dom/LocalStorageCommon.h"
|
|
#include "nsCOMPtr.h"
|
|
#include "nsEscape.h"
|
|
#include "nsIClearBySiteEntry.h"
|
|
#include "nsICookieNotification.h"
|
|
#include "nsICookiePermission.h"
|
|
#include "nsIObserverService.h"
|
|
#include "nsIPermission.h"
|
|
#include "nsIURI.h"
|
|
#include "nsNetCID.h"
|
|
#include "nsNetUtil.h"
|
|
#include "nsPrintfCString.h"
|
|
#include "nsServiceManagerUtils.h"
|
|
#include "nsXULAppAPI.h"
|
|
|
|
namespace mozilla::dom {
|
|
|
|
using namespace StorageUtils;
|
|
|
|
static const char kStartupTopic[] = "sessionstore-windows-restored";
|
|
static const uint32_t kStartupDelay = 0;
|
|
|
|
const char kTestingPref[] = "dom.storage.testing";
|
|
|
|
constexpr auto kPrivateBrowsingPattern = u"{ \"privateBrowsingId\": 1 }"_ns;
|
|
|
|
NS_IMPL_ISUPPORTS(StorageObserver, nsIObserver, nsINamed,
|
|
nsISupportsWeakReference)
|
|
|
|
StorageObserver* StorageObserver::sSelf = nullptr;
|
|
|
|
// static
|
|
nsresult StorageObserver::Init() {
|
|
static_assert(kPrivateBrowsingIdCount * sizeof(mBackgroundThread[0]) ==
|
|
sizeof(mBackgroundThread));
|
|
if (sSelf) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
|
|
if (!obs) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
sSelf = new StorageObserver();
|
|
NS_ADDREF(sSelf);
|
|
|
|
// Chrome clear operations.
|
|
obs->AddObserver(sSelf, kStartupTopic, true);
|
|
obs->AddObserver(sSelf, "cookie-changed", true);
|
|
obs->AddObserver(sSelf, "perm-changed", true);
|
|
obs->AddObserver(sSelf, "last-pb-context-exited", true);
|
|
obs->AddObserver(sSelf, "clear-origin-attributes-data", true);
|
|
obs->AddObserver(sSelf, "dom-storage:clear-origin-attributes-data", true);
|
|
obs->AddObserver(sSelf, "extension:purge-localStorage", true);
|
|
obs->AddObserver(sSelf, "browser:purge-sessionStorage", true);
|
|
obs->AddObserver(sSelf, "extension:purge-sessionStorage", true);
|
|
|
|
// Shutdown
|
|
obs->AddObserver(sSelf, "profile-after-change", true);
|
|
if (XRE_IsParentProcess()) {
|
|
obs->AddObserver(sSelf, "profile-before-change", true);
|
|
}
|
|
|
|
// Testing
|
|
#ifdef DOM_STORAGE_TESTS
|
|
Preferences::RegisterCallbackAndCall(TestingPrefChanged, kTestingPref);
|
|
#endif
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// static
|
|
nsresult StorageObserver::Shutdown() {
|
|
AssertIsOnMainThread();
|
|
|
|
if (!sSelf) {
|
|
return NS_ERROR_NOT_INITIALIZED; // Is this always an error?
|
|
}
|
|
|
|
sSelf->mSinks.Clear();
|
|
|
|
NS_RELEASE(sSelf);
|
|
return NS_OK;
|
|
}
|
|
|
|
// static
|
|
void StorageObserver::TestingPrefChanged(const char* aPrefName,
|
|
void* aClosure) {
|
|
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
|
|
if (!obs || !sSelf) {
|
|
return;
|
|
}
|
|
|
|
if (Preferences::GetBool(kTestingPref)) {
|
|
obs->AddObserver(sSelf, "domstorage-test-flush-force", true);
|
|
if (XRE_IsParentProcess()) {
|
|
// Only to forward to child process.
|
|
obs->AddObserver(sSelf, "domstorage-test-flushed", true);
|
|
}
|
|
obs->AddObserver(sSelf, "domstorage-test-reload", true);
|
|
} else {
|
|
obs->RemoveObserver(sSelf, "domstorage-test-flush-force");
|
|
if (XRE_IsParentProcess()) {
|
|
// Only to forward to child process.
|
|
obs->RemoveObserver(sSelf, "domstorage-test-flushed");
|
|
}
|
|
obs->RemoveObserver(sSelf, "domstorage-test-reload");
|
|
}
|
|
}
|
|
|
|
void StorageObserver::AddSink(StorageObserverSink* aObs) {
|
|
AssertIsOnMainThread();
|
|
|
|
MOZ_ASSERT(sSelf);
|
|
|
|
mSinks.AppendElement(aObs);
|
|
}
|
|
|
|
void StorageObserver::RemoveSink(StorageObserverSink* aObs) {
|
|
AssertIsOnMainThread();
|
|
|
|
MOZ_ASSERT(sSelf);
|
|
|
|
mSinks.RemoveElement(aObs);
|
|
}
|
|
|
|
void StorageObserver::Notify(const char* aTopic,
|
|
const nsAString& aOriginAttributesPattern,
|
|
const nsACString& aOriginScope) {
|
|
AssertIsOnMainThread();
|
|
|
|
MOZ_ASSERT(sSelf);
|
|
|
|
for (auto const& sink : mSinks.ForwardRange()) {
|
|
sink->Observe(aTopic, aOriginAttributesPattern, aOriginScope);
|
|
}
|
|
}
|
|
|
|
void StorageObserver::NoteBackgroundThread(const uint32_t aPrivateBrowsingId,
|
|
nsIEventTarget* aBackgroundThread) {
|
|
MOZ_RELEASE_ASSERT(aPrivateBrowsingId < kPrivateBrowsingIdCount);
|
|
|
|
mBackgroundThread[aPrivateBrowsingId] = aBackgroundThread;
|
|
}
|
|
|
|
nsresult StorageObserver::GetOriginScope(const char16_t* aData,
|
|
nsACString& aOriginScope) {
|
|
nsresult rv;
|
|
|
|
NS_ConvertUTF16toUTF8 domain(aData);
|
|
|
|
nsAutoCString convertedDomain;
|
|
rv = NS_DomainToASCIIAllowAnyGlyphfulASCII(domain, convertedDomain);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
nsCString originScope;
|
|
rv = CreateReversedDomain(convertedDomain, originScope);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
aOriginScope = originScope;
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
StorageObserver::Observe(nsISupports* aSubject, const char* aTopic,
|
|
const char16_t* aData) {
|
|
if (NS_WARN_IF(!sSelf)) { // Shutdown took place
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult rv;
|
|
|
|
// Start the thread that opens the database.
|
|
if (!strcmp(aTopic, kStartupTopic)) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
|
|
if (NextGenLocalStorageEnabled()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
|
|
obs->RemoveObserver(this, kStartupTopic);
|
|
|
|
return NS_NewTimerWithObserver(getter_AddRefs(mDBThreadStartDelayTimer),
|
|
this, nsITimer::TYPE_ONE_SHOT,
|
|
kStartupDelay);
|
|
}
|
|
|
|
// Timer callback used to start the database a short timer after startup
|
|
if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC)) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
MOZ_ASSERT(!NextGenLocalStorageEnabled());
|
|
|
|
nsCOMPtr<nsITimer> timer = do_QueryInterface(aSubject);
|
|
if (!timer) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
if (timer == mDBThreadStartDelayTimer) {
|
|
mDBThreadStartDelayTimer = nullptr;
|
|
|
|
for (const uint32_t id : {0, 1}) {
|
|
StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id);
|
|
if (NS_WARN_IF(!storageChild)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
storageChild->SendStartup();
|
|
}
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// Clear everything, caches + database
|
|
if (!strcmp(aTopic, "cookie-changed")) {
|
|
nsCOMPtr<nsICookieNotification> notification = do_QueryInterface(aSubject);
|
|
NS_ENSURE_TRUE(notification, NS_ERROR_FAILURE);
|
|
|
|
if (notification->GetAction() !=
|
|
nsICookieNotification::ALL_COOKIES_CLEARED) {
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!NextGenLocalStorageEnabled()) {
|
|
for (const uint32_t id : {0, 1}) {
|
|
StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id);
|
|
if (NS_WARN_IF(!storageChild)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
storageChild->AsyncClearAll();
|
|
|
|
if (XRE_IsParentProcess()) {
|
|
storageChild->SendClearAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
Notify("cookie-cleared");
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// Clear from caches everything that has been stored
|
|
// while in session-only mode
|
|
if (!strcmp(aTopic, "perm-changed")) {
|
|
// Check for cookie permission change
|
|
nsCOMPtr<nsIPermission> perm(do_QueryInterface(aSubject));
|
|
if (!perm) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsAutoCString type;
|
|
perm->GetType(type);
|
|
if (type != "cookie"_ns) {
|
|
return NS_OK;
|
|
}
|
|
|
|
uint32_t cap = 0;
|
|
perm->GetCapability(&cap);
|
|
if (!(cap & nsICookiePermission::ACCESS_SESSION) ||
|
|
!u"deleted"_ns.Equals(nsDependentString(aData))) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsCOMPtr<nsIPrincipal> principal;
|
|
perm->GetPrincipal(getter_AddRefs(principal));
|
|
if (!principal) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsAutoCString originSuffix;
|
|
BasePrincipal::Cast(principal)->OriginAttributesRef().CreateSuffix(
|
|
originSuffix);
|
|
|
|
nsAutoCString host;
|
|
principal->GetHost(host);
|
|
if (host.IsEmpty()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsAutoCString originScope;
|
|
rv = CreateReversedDomain(host, originScope);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
Notify("session-only-cleared", NS_ConvertUTF8toUTF16(originSuffix),
|
|
originScope);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!strcmp(aTopic, "extension:purge-localStorage")) {
|
|
if (NextGenLocalStorageEnabled()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
const char topic[] = "extension:purge-localStorage-caches";
|
|
|
|
if (aData) {
|
|
nsCString originScope;
|
|
|
|
rv = GetOriginScope(aData, originScope);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
if (XRE_IsParentProcess()) {
|
|
for (const uint32_t id : {0, 1}) {
|
|
StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id);
|
|
if (NS_WARN_IF(!storageChild)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
storageChild->SendClearMatchingOrigin(originScope);
|
|
}
|
|
}
|
|
|
|
Notify(topic, u""_ns, originScope);
|
|
} else {
|
|
for (const uint32_t id : {0, 1}) {
|
|
StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id);
|
|
if (NS_WARN_IF(!storageChild)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
storageChild->AsyncClearAll();
|
|
|
|
if (XRE_IsParentProcess()) {
|
|
storageChild->SendClearAll();
|
|
}
|
|
}
|
|
|
|
Notify(topic);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!strcmp(aTopic, "browser:purge-sessionStorage") ||
|
|
!strcmp(aTopic, "extension:purge-sessionStorage")) {
|
|
// The caller passed an nsIClearBySiteEntry object which consists of both
|
|
// site and pattern.
|
|
// If both are passed, aSubject takes precedence over aData.
|
|
if (aSubject) {
|
|
nsCOMPtr<nsIClearBySiteEntry> entry = do_QueryInterface(aSubject);
|
|
|
|
NS_ENSURE_TRUE(entry, NS_ERROR_FAILURE);
|
|
|
|
nsAutoCString schemelessSite;
|
|
rv = entry->GetSchemelessSite(schemelessSite);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsAutoString patternJSON;
|
|
rv = entry->GetPatternJSON(patternJSON);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
|
|
nsCString originScope;
|
|
if (!schemelessSite.IsEmpty()) {
|
|
rv = GetOriginScope(NS_ConvertUTF8toUTF16(schemelessSite).get(),
|
|
originScope);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
|
|
Notify(aTopic, patternJSON, originScope);
|
|
} else if (aData) {
|
|
// The caller passed only a schemeless site, origin or host.
|
|
nsCString originScope;
|
|
rv = GetOriginScope(aData, originScope);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
return rv;
|
|
}
|
|
|
|
Notify(aTopic, u""_ns, originScope);
|
|
} else {
|
|
// The caller passed nothing, clear everything.
|
|
Notify(aTopic, u""_ns, ""_ns);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// Clear all private-browsing caches
|
|
if (!strcmp(aTopic, "last-pb-context-exited")) {
|
|
if (NextGenLocalStorageEnabled()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// We get the notification in both processes (parent and content), but the
|
|
// clearing of the in-memory database should be triggered from the parent
|
|
// process only to avoid creation of redundant clearing operations.
|
|
// Also, if we create a new StorageDBChild instance late during content
|
|
// process shutdown, then it might be leaked in debug builds because it
|
|
// could happen that there is no chance to properly destroy it.
|
|
if (XRE_IsParentProcess()) {
|
|
// This doesn't use a loop with privateBrowsingId 0 and 1, since we only
|
|
// need to clear the in-memory database which is represented by
|
|
// privateBrowsingId 1.
|
|
static const uint32_t id = 1;
|
|
|
|
StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id);
|
|
if (NS_WARN_IF(!storageChild)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
OriginAttributesPattern pattern;
|
|
if (!pattern.Init(kPrivateBrowsingPattern)) {
|
|
NS_ERROR("Cannot parse origin attributes pattern");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
storageChild->SendClearMatchingOriginAttributes(pattern);
|
|
}
|
|
|
|
Notify("private-browsing-data-cleared", kPrivateBrowsingPattern);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// Clear data of the origins whose prefixes will match the suffix.
|
|
if (!strcmp(aTopic, "clear-origin-attributes-data") ||
|
|
!strcmp(aTopic, "dom-storage:clear-origin-attributes-data")) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
|
|
OriginAttributesPattern pattern;
|
|
if (!pattern.Init(nsDependentString(aData))) {
|
|
NS_ERROR("Cannot parse origin attributes pattern");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (NextGenLocalStorageEnabled()) {
|
|
Notify("session-storage:clear-origin-attributes-data",
|
|
nsDependentString(aData));
|
|
return NS_OK;
|
|
}
|
|
|
|
for (const uint32_t id : {0, 1}) {
|
|
StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id);
|
|
if (NS_WARN_IF(!storageChild)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
storageChild->SendClearMatchingOriginAttributes(pattern);
|
|
}
|
|
|
|
Notify(aTopic, nsDependentString(aData));
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!strcmp(aTopic, "profile-after-change")) {
|
|
Notify("profile-change");
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!strcmp(aTopic, "profile-before-change")) {
|
|
MOZ_ASSERT(XRE_IsParentProcess());
|
|
|
|
if (NextGenLocalStorageEnabled()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
for (const uint32_t id : {0, 1}) {
|
|
if (mBackgroundThread[id]) {
|
|
bool done = false;
|
|
|
|
RefPtr<StorageDBThread::ShutdownRunnable> shutdownRunnable =
|
|
new StorageDBThread::ShutdownRunnable(id, done);
|
|
MOZ_ALWAYS_SUCCEEDS(mBackgroundThread[id]->Dispatch(
|
|
shutdownRunnable, NS_DISPATCH_NORMAL));
|
|
|
|
MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
|
|
"StorageObserver::Observe profile-before-change"_ns,
|
|
[&]() { return done; }));
|
|
|
|
mBackgroundThread[id] = nullptr;
|
|
}
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
#ifdef DOM_STORAGE_TESTS
|
|
if (!strcmp(aTopic, "domstorage-test-flush-force")) {
|
|
if (NextGenLocalStorageEnabled()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
for (const uint32_t id : {0, 1}) {
|
|
StorageDBChild* storageChild = StorageDBChild::GetOrCreate(id);
|
|
if (NS_WARN_IF(!storageChild)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
storageChild->SendAsyncFlush();
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!strcmp(aTopic, "domstorage-test-flushed")) {
|
|
if (NextGenLocalStorageEnabled()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Only used to propagate to IPC children
|
|
Notify("test-flushed");
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!strcmp(aTopic, "domstorage-test-reload")) {
|
|
if (NextGenLocalStorageEnabled()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
Notify("test-reload");
|
|
|
|
return NS_OK;
|
|
}
|
|
#endif
|
|
|
|
NS_ERROR("Unexpected topic");
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
StorageObserver::GetName(nsACString& aName) {
|
|
aName.AssignLiteral("StorageObserver");
|
|
return NS_OK;
|
|
}
|
|
|
|
} // namespace mozilla::dom
|