|
| 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