ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

wp-plupload.js (16442B)


      1 /* global pluploadL10n, plupload, _wpPluploadSettings */
      2 
      3 /**
      4  * @namespace wp
      5  */
      6 window.wp = window.wp || {};
      7 
      8 ( function( exports, $ ) {
      9 	var Uploader;
     10 
     11 	if ( typeof _wpPluploadSettings === 'undefined' ) {
     12 		return;
     13 	}
     14 
     15 	/**
     16 	 * A WordPress uploader.
     17 	 *
     18 	 * The Plupload library provides cross-browser uploader UI integration.
     19 	 * This object bridges the Plupload API to integrate uploads into the
     20 	 * WordPress back end and the WordPress media experience.
     21 	 *
     22 	 * @class
     23 	 * @memberOf wp
     24 	 * @alias wp.Uploader
     25 	 *
     26 	 * @param {object} options           The options passed to the new plupload instance.
     27 	 * @param {object} options.container The id of uploader container.
     28 	 * @param {object} options.browser   The id of button to trigger the file select.
     29 	 * @param {object} options.dropzone  The id of file drop target.
     30 	 * @param {object} options.plupload  An object of parameters to pass to the plupload instance.
     31 	 * @param {object} options.params    An object of parameters to pass to $_POST when uploading the file.
     32 	 *                                   Extends this.plupload.multipart_params under the hood.
     33 	 */
     34 	Uploader = function( options ) {
     35 		var self = this,
     36 			isIE, // Not used, back-compat.
     37 			elements = {
     38 				container: 'container',
     39 				browser:   'browse_button',
     40 				dropzone:  'drop_element'
     41 			},
     42 			tryAgainCount = {},
     43 			tryAgain,
     44 			key,
     45 			error,
     46 			fileUploaded;
     47 
     48 		this.supports = {
     49 			upload: Uploader.browser.supported
     50 		};
     51 
     52 		this.supported = this.supports.upload;
     53 
     54 		if ( ! this.supported ) {
     55 			return;
     56 		}
     57 
     58 		// Arguments to send to pluplad.Uploader().
     59 		// Use deep extend to ensure that multipart_params and other objects are cloned.
     60 		this.plupload = $.extend( true, { multipart_params: {} }, Uploader.defaults );
     61 		this.container = document.body; // Set default container.
     62 
     63 		/*
     64 		 * Extend the instance with options.
     65 		 *
     66 		 * Use deep extend to allow options.plupload to override individual
     67 		 * default plupload keys.
     68 		 */
     69 		$.extend( true, this, options );
     70 
     71 		// Proxy all methods so this always refers to the current instance.
     72 		for ( key in this ) {
     73 			if ( typeof this[ key ] === 'function' ) {
     74 				this[ key ] = $.proxy( this[ key ], this );
     75 			}
     76 		}
     77 
     78 		// Ensure all elements are jQuery elements and have id attributes,
     79 		// then set the proper plupload arguments to the ids.
     80 		for ( key in elements ) {
     81 			if ( ! this[ key ] ) {
     82 				continue;
     83 			}
     84 
     85 			this[ key ] = $( this[ key ] ).first();
     86 
     87 			if ( ! this[ key ].length ) {
     88 				delete this[ key ];
     89 				continue;
     90 			}
     91 
     92 			if ( ! this[ key ].prop('id') ) {
     93 				this[ key ].prop( 'id', '__wp-uploader-id-' + Uploader.uuid++ );
     94 			}
     95 
     96 			this.plupload[ elements[ key ] ] = this[ key ].prop('id');
     97 		}
     98 
     99 		// If the uploader has neither a browse button nor a dropzone, bail.
    100 		if ( ! ( this.browser && this.browser.length ) && ! ( this.dropzone && this.dropzone.length ) ) {
    101 			return;
    102 		}
    103 
    104 		// Initialize the plupload instance.
    105 		this.uploader = new plupload.Uploader( this.plupload );
    106 		delete this.plupload;
    107 
    108 		// Set default params and remove this.params alias.
    109 		this.param( this.params || {} );
    110 		delete this.params;
    111 
    112 		/**
    113 		 * Attempt to create image sub-sizes when an image was uploaded successfully
    114 		 * but the server responded with HTTP 5xx error.
    115 		 *
    116 		 * @since 5.3.0
    117 		 *
    118 		 * @param {string}        message Error message.
    119 		 * @param {object}        data    Error data from Plupload.
    120 		 * @param {plupload.File} file    File that was uploaded.
    121 		 */
    122 		tryAgain = function( message, data, file ) {
    123 			var times, id;
    124 
    125 			if ( ! data || ! data.responseHeaders ) {
    126 				error( pluploadL10n.http_error_image, data, file, 'no-retry' );
    127 				return;
    128 			}
    129 
    130 			id = data.responseHeaders.match( /x-wp-upload-attachment-id:\s*(\d+)/i );
    131 
    132 			if ( id && id[1] ) {
    133 				id = id[1];
    134 			} else {
    135 				error( pluploadL10n.http_error_image, data, file, 'no-retry' );
    136 				return;
    137 			}
    138 
    139 			times = tryAgainCount[ file.id ];
    140 
    141 			if ( times && times > 4 ) {
    142 				/*
    143 				 * The file may have been uploaded and attachment post created,
    144 				 * but post-processing and resizing failed...
    145 				 * Do a cleanup then tell the user to scale down the image and upload it again.
    146 				 */
    147 				$.ajax({
    148 					type: 'post',
    149 					url: ajaxurl,
    150 					dataType: 'json',
    151 					data: {
    152 						action: 'media-create-image-subsizes',
    153 						_wpnonce: _wpPluploadSettings.defaults.multipart_params._wpnonce,
    154 						attachment_id: id,
    155 						_wp_upload_failed_cleanup: true,
    156 					}
    157 				});
    158 
    159 				error( message, data, file, 'no-retry' );
    160 				return;
    161 			}
    162 
    163 			if ( ! times ) {
    164 				tryAgainCount[ file.id ] = 1;
    165 			} else {
    166 				tryAgainCount[ file.id ] = ++times;
    167 			}
    168 
    169 			// Another request to try to create the missing image sub-sizes.
    170 			$.ajax({
    171 				type: 'post',
    172 				url: ajaxurl,
    173 				dataType: 'json',
    174 				data: {
    175 					action: 'media-create-image-subsizes',
    176 					_wpnonce: _wpPluploadSettings.defaults.multipart_params._wpnonce,
    177 					attachment_id: id,
    178 				}
    179 			}).done( function( response ) {
    180 				if ( response.success ) {
    181 					fileUploaded( self.uploader, file, response );
    182 				} else {
    183 					if ( response.data && response.data.message ) {
    184 						message = response.data.message;
    185 					}
    186 
    187 					error( message, data, file, 'no-retry' );
    188 				}
    189 			}).fail( function( jqXHR ) {
    190 				// If another HTTP 5xx error, try try again...
    191 				if ( jqXHR.status >= 500 && jqXHR.status < 600 ) {
    192 					tryAgain( message, data, file );
    193 					return;
    194 				}
    195 
    196 				error( message, data, file, 'no-retry' );
    197 			});
    198 		}
    199 
    200 		/**
    201 		 * Custom error callback.
    202 		 *
    203 		 * Add a new error to the errors collection, so other modules can track
    204 		 * and display errors. @see wp.Uploader.errors.
    205 		 *
    206 		 * @param {string}        message Error message.
    207 		 * @param {object}        data    Error data from Plupload.
    208 		 * @param {plupload.File} file    File that was uploaded.
    209 		 * @param {string}        retry   Whether to try again to create image sub-sizes. Passing 'no-retry' will prevent it.
    210 		 */
    211 		error = function( message, data, file, retry ) {
    212 			var isImage = file.type && file.type.indexOf( 'image/' ) === 0,
    213 				status = data && data.status;
    214 
    215 			// If the file is an image and the error is HTTP 5xx try to create sub-sizes again.
    216 			if ( retry !== 'no-retry' && isImage && status >= 500 && status < 600 ) {
    217 				tryAgain( message, data, file );
    218 				return;
    219 			}
    220 
    221 			if ( file.attachment ) {
    222 				file.attachment.destroy();
    223 			}
    224 
    225 			Uploader.errors.unshift({
    226 				message: message || pluploadL10n.default_error,
    227 				data:    data,
    228 				file:    file
    229 			});
    230 
    231 			self.error( message, data, file );
    232 		};
    233 
    234 		/**
    235 		 * After a file is successfully uploaded, update its model.
    236 		 *
    237 		 * @param {plupload.Uploader} up       Uploader instance.
    238 		 * @param {plupload.File}     file     File that was uploaded.
    239 		 * @param {Object}            response Object with response properties.
    240 		 */
    241 		fileUploaded = function( up, file, response ) {
    242 			var complete;
    243 
    244 			// Remove the "uploading" UI elements.
    245 			_.each( ['file','loaded','size','percent'], function( key ) {
    246 				file.attachment.unset( key );
    247 			} );
    248 
    249 			file.attachment.set( _.extend( response.data, { uploading: false } ) );
    250 
    251 			wp.media.model.Attachment.get( response.data.id, file.attachment );
    252 
    253 			complete = Uploader.queue.all( function( attachment ) {
    254 				return ! attachment.get( 'uploading' );
    255 			});
    256 
    257 			if ( complete ) {
    258 				Uploader.queue.reset();
    259 			}
    260 
    261 			self.success( file.attachment );
    262 		}
    263 
    264 		/**
    265 		 * After the Uploader has been initialized, initialize some behaviors for the dropzone.
    266 		 *
    267 		 * @param {plupload.Uploader} uploader Uploader instance.
    268 		 */
    269 		this.uploader.bind( 'init', function( uploader ) {
    270 			var timer, active, dragdrop,
    271 				dropzone = self.dropzone;
    272 
    273 			dragdrop = self.supports.dragdrop = uploader.features.dragdrop && ! Uploader.browser.mobile;
    274 
    275 			// Generate drag/drop helper classes.
    276 			if ( ! dropzone ) {
    277 				return;
    278 			}
    279 
    280 			dropzone.toggleClass( 'supports-drag-drop', !! dragdrop );
    281 
    282 			if ( ! dragdrop ) {
    283 				return dropzone.unbind('.wp-uploader');
    284 			}
    285 
    286 			// 'dragenter' doesn't fire correctly, simulate it with a limited 'dragover'.
    287 			dropzone.on( 'dragover.wp-uploader', function() {
    288 				if ( timer ) {
    289 					clearTimeout( timer );
    290 				}
    291 
    292 				if ( active ) {
    293 					return;
    294 				}
    295 
    296 				dropzone.trigger('dropzone:enter').addClass('drag-over');
    297 				active = true;
    298 			});
    299 
    300 			dropzone.on('dragleave.wp-uploader, drop.wp-uploader', function() {
    301 				/*
    302 				 * Using an instant timer prevents the drag-over class
    303 				 * from being quickly removed and re-added when elements
    304 				 * inside the dropzone are repositioned.
    305 				 *
    306 				 * @see https://core.trac.wordpress.org/ticket/21705
    307 				 */
    308 				timer = setTimeout( function() {
    309 					active = false;
    310 					dropzone.trigger('dropzone:leave').removeClass('drag-over');
    311 				}, 0 );
    312 			});
    313 
    314 			self.ready = true;
    315 			$(self).trigger( 'uploader:ready' );
    316 		});
    317 
    318 		this.uploader.bind( 'postinit', function( up ) {
    319 			up.refresh();
    320 			self.init();
    321 		});
    322 
    323 		this.uploader.init();
    324 
    325 		if ( this.browser ) {
    326 			this.browser.on( 'mouseenter', this.refresh );
    327 		} else {
    328 			this.uploader.disableBrowse( true );
    329 		}
    330 
    331 		$( self ).on( 'uploader:ready', function() {
    332 			$( '.moxie-shim-html5 input[type="file"]' )
    333 				.attr( {
    334 					tabIndex:      '-1',
    335 					'aria-hidden': 'true'
    336 				} );
    337 		} );
    338 
    339 		/**
    340 		 * After files were filtered and added to the queue, create a model for each.
    341 		 *
    342 		 * @param {plupload.Uploader} up    Uploader instance.
    343 		 * @param {Array}             files Array of file objects that were added to queue by the user.
    344 		 */
    345 		this.uploader.bind( 'FilesAdded', function( up, files ) {
    346 			_.each( files, function( file ) {
    347 				var attributes, image;
    348 
    349 				// Ignore failed uploads.
    350 				if ( plupload.FAILED === file.status ) {
    351 					return;
    352 				}
    353 
    354 				if ( file.type === 'image/heic' && up.settings.heic_upload_error ) {
    355 					// Show error but do not block uploading.
    356 					Uploader.errors.unshift({
    357 						message: pluploadL10n.unsupported_image,
    358 						data:    {},
    359 						file:    file
    360 					});
    361 				} else if ( file.type === 'image/webp' && up.settings.webp_upload_error ) {
    362 					// Disallow uploading of WebP images if the server cannot edit them.
    363 					error( pluploadL10n.noneditable_image, {}, file, 'no-retry' );
    364 					up.removeFile( file );
    365 					return;
    366 				}
    367 
    368 				// Generate attributes for a new `Attachment` model.
    369 				attributes = _.extend({
    370 					file:      file,
    371 					uploading: true,
    372 					date:      new Date(),
    373 					filename:  file.name,
    374 					menuOrder: 0,
    375 					uploadedTo: wp.media.model.settings.post.id
    376 				}, _.pick( file, 'loaded', 'size', 'percent' ) );
    377 
    378 				// Handle early mime type scanning for images.
    379 				image = /(?:jpe?g|png|gif)$/i.exec( file.name );
    380 
    381 				// For images set the model's type and subtype attributes.
    382 				if ( image ) {
    383 					attributes.type = 'image';
    384 
    385 					// `jpeg`, `png` and `gif` are valid subtypes.
    386 					// `jpg` is not, so map it to `jpeg`.
    387 					attributes.subtype = ( 'jpg' === image[0] ) ? 'jpeg' : image[0];
    388 				}
    389 
    390 				// Create a model for the attachment, and add it to the Upload queue collection
    391 				// so listeners to the upload queue can track and display upload progress.
    392 				file.attachment = wp.media.model.Attachment.create( attributes );
    393 				Uploader.queue.add( file.attachment );
    394 
    395 				self.added( file.attachment );
    396 			});
    397 
    398 			up.refresh();
    399 			up.start();
    400 		});
    401 
    402 		this.uploader.bind( 'UploadProgress', function( up, file ) {
    403 			file.attachment.set( _.pick( file, 'loaded', 'percent' ) );
    404 			self.progress( file.attachment );
    405 		});
    406 
    407 		/**
    408 		 * After a file is successfully uploaded, update its model.
    409 		 *
    410 		 * @param {plupload.Uploader} up       Uploader instance.
    411 		 * @param {plupload.File}     file     File that was uploaded.
    412 		 * @param {Object}            response Object with response properties.
    413 		 * @return {mixed}
    414 		 */
    415 		this.uploader.bind( 'FileUploaded', function( up, file, response ) {
    416 
    417 			try {
    418 				response = JSON.parse( response.response );
    419 			} catch ( e ) {
    420 				return error( pluploadL10n.default_error, e, file );
    421 			}
    422 
    423 			if ( ! _.isObject( response ) || _.isUndefined( response.success ) ) {
    424 				return error( pluploadL10n.default_error, null, file );
    425 			} else if ( ! response.success ) {
    426 				return error( response.data && response.data.message, response.data, file );
    427 			}
    428 
    429 			// Success. Update the UI with the new attachment.
    430 			fileUploaded( up, file, response );
    431 		});
    432 
    433 		/**
    434 		 * When plupload surfaces an error, send it to the error handler.
    435 		 *
    436 		 * @param {plupload.Uploader} up            Uploader instance.
    437 		 * @param {Object}            pluploadError Contains code, message and sometimes file and other details.
    438 		 */
    439 		this.uploader.bind( 'Error', function( up, pluploadError ) {
    440 			var message = pluploadL10n.default_error,
    441 				key;
    442 
    443 			// Check for plupload errors.
    444 			for ( key in Uploader.errorMap ) {
    445 				if ( pluploadError.code === plupload[ key ] ) {
    446 					message = Uploader.errorMap[ key ];
    447 
    448 					if ( typeof message === 'function' ) {
    449 						message = message( pluploadError.file, pluploadError );
    450 					}
    451 
    452 					break;
    453 				}
    454 			}
    455 
    456 			error( message, pluploadError, pluploadError.file );
    457 			up.refresh();
    458 		});
    459 
    460 	};
    461 
    462 	// Adds the 'defaults' and 'browser' properties.
    463 	$.extend( Uploader, _wpPluploadSettings );
    464 
    465 	Uploader.uuid = 0;
    466 
    467 	// Map Plupload error codes to user friendly error messages.
    468 	Uploader.errorMap = {
    469 		'FAILED':                 pluploadL10n.upload_failed,
    470 		'FILE_EXTENSION_ERROR':   pluploadL10n.invalid_filetype,
    471 		'IMAGE_FORMAT_ERROR':     pluploadL10n.not_an_image,
    472 		'IMAGE_MEMORY_ERROR':     pluploadL10n.image_memory_exceeded,
    473 		'IMAGE_DIMENSIONS_ERROR': pluploadL10n.image_dimensions_exceeded,
    474 		'GENERIC_ERROR':          pluploadL10n.upload_failed,
    475 		'IO_ERROR':               pluploadL10n.io_error,
    476 		'SECURITY_ERROR':         pluploadL10n.security_error,
    477 
    478 		'FILE_SIZE_ERROR': function( file ) {
    479 			return pluploadL10n.file_exceeds_size_limit.replace( '%s', file.name );
    480 		},
    481 
    482 		'HTTP_ERROR': function( file ) {
    483 			if ( file.type && file.type.indexOf( 'image/' ) === 0 ) {
    484 				return pluploadL10n.http_error_image;
    485 			}
    486 
    487 			return pluploadL10n.http_error;
    488 		},
    489 	};
    490 
    491 	$.extend( Uploader.prototype, /** @lends wp.Uploader.prototype */{
    492 		/**
    493 		 * Acts as a shortcut to extending the uploader's multipart_params object.
    494 		 *
    495 		 * param( key )
    496 		 *    Returns the value of the key.
    497 		 *
    498 		 * param( key, value )
    499 		 *    Sets the value of a key.
    500 		 *
    501 		 * param( map )
    502 		 *    Sets values for a map of data.
    503 		 */
    504 		param: function( key, value ) {
    505 			if ( arguments.length === 1 && typeof key === 'string' ) {
    506 				return this.uploader.settings.multipart_params[ key ];
    507 			}
    508 
    509 			if ( arguments.length > 1 ) {
    510 				this.uploader.settings.multipart_params[ key ] = value;
    511 			} else {
    512 				$.extend( this.uploader.settings.multipart_params, key );
    513 			}
    514 		},
    515 
    516 		/**
    517 		 * Make a few internal event callbacks available on the wp.Uploader object
    518 		 * to change the Uploader internals if absolutely necessary.
    519 		 */
    520 		init:     function() {},
    521 		error:    function() {},
    522 		success:  function() {},
    523 		added:    function() {},
    524 		progress: function() {},
    525 		complete: function() {},
    526 		refresh:  function() {
    527 			var node, attached, container, id;
    528 
    529 			if ( this.browser ) {
    530 				node = this.browser[0];
    531 
    532 				// Check if the browser node is in the DOM.
    533 				while ( node ) {
    534 					if ( node === document.body ) {
    535 						attached = true;
    536 						break;
    537 					}
    538 					node = node.parentNode;
    539 				}
    540 
    541 				/*
    542 				 * If the browser node is not attached to the DOM,
    543 				 * use a temporary container to house it, as the browser button shims
    544 				 * require the button to exist in the DOM at all times.
    545 				 */
    546 				if ( ! attached ) {
    547 					id = 'wp-uploader-browser-' + this.uploader.id;
    548 
    549 					container = $( '#' + id );
    550 					if ( ! container.length ) {
    551 						container = $('<div class="wp-uploader-browser" />').css({
    552 							position: 'fixed',
    553 							top: '-1000px',
    554 							left: '-1000px',
    555 							height: 0,
    556 							width: 0
    557 						}).attr( 'id', 'wp-uploader-browser-' + this.uploader.id ).appendTo('body');
    558 					}
    559 
    560 					container.append( this.browser );
    561 				}
    562 			}
    563 
    564 			this.uploader.refresh();
    565 		}
    566 	});
    567 
    568 	// Create a collection of attachments in the upload queue,
    569 	// so that other modules can track and display upload progress.
    570 	Uploader.queue = new wp.media.model.Attachments( [], { query: false });
    571 
    572 	// Create a collection to collect errors incurred while attempting upload.
    573 	Uploader.errors = new Backbone.Collection();
    574 
    575 	exports.Uploader = Uploader;
    576 })( wp, jQuery );