diff --git a/blink/renderer/core/dom/document.cc b/blink/renderer/core/dom/document.cc index 4e98dd5efce0..a20093f0e380 100644 --- a/blink/renderer/core/dom/document.cc +++ b/blink/renderer/core/dom/document.cc @@ -8185,6 +8185,12 @@ void Document::ActivateForPrerendering() { if (DocumentLoader* loader = Loader()) loader->NotifyPrerenderingDocumentActivated(); + Vector callbacks; + callbacks.swap(will_dispatch_prerenderingchange_callbacks_); + for (auto& callback : callbacks) { + std::move(callback).Run(); + } + // https://jeremyroman.github.io/alternate-loading-modes/#prerendering-browsing-context-activate // Step 8.3.4 "Fire an event named prerenderingchange at doc." DispatchEvent(*Event::Create(event_type_names::kPrerenderingchange)); @@ -8197,6 +8203,12 @@ void Document::ActivateForPrerendering() { frame->DidActivateForPrerendering(); } +void Document::AddWillDispatchPrerenderingchangeCallback( + base::OnceClosure closure) { + DCHECK(is_prerendering_); + will_dispatch_prerenderingchange_callbacks_.push_back(std::move(closure)); +} + void Document::AddPostPrerenderingActivationStep(base::OnceClosure callback) { DCHECK(is_prerendering_); post_prerendering_activation_callbacks_.push_back(std::move(callback)); diff --git a/blink/renderer/core/dom/document.h b/blink/renderer/core/dom/document.h index 6eb556a8f159..083cd83c87a2 100644 --- a/blink/renderer/core/dom/document.h +++ b/blink/renderer/core/dom/document.h @@ -1689,6 +1689,8 @@ class CORE_EXPORT Document : public ContainerNode, void ActivateForPrerendering(); + void AddWillDispatchPrerenderingchangeCallback(base::OnceClosure); + void AddPostPrerenderingActivationStep(base::OnceClosure callback); class CORE_EXPORT PaintPreviewScope { @@ -1863,6 +1865,10 @@ class CORE_EXPORT Document : public ContainerNode, // https://github.com/jeremyroman/alternate-loading-modes/blob/main/prerendering-state.md#documentprerendering bool is_prerendering_; + // Callbacks to execute upon activation of a prerendered page, just before the + // prerenderingchange event is dispatched. + Vector will_dispatch_prerenderingchange_callbacks_; + // The callback list for post-prerendering activation step. // https://jeremyroman.github.io/alternate-loading-modes/#document-post-prerendering-activation-steps-list Vector post_prerendering_activation_callbacks_; diff --git a/blink/renderer/modules/storage/cached_storage_area.cc b/blink/renderer/modules/storage/cached_storage_area.cc index cf9150051c07..4b98ae5eb00f 100644 --- a/blink/renderer/modules/storage/cached_storage_area.cc +++ b/blink/renderer/modules/storage/cached_storage_area.cc @@ -102,10 +102,13 @@ bool CachedStorageArea::SetItem(const String& key, KURL page_url = source->GetPageUrl(); String source_id = areas_->at(source); String source_string = PackSource(page_url, source_id); - remote_area_->Put(StringToUint8Vector(key, GetKeyFormat()), - StringToUint8Vector(value, value_format), - optional_old_value, source_string, - MakeSuccessCallback(source)); + + if (!is_session_storage_for_prerendering_) { + remote_area_->Put(StringToUint8Vector(key, GetKeyFormat()), + StringToUint8Vector(value, value_format), + optional_old_value, source_string, + MakeSuccessCallback(source)); + } if (!IsSessionStorage()) EnqueuePendingMutation(key, value, old_value, source_string); else if (old_value != value) @@ -127,9 +130,11 @@ void CachedStorageArea::RemoveItem(const String& key, Source* source) { KURL page_url = source->GetPageUrl(); String source_id = areas_->at(source); String source_string = PackSource(page_url, source_id); - remote_area_->Delete(StringToUint8Vector(key, GetKeyFormat()), - optional_old_value, source_string, - MakeSuccessCallback(source)); + if (!is_session_storage_for_prerendering_) { + remote_area_->Delete(StringToUint8Vector(key, GetKeyFormat()), + optional_old_value, source_string, + MakeSuccessCallback(source)); + } if (!IsSessionStorage()) EnqueuePendingMutation(key, String(), old_value, source_string); else @@ -164,8 +169,10 @@ void CachedStorageArea::Clear(Source* source) { KURL page_url = source->GetPageUrl(); String source_id = areas_->at(source); String source_string = PackSource(page_url, source_id); - remote_area_->DeleteAll(source_string, std::move(new_observer), - MakeSuccessCallback(source)); + if (!is_session_storage_for_prerendering_) { + remote_area_->DeleteAll(source_string, std::move(new_observer), + MakeSuccessCallback(source)); + } if (!IsSessionStorage()) EnqueuePendingMutation(String(), String(), String(), source_string); else if (!already_empty) @@ -182,10 +189,12 @@ CachedStorageArea::CachedStorageArea( AreaType type, scoped_refptr origin, scoped_refptr task_runner, - StorageNamespace* storage_namespace) + StorageNamespace* storage_namespace, + bool is_session_storage_for_prerendering) : type_(type), origin_(std::move(origin)), storage_namespace_(storage_namespace), + is_session_storage_for_prerendering_(is_session_storage_for_prerendering), task_runner_(std::move(task_runner)), areas_(MakeGarbageCollected, String>>()) { BindStorageArea(); @@ -218,6 +227,7 @@ void CachedStorageArea::BindStorageArea( void CachedStorageArea::ResetConnection( mojo::PendingRemote new_area) { + DCHECK(!is_session_storage_for_prerendering_); remote_area_.reset(); BindStorageArea(std::move(new_area)); diff --git a/blink/renderer/modules/storage/cached_storage_area.h b/blink/renderer/modules/storage/cached_storage_area.h index 9196428d1984..68caa52aab96 100644 --- a/blink/renderer/modules/storage/cached_storage_area.h +++ b/blink/renderer/modules/storage/cached_storage_area.h @@ -63,7 +63,8 @@ class MODULES_EXPORT CachedStorageArea CachedStorageArea(AreaType type, scoped_refptr origin, scoped_refptr ipc_runner, - StorageNamespace* storage_namespace); + StorageNamespace* storage_namespace, + bool is_session_storage_for_prerendering); // These correspond to blink::Storage. unsigned GetLength(); @@ -97,6 +98,10 @@ class MODULES_EXPORT CachedStorageArea void ResetConnection( mojo::PendingRemote new_area = {}); + bool is_session_storage_for_prerendering() const { + return is_session_storage_for_prerendering_; + } + void SetRemoteAreaForTesting( mojo::PendingRemote area) { remote_area_.Bind(std::move(area)); @@ -177,6 +182,12 @@ class MODULES_EXPORT CachedStorageArea const AreaType type_; const scoped_refptr origin_; const WeakPersistent storage_namespace_; + // Session storage state for prerendering is initialized by cloning the + // primary session storage state. It is used locally by the prerendering + // context, and does not get propagated back to the primary state (i.e., via + // remote_area_). For more details: + // https://docs.google.com/document/d/1I5Hr8I20-C1GBr4tAXdm0U8a1RDUKHt4n7WcH4fxiSE/edit?usp=sharing + const bool is_session_storage_for_prerendering_; const scoped_refptr task_runner_; std::unique_ptr map_; diff --git a/blink/renderer/modules/storage/cached_storage_area_test.cc b/blink/renderer/modules/storage/cached_storage_area_test.cc index b43460747d00..82997841f745 100644 --- a/blink/renderer/modules/storage/cached_storage_area_test.cc +++ b/blink/renderer/modules/storage/cached_storage_area_test.cc @@ -38,7 +38,7 @@ class CachedStorageAreaTest : public testing::Test { : CachedStorageArea::AreaType::kLocalStorage; cached_area_ = base::MakeRefCounted( area_type, kOrigin, scheduler::GetSingleThreadTaskRunnerForTesting(), - nullptr); + nullptr, /*is_session_storage_for_prerendering=*/false); cached_area_->SetRemoteAreaForTesting( mock_storage_area_.GetInterfaceRemote()); source_area_ = MakeGarbageCollected(kPageUrl); diff --git a/blink/renderer/modules/storage/dom_window_storage.cc b/blink/renderer/modules/storage/dom_window_storage.cc index 672329566061..9888455e9075 100644 --- a/blink/renderer/modules/storage/dom_window_storage.cc +++ b/blink/renderer/modules/storage/dom_window_storage.cc @@ -90,10 +90,16 @@ StorageArea* DOMWindowStorage::sessionStorage( StorageNamespace::From(window->GetFrame()->GetPage()); if (!storage_namespace) return nullptr; - auto storage_area = - storage_namespace->GetCachedArea(window->GetSecurityOrigin()); + scoped_refptr cached_storage_area; + if (window->document()->IsPrerendering()) { + cached_storage_area = storage_namespace->CreateCachedAreaForPrerender( + window->GetSecurityOrigin()); + } else { + cached_storage_area = + storage_namespace->GetCachedArea(window->GetSecurityOrigin()); + } session_storage_ = - StorageArea::Create(window, std::move(storage_area), + StorageArea::Create(window, std::move(cached_storage_area), StorageArea::StorageType::kSessionStorage); if (!session_storage_->CanAccessStorage()) { diff --git a/blink/renderer/modules/storage/storage_area.cc b/blink/renderer/modules/storage/storage_area.cc index 32373c87d730..a40cd639f958 100644 --- a/blink/renderer/modules/storage/storage_area.cc +++ b/blink/renderer/modules/storage/storage_area.cc @@ -41,6 +41,7 @@ #include "third_party/blink/renderer/modules/storage/storage_namespace.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/weborigin/security_origin.h" +#include "third_party/blink/renderer/platform/wtf/functional.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" namespace blink { @@ -73,6 +74,11 @@ StorageArea::StorageArea(LocalDOMWindow* window, DCHECK(window); DCHECK(cached_area_); cached_area_->RegisterSource(this); + if (cached_area_->is_session_storage_for_prerendering()) { + DomWindow()->document()->AddWillDispatchPrerenderingchangeCallback( + WTF::Bind(&StorageArea::OnDocumentActivatedForPrerendering, + WrapWeakPersistent(this))); + } } unsigned StorageArea::length(ExceptionState& exception_state) const { @@ -246,4 +252,18 @@ blink::WebScopedVirtualTimePauser StorageArea::CreateWebScopedVirtualTimePauser( ->CreateWebScopedVirtualTimePauser(name, duration); } +void StorageArea::OnDocumentActivatedForPrerendering() { + StorageNamespace* storage_namespace = + StorageNamespace::From(DomWindow()->GetFrame()->GetPage()); + if (!storage_namespace) + return; + + // Swap out the session storage state used within prerendering, and replace it + // with the normal session storage state. For more details: + // https://docs.google.com/document/d/1I5Hr8I20-C1GBr4tAXdm0U8a1RDUKHt4n7WcH4fxiSE/edit?usp=sharing + cached_area_ = + storage_namespace->GetCachedArea(DomWindow()->GetSecurityOrigin()); + cached_area_->RegisterSource(this); +} + } // namespace blink diff --git a/blink/renderer/modules/storage/storage_area.h b/blink/renderer/modules/storage/storage_area.h index 941e2bdf63d4..698c270cf873 100644 --- a/blink/renderer/modules/storage/storage_area.h +++ b/blink/renderer/modules/storage/storage_area.h @@ -92,7 +92,10 @@ class StorageArea final : public ScriptWrappable, private: void RecordModificationInMetrics(); - const scoped_refptr cached_area_; + + void OnDocumentActivatedForPrerendering(); + + scoped_refptr cached_area_; StorageType storage_type_; const bool should_enqueue_events_; diff --git a/blink/renderer/modules/storage/storage_namespace.cc b/blink/renderer/modules/storage/storage_namespace.cc index b10a0f2d9edc..3827a9afddbc 100644 --- a/blink/renderer/modules/storage/storage_namespace.cc +++ b/blink/renderer/modules/storage/storage_namespace.cc @@ -100,11 +100,23 @@ scoped_refptr StorageNamespace::GetCachedArea( result = base::MakeRefCounted( IsSessionStorage() ? CachedStorageArea::AreaType::kSessionStorage : CachedStorageArea::AreaType::kLocalStorage, - origin, controller_->TaskRunner(), this); + origin, controller_->TaskRunner(), this, + /*is_session_storage_for_prerendering=*/false); cached_areas_.insert(std::move(origin), result); return result; } +scoped_refptr StorageNamespace::CreateCachedAreaForPrerender( + const SecurityOrigin* origin_ptr) { + DCHECK((IsSessionStorage())); + scoped_refptr origin(origin_ptr); + return base::MakeRefCounted( + IsSessionStorage() ? CachedStorageArea::AreaType::kSessionStorage + : CachedStorageArea::AreaType::kLocalStorage, + origin, controller_->TaskRunner(), this, + /*is_session_storage_for_prerendering=*/true); +} + void StorageNamespace::CloneTo(const String& target) { DCHECK(IsSessionStorage()) << "Cannot clone a local storage namespace."; EnsureConnected(); diff --git a/blink/renderer/modules/storage/storage_namespace.h b/blink/renderer/modules/storage/storage_namespace.h index bb62188a8d2d..ec3658217d66 100644 --- a/blink/renderer/modules/storage/storage_namespace.h +++ b/blink/renderer/modules/storage/storage_namespace.h @@ -84,6 +84,9 @@ class MODULES_EXPORT StorageNamespace final scoped_refptr GetCachedArea(const SecurityOrigin* origin); + scoped_refptr CreateCachedAreaForPrerender( + const SecurityOrigin* origin); + // Only valid to call this if |this| and |target| are session storage // namespaces. void CloneTo(const String& target); diff --git a/blink/web_tests/wpt_internal/prerender/resources/session-storage-carry-over-to-prerender-page.html b/blink/web_tests/wpt_internal/prerender/resources/session-storage-carry-over-to-prerender-page.html new file mode 100644 index 000000000000..c3a6fe072145 --- /dev/null +++ b/blink/web_tests/wpt_internal/prerender/resources/session-storage-carry-over-to-prerender-page.html @@ -0,0 +1,20 @@ + + + + + + diff --git a/blink/web_tests/wpt_internal/prerender/resources/session-storage-isolated-while-prerendering.html b/blink/web_tests/wpt_internal/prerender/resources/session-storage-isolated-while-prerendering.html new file mode 100644 index 000000000000..4fcd5ac1f9f1 --- /dev/null +++ b/blink/web_tests/wpt_internal/prerender/resources/session-storage-isolated-while-prerendering.html @@ -0,0 +1,41 @@ + + + + + + diff --git a/blink/web_tests/wpt_internal/prerender/resources/session-storage-no-leak-to-initiator-page.html b/blink/web_tests/wpt_internal/prerender/resources/session-storage-no-leak-to-initiator-page.html new file mode 100644 index 000000000000..0348439363d0 --- /dev/null +++ b/blink/web_tests/wpt_internal/prerender/resources/session-storage-no-leak-to-initiator-page.html @@ -0,0 +1,35 @@ + + + + + + diff --git a/blink/web_tests/wpt_internal/prerender/resources/session-storage-swap-after-activate.html b/blink/web_tests/wpt_internal/prerender/resources/session-storage-swap-after-activate.html new file mode 100644 index 000000000000..f81841b1a44f --- /dev/null +++ b/blink/web_tests/wpt_internal/prerender/resources/session-storage-swap-after-activate.html @@ -0,0 +1,76 @@ + + + + + + diff --git a/blink/web_tests/wpt_internal/prerender/resources/session-storage-utils.js b/blink/web_tests/wpt_internal/prerender/resources/session-storage-utils.js new file mode 100644 index 000000000000..d76886077944 --- /dev/null +++ b/blink/web_tests/wpt_internal/prerender/resources/session-storage-utils.js @@ -0,0 +1,76 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +function getSessionStorageKeys() { + let keys = []; + let txt = ''; + for (let i = 0; i < sessionStorage.length; ++i) { + keys.push(sessionStorage.key(i)); + } + keys.sort(); + keys.forEach((key) => { + if (txt.length) { + txt += ', '; + } + txt += key; + }); + return txt; +} + +function getNextMessage(channel) { + return new Promise(resolve => { + channel.addEventListener('message', e => { + resolve(e.data); + }, {once: true}); + }); +} + +// session_storage_test() is a utility function for running session storage +// related tests that open a initiator page using window.open(). +function session_storage_test(testPath) { + promise_test(async t => { + const testChannel = new BroadcastChannel('test-channel'); + t.add_cleanup(() => { + testChannel.close(); + }); + const gotMessage = getNextMessage(testChannel); + const url = 'resources/' + testPath; + window.open(url, '_blank', 'noopener'); + assert_equals(await gotMessage, 'Done'); + }, testPath); +} + +// RunSessionStorageTest() is a utility function for running session storage +// related tests that requires coordinated code execution on both the initiator +// page and the prerendering page. The passed |func| function will be called +// with the following arguments: +// - isPrerendering: Whether the |func| is called in the prerendering page. +// - url: The URL of the prerendering page. |func| should call +// startPrerendering(url) when |isPrerendering| is false to start the +// prerendering. +// - channel: A Broadcast Channel which can be used to coordinate the code +// execution on the initiator page and the prerendering page. +// - done: A function that should be called when the test completes +// successfully. +async function RunSessionStorageTest(func) { + const url = new URL(document.URL); + url.searchParams.set('prerendering', ''); + const params = new URLSearchParams(location.search); + // The main test page loads the initiator page, then the initiator page will + // prerender itself with the `prerendering` parameter. + const isPrerendering = params.has('prerendering'); + const prerenderChannel = new BroadcastChannel('prerender-channel'); + const testChannel = new BroadcastChannel('test-channel'); + window.addEventListener('unload', () => { + prerenderChannel.close(); + testChannel.close(); + }); + try { + await func(isPrerendering, url.toString(), prerenderChannel, () => { + testChannel.postMessage('Done'); + }) + } catch (e) { + testChannel.postMessage(e.toString()); + } +} diff --git a/blink/web_tests/wpt_internal/prerender/session-storage.html b/blink/web_tests/wpt_internal/prerender/session-storage.html new file mode 100644 index 000000000000..8f6bfc109294 --- /dev/null +++ b/blink/web_tests/wpt_internal/prerender/session-storage.html @@ -0,0 +1,27 @@ + + +Same-origin prerendering can access sessionStorage + + + + + + +