Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
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 @@ -526,6 +526,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-library-upload.js' ]: [ './src/js/_enqueues/admin/media-library-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 @@ -1280,6 +1283,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-library-upload.js': 'src/js/_enqueues/admin/media-library-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
303 changes: 303 additions & 0 deletions src/js/_enqueues/admin/media-library-upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
/**
* Routes Media Library grid uploads through the client-side media pipeline.
*
* On wp-admin/upload.php (grid mode) WordPress 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 grid's own UI is reused: the same placeholder tiles, progress bars,
* "Uploading n/m" status, and error sidebar 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-library-upload.js
*/

/* global plupload */

/**
* The parts of a wp.Uploader instance this script relies on.
*
* @typedef {Object} WPUploader
* @property {plupload.Uploader} uploader The plupload uploader it wraps.
* @property {( model: WPAttachment ) => void} added Runs when a file is queued.
* @property {( model: WPAttachment ) => void} success Runs when an upload finished.
* @property {( message: string, data: Object, file: { name: string } ) => void} error Runs when an upload failed.
*/

/**
* The parts of a wp.media.model.Attachment (a Backbone model) this script
* relies on.
*
* @typedef {Object} WPAttachment
* @property {( key: string ) => unknown} get Reads a model attribute.
* @property {( attributes: Object, options?: Object ) => void} set Sets model attributes.
* @property {( key: string, options?: Object ) => void} unset Removes a model attribute.
* @property {() => JQuery.jqXHR} fetch Refetches the attachment from the REST API.
* @property {() => void} destroy Removes the model and its tile.
*/
Comment on lines +23 to +43

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not an expert with TypeScript, but why wouldn't these go in typings/media-uploads/index.d.ts as well?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also seems like WPAttachment should somehow extend from @types/backbone.


/**
* The placeholder attributes wp-plupload.js builds for an uploading tile.
*
* @typedef {Object} PlaceholderAttributes
* @property {plupload.File} file The file being uploaded.
* @property {boolean} uploading Always true while the upload runs.
* @property {Date} date When the upload started.
* @property {string} filename The file name.
* @property {number} menuOrder Menu order of the attachment.
* @property {number} uploadedTo The post the upload is attached to.
* @property {number} loaded Bytes uploaded so far.
* @property {number} size Size of the file in bytes.
* @property {number} percent Progress percentage.
* @property {string} [type] Mime type guessed from the file name.
* @property {string} [subtype] Mime subtype guessed from the file name.
*/
Comment on lines +45 to +60

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this. Why not typings/media-uploads/index.d.ts?


/*
* PipelineAttachment and UploadError are declared by media-upload-pipeline.js,
* which this script depends on.
*/

( function () {
// Guard against double execution (e.g. duplicate enqueues).
if ( window.__wpMediaLibraryUpload ) {
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.__wpMediaLibraryUpload = true;
Comment thread
adamsilverstein marked this conversation as resolved.

/**
* 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 ) {
model.set( { id: attachment.id }, { silent: true } );

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

model
.fetch()
.done( function () {
[ 'file', 'loaded', 'size', 'percent' ].forEach( function (
key
) {
model.unset( key, { silent: true } );
} );
model.set( { uploading: false } );
} )
.fail( function () {
// Fetch failed, but the upload succeeded: clear the uploading
// state with what the pipeline gave us so no tile is stuck.
[ 'file', 'loaded', 'size', 'percent' ].forEach( function (
key
) {
model.unset( key, { silent: true } );
} );
model.set( attachment );
model.set( { uploading: false } );
} )
.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 {File} nativeFile The original file (for the error label).
*/
function handleError( wpUploader, model, error, nativeFile ) {
const message = pipeline.getErrorText( error, nativeFile.name );
const file = { name: nativeFile.name };

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 );
}

/**
* Intercepts files added to a plupload uploader.
*
* Returns undefined (not false) when the pipeline cannot take the batch
* so the built-in handler runs and uploads server-side - a degradation,
* never data loss. Otherwise builds the same placeholder tiles as
* wp-plupload, routes each file through the pipeline, and returns false
* to suppress the 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 ( ! pipeline.isReady() || ! pipeline.canHandleBatch( files ) ) {
return;
}

// The classic flow posts plupload's multipart params to
// async-upload.php; forward them so plugins reading $_POST see the
// same fields, with `post_id` spelled `post` for the REST API.
const params = ( up.settings && up.settings.multipart_params ) || {};
const additionalData = pipeline.additionalDataFromParams(
params,
parseInt( params.post_id, 10 ) || 0
);

files.forEach( function ( file ) {
// Ignore failed uploads.
if ( plupload.FAILED === file.status ) {
return;
}

// 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`.
attributes.subtype =
'jpg' === image[ 0 ].toLowerCase()
? 'jpeg'
: image[ 0 ].toLowerCase();
}

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

// canHandleBatch() already established that every file has one.
const nativeFile = /** @type {File} */ ( file.getNative() );

// Remove the file from plupload so it is not uploaded twice.
up.removeFile( file );

pipeline.queueFile( nativeFile, additionalData, {
onSuccess: function (
/** @type {PipelineAttachment} */ attachment
) {
handleSuccess( wpUploader, model, attachment );
},
onError: function ( /** @type {UploadError} */ error ) {
handleError( wpUploader, model, error, nativeFile );
},
onProgress: function ( /** @type {number} */ percent ) {
model.set( { percent: percent } );
},
} );
} );

up.refresh();

return false;
}

// 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, including the Media Library grid's.
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.__wpMediaLibraryUploadBound ) {
return;
}
up.__wpMediaLibraryUploadBound = 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