@ckeditor/ckeditor5-cloud-services 41.2.0 → 41.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
package/dist/index.css ADDED
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
package/dist/index.js ADDED
@@ -0,0 +1,562 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import { ContextPlugin } from '@ckeditor/ckeditor5-core/dist/index.js';
6
+ import { ObservableMixin, CKEditorError, EmitterMixin } from '@ckeditor/ckeditor5-utils/dist/index.js';
7
+
8
+ /**
9
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
10
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
11
+ */
12
+ /**
13
+ * @module cloud-services/token/token
14
+ */
15
+ /* globals XMLHttpRequest, setTimeout, clearTimeout, atob */
16
+ const DEFAULT_OPTIONS = { autoRefresh: true };
17
+ const DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME = 3600000;
18
+ /**
19
+ * Class representing the token used for communication with CKEditor Cloud Services.
20
+ * Value of the token is retrieving from the specified URL and is refreshed every 1 hour by default.
21
+ */
22
+ class Token extends ObservableMixin() {
23
+ /**
24
+ * Creates `Token` instance.
25
+ * Method `init` should be called after using the constructor or use `create` method instead.
26
+ *
27
+ * @param tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the
28
+ * value is a function it has to match the {@link module:cloud-services/token/token~Token#refreshToken} interface.
29
+ */
30
+ constructor(tokenUrlOrRefreshToken, options = {}) {
31
+ super();
32
+ if (!tokenUrlOrRefreshToken) {
33
+ /**
34
+ * A `tokenUrl` must be provided as the first constructor argument.
35
+ *
36
+ * @error token-missing-token-url
37
+ */
38
+ throw new CKEditorError('token-missing-token-url', this);
39
+ }
40
+ if (options.initValue) {
41
+ this._validateTokenValue(options.initValue);
42
+ }
43
+ this.set('value', options.initValue);
44
+ if (typeof tokenUrlOrRefreshToken === 'function') {
45
+ this._refresh = tokenUrlOrRefreshToken;
46
+ }
47
+ else {
48
+ this._refresh = () => defaultRefreshToken(tokenUrlOrRefreshToken);
49
+ }
50
+ this._options = { ...DEFAULT_OPTIONS, ...options };
51
+ }
52
+ /**
53
+ * Initializes the token.
54
+ */
55
+ init() {
56
+ return new Promise((resolve, reject) => {
57
+ if (!this.value) {
58
+ this.refreshToken()
59
+ .then(resolve)
60
+ .catch(reject);
61
+ return;
62
+ }
63
+ if (this._options.autoRefresh) {
64
+ this._registerRefreshTokenTimeout();
65
+ }
66
+ resolve(this);
67
+ });
68
+ }
69
+ /**
70
+ * Refresh token method. Useful in a method form as it can be override in tests.
71
+ */
72
+ refreshToken() {
73
+ return this._refresh()
74
+ .then(value => {
75
+ this._validateTokenValue(value);
76
+ this.set('value', value);
77
+ if (this._options.autoRefresh) {
78
+ this._registerRefreshTokenTimeout();
79
+ }
80
+ return this;
81
+ });
82
+ }
83
+ /**
84
+ * Destroys token instance. Stops refreshing.
85
+ */
86
+ destroy() {
87
+ clearTimeout(this._tokenRefreshTimeout);
88
+ }
89
+ /**
90
+ * Checks whether the provided token follows the JSON Web Tokens (JWT) format.
91
+ *
92
+ * @param tokenValue The token to validate.
93
+ */
94
+ _validateTokenValue(tokenValue) {
95
+ // The token must be a string.
96
+ const isString = typeof tokenValue === 'string';
97
+ // The token must be a plain string without quotes ("").
98
+ const isPlainString = !/^".*"$/.test(tokenValue);
99
+ // JWT token contains 3 parts: header, payload, and signature.
100
+ // Each part is separated by a dot.
101
+ const isJWTFormat = isString && tokenValue.split('.').length === 3;
102
+ if (!(isPlainString && isJWTFormat)) {
103
+ /**
104
+ * The provided token must follow the [JSON Web Tokens](https://jwt.io/introduction/) format.
105
+ *
106
+ * @error token-not-in-jwt-format
107
+ */
108
+ throw new CKEditorError('token-not-in-jwt-format', this);
109
+ }
110
+ }
111
+ /**
112
+ * Registers a refresh token timeout for the time taken from token.
113
+ */
114
+ _registerRefreshTokenTimeout() {
115
+ const tokenRefreshTimeoutTime = this._getTokenRefreshTimeoutTime();
116
+ clearTimeout(this._tokenRefreshTimeout);
117
+ this._tokenRefreshTimeout = setTimeout(() => {
118
+ this.refreshToken();
119
+ }, tokenRefreshTimeoutTime);
120
+ }
121
+ /**
122
+ * Returns token refresh timeout time calculated from expire time in the token payload.
123
+ *
124
+ * If the token parse fails or the token payload doesn't contain, the default DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME is returned.
125
+ */
126
+ _getTokenRefreshTimeoutTime() {
127
+ try {
128
+ const [, binaryTokenPayload] = this.value.split('.');
129
+ const { exp: tokenExpireTime } = JSON.parse(atob(binaryTokenPayload));
130
+ if (!tokenExpireTime) {
131
+ return DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME;
132
+ }
133
+ const tokenRefreshTimeoutTime = Math.floor(((tokenExpireTime * 1000) - Date.now()) / 2);
134
+ return tokenRefreshTimeoutTime;
135
+ }
136
+ catch (err) {
137
+ return DEFAULT_TOKEN_REFRESH_TIMEOUT_TIME;
138
+ }
139
+ }
140
+ /**
141
+ * Creates a initialized {@link module:cloud-services/token/token~Token} instance.
142
+ *
143
+ * @param tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the
144
+ * value is a function it has to match the {@link module:cloud-services/token/token~Token#refreshToken} interface.
145
+ */
146
+ static create(tokenUrlOrRefreshToken, options = {}) {
147
+ const token = new Token(tokenUrlOrRefreshToken, options);
148
+ return token.init();
149
+ }
150
+ }
151
+ /**
152
+ * This function is called in a defined interval by the {@link ~Token} class. It also can be invoked manually.
153
+ * It should return a promise, which resolves with the new token value.
154
+ * If any error occurs it should return a rejected promise with an error message.
155
+ */
156
+ function defaultRefreshToken(tokenUrl) {
157
+ return new Promise((resolve, reject) => {
158
+ const xhr = new XMLHttpRequest();
159
+ xhr.open('GET', tokenUrl);
160
+ xhr.addEventListener('load', () => {
161
+ const statusCode = xhr.status;
162
+ const xhrResponse = xhr.response;
163
+ if (statusCode < 200 || statusCode > 299) {
164
+ /**
165
+ * Cannot download new token from the provided url.
166
+ *
167
+ * @error token-cannot-download-new-token
168
+ */
169
+ return reject(new CKEditorError('token-cannot-download-new-token', null));
170
+ }
171
+ return resolve(xhrResponse);
172
+ });
173
+ xhr.addEventListener('error', () => reject(new Error('Network Error')));
174
+ xhr.addEventListener('abort', () => reject(new Error('Abort')));
175
+ xhr.send();
176
+ });
177
+ }
178
+
179
+ /**
180
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
181
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
182
+ */
183
+ const BASE64_HEADER_REG_EXP = /^data:(\S*?);base64,/;
184
+ /**
185
+ * FileUploader class used to upload single file.
186
+ */
187
+ class FileUploader extends EmitterMixin() {
188
+ /**
189
+ * Creates `FileUploader` instance.
190
+ *
191
+ * @param fileOrData A blob object or a data string encoded with Base64.
192
+ * @param token Token used for authentication.
193
+ * @param apiAddress API address.
194
+ */
195
+ constructor(fileOrData, token, apiAddress) {
196
+ super();
197
+ if (!fileOrData) {
198
+ /**
199
+ * File must be provided as the first argument.
200
+ *
201
+ * @error fileuploader-missing-file
202
+ */
203
+ throw new CKEditorError('fileuploader-missing-file', null);
204
+ }
205
+ if (!token) {
206
+ /**
207
+ * Token must be provided as the second argument.
208
+ *
209
+ * @error fileuploader-missing-token
210
+ */
211
+ throw new CKEditorError('fileuploader-missing-token', null);
212
+ }
213
+ if (!apiAddress) {
214
+ /**
215
+ * Api address must be provided as the third argument.
216
+ *
217
+ * @error fileuploader-missing-api-address
218
+ */
219
+ throw new CKEditorError('fileuploader-missing-api-address', null);
220
+ }
221
+ this.file = _isBase64(fileOrData) ? _base64ToBlob(fileOrData) : fileOrData;
222
+ this._token = token;
223
+ this._apiAddress = apiAddress;
224
+ }
225
+ /**
226
+ * Registers callback on `progress` event.
227
+ */
228
+ onProgress(callback) {
229
+ this.on('progress', (event, data) => callback(data));
230
+ return this;
231
+ }
232
+ /**
233
+ * Registers callback on `error` event. Event is called once when error occurs.
234
+ */
235
+ onError(callback) {
236
+ this.once('error', (event, data) => callback(data));
237
+ return this;
238
+ }
239
+ /**
240
+ * Aborts upload process.
241
+ */
242
+ abort() {
243
+ this.xhr.abort();
244
+ }
245
+ /**
246
+ * Sends XHR request to API.
247
+ */
248
+ send() {
249
+ this._prepareRequest();
250
+ this._attachXHRListeners();
251
+ return this._sendRequest();
252
+ }
253
+ /**
254
+ * Prepares XHR request.
255
+ */
256
+ _prepareRequest() {
257
+ const xhr = new XMLHttpRequest();
258
+ xhr.open('POST', this._apiAddress);
259
+ xhr.setRequestHeader('Authorization', this._token.value);
260
+ xhr.responseType = 'json';
261
+ this.xhr = xhr;
262
+ }
263
+ /**
264
+ * Attaches listeners to the XHR.
265
+ */
266
+ _attachXHRListeners() {
267
+ const xhr = this.xhr;
268
+ const onError = (message) => {
269
+ return () => this.fire('error', message);
270
+ };
271
+ xhr.addEventListener('error', onError('Network Error'));
272
+ xhr.addEventListener('abort', onError('Abort'));
273
+ /* istanbul ignore else -- @preserve */
274
+ if (xhr.upload) {
275
+ xhr.upload.addEventListener('progress', event => {
276
+ if (event.lengthComputable) {
277
+ this.fire('progress', {
278
+ total: event.total,
279
+ uploaded: event.loaded
280
+ });
281
+ }
282
+ });
283
+ }
284
+ xhr.addEventListener('load', () => {
285
+ const statusCode = xhr.status;
286
+ const xhrResponse = xhr.response;
287
+ if (statusCode < 200 || statusCode > 299) {
288
+ return this.fire('error', xhrResponse.message || xhrResponse.error);
289
+ }
290
+ });
291
+ }
292
+ /**
293
+ * Sends XHR request.
294
+ */
295
+ _sendRequest() {
296
+ const formData = new FormData();
297
+ const xhr = this.xhr;
298
+ formData.append('file', this.file);
299
+ return new Promise((resolve, reject) => {
300
+ xhr.addEventListener('load', () => {
301
+ const statusCode = xhr.status;
302
+ const xhrResponse = xhr.response;
303
+ if (statusCode < 200 || statusCode > 299) {
304
+ if (xhrResponse.message) {
305
+ /**
306
+ * Uploading file failed.
307
+ *
308
+ * @error fileuploader-uploading-data-failed
309
+ */
310
+ return reject(new CKEditorError('fileuploader-uploading-data-failed', this, { message: xhrResponse.message }));
311
+ }
312
+ return reject(xhrResponse.error);
313
+ }
314
+ return resolve(xhrResponse);
315
+ });
316
+ xhr.addEventListener('error', () => reject(new Error('Network Error')));
317
+ xhr.addEventListener('abort', () => reject(new Error('Abort')));
318
+ xhr.send(formData);
319
+ });
320
+ }
321
+ }
322
+ /**
323
+ * Transforms Base64 string data into file.
324
+ *
325
+ * @param base64 String data.
326
+ */
327
+ function _base64ToBlob(base64, sliceSize = 512) {
328
+ try {
329
+ const contentType = base64.match(BASE64_HEADER_REG_EXP)[1];
330
+ const base64Data = atob(base64.replace(BASE64_HEADER_REG_EXP, ''));
331
+ const byteArrays = [];
332
+ for (let offset = 0; offset < base64Data.length; offset += sliceSize) {
333
+ const slice = base64Data.slice(offset, offset + sliceSize);
334
+ const byteNumbers = new Array(slice.length);
335
+ for (let i = 0; i < slice.length; i++) {
336
+ byteNumbers[i] = slice.charCodeAt(i);
337
+ }
338
+ byteArrays.push(new Uint8Array(byteNumbers));
339
+ }
340
+ return new Blob(byteArrays, { type: contentType });
341
+ }
342
+ catch (error) {
343
+ /**
344
+ * Problem with decoding Base64 image data.
345
+ *
346
+ * @error fileuploader-decoding-image-data-error
347
+ */
348
+ throw new CKEditorError('fileuploader-decoding-image-data-error', null);
349
+ }
350
+ }
351
+ /**
352
+ * Checks that string is Base64.
353
+ */
354
+ function _isBase64(string) {
355
+ if (typeof string !== 'string') {
356
+ return false;
357
+ }
358
+ const match = string.match(BASE64_HEADER_REG_EXP);
359
+ return !!(match && match.length);
360
+ }
361
+
362
+ /**
363
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
364
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
365
+ */
366
+ /**
367
+ * @module cloud-services/uploadgateway/uploadgateway
368
+ */
369
+ /**
370
+ * UploadGateway abstracts file uploads to CKEditor Cloud Services.
371
+ */
372
+ class UploadGateway {
373
+ /**
374
+ * Creates `UploadGateway` instance.
375
+ *
376
+ * @param token Token used for authentication.
377
+ * @param apiAddress API address.
378
+ */
379
+ constructor(token, apiAddress) {
380
+ if (!token) {
381
+ /**
382
+ * Token must be provided.
383
+ *
384
+ * @error uploadgateway-missing-token
385
+ */
386
+ throw new CKEditorError('uploadgateway-missing-token', null);
387
+ }
388
+ if (!apiAddress) {
389
+ /**
390
+ * Api address must be provided.
391
+ *
392
+ * @error uploadgateway-missing-api-address
393
+ */
394
+ throw new CKEditorError('uploadgateway-missing-api-address', null);
395
+ }
396
+ this._token = token;
397
+ this._apiAddress = apiAddress;
398
+ }
399
+ /**
400
+ * Creates a {@link module:cloud-services/uploadgateway/fileuploader~FileUploader} instance that wraps
401
+ * file upload process. The file is being sent at a time when the
402
+ * {@link module:cloud-services/uploadgateway/fileuploader~FileUploader#send} method is called.
403
+ *
404
+ * ```ts
405
+ * const token = await Token.create( 'https://token-endpoint' );
406
+ * new UploadGateway( token, 'https://example.org' )
407
+ * .upload( 'FILE' )
408
+ * .onProgress( ( data ) => console.log( data ) )
409
+ * .send()
410
+ * .then( ( response ) => console.log( response ) );
411
+ * ```
412
+ *
413
+ * @param {Blob|String} fileOrData A blob object or a data string encoded with Base64.
414
+ * @returns {module:cloud-services/uploadgateway/fileuploader~FileUploader} Returns `FileUploader` instance.
415
+ */
416
+ upload(fileOrData) {
417
+ return new FileUploader(fileOrData, this._token, this._apiAddress);
418
+ }
419
+ }
420
+
421
+ /**
422
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
423
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
424
+ */
425
+ /**
426
+ * @module cloud-services/cloudservicescore
427
+ */
428
+ /**
429
+ * The `CloudServicesCore` plugin exposes the base API for communication with CKEditor Cloud Services.
430
+ */
431
+ class CloudServicesCore extends ContextPlugin {
432
+ /**
433
+ * @inheritDoc
434
+ */
435
+ static get pluginName() {
436
+ return 'CloudServicesCore';
437
+ }
438
+ /**
439
+ * Creates the {@link module:cloud-services/token/token~Token} instance.
440
+ *
441
+ * @param tokenUrlOrRefreshToken Endpoint address to download the token or a callback that provides the token. If the
442
+ * value is a function it has to match the {@link module:cloud-services/token/token~Token#refreshToken} interface.
443
+ * @param options.initValue Initial value of the token.
444
+ * @param options.autoRefresh Specifies whether to start the refresh automatically.
445
+ */
446
+ createToken(tokenUrlOrRefreshToken, options) {
447
+ return new Token(tokenUrlOrRefreshToken, options);
448
+ }
449
+ /**
450
+ * Creates the {@link module:cloud-services/uploadgateway/uploadgateway~UploadGateway} instance.
451
+ *
452
+ * @param token Token used for authentication.
453
+ * @param apiAddress API address.
454
+ */
455
+ createUploadGateway(token, apiAddress) {
456
+ return new UploadGateway(token, apiAddress);
457
+ }
458
+ }
459
+
460
+ /**
461
+ * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
462
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
463
+ */
464
+ /**
465
+ * @module cloud-services/cloudservices
466
+ */
467
+ /**
468
+ * Plugin introducing the integration between CKEditor 5 and CKEditor Cloud Services .
469
+ *
470
+ * It initializes the token provider based on
471
+ * the {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig `config.cloudService`}.
472
+ */
473
+ class CloudServices extends ContextPlugin {
474
+ constructor() {
475
+ super(...arguments);
476
+ /**
477
+ * Other plugins use this token for the authorization process. It handles token requesting and refreshing.
478
+ * Its value is `null` when {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl} is not provided.
479
+ *
480
+ * @readonly
481
+ */
482
+ this.token = null;
483
+ /**
484
+ * A map of token object instances keyed by the token URLs.
485
+ */
486
+ this._tokens = new Map();
487
+ }
488
+ /**
489
+ * @inheritDoc
490
+ */
491
+ static get pluginName() {
492
+ return 'CloudServices';
493
+ }
494
+ /**
495
+ * @inheritDoc
496
+ */
497
+ static get requires() {
498
+ return [CloudServicesCore];
499
+ }
500
+ /**
501
+ * @inheritDoc
502
+ */
503
+ async init() {
504
+ const config = this.context.config;
505
+ const options = config.get('cloudServices') || {};
506
+ for (const [key, value] of Object.entries(options)) {
507
+ this[key] = value;
508
+ }
509
+ if (!this.tokenUrl) {
510
+ this.token = null;
511
+ return;
512
+ }
513
+ const cloudServicesCore = this.context.plugins.get('CloudServicesCore');
514
+ this.token = await cloudServicesCore.createToken(this.tokenUrl).init();
515
+ this._tokens.set(this.tokenUrl, this.token);
516
+ }
517
+ /**
518
+ * Registers an additional authentication token URL for CKEditor Cloud Services or a callback to the token value promise. See the
519
+ * {@link module:cloud-services/cloudservicesconfig~CloudServicesConfig#tokenUrl} for more details.
520
+ *
521
+ * @param tokenUrl The authentication token URL for CKEditor Cloud Services or a callback to the token value promise.
522
+ */
523
+ async registerTokenUrl(tokenUrl) {
524
+ // Reuse the token instance in case of multiple features using the same token URL.
525
+ if (this._tokens.has(tokenUrl)) {
526
+ return this.getTokenFor(tokenUrl);
527
+ }
528
+ const cloudServicesCore = this.context.plugins.get('CloudServicesCore');
529
+ const token = await cloudServicesCore.createToken(tokenUrl).init();
530
+ this._tokens.set(tokenUrl, token);
531
+ return token;
532
+ }
533
+ /**
534
+ * Returns an authentication token provider previously registered by {@link #registerTokenUrl}.
535
+ *
536
+ * @param tokenUrl The authentication token URL for CKEditor Cloud Services or a callback to the token value promise.
537
+ */
538
+ getTokenFor(tokenUrl) {
539
+ const token = this._tokens.get(tokenUrl);
540
+ if (!token) {
541
+ /**
542
+ * The provided `tokenUrl` was not registered by {@link module:cloud-services/cloudservices~CloudServices#registerTokenUrl}.
543
+ *
544
+ * @error cloudservices-token-not-registered
545
+ */
546
+ throw new CKEditorError('cloudservices-token-not-registered', this);
547
+ }
548
+ return token;
549
+ }
550
+ /**
551
+ * @inheritDoc
552
+ */
553
+ destroy() {
554
+ super.destroy();
555
+ for (const token of this._tokens.values()) {
556
+ token.destroy();
557
+ }
558
+ }
559
+ }
560
+
561
+ export { CloudServices, CloudServicesCore };
562
+ //# sourceMappingURL=index.js.map