fileuploader.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module cloud-services-core/uploadgateway
  7. */
  8. /* globals XMLHttpRequest, FormData, Blob, atob */
  9. import mix from '@ckeditor/ckeditor5-utils/src/mix';
  10. import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
  11. import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  12. const BASE64_HEADER_REG_EXP = /^data:(\S*?);base64,/;
  13. /**
  14. * FileUploader class used to upload single file.
  15. */
  16. export default class FileUploader {
  17. /**
  18. * Creates `FileUploader` instance.
  19. *
  20. * @param {Blob|String} fileOrData A blob object or a data string encoded with Base64.
  21. * @param {module:cloud-services-core/token~Token} token Token used for authentication.
  22. * @param {String} apiAddress API address.
  23. */
  24. constructor( fileOrData, token, apiAddress ) {
  25. if ( !fileOrData ) {
  26. /**
  27. * File must be provided as the first argument.
  28. *
  29. * @error fileuploader-missing-file
  30. */
  31. throw new CKEditorError( 'fileuploader-missing-file: File must be provided as the first argument', null );
  32. }
  33. if ( !token ) {
  34. /**
  35. * Token must be provided as the second argument.
  36. *
  37. * @error fileuploader-missing-token
  38. */
  39. throw new CKEditorError( 'fileuploader-missing-token: Token must be provided as the second argument.', null );
  40. }
  41. if ( !apiAddress ) {
  42. /**
  43. * Api address must be provided as the third argument.
  44. *
  45. * @error fileuploader-missing-api-address
  46. */
  47. throw new CKEditorError( 'fileuploader-missing-api-address: Api address must be provided as the third argument.', null );
  48. }
  49. /**
  50. * A file that is being uploaded.
  51. *
  52. * @type {Blob}
  53. */
  54. this.file = _isBase64( fileOrData ) ? _base64ToBlob( fileOrData ) : fileOrData;
  55. /**
  56. * CKEditor Cloud Services access token.
  57. *
  58. * @type {module:cloud-services-core/token~Token}
  59. * @private
  60. */
  61. this._token = token;
  62. /**
  63. * CKEditor Cloud Services API address.
  64. *
  65. * @type {String}
  66. * @private
  67. */
  68. this._apiAddress = apiAddress;
  69. }
  70. /**
  71. * Registers callback on `progress` event.
  72. *
  73. * @chainable
  74. * @param {Function} callback
  75. * @returns {module:cloud-services-core/uploadgateway~FileUploader}
  76. */
  77. onProgress( callback ) {
  78. this.on( 'progress', ( event, data ) => callback( data ) );
  79. return this;
  80. }
  81. /**
  82. * Registers callback on `error` event. Event is called once when error occurs.
  83. *
  84. * @chainable
  85. * @param {Function} callback
  86. * @returns {module:cloud-services-core/uploadgateway~FileUploader}
  87. */
  88. onError( callback ) {
  89. this.once( 'error', ( event, data ) => callback( data ) );
  90. return this;
  91. }
  92. /**
  93. * Aborts upload process.
  94. */
  95. abort() {
  96. this.xhr.abort();
  97. }
  98. /**
  99. * Sends XHR request to API.
  100. *
  101. * @chainable
  102. * @returns {Promise.<Object>}
  103. */
  104. send() {
  105. this._prepareRequest();
  106. this._attachXHRListeners();
  107. return this._sendRequest();
  108. }
  109. /**
  110. * Prepares XHR request.
  111. *
  112. * @private
  113. */
  114. _prepareRequest() {
  115. const xhr = new XMLHttpRequest();
  116. xhr.open( 'POST', this._apiAddress );
  117. xhr.setRequestHeader( 'Authorization', this._token.value );
  118. xhr.responseType = 'json';
  119. this.xhr = xhr;
  120. }
  121. /**
  122. * Attaches listeners to the XHR.
  123. *
  124. * @private
  125. */
  126. _attachXHRListeners() {
  127. const that = this;
  128. const xhr = this.xhr;
  129. xhr.addEventListener( 'error', onError( 'Network Error' ) );
  130. xhr.addEventListener( 'abort', onError( 'Abort' ) );
  131. /* istanbul ignore else */
  132. if ( xhr.upload ) {
  133. xhr.upload.addEventListener( 'progress', event => {
  134. if ( event.lengthComputable ) {
  135. this.fire( 'progress', {
  136. total: event.total,
  137. uploaded: event.loaded
  138. } );
  139. }
  140. } );
  141. }
  142. xhr.addEventListener( 'load', () => {
  143. const statusCode = xhr.status;
  144. const xhrResponse = xhr.response;
  145. if ( statusCode < 200 || statusCode > 299 ) {
  146. return this.fire( 'error', xhrResponse.message || xhrResponse.error );
  147. }
  148. } );
  149. function onError( message ) {
  150. return () => that.fire( 'error', message );
  151. }
  152. }
  153. /**
  154. * Sends XHR request.
  155. *
  156. * @private
  157. */
  158. _sendRequest() {
  159. const formData = new FormData();
  160. const xhr = this.xhr;
  161. formData.append( 'file', this.file );
  162. return new Promise( ( resolve, reject ) => {
  163. xhr.addEventListener( 'load', () => {
  164. const statusCode = xhr.status;
  165. const xhrResponse = xhr.response;
  166. if ( statusCode < 200 || statusCode > 299 ) {
  167. if ( xhrResponse.message ) {
  168. /**
  169. * Uploading file failed.
  170. *
  171. * @error fileuploader-uploading-data-failed
  172. */
  173. return reject( new CKEditorError(
  174. 'fileuploader-uploading-data-failed: Uploading file failed.',
  175. this,
  176. { message: xhrResponse.message }
  177. ) );
  178. }
  179. return reject( xhrResponse.error );
  180. }
  181. return resolve( xhrResponse );
  182. } );
  183. xhr.addEventListener( 'error', () => reject( new Error( 'Network Error' ) ) );
  184. xhr.addEventListener( 'abort', () => reject( new Error( 'Abort' ) ) );
  185. xhr.send( formData );
  186. } );
  187. }
  188. /**
  189. * Fired when error occurs.
  190. *
  191. * @event error
  192. * @param {String} error Error message
  193. */
  194. /**
  195. * Fired on upload progress.
  196. *
  197. * @event progress
  198. * @param {Object} status Total and uploaded status
  199. */
  200. }
  201. mix( FileUploader, EmitterMixin );
  202. /**
  203. * Transforms Base64 string data into file.
  204. *
  205. * @param {String} base64 String data.
  206. * @param {Number} [sliceSize=512]
  207. * @returns {Blob}
  208. * @private
  209. */
  210. function _base64ToBlob( base64, sliceSize = 512 ) {
  211. try {
  212. const contentType = base64.match( BASE64_HEADER_REG_EXP )[ 1 ];
  213. const base64Data = atob( base64.replace( BASE64_HEADER_REG_EXP, '' ) );
  214. const byteArrays = [];
  215. for ( let offset = 0; offset < base64Data.length; offset += sliceSize ) {
  216. const slice = base64Data.slice( offset, offset + sliceSize );
  217. const byteNumbers = new Array( slice.length );
  218. for ( let i = 0; i < slice.length; i++ ) {
  219. byteNumbers[ i ] = slice.charCodeAt( i );
  220. }
  221. byteArrays.push( new Uint8Array( byteNumbers ) );
  222. }
  223. return new Blob( byteArrays, { type: contentType } );
  224. } catch ( error ) {
  225. /**
  226. * Problem with decoding Base64 image data.
  227. *
  228. * @error fileuploader-decoding-image-data-error
  229. */
  230. throw new CKEditorError( 'fileuploader-decoding-image-data-error: Problem with decoding Base64 image data.', null );
  231. }
  232. }
  233. /**
  234. * Checks that string is Base64.
  235. *
  236. * @param {String} string
  237. * @returns {Boolean}
  238. * @private
  239. */
  240. function _isBase64( string ) {
  241. if ( typeof string !== 'string' ) {
  242. return false;
  243. }
  244. const match = string.match( BASE64_HEADER_REG_EXP );
  245. return !!( match && match.length );
  246. }