wp-plupload.js 15 KB

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