Skip to content

Commit 9fbaa2d

Browse files
Media: Enable client-side uploads in the Media Library and modal.
Enable client-side media processing for Uploads from the Media Library grid, the Add New Media File screen and the media modal in the block editor (in supported browsers). These screens now use the same pipeline as block editor uploads: the original is uploaded through the REST API, sub-sizes are generated in the browser and sideloaded, then the attachment is finalized. `wp_set_up_cross_origin_isolation()` now also isolates the Media Library grid and the Add New Media File screen using Document Isolation Policy. Uploads huse the client-side processing still use the existing progress, tile and error UI. When client-side processing is disabled, uploads fall back to the classic flow unchanged. Audio files, uploaders that do not post attachments, and JPEG/PNG files while plupload resizing is enabled also stay on the classic path, and a HEIC the browser cannot decode is handed to the server when its image editor supports HEIC. The Customizer, Site Icon picker and classic editor are not isolated and keep classic uploads. See WordPress/gutenberg#82409. Props adamsilverstein, westonruter, andrewserong, sanayasir, arkaprabhachowdhury, logicrays. Fixes #65661. See #66220. git-svn-id: https://develop.svn.wordpress.org/trunk@64588 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 7cede9a commit 9fbaa2d

25 files changed

Lines changed: 5518 additions & 63 deletions

