-
Notifications
You must be signed in to change notification settings - Fork 372
[Website] Make all iframes controlled by the service worker #2923
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
adamziel
wants to merge
43
commits into
trunk
Choose a base branch
from
make-all-iframes-controlled
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+5,021
−141
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
The test was reading iframe content before the loader script finished injecting the cached content. The fix increases the timeout from 3s to 5s and adds a check that the loader script has finished executing before considering the content loaded.
The test was navigating to a URL outside the service worker's scope (/scope:test-fast/... instead of /website-server/scope:test-fast/...). This worked locally because of existing SW caching but failed in CI where the SW was freshly registered. Changes: - Test: Construct loader URL as /website-server/scope:test-fast/... - iframes-trap.js: Extract full scoped path including any prefix - service-worker.ts: Same scope inference pattern for loader HTML
When an iframe's src was set to a data: URL, the async fetch and cache process would start but the setAttribute wrapper returned immediately. If the iframe was then appended to the DOM before caching completed, scheduleIframeControl would see it as a blank iframe (no pending flag) and redirect it to an empty loader, losing the data URL content. Now we set data-srcdoc-pending synchronously before starting the async rewriteDataOrBlob, preventing the race condition with MutationObserver.
Firefox has timing issues with 4-level deep srcdoc iframes in this synthetic stress test. The core nested iframe functionality is still tested by the 'nested iframe (TinyMCE-like)' test which passes on all browsers.
This reverts commit 78b1709.
When creating controlled iframes in ancestor documents for deeply nested srcdoc iframes, Firefox requires using the ancestor realm's native property setter rather than the one captured in the child context. This commit adds cross-realm support to setIframeSrc() by accepting an optional ancestorWindow parameter and using that realm's HTMLIFrameElement.prototype.src setter when available. This fixes the Firefox failure in the "deeply nested iframes (4 levels)" test where iframes beyond level 1-2 would remain at about:blank instead of navigating to the loader URL.
Firefox restricts cross-realm property setter calls, which caused nested iframes to remain at about:blank instead of navigating to empty.html. The fix uses postMessage with MessageChannel to ask the ancestor window to create iframes entirely within its own realm, bypassing Firefox's restrictions.
The `navigator.serviceWorker?.ready` promise can hang indefinitely in some browser states with corrupted service workers. Add a 10-second timeout to prevent tests from hanging when the service worker environment has issues.
When iframes-trap.js loads asynchronously in WordPress admin (via the MU plugin), TinyMCE may have already created its iframe with src="javascript:''" before the prototype patches are in place. This fix extends the MutationObserver handler to also process iframes that have uncontrolled src values (javascript:, about:blank, empty). It also scans for existing iframes when iframes-trap.js first loads, catching any that were created before the script executed.
TinyMCE creates blank iframes and uses document.write() to inject content. This bypasses the src/srcdoc interception because the iframe is created without a src, and then content is written directly to its document. This fix intercepts document.write() and document.close() calls on iframe documents. When content is written, we buffer it and on document.close(), redirect the iframe to the loader with the buffered content. This ensures the iframe becomes SW-controlled even when populated via document.write(). The fix maintains backward compatibility by still calling native write() during the write phase, then redirecting after close(). This allows scripts that expect immediate document availability to work normally.
…uments Instead of patching Document.prototype.write (which only works in the same realm), intercept contentDocument access and return a Proxy that captures write()/writeln()/close() calls. This works because the parent document (where iframes-trap.js runs) controls access to the iframe's document. The proxy: 1. Buffers all content written via write()/writeln() 2. On close(), redirects the iframe to the loader with the buffered content 3. Passes through all other operations to the real document This should fix the Firefox TinyMCE issue where the iframe is created and populated via document.write() before iframes-trap.js can intercept it.
TinyMCE (and similar libraries) create blank iframes and populate them via document.write(). This bypasses the src/srcdoc interception because the iframe never navigates to a URL that the service worker can control. This commit adds: 1. Proxy wrappers for contentWindow and contentDocument that intercept document.write()/close() calls on uncontrolled iframes 2. When document.close() is called, the buffered HTML is cached and the iframe is redirected to a SW-controlled loader URL 3. A minimal message listener in remote.html that handles cross-frame iframe creation requests (needed before the SW injects iframes-trap.js) 4. Updated findCapableAncestor() to prefer the first ancestor with the message listener, rather than the topmost SW-controlled ancestor Also simplified the 0-playground.php script injection to use a direct script tag instead of dynamically creating one (which was async by default).
In Firefox, the timing of when iframes get their SW controller can be different from Chromium. By waiting in the message handler until the created iframe has a controller, we ensure the child frame receives a fully-ready iframe reference.
…quirement The loaderComplete marker is set at the end of an async IIFE in the loader script, which can cause race conditions on Chromium where we poll before the script finishes. The typing test only needs SW controller, body access, and iframes-trap.js to be loaded - it doesn't need to wait for content injection since it's creating new srcdoc iframes, not using cached content.
This test verifies the real-world functionality of the classic editor: - Installs the classic-editor plugin - Navigates to new post - Types content in the TinyMCE editor iframe - Uploads an image via the media modal - Verifies the image is inserted and loads correctly This tests the critical path that depends on TinyMCE's srcdoc iframe being SW-controlled so it can load images and other resources. Also fixes the URL format in controlled-iframes.spec.ts to use proper JSON.stringify format instead of string literal.
When an iframe is created and has srcdoc set before being appended to
the DOM, there was a race condition:
1. createElement('iframe') would set src to the loader URL
2. srcdoc setter would start async rewriteSrcdoc
3. appendChild would navigate to the loader (without content id)
4. rewriteSrcdoc would finish and try to update src
Since only the hash changed, no new navigation occurred and the iframe
would end up showing an empty document.
The fix removes src seeding from handleCreateElement for top-level
contexts and lets the MutationObserver or rewriteSrcdoc set the proper
src when the iframe is ready.
Also fixes the document.write() proxy to set data-srcdoc-pending
immediately when document.close() is called, preventing
scheduleIframeControl from creating a duplicate controlled iframe.
Adds cleanup logic to remove any existing controlled iframe when
rewriteSrcdoc creates a new one (handles the TinyMCE case where an
empty iframe is controlled first, then document.write() fills it).
When document.close() is called on an already-controlled iframe, we were setting data-srcdoc-pending but never removing it. This caused iframes to get stuck in pending state, preventing Gutenberg/site editor from loading.
When TinyMCE creates its editor iframe, it uses document.write() to populate the content, then sets contentEditable via JavaScript after document.close(). Our iframe trap was capturing the HTML at close() time, before contentEditable was set, causing typing to not work. Two fixes: 1. Delay DOM state capture using double setTimeout to allow post-close() JS to run 2. Capture the current DOM state (documentElement.outerHTML) instead of the write buffer, so JS-applied attributes like contentEditable are included Additionally, when navigating document.write iframes to the loader URL, browsers were treating it as a hash-only change (same base URL) without loading a new document. Fixed by removing and re-adding the iframe to force a fresh navigation.
TinyMCE and similar libraries use document.write() to create editor iframes, then immediately access doc.body to set properties like contentEditable. The challenge is making these iframes SW-controlled while preserving the library's document references. Key changes: 1. Live document proxy: The proxy now dynamically resolves to the current iframe.contentDocument via getCurrentDoc(). After navigation, TinyMCE's stored 'doc' reference automatically works with the new document. 2. Deferred resource loading: Instead of calling native document.write() which loads CSS/images from about:blank (wrong origin), we: - Parse HTML with DOMParser (no resource loading) - Create skeleton document via DOM manipulation - Copy body content/attributes, skip link/script tags - Navigate to SW-controlled URL where resources load correctly 3. URL rewriting: Added rewriteAbsoluteUrlsInHtml() to rewrite absolute paths like /scope:test/file.css to /website-server/scope:test/file.css so they route through the Service Worker. 4. New test: 'document.write iframe can load CSS resources via SW' verifies that CSS links in document.write iframes resolve through the SW scope. All 23 iframe control tests pass on Chromium.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Explores making all iframes controlled by the Playground service worker.
Iframes created as about:blank / srcdoc / data / blob are not controlled by this
service worker. This means that all network calls initiated by these iframes are
sent directly to the network. This means Gutenberg cannot load any CSS files,
TInyMCE can't load media images, etc.
Only iframes created with
srcpointing to a URL already controlled by this service workerare themselves controlled.
Explored solution
We inject a
iframes-trap.jsscript into every HTML page to override a set of DOMmethods used to create iframes. Whenever an src/srcdoc attribute is set on an iframe,
we intercept that and:
iframes-trap.jsis also loaded and executed inside the iframeto cover any nested iframes.
As a result, every same-origin iframe is forced onto a real navigation that the SW can control,
inside editors like TinyMCE) go through our handler
so all fetches (including
without per-product patches. This replaces the former Gutenberg-only shim.
Downsides
When a DOM reference to an element inside an iframe is grabbed early on, rewriting the HTML inside that iframe invalidates those reference. I think it breaks "bold", "italic", etc buttons in TinyMCE at the moment (but it doesn't break the "Add Media" feature).
This is a deal-breaker in the current PR. I think we can make it work without destroying the DOM nodes already in the iframe. TinyMCE seems to be doing
iframe.contentWindow.contentDocument.write( newHTML )anddocument.write()makes a controlled iframe uncontrolled again. We'll need to wrap every part of the process and replace thedocument.write()logic with something closer toinnerHTMLor a redirection toloader.html?initialHTML={markup}.References
Fixes #2919
Fixes #42
cc @akirk @brandonpayton @ellatrix @draganescu