/* global pluploadL10n, plupload, _wpPluploadSettings */
window.wp = window.wp || {};
( function( exports, $ ) {
if ( typeof _wpPluploadSettings === 'undefined' ) {
* The Plupload library provides cross-browser uploader UI integration.
* This object bridges the Plupload API to integrate uploads into the
* WordPress back end and the WordPress media experience.
* @param {object} options The options passed to the new plupload instance.
* @param {object} options.container The id of uploader container.
* @param {object} options.browser The id of button to trigger the file select.
* @param {object} options.dropzone The id of file drop target.
* @param {object} options.plupload An object of parameters to pass to the plupload instance.
* @param {object} options.params An object of parameters to pass to $_POST when uploading the file.
* Extends this.plupload.multipart_params under the hood.
Uploader = function( options ) {
isIE, // Not used, back-compat.
browser: 'browse_button',
upload: Uploader.browser.supported
this.supported = this.supports.upload;
if ( ! this.supported ) {
// Arguments to send to pluplad.Uploader().
// Use deep extend to ensure that multipart_params and other objects are cloned.
this.plupload = $.extend( true, { multipart_params: {} }, Uploader.defaults );
this.container = document.body; // Set default container.
* Extend the instance with options.
* Use deep extend to allow options.plupload to override individual
$.extend( true, this, options );
// Proxy all methods so this always refers to the current instance.
if ( typeof this[ key ] === 'function' ) {
this[ key ] = $.proxy( this[ key ], this );
// Ensure all elements are jQuery elements and have id attributes,
// then set the proper plupload arguments to the ids.
for ( key in elements ) {
this[ key ] = $( this[ key ] ).first();
if ( ! this[ key ].length ) {
if ( ! this[ key ].prop('id') ) {
this[ key ].prop( 'id', '__wp-uploader-id-' + Uploader.uuid++ );
this.plupload[ elements[ key ] ] = this[ key ].prop('id');
// If the uploader has neither a browse button nor a dropzone, bail.
if ( ! ( this.browser && this.browser.length ) && ! ( this.dropzone && this.dropzone.length ) ) {
// Initialize the plupload instance.
this.uploader = new plupload.Uploader( this.plupload );
// Set default params and remove this.params alias.
this.param( this.params || {} );
* Attempt to create image sub-sizes when an image was uploaded successfully
* but the server responded with HTTP 5xx error.
* @param {string} message Error message.
* @param {object} data Error data from Plupload.
* @param {plupload.File} file File that was uploaded.
tryAgain = function( message, data, file ) {
if ( ! data || ! data.responseHeaders ) {
error( pluploadL10n.http_error_image, data, file, 'no-retry' );
id = data.responseHeaders.match( /x-wp-upload-attachment-id:\s*(\d+)/i );
error( pluploadL10n.http_error_image, data, file, 'no-retry' );
times = tryAgainCount[ file.id ];
if ( times && times > 4 ) {
* The file may have been uploaded and attachment post created,
* but post-processing and resizing failed...
* Do a cleanup then tell the user to scale down the image and upload it again.
action: 'media-create-image-subsizes',
_wpnonce: _wpPluploadSettings.defaults.multipart_params._wpnonce,
_wp_upload_failed_cleanup: true,
error( message, data, file, 'no-retry' );
tryAgainCount[ file.id ] = 1;
tryAgainCount[ file.id ] = ++times;
// Another request to try to create the missing image sub-sizes.
action: 'media-create-image-subsizes',
_wpnonce: _wpPluploadSettings.defaults.multipart_params._wpnonce,
}).done( function( response ) {
if ( response.success ) {
fileUploaded( self.uploader, file, response );
if ( response.data && response.data.message ) {
message = response.data.message;
error( message, data, file, 'no-retry' );
}).fail( function( jqXHR ) {
// If another HTTP 5xx error, try try again...
if ( jqXHR.status >= 500 && jqXHR.status < 600 ) {
tryAgain( message, data, file );
error( message, data, file, 'no-retry' );
* Add a new error to the errors collection, so other modules can track
* and display errors. @see wp.Uploader.errors.
* @param {string} message Error message.
* @param {object} data Error data from Plupload.
* @param {plupload.File} file File that was uploaded.
* @param {string} retry Whether to try again to create image sub-sizes. Passing 'no-retry' will prevent it.
error = function( message, data, file, retry ) {
var isImage = file.type && file.type.indexOf( 'image/' ) === 0,
status = data && data.status;
// If the file is an image and the error is HTTP 5xx try to create sub-sizes again.
if ( retry !== 'no-retry' && isImage && status >= 500 && status < 600 ) {
tryAgain( message, data, file );
file.attachment.destroy();
Uploader.errors.unshift({
message: message || pluploadL10n.default_error,
self.error( message, data, file );
* After a file is successfully uploaded, update its model.
* @param {plupload.Uploader} up Uploader instance.
* @param {plupload.File} file File that was uploaded.
* @param {Object} response Object with response properties.
fileUploaded = function( up, file, response ) {
// Remove the "uploading" UI elements.
_.each( ['file','loaded','size','percent'], function( key ) {
file.attachment.unset( key );
file.attachment.set( _.extend( response.data, { uploading: false } ) );
wp.media.model.Attachment.get( response.data.id, file.attachment );
complete = Uploader.queue.all( function( attachment ) {
return ! attachment.get( 'uploading' );
self.success( file.attachment );
* After the Uploader has been initialized, initialize some behaviors for the dropzone.
* @param {plupload.Uploader} uploader Uploader instance.
this.uploader.bind( 'init', function( uploader ) {
var timer, active, dragdrop,
dropzone = self.dropzone;
dragdrop = self.supports.dragdrop = uploader.features.dragdrop && ! Uploader.browser.mobile;
// Generate drag/drop helper classes.
dropzone.toggleClass( 'supports-drag-drop', !! dragdrop );
return dropzone.unbind('.wp-uploader');
// 'dragenter' doesn't fire correctly, simulate it with a limited 'dragover'.
dropzone.on( 'dragover.wp-uploader', function() {
dropzone.trigger('dropzone:enter').addClass('drag-over');
dropzone.on('dragleave.wp-uploader, drop.wp-uploader', function() {
* Using an instant timer prevents the drag-over class
* from being quickly removed and re-added when elements
* inside the dropzone are repositioned.
* @see https://core.trac.wordpress.org/ticket/21705
timer = setTimeout( function() {
dropzone.trigger('dropzone:leave').removeClass('drag-over');
$(self).trigger( 'uploader:ready' );
this.uploader.bind( 'postinit', function( up ) {
this.browser.on( 'mouseenter', this.refresh );
this.uploader.disableBrowse( true );
$( self ).on( 'uploader:ready', function() {
$( '.moxie-shim-html5 input[type="file"]' )
* After files were filtered and added to the queue, create a model for each.
* @param {plupload.Uploader} up Uploader instance.
* @param {Array} files Array of file objects that were added to queue by the user.
this.uploader.bind( 'FilesAdded', function( up, files ) {
_.each( files, function( file ) {
// Ignore failed uploads.
if ( plupload.FAILED === file.status ) {
if ( file.type === 'image/heic' && up.settings.heic_upload_error ) {
// Show error but do not block uploading.
Uploader.errors.unshift({
message: pluploadL10n.unsupported_image,
// Generate attributes for a new `Attachment` model.
uploadedTo: wp.media.model.settings.post.id
}, _.pick( file, 'loaded', 'size', 'percent' ) );
// Handle early mime type scanning for images.
image = /(?:jpe?g|png|gif)$/i.exec( file.name );
// For images set the model's type and subtype attributes.
attributes.type = 'image';
// `jpeg`, `png` and `gif` are valid subtypes.
// `jpg` is not, so map it to `jpeg`.
attributes.subtype = ( 'jpg' === image[0] ) ? 'jpeg' : image[0];
// Create a model for the attachment, and add it to the Upload queue collection
// so listeners to the upload queue can track and display upload progress.
file.attachment = wp.media.model.Attachment.create( attributes );
Uploader.queue.add( file.attachment );
self.added( file.attachment );
this.uploader.bind( 'UploadProgress', function( up, file ) {
file.attachment.set( _.pick( file, 'loaded', 'percent' ) );
self.progress( file.attachment );
* After a file is successfully uploaded, update its model.
* @param {plupload.Uploader} up Uploader instance.
* @param {plupload.File} file File that was uploaded.
* @param {Object} response Object with response properties.
this.uploader.bind( 'FileUploaded', function( up, file, response ) {
response = JSON.parse( response.response );
return error( pluploadL10n.default_error, e, file );
if ( ! _.isObject( response ) || _.isUndefined( response.success ) ) {
return error( pluploadL10n.default_error, null, file );
} else if ( ! response.success ) {
return error( response.data && response.data.message, response.data, file );
// Success. Update the UI with the new attachment.
fileUploaded( up, file, response );
* When plupload surfaces an error, send it to the error handler.
* @param {plupload.Uploader} up Uploader instance.
* @param {Object} pluploadError Contains code, message and sometimes file and other details.
this.uploader.bind( 'Error', function( up, pluploadError ) {
var message = pluploadL10n.default_error,
// Check for plupload errors.
for ( key in Uploader.errorMap ) {
if ( pluploadError.code === plupload[ key ] ) {
message = Uploader.errorMap[ key ];
if ( typeof message === 'function' ) {
message = message( pluploadError.file, pluploadError );
error( message, pluploadError, pluploadError.file );
// Adds the 'defaults' and 'browser' properties.
$.extend( Uploader, _wpPluploadSettings );
// Map Plupload error codes to user friendly error messages.
'FAILED': pluploadL10n.upload_failed,
'FILE_EXTENSION_ERROR': pluploadL10n.invalid_filetype,
'IMAGE_FORMAT_ERROR': pluploadL10n.not_an_image,
'IMAGE_MEMORY_ERROR': pluploadL10n.image_memory_exceeded,
'IMAGE_DIMENSIONS_ERROR': pluploadL10n.image_dimensions_exceeded,
'GENERIC_ERROR': pluploadL10n.upload_failed,
'IO_ERROR': pluploadL10n.io_error,
'SECURITY_ERROR': pluploadL10n.security_error,
'FILE_SIZE_ERROR': function( file ) {
return pluploadL10n.file_exceeds_size_limit.replace( '%s', file.name );
'HTTP_ERROR': function( file ) {
if ( file.type && file.type.indexOf( 'image/' ) === 0 ) {
return pluploadL10n.http_error_image;
return pluploadL10n.http_error;
$.extend( Uploader.prototype, /** @lends wp.Uploader.prototype */{
* Acts as a shortcut to extending the uploader's multipart_params object.
* Returns the value of the key.
* Sets the value of a key.
* Sets values for a map of data.
param: function( key, value ) {
if ( arguments.length === 1 && typeof key === 'string' ) {