| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 /** 2 * Shared glue for routing classic admin uploads through the client-side 3 * media pipeline. 4 * 5 * The media frames (media-frame-upload.js) and the Add New Media 6 * File screen (media-new-upload.js) both intercept plupload and hand files 7 * to @wordpress/upload-media instead. Everything that is not specific to 8 * one screen's UI lives here: feature detection, configuring the 9 * upload-media store once, the REST sideload/finalize/delete helpers the 10 * store needs, queueing a file with success, error, and progress 11 * callbacks, the error text shown for a failed upload, and the guard that 12 * warns before leaving while uploads are in flight. 13 * 14 * Exposed as wp.mediaUploadPipeline. 15 * 16 * @output wp-admin/js/media-upload-pipeline.js 17 */ 18 19 /* global plupload */ 20 21 window.wp = window.wp || {}; 22 23 ( function ( wp ) { 24 const __ = wp.i18n.__; 25 const settings = window._wpMediaUploadPipelineSettings || {}; 26 /** @type {any} */ 27 let uploadStore; 28 let configured = false; 29 30 // Uploads in flight, keyed by the onSuccess callback queueFile() passes 31 // to addItems(). The store keeps that callback on the queue item, so it 32 // identifies the item even after HEIC conversion swaps its file. 33 const entries = new Map(); 34 35 // Files handed back to classic plupload after the pipeline gave up on 36 // them, so the interceptors let plupload's own handler take them. 37 const classicFiles = new WeakSet(); 38 39 const imageSizeCount = Object.keys( settings.allImageSizes || {} ).length; 40 41 /** 42 * Recursively appends data to a FormData object, supporting nested objects. 43 * 44 * Mirrors flattenFormData() in the media-utils package. 45 * 46 * @param {FormData} formData The form data to append to. 47 * @param {string} key The key to append under. 48 * @param {*} data The value to append. 49 */ 50 function flattenFormData( formData, key, data ) { 51 if ( 52 data !== null && 53 typeof data === 'object' && 54 Object.getPrototypeOf( data ) === Object.prototype 55 ) { 56 Object.keys( data ).forEach( function ( name ) { 57 flattenFormData( formData, key + '[' + name + ']', data[ name ] ); 58 } ); 59 } else if ( data !== undefined ) { 60 formData.append( key, String( data ) ); 61 } 62 } 63 64 /** 65 * Sideloads a client-generated thumbnail to an existing attachment. 66 * 67 * Reimplements the private sideloadMedia() helper from the media-utils 68 * package as a thin apiFetch wrapper. 69 * 70 * @param {Object} args The sideload arguments. 71 * @param {File} args.file The sub-size to sideload. 72 * @param {number} args.attachmentId The attachment to sideload it to. 73 * @param {Record<string, unknown>} [args.additionalData] Extra fields to send with it. 74 * @param {AbortSignal} [args.signal] Signal aborting the request. 75 * @param {( subSize: Object ) => void} [args.onSuccess] Called with the sideloaded sub-size. 76 * @param {( error: Error ) => void} [args.onError] Called when the sideload failed. 77 */ 78 function mediaSideload( args ) { 79 const file = args.file; 80 const additionalData = args.additionalData || {}; 81 82 const data = new FormData(); 83 data.append( 'file', file, file.name || file.type.replace( '/', '.' ) ); 84 Object.keys( additionalData ).forEach( function ( key ) { 85 flattenFormData( data, key, additionalData[ key ] ); 86 } ); 87 88 wp.apiFetch( { 89 path: '/wp/v2/media/' + args.attachmentId + '/sideload', 90 body: data, 91 method: 'POST', 92 signal: args.signal, 93 } ) 94 .then( function ( /** @type {Object} */ subSize ) { 95 if ( args.onSuccess ) { 96 args.onSuccess( subSize ); 97 } 98 } ) 99 .catch( function ( /** @type {unknown} */ error ) { 100 if ( args.onError ) { 101 let normalized = error; 102 if ( ! ( error instanceof Error ) ) { 103 const rest = /** @type {UploadError} */ ( error ); 104 normalized = new Error( 105 rest && rest.message ? rest.message : String( error ) 106 ); 107 } 108 args.onError( /** @type {Error} */ ( normalized ) ); 109 } 110 } ); 111 } 112 113 /** 114 * Finalizes an upload once all client-side processing is complete. 115 * 116 * Reimplements the private mediaFinalize() helper. The returned 117 * attachment is load-bearing: it carries the post-finalize (scaled) 118 * URL used for srcset. 119 * 120 * @param {number} id The parent attachment ID. 121 * @param {Object[]} subSizes Accumulated sub-size data. 122 * @return {Promise<PipelineAttachment|undefined>} Resolves with the transformed attachment. 123 */ 124 async function mediaFinalize( id, subSizes ) { 125 /** @type {Object|undefined} */ 126 const response = await wp.apiFetch( { 127 path: '/wp/v2/media/' + id + '/finalize', 128 method: 'POST', 129 data: { sub_sizes: subSizes || [] }, 130 } ); 131 if ( ! response ) { 132 return undefined; 133 } 134 return wp.mediaUtils.transformAttachment( response ); 135 } 136 137 /** 138 * Deletes an attachment whose client-side processing failed outright. 139 * 140 * The queue calls this when every sub-size sideload for an upload fails: 141 * without it the original file is left behind as an attachment with no 142 * metadata, visible in the Media Library after the next page load. The 143 * block editor passes the same setting. 144 * 145 * @param {number} id The attachment ID to delete. 146 * @return {Promise<unknown>} Resolves once the attachment is deleted. 147 */ 148 function mediaDelete( id ) { 149 return wp.apiFetch( { 150 path: '/wp/v2/media/' + id + '?force=true', 151 method: 'DELETE', 152 } ); 153 } 154 155 /** 156 * Whether every script the pipeline relies on is present. 157 * 158 * @return {boolean} True when the dependencies loaded. 159 */ 160 function hasDependencies() { 161 return Boolean( 162 typeof plupload !== 'undefined' && 163 wp.uploadMedia && 164 wp.mediaUtils && 165 wp.data && 166 wp.element && 167 wp.apiFetch 168 ); 169 } 170 171 /** 172 * Whether the browser can run the client-side pipeline on this page. 173 * 174 * False when the isolation headers did not land (the page is not 175 * cross-origin isolated) or the browser lacks the required features, 176 * in which case classic plupload keeps handling uploads. 177 * 178 * @return {boolean} True when client-side processing is available. 179 */ 180 function isSupported() { 181 return Boolean( 182 hasDependencies() && 183 wp.uploadMedia.detectClientSideMediaSupport && 184 wp.uploadMedia.detectClientSideMediaSupport().supported 185 ); 186 } 187 188 /** 189 * Estimates the progress (0-100) of a queue item. 190 * 191 * The pipeline never reports a numeric `progress` on its queue items 192 * (nothing dispatches updateItemProgress), so estimate one from the 193 * item's operation queue instead: each finished operation (prepare, 194 * transcode, upload, thumbnails, finalize) advances the bar, and the 195 * sub-sizes sideloaded so far advance it within thumbnail generation. 196 * `item.progress` is preferred whenever it is present. 197 * 198 * @param {QueueItem} item The upload-media queue item. 199 * @param {UploadEntry} entry The pipeline's bookkeeping for the upload. 200 * @return {number} Estimated progress. 201 */ 202 function estimateProgress( item, entry ) { 203 if ( typeof item.progress === 'number' ) { 204 return item.progress; 205 } 206 207 const remaining = item.operations ? item.operations.length : 0; 208 let totals = entry.totals; 209 if ( ! totals ) { 210 totals = { total: remaining, remaining: remaining }; 211 entry.totals = totals; 212 } 213 // Operations are appended after preparation, so grow the total. 214 if ( remaining > totals.remaining ) { 215 totals.total += remaining - totals.remaining; 216 } 217 totals.remaining = remaining; 218 219 if ( totals.total === 0 ) { 220 return 0; 221 } 222 223 const completed = totals.total - remaining; 224 let fraction = 0; 225 if ( 226 'THUMBNAIL_GENERATION' === item.currentOperation && 227 imageSizeCount > 0 228 ) { 229 fraction = Math.min( 230 1, 231 ( item.subSizes || [] ).length / imageSizeCount 232 ); 233 } 234 235 return ( ( completed + fraction ) / totals.total ) * 100; 236 } 237 238 /** 239 * Tracks queued uploads on every change to the upload-media store. 240 * 241 * Reports progress, which holds at 99 until the upload's success 242 * callback has run so nothing looks finished before the screen has 243 * synced the result, and notes the attachment once the original is 244 * uploaded. Sub-size children are skipped; only top-level items matter. 245 * 246 * The store can also remove an item without calling either of its 247 * callbacks (it only calls onSuccess when the item has an attachment), 248 * so an upload that was seen in the store and is gone fails here 249 * instead of staying in flight, and holding the unload guard, for good. 250 */ 251 function onStoreChange() { 252 if ( entries.size === 0 ) { 253 return; 254 } 255 256 const present = new Set(); 257 const items = wp.data.select( uploadStore ).getItems(); 258 items.forEach( function ( /** @type {QueueItem} */ item ) { 259 if ( item.parentId ) { 260 return; 261 } 262 263 const entry = entries.get( item.onSuccess ); 264 if ( ! entry ) { 265 return; 266 } 267 268 present.add( item.onSuccess ); 269 entry.seen = true; 270 if ( item.attachment && item.attachment.id ) { 271 entry.attachmentId = item.attachment.id; 272 } 273 274 if ( ! entry.onProgress ) { 275 return; 276 } 277 278 const percent = Math.min( 279 99, 280 Math.round( estimateProgress( item, entry ) ) 281 ); 282 if ( percent !== entry.lastPercent ) { 283 entry.lastPercent = percent; 284 entry.onProgress( percent ); 285 } 286 } ); 287 288 entries.forEach( function ( entry, key ) { 289 if ( entry.seen && ! present.has( key ) ) { 290 entry.fail( { 291 message: __( 'An error occurred while uploading the file.' ), 292 } ); 293 } 294 } ); 295 } 296 297 /** 298 * Whether something else already configured the store on this page. 299 * 300 * The block editor renders its own MediaUploadProvider into the same 301 * store. Neither `mediaSideload` nor `mediaFinalize` has a default, so 302 * their presence proves a provider's settings landed, whichever one 303 * rendered them. 304 * 305 * @return {boolean} True when the store carries a provider's settings. 306 */ 307 function isStoreConfigured() { 308 const storeSettings = wp.data.select( uploadStore ).getSettings(); 309 return Boolean( 310 storeSettings && 311 storeSettings.mediaSideload && 312 storeSettings.mediaFinalize 313 ); 314 } 315 316 /** 317 * Configures the upload-media store for this page, once. 318 * 319 * Rendering the provider with useSubRegistry: false wires the settings 320 * into the store that wp.data.dispatch/select address. The block editor 321 * does the same for its own uploads, so on its screens no second 322 * provider is rendered: whichever committed last would win, and this 323 * one could replace the editor's mediaUpload. Uploads wait for the 324 * editor's settings instead (see isReady()). 325 * 326 * @return {boolean} True when the pipeline is configured and usable. 327 */ 328 function configure() { 329 if ( configured ) { 330 return true; 331 } 332 333 if ( ! isSupported() ) { 334 return false; 335 } 336 337 configured = true; 338 uploadStore = wp.uploadMedia.store; 339 340 if ( settings.isBlockEditor || isStoreConfigured() ) { 341 wp.data.subscribe( onStoreChange, uploadStore ); 342 addUnloadGuard(); 343 return true; 344 } 345 346 /* 347 * The media-utils package branches on this flag: without it 348 * uploadMedia() creates and revokes a throwaway blob URL per file 349 * and emits an extra onFileChange carrying it. The block editor 350 * sets the same flag before configuring the same pipeline. 351 */ 352 window.__clientSideMediaProcessing = true; 353 354 wp.element 355 .createRoot( document.createElement( 'div' ) ) 356 .render( 357 wp.element.createElement( wp.uploadMedia.MediaUploadProvider, { 358 settings: { 359 mediaUpload: wp.mediaUtils.uploadMedia, 360 mediaSideload: mediaSideload, 361 mediaFinalize: mediaFinalize, 362 mediaDelete: mediaDelete, 363 maxUploadFileSize: settings.maxUploadFileSize, 364 allowedMimeTypes: settings.allowedMimeTypes, 365 allImageSizes: settings.allImageSizes, 366 bigImageSizeThreshold: settings.bigImageSizeThreshold, 367 imageStripMeta: settings.imageStripMeta, 368 imageMaxBitDepth: settings.imageMaxBitDepth, 369 generateAnimatedImageSubsizes: 370 settings.generateAnimatedImageSubsizes, 371 }, 372 useSubRegistry: false, 373 } ) 374 ); 375 376 // Only the upload-media store can change what this listener reads. 377 wp.data.subscribe( onStoreChange, uploadStore ); 378 addUnloadGuard(); 379 380 return true; 381 } 382 383 /** 384 * Warns before leaving while uploads are in flight. 385 * 386 * Thumbnails that have not been sideloaded yet are lost and the 387 * attachment is left unfinalized, unlike classic uploads that complete 388 * server-side once the bytes arrive. 389 */ 390 function addUnloadGuard() { 391 window.addEventListener( 'beforeunload', function ( event ) { 392 if ( entries.size > 0 ) { 393 event.preventDefault(); 394 } 395 } ); 396 } 397 398 /** 399 * Whether the store has received a provider's settings. 400 * 401 * A provider renders asynchronously, so a file added before its 402 * settings land must be left to classic plupload - a degradation, 403 * never data loss. The store's default state already carries a no-op 404 * `mediaUpload`, so only settings a provider alone supplies prove the 405 * real ones have landed: a file queued before that would be handed to 406 * the no-op and never upload. On block editor screens only the 407 * editor's provider supplies them. 408 * 409 * @return {boolean} True when the store is ready to accept files. 410 */ 411 function isReady() { 412 return configured && isStoreConfigured(); 413 } 414 415 /** 416 * Splits a batch of plupload files between the pipeline and classic 417 * plupload. 418 * 419 * Audio files stay classic so they keep the title and description that 420 * media_handle_upload() derives from their ID3 tags, which the REST 421 * endpoint does not do. So do JPEG and PNG images while plupload's own 422 * client-side resize is enabled (the "Scale images" setting, or a 423 * `resize` passed through `plupload_init`): plupload scales those down 424 * before uploading them, the pipeline would not. 425 * 426 * The whole batch stays classic when a file cannot be re-queued on 427 * plupload or is already plupload's: plupload returns no native File 428 * for sources it cannot expose as one (the html4 runtime, say), and a 429 * file the pipeline handed back (see handOffToClassic()) is plupload's 430 * to upload. 431 * 432 * @param {plupload.Uploader} up The plupload uploader the files were added to. 433 * @param {plupload.File[]} files Files added to the plupload queue. 434 * @return {{ pipeline: plupload.File[], classic: plupload.File[] }|null} The split, 435 * or null when the whole batch stays on the classic path. 436 */ 437 function partitionBatch( up, files ) { 438 const resize = up.settings && up.settings.resize; 439 const isResizing = Boolean( resize && resize.enabled ); 440 /** @type {{ pipeline: plupload.File[], classic: plupload.File[] }} */ 441 const batch = { pipeline: [], classic: [] }; 442 443 for ( const file of files ) { 444 if ( plupload.FAILED === file.status ) { 445 continue; 446 } 447 const nativeFile = file.getNative && file.getNative(); 448 if ( ! nativeFile || classicFiles.has( nativeFile ) ) { 449 return null; 450 } 451 const type = file.type || ''; 452 if ( 453 /^audio\//i.test( type ) || 454 ( isResizing && /^image\/(?:jpeg|png)$/i.test( type ) ) 455 ) { 456 batch.classic.push( file ); 457 } else { 458 batch.pipeline.push( file ); 459 } 460 } 461 462 return batch.pipeline.length ? batch : null; 463 } 464 465 /** 466 * Hands a file to classic plupload, whose own FilesAdded handler queues 467 * and starts the upload. 468 * 469 * @param {plupload.Uploader} up The plupload uploader. 470 * @param {File} nativeFile The file to upload. 471 */ 472 function addClassicFile( up, nativeFile ) { 473 classicFiles.add( nativeFile ); 474 up.addFile( nativeFile ); 475 } 476 477 /** 478 * Re-queues the classic share of a batch on plupload. 479 * 480 * Suppressing plupload's built-in FilesAdded handler covers the whole 481 * batch, so the files partitionBatch() left to plupload are removed and 482 * added back on their own. 483 * 484 * @param {plupload.Uploader} up The plupload uploader. 485 * @param {plupload.File[]} files The files to upload classically. 486 */ 487 function handOffBatchToClassic( up, files ) { 488 files.forEach( function ( file ) { 489 // partitionBatch() already established that every file has one. 490 const nativeFile = /** @type {File} */ ( file.getNative() ); 491 up.removeFile( file ); 492 addClassicFile( up, nativeFile ); 493 } ); 494 } 495 496 /** 497 * Re-queues a file on classic plupload when the server can do what the 498 * browser could not. 499 * 500 * The pipeline converts HEIC in the browser and fails outright when the 501 * platform has no HEIC decoder (Linux, and Windows without the HEVC 502 * extension), whereas the classic upload lets the server convert it. 503 * When the server's image editor supports HEIC - plupload carries no 504 * `heic_upload_error` setting then - the file goes back to plupload, 505 * whose own FilesAdded handler queues and starts the classic upload. 506 * Nothing else can convert it, so any other failure stands. 507 * 508 * @param {plupload.Uploader} up The plupload uploader the file came from. 509 * @param {UploadError} error Why the pipeline gave up on it. 510 * @param {File} nativeFile The file the pipeline was given. 511 * @return {boolean} True when the file was handed to classic plupload. 512 */ 513 function handOffToClassic( up, error, nativeFile ) { 514 const errorCodes = wp.uploadMedia.ErrorCode || {}; 515 if ( 516 ! error || 517 ! errorCodes.HEIC_DECODE_ERROR || 518 error.code !== errorCodes.HEIC_DECODE_ERROR || 519 ! up || 520 typeof up.addFile !== 'function' || 521 ( up.settings && up.settings.heic_upload_error ) 522 ) { 523 return false; 524 } 525 526 addClassicFile( up, nativeFile ); 527 return true; 528 } 529 530 /** 531 * Builds the extra fields to send with an upload from plupload's 532 * multipart params. 533 * 534 * Anything a plugin added through the `plupload_default_params` filter 535 * or wp.Uploader.param() reached the classic upload as $_POST fields, 536 * so it is forwarded to the REST request the same way. The classic 537 * transport's own fields are dropped: `action` and `_wpnonce` belong to 538 * async-upload.php, and `post_id` is spelled `post` by the REST API and 539 * passed separately after the screen has validated it. 540 * 541 * @param {Record<string, string>} params Plupload's multipart_params. 542 * @param {number} postId The validated post to attach the upload to, or 0. 543 * @return {Record<string, unknown>} Additional data for the upload. 544 */ 545 function additionalDataFromParams( params, postId ) { 546 /** @type {Record<string, unknown>} */ 547 const additionalData = {}; 548 Object.keys( params || {} ).forEach( function ( key ) { 549 if ( 'action' === key || '_wpnonce' === key || 'post_id' === key ) { 550 return; 551 } 552 additionalData[ key ] = params[ key ]; 553 } ); 554 if ( postId ) { 555 additionalData.post = postId; 556 } 557 return additionalData; 558 } 559 560 /** 561 * Queues a file for client-side processing and upload. 562 * 563 * @param {File} nativeFile The file to upload. 564 * @param {Record<string, unknown>} additionalData Extra fields to send with the attachment. 565 * @param {QueueFileCallbacks} [callbacks] Lifecycle callbacks. 566 */ 567 function queueFile( nativeFile, additionalData, callbacks = {} ) { 568 /** 569 * Handles successful upload. 570 * 571 * @param {PipelineAttachment[]} attachments The uploaded attachments. 572 */ 573 function onSuccess( attachments ) { 574 entries.delete( onSuccess ); 575 if ( callbacks.onSuccess ) { 576 callbacks.onSuccess( attachments[ 0 ] ); 577 } 578 } 579 580 /** @type {UploadEntry} */ 581 const entry = { 582 onProgress: callbacks.onProgress, 583 lastPercent: -1, 584 totals: null, 585 seen: false, 586 attachmentId: 0, 587 fail: function ( error ) { 588 if ( ! entries.delete( onSuccess ) ) { 589 return; 590 } 591 // The original already reached the server (finalize failed, 592 // say), so delete it: otherwise a retry leaves a duplicate 593 // next to an attachment whose metadata was never finalized. 594 // The store only deletes it itself when every sub-size failed. 595 if ( entry.attachmentId ) { 596 mediaDelete( entry.attachmentId ).catch( function () {} ); 597 } 598 if ( callbacks.onError ) { 599 callbacks.onError( error ); 600 } 601 }, 602 }; 603 entries.set( onSuccess, entry ); 604 605 wp.data.dispatch( uploadStore ).addItems( { 606 files: [ nativeFile ], 607 additionalData: additionalData || {}, 608 onSuccess: onSuccess, 609 onError: function ( /** @type {UploadError|string} */ error ) { 610 // Also reached for files addItems() rejects before queueing. 611 // The block editor's mediaUpload() setting, which the store 612 // uses once the editor has configured it, reports transport 613 // failures as a bare message string. 614 entry.fail( 615 typeof error === 'string' ? { message: error } : error 616 ); 617 }, 618 } ); 619 } 620 621 /** 622 * Routes a batch of files added to a plupload uploader through the 623 * pipeline. 624 * 625 * Returns undefined (not false) when the pipeline cannot take the batch 626 * so plupload's built-in FilesAdded handler runs and uploads it 627 * server-side - a degradation, never data loss. Otherwise queues the 628 * files the pipeline can take, re-queues the rest on plupload, and 629 * returns false to suppress the built-in handler, which would otherwise 630 * start a classic upload of every file. 631 * 632 * The classic flow posts plupload's multipart params to 633 * async-upload.php, so they are forwarded to the REST request for 634 * plugins reading $_POST, with `post_id` replaced by `options.postId`. 635 * 636 * @param {plupload.Uploader} up The plupload uploader the files were added to. 637 * @param {plupload.File[]} files Files added to the queue. 638 * @param {RouteBatchOptions} options The screen's hooks. 639 * @return {boolean|undefined} False to suppress the built-in handler. 640 */ 641 function routeBatch( up, files, options ) { 642 if ( ! isReady() ) { 643 return; 644 } 645 646 const batch = partitionBatch( up, files ); 647 if ( ! batch ) { 648 return; 649 } 650 651 const params = ( up.settings && up.settings.multipart_params ) || {}; 652 const additionalData = additionalDataFromParams( 653 params, 654 options.postId 655 ); 656 657 if ( options.onStart ) { 658 options.onStart( batch.pipeline ); 659 } 660 661 batch.pipeline.forEach( function ( /** @type {plupload.File} */ file ) { 662 // partitionBatch() already established that every file has one. 663 const nativeFile = /** @type {File} */ ( file.getNative() ); 664 const callbacks = options.onQueue( file, nativeFile ); 665 666 // Remove the file from plupload so it is not uploaded twice. 667 up.removeFile( file ); 668 669 queueFile( nativeFile, additionalData, { 670 onSuccess: callbacks.onSuccess, 671 onProgress: callbacks.onProgress, 672 onError: function ( /** @type {UploadError} */ error ) { 673 // When the server can convert what the browser could 674 // not, plupload takes the file from here and shows its 675 // own progress. 676 if ( handOffToClassic( up, error, nativeFile ) ) { 677 callbacks.onHandOff(); 678 return; 679 } 680 callbacks.onError( error ); 681 }, 682 } ); 683 } ); 684 685 handOffBatchToClassic( up, batch.classic ); 686 687 up.refresh(); 688 689 return false; 690 } 691 692 /** 693 * Builds the display text for a failed upload. 694 * 695 * wp.uploadMedia.getErrorMessage() maps an error *code* and a file name 696 * to a { title, description, action } object, so it can neither be handed 697 * the Error itself nor used as a string. Only codes it actually maps are 698 * worth using: its fallback (and the GENERAL code) says nothing the 699 * error's own message does not, and preferring the message there keeps a 700 * server-supplied reason instead of replacing it with "Please try again." 701 * 702 * @param {UploadError} error The upload error. 703 * @param {string} fileName Name of the file that failed to upload. 704 * @return {string} A human-readable message. 705 */ 706 function getErrorText( error, fileName ) { 707 const errorCodes = wp.uploadMedia.ErrorCode || {}; 708 const code = error && error.code; 709 let details; 710 711 if ( 712 code && 713 code !== errorCodes.GENERAL && 714 Object.prototype.hasOwnProperty.call( errorCodes, code ) && 715 wp.uploadMedia.getErrorMessage 716 ) { 717 details = wp.uploadMedia.getErrorMessage( code, fileName ); 718 719 if ( details && details.description ) { 720 return details.action ? 721 details.description + ' ' + details.action : 722 details.description; 723 } 724 } 725 726 return ( 727 ( error && error.message ) || 728 __( 'An error occurred while uploading the file.' ) 729 ); 730 } 731 732 wp.mediaUploadPipeline = { 733 isSupported: isSupported, 734 configure: configure, 735 isReady: isReady, 736 routeBatch: routeBatch, 737 getErrorText: getErrorText, 738 }; 739 } )( window.wp );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Oct 20 08:20:34 2026 | Cross-referenced by PHPXref |