Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
c6c40e5
Media: Extend cross-origin isolation to the Media Library grid
adamsilverstein Jul 17, 2026
48e92f5
Media: Route Media Library grid uploads through the client-side pipeline
adamsilverstein Jul 17, 2026
88fed70
Media: Add E2E coverage for Media Library grid client-side uploads
adamsilverstein Jul 17, 2026
27d0ef1
Media: Reference Trac ticket 65661 in test annotations
adamsilverstein Jul 17, 2026
fa7a2e4
Media: Use an E2E test image large enough to generate sub-sizes
adamsilverstein Jul 18, 2026
962ea3b
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Jul 19, 2026
b873ce7
Media: Deepen test coverage for the grid client-side upload integration
adamsilverstein Jul 19, 2026
957860f
Media: Warn before leaving the grid while pipeline uploads are in flight
adamsilverstein Jul 17, 2026
1b5f526
Media: Route "Add New Media File" uploads through the client-side pip…
adamsilverstein Jul 17, 2026
ae180b0
Media: Add E2E coverage for "Add New Media File" client-side uploads
adamsilverstein Jul 17, 2026
ef3ef65
Media: Accept the dismiss link in the failed-upload E2E test
adamsilverstein Jul 18, 2026
4450c99
Media: Deepen test coverage for the media-new.php integration and bef…
adamsilverstein Jul 19, 2026
e4bfd9b
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Jul 19, 2026
cd73025
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Jul 21, 2026
52bfd80
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Jul 23, 2026
4d3ece2
Potential fix for pull request finding
adamsilverstein Jul 23, 2026
da0dd96
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Aug 4, 2026
3d82147
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Aug 4, 2026
89abe77
Tests: Send a Chromium User-Agent from the grid upload enqueue tests
adamsilverstein Aug 24, 2026
c258892
Media: Resolve the Media Library mode the way upload.php does
adamsilverstein Aug 24, 2026
819f47d
Media: Gate the media-new upload enqueue on Chromium 137
adamsilverstein Aug 24, 2026
a5f4b4a
Media: Render pipeline upload errors instead of "[object Object]"
adamsilverstein Aug 24, 2026
5803609
Media: Close pipeline parity gaps in the Media Library upload integra…
adamsilverstein Aug 24, 2026
6e93a0c
Merge remote branch state into review fixes
adamsilverstein Aug 24, 2026
e3f491d
Merge remote-tracking branch 'origin/trunk' into add/media-library-cl…
adamsilverstein Sep 3, 2026
803bc8c
Media: Estimate pipeline progress on the Media Library upload screens.
adamsilverstein Sep 3, 2026
7b519d4
Media: Expand e2e coverage for client-side Media Library uploads.
adamsilverstein Sep 3, 2026
64db15a
Media: Share the client-side upload glue between the Media Library sc…
adamsilverstein Sep 3, 2026
e74d9f7
Media: Isolate the Media Library screens from wp_set_up_cross_origin_…
adamsilverstein Sep 3, 2026
bb2a015
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Sep 4, 2026
72c98c7
Media: Mirror upload.php's truthiness check in wp_get_media_library_m…
adamsilverstein Sep 4, 2026
f0eaac0
Update src/js/_enqueues/admin/media-library-upload.js
adamsilverstein Sep 4, 2026
dc5384f
Apply const/let batched suggestions from code review
adamsilverstein Sep 4, 2026
eed1755
Media: Type check the client-side media upload scripts.
adamsilverstein Sep 4, 2026
99db821
Apply suggestion from @westonruter
adamsilverstein Sep 5, 2026
e78fbed
Media: Resolve unknown Media Library modes to list.
adamsilverstein Sep 5, 2026
11122eb
Media: Fix the jshint line-break error in media-library-upload.js.
adamsilverstein Sep 5, 2026
5fb6956
Media: Move the upload scripts' shared types into typings/media-uploads.
adamsilverstein Sep 5, 2026
ab98e5a
Media: Drop jQuery from media-new-upload.js.
adamsilverstein Sep 5, 2026
86ebc0b
Merge remote-tracking branch 'origin/trunk' into add/media-library-cl…
adamsilverstein Sep 18, 2026
2309e3f
Media: Fix three client-side upload edge cases found in review.
adamsilverstein Sep 18, 2026
3fa59e3
Media: Route media modal uploads through the client-side pipeline.
adamsilverstein Sep 30, 2026
e133bd5
Media: Re-key and clear uploading state like wp-plupload.js.
adamsilverstein Oct 6, 2026
a6389c6
Merge remote-tracking branch 'fork/add/media-library-client-side-uplo…
adamsilverstein Oct 6, 2026
834ec14
Merge remote-tracking branch 'origin/trunk' into add/media-library-cl…
adamsilverstein Oct 12, 2026
e60a8a7
Tests: Use a parseable HEIC for the client-side upload hand-off tests.
adamsilverstein Oct 12, 2026
d585c62
Merge remote-tracking branch 'fork/add/media-library-client-side-uplo…
adamsilverstein Oct 12, 2026
f83ed68
Merge trunk into add/media-library-client-side-uploads
adamsilverstein Oct 15, 2026
284bfb3
Merge add/media-library-client-side-uploads into add/media-modal-clie…
adamsilverstein Oct 15, 2026
610226c
Media: Honor a plugin-sent isolation header when enqueuing the pipeline
adamsilverstein Oct 16, 2026
0708a4b
Merge branch 'trunk' into add/media-modal-client-side-uploads
adamsilverstein Oct 16, 2026
9f5a939
Merge remote-tracking branch 'origin/trunk' into add/media-library-cl…
adamsilverstein Oct 16, 2026
8505406
Tests: Cover modal upload errors and non-attachment uploaders.
adamsilverstein Oct 16, 2026
996dc4e
Media: Keep the server's reason for modal upload failures.
adamsilverstein Oct 16, 2026
46bab80
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Oct 16, 2026
b170920
Media: Match pipeline uploads by their success callback.
adamsilverstein Oct 16, 2026
04a4a1d
Merge branch 'trunk' into add/media-library-client-side-uploads
westonruter Oct 17, 2026
9446c81
Narrow types for wp_get_mime_types()
westonruter Oct 17, 2026
f7bbf58
Narrow types for wp_get_ext_types()
westonruter Oct 17, 2026
87ad84c
Narrow types for get_allowed_mime_types()
westonruter Oct 17, 2026
912e361
Narrow types for wp_get_media_library_upload_settings()
westonruter Oct 17, 2026
c250725
Narrow type for _wp_enqueue_media_upload_pipeline_script()
westonruter Oct 17, 2026
f9549e2
Improve construction of JS in PHP
westonruter Oct 17, 2026
3802445
Match inline settings JSON encoding flags in tests
westonruter Oct 17, 2026
b71fce3
Read inline settings via get_inline_script_data() in tests
westonruter Oct 17, 2026
b53642d
Declare the $wp_scripts global in media upload test set-up
westonruter Oct 17, 2026
0ec3c4b
Add void return types to isolation test helpers
westonruter Oct 17, 2026
2947c47
Narrow superglobal snapshots to strings in media tests
westonruter Oct 17, 2026
e156003
Type the upload_mimes filter callback in media frame test
westonruter Oct 17, 2026
f3bc859
Remove ineffective docblock from upload_mimes test callback
westonruter Oct 17, 2026
7a63b26
Update src/wp-includes/media.php
adamsilverstein Oct 19, 2026
258d1bf
Merge remote-tracking branch 'origin/trunk' into add/media-library-cl…
adamsilverstein Oct 19, 2026
0002462
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Oct 19, 2026
6ba4872
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Oct 19, 2026
b89a7ef
Update src/wp-includes/media.php
adamsilverstein Oct 19, 2026
ce8ec4d
Update src/wp-includes/media.php
adamsilverstein Oct 19, 2026
426c7ab
Media: Declare the cross-origin isolation flag as a global
adamsilverstein Oct 19, 2026
ed3bb94
Media: Pass the upload_size_limit filter result through unchanged.
adamsilverstein Oct 19, 2026
cb95a46
Media: Add the upload pipeline settings once per page.
adamsilverstein Oct 19, 2026
e06f8fc
Media: Leave the upload-media store to the block editor on its screens.
adamsilverstein Oct 19, 2026
c2e6661
Media: Call the wp.Uploader progress callback for pipeline uploads.
adamsilverstein Oct 19, 2026
cd00245
Media: Split mixed upload batches between the pipeline and plupload.
adamsilverstein Oct 19, 2026
3b48762
Media: Leave JPEG and PNG uploads to plupload while it resizes them.
adamsilverstein Oct 19, 2026
c997b9a
Media: Share the image processing settings with the REST index.
adamsilverstein Oct 19, 2026
2c5795a
Merge branch 'trunk' into add/media-library-client-side-uploads
adamsilverstein Oct 19, 2026
f7a2aaf
Media: Avoid a fatal error enqueueing the upload pipeline on the fron…
adamsilverstein Oct 19, 2026
cd98d82
Media: Track the pipeline settings with a script data flag.
adamsilverstein Oct 19, 2026
11e485c
Media: Share batch routing and fix pipeline upload bookkeeping.
adamsilverstein Oct 19, 2026
d647e30
Merge branch 'trunk' into add/media-library-client-side-uploads
westonruter Oct 20, 2026
422cd66
Fix eslint errors
westonruter Oct 20, 2026
1fd488e
Media: Drop the deprecated returnValue from the unload guard
adamsilverstein Oct 20, 2026
baacd06
Media: Make the pipeline's mediaFinalize() an async function
adamsilverstein Oct 20, 2026
36aff7d
Media: Match the async-upload.php endpoint with the URL API
adamsilverstein Oct 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,9 @@ module.exports = function(grunt) {
[ WORKING_DIR + 'wp-admin/js/language-chooser.js' ]: [ './src/js/_enqueues/lib/language-chooser.js' ],
[ WORKING_DIR + 'wp-admin/js/link.js' ]: [ './src/js/_enqueues/admin/link.js' ],
[ WORKING_DIR + 'wp-admin/js/media-gallery.js' ]: [ './src/js/_enqueues/deprecated/media-gallery.js' ],
[ WORKING_DIR + 'wp-admin/js/media-frame-upload.js' ]: [ './src/js/_enqueues/admin/media-frame-upload.js' ],
[ WORKING_DIR + 'wp-admin/js/media-new-upload.js' ]: [ './src/js/_enqueues/admin/media-new-upload.js' ],
[ WORKING_DIR + 'wp-admin/js/media-upload-pipeline.js' ]: [ './src/js/_enqueues/admin/media-upload-pipeline.js' ],
[ WORKING_DIR + 'wp-admin/js/media-upload.js' ]: [ './src/js/_enqueues/admin/media-upload.js' ],
[ WORKING_DIR + 'wp-admin/js/media.js' ]: [ './src/js/_enqueues/admin/media.js' ],
[ WORKING_DIR + 'wp-admin/js/nav-menu.js' ]: [ './src/js/_enqueues/lib/nav-menu.js' ],
Expand Down Expand Up @@ -1325,6 +1328,9 @@ module.exports = function(grunt) {
'src/wp-admin/js/language-chooser.js': 'src/js/_enqueues/lib/language-chooser.js',
'src/wp-admin/js/link.js': 'src/js/_enqueues/admin/link.js',
'src/wp-admin/js/media-gallery.js': 'src/js/_enqueues/deprecated/media-gallery.js',
'src/wp-admin/js/media-frame-upload.js': 'src/js/_enqueues/admin/media-frame-upload.js',
'src/wp-admin/js/media-new-upload.js': 'src/js/_enqueues/admin/media-new-upload.js',
'src/wp-admin/js/media-upload-pipeline.js': 'src/js/_enqueues/admin/media-upload-pipeline.js',
'src/wp-admin/js/media-upload.js': 'src/js/_enqueues/admin/media-upload.js',
'src/wp-admin/js/media.js': 'src/js/_enqueues/admin/media.js',
'src/wp-admin/js/nav-menu.js': 'src/js/_enqueues/lib/nav-menu.js',
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@napi-rs/image": "1.14.0",
"@playwright/test": "1.61.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.6.2",
"@types/backbone": "1.4.23",
"@types/codemirror": "5.60.17",
"@types/espree": "10.1.0",
"@types/htmlhint": "1.1.5",
Expand Down
271 changes: 271 additions & 0 deletions src/js/_enqueues/admin/media-frame-upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/**
* Routes media frame uploads through the client-side media pipeline.
*
* Every wp.media frame - the Media Library grid on wp-admin/upload.php and
* the media modal the block editor, the site editor and the block widgets
* screen open - uploads via wp.Uploader / plupload to async-upload.php.
* When the browser is cross-origin isolated and supports the client-side
* pipeline, this script intercepts the uploader's FilesAdded handler and
* hands files to wp.mediaUploadPipeline (media-upload-pipeline.js)
* instead: the original image is uploaded via the REST API and thumbnails
* are generated in the browser (wasm-vips), then sideloaded and finalized.
*
* The frame's own UI is reused: the same placeholder tiles, progress bars,
* "Uploading n/m" status, and error list that wp-plupload.js drives.
*
* When client-side support is unavailable the script cleanly no-ops and
* the classic plupload flow is left untouched.
*
* @output wp-admin/js/media-frame-upload.js
*/

/* global plupload */

( function () {
// Guard against double execution (e.g. duplicate enqueues).
if ( window.__wpMediaFrameUpload ) {
return;
}

const pipeline = window.wp && wp.mediaUploadPipeline;

// Bail unless the browser actually supports client-side processing. This
// is the clean no-op: when the isolation headers did not land, classic
// plupload keeps handling uploads.
if (
! pipeline ||
typeof plupload === 'undefined' ||
! wp.Uploader ||
! wp.media ||
! pipeline.configure()
) {
return;
}

window.__wpMediaFrameUpload = true;

/**
* Resets the upload queue once every attachment has finished uploading.
*
* Parity with wp-plupload.js so browse mode flips back when done.
*/
function maybeResetQueue() {
const complete = wp.Uploader.queue.all( function (
/** @type {WPAttachment} */ attachment
) {
return ! attachment.get( 'uploading' );
} );

if ( complete ) {
wp.Uploader.queue.reset();
}
}

/**
* Handles a completed upload by syncing the grid tile with the server data.
*
* @param {WPUploader} wpUploader The wp.Uploader instance that queued the file.
* @param {WPAttachment} model The placeholder Attachment model.
* @param {PipelineAttachment} attachment The finalized attachment from the pipeline.
*/
function handleSuccess( wpUploader, model, attachment ) {
// Not silent: Attachments re-keys a model from its cid to its id when
// it sees the change event. Without that the collection cannot find the
// attachment by id, and refreshing the library duplicates its tile.
model.set( { id: attachment.id } );

// Register the model in Attachments.all (parity with wp-plupload.js).
wp.media.model.Attachment.get( attachment.id, model );

// Clear the uploading state before the refetch lands, not after. The
// attachment details view re-renders on the refetch's changes, so it has
// to see `uploading: false` by then or it keeps showing a progress bar.
// wp-plupload.js sets both in one call for the same reason.
[ 'file', 'loaded', 'size', 'percent' ].forEach( function ( key ) {
model.unset( key, { silent: true } );
} );
model.set( { uploading: false }, { silent: true } );

model
.fetch()
.fail( function () {
// Fetch failed, but the upload succeeded: fill the tile with what
// the pipeline gave us so it is not left empty.
model.set( attachment );
} )
.always( function () {
maybeResetQueue();

// Parity with wp-plupload.js, which exposes this callback so
// other code can react to a finished upload.
wpUploader.success( model );
} );
}

/**
* Handles an upload error by removing the tile and surfacing the message.
*
* The error goes into wp.Uploader.errors exactly like a classic upload
* error, so the grid's error sidebar renders it, announces it, and
* moves focus to its Dismiss button.
*
* @param {WPUploader} wpUploader The wp.Uploader instance that queued the file.
* @param {WPAttachment} model The placeholder Attachment model.
* @param {UploadError} error The upload error.
* @param {string} fileName Name of the file (for the error label).
*/
function handleError( wpUploader, model, error, fileName ) {
const message = pipeline.getErrorText( error, fileName );
const file = { name: fileName };

model.destroy();

wp.Uploader.errors.unshift( {
message: message,
data: {},
file: file,
} );

maybeResetQueue();

// Parity with wp-plupload.js, which exposes this callback so other
// code can react to a failed upload.
wpUploader.error( message, {}, file );
}

/**
* Whether a plupload uploader posts attachments the way wp.Uploader does
* by default.
*
* The init wrapper below reaches every wp.Uploader built on the page,
* and a plugin can build one for its own purposes. Core's defaults tell
* an attachment upload apart: it posts to async-upload.php with the
* `upload-attachment` action. Anything a plugin pointed elsewhere, or
* gave another action, is uploading something else and is left alone.
*
* @param {plupload.Uploader} up The plupload uploader instance.
* @return {boolean} True when the uploader posts attachments.
*/
function isAttachmentUploader( up ) {
const settings = up.settings || {};
const params = settings.multipart_params || {};
const url = new URL( settings.url || '', location.href );

return (
'upload-attachment' === params.action &&
url.pathname.endsWith( 'async-upload.php' )
);
}

/**
* Intercepts files added to a plupload uploader.
*
* Builds the same placeholder tiles as wp-plupload for the files the
* pipeline takes (see wp.mediaUploadPipeline.routeBatch()). Returns
* undefined to leave the whole batch to plupload's built-in handler.
*
* @param {WPUploader} wpUploader The wp.Uploader instance.
* @param {plupload.Uploader} up The plupload uploader instance.
* @param {plupload.File[]} files Files added to the queue.
* @return {boolean|undefined} False to suppress the built-in handler.
*/
function handleFilesAdded( wpUploader, up, files ) {
if ( ! isAttachmentUploader( up ) ) {
return;
}

// The classic flow attaches to `post_id` the same way, and rejects
// a post the user cannot edit just as the REST API does.
const params = ( up.settings && up.settings.multipart_params ) || {};

return pipeline.routeBatch( up, files, {
postId: parseInt( params.post_id, 10 ) || 0,
onQueue: function (
/** @type {plupload.File} */ file,
/** @type {File} */ nativeFile
) {
// Build the same placeholder attributes as wp-plupload.js so
// the grid's progress tiles and "Uploading n/m" status work
// unchanged.
/** @type {PlaceholderAttributes} */
const attributes = {
file: file,
uploading: true,
date: new Date(),
filename: file.name,
menuOrder: 0,
uploadedTo: wp.media.model.settings.post.id,
loaded: file.loaded,
size: file.size,
percent: file.percent,
};

/*
* Early mime type scanning for images, as wp-plupload.js does,
* extended with the formats the client-side pipeline accepts.
*/
const image = /(?:jpe?g|png|gif|webp|avif|heic|heif)$/i.exec(
file.name
);
if ( image ) {
attributes.type = 'image';
// `jpg` is not a valid subtype, so map it to `jpeg`.
const subtype = image[ 0 ].toLowerCase();
attributes.subtype = 'jpg' === subtype ? 'jpeg' : subtype;
}

const model = wp.media.model.Attachment.create( attributes );
wp.Uploader.queue.add( model );
wpUploader.added( model );

return {
onSuccess: function (
/** @type {PipelineAttachment} */ attachment
) {
handleSuccess( wpUploader, model, attachment );
},
onError: function ( /** @type {UploadError} */ error ) {
handleError( wpUploader, model, error, nativeFile.name );
},
// plupload builds its own tile for the file it took over.
onHandOff: function () {
model.destroy();
},
onProgress: function ( /** @type {number} */ percent ) {
model.set( { percent: percent } );
wpUploader.progress( model );
},
};
},
} );
}

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

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

if ( ! up || up.__wpMediaFrameUploadBound ) {
return;
}
up.__wpMediaFrameUploadBound = true;

// plupload sorts handlers by priority (descending) and a `false`
// return breaks the chain, so priority 100 runs before and suppresses
// the built-in FilesAdded handler.
up.bind(
'FilesAdded',
function ( uploader, files ) {
return handleFilesAdded( wpUploader, uploader, files );
},
this,
100
);
};
} )();
Loading
Loading