[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> media-upload-pipeline.js (source)

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


Generated : Tue Oct 20 08:20:34 2026 Cross-referenced by PHPXref