wp-plupload.js 16 KB

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