LocalStorageCache.cpp (17648B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "LocalStorageCache.h" 8 9 #include "LocalStorageManager.h" 10 #include "Storage.h" 11 #include "StorageDBThread.h" 12 #include "StorageIPC.h" 13 #include "StorageUtils.h" 14 #include "mozilla/glean/DomStorageMetrics.h" 15 #include "nsDOMString.h" 16 #include "nsProxyRelease.h" 17 #include "nsThreadUtils.h" 18 #include "nsXULAppAPI.h" 19 20 namespace mozilla::dom { 21 22 #define DOM_STORAGE_CACHE_KEEP_ALIVE_TIME_MS 20000 23 24 namespace { 25 26 const uint32_t kDefaultSet = 0; 27 const uint32_t kSessionSet = 1; 28 29 inline uint32_t GetDataSetIndex(bool aPrivateBrowsing, 30 bool aSessionScopedOrLess) { 31 if (!aPrivateBrowsing && aSessionScopedOrLess) { 32 return kSessionSet; 33 } 34 35 return kDefaultSet; 36 } 37 38 inline uint32_t GetDataSetIndex(const LocalStorage* aStorage) { 39 // A session only mode doesn't exist anymore, so having a separate data set 40 // for it here is basically useless. This code is only kept until we remove 41 // the old / legacy LocalStorage implementation. 42 return GetDataSetIndex(aStorage->IsPrivateBrowsing(), 43 aStorage->IsPrivateBrowsingOrLess()); 44 } 45 46 } // namespace 47 48 // LocalStorageCacheBridge 49 50 NS_IMPL_ADDREF(LocalStorageCacheBridge) 51 52 // Since there is no consumer of return value of Release, we can turn this 53 // method to void to make implementation of asynchronous 54 // LocalStorageCache::Release much simpler. 55 NS_IMETHODIMP_(void) LocalStorageCacheBridge::Release(void) { 56 MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); 57 nsrefcnt count = --mRefCnt; 58 NS_LOG_RELEASE(this, count, "LocalStorageCacheBridge"); 59 if (0 == count) { 60 mRefCnt = 1; /* stabilize */ 61 /* enable this to find non-threadsafe destructors: */ 62 /* NS_ASSERT_OWNINGTHREAD(_class); */ 63 delete (this); 64 } 65 } 66 67 // LocalStorageCache 68 69 LocalStorageCache::LocalStorageCache(const nsACString* aOriginNoSuffix) 70 : mActor(nullptr), 71 mOriginNoSuffix(*aOriginNoSuffix), 72 mMonitor("LocalStorageCache"), 73 mLoaded(false), 74 mLoadResult(NS_OK), 75 mInitialized(false), 76 mPersistent(false), 77 mPreloadTelemetryRecorded(false) { 78 MOZ_COUNT_CTOR(LocalStorageCache); 79 } 80 81 LocalStorageCache::~LocalStorageCache() { 82 if (mActor) { 83 mActor->SendDeleteMeInternal(); 84 MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); 85 } 86 87 if (mManager) { 88 mManager->DropCache(this); 89 } 90 91 MOZ_COUNT_DTOR(LocalStorageCache); 92 } 93 94 void LocalStorageCache::SetActor(LocalStorageCacheChild* aActor) { 95 AssertIsOnOwningThread(); 96 MOZ_ASSERT(aActor); 97 MOZ_ASSERT(!mActor); 98 99 mActor = aActor; 100 } 101 102 NS_IMETHODIMP_(void) 103 LocalStorageCache::Release(void) { 104 // We must actually release on the main thread since the cache removes it 105 // self from the manager's hash table. And we don't want to lock access to 106 // that hash table. 107 if (NS_IsMainThread()) { 108 LocalStorageCacheBridge::Release(); 109 return; 110 } 111 112 RefPtr<nsRunnableMethod<LocalStorageCacheBridge, void, false>> event = 113 NewNonOwningRunnableMethod("dom::LocalStorageCacheBridge::Release", 114 static_cast<LocalStorageCacheBridge*>(this), 115 &LocalStorageCacheBridge::Release); 116 117 nsresult rv = NS_DispatchToMainThread(event); 118 if (NS_FAILED(rv)) { 119 NS_WARNING("LocalStorageCache::Release() on a non-main thread"); 120 LocalStorageCacheBridge::Release(); 121 } 122 } 123 124 void LocalStorageCache::Init(LocalStorageManager* aManager, bool aPersistent, 125 nsIPrincipal* aPrincipal, 126 const nsACString& aQuotaOriginScope) { 127 MOZ_ASSERT(!aQuotaOriginScope.IsEmpty()); 128 129 if (mInitialized) { 130 return; 131 } 132 133 mInitialized = true; 134 aPrincipal->OriginAttributesRef().CreateSuffix(mOriginSuffix); 135 mPrivateBrowsingId = aPrincipal->GetPrivateBrowsingId(); 136 mPersistent = aPersistent; 137 mQuotaOriginScope = aQuotaOriginScope; 138 139 if (mPersistent) { 140 mManager = aManager; 141 Preload(); 142 } 143 144 // Check the quota string has (or has not) the identical origin suffix as 145 // this storage cache is bound to. 146 MOZ_ASSERT(StringBeginsWith(mQuotaOriginScope, mOriginSuffix)); 147 MOZ_ASSERT(mOriginSuffix.IsEmpty() != 148 StringBeginsWith(mQuotaOriginScope, "^"_ns)); 149 150 mUsage = aManager->GetOriginUsage(mQuotaOriginScope, mPrivateBrowsingId); 151 } 152 153 void LocalStorageCache::NotifyObservers(const LocalStorage* aStorage, 154 const nsAString& aKey, 155 const nsAString& aOldValue, 156 const nsAString& aNewValue) { 157 AssertIsOnOwningThread(); 158 MOZ_ASSERT(aStorage); 159 160 if (!mActor) { 161 return; 162 } 163 164 // We want to send a message to the parent in order to broadcast the 165 // StorageEvent correctly to any child process. 166 167 (void)mActor->SendNotify(aStorage->DocumentURI(), aKey, aOldValue, aNewValue); 168 } 169 170 inline bool LocalStorageCache::Persist(const LocalStorage* aStorage) const { 171 return mPersistent && (aStorage->IsPrivateBrowsing() || 172 !aStorage->IsPrivateBrowsingOrLess()); 173 } 174 175 const nsCString LocalStorageCache::Origin() const { 176 return LocalStorageManager::CreateOrigin(mOriginSuffix, mOriginNoSuffix); 177 } 178 179 LocalStorageCache::Data& LocalStorageCache::DataSet( 180 const LocalStorage* aStorage) { 181 return mData[GetDataSetIndex(aStorage)]; 182 } 183 184 bool LocalStorageCache::ProcessUsageDelta(const LocalStorage* aStorage, 185 int64_t aDelta, 186 const MutationSource aSource) { 187 return ProcessUsageDelta(GetDataSetIndex(aStorage), aDelta, aSource); 188 } 189 190 bool LocalStorageCache::ProcessUsageDelta(uint32_t aGetDataSetIndex, 191 const int64_t aDelta, 192 const MutationSource aSource) { 193 // Check limit per this origin 194 Data& data = mData[aGetDataSetIndex]; 195 uint64_t newOriginUsage = data.mOriginQuotaUsage + aDelta; 196 if (aSource == ContentMutation && aDelta > 0 && 197 newOriginUsage > LocalStorageManager::GetOriginQuota()) { 198 return false; 199 } 200 201 // Now check eTLD+1 limit 202 if (mUsage && 203 !mUsage->CheckAndSetETLD1UsageDelta(aGetDataSetIndex, aDelta, aSource)) { 204 return false; 205 } 206 207 // Update size in our data set 208 data.mOriginQuotaUsage = newOriginUsage; 209 return true; 210 } 211 212 void LocalStorageCache::Preload() { 213 if (mLoaded || !mPersistent) { 214 return; 215 } 216 217 StorageDBChild* storageChild = 218 StorageDBChild::GetOrCreate(mPrivateBrowsingId); 219 if (!storageChild) { 220 mLoaded = true; 221 mLoadResult = NS_ERROR_FAILURE; 222 return; 223 } 224 225 storageChild->AsyncPreload(this); 226 } 227 228 void LocalStorageCache::WaitForPreload() { 229 if (!mPersistent) { 230 return; 231 } 232 233 bool loaded = mLoaded; 234 235 // Telemetry of rates of pending preloads 236 if (!mPreloadTelemetryRecorded) { 237 mPreloadTelemetryRecorded = true; 238 glean::localdomstorage::preload_pending_on_first_access 239 .EnumGet(static_cast< 240 glean::localdomstorage::PreloadPendingOnFirstAccessLabel>( 241 !loaded)) 242 .Add(); 243 } 244 245 if (loaded) { 246 return; 247 } 248 249 // If preload already started (i.e. we got some first data, but not all) 250 // SyncPreload will just wait for it to finish rather then synchronously 251 // read from the database. It seems to me more optimal. 252 253 // TODO place for A/B testing (force main thread load vs. let preload finish) 254 255 // No need to check sDatabase for being non-null since preload is either 256 // done before we've shut the DB down or when the DB could not start, 257 // preload has not even be started. 258 StorageDBChild::Get(mPrivateBrowsingId)->SyncPreload(this); 259 } 260 261 nsresult LocalStorageCache::GetLength(const LocalStorage* aStorage, 262 uint32_t* aRetval) { 263 if (Persist(aStorage)) { 264 WaitForPreload(); 265 if (NS_FAILED(mLoadResult)) { 266 return mLoadResult; 267 } 268 } 269 270 *aRetval = DataSet(aStorage).mKeys.Count(); 271 return NS_OK; 272 } 273 274 nsresult LocalStorageCache::GetKey(const LocalStorage* aStorage, 275 uint32_t aIndex, nsAString& aRetval) { 276 // XXX: This does a linear search for the key at index, which would 277 // suck if there's a large numer of indexes. Do we care? If so, 278 // maybe we need to have a lazily populated key array here or 279 // something? 280 if (Persist(aStorage)) { 281 WaitForPreload(); 282 if (NS_FAILED(mLoadResult)) { 283 return mLoadResult; 284 } 285 } 286 287 aRetval.SetIsVoid(true); 288 for (auto iter = DataSet(aStorage).mKeys.Iter(); !iter.Done(); iter.Next()) { 289 if (aIndex == 0) { 290 aRetval = iter.Key(); 291 break; 292 } 293 aIndex--; 294 } 295 296 return NS_OK; 297 } 298 299 void LocalStorageCache::GetKeys(const LocalStorage* aStorage, 300 nsTArray<nsString>& aKeys) { 301 if (Persist(aStorage)) { 302 WaitForPreload(); 303 } 304 305 if (NS_FAILED(mLoadResult)) { 306 return; 307 } 308 309 AppendToArray(aKeys, DataSet(aStorage).mKeys.Keys()); 310 } 311 312 nsresult LocalStorageCache::GetItem(const LocalStorage* aStorage, 313 const nsAString& aKey, nsAString& aRetval) { 314 if (Persist(aStorage)) { 315 WaitForPreload(); 316 if (NS_FAILED(mLoadResult)) { 317 return mLoadResult; 318 } 319 } 320 321 // not using AutoString since we don't want to copy buffer to result 322 nsString value; 323 if (!DataSet(aStorage).mKeys.Get(aKey, &value)) { 324 SetDOMStringToNull(value); 325 } 326 327 aRetval = value; 328 329 return NS_OK; 330 } 331 332 nsresult LocalStorageCache::SetItem(const LocalStorage* aStorage, 333 const nsAString& aKey, 334 const nsAString& aValue, nsString& aOld, 335 const MutationSource aSource) { 336 // Size of the cache that will change after this action. 337 int64_t delta = 0; 338 339 if (Persist(aStorage)) { 340 WaitForPreload(); 341 if (NS_FAILED(mLoadResult)) { 342 return mLoadResult; 343 } 344 } 345 346 Data& data = DataSet(aStorage); 347 if (!data.mKeys.Get(aKey, &aOld)) { 348 SetDOMStringToNull(aOld); 349 350 // We only consider key size if the key doesn't exist before. 351 delta += static_cast<int64_t>(aKey.Length()); 352 } 353 354 delta += static_cast<int64_t>(aValue.Length()) - 355 static_cast<int64_t>(aOld.Length()); 356 357 if (!ProcessUsageDelta(aStorage, delta, aSource)) { 358 return NS_ERROR_DOM_QUOTA_EXCEEDED_ERR; 359 } 360 361 if (aValue == aOld && DOMStringIsNull(aValue) == DOMStringIsNull(aOld)) { 362 return NS_SUCCESS_DOM_NO_OPERATION; 363 } 364 365 data.mKeys.InsertOrUpdate(aKey, aValue); 366 367 if (aSource != ContentMutation) { 368 return NS_OK; 369 } 370 371 #if !defined(MOZ_WIDGET_ANDROID) 372 NotifyObservers(aStorage, aKey, aOld, aValue); 373 #endif 374 375 if (Persist(aStorage)) { 376 StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId); 377 if (!storageChild) { 378 NS_ERROR( 379 "Writing to localStorage after the database has been shut down" 380 ", data lose!"); 381 return NS_ERROR_NOT_INITIALIZED; 382 } 383 384 if (DOMStringIsNull(aOld)) { 385 return storageChild->AsyncAddItem(this, aKey, aValue); 386 } 387 388 return storageChild->AsyncUpdateItem(this, aKey, aValue); 389 } 390 391 return NS_OK; 392 } 393 394 nsresult LocalStorageCache::RemoveItem(const LocalStorage* aStorage, 395 const nsAString& aKey, nsString& aOld, 396 const MutationSource aSource) { 397 if (Persist(aStorage)) { 398 WaitForPreload(); 399 if (NS_FAILED(mLoadResult)) { 400 return mLoadResult; 401 } 402 } 403 404 Data& data = DataSet(aStorage); 405 if (!data.mKeys.Get(aKey, &aOld)) { 406 SetDOMStringToNull(aOld); 407 return NS_SUCCESS_DOM_NO_OPERATION; 408 } 409 410 // Recalculate the cached data size 411 const int64_t delta = -(static_cast<int64_t>(aOld.Length()) + 412 static_cast<int64_t>(aKey.Length())); 413 (void)ProcessUsageDelta(aStorage, delta, aSource); 414 data.mKeys.Remove(aKey); 415 416 if (aSource != ContentMutation) { 417 return NS_OK; 418 } 419 420 #if !defined(MOZ_WIDGET_ANDROID) 421 NotifyObservers(aStorage, aKey, aOld, VoidString()); 422 #endif 423 424 if (Persist(aStorage)) { 425 StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId); 426 if (!storageChild) { 427 NS_ERROR( 428 "Writing to localStorage after the database has been shut down" 429 ", data lose!"); 430 return NS_ERROR_NOT_INITIALIZED; 431 } 432 433 return storageChild->AsyncRemoveItem(this, aKey); 434 } 435 436 return NS_OK; 437 } 438 439 nsresult LocalStorageCache::Clear(const LocalStorage* aStorage, 440 const MutationSource aSource) { 441 bool refresh = false; 442 if (Persist(aStorage)) { 443 // We need to preload all data (know the size) before we can proceeed 444 // to correctly decrease cached usage number. 445 // XXX as in case of unload, this is not technically needed now, but 446 // after super-scope quota introduction we have to do this. Get telemetry 447 // right now. 448 WaitForPreload(); 449 if (NS_FAILED(mLoadResult)) { 450 // When we failed to load data from the database, force delete of the 451 // scope data and make use of the storage possible again. 452 refresh = true; 453 mLoadResult = NS_OK; 454 } 455 } 456 457 Data& data = DataSet(aStorage); 458 bool hadData = !!data.mKeys.Count(); 459 460 if (hadData) { 461 (void)ProcessUsageDelta(aStorage, -data.mOriginQuotaUsage, aSource); 462 data.mKeys.Clear(); 463 } 464 465 if (aSource != ContentMutation) { 466 return hadData ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION; 467 } 468 469 #if !defined(MOZ_WIDGET_ANDROID) 470 if (hadData) { 471 NotifyObservers(aStorage, VoidString(), VoidString(), VoidString()); 472 } 473 #endif 474 475 if (Persist(aStorage) && (refresh || hadData)) { 476 StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId); 477 if (!storageChild) { 478 NS_ERROR( 479 "Writing to localStorage after the database has been shut down" 480 ", data lose!"); 481 return NS_ERROR_NOT_INITIALIZED; 482 } 483 484 return storageChild->AsyncClear(this); 485 } 486 487 return hadData ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION; 488 } 489 490 int64_t LocalStorageCache::GetOriginQuotaUsage( 491 const LocalStorage* aStorage) const { 492 return mData[GetDataSetIndex(aStorage)].mOriginQuotaUsage; 493 } 494 495 void LocalStorageCache::UnloadItems(uint32_t aUnloadFlags) { 496 if (aUnloadFlags & kUnloadDefault) { 497 // Must wait for preload to pass correct usage to ProcessUsageDelta 498 // XXX this is not technically needed right now since there is just 499 // per-origin isolated quota handling, but when we introduce super- 500 // -scope quotas, we have to do this. Better to start getting 501 // telemetry right now. 502 WaitForPreload(); 503 504 mData[kDefaultSet].mKeys.Clear(); 505 ProcessUsageDelta(kDefaultSet, -mData[kDefaultSet].mOriginQuotaUsage); 506 } 507 508 if (aUnloadFlags & kUnloadSession) { 509 mData[kSessionSet].mKeys.Clear(); 510 ProcessUsageDelta(kSessionSet, -mData[kSessionSet].mOriginQuotaUsage); 511 } 512 513 #ifdef DOM_STORAGE_TESTS 514 if (aUnloadFlags & kTestReload) { 515 WaitForPreload(); 516 517 mData[kDefaultSet].mKeys.Clear(); 518 mLoaded = false; // This is only used in testing code 519 Preload(); 520 } 521 #endif 522 } 523 524 // LocalStorageCacheBridge 525 526 uint32_t LocalStorageCache::LoadedCount() { 527 MonitorAutoLock monitor(mMonitor); 528 Data& data = mData[kDefaultSet]; 529 return data.mKeys.Count(); 530 } 531 532 bool LocalStorageCache::LoadItem(const nsAString& aKey, 533 const nsAString& aValue) { 534 MonitorAutoLock monitor(mMonitor); 535 if (mLoaded) { 536 return false; 537 } 538 539 Data& data = mData[kDefaultSet]; 540 data.mKeys.LookupOrInsertWith(aKey, [&] { 541 data.mOriginQuotaUsage += aKey.Length() + aValue.Length(); 542 return nsString(aValue); 543 }); 544 return true; 545 } 546 547 void LocalStorageCache::LoadDone(nsresult aRv) { 548 MonitorAutoLock monitor(mMonitor); 549 mLoadResult = aRv; 550 mLoaded = true; 551 monitor.Notify(); 552 } 553 554 void LocalStorageCache::LoadWait() { 555 MonitorAutoLock monitor(mMonitor); 556 while (!mLoaded) { 557 monitor.Wait(); 558 } 559 } 560 561 // StorageUsage 562 563 StorageUsage::StorageUsage(const nsACString& aOriginScope) 564 : mOriginScope(aOriginScope) { 565 mUsage[kDefaultSet] = mUsage[kSessionSet] = 0LL; 566 } 567 568 namespace { 569 570 class LoadUsageRunnable : public Runnable { 571 public: 572 LoadUsageRunnable(int64_t* aUsage, const int64_t aDelta) 573 : Runnable("dom::LoadUsageRunnable"), mTarget(aUsage), mDelta(aDelta) {} 574 575 private: 576 int64_t* mTarget; 577 int64_t mDelta; 578 579 NS_IMETHOD Run() override { 580 *mTarget = mDelta; 581 return NS_OK; 582 } 583 }; 584 585 } // namespace 586 587 void StorageUsage::LoadUsage(const int64_t aUsage) { 588 // Using kDefaultSet index since it is the index for the persitent data 589 // stored in the database we have just loaded usage for. 590 if (!NS_IsMainThread()) { 591 // In single process scenario we get this call from the DB thread 592 RefPtr<LoadUsageRunnable> r = 593 new LoadUsageRunnable(mUsage + kDefaultSet, aUsage); 594 NS_DispatchToMainThread(r); 595 } else { 596 // On a child process we get this on the main thread already 597 mUsage[kDefaultSet] += aUsage; 598 } 599 } 600 601 bool StorageUsage::CheckAndSetETLD1UsageDelta( 602 uint32_t aDataSetIndex, const int64_t aDelta, 603 const LocalStorageCache::MutationSource aSource) { 604 MOZ_ASSERT(NS_IsMainThread()); 605 606 int64_t newUsage = mUsage[aDataSetIndex] + aDelta; 607 if (aSource == LocalStorageCache::ContentMutation && aDelta > 0 && 608 newUsage > LocalStorageManager::GetSiteQuota()) { 609 return false; 610 } 611 612 mUsage[aDataSetIndex] = newUsage; 613 return true; 614 } 615 616 } // namespace mozilla::dom