‎Gruntfile.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,9 @@ module.exports = function(grunt) {
531531
[ WORKING_DIR + 'wp-admin/js/language-chooser.js' ]: [ './src/js/_enqueues/lib/language-chooser.js' ],
532532
[ WORKING_DIR + 'wp-admin/js/link.js' ]: [ './src/js/_enqueues/admin/link.js' ],
533533
[ WORKING_DIR + 'wp-admin/js/media-gallery.js' ]: [ './src/js/_enqueues/deprecated/media-gallery.js' ],
534+
[ WORKING_DIR + 'wp-admin/js/media-frame-upload.js' ]: [ './src/js/_enqueues/admin/media-frame-upload.js' ],
535+
[ WORKING_DIR + 'wp-admin/js/media-new-upload.js' ]: [ './src/js/_enqueues/admin/media-new-upload.js' ],
536+
[ WORKING_DIR + 'wp-admin/js/media-upload-pipeline.js' ]: [ './src/js/_enqueues/admin/media-upload-pipeline.js' ],
534537
[ WORKING_DIR + 'wp-admin/js/media-upload.js' ]: [ './src/js/_enqueues/admin/media-upload.js' ],
535538
[ WORKING_DIR + 'wp-admin/js/media.js' ]: [ './src/js/_enqueues/admin/media.js' ],
536539
[ WORKING_DIR + 'wp-admin/js/nav-menu.js' ]: [ './src/js/_enqueues/lib/nav-menu.js' ],
@@ -1325,6 +1328,9 @@ module.exports = function(grunt) {
13251328
'src/wp-admin/js/language-chooser.js': 'src/js/_enqueues/lib/language-chooser.js',
13261329
'src/wp-admin/js/link.js': 'src/js/_enqueues/admin/link.js',
13271330
'src/wp-admin/js/media-gallery.js': 'src/js/_enqueues/deprecated/media-gallery.js',
1331+
'src/wp-admin/js/media-frame-upload.js': 'src/js/_enqueues/admin/media-frame-upload.js',
1332+
'src/wp-admin/js/media-new-upload.js': 'src/js/_enqueues/admin/media-new-upload.js',
1333+
'src/wp-admin/js/media-upload-pipeline.js': 'src/js/_enqueues/admin/media-upload-pipeline.js',
13281334
'src/wp-admin/js/media-upload.js': 'src/js/_enqueues/admin/media-upload.js',
13291335
'src/wp-admin/js/media.js': 'src/js/_enqueues/admin/media.js',
13301336
'src/wp-admin/js/nav-menu.js': 'src/js/_enqueues/lib/nav-menu.js',

‎package-lock.json‎

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@napi-rs/image": "1.14.0",
4545
"@playwright/test": "1.61.1",
4646
"@pmmmwh/react-refresh-webpack-plugin": "0.6.2",
47+
"@types/backbone": "1.4.23",
4748
"@types/codemirror": "5.60.17",
4849
"@types/espree": "10.1.0",
4950
"@types/htmlhint": "1.1.5",
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/**
2+
* Routes media frame uploads through the client-side media pipeline.
3+
*
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.
12+
*
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.
15+
*
16+
* When client-side support is unavailable the script cleanly no-ops and
17+
* the classic plupload flow is left untouched.
18+
*
19+
* @output wp-admin/js/media-frame-upload.js
20+
*/
21+
22+
/* global plupload */
23+
24+
( function () {
25+
// Guard against double execution (e.g. duplicate enqueues).
26+
if ( window.__wpMediaFrameUpload ) {
27+
return;
28+
}
29+
30+
const pipeline = window.wp && wp.mediaUploadPipeline;
31+
32+
// Bail unless the browser actually supports client-side processing. This
33+
// is the clean no-op: when the isolation headers did not land, classic
34+
// plupload keeps handling uploads.
35+
if (
36+
! pipeline ||
37+
typeof plupload === 'undefined' ||
38+
! wp.Uploader ||
39+
! wp.media ||
40+
! pipeline.configure()
41+
) {
42+
return;
43+
}
44+
45+
window.__wpMediaFrameUpload = true;
46+
47+
/**
48+
* Resets the upload queue once every attachment has finished uploading.
49+
*
50+
* Parity with wp-plupload.js so browse mode flips back when done.
51+
*/
52+
function maybeResetQueue() {
53+
const complete = wp.Uploader.queue.all( function (
54+
/** @type {WPAttachment} */ attachment
55+
) {
56+
return ! attachment.get( 'uploading' );
57+
} );
58+
59+
if ( complete ) {
60+
wp.Uploader.queue.reset();
61+
}
62+
}
63+
64+
/**
65+
* Handles a completed upload by syncing the grid tile with the server data.
66+
*
67+
* @param {WPUploader} wpUploader The wp.Uploader instance that queued the file.
68+
* @param {WPAttachment} model The placeholder Attachment model.
69+
* @param {PipelineAttachment} attachment The finalized attachment from the pipeline.
70+
*/
71+
function handleSuccess( wpUploader, model, attachment ) {
72+
// Not silent: Attachments re-keys a model from its cid to its id when
73+
// it sees the change event. Without that the collection cannot find the
74+
// attachment by id, and refreshing the library duplicates its tile.
75+
model.set( { id: attachment.id } );
76+
77+
// Register the model in Attachments.all (parity with wp-plupload.js).
78+
wp.media.model.Attachment.get( attachment.id, model );
79+
80+
// Clear the uploading state before the refetch lands, not after. The
81+
// attachment details view re-renders on the refetch's changes, so it has
82+
// to see `uploading: false` by then or it keeps showing a progress bar.
83+
// wp-plupload.js sets both in one call for the same reason.
84+
[ 'file', 'loaded', 'size', 'percent' ].forEach( function ( key ) {
85+
model.unset( key, { silent: true } );
86+
} );
87+
model.set( { uploading: false }, { silent: true } );
88+
89+
model
90+
.fetch()
91+
.fail( function () {
92+
// Fetch failed, but the upload succeeded: fill the tile with what
93+
// the pipeline gave us so it is not left empty.
94+
model.set( attachment );
95+
} )
96+
.always( function () {
97+
maybeResetQueue();
98+
99+
// Parity with wp-plupload.js, which exposes this callback so
100+
// other code can react to a finished upload.
101+
wpUploader.success( model );
102+
} );
103+
}
104+
105+
/**
106+
* Handles an upload error by removing the tile and surfacing the message.
107+
*
108+
* The error goes into wp.Uploader.errors exactly like a classic upload
109+
* error, so the grid's error sidebar renders it, announces it, and
110+
* moves focus to its Dismiss button.
111+
*
112+
* @param {WPUploader} wpUploader The wp.Uploader instance that queued the file.
113+
* @param {WPAttachment} model The placeholder Attachment model.
114+
* @param {UploadError} error The upload error.
115+
* @param {string} fileName Name of the file (for the error label).
116+
*/
117+
function handleError( wpUploader, model, error, fileName ) {
118+
const message = pipeline.getErrorText( error, fileName );
119+
const file = { name: fileName };
120+
121+
model.destroy();
122+
123+
wp.Uploader.errors.unshift( {
124+
message: message,
125+
data: {},
126+
file: file,
127+
} );
128+
129+
maybeResetQueue();
130+
131+
// Parity with wp-plupload.js, which exposes this callback so other
132+
// code can react to a failed upload.
133+
wpUploader.error( message, {}, file );
134+
}
135+
136+
/**
137+
* Whether a plupload uploader posts attachments the way wp.Uploader does
138+
* by default.
139+
*
140+
* The init wrapper below reaches every wp.Uploader built on the page,
141+
* and a plugin can build one for its own purposes. Core's defaults tell
142+
* an attachment upload apart: it posts to async-upload.php with the
143+
* `upload-attachment` action. Anything a plugin pointed elsewhere, or
144+
* gave another action, is uploading something else and is left alone.
145+
*
146+
* @param {plupload.Uploader} up The plupload uploader instance.
147+
* @return {boolean} True when the uploader posts attachments.
148+
*/
149+
function isAttachmentUploader( up ) {
150+
const settings = up.settings || {};
151+
const params = settings.multipart_params || {};
152+
const url = new URL( settings.url || '', location.href );
153+
154+
return (
155+
'upload-attachment' === params.action &&
156+
url.pathname.endsWith( 'async-upload.php' )
157+
);
158+
}
159+
160+
/**
161+
* Intercepts files added to a plupload uploader.
162+
*
163+
* Builds the same placeholder tiles as wp-plupload for the files the
164+
* pipeline takes (see wp.mediaUploadPipeline.routeBatch()). Returns
165+
* undefined to leave the whole batch to plupload's built-in handler.
166+
*
167+
* @param {WPUploader} wpUploader The wp.Uploader instance.
168+
* @param {plupload.Uploader} up The plupload uploader instance.
169+
* @param {plupload.File[]} files Files added to the queue.
170+
* @return {boolean|undefined} False to suppress the built-in handler.
171+
*/
172+
function handleFilesAdded( wpUploader, up, files ) {
173+
if ( ! isAttachmentUploader( up ) ) {
174+
return;
175+
}
176+
177+
// The classic flow attaches to `post_id` the same way, and rejects
178+
// a post the user cannot edit just as the REST API does.
179+
const params = ( up.settings && up.settings.multipart_params ) || {};
180+
181+
return pipeline.routeBatch( up, files, {
182+
postId: parseInt( params.post_id, 10 ) || 0,
183+
onQueue: function (
184+
/** @type {plupload.File} */ file,
185+
/** @type {File} */ nativeFile
186+
) {
187+
// Build the same placeholder attributes as wp-plupload.js so
188+
// the grid's progress tiles and "Uploading n/m" status work
189+
// unchanged.
190+
/** @type {PlaceholderAttributes} */
191+
const attributes = {
192+
file: file,
193+
uploading: true,
194+
date: new Date(),
195+
filename: file.name,
196+
menuOrder: 0,
197+
uploadedTo: wp.media.model.settings.post.id,
198+
loaded: file.loaded,
199+
size: file.size,
200+
percent: file.percent,
201+
};
202+
203+
/*
204+
* Early mime type scanning for images, as wp-plupload.js does,
205+
* extended with the formats the client-side pipeline accepts.
206+
*/
207+
const image = /(?:jpe?g|png|gif|webp|avif|heic|heif)$/i.exec(
208+
file.name
209+
);
210+
if ( image ) {
211+
attributes.type = 'image';
212+
// `jpg` is not a valid subtype, so map it to `jpeg`.
213+
const subtype = image[ 0 ].toLowerCase();
214+
attributes.subtype = 'jpg' === subtype ? 'jpeg' : subtype;
215+
}
216+
217+
const model = wp.media.model.Attachment.create( attributes );
218+
wp.Uploader.queue.add( model );
219+
wpUploader.added( model );
220+
221+
return {
222+
onSuccess: function (
223+
/** @type {PipelineAttachment} */ attachment
224+
) {
225+
handleSuccess( wpUploader, model, attachment );
226+
},
227+
onError: function ( /** @type {UploadError} */ error ) {
228+
handleError( wpUploader, model, error, nativeFile.name );
229+
},
230+
// plupload builds its own tile for the file it took over.
231+
onHandOff: function () {
232+
model.destroy();
233+
},
234+
onProgress: function ( /** @type {number} */ percent ) {
235+
model.set( { percent: percent } );
236+
wpUploader.progress( model );
237+
},
238+
};
239+
},
240+
} );
241+
}
242+
243+
// Wrap wp.Uploader.prototype.init (an empty stub called once per instance
244+
// after plupload is initialized) to bind a higher-priority FilesAdded
245+
// handler on every uploader instance: the grid's, and the one each media
246+
// modal creates when it opens.
247+
const originalInit = wp.Uploader.prototype.init;
248+
wp.Uploader.prototype.init = function () {
249+
originalInit.apply( this, arguments );
250+
251+
const wpUploader = /** @type {WPUploader} */ ( this );
252+
const up = /** @type {plupload.Uploader|undefined} */ ( this.uploader );
253+
254+
if ( ! up || up.__wpMediaFrameUploadBound ) {
255+
return;
256+
}
257+
up.__wpMediaFrameUploadBound = true;
258+
259+
// plupload sorts handlers by priority (descending) and a `false`
260+
// return breaks the chain, so priority 100 runs before and suppresses
261+
// the built-in FilesAdded handler.
262+
up.bind(
263+
'FilesAdded',
264+
function ( uploader, files ) {
265+
return handleFilesAdded( wpUploader, uploader, files );
266+
},
267+
this,
268+
100
269+
);
270+
};
271+
} )();

0 commit comments

Comments
 (0)