ckeditor5-image-upload-base64
Version:
The development environment of CKEditor 5 – the best browser-based rich text editor.
292 lines (246 loc) • 6.75 kB
JavaScript
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module cloud-services-core/uploadgateway/fileuploader
*/
/* globals XMLHttpRequest, FormData, Blob, atob */
import mix from '@ckeditor/ckeditor5-utils/src/mix';
import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
const BASE64_HEADER_REG_EXP = /^data:(\S*?);base64,/;
/**
* FileUploader class used to upload single file.
*/
export default class FileUploader {
/**
* Creates `FileUploader` instance.
*
* @param {Blob|String} fileOrData A blob object or a data string encoded with Base64.
* @param {module:cloud-services-core/token~Token} token Token used for authentication.
* @param {String} apiAddress API address.
*/
constructor( fileOrData, token, apiAddress ) {
if ( !fileOrData ) {
/**
* File must be provided as the first argument.
*
* @error fileuploader-missing-file
*/
throw new CKEditorError( 'fileuploader-missing-file: File must be provided as the first argument', null );
}
if ( !token ) {
/**
* Token must be provided as the second argument.
*
* @error fileuploader-missing-token
*/
throw new CKEditorError( 'fileuploader-missing-token: Token must be provided as the second argument.', null );
}
if ( !apiAddress ) {
/**
* Api address must be provided as the third argument.
*
* @error fileuploader-missing-api-address
*/
throw new CKEditorError( 'fileuploader-missing-api-address: Api address must be provided as the third argument.', null );
}
/**
* A file that is being uploaded.
*
* @type {Blob}
*/
this.file = _isBase64( fileOrData ) ? _base64ToBlob( fileOrData ) : fileOrData;
/**
* CKEditor Cloud Services access token.
*
* @type {module:cloud-services-core/token~Token}
* @private
*/
this._token = token;
/**
* CKEditor Cloud Services API address.
*
* @type {String}
* @private
*/
this._apiAddress = apiAddress;
}
/**
* Registers callback on `progress` event.
*
* @chainable
* @param {Function} callback
* @returns {module:cloud-services-core/uploadgateway/fileuploader~FileUploader}
*/
onProgress( callback ) {
this.on( 'progress', ( event, data ) => callback( data ) );
return this;
}
/**
* Registers callback on `error` event. Event is called once when error occurs.
*
* @chainable
* @param {Function} callback
* @returns {module:cloud-services-core/uploadgateway/fileuploader~FileUploader}
*/
onError( callback ) {
this.once( 'error', ( event, data ) => callback( data ) );
return this;
}
/**
* Aborts upload process.
*/
abort() {
this.xhr.abort();
}
/**
* Sends XHR request to API.
*
* @chainable
* @returns {Promise.<Object>}
*/
send() {
this._prepareRequest();
this._attachXHRListeners();
return this._sendRequest();
}
/**
* Prepares XHR request.
*
* @private
*/
_prepareRequest() {
const xhr = new XMLHttpRequest();
xhr.open( 'POST', this._apiAddress );
xhr.setRequestHeader( 'Authorization', this._token.value );
xhr.responseType = 'json';
this.xhr = xhr;
}
/**
* Attaches listeners to the XHR.
*
* @private
*/
_attachXHRListeners() {
const that = this;
const xhr = this.xhr;
xhr.addEventListener( 'error', onError( 'Network Error' ) );
xhr.addEventListener( 'abort', onError( 'Abort' ) );
/* istanbul ignore else */
if ( xhr.upload ) {
xhr.upload.addEventListener( 'progress', event => {
if ( event.lengthComputable ) {
this.fire( 'progress', {
total: event.total,
uploaded: event.loaded
} );
}
} );
}
xhr.addEventListener( 'load', () => {
const statusCode = xhr.status;
const xhrResponse = xhr.response;
if ( statusCode < 200 || statusCode > 299 ) {
return this.fire( 'error', xhrResponse.message || xhrResponse.error );
}
} );
function onError( message ) {
return () => that.fire( 'error', message );
}
}
/**
* Sends XHR request.
*
* @private
*/
_sendRequest() {
const formData = new FormData();
const xhr = this.xhr;
formData.append( 'file', this.file );
return new Promise( ( resolve, reject ) => {
xhr.addEventListener( 'load', () => {
const statusCode = xhr.status;
const xhrResponse = xhr.response;
if ( statusCode < 200 || statusCode > 299 ) {
if ( xhrResponse.message ) {
/**
* Uploading file failed.
*
* @error fileuploader-uploading-data-failed
*/
return reject( new CKEditorError(
'fileuploader-uploading-data-failed: Uploading file failed.',
this,
{ message: xhrResponse.message }
) );
}
return reject( xhrResponse.error );
}
return resolve( xhrResponse );
} );
xhr.addEventListener( 'error', () => reject( new Error( 'Network Error' ) ) );
xhr.addEventListener( 'abort', () => reject( new Error( 'Abort' ) ) );
xhr.send( formData );
} );
}
/**
* Fired when error occurs.
*
* @event error
* @param {String} error Error message
*/
/**
* Fired on upload progress.
*
* @event progress
* @param {Object} status Total and uploaded status
*/
}
mix( FileUploader, EmitterMixin );
/**
* Transforms Base64 string data into file.
*
* @param {String} base64 String data.
* @param {Number} [sliceSize=512]
* @returns {Blob}
* @private
*/
function _base64ToBlob( base64, sliceSize = 512 ) {
try {
const contentType = base64.match( BASE64_HEADER_REG_EXP )[ 1 ];
const base64Data = atob( base64.replace( BASE64_HEADER_REG_EXP, '' ) );
const byteArrays = [];
for ( let offset = 0; offset < base64Data.length; offset += sliceSize ) {
const slice = base64Data.slice( offset, offset + sliceSize );
const byteNumbers = new Array( slice.length );
for ( let i = 0; i < slice.length; i++ ) {
byteNumbers[ i ] = slice.charCodeAt( i );
}
byteArrays.push( new Uint8Array( byteNumbers ) );
}
return new Blob( byteArrays, { type: contentType } );
} catch ( error ) {
/**
* Problem with decoding Base64 image data.
*
* @error fileuploader-decoding-image-data-error
*/
throw new CKEditorError( 'fileuploader-decoding-image-data-error: Problem with decoding Base64 image data.', null );
}
}
/**
* Checks that string is Base64.
*
* @param {String} string
* @returns {Boolean}
* @private
*/
function _isBase64( string ) {
if ( typeof string !== 'string' ) {
return false;
}
const match = string.match( BASE64_HEADER_REG_EXP );
return !!( match && match.length );
}