Media: Enable client-side media uploads in the Media Library - #12585
Media: Enable client-side media uploads in the Media Library #12585adamsilverstein wants to merge 16 commits into
Conversation
The client-side media pipeline needs SharedArrayBuffer, which requires a cross-origin isolated context. Core only isolates the block editor screens, so uploads from the Media Library grid cannot use the pipeline. Hook the existing Document-Isolation-Policy output buffer on load-upload.php, gated to grid mode for users who can upload files. The mode is resolved the same way upload.php resolves it later in the request, without updating the saved user option. List mode has no pipeline integration and stays untouched, avoiding isolation side effects on a screen that gets no benefit.
Grid uploads go through wp.Uploader/plupload to async-upload.php, doing all image processing server-side even when the browser could handle it. Add a media-library-upload script that configures the @wordpress/upload-media store and intercepts plupload's FilesAdded at a higher priority, routing each file through the pipeline: REST upload of the original, client-side thumbnails via wasm-vips, then sideload and finalize. The grid UI is preserved by mirroring wp-plupload's placeholder tiles, progress, queue reset, and error sidebar. mediaSideload/mediaFinalize are thin apiFetch wrappers because the @wordpress/media-utils equivalents are private APIs. When the browser is not cross-origin isolated or lacks client-side support, the script no-ops and classic plupload keeps handling uploads, so degraded environments lose nothing.
Assert the Document-Isolation-Policy header is sent on the grid and not in list mode, that a JPEG upload flows through the REST create, sideload, and finalize endpoints with no async-upload.php requests, and that a disallowed file type surfaces in the error sidebar. Playwright's Chromium build ships without Document-Isolation-Policy support, so the upload assertions skip when the context is not cross-origin isolated; the header assertions still run everywhere.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
The ticket did not exist yet when the tests were written; annotate all new test methods now that it has been filed.
CI's Playwright Chromium now supports Document-Isolation-Policy, so the pipeline E2E test runs for real instead of skipping. The previous asset was the 50x50 phpunit fixture, smaller than every registered sub-size, so the pipeline correctly generated zero thumbnails and the sideload-count assertion failed. Swap in the 640x480 canola.jpg fixture so thumbnail and medium sub-sizes are generated and sideloaded, and update the skip comments that claimed Playwright lacks DIP support. See #65661
The suites gated the isolation callback and enqueue but never proved the callback is actually wired to load-upload.php, that the inline settings match what the server computes, or that the pipeline's output is real: * Assert the default-filters.php hook registration, without which none of the gating logic runs. * Assert the inline settings are exactly the JSON encoding of wp_get_media_library_upload_settings(), and that the upload_mimes, image_strip_meta, and image_max_bit_depth filters flow through to the settings the browser pipeline consumes. * Extend the E2E happy path past request counting: the finalized attachment must carry thumbnail and medium sub-sizes in its metadata, and the sideloaded thumbnail file must actually be servable.
Interrupting a client-side pipeline upload is worse than interrupting a classic plupload one: classic uploads complete server-side once the bytes arrive, but an interrupted pipeline upload loses browser-generated thumbnails that were not sideloaded yet and leaves the attachment unfinalized. Trigger the browser's leave confirmation while the progress map is non-empty; the guard is scoped to the pipeline, so classic uploads behave exactly as before. See #65662
…eline media-new.php was the last admin upload surface without pipeline integration: plupload-handlers creates a raw plupload.Uploader (wp.Uploader never loads there) posting to async-upload.php with all image processing server-side. Extend cross-origin isolation to the screen via a new wp_set_up_media_new_cross_origin_isolation() on load-media-new.php, gated on client-side processing being enabled and the upload_files capability (the screen itself already requires it). Add a new media-new-upload admin script that binds a higher-priority FilesAdded handler on the plupload-handlers uploader instance and routes files through the @wordpress/upload-media store, sharing its settings with the grid integration via wp_get_media_library_upload_settings(). The screen's existing UI helpers are reused rather than replicated: fileQueued() builds the progress item, uploadSuccess() renders the finished attachment row through the existing async-upload.php markup endpoint, itemAjaxError() surfaces per-file errors, and uploadComplete() runs when the queue drains, so the screen looks and behaves unchanged. The same beforeunload guard as the grid warns while pipeline uploads are in flight. When the browser is not cross-origin isolated or lacks client-side support the script no-ops and the classic plupload flow (and the browser-uploader HTML fallback form) keep working unchanged. See #65662
Assert the Document-Isolation-Policy header on media-new.php, the happy-path pipeline upload (create, sideload, and finalize via REST with no file upload through async-upload.php; the fetch=3 markup POST is expected and excluded), and the disallowed-file-type error path. CI's Playwright Chromium supports Document-Isolation-Policy, so the full pipeline is exercised there; the upload assertions still skip in browsers where isolation is unavailable, and the existing media-upload spec keeps covering the classic path as the degradation check. See #65662
The test asserts that a failed SVG upload on media-new.php shows a dismissible error. With the client-side pipeline active (CI's Chromium is cross-origin isolated), the disallowed file is rejected client-side and the error renders through the standard per-file error UI, where the dismiss control is a link, instead of the server-rendered async-upload.php notice, where it is a button. Target the .dismiss control by class so both variants pass; the error text and the dismissal behavior asserted are unchanged. See #65662
…oreunload guards The beforeunload guards from this branch had no test coverage at all, and the media-new.php suites had the same blind spots just closed for the grid on the base branch: * Assert the load-media-new.php hook registration in default-filters.php and that the inline settings are exactly the JSON encoding of wp_get_media_library_upload_settings(). * Add an E2E test per screen for the beforeunload guard: hold sideload requests via routing so the upload is deterministically in flight, then dispatch a synthetic cancelable beforeunload and assert it is prevented while uploading and no longer prevented after completion. * Extend the media-new.php E2E happy path past request counting: the finalized attachment must carry thumbnail and medium sub-sizes and the sideloaded thumbnail file must actually be servable.
There was a problem hiding this comment.
Pull request overview
Extends WordPress’s client-side media processing pipeline beyond the block editor to the remaining admin upload surfaces (Media Library grid and Media > Add New), including cross-origin isolation setup, upload routing via the @wordpress/upload-media store, and new automated coverage to validate the end-to-end pipeline and headers.
Changes:
- Adds Media Library mode resolution and new helpers to enable Document-Isolation-Policy isolation and enqueue upload-routing scripts for
upload.php(grid) andmedia-new.php. - Introduces new admin upload integration scripts (
media-library-upload,media-new-upload) that intercept plupload flows and route files through the REST-based client-side pipeline. - Adds comprehensive PHPUnit + Playwright E2E tests covering header gating, pipeline upload behavior (create/sideload/finalize), beforeunload guards, and error handling.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/phpunit/tests/media/wpMediaNewCrossOriginIsolation.php | Adds PHPUnit coverage for DIP isolation setup on media-new.php. |
| tests/phpunit/tests/media/wpMediaLibraryCrossOriginIsolation.php | Adds PHPUnit coverage for DIP isolation setup and Media Library mode resolution on upload.php grid. |
| tests/phpunit/tests/media/wpEnqueueMediaNewUpload.php | Adds PHPUnit coverage for enqueueing media-new-upload and inline settings. |
| tests/phpunit/tests/media/wpEnqueueMediaLibraryUpload.php | Adds PHPUnit coverage for enqueueing media-library-upload, inline settings, and filter plumbing into settings. |
| tests/e2e/specs/media-upload.test.js | Adjusts an existing E2E assertion to support both server and client-side rejection UIs. |
| tests/e2e/specs/media-new-client-side-upload.test.js | Adds E2E coverage for media-new.php DIP header, pipeline upload, beforeunload guard, and disallowed types. |
| tests/e2e/specs/media-library-client-side-upload.test.js | Adds E2E coverage for upload.php grid DIP header, pipeline upload, beforeunload guard, and disallowed types (plus list-mode header absence). |
| src/wp-includes/script-loader.php | Registers the new admin script handles and their dependencies/translations. |
| src/wp-includes/media.php | Adds mode helper, isolation setup helpers, shared upload settings helper, and enqueue helpers for the two upload surfaces. |
| src/wp-includes/default-filters.php | Wires the new isolation setup callbacks into the relevant load-* hooks. |
| src/wp-admin/upload.php | Enqueues the new grid upload integration script in Media Library grid mode. |
| src/wp-admin/media-new.php | Enqueues the new Media > Add New upload integration script. |
| src/js/_enqueues/admin/media-new-upload.js | Implements media-new.php plupload interception, pipeline routing, UI mirroring, progress sync, and beforeunload guard. |
| src/js/_enqueues/admin/media-library-upload.js | Implements Media Library grid uploader interception, pipeline routing, UI mirroring, progress sync, and beforeunload guard. |
| Gruntfile.js | Adds build mappings for the two new admin scripts. |
Comments suppressed due to low confidence (1)
src/wp-includes/media.php:6846
wp_enqueue_media_new_upload()enqueues the pipeline integration on any secure origin, butwp_start_cross_origin_isolation_output_buffer()(and thereforecrossOriginIsolated) is Chromium 137+ only. On other browsers this script cannot run and will always no-op, so enqueueing it adds avoidable page weight. Consider gating enqueueing onwp_get_chromium_major_version() >= 137to skip loading unused bundles in browsers that can’t be isolated by DIP.
if ( ! wp_is_client_side_media_processing_enabled() ) {
return;
}
wp_enqueue_script( 'media-new-upload' );
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
tests/phpunit/tests/media/wpEnqueueMediaLibraryUpload.php:46
- tear_down() restores HTTP_HOST but not HTTP_USER_AGENT. If set_up() sets a Chromium UA for deterministic enqueue behavior, tear_down() should restore/unset it to avoid leaking the UA into later tests.
if ( null === $this->original_http_host ) {
unset( $_SERVER['HTTP_HOST'] );
} else {
$_SERVER['HTTP_HOST'] = $this->original_http_host;
}
Trac ticket: https://core.trac.wordpress.org/ticket/65661
Description
Client-side media processing currently only works in the block editor - uploads from the Media Library grid and the Add New Media File screen still send the original file to
async-upload.phpand generate every sub-size on the server. This PR routes uploads on both screens through the same client-side pipeline the editor uses, so the browser resizes the image and generates the thumbnails.Why this matters: server-side image processing is a common source of timeouts and memory errors on large images, and results vary depending on what image libraries the host has installed. Processing in the browser avoids both problems, and users get the same upload experience everywhere media is uploaded, not just in the editor.
How it works:
Both screens send the
Document-Isolation-Policyheader so the page is cross-origin isolated, which the wasm-vips image library requires. A small script on each screen intercepts files as they are added to the existing uploader and runs them through the@wordpress/upload-mediastore instead: the original is uploaded via the REST API, the browser generates the sub-sizes and sideloads them, then the attachment is finalized. The existing UI - progress bars, grid tiles, error notices - is reused, so the screens look and behave unchanged. While an upload is in flight the page warns before you navigate away, since an interrupted client-side upload would lose thumbnails that were not sideloaded yet.If the browser does not support cross-origin isolation (currently Chromium 137+ on a secure origin) or client-side processing is disabled, the scripts do nothing and uploads fall back to the classic flow unchanged.
Ports adamsilverstein/client-side-media-everywhere#49 into core. Includes e2e coverage for both screens plus 33 new PHPUnit tests.
Testing Instructions
Test this pull request with WordPress Playground.
Test in Chrome 137 or newer on a secure origin (https or localhost).
Check that client-side processing is enabled:
crossOriginIsolatedin the console - it should returntrue. You can also confirm the page response includes theDocument-Isolation-Policy: isolate-and-credentiallessheader in the Network tab. Client-side processing is on by default in a secure context; thewp_client_side_media_processing_enabledfilter can turn it off.Verify uploads go through the client-side pipeline:
wp/v2/mediacreating the attachment, followed by sideload and finalize requests - and no file POST toasync-upload.php. That is how you can tell the client-side pipeline handled the upload.?browser-uploaderon media-new.php) - files should upload throughasync-upload.phpas before.