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 "Storage.h"
10 #include "StorageDBThread.h"
11 #include "StorageIPC.h"
12 #include "StorageUtils.h"
13 #include "LocalStorageManager.h"
14
15 #include "nsDOMString.h"
16 #include "nsXULAppAPI.h"
17 #include "mozilla/Unused.h"
18 #include "nsProxyRelease.h"
19 #include "nsThreadUtils.h"
20
21 namespace mozilla {
22 namespace dom {
23
24 #define DOM_STORAGE_CACHE_KEEP_ALIVE_TIME_MS 20000
25
26 namespace {
27
28 const uint32_t kDefaultSet = 0;
29 const uint32_t kSessionSet = 1;
30
GetDataSetIndex(bool aPrivateBrowsing,bool aSessionScopedOrLess)31 inline uint32_t GetDataSetIndex(bool aPrivateBrowsing,
32 bool aSessionScopedOrLess) {
33 if (!aPrivateBrowsing && aSessionScopedOrLess) {
34 return kSessionSet;
35 }
36
37 return kDefaultSet;
38 }
39
GetDataSetIndex(const LocalStorage * aStorage)40 inline uint32_t GetDataSetIndex(const LocalStorage* aStorage) {
41 return GetDataSetIndex(aStorage->IsPrivateBrowsing(),
42 aStorage->IsSessionScopedOrLess());
43 }
44
45 } // namespace
46
47 // LocalStorageCacheBridge
48
49 NS_IMPL_ADDREF(LocalStorageCacheBridge)
50
51 // Since there is no consumer of return value of Release, we can turn this
52 // method to void to make implementation of asynchronous
53 // LocalStorageCache::Release much simpler.
NS_IMETHODIMP_(void)54 NS_IMETHODIMP_(void) LocalStorageCacheBridge::Release(void) {
55 MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release");
56 nsrefcnt count = --mRefCnt;
57 NS_LOG_RELEASE(this, count, "LocalStorageCacheBridge");
58 if (0 == count) {
59 mRefCnt = 1; /* stabilize */
60 /* enable this to find non-threadsafe destructors: */
61 /* NS_ASSERT_OWNINGTHREAD(_class); */
62 delete (this);
63 }
64 }
65
66 // LocalStorageCache
67
LocalStorageCache(const nsACString * aOriginNoSuffix)68 LocalStorageCache::LocalStorageCache(const nsACString* aOriginNoSuffix)
69 : mActor(nullptr),
70 mOriginNoSuffix(*aOriginNoSuffix),
71 mMonitor("LocalStorageCache"),
72 mLoaded(false),
73 mLoadResult(NS_OK),
74 mInitialized(false),
75 mPersistent(false),
76 mPreloadTelemetryRecorded(false) {
77 MOZ_COUNT_CTOR(LocalStorageCache);
78 }
79
~LocalStorageCache()80 LocalStorageCache::~LocalStorageCache() {
81 if (mActor) {
82 mActor->SendDeleteMeInternal();
83 MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!");
84 }
85
86 if (mManager) {
87 mManager->DropCache(this);
88 }
89
90 MOZ_COUNT_DTOR(LocalStorageCache);
91 }
92
SetActor(LocalStorageCacheChild * aActor)93 void LocalStorageCache::SetActor(LocalStorageCacheChild* aActor) {
94 AssertIsOnOwningThread();
95 MOZ_ASSERT(aActor);
96 MOZ_ASSERT(!mActor);
97
98 mActor = aActor;
99 }
100
NS_IMETHODIMP_(void)101 NS_IMETHODIMP_(void)
102 LocalStorageCache::Release(void) {
103 // We must actually release on the main thread since the cache removes it
104 // self from the manager's hash table. And we don't want to lock access to
105 // that hash table.
106 if (NS_IsMainThread()) {
107 LocalStorageCacheBridge::Release();
108 return;
109 }
110
111 RefPtr<nsRunnableMethod<LocalStorageCacheBridge, void, false>> event =
112 NewNonOwningRunnableMethod("dom::LocalStorageCacheBridge::Release",
113 static_cast<LocalStorageCacheBridge*>(this),
114 &LocalStorageCacheBridge::Release);
115
116 nsresult rv = NS_DispatchToMainThread(event);
117 if (NS_FAILED(rv)) {
118 NS_WARNING("LocalStorageCache::Release() on a non-main thread");
119 LocalStorageCacheBridge::Release();
120 }
121 }
122
Init(LocalStorageManager * aManager,bool aPersistent,nsIPrincipal * aPrincipal,const nsACString & aQuotaOriginScope)123 void LocalStorageCache::Init(LocalStorageManager* aManager, bool aPersistent,
124 nsIPrincipal* aPrincipal,
125 const nsACString& aQuotaOriginScope) {
126 MOZ_ASSERT(!aQuotaOriginScope.IsEmpty());
127
128 if (mInitialized) {
129 return;
130 }
131
132 mInitialized = true;
133 aPrincipal->OriginAttributesRef().CreateSuffix(mOriginSuffix);
134 mPrivateBrowsingId = aPrincipal->GetPrivateBrowsingId();
135 mPersistent = aPersistent;
136 mQuotaOriginScope = aQuotaOriginScope;
137
138 if (mPersistent) {
139 mManager = aManager;
140 Preload();
141 }
142
143 // Check the quota string has (or has not) the identical origin suffix as
144 // this storage cache is bound to.
145 MOZ_ASSERT(StringBeginsWith(mQuotaOriginScope, mOriginSuffix));
146 MOZ_ASSERT(mOriginSuffix.IsEmpty() !=
147 StringBeginsWith(mQuotaOriginScope, "^"_ns));
148
149 mUsage = aManager->GetOriginUsage(mQuotaOriginScope, mPrivateBrowsingId);
150 }
151
NotifyObservers(const LocalStorage * aStorage,const nsString & aKey,const nsString & aOldValue,const nsString & aNewValue)152 void LocalStorageCache::NotifyObservers(const LocalStorage* aStorage,
153 const nsString& aKey,
154 const nsString& aOldValue,
155 const nsString& aNewValue) {
156 AssertIsOnOwningThread();
157 MOZ_ASSERT(aStorage);
158
159 if (!mActor) {
160 return;
161 }
162
163 // We want to send a message to the parent in order to broadcast the
164 // StorageEvent correctly to any child process.
165
166 Unused << mActor->SendNotify(aStorage->DocumentURI(), aKey, aOldValue,
167 aNewValue);
168 }
169
Persist(const LocalStorage * aStorage) const170 inline bool LocalStorageCache::Persist(const LocalStorage* aStorage) const {
171 return mPersistent &&
172 (aStorage->IsPrivateBrowsing() || !aStorage->IsSessionScopedOrLess());
173 }
174
Origin() const175 const nsCString LocalStorageCache::Origin() const {
176 return LocalStorageManager::CreateOrigin(mOriginSuffix, mOriginNoSuffix);
177 }
178
DataSet(const LocalStorage * aStorage)179 LocalStorageCache::Data& LocalStorageCache::DataSet(
180 const LocalStorage* aStorage) {
181 return mData[GetDataSetIndex(aStorage)];
182 }
183
ProcessUsageDelta(const LocalStorage * aStorage,int64_t aDelta,const MutationSource aSource)184 bool LocalStorageCache::ProcessUsageDelta(const LocalStorage* aStorage,
185 int64_t aDelta,
186 const MutationSource aSource) {
187 return ProcessUsageDelta(GetDataSetIndex(aStorage), aDelta, aSource);
188 }
189
ProcessUsageDelta(uint32_t aGetDataSetIndex,const int64_t aDelta,const MutationSource aSource)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
Preload()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
WaitForPreload(Telemetry::HistogramID aTelemetryID)228 void LocalStorageCache::WaitForPreload(Telemetry::HistogramID aTelemetryID) {
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 Telemetry::Accumulate(
239 Telemetry::LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS, !loaded);
240 }
241
242 if (loaded) {
243 return;
244 }
245
246 // Measure which operation blocks and for how long
247 Telemetry::RuntimeAutoTimer timer(aTelemetryID);
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
GetLength(const LocalStorage * aStorage,uint32_t * aRetval)261 nsresult LocalStorageCache::GetLength(const LocalStorage* aStorage,
262 uint32_t* aRetval) {
263 if (Persist(aStorage)) {
264 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS);
265 if (NS_FAILED(mLoadResult)) {
266 return mLoadResult;
267 }
268 }
269
270 *aRetval = DataSet(aStorage).mKeys.Count();
271 return NS_OK;
272 }
273
GetKey(const LocalStorage * aStorage,uint32_t aIndex,nsAString & aRetval)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(Telemetry::LOCALDOMSTORAGE_GETKEY_BLOCKING_MS);
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
GetKeys(const LocalStorage * aStorage,nsTArray<nsString> & aKeys)299 void LocalStorageCache::GetKeys(const LocalStorage* aStorage,
300 nsTArray<nsString>& aKeys) {
301 if (Persist(aStorage)) {
302 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS);
303 }
304
305 if (NS_FAILED(mLoadResult)) {
306 return;
307 }
308
309 AppendToArray(aKeys, DataSet(aStorage).mKeys.Keys());
310 }
311
GetItem(const LocalStorage * aStorage,const nsAString & aKey,nsAString & aRetval)312 nsresult LocalStorageCache::GetItem(const LocalStorage* aStorage,
313 const nsAString& aKey, nsAString& aRetval) {
314 if (Persist(aStorage)) {
315 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS);
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
SetItem(const LocalStorage * aStorage,const nsAString & aKey,const nsString & aValue,nsString & aOld,const MutationSource aSource)332 nsresult LocalStorageCache::SetItem(const LocalStorage* aStorage,
333 const nsAString& aKey,
334 const nsString& 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(Telemetry::LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS);
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, nsString(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
RemoveItem(const LocalStorage * aStorage,const nsAString & aKey,nsString & aOld,const MutationSource aSource)394 nsresult LocalStorageCache::RemoveItem(const LocalStorage* aStorage,
395 const nsAString& aKey, nsString& aOld,
396 const MutationSource aSource) {
397 if (Persist(aStorage)) {
398 WaitForPreload(Telemetry::LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS);
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 Unused << 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, nsString(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
Clear(const LocalStorage * aStorage,const MutationSource aSource)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(Telemetry::LOCALDOMSTORAGE_CLEAR_BLOCKING_MS);
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 Unused << 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
GetOriginQuotaUsage(const LocalStorage * aStorage) const490 int64_t LocalStorageCache::GetOriginQuotaUsage(
491 const LocalStorage* aStorage) const {
492 return mData[GetDataSetIndex(aStorage)].mOriginQuotaUsage;
493 }
494
UnloadItems(uint32_t aUnloadFlags)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(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS);
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(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS);
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
LoadedCount()526 uint32_t LocalStorageCache::LoadedCount() {
527 MonitorAutoLock monitor(mMonitor);
528 Data& data = mData[kDefaultSet];
529 return data.mKeys.Count();
530 }
531
LoadItem(const nsAString & aKey,const nsString & aValue)532 bool LocalStorageCache::LoadItem(const nsAString& aKey,
533 const nsString& 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 aValue;
543 });
544 return true;
545 }
546
LoadDone(nsresult aRv)547 void LocalStorageCache::LoadDone(nsresult aRv) {
548 MonitorAutoLock monitor(mMonitor);
549 mLoadResult = aRv;
550 mLoaded = true;
551 monitor.Notify();
552 }
553
LoadWait()554 void LocalStorageCache::LoadWait() {
555 MonitorAutoLock monitor(mMonitor);
556 while (!mLoaded) {
557 monitor.Wait();
558 }
559 }
560
561 // StorageUsage
562
StorageUsage(const nsACString & aOriginScope)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:
LoadUsageRunnable(int64_t * aUsage,const int64_t aDelta)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
Run()579 NS_IMETHOD Run() override {
580 *mTarget = mDelta;
581 return NS_OK;
582 }
583 };
584
585 } // namespace
586
LoadUsage(const int64_t aUsage)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
CheckAndSetETLD1UsageDelta(uint32_t aDataSetIndex,const int64_t aDelta,const LocalStorageCache::MutationSource aSource)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 dom
617 } // namespace mozilla
618