| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 /** 2 * Routes "Add New Media File" uploads through the client-side media pipeline. 3 * 4 * On wp-admin/media-new.php WordPress uploads via a raw plupload.Uploader 5 * (created by plupload-handlers) posting to async-upload.php. When the 6 * browser is cross-origin isolated and supports the client-side pipeline, 7 * this script intercepts the uploader's FilesAdded handler and hands files 8 * to wp.mediaUploadPipeline (media-upload-pipeline.js) instead: the 9 * original image is uploaded via the REST API and thumbnails are generated 10 * in the browser (wasm-vips), then sideloaded and finalized. 11 * 12 * The screen's existing UI helpers from plupload-handlers are reused: 13 * fileQueued() builds the progress item, uploadSuccess() renders the 14 * finished attachment row (via the async-upload.php markup endpoint), and 15 * uploadComplete() runs when the queue drains. Failed uploads render the 16 * same notice async-upload.php returns for a server-side failure, so the 17 * screen looks and behaves unchanged. 18 * 19 * When client-side support is unavailable the script cleanly no-ops and 20 * the classic plupload flow is left untouched. 21 * 22 * @output wp-admin/js/media-new-upload.js 23 */ 24 25 /* global plupload, pluploadL10n, wpUploaderInit, fileQueued, uploadStart, uploadSuccess, uploadComplete */ 26 27 ( function () { 28 // Guard against double execution (e.g. duplicate enqueues). 29 if ( window.__wpMediaNewUpload ) { 30 return; 31 } 32 33 const pipeline = window.wp && wp.mediaUploadPipeline; 34 35 // Bail unless the browser actually supports client-side processing. This 36 // is the clean no-op: when the isolation headers did not land, classic 37 // plupload keeps handling uploads. 38 if ( 39 ! pipeline || 40 typeof plupload === 'undefined' || 41 ! wp.a11y || 42 ! pipeline.configure() 43 ) { 44 return; 45 } 46 47 window.__wpMediaNewUpload = true; 48 49 const __ = wp.i18n.__; 50 const sprintf = wp.i18n.sprintf; 51 52 // Number of pipeline uploads currently in flight, used to fire 53 // uploadComplete() once both the pipeline and plupload are done. 54 let inFlightCount = 0; 55 56 /** 57 * Marks one pipeline upload as finished, firing uploadComplete() when 58 * nothing is left uploading. 59 * 60 * The built-in UploadComplete binding never fires for pipeline uploads 61 * because every file is removed from plupload before its queue starts. 62 * Files plupload is still uploading (a batch's classic share, a HEIC 63 * handed back) end with plupload's own UploadComplete instead. 64 * 65 * @param {plupload.Uploader} up The plupload uploader. 66 */ 67 function finishUpload( up ) { 68 inFlightCount--; 69 if ( inFlightCount === 0 && up.state !== plupload.STARTED ) { 70 uploadComplete(); 71 } 72 } 73 74 /** 75 * Returns the post the screen attaches uploads to, or 0. 76 * 77 * media-new.php validates `post_id` (the post must exist and be 78 * editable by the user) before printing it into the form, whereas the 79 * raw request value is what plupload carries in its multipart params. 80 * async-upload.php re-validates that raw value and silently uploads 81 * unattached; the REST API rejects it instead, so read the validated one. 82 * 83 * @return {number} The parent post ID, or 0. 84 */ 85 function getParentPostId() { 86 const input = /** @type {HTMLInputElement|null} */ ( 87 document.getElementById( 'post_id' ) 88 ); 89 const postId = input ? parseInt( input.value, 10 ) : 0; 90 return postId > 0 ? postId : 0; 91 } 92 93 /** 94 * Returns the screen's progress item for a plupload file. 95 * 96 * @param {plupload.File} file The plupload file. 97 * @return {HTMLElement|null} The item fileQueued() built, if it is still on the page. 98 */ 99 function getItem( file ) { 100 return document.getElementById( 'media-item-' + file.id ); 101 } 102 103 /** 104 * Renders a failed upload the way async-upload.php does for a 105 * server-side failure: an error notice with a real Dismiss button 106 * described by the notice, a screen reader announcement, and focus 107 * returned to the browse button once dismissed. 108 * 109 * @param {plupload.File} file The plupload file that failed. 110 * @param {string} message The reason the upload failed. 111 */ 112 function renderError( file, message ) { 113 const item = getItem( file ); 114 if ( ! item ) { 115 return; 116 } 117 118 const descriptionId = 'error-description-' + file.id; 119 120 const button = document.createElement( 'button' ); 121 button.type = 'button'; 122 button.id = 'dismiss-' + file.id; 123 button.className = 'dismiss button-link'; 124 button.setAttribute( 'aria-describedby', descriptionId ); 125 button.textContent = pluploadL10n.dismiss; 126 127 const heading = document.createElement( 'strong' ); 128 heading.textContent = pluploadL10n.error_uploading.replace( 129 '%s', 130 file.name 131 ); 132 133 const notice = document.createElement( 'div' ); 134 notice.id = descriptionId; 135 notice.className = 'notice notice-error error-div error'; 136 notice.append( 137 button, 138 ' ', 139 heading, 140 document.createElement( 'br' ), 141 message 142 ); 143 144 item.replaceChildren( notice ); 145 // Read by itemAjaxError() in plupload-handlers so a later server 146 // error for the same file is not rendered twice. 147 item.dataset.lastErr = file.id; 148 149 setTimeout( function () { 150 wp.a11y.speak( 151 sprintf( 152 /* translators: %s: Name of the file that failed to upload. */ 153 __( '%s has failed to upload.' ), 154 file.name 155 ) 156 ); 157 }, 1500 ); 158 159 button.addEventListener( 'click', function () { 160 item.remove(); 161 wp.a11y.speak( __( 'Error dismissed.' ) ); 162 const browseButton = document.getElementById( 163 'plupload-browse-button' 164 ); 165 if ( browseButton ) { 166 browseButton.focus(); 167 } 168 } ); 169 } 170 171 /** 172 * Reflects pipeline progress onto the screen's progress bar for a file. 173 * 174 * The bar is 200px wide at 100%, matching uploadProgress() in 175 * plupload-handlers. 176 * 177 * @param {plupload.File} file The plupload file being uploaded. 178 * @param {number} percent Progress percentage. 179 */ 180 function renderProgress( file, percent ) { 181 const item = getItem( file ); 182 if ( ! item ) { 183 return; 184 } 185 186 const bar = /** @type {HTMLElement|null} */ ( 187 item.querySelector( '.bar' ) 188 ); 189 if ( bar ) { 190 bar.style.width = 2 * percent + 'px'; 191 } 192 193 const label = item.querySelector( '.percent' ); 194 if ( label ) { 195 label.textContent = percent + '%'; 196 } 197 } 198 199 /** 200 * Intercepts files added to the plupload uploader. 201 * 202 * Builds the screen's progress items for the files the pipeline takes 203 * (see wp.mediaUploadPipeline.routeBatch()). Returns undefined to leave 204 * the whole batch to plupload's built-in handler. 205 * 206 * @param {plupload.Uploader} up The plupload uploader instance. 207 * @param {plupload.File[]} files Files added to the queue. 208 * @return {boolean|undefined} False to suppress the built-in handler. 209 */ 210 function handleFilesAdded( up, files ) { 211 return pipeline.routeBatch( up, files, { 212 // Without the parent post a file uploaded from 213 // media-new.php?post_id=N would land unattached even though the 214 // screen counts it against that post. 215 postId: getParentPostId(), 216 onStart: function ( /** @type {plupload.File[]} */ queued ) { 217 // Parity with the built-in handler: clear stale queue errors 218 // and run the shared upload-start housekeeping. 219 const queueErrors = document.getElementById( 'media-upload-error' ); 220 if ( queueErrors ) { 221 queueErrors.replaceChildren(); 222 } 223 uploadStart(); 224 225 // Count the whole batch first: addItems() can reject a file 226 // synchronously, which must not drain the count mid-batch. 227 inFlightCount += queued.length; 228 }, 229 onQueue: function ( 230 /** @type {plupload.File} */ file, 231 /** @type {File} */ nativeFile 232 ) { 233 // Build the screen's progress item for this file. 234 fileQueued( file ); 235 236 return { 237 onSuccess: function ( 238 /** @type {PipelineAttachment} */ attachment 239 ) { 240 // uploadSuccess() renders the finished attachment row 241 // via the existing async-upload.php markup endpoint; 242 // the server normally returns the ID as a string. 243 uploadSuccess( file, String( attachment.id ) ); 244 finishUpload( up ); 245 }, 246 onError: function ( /** @type {UploadError} */ error ) { 247 renderError( 248 file, 249 pipeline.getErrorText( error, nativeFile.name ) 250 ); 251 finishUpload( up ); 252 }, 253 // plupload builds its own progress item for the file it 254 // took over, so this one goes away. 255 onHandOff: function () { 256 const item = getItem( file ); 257 if ( item ) { 258 item.remove(); 259 } 260 finishUpload( up ); 261 }, 262 onProgress: function ( /** @type {number} */ percent ) { 263 renderProgress( file, percent ); 264 }, 265 }; 266 }, 267 } ); 268 } 269 270 /** 271 * Binds the interceptor to an uploader once it has initialized. 272 * 273 * plupload sorts handlers by priority (descending) and a `false` return 274 * breaks the chain, so priority 100 runs before and suppresses the 275 * built-in FilesAdded and UploadComplete handlers. 276 * 277 * @param {plupload.Uploader} up The uploader that initialized. 278 */ 279 function bindUploader( up ) { 280 if ( up.__wpMediaNewUploadBound ) { 281 return; 282 } 283 up.__wpMediaNewUploadBound = true; 284 285 up.bind( 286 'FilesAdded', 287 function ( up, files ) { 288 return handleFilesAdded( up, files ); 289 }, 290 null, 291 100 292 ); 293 294 // Hold back the built-in uploadComplete() while pipeline uploads 295 // are still running; finishUpload() fires it once they are done. 296 up.bind( 297 'UploadComplete', 298 function () { 299 return inFlightCount > 0 ? false : undefined; 300 }, 301 null, 302 100 303 ); 304 } 305 306 // plupload-handlers creates its uploader from the `wpUploaderInit` 307 // settings once the DOM is ready, after this script has run, and 308 // plupload binds the handlers in a settings `init` map while the 309 // uploader initializes. Hooking PostInit there reaches the uploader 310 // without depending on ready-callback ordering. The settings are 311 // missing when the browser uploader is disabled (the html-uploader 312 // fallback), in which case there is nothing to intercept. 313 if ( typeof wpUploaderInit !== 'object' || ! wpUploaderInit ) { 314 return; 315 } 316 317 // Either shape may already hold another script's handlers, which keep 318 // running ahead of the interceptor being bound. 319 const init = wpUploaderInit.init; 320 if ( typeof init === 'function' ) { 321 wpUploaderInit.init = function ( up ) { 322 init( up ); 323 bindUploader( up ); 324 }; 325 } else { 326 const postInit = init && init.PostInit; 327 wpUploaderInit.init = Object.assign( {}, init, { 328 PostInit: function ( 329 /** @type {plupload.Uploader} */ up, 330 /** @type {unknown[]} */ ...args 331 ) { 332 if ( typeof postInit === 'function' ) { 333 postInit.call( this, up, ...args ); 334 } 335 bindUploader( up ); 336 }, 337 } ); 338 } 339 } )();
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Oct 20 08:20:34 2026 | Cross-referenced by PHPXref |