Skip to content

Commit 3fa59e3

Browse files
Media: Route media modal uploads through the client-side pipeline.
Rename the Media Library grid integration to media-frame-upload and enqueue it from wp_enqueue_media() on every screen wp_set_up_cross_origin_isolation() isolated, so the modal the block editor, the site editor and the block widgets screen open uploads through the same pipeline as the grid. Screens that were not isolated, such as the Customizer, keep their classic uploads and do not download the pipeline bundles. Let the shared pipeline script adopt the upload-media store the block editor has already configured instead of rendering a second provider into it, and only intercept uploaders that post attachments so a plugin's own wp.Uploader is left alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015q272S5jKgWCuQYrbz2SKb
1 parent 2309e3f commit 3fa59e3

12 files changed

Lines changed: 421 additions & 66 deletions

File tree

‎Gruntfile.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ module.exports = function(grunt) {
530530
[ WORKING_DIR + 'wp-admin/js/language-chooser.js' ]: [ './src/js/_enqueues/lib/language-chooser.js' ],
531531
[ WORKING_DIR + 'wp-admin/js/link.js' ]: [ './src/js/_enqueues/admin/link.js' ],
532532
[ WORKING_DIR + 'wp-admin/js/media-gallery.js' ]: [ './src/js/_enqueues/deprecated/media-gallery.js' ],
533-
[ WORKING_DIR + 'wp-admin/js/media-library-upload.js' ]: [ './src/js/_enqueues/admin/media-library-upload.js' ],
533+
[ WORKING_DIR + 'wp-admin/js/media-frame-upload.js' ]: [ './src/js/_enqueues/admin/media-frame-upload.js' ],
534534
[ WORKING_DIR + 'wp-admin/js/media-new-upload.js' ]: [ './src/js/_enqueues/admin/media-new-upload.js' ],
535535
[ WORKING_DIR + 'wp-admin/js/media-upload-pipeline.js' ]: [ './src/js/_enqueues/admin/media-upload-pipeline.js' ],
536536
[ WORKING_DIR + 'wp-admin/js/media-upload.js' ]: [ './src/js/_enqueues/admin/media-upload.js' ],
@@ -1287,7 +1287,7 @@ module.exports = function(grunt) {
12871287
'src/wp-admin/js/language-chooser.js': 'src/js/_enqueues/lib/language-chooser.js',
12881288
'src/wp-admin/js/link.js': 'src/js/_enqueues/admin/link.js',
12891289
'src/wp-admin/js/media-gallery.js': 'src/js/_enqueues/deprecated/media-gallery.js',
1290-
'src/wp-admin/js/media-library-upload.js': 'src/js/_enqueues/admin/media-library-upload.js',
1290+
'src/wp-admin/js/media-frame-upload.js': 'src/js/_enqueues/admin/media-frame-upload.js',
12911291
'src/wp-admin/js/media-new-upload.js': 'src/js/_enqueues/admin/media-new-upload.js',
12921292
'src/wp-admin/js/media-upload-pipeline.js': 'src/js/_enqueues/admin/media-upload-pipeline.js',
12931293
'src/wp-admin/js/media-upload.js': 'src/js/_enqueues/admin/media-upload.js',

src/js/_enqueues/admin/media-library-upload.js renamed to src/js/_enqueues/admin/media-frame-upload.js

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,29 @@
11
/**
2-
* Routes Media Library grid uploads through the client-side media pipeline.
2+
* Routes media frame uploads through the client-side media pipeline.
33
*
4-
* On wp-admin/upload.php (grid mode) WordPress uploads via wp.Uploader /
5-
* plupload to async-upload.php. When the browser is cross-origin isolated
6-
* and supports the client-side pipeline, this script intercepts the
7-
* uploader's FilesAdded handler and hands files to wp.mediaUploadPipeline
8-
* (media-upload-pipeline.js) instead: the original image is uploaded via
9-
* the REST API and thumbnails are generated in the browser (wasm-vips),
10-
* then sideloaded and finalized.
4+
* Every wp.media frame - the Media Library grid on wp-admin/upload.php and
5+
* the media modal the block editor, the site editor and the block widgets
6+
* screen open - uploads via wp.Uploader / plupload to async-upload.php.
7+
* When the browser is cross-origin isolated and supports the client-side
8+
* pipeline, this script intercepts the uploader's FilesAdded handler and
9+
* hands files to wp.mediaUploadPipeline (media-upload-pipeline.js)
10+
* instead: the original image is uploaded via the REST API and thumbnails
11+
* are generated in the browser (wasm-vips), then sideloaded and finalized.
1112
*
12-
* The grid's own UI is reused: the same placeholder tiles, progress bars,
13-
* "Uploading n/m" status, and error sidebar that wp-plupload.js drives.
13+
* The frame's own UI is reused: the same placeholder tiles, progress bars,
14+
* "Uploading n/m" status, and error list that wp-plupload.js drives.
1415
*
1516
* When client-side support is unavailable the script cleanly no-ops and
1617
* the classic plupload flow is left untouched.
1718
*
18-
* @output wp-admin/js/media-library-upload.js
19+
* @output wp-admin/js/media-frame-upload.js
1920
*/
2021

2122
/* global plupload */
2223

2324
( function () {
2425
// Guard against double execution (e.g. duplicate enqueues).
25-
if ( window.__wpMediaLibraryUpload ) {
26+
if ( window.__wpMediaFrameUpload ) {
2627
return;
2728
}
2829

@@ -41,7 +42,7 @@
4142
return;
4243
}
4344

44-
window.__wpMediaLibraryUpload = true;
45+
window.__wpMediaFrameUpload = true;
4546

4647
/**
4748
* Resets the upload queue once every attachment has finished uploading.
@@ -134,6 +135,30 @@
134135
wpUploader.error( message, {}, file );
135136
}
136137

138+
/**
139+
* Whether a plupload uploader posts attachments the way wp.Uploader does
140+
* by default.
141+
*
142+
* The init wrapper below reaches every wp.Uploader built on the page,
143+
* and a plugin can build one for its own purposes. Core's defaults tell
144+
* an attachment upload apart: it posts to async-upload.php with the
145+
* `upload-attachment` action. Anything a plugin pointed elsewhere, or
146+
* gave another action, is uploading something else and is left alone.
147+
*
148+
* @param {plupload.Uploader} up The plupload uploader instance.
149+
* @return {boolean} True when the uploader posts attachments.
150+
*/
151+
function isAttachmentUploader( up ) {
152+
const settings = up.settings || {};
153+
const params = settings.multipart_params || {};
154+
const url = String( settings.url || '' ).split( '?' )[ 0 ];
155+
156+
return (
157+
'upload-attachment' === params.action &&
158+
url.endsWith( 'async-upload.php' )
159+
);
160+
}
161+
137162
/**
138163
* Intercepts files added to a plupload uploader.
139164
*
@@ -149,7 +174,11 @@
149174
* @return {boolean|undefined} False to suppress the built-in handler.
150175
*/
151176
function handleFilesAdded( wpUploader, up, files ) {
152-
if ( ! pipeline.isReady() || ! pipeline.canHandleBatch( files ) ) {
177+
if (
178+
! isAttachmentUploader( up ) ||
179+
! pipeline.isReady() ||
180+
! pipeline.canHandleBatch( files )
181+
) {
153182
return;
154183
}
155184

@@ -236,18 +265,19 @@
236265

237266
// Wrap wp.Uploader.prototype.init (an empty stub called once per instance
238267
// after plupload is initialized) to bind a higher-priority FilesAdded
239-
// handler on every uploader instance, including the Media Library grid's.
268+
// handler on every uploader instance: the grid's, and the one each media
269+
// modal creates when it opens.
240270
const originalInit = wp.Uploader.prototype.init;
241271
wp.Uploader.prototype.init = function () {
242272
originalInit.apply( this, arguments );
243273

244274
const wpUploader = /** @type {WPUploader} */ ( this );
245275
const up = /** @type {plupload.Uploader|undefined} */ ( this.uploader );
246276

247-
if ( ! up || up.__wpMediaLibraryUploadBound ) {
277+
if ( ! up || up.__wpMediaFrameUploadBound ) {
248278
return;
249279
}
250-
up.__wpMediaLibraryUploadBound = true;
280+
up.__wpMediaFrameUploadBound = true;
251281

252282
// plupload sorts handlers by priority (descending) and a `false`
253283
// return breaks the chain, so priority 100 runs before and suppresses

‎src/js/_enqueues/admin/media-upload-pipeline.js‎

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Shared glue for routing classic admin uploads through the client-side
33
* media pipeline.
44
*
5-
* The Media Library grid (media-library-upload.js) and the Add New Media
5+
* The media frames (media-frame-upload.js) and the Add New Media
66
* File screen (media-new-upload.js) both intercept plupload and hand files
77
* to @wordpress/upload-media instead. Everything that is not specific to
88
* one screen's UI lives here: feature detection, configuring the
@@ -335,12 +335,33 @@ window.wp = window.wp || {};
335335
}
336336
}
337337

338+
/**
339+
* Whether something else already configured the store on this page.
340+
*
341+
* The block editor renders its own MediaUploadProvider into the same
342+
* store. Neither `mediaSideload` nor `mediaFinalize` has a default, so
343+
* their presence proves a provider's settings landed, whichever one
344+
* rendered them.
345+
*
346+
* @return {boolean} True when the store carries a provider's settings.
347+
*/
348+
function isStoreConfigured() {
349+
const storeSettings = wp.data.select( uploadStore ).getSettings();
350+
return Boolean(
351+
storeSettings &&
352+
storeSettings.mediaSideload &&
353+
storeSettings.mediaFinalize
354+
);
355+
}
356+
338357
/**
339358
* Configures the upload-media store for this page, once.
340359
*
341360
* Rendering the provider with useSubRegistry: false wires the settings
342-
* into the store that wp.data.dispatch/select address (the block editor
343-
* does the same).
361+
* into the store that wp.data.dispatch/select address. The block editor
362+
* does the same for its own uploads, so on a screen where it already
363+
* has, its settings are used as they are and no second provider is
364+
* rendered.
344365
*
345366
* @return {boolean} True when the pipeline is configured and usable.
346367
*/
@@ -356,6 +377,12 @@ window.wp = window.wp || {};
356377
configured = true;
357378
uploadStore = wp.uploadMedia.store;
358379

380+
if ( isStoreConfigured() ) {
381+
wp.data.subscribe( onStoreChange, uploadStore );
382+
addUnloadGuard();
383+
return true;
384+
}
385+
359386
/*
360387
* The media-utils package branches on this flag: without it
361388
* uploadMedia() creates and revokes a throwaway blob URL per file
@@ -386,42 +413,44 @@ window.wp = window.wp || {};
386413

387414
// Only the upload-media store can change what this listener reads.
388415
wp.data.subscribe( onStoreChange, uploadStore );
416+
addUnloadGuard();
389417

390-
// Warn before leaving while uploads are in flight: thumbnails that
391-
// have not been sideloaded yet are lost and the attachment is left
392-
// unfinalized, unlike classic uploads that complete server-side
393-
// once the bytes arrive.
418+
return true;
419+
}
420+
421+
/**
422+
* Warns before leaving while uploads are in flight.
423+
*
424+
* Thumbnails that have not been sideloaded yet are lost and the
425+
* attachment is left unfinalized, unlike classic uploads that complete
426+
* server-side once the bytes arrive.
427+
*/
428+
function addUnloadGuard() {
394429
window.addEventListener( 'beforeunload', function ( event ) {
395430
if ( inFlight > 0 ) {
396431
event.preventDefault();
397432
// Some Chromium versions only show the prompt for returnValue.
398433
event.returnValue = '';
399434
}
400435
} );
401-
402-
return true;
403436
}
404437

405438
/**
406-
* Whether the store has received its settings.
439+
* Whether the store has received a provider's settings.
407440
*
408-
* The provider renders asynchronously, so a file added before the
441+
* A provider renders asynchronously, so a file added before its
409442
* settings land must be left to classic plupload - a degradation,
410443
* never data loss. The store's default state already carries a no-op
411-
* `mediaUpload`, so only a setting the provider alone supplies proves
412-
* the real ones have landed: a file queued before that would be handed
413-
* to the no-op and never upload.
444+
* `mediaUpload`, so only settings a provider alone supplies prove the
445+
* real ones have landed: a file queued before that would be handed to
446+
* the no-op and never upload. The block editor's provider counts too:
447+
* when it renders after this script's, its settings replace them, and
448+
* they drive the same pipeline.
414449
*
415450
* @return {boolean} True when the store is ready to accept files.
416451
*/
417452
function isReady() {
418-
if ( ! configured ) {
419-
return false;
420-
}
421-
const storeSettings = wp.data.select( uploadStore ).getSettings();
422-
return Boolean(
423-
storeSettings && storeSettings.mediaSideload === mediaSideload
424-
);
453+
return configured && isStoreConfigured();
425454
}
426455

427456
/**

‎src/wp-admin/upload.php‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@
141141
wp_enqueue_media();
142142
wp_enqueue_script( 'media-grid' );
143143
wp_enqueue_script( 'media' );
144-
wp_enqueue_media_library_upload();
145144

146145
// Remove the error parameter added by deprecation of wp-admin/media.php.
147146
add_filter(

‎src/wp-includes/media.php‎

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4913,6 +4913,7 @@ function wp_prepare_attachment_for_js( $attachment ) {
49134913
* all media JS APIs.
49144914
*
49154915
* @since 3.5.0
4916+
* @since 7.2.0 Also enqueues the client-side upload integration for the media frame on cross-origin isolated screens.
49164917
*
49174918
* @global int $content_width
49184919
* @global wpdb $wpdb WordPress database abstraction object.
@@ -5302,6 +5303,7 @@ function wp_enqueue_media( $args = array() ) {
53025303
* is registered internally before we try to localize it. See #24724.
53035304
*/
53045305
wp_enqueue_script( 'media-editor' );
5306+
wp_enqueue_media_frame_upload();
53055307
wp_localize_script( 'media-views', '_wpMediaViewsL10n', $strings );
53065308

53075309
wp_enqueue_script( 'media-audiovideo' );
@@ -6810,7 +6812,10 @@ function wp_get_media_library_upload_settings(): array {
68106812
* Nothing is enqueued unless client-side media processing is enabled and the
68116813
* browser honors Document-Isolation-Policy: without isolation the page never
68126814
* becomes cross-origin isolated, the script would no-op, and the whole
6813-
* wp-upload-media dependency chain would be loaded for nothing.
6815+
* wp-upload-media dependency chain would be loaded for nothing. The same
6816+
* goes for a screen wp_set_up_cross_origin_isolation() did not isolate,
6817+
* such as the Customizer or the Site Icon picker: the media frame loads
6818+
* there too, but its uploads stay on the classic path.
68146819
*
68156820
* The screen script self-guards at runtime too: when the browser turns out
68166821
* not to be isolated or lacks client-side media support, it no-ops and the
@@ -6830,6 +6835,10 @@ function _wp_enqueue_media_upload_pipeline_script( string $handle ): void {
68306835
return;
68316836
}
68326837

6838+
if ( ! wp_is_cross_origin_isolated_request() ) {
6839+
return;
6840+
}
6841+
68336842
wp_enqueue_script( $handle );
68346843

68356844
wp_add_inline_script(
@@ -6840,13 +6849,18 @@ function _wp_enqueue_media_upload_pipeline_script( string $handle ): void {
68406849
}
68416850

68426851
/**
6843-
* Enqueues the script that routes Media Library grid uploads through
6844-
* the client-side media processing pipeline.
6852+
* Enqueues the script that routes uploads started from a media frame
6853+
* through the client-side media processing pipeline.
6854+
*
6855+
* Covers every `wp.media` frame that uploads through `wp.Uploader`: the
6856+
* Media Library grid and the media modal the block editor, the site editor
6857+
* and the block widgets screen open. wp_enqueue_media() calls this on any
6858+
* screen that was cross-origin isolated, so it rarely needs calling directly.
68456859
*
68466860
* @since 7.2.0
68476861
*/
6848-
function wp_enqueue_media_library_upload(): void {
6849-
_wp_enqueue_media_upload_pipeline_script( 'media-library-upload' );
6862+
function wp_enqueue_media_frame_upload(): void {
6863+
_wp_enqueue_media_upload_pipeline_script( 'media-frame-upload' );
68506864
}
68516865

68526866
/**
@@ -6878,6 +6892,24 @@ static function ( string $output ): string {
68786892
return wp_add_crossorigin_attributes( $output );
68796893
}
68806894
);
6895+
6896+
$GLOBALS['_wp_cross_origin_isolated'] = true;
6897+
}
6898+
6899+
/**
6900+
* Whether the current request has been set up for cross-origin isolation.
6901+
*
6902+
* True once wp_set_up_cross_origin_isolation() has armed the
6903+
* Document-Isolation-Policy header for the screen being rendered, which is
6904+
* what lets scripts that need a cross-origin isolated page, such as the
6905+
* client-side media upload integration, decide whether to load at all.
6906+
*
6907+
* @since 7.2.0
6908+
*
6909+
* @return bool True when the response will be cross-origin isolated.
6910+
*/
6911+
function wp_is_cross_origin_isolated_request(): bool {
6912+
return ! empty( $GLOBALS['_wp_cross_origin_isolated'] );
68816913
}
68826914

68836915
/**

‎src/wp-includes/script-loader.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1519,7 +1519,7 @@ function wp_default_scripts( $scripts ) {
15191519
$scripts->add( 'media-upload-pipeline', "/wp-admin/js/media-upload-pipeline$suffix.js", array( 'plupload', 'wp-upload-media', 'wp-media-utils', 'wp-api-fetch', 'wp-data', 'wp-element', 'wp-i18n' ), false, 1 );
15201520
$scripts->set_translations( 'media-upload-pipeline' );
15211521

1522-
$scripts->add( 'media-library-upload', "/wp-admin/js/media-library-upload$suffix.js", array( 'media-views', 'media-upload-pipeline' ), false, 1 );
1522+
$scripts->add( 'media-frame-upload', "/wp-admin/js/media-frame-upload$suffix.js", array( 'media-views', 'media-upload-pipeline' ), false, 1 );
15231523

15241524
$scripts->add( 'media-new-upload', "/wp-admin/js/media-new-upload$suffix.js", array( 'plupload-handlers', 'media-upload-pipeline', 'wp-a11y', 'wp-i18n' ), false, 1 );
15251525
$scripts->set_translations( 'media-new-upload' );

‎tests/e2e/specs/media-library-client-side-upload.test.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ test.describe( 'Media Library grid client-side uploads', () => {
195195
// List mode uploads via media-new.php, so the grid integration
196196
// script has no business on this screen either.
197197
await expect(
198-
page.locator( 'script[src*="media-library-upload"]' )
198+
page.locator( 'script[src*="media-frame-upload"]' )
199199
).toHaveCount( 0 );
200200

201201
// Visiting with ?mode= persists the user's preference; restore it.

0 commit comments

Comments
 (0)