@ckeditor/ckeditor5-cloud-services
Advanced tools
Comparing version 36.0.1 to 37.0.0-alpha.0
/*! | ||
* @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/(()=>{var e={704:(e,t,r)=>{e.exports=r(79)("./src/core.js")},209:(e,t,r)=>{e.exports=r(79)("./src/utils.js")},79:e=>{"use strict";e.exports=CKEditor5.dll}},t={};function r(s){var o=t[s];if(void 0!==o)return o.exports;var n=t[s]={exports:{}};return e[s](n,n.exports,r),n.exports}r.d=(e,t)=>{for(var s in t)r.o(t,s)&&!r.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var s={};(()=>{"use strict";r.r(s),r.d(s,{CloudServices:()=>c,CloudServicesCore:()=>d});var e=r(704),t=r(209);const o={autoRefresh:!0},n=36e5;class i{constructor(e,r=o){if(!e)throw new t.CKEditorError("token-missing-token-url",this);r.initValue&&this._validateTokenValue(r.initValue),this.set("value",r.initValue),this._refresh="function"==typeof e?e:()=>{return r=e,new Promise(((e,s)=>{const o=new XMLHttpRequest;o.open("GET",r),o.addEventListener("load",(()=>{const r=o.status,n=o.response;return r<200||r>299?s(new t.CKEditorError("token-cannot-download-new-token",null)):e(n)})),o.addEventListener("error",(()=>s(new Error("Network Error")))),o.addEventListener("abort",(()=>s(new Error("Abort")))),o.send()}));var r},this._options=Object.assign({},o,r)}init(){return new Promise(((e,t)=>{this.value?(this._options.autoRefresh&&this._registerRefreshTokenTimeout(),e(this)):this.refreshToken().then(e).catch(t)}))}refreshToken(){return this._refresh().then((e=>{this._validateTokenValue(e),this.set("value",e),this._options.autoRefresh&&this._registerRefreshTokenTimeout()})).then((()=>this))}destroy(){clearTimeout(this._tokenRefreshTimeout)}_validateTokenValue(e){const r="string"==typeof e,s=!/^".*"$/.test(e),o=r&&3===e.split(".").length;if(!s||!o)throw new t.CKEditorError("token-not-in-jwt-format",this)}_registerRefreshTokenTimeout(){const e=this._getTokenRefreshTimeoutTime();clearTimeout(this._tokenRefreshTimeout),this._tokenRefreshTimeout=setTimeout((()=>{this.refreshToken()}),e)}_getTokenRefreshTimeoutTime(){try{const[,e]=this.value.split("."),{exp:t}=JSON.parse(atob(e));if(!t)return n;return Math.floor((1e3*t-Date.now())/2)}catch(e){return n}}static create(e,t=o){return new i(e,t).init()}}(0,t.mix)(i,t.ObservableMixin);const a=i,u=/^data:(\S*?);base64,/;class h{constructor(e,r,s){if(!e)throw new t.CKEditorError("fileuploader-missing-file",null);if(!r)throw new t.CKEditorError("fileuploader-missing-token",null);if(!s)throw new t.CKEditorError("fileuploader-missing-api-address",null);this.file=function(e){if("string"!=typeof e)return!1;const t=e.match(u);return!(!t||!t.length)}(e)?function(e,r=512){try{const t=e.match(u)[1],s=atob(e.replace(u,"")),o=[];for(let e=0;e<s.length;e+=r){const t=s.slice(e,e+r),n=new Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);o.push(new Uint8Array(n))}return new Blob(o,{type:t})}catch(e){throw new t.CKEditorError("fileuploader-decoding-image-data-error",null)}}(e):e,this._token=r,this._apiAddress=s}onProgress(e){return this.on("progress",((t,r)=>e(r))),this}onError(e){return this.once("error",((t,r)=>e(r))),this}abort(){this.xhr.abort()}send(){return this._prepareRequest(),this._attachXHRListeners(),this._sendRequest()}_prepareRequest(){const e=new XMLHttpRequest;e.open("POST",this._apiAddress),e.setRequestHeader("Authorization",this._token.value),e.responseType="json",this.xhr=e}_attachXHRListeners(){const e=this,t=this.xhr;function r(t){return()=>e.fire("error",t)}t.addEventListener("error",r("Network Error")),t.addEventListener("abort",r("Abort")),t.upload&&t.upload.addEventListener("progress",(e=>{e.lengthComputable&&this.fire("progress",{total:e.total,uploaded:e.loaded})})),t.addEventListener("load",(()=>{const e=t.status,r=t.response;if(e<200||e>299)return this.fire("error",r.message||r.error)}))}_sendRequest(){const e=new FormData,r=this.xhr;return e.append("file",this.file),new Promise(((s,o)=>{r.addEventListener("load",(()=>{const e=r.status,n=r.response;return e<200||e>299?n.message?o(new t.CKEditorError("fileuploader-uploading-data-failed",this,{message:n.message})):o(n.error):s(n)})),r.addEventListener("error",(()=>o(new Error("Network Error")))),r.addEventListener("abort",(()=>o(new Error("Abort")))),r.send(e)}))}}(0,t.mix)(h,t.EmitterMixin);class l{constructor(e,r){if(!e)throw new t.CKEditorError("uploadgateway-missing-token",null);if(!r)throw new t.CKEditorError("uploadgateway-missing-api-address",null);this._token=e,this._apiAddress=r}upload(e){return new h(e,this._token,this._apiAddress)}}class d extends e.ContextPlugin{static get pluginName(){return"CloudServicesCore"}createToken(e,t){return new a(e,t)}createUploadGateway(e,t){return new l(e,t)}}class c extends e.ContextPlugin{static get pluginName(){return"CloudServices"}static get requires(){return[d]}init(){const e=this.context.config.get("cloudServices")||{};for(const t in e)this[t]=e[t];if(this._tokens=new Map,this.tokenUrl)return this.token=this.context.plugins.get("CloudServicesCore").createToken(this.tokenUrl),this._tokens.set(this.tokenUrl,this.token),this.token.init();this.token=null}registerTokenUrl(e){if(this._tokens.has(e))return Promise.resolve(this.getTokenFor(e));const t=this.context.plugins.get("CloudServicesCore").createToken(e);return this._tokens.set(e,t),t.init()}getTokenFor(e){const r=this._tokens.get(e);if(!r)throw new t.CKEditorError("cloudservices-token-not-registered",this);return r}destroy(){super.destroy();for(const e of this._tokens.values())e.destroy()}}})(),(window.CKEditor5=window.CKEditor5||{}).cloudServices=s})(); | ||
*/(()=>{var e={704:(e,t,r)=>{e.exports=r(79)("./src/core.js")},209:(e,t,r)=>{e.exports=r(79)("./src/utils.js")},79:e=>{"use strict";e.exports=CKEditor5.dll}},t={};function r(s){var o=t[s];if(void 0!==o)return o.exports;var n=t[s]={exports:{}};return e[s](n,n.exports,r),n.exports}r.d=(e,t)=>{for(var s in t)r.o(t,s)&&!r.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var s={};(()=>{"use strict";r.r(s),r.d(s,{CloudServices:()=>d,CloudServicesCore:()=>h});var e=r(704),t=r(209);const o={autoRefresh:!0},n=36e5;class i extends((0,t.ObservableMixin)()){constructor(e,r={}){if(super(),!e)throw new t.CKEditorError("token-missing-token-url",this);r.initValue&&this._validateTokenValue(r.initValue),this.set("value",r.initValue),this._refresh="function"==typeof e?e:()=>{return r=e,new Promise(((e,s)=>{const o=new XMLHttpRequest;o.open("GET",r),o.addEventListener("load",(()=>{const r=o.status,n=o.response;return r<200||r>299?s(new t.CKEditorError("token-cannot-download-new-token",null)):e(n)})),o.addEventListener("error",(()=>s(new Error("Network Error")))),o.addEventListener("abort",(()=>s(new Error("Abort")))),o.send()}));var r},this._options={...o,...r}}init(){return new Promise(((e,t)=>{this.value?(this._options.autoRefresh&&this._registerRefreshTokenTimeout(),e(this)):this.refreshToken().then(e).catch(t)}))}refreshToken(){return this._refresh().then((e=>(this._validateTokenValue(e),this.set("value",e),this._options.autoRefresh&&this._registerRefreshTokenTimeout(),this)))}destroy(){clearTimeout(this._tokenRefreshTimeout)}_validateTokenValue(e){const r="string"==typeof e,s=!/^".*"$/.test(e),o=r&&3===e.split(".").length;if(!s||!o)throw new t.CKEditorError("token-not-in-jwt-format",this)}_registerRefreshTokenTimeout(){const e=this._getTokenRefreshTimeoutTime();clearTimeout(this._tokenRefreshTimeout),this._tokenRefreshTimeout=setTimeout((()=>{this.refreshToken()}),e)}_getTokenRefreshTimeoutTime(){try{const[,e]=this.value.split("."),{exp:t}=JSON.parse(atob(e));if(!t)return n;return Math.floor((1e3*t-Date.now())/2)}catch(e){return n}}static create(e,t={}){return new i(e,t).init()}}const a=/^data:(\S*?);base64,/;class u extends((0,t.EmitterMixin)()){constructor(e,r,s){if(super(),!e)throw new t.CKEditorError("fileuploader-missing-file",null);if(!r)throw new t.CKEditorError("fileuploader-missing-token",null);if(!s)throw new t.CKEditorError("fileuploader-missing-api-address",null);this.file=function(e){if("string"!=typeof e)return!1;const t=e.match(a);return!(!t||!t.length)}(e)?function(e,r=512){try{const t=e.match(a)[1],s=atob(e.replace(a,"")),o=[];for(let e=0;e<s.length;e+=r){const t=s.slice(e,e+r),n=new Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);o.push(new Uint8Array(n))}return new Blob(o,{type:t})}catch(e){throw new t.CKEditorError("fileuploader-decoding-image-data-error",null)}}(e):e,this._token=r,this._apiAddress=s}onProgress(e){return this.on("progress",((t,r)=>e(r))),this}onError(e){return this.once("error",((t,r)=>e(r))),this}abort(){this.xhr.abort()}send(){return this._prepareRequest(),this._attachXHRListeners(),this._sendRequest()}_prepareRequest(){const e=new XMLHttpRequest;e.open("POST",this._apiAddress),e.setRequestHeader("Authorization",this._token.value),e.responseType="json",this.xhr=e}_attachXHRListeners(){const e=this.xhr,t=e=>()=>this.fire("error",e);e.addEventListener("error",t("Network Error")),e.addEventListener("abort",t("Abort")),e.upload&&e.upload.addEventListener("progress",(e=>{e.lengthComputable&&this.fire("progress",{total:e.total,uploaded:e.loaded})})),e.addEventListener("load",(()=>{const t=e.status,r=e.response;if(t<200||t>299)return this.fire("error",r.message||r.error)}))}_sendRequest(){const e=new FormData,r=this.xhr;return e.append("file",this.file),new Promise(((s,o)=>{r.addEventListener("load",(()=>{const e=r.status,n=r.response;return e<200||e>299?n.message?o(new t.CKEditorError("fileuploader-uploading-data-failed",this,{message:n.message})):o(n.error):s(n)})),r.addEventListener("error",(()=>o(new Error("Network Error")))),r.addEventListener("abort",(()=>o(new Error("Abort")))),r.send(e)}))}}class l{constructor(e,r){if(!e)throw new t.CKEditorError("uploadgateway-missing-token",null);if(!r)throw new t.CKEditorError("uploadgateway-missing-api-address",null);this._token=e,this._apiAddress=r}upload(e){return new u(e,this._token,this._apiAddress)}}class h extends e.ContextPlugin{static get pluginName(){return"CloudServicesCore"}createToken(e,t){return new i(e,t)}createUploadGateway(e,t){return new l(e,t)}}class d extends e.ContextPlugin{constructor(){super(...arguments),this.token=null,this._tokens=new Map}static get pluginName(){return"CloudServices"}static get requires(){return[h]}async init(){const e=this.context.config.get("cloudServices")||{};for(const[t,r]of Object.entries(e))this[t]=r;if(!this.tokenUrl)return void(this.token=null);const t=this.context.plugins.get("CloudServicesCore");this.token=await t.createToken(this.tokenUrl).init(),this._tokens.set(this.tokenUrl,this.token)}async registerTokenUrl(e){if(this._tokens.has(e))return this.getTokenFor(e);const t=this.context.plugins.get("CloudServicesCore"),r=await t.createToken(e).init();return this._tokens.set(e,r),r}getTokenFor(e){const r=this._tokens.get(e);if(!r)throw new t.CKEditorError("cloudservices-token-not-registered",this);return r}destroy(){super.destroy();for(const e of this._tokens.values())e.destroy()}}})(),(window.CKEditor5=window.CKEditor5||{}).cloudServices=s})(); |
{ | ||
"name": "@ckeditor/ckeditor5-cloud-services", | ||
"version": "36.0.1", | ||
"version": "37.0.0-alpha.0", | ||
"description": "CKEditor 5's Cloud Services integration layer.", | ||
@@ -14,10 +14,11 @@ "keywords": [ | ||
"dependencies": { | ||
"ckeditor5": "^36.0.1" | ||
"ckeditor5": "^37.0.0-alpha.0" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-core": "^36.0.1", | ||
"@ckeditor/ckeditor5-dev-utils": "^32.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^36.0.1", | ||
"@ckeditor/ckeditor5-theme-lark": "^36.0.1", | ||
"@ckeditor/ckeditor5-utils": "^36.0.1", | ||
"@ckeditor/ckeditor5-core": "^37.0.0-alpha.0", | ||
"@ckeditor/ckeditor5-dev-utils": "^34.0.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^37.0.0-alpha.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^37.0.0-alpha.0", | ||
"@ckeditor/ckeditor5-utils": "^37.0.0-alpha.0", | ||
"typescript": "^4.8.4", | ||
"webpack": "^5.58.1", | ||
@@ -41,3 +42,4 @@ "webpack-cli": "^4.9.0" | ||
"lang", | ||
"src", | ||
"src/**/*.js", | ||
"src/**/*.d.ts", | ||
"theme", | ||
@@ -49,4 +51,7 @@ "build", | ||
"scripts": { | ||
"dll:build": "webpack" | ||
} | ||
"dll:build": "webpack", | ||
"build": "tsc -p ./tsconfig.release.json", | ||
"postversion": "npm run build" | ||
}, | ||
"types": "src/index.d.ts" | ||
} |
@@ -5,11 +5,8 @@ /** | ||
*/ | ||
/** | ||
* @module cloud-services/cloudservices | ||
*/ | ||
import { ContextPlugin } from 'ckeditor5/src/core'; | ||
import { CKEditorError } from 'ckeditor5/src/utils'; | ||
import CloudServicesCore from './cloudservicescore'; | ||
/** | ||
@@ -19,247 +16,90 @@ * Plugin introducing the integration between CKEditor 5 and CKEditor Cloud Services . | ||
* It initializes the token provider based on | ||
* the {@link module:cloud-services/cloudservices~CloudServicesConfig `config.cloudService`}. | ||
* | ||
* @extends module:core/contextplugin~ContextPlugin | ||
* the {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig `config.cloudService`}. | ||
*/ | ||
export default class CloudServices extends ContextPlugin { | ||
/** | ||
* @inheritdoc | ||
*/ | ||
static get pluginName() { | ||
return 'CloudServices'; | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
static get requires() { | ||
return [ CloudServicesCore ]; | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
init() { | ||
const config = this.context.config; | ||
const options = config.get( 'cloudServices' ) || {}; | ||
for ( const optionName in options ) { | ||
this[ optionName ] = options[ optionName ]; | ||
} | ||
/** | ||
* A map of token object instances keyed by the token URLs. | ||
* | ||
* @private | ||
* @type {Map.<String, module:cloud-services/token~Token>} | ||
*/ | ||
this._tokens = new Map(); | ||
/** | ||
* The authentication token URL for CKEditor Cloud Services or a callback to the token value promise. See the | ||
* {@link module:cloud-services/cloudservices~CloudServicesConfig#tokenUrl} for more details. | ||
* | ||
* @readonly | ||
* @member {String|Function|undefined} #tokenUrl | ||
*/ | ||
/** | ||
* The URL to which the files should be uploaded. | ||
* | ||
* @readonly | ||
* @member {String} #uploadUrl | ||
*/ | ||
/** | ||
* Other plugins use this token for the authorization process. It handles token requesting and refreshing. | ||
* Its value is `null` when {@link module:cloud-services/cloudservices~CloudServicesConfig#tokenUrl} is not provided. | ||
* | ||
* @readonly | ||
* @member {module:cloud-services/token~Token|null} #token | ||
*/ | ||
if ( !this.tokenUrl ) { | ||
this.token = null; | ||
return; | ||
} | ||
this.token = this.context.plugins.get( 'CloudServicesCore' ).createToken( this.tokenUrl ); | ||
this._tokens.set( this.tokenUrl, this.token ); | ||
return this.token.init(); | ||
} | ||
/** | ||
* Registers an additional authentication token URL for CKEditor Cloud Services or a callback to the token value promise. See the | ||
* {@link module:cloud-services/cloudservices~CloudServicesConfig#tokenUrl} for more details. | ||
* | ||
* @param {String|Function} tokenUrl The authentication token URL for CKEditor Cloud Services or a callback to the token value promise. | ||
* @returns {Promise.<module:cloud-services/token~Token>} | ||
*/ | ||
registerTokenUrl( tokenUrl ) { | ||
// Reuse the token instance in case of multiple features using the same token URL. | ||
if ( this._tokens.has( tokenUrl ) ) { | ||
return Promise.resolve( this.getTokenFor( tokenUrl ) ); | ||
} | ||
const token = this.context.plugins.get( 'CloudServicesCore' ).createToken( tokenUrl ); | ||
this._tokens.set( tokenUrl, token ); | ||
return token.init(); | ||
} | ||
/** | ||
* Returns an authentication token provider previously registered by {@link #registerTokenUrl}. | ||
* | ||
* @param {String|Function} tokenUrl The authentication token URL for CKEditor Cloud Services or a callback to the token value promise. | ||
* @returns {module:cloud-services/token~Token} | ||
*/ | ||
getTokenFor( tokenUrl ) { | ||
const token = this._tokens.get( tokenUrl ); | ||
if ( !token ) { | ||
/** | ||
* The provided `tokenUrl` was not registered by {@link module:cloud-services/cloudservices~CloudServices#registerTokenUrl}. | ||
* | ||
* @error cloudservices-token-not-registered | ||
*/ | ||
throw new CKEditorError( 'cloudservices-token-not-registered', this ); | ||
} | ||
return token; | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
destroy() { | ||
super.destroy(); | ||
for ( const token of this._tokens.values() ) { | ||
token.destroy(); | ||
} | ||
} | ||
constructor() { | ||
super(...arguments); | ||
/** | ||
* Other plugins use this token for the authorization process. It handles token requesting and refreshing. | ||
* Its value is `null` when {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl} is not provided. | ||
* | ||
* @readonly | ||
*/ | ||
this.token = null; | ||
/** | ||
* A map of token object instances keyed by the token URLs. | ||
*/ | ||
this._tokens = new Map(); | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
static get pluginName() { | ||
return 'CloudServices'; | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
static get requires() { | ||
return [CloudServicesCore]; | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
async init() { | ||
const config = this.context.config; | ||
const options = config.get('cloudServices') || {}; | ||
for (const [key, value] of Object.entries(options)) { | ||
this[key] = value; | ||
} | ||
if (!this.tokenUrl) { | ||
this.token = null; | ||
return; | ||
} | ||
const cloudServicesCore = this.context.plugins.get('CloudServicesCore'); | ||
this.token = await cloudServicesCore.createToken(this.tokenUrl).init(); | ||
this._tokens.set(this.tokenUrl, this.token); | ||
} | ||
/** | ||
* Registers an additional authentication token URL for CKEditor Cloud Services or a callback to the token value promise. See the | ||
* {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl} for more details. | ||
* | ||
* @param tokenUrl The authentication token URL for CKEditor Cloud Services or a callback to the token value promise. | ||
*/ | ||
async registerTokenUrl(tokenUrl) { | ||
// Reuse the token instance in case of multiple features using the same token URL. | ||
if (this._tokens.has(tokenUrl)) { | ||
return this.getTokenFor(tokenUrl); | ||
} | ||
const cloudServicesCore = this.context.plugins.get('CloudServicesCore'); | ||
const token = await cloudServicesCore.createToken(tokenUrl).init(); | ||
this._tokens.set(tokenUrl, token); | ||
return token; | ||
} | ||
/** | ||
* Returns an authentication token provider previously registered by {@link #registerTokenUrl}. | ||
* | ||
* @param tokenUrl The authentication token URL for CKEditor Cloud Services or a callback to the token value promise. | ||
*/ | ||
getTokenFor(tokenUrl) { | ||
const token = this._tokens.get(tokenUrl); | ||
if (!token) { | ||
/** | ||
* The provided `tokenUrl` was not registered by {@link module:cloud-services/cloudservices~CloudServices#registerTokenUrl}. | ||
* | ||
* @error cloudservices-token-not-registered | ||
*/ | ||
throw new CKEditorError('cloudservices-token-not-registered', this); | ||
} | ||
return token; | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
destroy() { | ||
super.destroy(); | ||
for (const token of this._tokens.values()) { | ||
token.destroy(); | ||
} | ||
} | ||
} | ||
/** | ||
* The configuration of CKEditor Cloud Services. Introduced by the {@link module:cloud-services/cloudservices~CloudServices} plugin. | ||
* | ||
* Read more in {@link module:cloud-services/cloudservices~CloudServicesConfig}. | ||
* | ||
* @member {module:cloud-services/cloudservices~CloudServicesConfig} module:core/editor/editorconfig~EditorConfig#cloudServices | ||
*/ | ||
/** | ||
* The configuration for all plugins using CKEditor Cloud Services. | ||
* | ||
* ClassicEditor | ||
* .create( document.querySelector( '#editor' ), { | ||
* cloudServices: { | ||
* tokenUrl: 'https://example.com/cs-token-endpoint', | ||
* uploadUrl: 'https://your-organization-id.cke-cs.com/easyimage/upload/' | ||
* } | ||
* } ) | ||
* .then( ... ) | ||
* .catch( ... ); | ||
* | ||
* See {@link module:core/editor/editorconfig~EditorConfig all editor options}. | ||
* | ||
* @interface CloudServicesConfig | ||
*/ | ||
/** | ||
* A token URL or a token request function. | ||
* | ||
* As a string, it should be a URL to the security token endpoint in your application. The role of this endpoint is to securely authorize | ||
* the end users of your application to use [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services) only | ||
* if they should have access e.g. to upload files with {@glink @cs guides/easy-image/quick-start Easy Image} or to use the | ||
* {@glink @cs guides/collaboration/quick-start Collaboration} service. | ||
* | ||
* ClassicEditor | ||
* .create( document.querySelector( '#editor' ), { | ||
* cloudServices: { | ||
* tokenUrl: 'https://example.com/cs-token-endpoint', | ||
* ... | ||
* } | ||
* } ) | ||
* .then( ... ) | ||
* .catch( ... ); | ||
* | ||
* As a function, it should provide a promise to the token value, so you can highly customize the token and provide your token URL endpoint. | ||
* By using this approach you can set your own headers for the request. | ||
* | ||
* ClassicEditor | ||
* .create( document.querySelector( '#editor' ), { | ||
* cloudServices: { | ||
* tokenUrl: () => new Promise( ( resolve, reject ) => { | ||
* const xhr = new XMLHttpRequest(); | ||
* | ||
* xhr.open( 'GET', 'https://example.com/cs-token-endpoint' ); | ||
* | ||
* xhr.addEventListener( 'load', () => { | ||
* const statusCode = xhr.status; | ||
* const xhrResponse = xhr.response; | ||
* | ||
* if ( statusCode < 200 || statusCode > 299 ) { | ||
* return reject( new Error( 'Cannot download new token!' ) ); | ||
* } | ||
* | ||
* return resolve( xhrResponse ); | ||
* } ); | ||
* | ||
* xhr.addEventListener( 'error', () => reject( new Error( 'Network Error' ) ) ); | ||
* xhr.addEventListener( 'abort', () => reject( new Error( 'Abort' ) ) ); | ||
* | ||
* xhr.setRequestHeader( customHeader, customValue ); | ||
* | ||
* xhr.send(); | ||
* } ), | ||
* ... | ||
* } | ||
* } ) | ||
* | ||
* You can find more information about token endpoints in the | ||
* {@glink @cs guides/easy-image/quick-start#create-token-endpoint Cloud Services - Quick start} | ||
* and {@glink @cs guides/security/token-endpoint Cloud Services - Token endpoint} documentation. | ||
* | ||
* Without a properly working token endpoint (token URL) CKEditor plugins will not be able to connect to CKEditor Cloud Services. | ||
* | ||
* @member {String|Function} module:cloud-services/cloudservices~CloudServicesConfig#tokenUrl | ||
*/ | ||
/** | ||
* The endpoint URL for [CKEditor Cloud Services](https://ckeditor.com/ckeditor-cloud-services) uploads. | ||
* This option must be set for Easy Image to work correctly. | ||
* | ||
* The upload URL is unique for each customer and can be found in the | ||
* [CKEditor Ecosystem customer dashboard](https://dashboard.ckeditor.com) after subscribing to the Easy Image service. | ||
* To learn how to start using Easy Image, check the {@glink @cs guides/easy-image/quick-start Easy Image - Quick start} documentation. | ||
* | ||
* Note: Make sure to also set the {@link module:cloud-services/cloudservices~CloudServicesConfig#tokenUrl} configuration option. | ||
* | ||
* @member {String} module:cloud-services/cloudservices~CloudServicesConfig#uploadUrl | ||
*/ | ||
/** | ||
* The URL for web socket communication, used by the `RealTimeCollaborativeEditing` plugin. Every customer (organization in the CKEditor | ||
* Ecosystem dashboard) has their own, unique URLs to communicate with CKEditor Cloud Services. The URL can be found in the | ||
* CKEditor Ecosystem customer dashboard. | ||
* | ||
* Note: Unlike most plugins, `RealTimeCollaborativeEditing` is not included in any CKEditor 5 build and needs to be installed manually. | ||
* Check [Collaboration overview](https://ckeditor.com/docs/ckeditor5/latest/features/collaboration/overview.html) for more details. | ||
* | ||
* @member {String} module:cloud-services/cloudservices~CloudServicesConfig#webSocketUrl | ||
*/ | ||
/** | ||
* An optional parameter used for integration with CKEditor Cloud Services when uploading the editor build to cloud services. | ||
* | ||
* Whenever the editor build or the configuration changes, this parameter should be set to a new, unique value to differentiate | ||
* the new bundle (build + configuration) from the old ones. | ||
* | ||
* @member {String} module:cloud-services/cloudservices~CloudServicesConfig#bundleVersion | ||
*/ |
@@ -5,48 +5,38 @@ /** | ||
*/ | ||
/** | ||
* @module cloud-services/cloudservicescore | ||
*/ | ||
import { ContextPlugin } from 'ckeditor5/src/core'; | ||
import Token from './token/token'; | ||
import UploadGateway from './uploadgateway/uploadgateway'; | ||
/** | ||
* The `CloudServicesCore` plugin exposes the base API for communication with CKEditor Cloud Services. | ||
* | ||
* @extends module:core/contextplugin~ContextPlugin | ||
*/ | ||
export default class CloudServicesCore extends ContextPlugin { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
static get pluginName() { | ||
return 'CloudServicesCore'; | ||
} | ||
/** | ||
* Creates the {@link module:cloud-services/token~Token} instance. | ||
* | ||
* @param {String|Function} tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the | ||
* value is a function it has to match the {@link module:cloud-services/token~refreshToken} interface. | ||
* @param {Object} [options] | ||
* @param {String} [options.initValue] Initial value of the token. | ||
* @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically. | ||
* @returns {module:cloud-services/token~Token} | ||
*/ | ||
createToken( tokenUrlOrRefreshToken, options ) { | ||
return new Token( tokenUrlOrRefreshToken, options ); | ||
} | ||
/** | ||
* Creates the {@link module:cloud-services/uploadgateway/uploadgateway~UploadGateway} instance. | ||
* | ||
* @param {module:cloud-services/token~Token} token Token used for authentication. | ||
* @param {String} apiAddress API address. | ||
* @returns {module:cloud-services/uploadgateway/uploadgateway~UploadGateway} | ||
*/ | ||
createUploadGateway( token, apiAddress ) { | ||
return new UploadGateway( token, apiAddress ); | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
static get pluginName() { | ||
return 'CloudServicesCore'; | ||
} | ||
/** | ||
* Creates the {@link module:cloud-services/token~Token} instance. | ||
* | ||
* @param tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the | ||
* value is a function it has to match the {@link module:cloud-services/token~refreshToken} interface. | ||
* @param options.initValue Initial value of the token. | ||
* @param options.autoRefresh Specifies whether to start the refresh automatically. | ||
*/ | ||
createToken(tokenUrlOrRefreshToken, options) { | ||
return new Token(tokenUrlOrRefreshToken, options); | ||
} | ||
/** | ||
* Creates the {@link module:cloud-services/uploadgateway/uploadgateway~UploadGateway} instance. | ||
* | ||
* @param token Token used for authentication. | ||
* @param apiAddress API address. | ||
*/ | ||
createUploadGateway(token, apiAddress) { | ||
return new UploadGateway(token, apiAddress); | ||
} | ||
} |
@@ -5,8 +5,6 @@ /** | ||
*/ | ||
/** | ||
* @module cloud-services | ||
*/ | ||
export { default as CloudServices } from './cloudservices'; | ||
export { default as CloudServicesCore } from './cloudservicescore'; |
@@ -5,212 +5,142 @@ /** | ||
*/ | ||
/** | ||
* @module cloud-services/token | ||
*/ | ||
/* globals XMLHttpRequest, setTimeout, clearTimeout, atob */ | ||
import { mix, ObservableMixin, CKEditorError } from 'ckeditor5/src/utils'; | ||
import { ObservableMixin, CKEditorError } from 'ckeditor5/src/utils'; | ||
const DEFAULT_OPTIONS = { autoRefresh: true }; | ||
const DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME = 3600000; | ||
/** | ||
* Class representing the token used for communication with CKEditor Cloud Services. | ||
* Value of the token is retrieving from the specified URL and is refreshed every 1 hour by default. | ||
* | ||
* @mixes ObservableMixin | ||
*/ | ||
class Token { | ||
/** | ||
* Creates `Token` instance. | ||
* Method `init` should be called after using the constructor or use `create` method instead. | ||
* | ||
* @param {String|Function} tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the | ||
* value is a function it has to match the {@link module:cloud-services/token~refreshToken} interface. | ||
* @param {Object} options | ||
* @param {String} [options.initValue] Initial value of the token. | ||
* @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically. | ||
*/ | ||
constructor( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) { | ||
if ( !tokenUrlOrRefreshToken ) { | ||
/** | ||
* A `tokenUrl` must be provided as the first constructor argument. | ||
* | ||
* @error token-missing-token-url | ||
*/ | ||
throw new CKEditorError( | ||
'token-missing-token-url', | ||
this | ||
); | ||
} | ||
if ( options.initValue ) { | ||
this._validateTokenValue( options.initValue ); | ||
} | ||
/** | ||
* Value of the token. | ||
* The value of the token is null if `initValue` is not provided or `init` method was not called. | ||
* `create` method creates token with initialized value from url. | ||
* | ||
* @name value | ||
* @member {String} #value | ||
* @observable | ||
* @readonly | ||
*/ | ||
this.set( 'value', options.initValue ); | ||
/** | ||
* Base refreshing function. | ||
* | ||
* @private | ||
* @member {String|Function} #_refresh | ||
*/ | ||
if ( typeof tokenUrlOrRefreshToken === 'function' ) { | ||
this._refresh = tokenUrlOrRefreshToken; | ||
} else { | ||
this._refresh = () => defaultRefreshToken( tokenUrlOrRefreshToken ); | ||
} | ||
/** | ||
* @type {Object} | ||
* @private | ||
*/ | ||
this._options = Object.assign( {}, DEFAULT_OPTIONS, options ); | ||
} | ||
/** | ||
* Initializes the token. | ||
* | ||
* @returns {Promise.<module:cloud-services/token~Token>} | ||
*/ | ||
init() { | ||
return new Promise( ( resolve, reject ) => { | ||
if ( !this.value ) { | ||
this.refreshToken() | ||
.then( resolve ) | ||
.catch( reject ); | ||
return; | ||
} | ||
if ( this._options.autoRefresh ) { | ||
this._registerRefreshTokenTimeout(); | ||
} | ||
resolve( this ); | ||
} ); | ||
} | ||
/** | ||
* Refresh token method. Useful in a method form as it can be override in tests. | ||
* @returns {Promise.<String>} | ||
*/ | ||
refreshToken() { | ||
return this._refresh() | ||
.then( value => { | ||
this._validateTokenValue( value ); | ||
this.set( 'value', value ); | ||
if ( this._options.autoRefresh ) { | ||
this._registerRefreshTokenTimeout(); | ||
} | ||
} ) | ||
.then( () => this ); | ||
} | ||
/** | ||
* Destroys token instance. Stops refreshing. | ||
*/ | ||
destroy() { | ||
clearTimeout( this._tokenRefreshTimeout ); | ||
} | ||
/** | ||
* Checks whether the provided token follows the JSON Web Tokens (JWT) format. | ||
* | ||
* @protected | ||
* @param {String} tokenValue The token to validate. | ||
*/ | ||
_validateTokenValue( tokenValue ) { | ||
// The token must be a string. | ||
const isString = typeof tokenValue === 'string'; | ||
// The token must be a plain string without quotes (""). | ||
const isPlainString = !/^".*"$/.test( tokenValue ); | ||
// JWT token contains 3 parts: header, payload, and signature. | ||
// Each part is separated by a dot. | ||
const isJWTFormat = isString && tokenValue.split( '.' ).length === 3; | ||
if ( !( isPlainString && isJWTFormat ) ) { | ||
/** | ||
* The provided token must follow the [JSON Web Tokens](https://jwt.io/introduction/) format. | ||
* | ||
* @error token-not-in-jwt-format | ||
*/ | ||
throw new CKEditorError( 'token-not-in-jwt-format', this ); | ||
} | ||
} | ||
/** | ||
* Registers a refresh token timeout for the time taken from token. | ||
* | ||
* @protected | ||
*/ | ||
_registerRefreshTokenTimeout() { | ||
const tokenRefreshTimeoutTime = this._getTokenRefreshTimeoutTime(); | ||
clearTimeout( this._tokenRefreshTimeout ); | ||
this._tokenRefreshTimeout = setTimeout( () => { | ||
this.refreshToken(); | ||
}, tokenRefreshTimeoutTime ); | ||
} | ||
/** | ||
* Returns token refresh timeout time calculated from expire time in the token payload. | ||
* | ||
* If the token parse fails or the token payload doesn't contain, the default DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME is returned. | ||
* | ||
* @protected | ||
* @returns {Number} | ||
*/ | ||
_getTokenRefreshTimeoutTime() { | ||
try { | ||
const [ , binaryTokenPayload ] = this.value.split( '.' ); | ||
const { exp: tokenExpireTime } = JSON.parse( atob( binaryTokenPayload ) ); | ||
if ( !tokenExpireTime ) { | ||
return DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME; | ||
} | ||
const tokenRefreshTimeoutTime = Math.floor( ( ( tokenExpireTime * 1000 ) - Date.now() ) / 2 ); | ||
return tokenRefreshTimeoutTime; | ||
} catch ( err ) { | ||
return DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME; | ||
} | ||
} | ||
/** | ||
* Creates a initialized {@link module:cloud-services/token~Token} instance. | ||
* | ||
* @param {String|Function} tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the | ||
* value is a function it has to match the {@link module:cloud-services/token~refreshToken} interface. | ||
* @param {Object} options | ||
* @param {String} [options.initValue] Initial value of the token. | ||
* @param {Boolean} [options.autoRefresh=true] Specifies whether to start the refresh automatically. | ||
* @returns {Promise.<module:cloud-services/token~Token>} | ||
*/ | ||
static create( tokenUrlOrRefreshToken, options = DEFAULT_OPTIONS ) { | ||
const token = new Token( tokenUrlOrRefreshToken, options ); | ||
return token.init(); | ||
} | ||
export default class Token extends ObservableMixin() { | ||
/** | ||
* Creates `Token` instance. | ||
* Method `init` should be called after using the constructor or use `create` method instead. | ||
* | ||
* @param tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the | ||
* value is a function it has to match the {@link module:cloud-services/token~refreshToken} interface. | ||
*/ | ||
constructor(tokenUrlOrRefreshToken, options = {}) { | ||
super(); | ||
if (!tokenUrlOrRefreshToken) { | ||
/** | ||
* A `tokenUrl` must be provided as the first constructor argument. | ||
* | ||
* @error token-missing-token-url | ||
*/ | ||
throw new CKEditorError('token-missing-token-url', this); | ||
} | ||
if (options.initValue) { | ||
this._validateTokenValue(options.initValue); | ||
} | ||
this.set('value', options.initValue); | ||
if (typeof tokenUrlOrRefreshToken === 'function') { | ||
this._refresh = tokenUrlOrRefreshToken; | ||
} | ||
else { | ||
this._refresh = () => defaultRefreshToken(tokenUrlOrRefreshToken); | ||
} | ||
this._options = { ...DEFAULT_OPTIONS, ...options }; | ||
} | ||
/** | ||
* Initializes the token. | ||
*/ | ||
init() { | ||
return new Promise((resolve, reject) => { | ||
if (!this.value) { | ||
this.refreshToken() | ||
.then(resolve) | ||
.catch(reject); | ||
return; | ||
} | ||
if (this._options.autoRefresh) { | ||
this._registerRefreshTokenTimeout(); | ||
} | ||
resolve(this); | ||
}); | ||
} | ||
/** | ||
* Refresh token method. Useful in a method form as it can be override in tests. | ||
*/ | ||
refreshToken() { | ||
return this._refresh() | ||
.then(value => { | ||
this._validateTokenValue(value); | ||
this.set('value', value); | ||
if (this._options.autoRefresh) { | ||
this._registerRefreshTokenTimeout(); | ||
} | ||
return this; | ||
}); | ||
} | ||
/** | ||
* Destroys token instance. Stops refreshing. | ||
*/ | ||
destroy() { | ||
clearTimeout(this._tokenRefreshTimeout); | ||
} | ||
/** | ||
* Checks whether the provided token follows the JSON Web Tokens (JWT) format. | ||
* | ||
* @param tokenValue The token to validate. | ||
*/ | ||
_validateTokenValue(tokenValue) { | ||
// The token must be a string. | ||
const isString = typeof tokenValue === 'string'; | ||
// The token must be a plain string without quotes (""). | ||
const isPlainString = !/^".*"$/.test(tokenValue); | ||
// JWT token contains 3 parts: header, payload, and signature. | ||
// Each part is separated by a dot. | ||
const isJWTFormat = isString && tokenValue.split('.').length === 3; | ||
if (!(isPlainString && isJWTFormat)) { | ||
/** | ||
* The provided token must follow the [JSON Web Tokens](https://jwt.io/introduction/) format. | ||
* | ||
* @error token-not-in-jwt-format | ||
*/ | ||
throw new CKEditorError('token-not-in-jwt-format', this); | ||
} | ||
} | ||
/** | ||
* Registers a refresh token timeout for the time taken from token. | ||
*/ | ||
_registerRefreshTokenTimeout() { | ||
const tokenRefreshTimeoutTime = this._getTokenRefreshTimeoutTime(); | ||
clearTimeout(this._tokenRefreshTimeout); | ||
this._tokenRefreshTimeout = setTimeout(() => { | ||
this.refreshToken(); | ||
}, tokenRefreshTimeoutTime); | ||
} | ||
/** | ||
* Returns token refresh timeout time calculated from expire time in the token payload. | ||
* | ||
* If the token parse fails or the token payload doesn't contain, the default DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME is returned. | ||
*/ | ||
_getTokenRefreshTimeoutTime() { | ||
try { | ||
const [, binaryTokenPayload] = this.value.split('.'); | ||
const { exp: tokenExpireTime } = JSON.parse(atob(binaryTokenPayload)); | ||
if (!tokenExpireTime) { | ||
return DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME; | ||
} | ||
const tokenRefreshTimeoutTime = Math.floor(((tokenExpireTime * 1000) - Date.now()) / 2); | ||
return tokenRefreshTimeoutTime; | ||
} | ||
catch (err) { | ||
return DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME; | ||
} | ||
} | ||
/** | ||
* Creates a initialized {@link module:cloud-services/token~Token} instance. | ||
* | ||
* @param tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the | ||
* value is a function it has to match the {@link module:cloud-services/token~refreshToken} interface. | ||
*/ | ||
static create(tokenUrlOrRefreshToken, options = {}) { | ||
const token = new Token(tokenUrlOrRefreshToken, options); | ||
return token.init(); | ||
} | ||
} | ||
mix( Token, ObservableMixin ); | ||
/** | ||
@@ -220,42 +150,24 @@ * This function is called in a defined interval by the {@link ~Token} class. It also can be invoked manually. | ||
* If any error occurs it should return a rejected promise with an error message. | ||
* | ||
* @function refreshToken | ||
* @returns {Promise.<String>} | ||
*/ | ||
/** | ||
* @private | ||
* @param {String} tokenUrl | ||
*/ | ||
function defaultRefreshToken( tokenUrl ) { | ||
return new Promise( ( resolve, reject ) => { | ||
const xhr = new XMLHttpRequest(); | ||
xhr.open( 'GET', tokenUrl ); | ||
xhr.addEventListener( 'load', () => { | ||
const statusCode = xhr.status; | ||
const xhrResponse = xhr.response; | ||
if ( statusCode < 200 || statusCode > 299 ) { | ||
/** | ||
* Cannot download new token from the provided url. | ||
* | ||
* @error token-cannot-download-new-token | ||
*/ | ||
return reject( | ||
new CKEditorError( 'token-cannot-download-new-token', null ) | ||
); | ||
} | ||
return resolve( xhrResponse ); | ||
} ); | ||
xhr.addEventListener( 'error', () => reject( new Error( 'Network Error' ) ) ); | ||
xhr.addEventListener( 'abort', () => reject( new Error( 'Abort' ) ) ); | ||
xhr.send(); | ||
} ); | ||
function defaultRefreshToken(tokenUrl) { | ||
return new Promise((resolve, reject) => { | ||
const xhr = new XMLHttpRequest(); | ||
xhr.open('GET', tokenUrl); | ||
xhr.addEventListener('load', () => { | ||
const statusCode = xhr.status; | ||
const xhrResponse = xhr.response; | ||
if (statusCode < 200 || statusCode > 299) { | ||
/** | ||
* Cannot download new token from the provided url. | ||
* | ||
* @error token-cannot-download-new-token | ||
*/ | ||
return reject(new CKEditorError('token-cannot-download-new-token', null)); | ||
} | ||
return resolve(xhrResponse); | ||
}); | ||
xhr.addEventListener('error', () => reject(new Error('Network Error'))); | ||
xhr.addEventListener('abort', () => reject(new Error('Abort'))); | ||
xhr.send(); | ||
}); | ||
} | ||
export default Token; |
@@ -5,286 +5,180 @@ /** | ||
*/ | ||
/** | ||
* @module cloud-services/uploadgateway/fileuploader | ||
*/ | ||
/* globals XMLHttpRequest, FormData, Blob, atob */ | ||
import { mix, EmitterMixin, CKEditorError } from 'ckeditor5/src/utils'; | ||
import { EmitterMixin, CKEditorError } from 'ckeditor5/src/utils'; | ||
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/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', null ); | ||
} | ||
if ( !token ) { | ||
/** | ||
* Token must be provided as the second argument. | ||
* | ||
* @error fileuploader-missing-token | ||
*/ | ||
throw new CKEditorError( 'fileuploader-missing-token', null ); | ||
} | ||
if ( !apiAddress ) { | ||
/** | ||
* Api address must be provided as the third argument. | ||
* | ||
* @error fileuploader-missing-api-address | ||
*/ | ||
throw new CKEditorError( 'fileuploader-missing-api-address', 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/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/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/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', | ||
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 | ||
*/ | ||
export default class FileUploader extends EmitterMixin() { | ||
/** | ||
* Creates `FileUploader` instance. | ||
* | ||
* @param fileOrData A blob object or a data string encoded with Base64. | ||
* @param token Token used for authentication. | ||
* @param apiAddress API address. | ||
*/ | ||
constructor(fileOrData, token, apiAddress) { | ||
super(); | ||
if (!fileOrData) { | ||
/** | ||
* File must be provided as the first argument. | ||
* | ||
* @error fileuploader-missing-file | ||
*/ | ||
throw new CKEditorError('fileuploader-missing-file', null); | ||
} | ||
if (!token) { | ||
/** | ||
* Token must be provided as the second argument. | ||
* | ||
* @error fileuploader-missing-token | ||
*/ | ||
throw new CKEditorError('fileuploader-missing-token', null); | ||
} | ||
if (!apiAddress) { | ||
/** | ||
* Api address must be provided as the third argument. | ||
* | ||
* @error fileuploader-missing-api-address | ||
*/ | ||
throw new CKEditorError('fileuploader-missing-api-address', null); | ||
} | ||
this.file = _isBase64(fileOrData) ? _base64ToBlob(fileOrData) : fileOrData; | ||
this._token = token; | ||
this._apiAddress = apiAddress; | ||
} | ||
/** | ||
* Registers callback on `progress` event. | ||
*/ | ||
onProgress(callback) { | ||
this.on('progress', (event, data) => callback(data)); | ||
return this; | ||
} | ||
/** | ||
* Registers callback on `error` event. Event is called once when error occurs. | ||
*/ | ||
onError(callback) { | ||
this.once('error', (event, data) => callback(data)); | ||
return this; | ||
} | ||
/** | ||
* Aborts upload process. | ||
*/ | ||
abort() { | ||
this.xhr.abort(); | ||
} | ||
/** | ||
* Sends XHR request to API. | ||
*/ | ||
send() { | ||
this._prepareRequest(); | ||
this._attachXHRListeners(); | ||
return this._sendRequest(); | ||
} | ||
/** | ||
* Prepares XHR request. | ||
*/ | ||
_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. | ||
*/ | ||
_attachXHRListeners() { | ||
const xhr = this.xhr; | ||
const onError = (message) => { | ||
return () => this.fire('error', message); | ||
}; | ||
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); | ||
} | ||
}); | ||
} | ||
/** | ||
* Sends XHR request. | ||
*/ | ||
_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', 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); | ||
}); | ||
} | ||
} | ||
mix( FileUploader, EmitterMixin ); | ||
/** | ||
* Transforms Base64 string data into file. | ||
* | ||
* @param {String} base64 String data. | ||
* @param {Number} [sliceSize=512] | ||
* @returns {Blob} | ||
* @private | ||
* @param base64 String data. | ||
*/ | ||
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', null ); | ||
} | ||
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', 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 ); | ||
function _isBase64(string) { | ||
if (typeof string !== 'string') { | ||
return false; | ||
} | ||
const match = string.match(BASE64_HEADER_REG_EXP); | ||
return !!(match && match.length); | ||
} |
@@ -5,10 +5,7 @@ /** | ||
*/ | ||
/** | ||
* @module cloud-services/uploadgateway/uploadgateway | ||
*/ | ||
import FileUploader from './fileuploader'; | ||
import { CKEditorError } from 'ckeditor5/src/utils'; | ||
/** | ||
@@ -18,63 +15,48 @@ * UploadGateway abstracts file uploads to CKEditor Cloud Services. | ||
export default class UploadGateway { | ||
/** | ||
* Creates `UploadGateway` instance. | ||
* | ||
* @param {module:cloud-services/token~Token} token Token used for authentication. | ||
* @param {String} apiAddress API address. | ||
*/ | ||
constructor( token, apiAddress ) { | ||
if ( !token ) { | ||
/** | ||
* Token must be provided. | ||
* | ||
* @error uploadgateway-missing-token | ||
*/ | ||
throw new CKEditorError( 'uploadgateway-missing-token', null ); | ||
} | ||
if ( !apiAddress ) { | ||
/** | ||
* Api address must be provided. | ||
* | ||
* @error uploadgateway-missing-api-address | ||
*/ | ||
throw new CKEditorError( 'uploadgateway-missing-api-address', null ); | ||
} | ||
/** | ||
* CKEditor Cloud Services access token. | ||
* | ||
* @type {module:cloud-services/token~Token} | ||
* @private | ||
*/ | ||
this._token = token; | ||
/** | ||
* CKEditor Cloud Services API address. | ||
* | ||
* @type {String} | ||
* @private | ||
*/ | ||
this._apiAddress = apiAddress; | ||
} | ||
/** | ||
* Creates a {@link module:cloud-services/uploadgateway/fileuploader~FileUploader} instance that wraps | ||
* file upload process. The file is being sent at a time when the | ||
* {@link module:cloud-services/uploadgateway/fileuploader~FileUploader#send} method is called. | ||
* | ||
* const token = await Token.create( 'https://token-endpoint' ); | ||
* new UploadGateway( token, 'https://example.org' ) | ||
* .upload( 'FILE' ) | ||
* .onProgress( ( data ) => console.log( data ) ) | ||
* .send() | ||
* .then( ( response ) => console.log( response ) ); | ||
* | ||
* @param {Blob|String} fileOrData A blob object or a data string encoded with Base64. | ||
* @returns {module:cloud-services/uploadgateway/fileuploader~FileUploader} Returns `FileUploader` instance. | ||
*/ | ||
upload( fileOrData ) { | ||
return new FileUploader( fileOrData, this._token, this._apiAddress ); | ||
} | ||
/** | ||
* Creates `UploadGateway` instance. | ||
* | ||
* @param token Token used for authentication. | ||
* @param apiAddress API address. | ||
*/ | ||
constructor(token, apiAddress) { | ||
if (!token) { | ||
/** | ||
* Token must be provided. | ||
* | ||
* @error uploadgateway-missing-token | ||
*/ | ||
throw new CKEditorError('uploadgateway-missing-token', null); | ||
} | ||
if (!apiAddress) { | ||
/** | ||
* Api address must be provided. | ||
* | ||
* @error uploadgateway-missing-api-address | ||
*/ | ||
throw new CKEditorError('uploadgateway-missing-api-address', null); | ||
} | ||
this._token = token; | ||
this._apiAddress = apiAddress; | ||
} | ||
/** | ||
* Creates a {@link module:cloud-services/uploadgateway/fileuploader~FileUploader} instance that wraps | ||
* file upload process. The file is being sent at a time when the | ||
* {@link module:cloud-services/uploadgateway/fileuploader~FileUploader#send} method is called. | ||
* | ||
* ```ts | ||
* const token = await Token.create( 'https://token-endpoint' ); | ||
* new UploadGateway( token, 'https://example.org' ) | ||
* .upload( 'FILE' ) | ||
* .onProgress( ( data ) => console.log( data ) ) | ||
* .send() | ||
* .then( ( response ) => console.log( response ) ); | ||
* ``` | ||
* | ||
* @param {Blob|String} fileOrData A blob object or a data string encoded with Base64. | ||
* @returns {module:cloud-services/uploadgateway/fileuploader~FileUploader} Returns `FileUploader` instance. | ||
*/ | ||
upload(fileOrData) { | ||
return new FileUploader(fileOrData, this._token, this._apiAddress); | ||
} | ||
} | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
54984
20
1096
8
1
1
+ Added@ckeditor/ckeditor5-clipboard@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-core@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-engine@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-enter@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-paragraph@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-select-all@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-typing@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-ui@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-undo@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-upload@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-utils@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-watchdog@37.1.0(transitive)
+ Added@ckeditor/ckeditor5-widget@37.1.0(transitive)
+ Addedckeditor5@37.1.0(transitive)
- Removed@ckeditor/ckeditor5-clipboard@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-core@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-engine@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-enter@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-paragraph@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-select-all@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-typing@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-ui@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-undo@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-upload@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-utils@36.0.1(transitive)
- Removed@ckeditor/ckeditor5-widget@36.0.1(transitive)
- Removedckeditor5@36.0.1(transitive)
Updatedckeditor5@^37.0.0-alpha.0